@tishlang/tish-lsp 2.12.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish-lsp +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -27,19 +27,49 @@ use tishlang_core::{
|
|
|
27
27
|
/// instead and is picked up at the next call site (or the top-level boundary).
|
|
28
28
|
const PENDING_THROW_SENTINEL: &str = "\u{1}__tish_pending_throw__";
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// #303 — the VM shares the one pending-throw slot defined in `tishlang_core`, so the array builtins
|
|
31
|
+
// it calls (`tishlang_builtins::array`) can poll the same slot and so native + VM agree. These thin
|
|
32
|
+
// wrappers keep every VM call site unchanged. `core::set_pending_throw` is first-throw-wins, but the
|
|
33
|
+
// VM only sets (one site) after draining at a try boundary, so behavior is unchanged.
|
|
35
34
|
fn set_pending_throw(v: Value) {
|
|
36
|
-
|
|
35
|
+
tishlang_core::set_pending_throw(v);
|
|
37
36
|
}
|
|
38
37
|
fn take_pending_throw() -> Option<Value> {
|
|
39
|
-
|
|
38
|
+
tishlang_core::take_pending_throw()
|
|
40
39
|
}
|
|
41
40
|
fn pending_throw_is_set() -> bool {
|
|
42
|
-
|
|
41
|
+
tishlang_core::has_pending_throw()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The recursion ceiling + trip error moved to `tishlang_core` (#381) so the native backend's
|
|
45
|
+
// boxed-call guard shares one implementation with the VM. Same semantics: thread-local ceiling
|
|
46
|
+
// lazily read from `TISH_MAX_CALL_DEPTH` (default 20000), `{name, message}` RangeError.
|
|
47
|
+
use tishlang_core::{max_call_depth, stack_overflow_error};
|
|
48
|
+
|
|
49
|
+
/// Headroom (bytes) a self-recursive JIT'd function leaves below the real stack bottom before it
|
|
50
|
+
/// bails (#381). Must cover the bail path plus building the `RangeError` back in the VM; 256 KiB is
|
|
51
|
+
/// comfortably more than either needs while still far larger than a single f64-ABI recursion frame.
|
|
52
|
+
const RECUR_STACK_MARGIN: usize = 256 * 1024;
|
|
53
|
+
|
|
54
|
+
/// OSR back-edge count at which a hot top-level loop is first offered to the region JIT (#190). High
|
|
55
|
+
/// enough that the compile + first-attempt cost is amortized over a genuinely hot loop.
|
|
56
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
57
|
+
const OSR_THRESHOLD: u32 = 10_000;
|
|
58
|
+
/// After the first attempt, retry OSR every this-many back-edges — only relevant to the live-in-miss
|
|
59
|
+
/// path (a numeric loop compiles and consumes itself on the first attempt). Keeps the re-check off the
|
|
60
|
+
/// per-iteration hot path.
|
|
61
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
62
|
+
const OSR_RETRY: u32 = 50_000;
|
|
63
|
+
|
|
64
|
+
/// Outcome of an OSR attempt (#190).
|
|
65
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
66
|
+
enum OsrResult {
|
|
67
|
+
/// Ran the loop natively; resume interpreting at this chunk `ip` (the loop exit).
|
|
68
|
+
Compiled(usize),
|
|
69
|
+
/// A live-in slot is non-numeric; keep interpreting (may become eligible later → retry).
|
|
70
|
+
LiveInMiss,
|
|
71
|
+
/// The region is not a pure-numeric slot loop; give up on this loop (negative-cached).
|
|
72
|
+
NotCompilable,
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
/// Append the source location of the instruction at `off` to a runtime-error message, e.g.
|
|
@@ -150,28 +180,65 @@ pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
|
150
180
|
fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str) -> Option<Value> {
|
|
151
181
|
#[cfg(feature = "fs")]
|
|
152
182
|
if spec == "tish:fs" && cap_allows(enabled, "fs") {
|
|
183
|
+
use tishlang_runtime::fs_ext as fx;
|
|
153
184
|
return match export_name {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
// tish names → the dual fs_ext functions (sync, or Node callback if a fn is passed)
|
|
186
|
+
"readFile" => Some(Value::native(|a: &[Value]| fx::read_file(a))),
|
|
187
|
+
"writeFile" => Some(Value::native(|a: &[Value]| fx::write_file(a))),
|
|
188
|
+
"fileExists" => Some(Value::native(|a: &[Value]| tishlang_runtime::file_exists(a))),
|
|
189
|
+
"isDir" => Some(Value::native(|a: &[Value]| tishlang_runtime::is_dir(a))),
|
|
190
|
+
"readDir" => Some(Value::native(|a: &[Value]| fx::readdir(a))),
|
|
191
|
+
"readFileBytes" => Some(Value::native(|a: &[Value]| fx::read_file_bytes(a))),
|
|
192
|
+
// Node-compatible names + new methods (sync) — issue #122
|
|
193
|
+
"readFileSync" => Some(Value::native(|a: &[Value]| fx::read_file(a))),
|
|
194
|
+
"readFileBytesSync" => Some(Value::native(|a: &[Value]| fx::read_file_bytes(a))),
|
|
195
|
+
"writeFileSync" => Some(Value::native(|a: &[Value]| fx::write_file(a))),
|
|
196
|
+
"appendFile" | "appendFileSync" => Some(Value::native(|a: &[Value]| fx::append_file(a))),
|
|
197
|
+
"existsSync" => Some(Value::native(|a: &[Value]| fx::exists(a))),
|
|
198
|
+
"mkdir" | "mkdirSync" => Some(Value::native(|a: &[Value]| fx::mkdir(a))),
|
|
199
|
+
"readdir" | "readdirSync" => Some(Value::native(|a: &[Value]| fx::readdir(a))),
|
|
200
|
+
"stat" | "statSync" => Some(Value::native(|a: &[Value]| fx::stat(a))),
|
|
201
|
+
"lstat" | "lstatSync" => Some(Value::native(|a: &[Value]| fx::lstat(a))),
|
|
202
|
+
"rm" | "rmSync" => Some(Value::native(|a: &[Value]| fx::rm(a))),
|
|
203
|
+
"rmdir" | "rmdirSync" => Some(Value::native(|a: &[Value]| fx::rmdir(a))),
|
|
204
|
+
"unlink" | "unlinkSync" => Some(Value::native(|a: &[Value]| fx::unlink(a))),
|
|
205
|
+
"rename" | "renameSync" => Some(Value::native(|a: &[Value]| fx::rename(a))),
|
|
206
|
+
"copyFile" | "copyFileSync" => Some(Value::native(|a: &[Value]| fx::copy_file(a))),
|
|
207
|
+
"realpath" | "realpathSync" => Some(Value::native(|a: &[Value]| fx::realpath(a))),
|
|
208
|
+
"readlink" | "readlinkSync" => Some(Value::native(|a: &[Value]| fx::readlink(a))),
|
|
209
|
+
"truncate" | "truncateSync" => Some(Value::native(|a: &[Value]| fx::truncate(a))),
|
|
210
|
+
"mkdtemp" | "mkdtempSync" => Some(Value::native(|a: &[Value]| fx::mkdtemp(a))),
|
|
211
|
+
"cp" | "cpSync" => Some(Value::native(|a: &[Value]| fx::cp(a))),
|
|
212
|
+
"access" | "accessSync" => Some(Value::native(|a: &[Value]| fx::access(a))),
|
|
213
|
+
"constants" => Some(fx::constants()),
|
|
214
|
+
_ => None,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
#[cfg(feature = "fs")]
|
|
218
|
+
if spec == "tish:fs/promises" && cap_allows(enabled, "fs") {
|
|
219
|
+
use tishlang_runtime::fs_ext as fx;
|
|
220
|
+
return match export_name {
|
|
221
|
+
"readFile" => Some(Value::native(|a: &[Value]| fx::read_file_promise(a))),
|
|
222
|
+
"readFileBytes" => Some(Value::native(|a: &[Value]| fx::read_file_bytes_promise(a))),
|
|
223
|
+
"writeFile" => Some(Value::native(|a: &[Value]| fx::write_file_promise(a))),
|
|
224
|
+
"appendFile" => Some(Value::native(|a: &[Value]| fx::append_file_promise(a))),
|
|
225
|
+
"mkdir" => Some(Value::native(|a: &[Value]| fx::mkdir_promise(a))),
|
|
226
|
+
"readdir" => Some(Value::native(|a: &[Value]| fx::readdir_promise(a))),
|
|
227
|
+
"stat" => Some(Value::native(|a: &[Value]| fx::stat_promise(a))),
|
|
228
|
+
"lstat" => Some(Value::native(|a: &[Value]| fx::lstat_promise(a))),
|
|
229
|
+
"rm" => Some(Value::native(|a: &[Value]| fx::rm_promise(a))),
|
|
230
|
+
"rmdir" => Some(Value::native(|a: &[Value]| fx::rmdir_promise(a))),
|
|
231
|
+
"unlink" => Some(Value::native(|a: &[Value]| fx::unlink_promise(a))),
|
|
232
|
+
"rename" => Some(Value::native(|a: &[Value]| fx::rename_promise(a))),
|
|
233
|
+
"copyFile" => Some(Value::native(|a: &[Value]| fx::copy_file_promise(a))),
|
|
234
|
+
"realpath" => Some(Value::native(|a: &[Value]| fx::realpath_promise(a))),
|
|
235
|
+
"readlink" => Some(Value::native(|a: &[Value]| fx::readlink_promise(a))),
|
|
236
|
+
"truncate" => Some(Value::native(|a: &[Value]| fx::truncate_promise(a))),
|
|
237
|
+
"mkdtemp" => Some(Value::native(|a: &[Value]| fx::mkdtemp_promise(a))),
|
|
238
|
+
"cp" => Some(Value::native(|a: &[Value]| fx::cp_promise(a))),
|
|
239
|
+
"access" => Some(Value::native(|a: &[Value]| fx::access_promise(a))),
|
|
240
|
+
"exists" => Some(Value::native(|a: &[Value]| fx::exists_promise(a))),
|
|
241
|
+
"constants" => Some(fx::constants()),
|
|
175
242
|
_ => None,
|
|
176
243
|
};
|
|
177
244
|
}
|
|
@@ -199,13 +266,17 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
199
266
|
Value::Function(_) => raw,
|
|
200
267
|
Value::Object(ref obj) => {
|
|
201
268
|
let obj_ref = obj.borrow();
|
|
202
|
-
if let Some(Value::Function(on_worker)) =
|
|
203
|
-
|
|
269
|
+
if let Some(Value::Function(on_worker)) = obj_ref
|
|
270
|
+
.strings
|
|
271
|
+
.get(&std::sync::Arc::from("onWorker"))
|
|
272
|
+
.cloned()
|
|
204
273
|
{
|
|
205
274
|
let args_for_init = [Value::Number(0.0)];
|
|
206
275
|
on_worker.call(&args_for_init)
|
|
207
|
-
} else if let Some(h) =
|
|
208
|
-
|
|
276
|
+
} else if let Some(h) = obj_ref
|
|
277
|
+
.strings
|
|
278
|
+
.get(&std::sync::Arc::from("handler"))
|
|
279
|
+
.cloned()
|
|
209
280
|
{
|
|
210
281
|
h
|
|
211
282
|
} else {
|
|
@@ -263,8 +334,14 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
263
334
|
"exec" => Some(Value::native(|args: &[Value]| {
|
|
264
335
|
tishlang_runtime::process_exec(args)
|
|
265
336
|
})),
|
|
337
|
+
"execFile" => Some(Value::native(|args: &[Value]| {
|
|
338
|
+
tishlang_runtime::process_exec_file(args)
|
|
339
|
+
})),
|
|
266
340
|
"argv" => Some(Value::Array(VmRef::new(
|
|
267
|
-
tishlang_core::process_argv()
|
|
341
|
+
tishlang_core::process_argv()
|
|
342
|
+
.into_iter()
|
|
343
|
+
.map(|s| Value::String(s.into()))
|
|
344
|
+
.collect(),
|
|
268
345
|
))),
|
|
269
346
|
"env" => Some(value_object_from_map(
|
|
270
347
|
std::env::vars()
|
|
@@ -285,10 +362,17 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
285
362
|
"exec".into(),
|
|
286
363
|
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
|
|
287
364
|
);
|
|
365
|
+
m.insert(
|
|
366
|
+
"execFile".into(),
|
|
367
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exec_file(args)),
|
|
368
|
+
);
|
|
288
369
|
m.insert(
|
|
289
370
|
"argv".into(),
|
|
290
371
|
Value::Array(VmRef::new(
|
|
291
|
-
tishlang_core::process_argv()
|
|
372
|
+
tishlang_core::process_argv()
|
|
373
|
+
.into_iter()
|
|
374
|
+
.map(|s| Value::String(s.into()))
|
|
375
|
+
.collect(),
|
|
292
376
|
)),
|
|
293
377
|
);
|
|
294
378
|
m.insert(
|
|
@@ -331,8 +415,12 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
331
415
|
#[cfg(feature = "tty")]
|
|
332
416
|
if spec == "tish:tty" && cap_allows(enabled, "tty") {
|
|
333
417
|
return match export_name {
|
|
334
|
-
"size" => Some(Value::native(|args: &[Value]|
|
|
335
|
-
|
|
418
|
+
"size" => Some(Value::native(|args: &[Value]| {
|
|
419
|
+
tishlang_runtime::tty_size(args)
|
|
420
|
+
})),
|
|
421
|
+
"isTTY" => Some(Value::native(|args: &[Value]| {
|
|
422
|
+
tishlang_runtime::tty_is_tty(args)
|
|
423
|
+
})),
|
|
336
424
|
"setRawMode" => Some(Value::native(|args: &[Value]| {
|
|
337
425
|
tishlang_runtime::tty_set_raw_mode(args)
|
|
338
426
|
})),
|
|
@@ -342,7 +430,9 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
342
430
|
"leaveAltScreen" => Some(Value::native(|args: &[Value]| {
|
|
343
431
|
tishlang_runtime::tty_leave_alt_screen(args)
|
|
344
432
|
})),
|
|
345
|
-
"read" => Some(Value::native(|args: &[Value]|
|
|
433
|
+
"read" => Some(Value::native(|args: &[Value]| {
|
|
434
|
+
tishlang_runtime::tty_read(args)
|
|
435
|
+
})),
|
|
346
436
|
"readLine" => Some(Value::native(|args: &[Value]| {
|
|
347
437
|
tishlang_runtime::tty_read_line(args)
|
|
348
438
|
})),
|
|
@@ -484,6 +574,13 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
484
574
|
"pow".into(),
|
|
485
575
|
Value::native(|args: &[Value]| math_builtins::pow(args)),
|
|
486
576
|
);
|
|
577
|
+
// `imul` delegates to the shared builtin (ToInt32 + wrapping i32 multiply) so the vm never
|
|
578
|
+
// diverges from interp/native (#247). Was previously absent from the vm `Math` object, so
|
|
579
|
+
// `Math.imul(...)` threw "Call of non-function" on the bytecode VM.
|
|
580
|
+
math.insert(
|
|
581
|
+
"imul".into(),
|
|
582
|
+
Value::native(|args: &[Value]| math_builtins::imul(args)),
|
|
583
|
+
);
|
|
487
584
|
math.insert(
|
|
488
585
|
"sin".into(),
|
|
489
586
|
Value::native(|args: &[Value]| math_builtins::sin(args)),
|
|
@@ -544,13 +641,12 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
544
641
|
Value::Number(n.acos())
|
|
545
642
|
}),
|
|
546
643
|
);
|
|
644
|
+
// hypot delegates to the shared builtin (variadic, NaN/Infinity-correct) so the vm never diverges
|
|
645
|
+
// from interp/native (#247). The old inline `filter_map(as_number)` dropped non-number args
|
|
646
|
+
// instead of propagating NaN.
|
|
547
647
|
math.insert(
|
|
548
648
|
"hypot".into(),
|
|
549
|
-
Value::native(|args: &[Value]|
|
|
550
|
-
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
|
|
551
|
-
let sum_sq: f64 = nums.iter().map(|n| n * n).sum();
|
|
552
|
-
Value::Number(sum_sq.sqrt())
|
|
553
|
-
}),
|
|
649
|
+
Value::native(|args: &[Value]| math_builtins::hypot(args)),
|
|
554
650
|
);
|
|
555
651
|
// Hyperbolic, inverse-hyperbolic, cbrt and base-2/10 logs. Like the trig block above
|
|
556
652
|
// these aren't in `math_builtins`, and on the wasm/native VM there is no host `Math`
|
|
@@ -575,6 +671,10 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
575
671
|
math_unary!("cbrt", cbrt);
|
|
576
672
|
math_unary!("log2", log2);
|
|
577
673
|
math_unary!("log10", log10);
|
|
674
|
+
math_unary!("expm1", exp_m1);
|
|
675
|
+
math_unary!("log1p", ln_1p);
|
|
676
|
+
math.insert("clz32".into(), Value::native(|a: &[Value]| math_builtins::clz32(a)));
|
|
677
|
+
math.insert("fround".into(), Value::native(|a: &[Value]| math_builtins::fround(a)));
|
|
578
678
|
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
|
|
579
679
|
math.insert("E".into(), Value::Number(std::f64::consts::E));
|
|
580
680
|
g.insert("Math".into(), value_object_from_map(math));
|
|
@@ -615,6 +715,18 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
615
715
|
"decodeURI".into(),
|
|
616
716
|
Value::native(|args: &[Value]| globals_builtins::decode_uri(args)),
|
|
617
717
|
);
|
|
718
|
+
g.insert(
|
|
719
|
+
"encodeURIComponent".into(),
|
|
720
|
+
Value::native(|args: &[Value]| globals_builtins::encode_uri_component(args)),
|
|
721
|
+
);
|
|
722
|
+
g.insert(
|
|
723
|
+
"decodeURIComponent".into(),
|
|
724
|
+
Value::native(|args: &[Value]| globals_builtins::decode_uri_component(args)),
|
|
725
|
+
);
|
|
726
|
+
g.insert(
|
|
727
|
+
"structuredClone".into(),
|
|
728
|
+
Value::native(|args: &[Value]| globals_builtins::structured_clone(args)),
|
|
729
|
+
);
|
|
618
730
|
g.insert(
|
|
619
731
|
"htmlEscape".into(),
|
|
620
732
|
Value::native(|args: &[Value]| {
|
|
@@ -642,10 +754,7 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
642
754
|
Value::String(v.type_name().into())
|
|
643
755
|
}),
|
|
644
756
|
);
|
|
645
|
-
g.insert(
|
|
646
|
-
"Symbol".into(),
|
|
647
|
-
tishlang_builtins::symbol::symbol_object(),
|
|
648
|
-
);
|
|
757
|
+
g.insert("Symbol".into(), tishlang_builtins::symbol::symbol_object());
|
|
649
758
|
|
|
650
759
|
// Date - full constructor (new Date(...)) plus statics now()/parse()/UTC().
|
|
651
760
|
g.insert(
|
|
@@ -666,14 +775,38 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
666
775
|
"Float64Array",
|
|
667
776
|
tishlang_builtins::typedarrays::float64_array_constructor_value as fn() -> Value,
|
|
668
777
|
),
|
|
669
|
-
(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
778
|
+
(
|
|
779
|
+
"Float32Array",
|
|
780
|
+
tishlang_builtins::typedarrays::float32_array_constructor_value,
|
|
781
|
+
),
|
|
782
|
+
(
|
|
783
|
+
"Int8Array",
|
|
784
|
+
tishlang_builtins::typedarrays::int8_array_constructor_value,
|
|
785
|
+
),
|
|
786
|
+
(
|
|
787
|
+
"Uint8Array",
|
|
788
|
+
tishlang_builtins::typedarrays::uint8_array_constructor_value,
|
|
789
|
+
),
|
|
790
|
+
(
|
|
791
|
+
"Uint8ClampedArray",
|
|
792
|
+
tishlang_builtins::typedarrays::uint8_clamped_array_constructor_value,
|
|
793
|
+
),
|
|
794
|
+
(
|
|
795
|
+
"Int16Array",
|
|
796
|
+
tishlang_builtins::typedarrays::int16_array_constructor_value,
|
|
797
|
+
),
|
|
798
|
+
(
|
|
799
|
+
"Uint16Array",
|
|
800
|
+
tishlang_builtins::typedarrays::uint16_array_constructor_value,
|
|
801
|
+
),
|
|
802
|
+
(
|
|
803
|
+
"Int32Array",
|
|
804
|
+
tishlang_builtins::typedarrays::int32_array_constructor_value,
|
|
805
|
+
),
|
|
806
|
+
(
|
|
807
|
+
"Uint32Array",
|
|
808
|
+
tishlang_builtins::typedarrays::uint32_array_constructor_value,
|
|
809
|
+
),
|
|
677
810
|
] {
|
|
678
811
|
g.insert(name.into(), ctor());
|
|
679
812
|
}
|
|
@@ -683,7 +816,10 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
683
816
|
);
|
|
684
817
|
// Error constructors (issue #60): `new Error(msg)` / `Error(msg)` → `{ name, message }`.
|
|
685
818
|
for name in ["Error", "TypeError", "RangeError", "SyntaxError"] {
|
|
686
|
-
g.insert(
|
|
819
|
+
g.insert(
|
|
820
|
+
name.into(),
|
|
821
|
+
construct_builtin::error_constructor_value(name),
|
|
822
|
+
);
|
|
687
823
|
}
|
|
688
824
|
|
|
689
825
|
// Object methods - delegate to tishlang_builtins::globals
|
|
@@ -692,6 +828,18 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
692
828
|
"assign".into(),
|
|
693
829
|
Value::native(|args: &[Value]| globals_builtins::object_assign(args)),
|
|
694
830
|
);
|
|
831
|
+
object_methods.insert(
|
|
832
|
+
"is".into(),
|
|
833
|
+
Value::native(|args: &[Value]| globals_builtins::object_is(args)),
|
|
834
|
+
);
|
|
835
|
+
object_methods.insert(
|
|
836
|
+
"hasOwn".into(),
|
|
837
|
+
Value::native(|args: &[Value]| globals_builtins::object_has_own(args)),
|
|
838
|
+
);
|
|
839
|
+
object_methods.insert(
|
|
840
|
+
"getOwnPropertyNames".into(),
|
|
841
|
+
Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
|
|
842
|
+
);
|
|
695
843
|
object_methods.insert(
|
|
696
844
|
"fromEntries".into(),
|
|
697
845
|
Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
|
|
@@ -717,6 +865,14 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
717
865
|
"isArray".into(),
|
|
718
866
|
Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
|
|
719
867
|
);
|
|
868
|
+
array_static.insert(
|
|
869
|
+
"of".into(),
|
|
870
|
+
Value::native(|args: &[Value]| globals_builtins::array_of(args)),
|
|
871
|
+
);
|
|
872
|
+
array_static.insert(
|
|
873
|
+
"from".into(),
|
|
874
|
+
Value::native(|args: &[Value]| globals_builtins::array_from(args)),
|
|
875
|
+
);
|
|
720
876
|
array_static.insert(
|
|
721
877
|
Arc::from("__call"),
|
|
722
878
|
Value::native(|args: &[Value]| construct_builtin::array_construct(args)),
|
|
@@ -733,12 +889,26 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
733
889
|
string_static.insert(Arc::from("__call"), string_convert_fn);
|
|
734
890
|
g.insert("String".into(), value_object_from_map(string_static));
|
|
735
891
|
|
|
736
|
-
// Number(value) coercion as a callable global (issue #36).
|
|
892
|
+
// Number(value) coercion as a callable global (issue #36) + the `Number.*` statics.
|
|
737
893
|
let mut number_static = ObjectMap::default();
|
|
738
894
|
number_static.insert(
|
|
739
895
|
Arc::from("__call"),
|
|
740
896
|
Value::native(|args: &[Value]| globals_builtins::number_convert(args)),
|
|
741
897
|
);
|
|
898
|
+
number_static.insert(Arc::from("isInteger"), Value::native(|a: &[Value]| globals_builtins::number_is_integer(a)));
|
|
899
|
+
number_static.insert(Arc::from("isSafeInteger"), Value::native(|a: &[Value]| globals_builtins::number_is_safe_integer(a)));
|
|
900
|
+
number_static.insert(Arc::from("isNaN"), Value::native(|a: &[Value]| globals_builtins::number_is_nan(a)));
|
|
901
|
+
number_static.insert(Arc::from("isFinite"), Value::native(|a: &[Value]| globals_builtins::number_is_finite(a)));
|
|
902
|
+
number_static.insert(Arc::from("parseInt"), Value::native(|a: &[Value]| globals_builtins::parse_int(a)));
|
|
903
|
+
number_static.insert(Arc::from("parseFloat"), Value::native(|a: &[Value]| globals_builtins::parse_float(a)));
|
|
904
|
+
number_static.insert(Arc::from("MAX_SAFE_INTEGER"), Value::Number(9_007_199_254_740_991.0));
|
|
905
|
+
number_static.insert(Arc::from("MIN_SAFE_INTEGER"), Value::Number(-9_007_199_254_740_991.0));
|
|
906
|
+
number_static.insert(Arc::from("EPSILON"), Value::Number(f64::EPSILON));
|
|
907
|
+
number_static.insert(Arc::from("MAX_VALUE"), Value::Number(f64::MAX));
|
|
908
|
+
number_static.insert(Arc::from("MIN_VALUE"), Value::Number(5e-324));
|
|
909
|
+
number_static.insert(Arc::from("POSITIVE_INFINITY"), Value::Number(f64::INFINITY));
|
|
910
|
+
number_static.insert(Arc::from("NEGATIVE_INFINITY"), Value::Number(f64::NEG_INFINITY));
|
|
911
|
+
number_static.insert(Arc::from("NaN"), Value::Number(f64::NAN));
|
|
742
912
|
g.insert("Number".into(), value_object_from_map(number_static));
|
|
743
913
|
|
|
744
914
|
// JSX / Lattish: stubs for bytecode VM when no DOM (e.g. console). Override via set_global in browser.
|
|
@@ -785,10 +955,17 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
785
955
|
"exec".into(),
|
|
786
956
|
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
|
|
787
957
|
);
|
|
958
|
+
process_obj.insert(
|
|
959
|
+
"execFile".into(),
|
|
960
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exec_file(args)),
|
|
961
|
+
);
|
|
788
962
|
process_obj.insert(
|
|
789
963
|
"argv".into(),
|
|
790
964
|
Value::Array(VmRef::new(
|
|
791
|
-
tishlang_core::process_argv()
|
|
965
|
+
tishlang_core::process_argv()
|
|
966
|
+
.into_iter()
|
|
967
|
+
.map(|s| Value::String(s.into()))
|
|
968
|
+
.collect(),
|
|
792
969
|
)),
|
|
793
970
|
);
|
|
794
971
|
process_obj.insert(
|
|
@@ -860,13 +1037,17 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
860
1037
|
Value::Function(_) => raw,
|
|
861
1038
|
Value::Object(ref obj) => {
|
|
862
1039
|
let obj_ref = obj.borrow();
|
|
863
|
-
if let Some(Value::Function(on_worker)) =
|
|
864
|
-
|
|
1040
|
+
if let Some(Value::Function(on_worker)) = obj_ref
|
|
1041
|
+
.strings
|
|
1042
|
+
.get(&std::sync::Arc::from("onWorker"))
|
|
1043
|
+
.cloned()
|
|
865
1044
|
{
|
|
866
1045
|
let args_for_init = [Value::Number(0.0)];
|
|
867
1046
|
on_worker.call(&args_for_init)
|
|
868
|
-
} else if let Some(h) =
|
|
869
|
-
|
|
1047
|
+
} else if let Some(h) = obj_ref
|
|
1048
|
+
.strings
|
|
1049
|
+
.get(&std::sync::Arc::from("handler"))
|
|
1050
|
+
.cloned()
|
|
870
1051
|
{
|
|
871
1052
|
h
|
|
872
1053
|
} else {
|
|
@@ -967,24 +1148,66 @@ fn try_call_array_jit(
|
|
|
967
1148
|
arity: usize,
|
|
968
1149
|
mask: u8,
|
|
969
1150
|
) -> Option<Value> {
|
|
1151
|
+
let writable = nf.array_writable_mask();
|
|
970
1152
|
let mut numeric: Vec<f64> = Vec::with_capacity(arity);
|
|
971
1153
|
// `scratch` OWNS the extracted f64 data; handles point into it. Build handles only AFTER scratch is
|
|
972
1154
|
// fully populated so its backing buffers never reallocate out from under a live pointer.
|
|
973
1155
|
let mut scratch: Vec<Vec<f64>> = Vec::new();
|
|
974
|
-
|
|
1156
|
+
let mut array_arg: Vec<usize> = Vec::new(); // the `args` index each scratch buffer came from
|
|
1157
|
+
let mut is_bool_arr: Vec<bool> = Vec::new(); // #187: parallel to `scratch` — was it a Bool array?
|
|
1158
|
+
// #187: track whether ANY arg is a (non-empty) Bool array and whether ANY is a Number array. If both
|
|
1159
|
+
// element types are present the JIT could launder a bool value read from one array into a number
|
|
1160
|
+
// array (or vice versa) — the flattened `f64` writeback would then re-box it wrong — so we bail
|
|
1161
|
+
// (below). queens (all-bool arrays) and spectral_norm (all-number) never mix, so they stay JIT'd.
|
|
1162
|
+
let mut any_bool_arr = false;
|
|
1163
|
+
let mut any_num_arr = false;
|
|
1164
|
+
#[allow(clippy::needless_range_loop)]
|
|
1165
|
+
// `i` drives bit-mask math (`mask >> i`), not just indexing
|
|
975
1166
|
for i in 0..arity {
|
|
976
1167
|
if (mask >> i) & 1 == 1 {
|
|
977
1168
|
match &args[i] {
|
|
978
1169
|
Value::Array(a) => {
|
|
1170
|
+
// #187: a writable array must not alias another array arg — the JIT reads/writes a
|
|
1171
|
+
// private scratch copy, so aliasing would make reads see stale data or a writeback
|
|
1172
|
+
// silently drop the other param's writes. Bail (→ interpret) on any such overlap.
|
|
1173
|
+
if writable != 0 {
|
|
1174
|
+
for &j in &array_arg {
|
|
1175
|
+
if let Value::Array(other) = &args[j] {
|
|
1176
|
+
if VmRef::ptr_eq(a, other)
|
|
1177
|
+
&& ((writable >> i) & 1 == 1 || (writable >> j) & 1 == 1)
|
|
1178
|
+
{
|
|
1179
|
+
return None;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// #187: accept an all-`Number` OR an all-`Bool` array (bool → `f64` 0/1, e.g.
|
|
1185
|
+
// queens' `cols`/`diag*`). A MIXED array is ambiguous for the typed writeback → bail.
|
|
979
1186
|
let b = a.borrow();
|
|
980
1187
|
let mut buf: Vec<f64> = Vec::with_capacity(b.len());
|
|
1188
|
+
let mut seen_bool = false;
|
|
1189
|
+
let mut seen_num = false;
|
|
981
1190
|
for el in b.iter() {
|
|
982
1191
|
match el {
|
|
983
|
-
Value::Number(n) =>
|
|
984
|
-
|
|
1192
|
+
Value::Number(n) => {
|
|
1193
|
+
seen_num = true;
|
|
1194
|
+
buf.push(*n);
|
|
1195
|
+
}
|
|
1196
|
+
Value::Bool(bl) => {
|
|
1197
|
+
seen_bool = true;
|
|
1198
|
+
buf.push(if *bl { 1.0 } else { 0.0 });
|
|
1199
|
+
}
|
|
1200
|
+
_ => return None, // non-numeric/non-bool element → interpreter
|
|
985
1201
|
}
|
|
986
1202
|
}
|
|
1203
|
+
if seen_bool && seen_num {
|
|
1204
|
+
return None; // mixed Bool/Number array → can't type the writeback
|
|
1205
|
+
}
|
|
1206
|
+
any_bool_arr |= seen_bool;
|
|
1207
|
+
any_num_arr |= seen_num;
|
|
987
1208
|
scratch.push(buf);
|
|
1209
|
+
array_arg.push(i);
|
|
1210
|
+
is_bool_arr.push(seen_bool);
|
|
988
1211
|
}
|
|
989
1212
|
_ => return None, // NumberArray / non-array → interpreter
|
|
990
1213
|
}
|
|
@@ -995,20 +1218,311 @@ fn try_call_array_jit(
|
|
|
995
1218
|
}
|
|
996
1219
|
}
|
|
997
1220
|
}
|
|
1221
|
+
// #187: mixed Bool + Number array args in one call could launder a value across element types (a bool
|
|
1222
|
+
// read from one array stored into a number array, or vice versa) — the flat `f64` writeback can't
|
|
1223
|
+
// re-box that correctly. Bail when both element types are present among the args.
|
|
1224
|
+
if any_bool_arr && any_num_arr {
|
|
1225
|
+
return None;
|
|
1226
|
+
}
|
|
1227
|
+
// #187: the writeback re-boxes a writable array by its ENTRY element type (bool vs number, per
|
|
1228
|
+
// `is_bool_arr`), so the value kind the JIT actually stored must match that type. A function that
|
|
1229
|
+
// writes bool consts (`arr[i] = true`) handed a NUMBER array — or one that writes numbers handed a
|
|
1230
|
+
// BOOL array — would re-box to the wrong type (e.g. `arr[i] = true` on `[0,0,0]` boxing `Number(1)`
|
|
1231
|
+
// where the interpreter stores `Bool(true)`). Bail to the interpreter on any such mismatch. (A single
|
|
1232
|
+
// array with mixed bool + non-bool writes was already rejected at compile time in `classify_params`.)
|
|
1233
|
+
let bool_write_mask = nf.array_bool_write_mask();
|
|
1234
|
+
if writable != 0 {
|
|
1235
|
+
for (k, &entry_bool) in is_bool_arr.iter().enumerate() {
|
|
1236
|
+
let i = array_arg[k];
|
|
1237
|
+
if (writable >> i) & 1 == 1 && entry_bool != ((bool_write_mask >> i) & 1 == 1) {
|
|
1238
|
+
return None;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
// `as_mut_ptr`: #187 writable params store through this pointer, so it must carry mut provenance
|
|
1243
|
+
// (read-only params only load — harmless). Handles are built only AFTER `scratch` is fully
|
|
1244
|
+
// populated so its backing buffers never reallocate out from under a live pointer.
|
|
998
1245
|
let handles: Vec<crate::jit::ArrayHandle> = scratch
|
|
999
|
-
.
|
|
1246
|
+
.iter_mut()
|
|
1000
1247
|
.map(|buf| crate::jit::ArrayHandle {
|
|
1001
|
-
ptr: buf.
|
|
1248
|
+
ptr: buf.as_mut_ptr(),
|
|
1002
1249
|
len: buf.len(),
|
|
1003
1250
|
})
|
|
1004
1251
|
.collect();
|
|
1005
|
-
|
|
1252
|
+
// #187: a self-recursive array-mode function (queens' `place`) recurses on the NATIVE stack, so
|
|
1253
|
+
// pass a RecurGuard (like #381) — its entry SP-bail turns unbounded recursion into a catchable
|
|
1254
|
+
// RangeError instead of a SIGSEGV. Non-recursive array fns pass a null guard (unchanged ABI).
|
|
1255
|
+
let (res, deopt, tripped) = if nf.recur_guarded() {
|
|
1256
|
+
let anchor = 0u8;
|
|
1257
|
+
let current_sp = &anchor as *const u8 as usize;
|
|
1258
|
+
let stack_limit = match stacker::remaining_stack() {
|
|
1259
|
+
Some(rem) => {
|
|
1260
|
+
let margin = RECUR_STACK_MARGIN.min(rem / 2);
|
|
1261
|
+
current_sp.saturating_sub(rem).saturating_add(margin)
|
|
1262
|
+
}
|
|
1263
|
+
None => 0,
|
|
1264
|
+
};
|
|
1265
|
+
let mut guard = crate::jit::RecurGuard {
|
|
1266
|
+
stack_limit,
|
|
1267
|
+
tripped: 0,
|
|
1268
|
+
};
|
|
1269
|
+
let (res, deopt) = nf.call_arrays_guarded(&numeric, &handles, &mut guard);
|
|
1270
|
+
(res, deopt, guard.tripped != 0)
|
|
1271
|
+
} else {
|
|
1272
|
+
let (res, deopt) = nf.call_arrays(&numeric, &handles);
|
|
1273
|
+
(res, deopt, false)
|
|
1274
|
+
};
|
|
1006
1275
|
if deopt {
|
|
1007
|
-
return None; // OOB access → re-run interpreter (
|
|
1276
|
+
return None; // OOB access → re-run interpreter (the discarded scratch writes never escaped)
|
|
1277
|
+
}
|
|
1278
|
+
// #187: copy each WRITTEN array param's (possibly-mutated) scratch back into its `Value::Array`. The
|
|
1279
|
+
// length is unchanged (`SetIndex` can't grow; an OOB write deopts above), so this is a same-length
|
|
1280
|
+
// element overwrite. On a normal return this reflects the final state; on a recursion-guard TRIP
|
|
1281
|
+
// (below) it reflects the partial in-place mutations made before the overflow — matching the
|
|
1282
|
+
// interpreter/node, which likewise leak whatever was mutated when a deep recursion throws.
|
|
1283
|
+
if writable != 0 {
|
|
1284
|
+
for (k, buf) in scratch.iter().enumerate() {
|
|
1285
|
+
let i = array_arg[k];
|
|
1286
|
+
if (writable >> i) & 1 == 1 {
|
|
1287
|
+
if let Value::Array(a) = &args[i] {
|
|
1288
|
+
let bool_arr = is_bool_arr[k]; // #187: re-box a bool array's elements as Bool
|
|
1289
|
+
let mut b = a.borrow_mut();
|
|
1290
|
+
for (j, &v) in buf.iter().enumerate() {
|
|
1291
|
+
if let Some(slot) = b.get_mut(j) {
|
|
1292
|
+
*slot = if bool_arr {
|
|
1293
|
+
Value::Bool(v != 0.0)
|
|
1294
|
+
} else {
|
|
1295
|
+
Value::Number(v)
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
// #187: a self-recursive array-mode fn that overflowed the native stack raises the same catchable
|
|
1304
|
+
// RangeError as every other tier (#381). The writeback above already flushed the partial mutations,
|
|
1305
|
+
// so the caller's arrays reflect the same "leaked" state the interpreter/node leave on a deep throw.
|
|
1306
|
+
if tripped {
|
|
1307
|
+
set_pending_throw(stack_overflow_error());
|
|
1308
|
+
return Some(Value::Null);
|
|
1309
|
+
}
|
|
1310
|
+
// #187: a VOID function (side-effect writer, e.g. `multiplyAv`) returns the implicit `null` — its
|
|
1311
|
+
// `f64` result is a dummy. Return `Value::Null` to match the interpreter; the real effect is the
|
|
1312
|
+
// array writeback above.
|
|
1313
|
+
if nf.returns_void() {
|
|
1314
|
+
return Some(Value::Null);
|
|
1008
1315
|
}
|
|
1009
1316
|
Some(Value::Number(res))
|
|
1010
1317
|
}
|
|
1011
1318
|
|
|
1319
|
+
/// #187 native HOF fusion. When `arr.map/filter/reduce/forEach(cb)` has a JIT-compiled pure-numeric
|
|
1320
|
+
/// callback (`cb` is a [`VmClosure`] with a plain register-`f64` [`crate::jit::NumericFn`]) AND the
|
|
1321
|
+
/// array is all-numeric, the whole higher-order call runs as a tight native loop calling the
|
|
1322
|
+
/// `NumericFn` per element — skipping the per-element boxed `Callable::call` dispatch AND the
|
|
1323
|
+
/// `Value` (un)boxing of each element that the generic `arr_builtins` path pays.
|
|
1324
|
+
///
|
|
1325
|
+
/// This is PURELY ADDITIVE: every helper returns `None` to fall back to the existing generic path
|
|
1326
|
+
/// (which is byte-identical to the interpreter), so a bail is always sound. The gate is deliberately
|
|
1327
|
+
/// narrow — see [`fused_numeric_fn`] and the per-helper arity/`result_bool` conditions.
|
|
1328
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1329
|
+
mod hof_fusion {
|
|
1330
|
+
use super::VmClosure;
|
|
1331
|
+
use tishlang_core::{Value, VmRef};
|
|
1332
|
+
|
|
1333
|
+
/// Extract the callback's fusable [`crate::jit::NumericFn`], if any. Fires ONLY for a `VmClosure`
|
|
1334
|
+
/// whose JIT is a plain pure-numeric register-`f64` function:
|
|
1335
|
+
/// * `array_param_mask() == 0` — not an array-mode ABI (that path reads `arr[i]`, needs handles);
|
|
1336
|
+
/// * `!is_jv()` — no function-local JIT arrays (those signal a deopt via a per-thread flag we'd
|
|
1337
|
+
/// have to poll — the generic path handles them correctly, so bail);
|
|
1338
|
+
/// * `!recur_guarded()` — not self-recursive (would need the `RecurGuard` trailing-ptr ABI; a
|
|
1339
|
+
/// lambda callback is never self-recursive, so this only ever excludes pathological input).
|
|
1340
|
+
///
|
|
1341
|
+
/// Any other callable (builtin native fn, non-JIT closure, array-mode/JV/recursive fn) → `None`.
|
|
1342
|
+
#[inline]
|
|
1343
|
+
pub(super) fn fused_numeric_fn(cb: &Value) -> Option<crate::jit::NumericFn> {
|
|
1344
|
+
let Value::Function(f) = cb else { return None };
|
|
1345
|
+
let vc = f.as_any().downcast_ref::<VmClosure>()?;
|
|
1346
|
+
let nf = vc.jit_fn?;
|
|
1347
|
+
if nf.array_param_mask() != 0 || nf.is_jv() || nf.recur_guarded() {
|
|
1348
|
+
return None;
|
|
1349
|
+
}
|
|
1350
|
+
Some(nf)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/// Snapshot an all-numeric array as a `Vec<f64>`. Accepts a packed [`Value::NumberArray`] and a
|
|
1354
|
+
/// boxed [`Value::Array`] whose every element is a [`Value::Number`]. Returns `None` for any other
|
|
1355
|
+
/// shape (mixed/boxed non-number element, a non-array) so the caller falls back to the generic
|
|
1356
|
+
/// path. Snapshots (clones) so — exactly like the generic `arr_builtins` path (#382) — a callback
|
|
1357
|
+
/// that re-enters the same array observes the pre-call contents and can never deadlock the borrow.
|
|
1358
|
+
#[inline]
|
|
1359
|
+
fn numeric_snapshot(arr: &Value) -> Option<Vec<f64>> {
|
|
1360
|
+
match arr {
|
|
1361
|
+
Value::NumberArray(a) => Some(a.borrow().clone()),
|
|
1362
|
+
Value::Array(a) => {
|
|
1363
|
+
let b = a.borrow();
|
|
1364
|
+
let mut out = Vec::with_capacity(b.len());
|
|
1365
|
+
for v in b.iter() {
|
|
1366
|
+
match v {
|
|
1367
|
+
Value::Number(n) => out.push(*n),
|
|
1368
|
+
_ => return None,
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
Some(out)
|
|
1372
|
+
}
|
|
1373
|
+
_ => None,
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/// Box a `NumericFn` f64 result the SAME way the interpreter/generic path does: as `Value::Bool`
|
|
1378
|
+
/// when the callback's result is a comparison (`result_is_bool`), else `Value::Number`. A bool
|
|
1379
|
+
/// `NumericFn` returns exactly `0.0`/`1.0`, so `r != 0.0` is exact.
|
|
1380
|
+
#[inline]
|
|
1381
|
+
fn box_result(nf: &crate::jit::NumericFn, r: f64) -> Value {
|
|
1382
|
+
if nf.result_is_bool() {
|
|
1383
|
+
Value::Bool(r != 0.0)
|
|
1384
|
+
} else {
|
|
1385
|
+
Value::Number(r)
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/// Truthiness of a `NumericFn` result under tish's rules — matches `Value::is_truthy` on the
|
|
1390
|
+
/// boxed form: a number is truthy iff `!= 0` and not NaN; a bool-result fn returns `0.0`/`1.0`
|
|
1391
|
+
/// (never NaN) so the same test is exact for it too.
|
|
1392
|
+
#[inline]
|
|
1393
|
+
fn result_truthy(r: f64) -> bool {
|
|
1394
|
+
r != 0.0 && !r.is_nan()
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/// Fused `map`. Fires when the callback is a fusable numeric fn of arity ≤ 2 (it may read the
|
|
1398
|
+
/// element and the index; a 3rd `array` param can't be a number, so arity 3+ bails). Returns the
|
|
1399
|
+
/// SAME `Value` variant the generic path would: a numeric-returning map over a `NumberArray` yields
|
|
1400
|
+
/// a packed `NumberArray` (empty → boxed empty `Array`, matching `packed_or_empty`); a
|
|
1401
|
+
/// bool-returning map, or any map over a boxed `Array`, yields a boxed `Value::Array`.
|
|
1402
|
+
pub(super) fn map(arr: &Value, cb: &Value) -> Option<Value> {
|
|
1403
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1404
|
+
if nf.arity() > 2 {
|
|
1405
|
+
return None; // 3rd param is the array arg — not a number; bail (sound).
|
|
1406
|
+
}
|
|
1407
|
+
let data = numeric_snapshot(arr)?;
|
|
1408
|
+
let arity = nf.arity();
|
|
1409
|
+
let packed_input = matches!(arr, Value::NumberArray(_));
|
|
1410
|
+
// A numeric-returning map over a packed input keeps its result packed (byte-identical to
|
|
1411
|
+
// `packed_or_empty`); every other case (bool result, or a boxed `Array` input) boxes.
|
|
1412
|
+
if !nf.result_is_bool() && packed_input {
|
|
1413
|
+
let mut out: Vec<f64> = Vec::with_capacity(data.len());
|
|
1414
|
+
let mut args = [0.0f64; 2];
|
|
1415
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1416
|
+
args[0] = n;
|
|
1417
|
+
args[1] = i as f64;
|
|
1418
|
+
out.push(nf.call(&args[..arity]));
|
|
1419
|
+
}
|
|
1420
|
+
// Empty → boxed empty `Array`, matching `packed_or_empty`.
|
|
1421
|
+
if out.is_empty() {
|
|
1422
|
+
return Some(Value::Array(VmRef::new(Vec::new())));
|
|
1423
|
+
}
|
|
1424
|
+
return Some(Value::number_array(out));
|
|
1425
|
+
}
|
|
1426
|
+
let mut out: Vec<Value> = Vec::with_capacity(data.len());
|
|
1427
|
+
let mut args = [0.0f64; 2];
|
|
1428
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1429
|
+
args[0] = n;
|
|
1430
|
+
args[1] = i as f64;
|
|
1431
|
+
out.push(box_result(&nf, nf.call(&args[..arity])));
|
|
1432
|
+
}
|
|
1433
|
+
Some(Value::Array(VmRef::new(out)))
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/// Fused `filter`. Callback is a predicate of arity ≤ 2. Keeps the ORIGINAL element (not the
|
|
1437
|
+
/// callback result) when the result is truthy — so the output is always a subset of the numeric
|
|
1438
|
+
/// input. Packed input → packed output (`packed_or_empty`); boxed `Array` input → boxed `Array`.
|
|
1439
|
+
pub(super) fn filter(arr: &Value, cb: &Value) -> Option<Value> {
|
|
1440
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1441
|
+
if nf.arity() > 2 {
|
|
1442
|
+
return None;
|
|
1443
|
+
}
|
|
1444
|
+
let data = numeric_snapshot(arr)?;
|
|
1445
|
+
let arity = nf.arity();
|
|
1446
|
+
let packed_input = matches!(arr, Value::NumberArray(_));
|
|
1447
|
+
let mut args = [0.0f64; 2];
|
|
1448
|
+
if packed_input {
|
|
1449
|
+
let mut out: Vec<f64> = Vec::new();
|
|
1450
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1451
|
+
args[0] = n;
|
|
1452
|
+
args[1] = i as f64;
|
|
1453
|
+
if result_truthy(nf.call(&args[..arity])) {
|
|
1454
|
+
out.push(n);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if out.is_empty() {
|
|
1458
|
+
return Some(Value::Array(VmRef::new(Vec::new())));
|
|
1459
|
+
}
|
|
1460
|
+
return Some(Value::number_array(out));
|
|
1461
|
+
}
|
|
1462
|
+
let mut out: Vec<Value> = Vec::new();
|
|
1463
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1464
|
+
args[0] = n;
|
|
1465
|
+
args[1] = i as f64;
|
|
1466
|
+
if result_truthy(nf.call(&args[..arity])) {
|
|
1467
|
+
out.push(Value::Number(n));
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
Some(Value::Array(VmRef::new(out)))
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/// Fused `reduce`. Callback is `(acc, element, index)` — arity ≤ 3 (a 4th `array` param bails).
|
|
1474
|
+
/// The accumulator stays an `f64` across the whole fold, so the callback result must NOT be a bool
|
|
1475
|
+
/// (`!result_is_bool()`): a bool acc fed back into the numeric fn would diverge from the interpreter
|
|
1476
|
+
/// (which would pass a `Value::Bool`, not a number). No-initial-value semantics match the generic
|
|
1477
|
+
/// path: absent init (`Value::Null`) with a non-empty array seeds `acc = data[0]` and scans from
|
|
1478
|
+
/// index 1; an empty array with no init throws (bail to the generic path, which raises it).
|
|
1479
|
+
pub(super) fn reduce(arr: &Value, cb: &Value, initial: Option<&Value>) -> Option<Value> {
|
|
1480
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1481
|
+
if nf.arity() > 3 || nf.result_is_bool() {
|
|
1482
|
+
return None;
|
|
1483
|
+
}
|
|
1484
|
+
let data = numeric_snapshot(arr)?;
|
|
1485
|
+
let arity = nf.arity();
|
|
1486
|
+
// Determine seed. Reduce with no initial value AND an empty array is a TypeError in JS — let
|
|
1487
|
+
// the generic path produce that exact throw rather than replicate it here.
|
|
1488
|
+
let (start, mut acc) = match initial {
|
|
1489
|
+
None if !data.is_empty() => (1usize, data[0]),
|
|
1490
|
+
None => return None, // empty + no init → generic path throws
|
|
1491
|
+
Some(Value::Number(n)) => (0usize, *n),
|
|
1492
|
+
Some(_) => return None, // non-numeric/explicit-null init → generic path (acc wouldn't stay f64)
|
|
1493
|
+
};
|
|
1494
|
+
let mut args = [0.0f64; 3];
|
|
1495
|
+
for (i, &x) in data.iter().enumerate().skip(start) {
|
|
1496
|
+
args[0] = acc;
|
|
1497
|
+
args[1] = x;
|
|
1498
|
+
args[2] = i as f64;
|
|
1499
|
+
acc = nf.call(&args[..arity]);
|
|
1500
|
+
}
|
|
1501
|
+
Some(Value::Number(acc))
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/// Fused `forEach`. Callback arity ≤ 2; result discarded; always returns `Value::Null`. Only worth
|
|
1505
|
+
/// fusing when the callback is side-effect-free numeric arithmetic — but a `NumericFn` is pure by
|
|
1506
|
+
/// construction (no calls/member/throw), so a fused `forEach` is observably a no-op EXCEPT for its
|
|
1507
|
+
/// (absent) side effects: it computes and discards. Kept for completeness / uniformity; it can
|
|
1508
|
+
/// never diverge because there is nothing observable to diverge on.
|
|
1509
|
+
pub(super) fn for_each(arr: &Value, cb: &Value) -> Option<Value> {
|
|
1510
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1511
|
+
if nf.arity() > 2 {
|
|
1512
|
+
return None;
|
|
1513
|
+
}
|
|
1514
|
+
let data = numeric_snapshot(arr)?;
|
|
1515
|
+
let arity = nf.arity();
|
|
1516
|
+
let mut args = [0.0f64; 2];
|
|
1517
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1518
|
+
args[0] = n;
|
|
1519
|
+
args[1] = i as f64;
|
|
1520
|
+
let _ = nf.call(&args[..arity]);
|
|
1521
|
+
}
|
|
1522
|
+
Some(Value::Null)
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1012
1526
|
struct VmClosure {
|
|
1013
1527
|
chunk: Arc<Chunk>,
|
|
1014
1528
|
/// Whether this closure can run on the frame stack — computed ONCE at creation (eligibility is an
|
|
@@ -1044,12 +1558,66 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1044
1558
|
}
|
|
1045
1559
|
}
|
|
1046
1560
|
if all_numbers {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1561
|
+
// #189: a JV function has function-local `f64` arrays lowered to
|
|
1562
|
+
// `tish_jv_*` calls over a per-thread arena. An out-of-bounds access (or a
|
|
1563
|
+
// non-numeric return) sets a per-thread deopt flag; we discard the numeric
|
|
1564
|
+
// result and re-run the interpreter. Sound because JV arrays never escape
|
|
1565
|
+
// the function — nothing observable was mutated, so re-execution
|
|
1566
|
+
// reproduces identical behaviour.
|
|
1567
|
+
if nf.is_jv() {
|
|
1568
|
+
crate::jit::jv_reset_deopt();
|
|
1569
|
+
let r = nf.call(&nums[..arity]);
|
|
1570
|
+
if !crate::jit::jv_take_deopt() {
|
|
1571
|
+
return if nf.result_is_bool() {
|
|
1572
|
+
Value::Bool(r != 0.0)
|
|
1573
|
+
} else {
|
|
1574
|
+
Value::Number(r)
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
// deopt ⇒ fall through to the interpreter below.
|
|
1050
1578
|
} else {
|
|
1051
|
-
|
|
1052
|
-
|
|
1579
|
+
// #381: a self-recursive JIT'd function carries a RecurGuard so it bails
|
|
1580
|
+
// (rather than overflowing the native stack) when the recursion nears the
|
|
1581
|
+
// real remaining stack. On a bail we raise the same catchable RangeError as
|
|
1582
|
+
// the non-JIT paths — tish's deopt tier producing the throw, not the JIT.
|
|
1583
|
+
let res = if nf.recur_guarded() {
|
|
1584
|
+
let anchor = 0u8;
|
|
1585
|
+
let current_sp = &anchor as *const u8 as usize;
|
|
1586
|
+
// `stack_limit` = the stack address below which we bail, leaving a
|
|
1587
|
+
// headroom margin. If the remaining stack is unknown we pass 0 (SP is
|
|
1588
|
+
// never below 0 → never trips), i.e. behave as before.
|
|
1589
|
+
let stack_limit = match stacker::remaining_stack() {
|
|
1590
|
+
Some(rem) => {
|
|
1591
|
+
// Cap the margin at half of what's left: when a JIT'd function
|
|
1592
|
+
// is first entered on an already-deep stack (rem < margin),
|
|
1593
|
+
// `bottom + margin` would sit ABOVE the current SP and trip on
|
|
1594
|
+
// the very first call — a false positive on shallow recursion.
|
|
1595
|
+
// Capping keeps the limit strictly below SP for any rem, while
|
|
1596
|
+
// still bailing with real headroom to spare.
|
|
1597
|
+
let margin = RECUR_STACK_MARGIN.min(rem / 2);
|
|
1598
|
+
current_sp.saturating_sub(rem).saturating_add(margin)
|
|
1599
|
+
}
|
|
1600
|
+
None => 0,
|
|
1601
|
+
};
|
|
1602
|
+
let mut guard = crate::jit::RecurGuard {
|
|
1603
|
+
stack_limit,
|
|
1604
|
+
tripped: 0,
|
|
1605
|
+
};
|
|
1606
|
+
let r = nf.call_guarded(&nums[..arity], &mut guard);
|
|
1607
|
+
if guard.tripped != 0 {
|
|
1608
|
+
set_pending_throw(stack_overflow_error());
|
|
1609
|
+
return Value::Null;
|
|
1610
|
+
}
|
|
1611
|
+
r
|
|
1612
|
+
} else {
|
|
1613
|
+
nf.call(&nums[..arity])
|
|
1614
|
+
};
|
|
1615
|
+
return if nf.result_is_bool() {
|
|
1616
|
+
Value::Bool(res != 0.0)
|
|
1617
|
+
} else {
|
|
1618
|
+
Value::Number(res)
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1053
1621
|
}
|
|
1054
1622
|
} else if let Some(v) = try_call_array_jit(&nf, args, arity, mask) {
|
|
1055
1623
|
// Array-mode path: succeeded (all-numeric arrays, in-bounds). On any bail
|
|
@@ -1060,6 +1628,16 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1060
1628
|
}
|
|
1061
1629
|
}
|
|
1062
1630
|
}
|
|
1631
|
+
// #381: bound recursion so a runaway recursive closure throws a catchable RangeError rather
|
|
1632
|
+
// than overflowing the native stack (this recursive re-entry is the DEFAULT `tish run` path).
|
|
1633
|
+
// The counter is thread-local because each closure call builds a fresh `Vm`; the parked throw
|
|
1634
|
+
// is picked up by the caller's `take_pending_throw()` check (Call/SelfCall post-call).
|
|
1635
|
+
let depth = tishlang_core::inc_call_depth();
|
|
1636
|
+
if depth > max_call_depth() {
|
|
1637
|
+
tishlang_core::dec_call_depth();
|
|
1638
|
+
set_pending_throw(stack_overflow_error());
|
|
1639
|
+
return Value::Null;
|
|
1640
|
+
}
|
|
1063
1641
|
let mut vm = Vm {
|
|
1064
1642
|
stack: Vec::new(),
|
|
1065
1643
|
scope: ObjectMap::default(),
|
|
@@ -1069,17 +1647,19 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1069
1647
|
native_modules: self.native_modules.clone(),
|
|
1070
1648
|
};
|
|
1071
1649
|
#[cfg(not(target_arch = "wasm32"))]
|
|
1072
|
-
{
|
|
1650
|
+
let result = {
|
|
1073
1651
|
stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
1074
1652
|
vm.run_chunk(self.chunk.as_ref(), &self.chunk.nested, args, false)
|
|
1075
1653
|
.unwrap_or(Value::Null)
|
|
1076
1654
|
})
|
|
1077
|
-
}
|
|
1655
|
+
};
|
|
1078
1656
|
#[cfg(target_arch = "wasm32")]
|
|
1079
|
-
{
|
|
1657
|
+
let result = {
|
|
1080
1658
|
vm.run_chunk(&self.chunk, &self.chunk.nested, args, false)
|
|
1081
1659
|
.unwrap_or(Value::Null)
|
|
1082
|
-
}
|
|
1660
|
+
};
|
|
1661
|
+
tishlang_core::dec_call_depth();
|
|
1662
|
+
result
|
|
1083
1663
|
}
|
|
1084
1664
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
1085
1665
|
self
|
|
@@ -1170,6 +1750,12 @@ impl Vm {
|
|
|
1170
1750
|
|
|
1171
1751
|
/// Run a chunk using this VM's capability set. `repl_mode` persists top-level `let` across REPL lines.
|
|
1172
1752
|
pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
|
|
1753
|
+
// #187: the directly-callable-callee registry is scoped to ONE top-level program run. Clear it
|
|
1754
|
+
// here (the outermost entry — closures run via `run_chunk`, not this) so a long-lived process
|
|
1755
|
+
// (REPL / embedder) running a NEW program never resolves a stale callee registered by a prior
|
|
1756
|
+
// one. A cross-caller and its callee always compile within the same run, so this is sufficient.
|
|
1757
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1758
|
+
crate::jit::reset_callees();
|
|
1173
1759
|
let result = self.run_chunk(chunk, &chunk.nested, &[], repl_mode);
|
|
1174
1760
|
// A throw that escaped every `catch` reaches here as the pending-throw sentinel; turn the
|
|
1175
1761
|
// parked value into the conventional uncaught-error message (issue #60).
|
|
@@ -1192,7 +1778,11 @@ impl Vm {
|
|
|
1192
1778
|
// regression to the DEFAULT path — caching makes the flag-off check a single atomic load.
|
|
1193
1779
|
use std::sync::OnceLock;
|
|
1194
1780
|
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
1195
|
-
*ENABLED.get_or_init(||
|
|
1781
|
+
*ENABLED.get_or_init(|| {
|
|
1782
|
+
std::env::var("TISH_FRAME_VM")
|
|
1783
|
+
.map(|v| v == "1")
|
|
1784
|
+
.unwrap_or(false)
|
|
1785
|
+
})
|
|
1196
1786
|
}
|
|
1197
1787
|
|
|
1198
1788
|
/// A `VmClosure` runs on the frame stack iff its chunk is frame-eligible AND it has no numeric
|
|
@@ -1245,6 +1835,64 @@ impl Vm {
|
|
|
1245
1835
|
true
|
|
1246
1836
|
}
|
|
1247
1837
|
|
|
1838
|
+
/// On-stack replacement (#190): run one hot loop region natively, or report why we can't.
|
|
1839
|
+
///
|
|
1840
|
+
/// Called from the frame VM's `JumpBack` handler once a loop's back-edge counter trips. Compiles
|
|
1841
|
+
/// (cached) the region `[header_ip, region_end)`; if it is a pure-numeric slot loop AND every live
|
|
1842
|
+
/// slot currently holds a `Number` (else the native f64 math would diverge from the interpreter),
|
|
1843
|
+
/// copies the live-ins into an f64 buffer, runs the whole remaining loop natively, writes the
|
|
1844
|
+
/// live-outs back as `Number`s, and returns the chunk `ip` of the loop exit to resume dispatch at.
|
|
1845
|
+
///
|
|
1846
|
+
/// Soundness: the v1 region whitelist is pure slot/stack arithmetic — no calls, no member/index,
|
|
1847
|
+
/// no object or array mutation — so nothing observable happens inside the region, and the
|
|
1848
|
+
/// interpreter running the same bytecode from the same numeric live-ins produces bit-identical
|
|
1849
|
+
/// slots (the `emit_simple_op` lowering matches `eval_binop`). A non-numeric live-in is the deopt
|
|
1850
|
+
/// case: we simply don't OSR and keep interpreting, so state is never corrupted.
|
|
1851
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1852
|
+
fn run_osr(
|
|
1853
|
+
chunk: &Chunk,
|
|
1854
|
+
header_ip: usize,
|
|
1855
|
+
region_end: usize,
|
|
1856
|
+
slots: &mut [Value],
|
|
1857
|
+
slot_base: usize,
|
|
1858
|
+
) -> OsrResult {
|
|
1859
|
+
let loopfn = match crate::jit::try_compile_loop(chunk, header_ip, region_end) {
|
|
1860
|
+
Some(lf) => lf,
|
|
1861
|
+
None => return OsrResult::NotCompilable,
|
|
1862
|
+
};
|
|
1863
|
+
let mut buf: Vec<f64> = Vec::with_capacity(loopfn.used_slots.len());
|
|
1864
|
+
for &slot in &loopfn.used_slots {
|
|
1865
|
+
match slots.get(slot_base + slot as usize) {
|
|
1866
|
+
Some(Value::Number(n)) => buf.push(*n),
|
|
1867
|
+
_ => return OsrResult::LiveInMiss, // a live slot is non-numeric → deopt, keep interpreting
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
let mut deopt: u8 = 0;
|
|
1871
|
+
// SAFETY: `buf` is a valid `[f64; used_slots.len()]`; `deopt` is a valid `*mut u8`. The region
|
|
1872
|
+
// was compiled for exactly this chunk+header (fingerprint-checked in the cache).
|
|
1873
|
+
let exit_id = loopfn.call(&mut buf, &mut deopt);
|
|
1874
|
+
for (p, &slot) in loopfn.used_slots.iter().enumerate() {
|
|
1875
|
+
if let Some(d) = slots.get_mut(slot_base + slot as usize) {
|
|
1876
|
+
*d = Value::Number(buf[p]);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
match loopfn.exits.get(exit_id as usize) {
|
|
1880
|
+
Some(&ip) => OsrResult::Compiled(ip),
|
|
1881
|
+
// Out-of-range exit id is impossible (the region returns an in-range id), but if it ever
|
|
1882
|
+
// happened the slots are already a consistent post-loop state, so re-interpreting the
|
|
1883
|
+
// (now-false) loop header exits correctly.
|
|
1884
|
+
None => OsrResult::LiveInMiss,
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/// OSR is **default ON**; `TISH_OSR=0` disables it (escape hatch, mirrors `TISH_JIT_ARRAYS`).
|
|
1889
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1890
|
+
#[inline]
|
|
1891
|
+
fn osr_enabled() -> bool {
|
|
1892
|
+
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
|
1893
|
+
*ENABLED.get_or_init(|| std::env::var("TISH_OSR").map(|v| v != "0").unwrap_or(true))
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1248
1896
|
/// Iterative frame-stack execution of a frame-eligible `VmClosure` (the frame-VM, flag-on).
|
|
1249
1897
|
/// Returns `None` if the entry chunk is ineligible (caller falls back to `VmClosure::call`).
|
|
1250
1898
|
/// Calls + recursion run on the heap `frames` stack — no per-call `Vm`, no recursive `run_chunk`
|
|
@@ -1264,6 +1912,14 @@ impl Vm {
|
|
|
1264
1912
|
let mut slots: Vec<Value> = Vec::new();
|
|
1265
1913
|
let mut slot_base: usize = 0;
|
|
1266
1914
|
slots.resize(cur.num_slots as usize, Value::Null);
|
|
1915
|
+
// #190 OSR: per-`(chunk, loop header)` back-edge counters (`u32::MAX` = gave up), with a
|
|
1916
|
+
// single-entry fast slot for the loop currently spinning (see the `run_chunk` copy for the
|
|
1917
|
+
// rationale) so a resolved hot loop stays off the HashMap.
|
|
1918
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1919
|
+
let mut osr_counters: std::collections::HashMap<(usize, usize), u32> =
|
|
1920
|
+
std::collections::HashMap::new();
|
|
1921
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1922
|
+
let mut osr_hot: ((usize, usize), u32) = ((usize::MAX, usize::MAX), 0);
|
|
1267
1923
|
for i in 0..(cur.param_count as usize) {
|
|
1268
1924
|
if let Some(v) = args.get(i) {
|
|
1269
1925
|
if let Some(d) = slots.get_mut(slot_base + i) {
|
|
@@ -1339,7 +1995,9 @@ impl Vm {
|
|
|
1339
1995
|
let idx = Self::read_u16(code, &mut ip) as usize;
|
|
1340
1996
|
let v = match cur.constants.get(idx) {
|
|
1341
1997
|
Some(Constant::Number(n)) => Value::Number(*n),
|
|
1342
|
-
Some(Constant::String(s)) =>
|
|
1998
|
+
Some(Constant::String(s)) => {
|
|
1999
|
+
Value::String(tishlang_core::ArcStr::from(s.as_ref()))
|
|
2000
|
+
}
|
|
1343
2001
|
Some(Constant::Bool(b)) => Value::Bool(*b),
|
|
1344
2002
|
Some(Constant::Null) => Value::Null,
|
|
1345
2003
|
_ => ferr!("Ineligible constant {} in run_framed", idx),
|
|
@@ -1388,6 +2046,41 @@ impl Vm {
|
|
|
1388
2046
|
}
|
|
1389
2047
|
Opcode::JumpBack => {
|
|
1390
2048
|
let dist = Self::read_u16(code, &mut ip) as usize;
|
|
2049
|
+
// #190 OSR: `ip` now points just past the JumpBack (region end); the loop header is
|
|
2050
|
+
// `region_end - dist`. Once a loop is hot, try to run its remaining iterations in
|
|
2051
|
+
// native code (numeric slot loops only; anything else falls straight through).
|
|
2052
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2053
|
+
if Self::osr_enabled() {
|
|
2054
|
+
let region_end = ip;
|
|
2055
|
+
let header_ip = ip.saturating_sub(dist);
|
|
2056
|
+
let key = (Arc::as_ptr(&cur) as usize, header_ip);
|
|
2057
|
+
if key != osr_hot.0 {
|
|
2058
|
+
if osr_hot.0 .0 != usize::MAX {
|
|
2059
|
+
osr_counters.insert(osr_hot.0, osr_hot.1);
|
|
2060
|
+
}
|
|
2061
|
+
osr_hot = (key, osr_counters.get(&key).copied().unwrap_or(0));
|
|
2062
|
+
}
|
|
2063
|
+
if osr_hot.1 != u32::MAX {
|
|
2064
|
+
osr_hot.1 = osr_hot.1.saturating_add(1);
|
|
2065
|
+
if osr_hot.1 >= OSR_THRESHOLD
|
|
2066
|
+
&& osr_hot
|
|
2067
|
+
.1
|
|
2068
|
+
.saturating_sub(OSR_THRESHOLD)
|
|
2069
|
+
.is_multiple_of(OSR_RETRY)
|
|
2070
|
+
{
|
|
2071
|
+
match Self::run_osr(
|
|
2072
|
+
&cur, header_ip, region_end, &mut slots, slot_base,
|
|
2073
|
+
) {
|
|
2074
|
+
OsrResult::Compiled(exit_ip) => {
|
|
2075
|
+
ip = exit_ip;
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
OsrResult::LiveInMiss => {}
|
|
2079
|
+
OsrResult::NotCompilable => osr_hot.1 = u32::MAX,
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
1391
2084
|
ip = ip.saturating_sub(dist);
|
|
1392
2085
|
}
|
|
1393
2086
|
Opcode::Pop => {
|
|
@@ -1400,6 +2093,12 @@ impl Vm {
|
|
|
1400
2093
|
call_args.push(fpop!());
|
|
1401
2094
|
}
|
|
1402
2095
|
call_args.reverse();
|
|
2096
|
+
// #381: bound the heap frame stack (the opt-in frame-VM path grows a Vec instead
|
|
2097
|
+
// of the native stack, so unbounded recursion here is OOM rather than overflow).
|
|
2098
|
+
if frames.len() >= max_call_depth() {
|
|
2099
|
+
set_pending_throw(stack_overflow_error());
|
|
2100
|
+
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
2101
|
+
}
|
|
1403
2102
|
frames.push((cur.clone(), ip, slot_base, stack_base, enclosing.clone()));
|
|
1404
2103
|
let new_base = slots.len();
|
|
1405
2104
|
slots.resize(new_base + cur.num_slots as usize, Value::Null);
|
|
@@ -1435,6 +2134,10 @@ impl Vm {
|
|
|
1435
2134
|
// refcounts are unchanged (the chunk heap data doesn't move, so the
|
|
1436
2135
|
// laundered `code` ptr stays valid until rebind below). Halves the
|
|
1437
2136
|
// per-call Arc traffic vs cloning for the push.
|
|
2137
|
+
if frames.len() >= max_call_depth() {
|
|
2138
|
+
set_pending_throw(stack_overflow_error());
|
|
2139
|
+
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
2140
|
+
}
|
|
1438
2141
|
frames.push((cur, ip, slot_base, stack_base, enclosing));
|
|
1439
2142
|
cur = next_chunk;
|
|
1440
2143
|
enclosing = next_enc;
|
|
@@ -1528,6 +2231,35 @@ impl Vm {
|
|
|
1528
2231
|
// frame indexed by slot — no per-call hashmap, no name lookups. Args bind
|
|
1529
2232
|
// to slots 0..param_count. Empty for name-based chunks.
|
|
1530
2233
|
let mut slot_locals: Vec<Value> = Vec::new();
|
|
2234
|
+
// #190 OSR: per-loop-header back-edge counters for this frame (`u32::MAX` = gave up). `osr_hot`
|
|
2235
|
+
// is a single-entry fast slot for the loop currently spinning — its header is constant across
|
|
2236
|
+
// iterations, so the common hot loop never touches the HashMap (which is hit only when the
|
|
2237
|
+
// active loop changes: entry, exit, or a nested-loop switch). Keeps the per-back-edge cost of a
|
|
2238
|
+
// resolved loop to two integer compares.
|
|
2239
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2240
|
+
let mut osr_counters: std::collections::HashMap<usize, u32> =
|
|
2241
|
+
std::collections::HashMap::new();
|
|
2242
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2243
|
+
let mut osr_hot: (usize, u32) = (usize::MAX, 0);
|
|
2244
|
+
// Frame-local string builder for `acc += x` on a slot local (Opcode::AppendLocal): keeps the
|
|
2245
|
+
// accumulator in a growable `sb_buf` so appends are amortized O(1) instead of reallocating the
|
|
2246
|
+
// whole string each time. `sb_slot` is the slot currently buffered (at most one). The slot's
|
|
2247
|
+
// own value is stale while buffered; `sb_flush!()` writes the buffer back. Slots are
|
|
2248
|
+
// frame-private (never captured by closures and invisible to callees), and every read of a
|
|
2249
|
+
// slot goes through `LoadLocal`, so flushing there is sufficient for soundness — no
|
|
2250
|
+
// cross-frame state. JS string immutability is preserved: reads always see the full string.
|
|
2251
|
+
let mut sb_slot: Option<usize> = None;
|
|
2252
|
+
let mut sb_buf = String::new();
|
|
2253
|
+
macro_rules! sb_flush {
|
|
2254
|
+
() => {{
|
|
2255
|
+
if let Some(s) = sb_slot.take() {
|
|
2256
|
+
if let Some(dst) = slot_locals.get_mut(s) {
|
|
2257
|
+
*dst = Value::String(std::mem::take(&mut sb_buf).into());
|
|
2258
|
+
}
|
|
2259
|
+
sb_buf.clear();
|
|
2260
|
+
}
|
|
2261
|
+
}};
|
|
2262
|
+
}
|
|
1531
2263
|
if chunk.slot_based {
|
|
1532
2264
|
slot_locals = vec![Value::Null; chunk.num_slots as usize];
|
|
1533
2265
|
let param_count = chunk.param_count as usize;
|
|
@@ -1620,6 +2352,10 @@ impl Vm {
|
|
|
1620
2352
|
Opcode::Nop => {}
|
|
1621
2353
|
Opcode::LoadLocal => {
|
|
1622
2354
|
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
2355
|
+
// Flush a pending string-builder for this slot so the read sees the full string.
|
|
2356
|
+
if sb_slot == Some(slot) {
|
|
2357
|
+
sb_flush!();
|
|
2358
|
+
}
|
|
1623
2359
|
let v = slot_locals
|
|
1624
2360
|
.get(slot)
|
|
1625
2361
|
.cloned()
|
|
@@ -1628,6 +2364,11 @@ impl Vm {
|
|
|
1628
2364
|
}
|
|
1629
2365
|
Opcode::StoreLocal => {
|
|
1630
2366
|
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
2367
|
+
// A plain store overwrites the slot — drop any builder buffering it.
|
|
2368
|
+
if sb_slot == Some(slot) {
|
|
2369
|
+
sb_slot = None;
|
|
2370
|
+
sb_buf.clear();
|
|
2371
|
+
}
|
|
1631
2372
|
let v = self
|
|
1632
2373
|
.stack
|
|
1633
2374
|
.pop()
|
|
@@ -1637,10 +2378,59 @@ impl Vm {
|
|
|
1637
2378
|
None => return Err(format!("Local slot out of bounds: {}", slot)),
|
|
1638
2379
|
}
|
|
1639
2380
|
}
|
|
2381
|
+
Opcode::AppendLocal => {
|
|
2382
|
+
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
2383
|
+
let rhs = self
|
|
2384
|
+
.stack
|
|
2385
|
+
.pop()
|
|
2386
|
+
.ok_or_else(|| "Stack underflow in AppendLocal".to_string())?;
|
|
2387
|
+
if sb_slot == Some(slot) {
|
|
2388
|
+
// Continue the active builder for this slot — amortized O(1) append.
|
|
2389
|
+
append_value_for_string_concat(&mut sb_buf, &rhs);
|
|
2390
|
+
} else {
|
|
2391
|
+
// Switching slots: flush the previous builder, then (re)start for this one.
|
|
2392
|
+
sb_flush!();
|
|
2393
|
+
match slot_locals.get(slot) {
|
|
2394
|
+
Some(Value::String(a)) => {
|
|
2395
|
+
sb_buf.clear();
|
|
2396
|
+
sb_buf.reserve(a.len() + estimate_string_concat_len(&rhs));
|
|
2397
|
+
sb_buf.push_str(a);
|
|
2398
|
+
append_value_for_string_concat(&mut sb_buf, &rhs);
|
|
2399
|
+
sb_slot = Some(slot);
|
|
2400
|
+
}
|
|
2401
|
+
_ => {
|
|
2402
|
+
// Non-string (or absent) accumulator: generic `+=` in place,
|
|
2403
|
+
// identical to LoadLocal; BinOp Add; StoreLocal.
|
|
2404
|
+
let current = slot_locals.get(slot).cloned().unwrap_or(Value::Null);
|
|
2405
|
+
let result = eval_binop(BinOp::Add, ¤t, &rhs)?;
|
|
2406
|
+
match slot_locals.get_mut(slot) {
|
|
2407
|
+
Some(dst) => *dst = result,
|
|
2408
|
+
None => {
|
|
2409
|
+
return Err(format!("Local slot out of bounds: {}", slot))
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
1640
2416
|
Opcode::LoadUpvalue | Opcode::StoreUpvalue => {
|
|
1641
2417
|
// Reserved for the linked-frame upvalue model (not emitted yet).
|
|
1642
2418
|
return Err("Upvalue opcodes not supported in this VM build".to_string());
|
|
1643
2419
|
}
|
|
2420
|
+
Opcode::MathUnary => {
|
|
2421
|
+
// #186 — `Math.<fn>(x)` on a number. The compiler only emits this when `Math` is
|
|
2422
|
+
// the global builtin, so it is behaviour-identical to the general call: a number
|
|
2423
|
+
// maps through `MathUnaryFn::apply`, a non-number coerces to NaN (matching the
|
|
2424
|
+
// builtin's `extract_num(..).unwrap_or(NaN)`).
|
|
2425
|
+
let id = Self::read_u16(code, &mut ip);
|
|
2426
|
+
let x = match self.stack.pop() {
|
|
2427
|
+
Some(Value::Number(n)) => n,
|
|
2428
|
+
Some(_) | None => f64::NAN,
|
|
2429
|
+
};
|
|
2430
|
+
let mfn = tishlang_bytecode::MathUnaryFn::from_u16(id)
|
|
2431
|
+
.ok_or_else(|| format!("Bad MathUnary id: {}", id))?;
|
|
2432
|
+
self.stack.push(Value::Number(mfn.apply(x)));
|
|
2433
|
+
}
|
|
1644
2434
|
Opcode::LoadConst => {
|
|
1645
2435
|
let idx = Self::read_u16(code, &mut ip);
|
|
1646
2436
|
let c = constants
|
|
@@ -1648,7 +2438,9 @@ impl Vm {
|
|
|
1648
2438
|
.ok_or_else(|| format!("Constant index out of bounds: {}", idx))?;
|
|
1649
2439
|
let v = match c {
|
|
1650
2440
|
Constant::Number(n) => Value::Number(*n),
|
|
1651
|
-
Constant::String(s) =>
|
|
2441
|
+
Constant::String(s) => {
|
|
2442
|
+
Value::String(tishlang_core::ArcStr::from(s.as_ref()))
|
|
2443
|
+
}
|
|
1652
2444
|
Constant::Bool(b) => Value::Bool(*b),
|
|
1653
2445
|
Constant::Null => Value::Null,
|
|
1654
2446
|
Constant::Closure(nested_idx) => {
|
|
@@ -1669,45 +2461,45 @@ impl Vm {
|
|
|
1669
2461
|
// A closure must capture a real scope (even if empty) so that, post-creation,
|
|
1670
2462
|
// the parent's name-based locals are visible. Materialise local_scope here.
|
|
1671
2463
|
let captured_scope: ScopeMap = ls_get_or_init!().clone();
|
|
1672
|
-
let enclosing_chain: SharedChain =
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2464
|
+
let enclosing_chain: SharedChain =
|
|
2465
|
+
SharedChain::new(if active_loop_vars.is_empty() {
|
|
2466
|
+
let mut chain = Vec::with_capacity(self.enclosing.len() + 1);
|
|
2467
|
+
chain.push(captured_scope.clone());
|
|
2468
|
+
chain.extend(self.enclosing.iter().cloned());
|
|
2469
|
+
chain
|
|
2470
|
+
} else {
|
|
2471
|
+
// Per-iteration `let`: freeze the loop var(s) into an overlay that
|
|
2472
|
+
// shadows the still-shared frame scope, then the inherited chain.
|
|
2473
|
+
let mut overlay = ObjectMap::default();
|
|
2474
|
+
{
|
|
2475
|
+
let ls = captured_scope.borrow();
|
|
2476
|
+
for n in &active_loop_vars {
|
|
2477
|
+
if let Some(v) = ls.get(n.as_ref()) {
|
|
2478
|
+
overlay.insert(Arc::clone(n), v.clone());
|
|
2479
|
+
}
|
|
1686
2480
|
}
|
|
1687
2481
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
});
|
|
2482
|
+
let mut chain = Vec::with_capacity(self.enclosing.len() + 2);
|
|
2483
|
+
chain.push(VmRef::new(overlay));
|
|
2484
|
+
chain.push(captured_scope.clone());
|
|
2485
|
+
chain.extend(self.enclosing.iter().cloned());
|
|
2486
|
+
chain
|
|
2487
|
+
});
|
|
1695
2488
|
let capabilities = Arc::clone(&self.capabilities);
|
|
1696
2489
|
let native_modules = self.native_modules.clone();
|
|
1697
2490
|
// Frame-eligibility is an O(chunk) bytecode scan; gate it behind the
|
|
1698
2491
|
// (cached) frame-VM flag so the DEFAULT path skips it entirely — flag-off
|
|
1699
2492
|
// closure creation pays nothing.
|
|
1700
|
-
let frameable = Vm::frame_vm_enabled()
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
};
|
|
2493
|
+
let frameable = Vm::frame_vm_enabled() && {
|
|
2494
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2495
|
+
{
|
|
2496
|
+
jit_fn.is_none() && Vm::chunk_frame_eligible(&inner_clone)
|
|
2497
|
+
}
|
|
2498
|
+
#[cfg(target_arch = "wasm32")]
|
|
2499
|
+
{
|
|
2500
|
+
Vm::chunk_frame_eligible(&inner_clone)
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
1711
2503
|
let vmclosure = VmClosure {
|
|
1712
2504
|
chunk: std::sync::Arc::new(inner_clone),
|
|
1713
2505
|
frameable,
|
|
@@ -1759,7 +2551,10 @@ impl Vm {
|
|
|
1759
2551
|
.pop()
|
|
1760
2552
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1761
2553
|
// Update innermost scope that has the variable (matches interpreter Scope.assign)
|
|
1762
|
-
if local_scope
|
|
2554
|
+
if local_scope
|
|
2555
|
+
.as_ref()
|
|
2556
|
+
.is_some_and(|ls| ls.borrow().contains_key(name.as_ref()))
|
|
2557
|
+
{
|
|
1763
2558
|
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1764
2559
|
} else if let Some(e) = self
|
|
1765
2560
|
.enclosing
|
|
@@ -1994,6 +2789,14 @@ impl Vm {
|
|
|
1994
2789
|
);
|
|
1995
2790
|
}
|
|
1996
2791
|
args.reverse();
|
|
2792
|
+
// #381: SelfCall is a second native recursive re-entry (a self-recursive function
|
|
2793
|
+
// re-enters run_chunk directly). Bound it with the same shared counter; `raise!` is
|
|
2794
|
+
// in scope here, so throw the catchable RangeError directly.
|
|
2795
|
+
let depth = tishlang_core::inc_call_depth();
|
|
2796
|
+
if depth > max_call_depth() {
|
|
2797
|
+
tishlang_core::dec_call_depth();
|
|
2798
|
+
raise!(stack_overflow_error());
|
|
2799
|
+
}
|
|
1997
2800
|
let mut vm = Vm {
|
|
1998
2801
|
stack: Vec::new(),
|
|
1999
2802
|
scope: ObjectMap::default(),
|
|
@@ -2008,7 +2811,10 @@ impl Vm {
|
|
|
2008
2811
|
.unwrap_or(Value::Null)
|
|
2009
2812
|
});
|
|
2010
2813
|
#[cfg(target_arch = "wasm32")]
|
|
2011
|
-
let result = vm
|
|
2814
|
+
let result = vm
|
|
2815
|
+
.run_chunk(chunk, nested, &args, false)
|
|
2816
|
+
.unwrap_or(Value::Null);
|
|
2817
|
+
tishlang_core::dec_call_depth();
|
|
2012
2818
|
if let Some(v) = take_pending_throw() {
|
|
2013
2819
|
raise!(v);
|
|
2014
2820
|
}
|
|
@@ -2040,8 +2846,7 @@ impl Vm {
|
|
|
2040
2846
|
let f = match &callee {
|
|
2041
2847
|
Value::Function(f) => f.clone(),
|
|
2042
2848
|
Value::Object(o) => {
|
|
2043
|
-
if let Some(Value::Function(call_fn)) =
|
|
2044
|
-
o.borrow().strings.get("__call")
|
|
2849
|
+
if let Some(Value::Function(call_fn)) = o.borrow().strings.get("__call")
|
|
2045
2850
|
{
|
|
2046
2851
|
call_fn.clone()
|
|
2047
2852
|
} else {
|
|
@@ -2131,6 +2936,49 @@ impl Vm {
|
|
|
2131
2936
|
}
|
|
2132
2937
|
Opcode::JumpBack => {
|
|
2133
2938
|
let dist = Self::read_u16(code, &mut ip) as usize;
|
|
2939
|
+
// #190 OSR: on a hot back-edge, try to finish the loop natively. Slot-based frames
|
|
2940
|
+
// only (`slot_locals` is the live frame); non-slot / non-numeric loops fail the
|
|
2941
|
+
// whitelist or the live-in check and fall straight through to the interpreter.
|
|
2942
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2943
|
+
if chunk.slot_based && Self::osr_enabled() {
|
|
2944
|
+
let region_end = ip;
|
|
2945
|
+
let header_ip = ip.saturating_sub(dist);
|
|
2946
|
+
// Switch the fast slot only when the active loop changes: stash the old count,
|
|
2947
|
+
// load this header's (default 0). The hot loop keeps `header_ip == osr_hot.0`.
|
|
2948
|
+
if header_ip != osr_hot.0 {
|
|
2949
|
+
if osr_hot.0 != usize::MAX {
|
|
2950
|
+
osr_counters.insert(osr_hot.0, osr_hot.1);
|
|
2951
|
+
}
|
|
2952
|
+
osr_hot = (
|
|
2953
|
+
header_ip,
|
|
2954
|
+
osr_counters.get(&header_ip).copied().unwrap_or(0),
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
if osr_hot.1 != u32::MAX {
|
|
2958
|
+
osr_hot.1 = osr_hot.1.saturating_add(1);
|
|
2959
|
+
if osr_hot.1 >= OSR_THRESHOLD
|
|
2960
|
+
&& osr_hot
|
|
2961
|
+
.1
|
|
2962
|
+
.saturating_sub(OSR_THRESHOLD)
|
|
2963
|
+
.is_multiple_of(OSR_RETRY)
|
|
2964
|
+
{
|
|
2965
|
+
match Self::run_osr(
|
|
2966
|
+
chunk,
|
|
2967
|
+
header_ip,
|
|
2968
|
+
region_end,
|
|
2969
|
+
&mut slot_locals,
|
|
2970
|
+
0,
|
|
2971
|
+
) {
|
|
2972
|
+
OsrResult::Compiled(exit_ip) => {
|
|
2973
|
+
ip = exit_ip;
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
OsrResult::LiveInMiss => {}
|
|
2977
|
+
OsrResult::NotCompilable => osr_hot.1 = u32::MAX,
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2134
2982
|
ip = ip.saturating_sub(dist);
|
|
2135
2983
|
}
|
|
2136
2984
|
Opcode::BinOp => {
|
|
@@ -2265,8 +3113,12 @@ impl Vm {
|
|
|
2265
3113
|
if let Some(nums) = elems.iter().try_fold(
|
|
2266
3114
|
Vec::<f64>::with_capacity(elems.len()),
|
|
2267
3115
|
|mut acc, v| {
|
|
2268
|
-
if let Value::Number(n) = v {
|
|
2269
|
-
|
|
3116
|
+
if let Value::Number(n) = v {
|
|
3117
|
+
acc.push(*n);
|
|
3118
|
+
Some(acc)
|
|
3119
|
+
} else {
|
|
3120
|
+
None
|
|
3121
|
+
}
|
|
2270
3122
|
},
|
|
2271
3123
|
) {
|
|
2272
3124
|
self.stack.push(Value::number_array(nums));
|
|
@@ -2289,10 +3141,8 @@ impl Vm {
|
|
|
2289
3141
|
let base = self.stack.len() - 2 * n;
|
|
2290
3142
|
let mut map = PropMap::with_capacity(n);
|
|
2291
3143
|
for i in 0..n {
|
|
2292
|
-
let key_val =
|
|
2293
|
-
|
|
2294
|
-
let val =
|
|
2295
|
-
std::mem::replace(&mut self.stack[base + 2 * i + 1], Value::Null);
|
|
3144
|
+
let key_val = std::mem::replace(&mut self.stack[base + 2 * i], Value::Null);
|
|
3145
|
+
let val = std::mem::replace(&mut self.stack[base + 2 * i + 1], Value::Null);
|
|
2296
3146
|
let key: Arc<str> = key_val.to_display_string().into();
|
|
2297
3147
|
map.insert(key, val);
|
|
2298
3148
|
}
|
|
@@ -2448,13 +3298,25 @@ impl Vm {
|
|
|
2448
3298
|
.iter()
|
|
2449
3299
|
.map(|&n| {
|
|
2450
3300
|
let elem = Value::Number(n);
|
|
2451
|
-
let (l, r) = if param_left {
|
|
3301
|
+
let (l, r) = if param_left {
|
|
3302
|
+
(elem, const_val.clone())
|
|
3303
|
+
} else {
|
|
3304
|
+
(const_val.clone(), elem)
|
|
3305
|
+
};
|
|
2452
3306
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
|
|
2453
3307
|
})
|
|
2454
3308
|
.collect();
|
|
2455
3309
|
// If every result is numeric, stay packed (the common case for x*2, x+1, etc).
|
|
2456
3310
|
if mapped.iter().all(|v| matches!(v, Value::Number(_))) {
|
|
2457
|
-
Value::number_array(
|
|
3311
|
+
Value::number_array(
|
|
3312
|
+
mapped
|
|
3313
|
+
.into_iter()
|
|
3314
|
+
.map(|v| match v {
|
|
3315
|
+
Value::Number(n) => n,
|
|
3316
|
+
_ => unreachable!(),
|
|
3317
|
+
})
|
|
3318
|
+
.collect(),
|
|
3319
|
+
)
|
|
2458
3320
|
} else {
|
|
2459
3321
|
Value::Array(VmRef::new(mapped))
|
|
2460
3322
|
}
|
|
@@ -2464,8 +3326,16 @@ impl Vm {
|
|
|
2464
3326
|
let mapped: Vec<Value> = arr_borrow
|
|
2465
3327
|
.iter()
|
|
2466
3328
|
.map(|v| {
|
|
2467
|
-
let l: Value = if param_left {
|
|
2468
|
-
|
|
3329
|
+
let l: Value = if param_left {
|
|
3330
|
+
(*v).clone()
|
|
3331
|
+
} else {
|
|
3332
|
+
const_val.clone()
|
|
3333
|
+
};
|
|
3334
|
+
let r: Value = if param_left {
|
|
3335
|
+
const_val.clone()
|
|
3336
|
+
} else {
|
|
3337
|
+
(*v).clone()
|
|
3338
|
+
};
|
|
2469
3339
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
|
|
2470
3340
|
})
|
|
2471
3341
|
.collect();
|
|
@@ -2499,7 +3369,11 @@ impl Vm {
|
|
|
2499
3369
|
.iter()
|
|
2500
3370
|
.filter(|&&n| {
|
|
2501
3371
|
let elem = Value::Number(n);
|
|
2502
|
-
let (l, r) = if param_left {
|
|
3372
|
+
let (l, r) = if param_left {
|
|
3373
|
+
(elem, const_val.clone())
|
|
3374
|
+
} else {
|
|
3375
|
+
(const_val.clone(), elem)
|
|
3376
|
+
};
|
|
2503
3377
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
2504
3378
|
})
|
|
2505
3379
|
.copied()
|
|
@@ -2511,7 +3385,11 @@ impl Vm {
|
|
|
2511
3385
|
let filtered: Vec<Value> = arr_borrow
|
|
2512
3386
|
.iter()
|
|
2513
3387
|
.filter(|v| {
|
|
2514
|
-
let (l, r) = if param_left {
|
|
3388
|
+
let (l, r) = if param_left {
|
|
3389
|
+
((*v).clone(), const_val.clone())
|
|
3390
|
+
} else {
|
|
3391
|
+
(const_val.clone(), (*v).clone())
|
|
3392
|
+
};
|
|
2515
3393
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
2516
3394
|
})
|
|
2517
3395
|
.cloned()
|
|
@@ -2581,15 +3459,14 @@ impl Vm {
|
|
|
2581
3459
|
// on the cranelift / llvm backends that want to expose
|
|
2582
3460
|
// `cargo:…` Rust crates should register the module's
|
|
2583
3461
|
// exports map before calling `vm.run(chunk)`.
|
|
2584
|
-
let from_registry: Option<Value> =
|
|
2585
|
-
|| spec.starts_with("ffi:")
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
};
|
|
3462
|
+
let from_registry: Option<Value> =
|
|
3463
|
+
if spec.starts_with("cargo:") || spec.starts_with("ffi:") {
|
|
3464
|
+
let regs = self.native_modules.borrow();
|
|
3465
|
+
regs.get(spec)
|
|
3466
|
+
.and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
|
|
3467
|
+
} else {
|
|
3468
|
+
None
|
|
3469
|
+
};
|
|
2593
3470
|
let v = from_registry
|
|
2594
3471
|
.or_else(|| get_builtin_export(self.capabilities.as_ref(), spec, export_name))
|
|
2595
3472
|
.ok_or_else(|| {
|
|
@@ -2772,7 +3649,12 @@ fn eval_unary(op: UnaryOp, o: &Value) -> Result<Value, String> {
|
|
|
2772
3649
|
/// error), refilling the cache when the object *does* have the property. Result-equivalent to
|
|
2773
3650
|
/// `get_member` — the cache only skips the lookup; the shape uniquely fixes the slot for a property.
|
|
2774
3651
|
#[inline]
|
|
2775
|
-
fn ic_get_member(
|
|
3652
|
+
fn ic_get_member(
|
|
3653
|
+
chunk: &Chunk,
|
|
3654
|
+
name_idx: u16,
|
|
3655
|
+
obj: &Value,
|
|
3656
|
+
key: &Arc<str>,
|
|
3657
|
+
) -> Result<Value, String> {
|
|
2776
3658
|
use std::sync::atomic::Ordering::Relaxed;
|
|
2777
3659
|
if let Value::Object(od) = obj {
|
|
2778
3660
|
let b = od.borrow();
|
|
@@ -2853,13 +3735,21 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2853
3735
|
let map = m.borrow();
|
|
2854
3736
|
// Reading a missing own property returns `null` (tish's nullish value), matching
|
|
2855
3737
|
// JS object semantics and the tree-walk interpreter — not a thrown error (#66).
|
|
2856
|
-
Ok(map
|
|
3738
|
+
Ok(map
|
|
3739
|
+
.strings
|
|
3740
|
+
.get(key.as_ref())
|
|
3741
|
+
.cloned()
|
|
3742
|
+
.unwrap_or(Value::Null))
|
|
2857
3743
|
}
|
|
2858
3744
|
Value::NumberArray(a) => {
|
|
2859
3745
|
let key_s = key.as_ref();
|
|
2860
3746
|
// Numeric index fast path.
|
|
2861
3747
|
if let Ok(idx) = key_s.parse::<usize>() {
|
|
2862
|
-
return Ok(a
|
|
3748
|
+
return Ok(a
|
|
3749
|
+
.borrow()
|
|
3750
|
+
.get(idx)
|
|
3751
|
+
.map(|&n| Value::Number(n))
|
|
3752
|
+
.unwrap_or(Value::Null));
|
|
2863
3753
|
}
|
|
2864
3754
|
if key_s == "length" {
|
|
2865
3755
|
return Ok(Value::Number(a.borrow().len() as f64));
|
|
@@ -2880,19 +3770,38 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2880
3770
|
Value::Number(arr.len() as f64)
|
|
2881
3771
|
}),
|
|
2882
3772
|
"pop" => make_native_fn(move |_: &[Value]| {
|
|
2883
|
-
a_clone
|
|
2884
|
-
.
|
|
3773
|
+
a_clone
|
|
3774
|
+
.borrow_mut()
|
|
3775
|
+
.pop()
|
|
3776
|
+
.map(|n| {
|
|
3777
|
+
if n.is_nan() {
|
|
3778
|
+
Value::Null
|
|
3779
|
+
} else {
|
|
3780
|
+
Value::Number(n)
|
|
3781
|
+
}
|
|
3782
|
+
})
|
|
2885
3783
|
.unwrap_or(Value::Null)
|
|
2886
3784
|
}),
|
|
2887
3785
|
"shift" => make_native_fn(move |_: &[Value]| {
|
|
2888
3786
|
let mut arr = a_clone.borrow_mut();
|
|
2889
|
-
if arr.is_empty() {
|
|
2890
|
-
|
|
3787
|
+
if arr.is_empty() {
|
|
3788
|
+
Value::Null
|
|
3789
|
+
} else {
|
|
3790
|
+
let n = arr.remove(0);
|
|
3791
|
+
if n.is_nan() {
|
|
3792
|
+
Value::Null
|
|
3793
|
+
} else {
|
|
3794
|
+
Value::Number(n)
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
2891
3797
|
}),
|
|
2892
3798
|
"unshift" => make_native_fn(move |args: &[Value]| {
|
|
2893
3799
|
let mut arr = a_clone.borrow_mut();
|
|
2894
3800
|
for (i, v) in args.iter().enumerate() {
|
|
2895
|
-
let n = match v {
|
|
3801
|
+
let n = match v {
|
|
3802
|
+
Value::Number(n) => *n,
|
|
3803
|
+
_ => f64::NAN,
|
|
3804
|
+
};
|
|
2896
3805
|
arr.insert(i, n);
|
|
2897
3806
|
}
|
|
2898
3807
|
Value::Number(arr.len() as f64)
|
|
@@ -2905,7 +3814,10 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2905
3814
|
let a2 = a_clone.clone();
|
|
2906
3815
|
make_native_fn(move |args: &[Value]| {
|
|
2907
3816
|
// Check if there are non-numeric items to insert (args[2..]).
|
|
2908
|
-
let has_non_numeric = args
|
|
3817
|
+
let has_non_numeric = args
|
|
3818
|
+
.get(2..)
|
|
3819
|
+
.unwrap_or(&[])
|
|
3820
|
+
.iter()
|
|
2909
3821
|
.any(|v| !matches!(v, Value::Number(_)));
|
|
2910
3822
|
if has_non_numeric {
|
|
2911
3823
|
// Deopt: materialise, splice on the boxed array, then write numeric
|
|
@@ -2913,18 +3825,37 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2913
3825
|
// identity for subsequent accesses. The array may have non-numeric
|
|
2914
3826
|
// elements after this splice — they become NaN holes in the VmRef.
|
|
2915
3827
|
let boxed = Value::materialize_number_array(&a2);
|
|
2916
|
-
let result = arr_builtins::splice(
|
|
3828
|
+
let result = arr_builtins::splice(
|
|
3829
|
+
&boxed,
|
|
3830
|
+
args.first().unwrap_or(&Value::Null),
|
|
3831
|
+
args.get(1),
|
|
3832
|
+
args.get(2..).unwrap_or(&[]),
|
|
3833
|
+
);
|
|
2917
3834
|
// Sync the modified boxed Vec back into the original VmRef.
|
|
2918
3835
|
if let Value::Array(boxed_vmref) = &boxed {
|
|
2919
3836
|
let mut packed = a2.borrow_mut();
|
|
2920
|
-
*packed = boxed_vmref
|
|
3837
|
+
*packed = boxed_vmref
|
|
3838
|
+
.borrow()
|
|
3839
|
+
.iter()
|
|
3840
|
+
.map(|v| match v {
|
|
3841
|
+
Value::Number(n) => *n,
|
|
3842
|
+
_ => f64::NAN,
|
|
3843
|
+
})
|
|
3844
|
+
.collect();
|
|
2921
3845
|
}
|
|
2922
3846
|
result
|
|
2923
3847
|
} else {
|
|
2924
3848
|
let mut arr = a2.borrow_mut();
|
|
2925
3849
|
let len = arr.len() as i64;
|
|
2926
3850
|
let start = match args.first() {
|
|
2927
|
-
Some(Value::Number(n)) => {
|
|
3851
|
+
Some(Value::Number(n)) => {
|
|
3852
|
+
let s = *n as i64;
|
|
3853
|
+
if s < 0 {
|
|
3854
|
+
(len + s).max(0) as usize
|
|
3855
|
+
} else {
|
|
3856
|
+
(s as usize).min(arr.len())
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
2928
3859
|
_ => 0,
|
|
2929
3860
|
};
|
|
2930
3861
|
let del = match args.get(1) {
|
|
@@ -2932,8 +3863,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2932
3863
|
_ => arr.len().saturating_sub(start),
|
|
2933
3864
|
};
|
|
2934
3865
|
let del = del.min(arr.len().saturating_sub(start));
|
|
2935
|
-
let new_nums: Vec<f64> = args
|
|
2936
|
-
|
|
3866
|
+
let new_nums: Vec<f64> = args
|
|
3867
|
+
.get(2..)
|
|
3868
|
+
.unwrap_or(&[])
|
|
3869
|
+
.iter()
|
|
3870
|
+
.map(|v| match v {
|
|
3871
|
+
Value::Number(n) => *n,
|
|
3872
|
+
_ => f64::NAN,
|
|
3873
|
+
})
|
|
3874
|
+
.collect();
|
|
3875
|
+
let removed: Vec<f64> =
|
|
3876
|
+
arr.splice(start..start + del, new_nums).collect();
|
|
2937
3877
|
Value::number_array(removed)
|
|
2938
3878
|
}
|
|
2939
3879
|
})
|
|
@@ -2956,29 +3896,144 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2956
3896
|
let boxed = Value::materialize_number_array(&a_clone);
|
|
2957
3897
|
let bv = boxed.clone();
|
|
2958
3898
|
match key_s {
|
|
2959
|
-
"map"
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
"
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
"
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3899
|
+
"map" => make_native_fn(move |args| {
|
|
3900
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3901
|
+
// #187 fusion over the SAME boxed snapshot `bv` the generic path uses, so a
|
|
3902
|
+
// fused result is byte-identical (a boxed all-number `Array` → boxed output).
|
|
3903
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3904
|
+
if let Some(v) = hof_fusion::map(&bv, &cb) {
|
|
3905
|
+
return v;
|
|
3906
|
+
}
|
|
3907
|
+
arr_builtins::map(&bv, &cb)
|
|
3908
|
+
}),
|
|
3909
|
+
"filter" => make_native_fn(move |args| {
|
|
3910
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3911
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3912
|
+
if let Some(v) = hof_fusion::filter(&bv, &cb) {
|
|
3913
|
+
return v;
|
|
3914
|
+
}
|
|
3915
|
+
arr_builtins::filter(&bv, &cb)
|
|
3916
|
+
}),
|
|
3917
|
+
"reduce" => make_native_fn(move |args| {
|
|
3918
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3919
|
+
let init = args.get(1).cloned(); // None = no initial value (empty array → throw)
|
|
3920
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3921
|
+
if let Some(v) = hof_fusion::reduce(&bv, &cb, init.as_ref()) {
|
|
3922
|
+
return v;
|
|
3923
|
+
}
|
|
3924
|
+
arr_builtins::reduce(&bv, &cb, init.as_ref())
|
|
3925
|
+
}),
|
|
3926
|
+
"reduceRight" => make_native_fn(move |args| {
|
|
3927
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3928
|
+
let init = args.get(1).cloned();
|
|
3929
|
+
arr_builtins::reduce_right(&bv, &cb, init.as_ref())
|
|
3930
|
+
}),
|
|
3931
|
+
"keys" => make_native_fn(move |_| arr_builtins::keys(&bv)),
|
|
3932
|
+
"values" => make_native_fn(move |_| arr_builtins::values(&bv)),
|
|
3933
|
+
"entries" => make_native_fn(move |_| arr_builtins::entries(&bv)),
|
|
3934
|
+
"forEach" => make_native_fn(move |args| {
|
|
3935
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3936
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3937
|
+
if let Some(v) = hof_fusion::for_each(&bv, &cb) {
|
|
3938
|
+
return v;
|
|
3939
|
+
}
|
|
3940
|
+
arr_builtins::for_each(&bv, &cb)
|
|
3941
|
+
}),
|
|
3942
|
+
"find" => make_native_fn(move |args| {
|
|
3943
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3944
|
+
arr_builtins::find(&bv, &cb)
|
|
3945
|
+
}),
|
|
3946
|
+
"findIndex" => make_native_fn(move |args| {
|
|
3947
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3948
|
+
arr_builtins::find_index(&bv, &cb)
|
|
3949
|
+
}),
|
|
3950
|
+
"findLast" => make_native_fn(move |args| {
|
|
3951
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3952
|
+
arr_builtins::find_last(&bv, &cb)
|
|
3953
|
+
}),
|
|
3954
|
+
"findLastIndex" => make_native_fn(move |args| {
|
|
3955
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3956
|
+
arr_builtins::find_last_index(&bv, &cb)
|
|
3957
|
+
}),
|
|
3958
|
+
"at" => make_native_fn(move |args| {
|
|
3959
|
+
let i = args.first().cloned().unwrap_or(Value::Null);
|
|
3960
|
+
arr_builtins::at(&bv, &i)
|
|
3961
|
+
}),
|
|
3962
|
+
"some" => make_native_fn(move |args| {
|
|
3963
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3964
|
+
arr_builtins::some(&bv, &cb)
|
|
3965
|
+
}),
|
|
3966
|
+
"every" => make_native_fn(move |args| {
|
|
3967
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3968
|
+
arr_builtins::every(&bv, &cb)
|
|
3969
|
+
}),
|
|
3970
|
+
"join" => make_native_fn(move |args| {
|
|
3971
|
+
let sep = args.first().cloned().unwrap_or(Value::Null);
|
|
3972
|
+
arr_builtins::join(&bv, &sep)
|
|
3973
|
+
}),
|
|
3974
|
+
"flat" => make_native_fn(move |args| {
|
|
3975
|
+
let d = args.first().cloned().unwrap_or(Value::Number(1.0));
|
|
3976
|
+
arr_builtins::flat(&bv, &d)
|
|
3977
|
+
}),
|
|
3978
|
+
"flatMap" => make_native_fn(move |args| {
|
|
3979
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3980
|
+
arr_builtins::flat_map(&bv, &cb)
|
|
3981
|
+
}),
|
|
3982
|
+
"reverse" => make_native_fn(move |_| arr_builtins::reverse(&bv)),
|
|
3983
|
+
"toReversed" => make_native_fn(move |_| arr_builtins::to_reversed(&bv)),
|
|
3984
|
+
"toSorted" => {
|
|
3985
|
+
make_native_fn(move |args| arr_builtins::to_sorted(&bv, args.first()))
|
|
3986
|
+
}
|
|
3987
|
+
"with" => make_native_fn(move |args| {
|
|
3988
|
+
let i = args.first().cloned().unwrap_or(Value::Null);
|
|
3989
|
+
let v = args.get(1).cloned().unwrap_or(Value::Null);
|
|
3990
|
+
arr_builtins::with(&bv, &i, &v)
|
|
3991
|
+
}),
|
|
3992
|
+
"toSpliced" => make_native_fn(move |args| {
|
|
3993
|
+
let start = args.first().cloned().unwrap_or(Value::Null);
|
|
3994
|
+
let dc = args.get(1).cloned();
|
|
3995
|
+
let items: Vec<Value> = args.iter().skip(2).cloned().collect();
|
|
3996
|
+
arr_builtins::to_spliced(&bv, &start, dc.as_ref(), &items)
|
|
3997
|
+
}),
|
|
3998
|
+
"fill" => make_native_fn(move |args| {
|
|
3999
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
4000
|
+
let s = args.get(1).cloned().unwrap_or(Value::Null);
|
|
4001
|
+
let e = args.get(2).cloned().unwrap_or(Value::Null);
|
|
4002
|
+
arr_builtins::fill(&bv, &v, &s, &e)
|
|
4003
|
+
}),
|
|
4004
|
+
"slice" => make_native_fn(move |args| {
|
|
4005
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
4006
|
+
let e = args.get(1).cloned().unwrap_or(Value::Null);
|
|
4007
|
+
arr_builtins::slice(&bv, &s, &e)
|
|
4008
|
+
}),
|
|
4009
|
+
"concat" => make_native_fn(move |args| arr_builtins::concat(&bv, args)),
|
|
4010
|
+
"indexOf" => make_native_fn(move |args| {
|
|
4011
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
4012
|
+
arr_builtins::index_of(&bv, &s, args.get(1))
|
|
4013
|
+
}),
|
|
4014
|
+
"lastIndexOf" => make_native_fn(move |args| {
|
|
4015
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
4016
|
+
arr_builtins::last_index_of(&bv, &s, args.get(1))
|
|
4017
|
+
}),
|
|
4018
|
+
"copyWithin" => make_native_fn(move |args| {
|
|
4019
|
+
let t = args.first().cloned().unwrap_or(Value::Null);
|
|
4020
|
+
let s = args.get(1).cloned().unwrap_or(Value::Null);
|
|
4021
|
+
let e = args.get(2).cloned().unwrap_or(Value::Null);
|
|
4022
|
+
arr_builtins::copy_within(&bv, &t, &s, &e)
|
|
4023
|
+
}),
|
|
4024
|
+
"includes" => make_native_fn(move |args| {
|
|
4025
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
4026
|
+
let f = args.get(1).cloned();
|
|
4027
|
+
arr_builtins::includes(&bv, &s, f.as_ref())
|
|
4028
|
+
}),
|
|
4029
|
+
"unshift" => make_native_fn(move |args| arr_builtins::unshift(&bv, args)),
|
|
4030
|
+
"shift" => make_native_fn(move |_| arr_builtins::shift(&bv)),
|
|
4031
|
+
"splice" => make_native_fn(move |args| {
|
|
4032
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
4033
|
+
let dc = args.get(1).cloned();
|
|
4034
|
+
let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
|
|
4035
|
+
arr_builtins::splice(&bv, &s, dc.as_ref(), &items)
|
|
4036
|
+
}),
|
|
2982
4037
|
_ => return Err(format!("Property '{}' not found", key)),
|
|
2983
4038
|
}
|
|
2984
4039
|
}
|
|
@@ -3037,7 +4092,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3037
4092
|
}),
|
|
3038
4093
|
"indexOf" => make_native_fn(move |args: &[Value]| {
|
|
3039
4094
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3040
|
-
arr_builtins::index_of(&Value::Array(a_clone.clone()), search)
|
|
4095
|
+
arr_builtins::index_of(&Value::Array(a_clone.clone()), search, args.get(1))
|
|
4096
|
+
}),
|
|
4097
|
+
"lastIndexOf" => make_native_fn(move |args: &[Value]| {
|
|
4098
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
4099
|
+
arr_builtins::last_index_of(&Value::Array(a_clone.clone()), search, args.get(1))
|
|
4100
|
+
}),
|
|
4101
|
+
"copyWithin" => make_native_fn(move |args: &[Value]| {
|
|
4102
|
+
let t = args.first().cloned().unwrap_or(Value::Null);
|
|
4103
|
+
let s = args.get(1).cloned().unwrap_or(Value::Null);
|
|
4104
|
+
let e = args.get(2).cloned().unwrap_or(Value::Null);
|
|
4105
|
+
arr_builtins::copy_within(&Value::Array(a_clone.clone()), &t, &s, &e)
|
|
3041
4106
|
}),
|
|
3042
4107
|
"includes" => make_native_fn(move |args: &[Value]| {
|
|
3043
4108
|
let search = args.first().unwrap_or(&Value::Null);
|
|
@@ -3046,20 +4111,50 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3046
4111
|
}),
|
|
3047
4112
|
"map" => make_native_fn(move |args: &[Value]| {
|
|
3048
4113
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3049
|
-
|
|
4114
|
+
let arr = Value::Array(a_clone.clone());
|
|
4115
|
+
// #187 native HOF fusion — tight native loop over a JIT'd numeric callback; bails
|
|
4116
|
+
// (None) to the byte-identical generic path for any non-fusable callback/array.
|
|
4117
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
4118
|
+
if let Some(v) = hof_fusion::map(&arr, &cb) {
|
|
4119
|
+
return v;
|
|
4120
|
+
}
|
|
4121
|
+
arr_builtins::map(&arr, &cb)
|
|
3050
4122
|
}),
|
|
3051
4123
|
"filter" => make_native_fn(move |args: &[Value]| {
|
|
3052
4124
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3053
|
-
|
|
4125
|
+
let arr = Value::Array(a_clone.clone());
|
|
4126
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
4127
|
+
if let Some(v) = hof_fusion::filter(&arr, &cb) {
|
|
4128
|
+
return v;
|
|
4129
|
+
}
|
|
4130
|
+
arr_builtins::filter(&arr, &cb)
|
|
3054
4131
|
}),
|
|
3055
4132
|
"reduce" => make_native_fn(move |args: &[Value]| {
|
|
3056
4133
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3057
|
-
let init = args.get(1).cloned()
|
|
3058
|
-
|
|
4134
|
+
let init = args.get(1).cloned(); // None = no initial value (empty array → throw)
|
|
4135
|
+
let arr = Value::Array(a_clone.clone());
|
|
4136
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
4137
|
+
if let Some(v) = hof_fusion::reduce(&arr, &cb, init.as_ref()) {
|
|
4138
|
+
return v;
|
|
4139
|
+
}
|
|
4140
|
+
arr_builtins::reduce(&arr, &cb, init.as_ref())
|
|
4141
|
+
}),
|
|
4142
|
+
"reduceRight" => make_native_fn(move |args: &[Value]| {
|
|
4143
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
4144
|
+
let init = args.get(1).cloned();
|
|
4145
|
+
arr_builtins::reduce_right(&Value::Array(a_clone.clone()), &cb, init.as_ref())
|
|
3059
4146
|
}),
|
|
4147
|
+
"keys" => make_native_fn(move |_: &[Value]| arr_builtins::keys(&Value::Array(a_clone.clone()))),
|
|
4148
|
+
"values" => make_native_fn(move |_: &[Value]| arr_builtins::values(&Value::Array(a_clone.clone()))),
|
|
4149
|
+
"entries" => make_native_fn(move |_: &[Value]| arr_builtins::entries(&Value::Array(a_clone.clone()))),
|
|
3060
4150
|
"forEach" => make_native_fn(move |args: &[Value]| {
|
|
3061
4151
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3062
|
-
|
|
4152
|
+
let arr = Value::Array(a_clone.clone());
|
|
4153
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
4154
|
+
if let Some(v) = hof_fusion::for_each(&arr, &cb) {
|
|
4155
|
+
return v;
|
|
4156
|
+
}
|
|
4157
|
+
arr_builtins::for_each(&arr, &cb)
|
|
3063
4158
|
}),
|
|
3064
4159
|
"find" => make_native_fn(move |args: &[Value]| {
|
|
3065
4160
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
@@ -3119,6 +4214,28 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3119
4214
|
&items,
|
|
3120
4215
|
)
|
|
3121
4216
|
}),
|
|
4217
|
+
"toReversed" => make_native_fn(move |_: &[Value]| {
|
|
4218
|
+
arr_builtins::to_reversed(&Value::Array(a_clone.clone()))
|
|
4219
|
+
}),
|
|
4220
|
+
"toSorted" => make_native_fn(move |args: &[Value]| {
|
|
4221
|
+
arr_builtins::to_sorted(&Value::Array(a_clone.clone()), args.first())
|
|
4222
|
+
}),
|
|
4223
|
+
"with" => make_native_fn(move |args: &[Value]| {
|
|
4224
|
+
let i = args.first().cloned().unwrap_or(Value::Null);
|
|
4225
|
+
let v = args.get(1).cloned().unwrap_or(Value::Null);
|
|
4226
|
+
arr_builtins::with(&Value::Array(a_clone.clone()), &i, &v)
|
|
4227
|
+
}),
|
|
4228
|
+
"toSpliced" => make_native_fn(move |args: &[Value]| {
|
|
4229
|
+
let start = args.first().unwrap_or(&Value::Null);
|
|
4230
|
+
let delete_count = args.get(1).map(|v| v as &Value);
|
|
4231
|
+
let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
|
|
4232
|
+
arr_builtins::to_spliced(
|
|
4233
|
+
&Value::Array(a_clone.clone()),
|
|
4234
|
+
start,
|
|
4235
|
+
delete_count,
|
|
4236
|
+
&items,
|
|
4237
|
+
)
|
|
4238
|
+
}),
|
|
3122
4239
|
_ => return Err(format!("Property '{}' not found", key)),
|
|
3123
4240
|
};
|
|
3124
4241
|
Ok(Value::Function(method))
|
|
@@ -3126,13 +4243,13 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3126
4243
|
Value::String(s) => {
|
|
3127
4244
|
let key_s = key.as_ref();
|
|
3128
4245
|
if let Ok(idx) = key_s.parse::<usize>() {
|
|
3129
|
-
return match s
|
|
4246
|
+
return match str_builtins::nth_char(s, idx) {
|
|
3130
4247
|
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
3131
4248
|
None => Err("Index out of bounds".to_string()),
|
|
3132
4249
|
};
|
|
3133
4250
|
}
|
|
3134
4251
|
if key_s == "length" {
|
|
3135
|
-
return Ok(Value::Number(s
|
|
4252
|
+
return Ok(Value::Number(str_builtins::char_count(s) as f64));
|
|
3136
4253
|
}
|
|
3137
4254
|
let s_clone: tishlang_core::ArcStr = s.clone();
|
|
3138
4255
|
let method: ArrayMethodFn = match key_s {
|
|
@@ -3144,11 +4261,7 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3144
4261
|
"lastIndexOf" => make_native_fn(move |args: &[Value]| {
|
|
3145
4262
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3146
4263
|
let position = args.get(1).cloned().unwrap_or(Value::Number(f64::INFINITY));
|
|
3147
|
-
str_builtins::last_index_of(
|
|
3148
|
-
&Value::String(s_clone.clone()),
|
|
3149
|
-
search,
|
|
3150
|
-
&position,
|
|
3151
|
-
)
|
|
4264
|
+
str_builtins::last_index_of(&Value::String(s_clone.clone()), search, &position)
|
|
3152
4265
|
}),
|
|
3153
4266
|
"includes" => make_native_fn(move |args: &[Value]| {
|
|
3154
4267
|
let search = args.first().unwrap_or(&Value::Null);
|
|
@@ -3189,6 +4302,16 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3189
4302
|
"trim" => make_native_fn(move |_args: &[Value]| {
|
|
3190
4303
|
str_builtins::trim(&Value::String(s_clone.clone()))
|
|
3191
4304
|
}),
|
|
4305
|
+
"trimStart" => make_native_fn(move |_args: &[Value]| {
|
|
4306
|
+
str_builtins::trim_start(&Value::String(s_clone.clone()))
|
|
4307
|
+
}),
|
|
4308
|
+
"trimEnd" => make_native_fn(move |_args: &[Value]| {
|
|
4309
|
+
str_builtins::trim_end(&Value::String(s_clone.clone()))
|
|
4310
|
+
}),
|
|
4311
|
+
"normalize" => make_native_fn(move |args: &[Value]| {
|
|
4312
|
+
let form = args.first().cloned().unwrap_or(Value::Null);
|
|
4313
|
+
str_builtins::normalize(&Value::String(s_clone.clone()), &form)
|
|
4314
|
+
}),
|
|
3192
4315
|
"toUpperCase" => make_native_fn(move |_args: &[Value]| {
|
|
3193
4316
|
str_builtins::to_upper_case(&Value::String(s_clone.clone()))
|
|
3194
4317
|
}),
|
|
@@ -3197,11 +4320,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3197
4320
|
}),
|
|
3198
4321
|
"startsWith" => make_native_fn(move |args: &[Value]| {
|
|
3199
4322
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3200
|
-
str_builtins::starts_with(&Value::String(s_clone.clone()), search)
|
|
4323
|
+
str_builtins::starts_with(&Value::String(s_clone.clone()), search, args.get(1))
|
|
3201
4324
|
}),
|
|
3202
4325
|
"endsWith" => make_native_fn(move |args: &[Value]| {
|
|
3203
4326
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3204
|
-
str_builtins::ends_with(&Value::String(s_clone.clone()), search)
|
|
4327
|
+
str_builtins::ends_with(&Value::String(s_clone.clone()), search, args.get(1))
|
|
3205
4328
|
}),
|
|
3206
4329
|
"replace" => make_native_fn(move |args: &[Value]| {
|
|
3207
4330
|
let search = args.first().unwrap_or(&Value::Null);
|
|
@@ -3221,11 +4344,7 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3221
4344
|
"replaceAll" => make_native_fn(move |args: &[Value]| {
|
|
3222
4345
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3223
4346
|
let replacement = args.get(1).unwrap_or(&Value::Null);
|
|
3224
|
-
str_builtins::replace_all(
|
|
3225
|
-
&Value::String(s_clone.clone()),
|
|
3226
|
-
search,
|
|
3227
|
-
replacement,
|
|
3228
|
-
)
|
|
4347
|
+
str_builtins::replace_all(&Value::String(s_clone.clone()), search, replacement)
|
|
3229
4348
|
}),
|
|
3230
4349
|
#[cfg(feature = "regex")]
|
|
3231
4350
|
"match" => make_native_fn(move |args: &[Value]| {
|
|
@@ -3233,6 +4352,11 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3233
4352
|
tishlang_runtime::string_match_regex(&Value::String(s_clone.clone()), re)
|
|
3234
4353
|
}),
|
|
3235
4354
|
#[cfg(feature = "regex")]
|
|
4355
|
+
"matchAll" => make_native_fn(move |args: &[Value]| {
|
|
4356
|
+
let re = args.first().unwrap_or(&Value::Null);
|
|
4357
|
+
tishlang_runtime::string_match_all_regex(&Value::String(s_clone.clone()), re)
|
|
4358
|
+
}),
|
|
4359
|
+
#[cfg(feature = "regex")]
|
|
3236
4360
|
"search" => make_native_fn(move |args: &[Value]| {
|
|
3237
4361
|
let re = args.first().unwrap_or(&Value::Null);
|
|
3238
4362
|
tishlang_runtime::string_search_regex(&Value::String(s_clone.clone()), re)
|
|
@@ -3249,6 +4373,15 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3249
4373
|
let idx = args.first().unwrap_or(&Value::Null);
|
|
3250
4374
|
str_builtins::char_code_at(&Value::String(s_clone.clone()), idx)
|
|
3251
4375
|
}),
|
|
4376
|
+
"codePointAt" => make_native_fn(move |args: &[Value]| {
|
|
4377
|
+
let idx = args.first().unwrap_or(&Value::Null);
|
|
4378
|
+
str_builtins::code_point_at(&Value::String(s_clone.clone()), idx)
|
|
4379
|
+
}),
|
|
4380
|
+
"substr" => make_native_fn(move |args: &[Value]| {
|
|
4381
|
+
let start = args.first().cloned().unwrap_or(Value::Null);
|
|
4382
|
+
let length = args.get(1).cloned().unwrap_or(Value::Null);
|
|
4383
|
+
str_builtins::substr(&Value::String(s_clone.clone()), &start, &length)
|
|
4384
|
+
}),
|
|
3252
4385
|
"repeat" => make_native_fn(move |args: &[Value]| {
|
|
3253
4386
|
let count = args.first().unwrap_or(&Value::Null);
|
|
3254
4387
|
str_builtins::repeat(&Value::String(s_clone.clone()), count)
|
|
@@ -3276,6 +4409,14 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3276
4409
|
let digits = args.first().unwrap_or(&Value::Null);
|
|
3277
4410
|
num_builtins::to_fixed(&Value::Number(n_val), digits)
|
|
3278
4411
|
}),
|
|
4412
|
+
"toExponential" => make_native_fn(move |args: &[Value]| {
|
|
4413
|
+
let digits = args.first().unwrap_or(&Value::Null);
|
|
4414
|
+
num_builtins::to_exponential(&Value::Number(n_val), digits)
|
|
4415
|
+
}),
|
|
4416
|
+
"toPrecision" => make_native_fn(move |args: &[Value]| {
|
|
4417
|
+
let precision = args.first().unwrap_or(&Value::Null);
|
|
4418
|
+
num_builtins::to_precision(&Value::Number(n_val), precision)
|
|
4419
|
+
}),
|
|
3279
4420
|
"toString" => make_native_fn(move |args: &[Value]| {
|
|
3280
4421
|
let radix = args.first().unwrap_or(&Value::Null);
|
|
3281
4422
|
num_builtins::to_string(&Value::Number(n_val), radix)
|
|
@@ -3390,43 +4531,48 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3390
4531
|
Value::NumberArray(a) => {
|
|
3391
4532
|
let i = match idx {
|
|
3392
4533
|
Value::Number(n) => *n as usize,
|
|
3393
|
-
|
|
4534
|
+
// A canonical integer string key ("0","1",…) indexes the array (#432); any other key
|
|
4535
|
+
// reads Null (JS reads a missing property as undefined) rather than throwing.
|
|
4536
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
4537
|
+
Some(i) => i,
|
|
4538
|
+
None => return Ok(Value::Null),
|
|
4539
|
+
},
|
|
4540
|
+
_ => return Ok(Value::Null),
|
|
3394
4541
|
};
|
|
3395
4542
|
// NaN is used as the hole marker (sparse-array positions); reads return Null.
|
|
3396
|
-
Ok(a.borrow()
|
|
4543
|
+
Ok(a.borrow()
|
|
4544
|
+
.get(i)
|
|
4545
|
+
.map(|&n| {
|
|
4546
|
+
if n.is_nan() {
|
|
4547
|
+
Value::Null
|
|
4548
|
+
} else {
|
|
4549
|
+
Value::Number(n)
|
|
4550
|
+
}
|
|
4551
|
+
})
|
|
4552
|
+
.unwrap_or(Value::Null))
|
|
3397
4553
|
}
|
|
3398
4554
|
Value::Array(a) => {
|
|
3399
4555
|
let i = match idx {
|
|
3400
4556
|
Value::Number(n) => *n as usize,
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
4557
|
+
// Canonical integer string key → index (#432); any other key reads Null, not a throw.
|
|
4558
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
4559
|
+
Some(i) => i,
|
|
4560
|
+
None => return Ok(Value::Null),
|
|
4561
|
+
},
|
|
4562
|
+
_ => return Ok(Value::Null),
|
|
3407
4563
|
};
|
|
3408
|
-
Ok(a
|
|
3409
|
-
.borrow()
|
|
3410
|
-
.get(i)
|
|
3411
|
-
.cloned()
|
|
3412
|
-
.unwrap_or(Value::Null))
|
|
4564
|
+
Ok(a.borrow().get(i).cloned().unwrap_or(Value::Null))
|
|
3413
4565
|
}
|
|
3414
4566
|
Value::String(s) => {
|
|
3415
4567
|
let i = match idx {
|
|
3416
4568
|
Value::Number(n) => {
|
|
3417
4569
|
let n = *n;
|
|
3418
4570
|
if n < 0.0 || n.fract() != 0.0 {
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
));
|
|
3423
|
-
}
|
|
3424
|
-
let i = n as usize;
|
|
3425
|
-
let len = s.chars().count();
|
|
3426
|
-
if i >= len {
|
|
3427
|
-
return Err("Index out of bounds".to_string());
|
|
4571
|
+
// A negative / non-integer string index reads `null` (JS `undefined`), NOT a
|
|
4572
|
+
// throw — interp and native already return null; the vm must agree. (#437)
|
|
4573
|
+
return Ok(Value::Null);
|
|
3428
4574
|
}
|
|
3429
|
-
|
|
4575
|
+
n as usize
|
|
3430
4576
|
}
|
|
3431
4577
|
_ => {
|
|
3432
4578
|
return Err(format!(
|
|
@@ -3435,9 +4581,12 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3435
4581
|
));
|
|
3436
4582
|
}
|
|
3437
4583
|
};
|
|
3438
|
-
|
|
4584
|
+
// `nth_char` returns None past the end, so the cursor cache handles bounds — no separate
|
|
4585
|
+
// O(n) `chars().count()` pre-check (#203).
|
|
4586
|
+
match str_builtins::nth_char(s, i) {
|
|
3439
4587
|
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
3440
|
-
|
|
4588
|
+
// Past the end → `null` (JS `undefined`), matching interp/native, not a throw. (#437)
|
|
4589
|
+
None => Ok(Value::Null),
|
|
3441
4590
|
}
|
|
3442
4591
|
}
|
|
3443
4592
|
// A missing own property returns `null`, not a thrown error — matching dot reads
|
|
@@ -3455,7 +4604,7 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3455
4604
|
}
|
|
3456
4605
|
};
|
|
3457
4606
|
get_member(obj, &key_arc)
|
|
3458
|
-
}
|
|
4607
|
+
}
|
|
3459
4608
|
_ => Err(format!(
|
|
3460
4609
|
"Cannot read property '{}' of {}",
|
|
3461
4610
|
idx.to_display_string(),
|
|
@@ -3497,7 +4646,18 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3497
4646
|
Value::NumberArray(a) => {
|
|
3498
4647
|
let i = match idx {
|
|
3499
4648
|
Value::Number(n) => *n as usize,
|
|
3500
|
-
|
|
4649
|
+
// Canonical integer string key → index (#432); a non-index string key can't be stored
|
|
4650
|
+
// in a Vec-backed array, so the write is a no-op (rather than a throw).
|
|
4651
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
4652
|
+
Some(i) => i,
|
|
4653
|
+
None => return Ok(()),
|
|
4654
|
+
},
|
|
4655
|
+
_ => {
|
|
4656
|
+
return Err(format!(
|
|
4657
|
+
"Array index must be number, got {}",
|
|
4658
|
+
idx.type_name()
|
|
4659
|
+
))
|
|
4660
|
+
}
|
|
3501
4661
|
};
|
|
3502
4662
|
// In-bounds numeric assignment stays packed.
|
|
3503
4663
|
// Out-of-bounds or non-numeric falls through to the Array path by returning
|
|
@@ -3508,7 +4668,9 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3508
4668
|
Value::Number(n) => {
|
|
3509
4669
|
let mut arr = a.borrow_mut();
|
|
3510
4670
|
// Extend with NaN "holes" if needed (NaN = sparse hole; read back as Null).
|
|
3511
|
-
while arr.len() <= i {
|
|
4671
|
+
while arr.len() <= i {
|
|
4672
|
+
arr.push(f64::NAN);
|
|
4673
|
+
}
|
|
3512
4674
|
arr[i] = n;
|
|
3513
4675
|
}
|
|
3514
4676
|
// Non-numeric set: the Vec<f64> can't represent this type. Extend with NaN holes
|
|
@@ -3518,7 +4680,9 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3518
4680
|
// numeric elements and Null for the NaN holes.
|
|
3519
4681
|
_ => {
|
|
3520
4682
|
let mut arr = a.borrow_mut();
|
|
3521
|
-
while arr.len() <= i {
|
|
4683
|
+
while arr.len() <= i {
|
|
4684
|
+
arr.push(f64::NAN);
|
|
4685
|
+
}
|
|
3522
4686
|
// arr[i] is already NaN (hole); we can't store the non-numeric value — acceptable
|
|
3523
4687
|
// for the experimental TISH_PACKED_ARRAYS path.
|
|
3524
4688
|
}
|
|
@@ -3528,6 +4692,11 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3528
4692
|
Value::Array(a) => {
|
|
3529
4693
|
let i = match idx {
|
|
3530
4694
|
Value::Number(n) => *n as usize,
|
|
4695
|
+
// Canonical integer string key → index (#432); a non-index string key is a no-op write.
|
|
4696
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
4697
|
+
Some(i) => i,
|
|
4698
|
+
None => return Ok(()),
|
|
4699
|
+
},
|
|
3531
4700
|
_ => {
|
|
3532
4701
|
return Err(format!(
|
|
3533
4702
|
"Array index must be number, got {}",
|
|
@@ -3558,3 +4727,89 @@ pub fn run_with_options(chunk: &Chunk, opts: VmRunOptions) -> Result<Value, Stri
|
|
|
3558
4727
|
let mut vm = Vm::with_capabilities(opts.capabilities);
|
|
3559
4728
|
vm.run_with_options(chunk, opts.repl_mode)
|
|
3560
4729
|
}
|
|
4730
|
+
|
|
4731
|
+
#[cfg(test)]
|
|
4732
|
+
mod recursion_limit_tests_381 {
|
|
4733
|
+
use tishlang_core::{set_max_call_depth_for_test, Value, DEFAULT_MAX_CALL_DEPTH};
|
|
4734
|
+
|
|
4735
|
+
fn run_src(src: &str) -> Result<Value, String> {
|
|
4736
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
4737
|
+
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
4738
|
+
super::run(&chunk)
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
#[test]
|
|
4742
|
+
fn deep_recursion_is_catchable_not_abort() {
|
|
4743
|
+
// Without the guard this infinite recursion aborts the process (SIGABRT). With it, the throw
|
|
4744
|
+
// is a catchable RangeError: try/catch recovers and the program returns normally.
|
|
4745
|
+
set_max_call_depth_for_test(300);
|
|
4746
|
+
// Object-returning recursion is NOT JIT-eligible, so it takes the guarded VM call path.
|
|
4747
|
+
let src = "let ok = false\n\
|
|
4748
|
+
fn rec(n) { return { v: rec(n + 1) } }\n\
|
|
4749
|
+
try { rec(0) } catch (e) { ok = true }\n\
|
|
4750
|
+
ok";
|
|
4751
|
+
let out = run_src(src);
|
|
4752
|
+
set_max_call_depth_for_test(DEFAULT_MAX_CALL_DEPTH);
|
|
4753
|
+
assert!(
|
|
4754
|
+
out.is_ok(),
|
|
4755
|
+
"deep recursion must be catchable, not abort/error: {out:?}"
|
|
4756
|
+
);
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
#[test]
|
|
4760
|
+
fn uncaught_deep_recursion_surfaces_error_not_abort() {
|
|
4761
|
+
// An UNCAUGHT infinite recursion must surface as a returned error, never a SIGABRT.
|
|
4762
|
+
set_max_call_depth_for_test(300);
|
|
4763
|
+
let out = run_src("fn rec(n) { return { v: rec(n + 1) } }\nrec(0)");
|
|
4764
|
+
set_max_call_depth_for_test(DEFAULT_MAX_CALL_DEPTH);
|
|
4765
|
+
assert!(
|
|
4766
|
+
out.is_err(),
|
|
4767
|
+
"uncaught deep recursion must return an error, got {out:?}"
|
|
4768
|
+
);
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
#[test]
|
|
4772
|
+
fn normal_recursion_is_unaffected() {
|
|
4773
|
+
set_max_call_depth_for_test(20_000);
|
|
4774
|
+
let out = run_src(
|
|
4775
|
+
"fn fib(n) { if (n < 2) { return n } return fib(n - 1) + fib(n - 2) }\nfib(15)",
|
|
4776
|
+
);
|
|
4777
|
+
set_max_call_depth_for_test(DEFAULT_MAX_CALL_DEPTH);
|
|
4778
|
+
assert!(
|
|
4779
|
+
out.is_ok(),
|
|
4780
|
+
"normal recursion must not be affected: {out:?}"
|
|
4781
|
+
);
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4784
|
+
// The core of #381 for the JIT tier: a pure-numeric self-recursive function JIT-compiles and
|
|
4785
|
+
// recurses on the native stack via SelfCall, BELOW the VM's depth counter (so `set_max_call_depth`
|
|
4786
|
+
// can't bound it). Without the guard this overflows the native stack — an uncatchable SIGSEGV/abort
|
|
4787
|
+
// that would kill the whole process. With it, the entry SP check turns the overflow into a
|
|
4788
|
+
// RangeError which, uncaught, must surface as a returned `Err`. Run on a bounded-stack thread so
|
|
4789
|
+
// the guard (or, on a regression, the overflow) is reached fast: a working guard lets the thread
|
|
4790
|
+
// `join` cleanly with the error flag; a broken one aborts the process (the loud regression signal).
|
|
4791
|
+
#[test]
|
|
4792
|
+
fn jit_deep_recursion_surfaces_error_not_abort() {
|
|
4793
|
+
// `Value` is `!Send` (holds `Rc`), so reduce to a Send bool inside the thread: true iff the
|
|
4794
|
+
// overflow surfaced as the catchable stack-overflow error rather than aborting.
|
|
4795
|
+
let handle = std::thread::Builder::new()
|
|
4796
|
+
.stack_size(1024 * 1024)
|
|
4797
|
+
.spawn(|| {
|
|
4798
|
+
matches!(
|
|
4799
|
+
run_src(
|
|
4800
|
+
"fn dive(n) { if (n <= 0.0) { return 0.0 } return dive(n - 1.0) + 1.0 }\n\
|
|
4801
|
+
dive(50000000.0)",
|
|
4802
|
+
),
|
|
4803
|
+
Err(ref e) if e.contains("call stack")
|
|
4804
|
+
)
|
|
4805
|
+
})
|
|
4806
|
+
.expect("spawn");
|
|
4807
|
+
let surfaced = handle
|
|
4808
|
+
.join()
|
|
4809
|
+
.expect("thread must not abort — the guard must catch the overflow");
|
|
4810
|
+
assert!(
|
|
4811
|
+
surfaced,
|
|
4812
|
+
"uncaught JIT deep recursion must surface as a RangeError, not abort"
|
|
4813
|
+
);
|
|
4814
|
+
}
|
|
4815
|
+
}
|