@tishlang/tish 2.12.0 → 2.16.13
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 +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -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 +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- 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 +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- 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 +75 -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/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_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -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_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
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.
|
|
@@ -199,13 +229,17 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
199
229
|
Value::Function(_) => raw,
|
|
200
230
|
Value::Object(ref obj) => {
|
|
201
231
|
let obj_ref = obj.borrow();
|
|
202
|
-
if let Some(Value::Function(on_worker)) =
|
|
203
|
-
|
|
232
|
+
if let Some(Value::Function(on_worker)) = obj_ref
|
|
233
|
+
.strings
|
|
234
|
+
.get(&std::sync::Arc::from("onWorker"))
|
|
235
|
+
.cloned()
|
|
204
236
|
{
|
|
205
237
|
let args_for_init = [Value::Number(0.0)];
|
|
206
238
|
on_worker.call(&args_for_init)
|
|
207
|
-
} else if let Some(h) =
|
|
208
|
-
|
|
239
|
+
} else if let Some(h) = obj_ref
|
|
240
|
+
.strings
|
|
241
|
+
.get(&std::sync::Arc::from("handler"))
|
|
242
|
+
.cloned()
|
|
209
243
|
{
|
|
210
244
|
h
|
|
211
245
|
} else {
|
|
@@ -263,8 +297,14 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
263
297
|
"exec" => Some(Value::native(|args: &[Value]| {
|
|
264
298
|
tishlang_runtime::process_exec(args)
|
|
265
299
|
})),
|
|
300
|
+
"execFile" => Some(Value::native(|args: &[Value]| {
|
|
301
|
+
tishlang_runtime::process_exec_file(args)
|
|
302
|
+
})),
|
|
266
303
|
"argv" => Some(Value::Array(VmRef::new(
|
|
267
|
-
tishlang_core::process_argv()
|
|
304
|
+
tishlang_core::process_argv()
|
|
305
|
+
.into_iter()
|
|
306
|
+
.map(|s| Value::String(s.into()))
|
|
307
|
+
.collect(),
|
|
268
308
|
))),
|
|
269
309
|
"env" => Some(value_object_from_map(
|
|
270
310
|
std::env::vars()
|
|
@@ -285,10 +325,17 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
285
325
|
"exec".into(),
|
|
286
326
|
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
|
|
287
327
|
);
|
|
328
|
+
m.insert(
|
|
329
|
+
"execFile".into(),
|
|
330
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exec_file(args)),
|
|
331
|
+
);
|
|
288
332
|
m.insert(
|
|
289
333
|
"argv".into(),
|
|
290
334
|
Value::Array(VmRef::new(
|
|
291
|
-
tishlang_core::process_argv()
|
|
335
|
+
tishlang_core::process_argv()
|
|
336
|
+
.into_iter()
|
|
337
|
+
.map(|s| Value::String(s.into()))
|
|
338
|
+
.collect(),
|
|
292
339
|
)),
|
|
293
340
|
);
|
|
294
341
|
m.insert(
|
|
@@ -331,8 +378,12 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
331
378
|
#[cfg(feature = "tty")]
|
|
332
379
|
if spec == "tish:tty" && cap_allows(enabled, "tty") {
|
|
333
380
|
return match export_name {
|
|
334
|
-
"size" => Some(Value::native(|args: &[Value]|
|
|
335
|
-
|
|
381
|
+
"size" => Some(Value::native(|args: &[Value]| {
|
|
382
|
+
tishlang_runtime::tty_size(args)
|
|
383
|
+
})),
|
|
384
|
+
"isTTY" => Some(Value::native(|args: &[Value]| {
|
|
385
|
+
tishlang_runtime::tty_is_tty(args)
|
|
386
|
+
})),
|
|
336
387
|
"setRawMode" => Some(Value::native(|args: &[Value]| {
|
|
337
388
|
tishlang_runtime::tty_set_raw_mode(args)
|
|
338
389
|
})),
|
|
@@ -342,7 +393,9 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
342
393
|
"leaveAltScreen" => Some(Value::native(|args: &[Value]| {
|
|
343
394
|
tishlang_runtime::tty_leave_alt_screen(args)
|
|
344
395
|
})),
|
|
345
|
-
"read" => Some(Value::native(|args: &[Value]|
|
|
396
|
+
"read" => Some(Value::native(|args: &[Value]| {
|
|
397
|
+
tishlang_runtime::tty_read(args)
|
|
398
|
+
})),
|
|
346
399
|
"readLine" => Some(Value::native(|args: &[Value]| {
|
|
347
400
|
tishlang_runtime::tty_read_line(args)
|
|
348
401
|
})),
|
|
@@ -642,10 +695,7 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
642
695
|
Value::String(v.type_name().into())
|
|
643
696
|
}),
|
|
644
697
|
);
|
|
645
|
-
g.insert(
|
|
646
|
-
"Symbol".into(),
|
|
647
|
-
tishlang_builtins::symbol::symbol_object(),
|
|
648
|
-
);
|
|
698
|
+
g.insert("Symbol".into(), tishlang_builtins::symbol::symbol_object());
|
|
649
699
|
|
|
650
700
|
// Date - full constructor (new Date(...)) plus statics now()/parse()/UTC().
|
|
651
701
|
g.insert(
|
|
@@ -666,14 +716,38 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
666
716
|
"Float64Array",
|
|
667
717
|
tishlang_builtins::typedarrays::float64_array_constructor_value as fn() -> Value,
|
|
668
718
|
),
|
|
669
|
-
(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
719
|
+
(
|
|
720
|
+
"Float32Array",
|
|
721
|
+
tishlang_builtins::typedarrays::float32_array_constructor_value,
|
|
722
|
+
),
|
|
723
|
+
(
|
|
724
|
+
"Int8Array",
|
|
725
|
+
tishlang_builtins::typedarrays::int8_array_constructor_value,
|
|
726
|
+
),
|
|
727
|
+
(
|
|
728
|
+
"Uint8Array",
|
|
729
|
+
tishlang_builtins::typedarrays::uint8_array_constructor_value,
|
|
730
|
+
),
|
|
731
|
+
(
|
|
732
|
+
"Uint8ClampedArray",
|
|
733
|
+
tishlang_builtins::typedarrays::uint8_clamped_array_constructor_value,
|
|
734
|
+
),
|
|
735
|
+
(
|
|
736
|
+
"Int16Array",
|
|
737
|
+
tishlang_builtins::typedarrays::int16_array_constructor_value,
|
|
738
|
+
),
|
|
739
|
+
(
|
|
740
|
+
"Uint16Array",
|
|
741
|
+
tishlang_builtins::typedarrays::uint16_array_constructor_value,
|
|
742
|
+
),
|
|
743
|
+
(
|
|
744
|
+
"Int32Array",
|
|
745
|
+
tishlang_builtins::typedarrays::int32_array_constructor_value,
|
|
746
|
+
),
|
|
747
|
+
(
|
|
748
|
+
"Uint32Array",
|
|
749
|
+
tishlang_builtins::typedarrays::uint32_array_constructor_value,
|
|
750
|
+
),
|
|
677
751
|
] {
|
|
678
752
|
g.insert(name.into(), ctor());
|
|
679
753
|
}
|
|
@@ -683,7 +757,10 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
683
757
|
);
|
|
684
758
|
// Error constructors (issue #60): `new Error(msg)` / `Error(msg)` → `{ name, message }`.
|
|
685
759
|
for name in ["Error", "TypeError", "RangeError", "SyntaxError"] {
|
|
686
|
-
g.insert(
|
|
760
|
+
g.insert(
|
|
761
|
+
name.into(),
|
|
762
|
+
construct_builtin::error_constructor_value(name),
|
|
763
|
+
);
|
|
687
764
|
}
|
|
688
765
|
|
|
689
766
|
// Object methods - delegate to tishlang_builtins::globals
|
|
@@ -785,10 +862,17 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
785
862
|
"exec".into(),
|
|
786
863
|
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
|
|
787
864
|
);
|
|
865
|
+
process_obj.insert(
|
|
866
|
+
"execFile".into(),
|
|
867
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exec_file(args)),
|
|
868
|
+
);
|
|
788
869
|
process_obj.insert(
|
|
789
870
|
"argv".into(),
|
|
790
871
|
Value::Array(VmRef::new(
|
|
791
|
-
tishlang_core::process_argv()
|
|
872
|
+
tishlang_core::process_argv()
|
|
873
|
+
.into_iter()
|
|
874
|
+
.map(|s| Value::String(s.into()))
|
|
875
|
+
.collect(),
|
|
792
876
|
)),
|
|
793
877
|
);
|
|
794
878
|
process_obj.insert(
|
|
@@ -860,13 +944,17 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
860
944
|
Value::Function(_) => raw,
|
|
861
945
|
Value::Object(ref obj) => {
|
|
862
946
|
let obj_ref = obj.borrow();
|
|
863
|
-
if let Some(Value::Function(on_worker)) =
|
|
864
|
-
|
|
947
|
+
if let Some(Value::Function(on_worker)) = obj_ref
|
|
948
|
+
.strings
|
|
949
|
+
.get(&std::sync::Arc::from("onWorker"))
|
|
950
|
+
.cloned()
|
|
865
951
|
{
|
|
866
952
|
let args_for_init = [Value::Number(0.0)];
|
|
867
953
|
on_worker.call(&args_for_init)
|
|
868
|
-
} else if let Some(h) =
|
|
869
|
-
|
|
954
|
+
} else if let Some(h) = obj_ref
|
|
955
|
+
.strings
|
|
956
|
+
.get(&std::sync::Arc::from("handler"))
|
|
957
|
+
.cloned()
|
|
870
958
|
{
|
|
871
959
|
h
|
|
872
960
|
} else {
|
|
@@ -967,24 +1055,66 @@ fn try_call_array_jit(
|
|
|
967
1055
|
arity: usize,
|
|
968
1056
|
mask: u8,
|
|
969
1057
|
) -> Option<Value> {
|
|
1058
|
+
let writable = nf.array_writable_mask();
|
|
970
1059
|
let mut numeric: Vec<f64> = Vec::with_capacity(arity);
|
|
971
1060
|
// `scratch` OWNS the extracted f64 data; handles point into it. Build handles only AFTER scratch is
|
|
972
1061
|
// fully populated so its backing buffers never reallocate out from under a live pointer.
|
|
973
1062
|
let mut scratch: Vec<Vec<f64>> = Vec::new();
|
|
974
|
-
|
|
1063
|
+
let mut array_arg: Vec<usize> = Vec::new(); // the `args` index each scratch buffer came from
|
|
1064
|
+
let mut is_bool_arr: Vec<bool> = Vec::new(); // #187: parallel to `scratch` — was it a Bool array?
|
|
1065
|
+
// #187: track whether ANY arg is a (non-empty) Bool array and whether ANY is a Number array. If both
|
|
1066
|
+
// element types are present the JIT could launder a bool value read from one array into a number
|
|
1067
|
+
// array (or vice versa) — the flattened `f64` writeback would then re-box it wrong — so we bail
|
|
1068
|
+
// (below). queens (all-bool arrays) and spectral_norm (all-number) never mix, so they stay JIT'd.
|
|
1069
|
+
let mut any_bool_arr = false;
|
|
1070
|
+
let mut any_num_arr = false;
|
|
1071
|
+
#[allow(clippy::needless_range_loop)]
|
|
1072
|
+
// `i` drives bit-mask math (`mask >> i`), not just indexing
|
|
975
1073
|
for i in 0..arity {
|
|
976
1074
|
if (mask >> i) & 1 == 1 {
|
|
977
1075
|
match &args[i] {
|
|
978
1076
|
Value::Array(a) => {
|
|
1077
|
+
// #187: a writable array must not alias another array arg — the JIT reads/writes a
|
|
1078
|
+
// private scratch copy, so aliasing would make reads see stale data or a writeback
|
|
1079
|
+
// silently drop the other param's writes. Bail (→ interpret) on any such overlap.
|
|
1080
|
+
if writable != 0 {
|
|
1081
|
+
for &j in &array_arg {
|
|
1082
|
+
if let Value::Array(other) = &args[j] {
|
|
1083
|
+
if VmRef::ptr_eq(a, other)
|
|
1084
|
+
&& ((writable >> i) & 1 == 1 || (writable >> j) & 1 == 1)
|
|
1085
|
+
{
|
|
1086
|
+
return None;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
// #187: accept an all-`Number` OR an all-`Bool` array (bool → `f64` 0/1, e.g.
|
|
1092
|
+
// queens' `cols`/`diag*`). A MIXED array is ambiguous for the typed writeback → bail.
|
|
979
1093
|
let b = a.borrow();
|
|
980
1094
|
let mut buf: Vec<f64> = Vec::with_capacity(b.len());
|
|
1095
|
+
let mut seen_bool = false;
|
|
1096
|
+
let mut seen_num = false;
|
|
981
1097
|
for el in b.iter() {
|
|
982
1098
|
match el {
|
|
983
|
-
Value::Number(n) =>
|
|
984
|
-
|
|
1099
|
+
Value::Number(n) => {
|
|
1100
|
+
seen_num = true;
|
|
1101
|
+
buf.push(*n);
|
|
1102
|
+
}
|
|
1103
|
+
Value::Bool(bl) => {
|
|
1104
|
+
seen_bool = true;
|
|
1105
|
+
buf.push(if *bl { 1.0 } else { 0.0 });
|
|
1106
|
+
}
|
|
1107
|
+
_ => return None, // non-numeric/non-bool element → interpreter
|
|
985
1108
|
}
|
|
986
1109
|
}
|
|
1110
|
+
if seen_bool && seen_num {
|
|
1111
|
+
return None; // mixed Bool/Number array → can't type the writeback
|
|
1112
|
+
}
|
|
1113
|
+
any_bool_arr |= seen_bool;
|
|
1114
|
+
any_num_arr |= seen_num;
|
|
987
1115
|
scratch.push(buf);
|
|
1116
|
+
array_arg.push(i);
|
|
1117
|
+
is_bool_arr.push(seen_bool);
|
|
988
1118
|
}
|
|
989
1119
|
_ => return None, // NumberArray / non-array → interpreter
|
|
990
1120
|
}
|
|
@@ -995,20 +1125,311 @@ fn try_call_array_jit(
|
|
|
995
1125
|
}
|
|
996
1126
|
}
|
|
997
1127
|
}
|
|
1128
|
+
// #187: mixed Bool + Number array args in one call could launder a value across element types (a bool
|
|
1129
|
+
// read from one array stored into a number array, or vice versa) — the flat `f64` writeback can't
|
|
1130
|
+
// re-box that correctly. Bail when both element types are present among the args.
|
|
1131
|
+
if any_bool_arr && any_num_arr {
|
|
1132
|
+
return None;
|
|
1133
|
+
}
|
|
1134
|
+
// #187: the writeback re-boxes a writable array by its ENTRY element type (bool vs number, per
|
|
1135
|
+
// `is_bool_arr`), so the value kind the JIT actually stored must match that type. A function that
|
|
1136
|
+
// writes bool consts (`arr[i] = true`) handed a NUMBER array — or one that writes numbers handed a
|
|
1137
|
+
// BOOL array — would re-box to the wrong type (e.g. `arr[i] = true` on `[0,0,0]` boxing `Number(1)`
|
|
1138
|
+
// where the interpreter stores `Bool(true)`). Bail to the interpreter on any such mismatch. (A single
|
|
1139
|
+
// array with mixed bool + non-bool writes was already rejected at compile time in `classify_params`.)
|
|
1140
|
+
let bool_write_mask = nf.array_bool_write_mask();
|
|
1141
|
+
if writable != 0 {
|
|
1142
|
+
for (k, &entry_bool) in is_bool_arr.iter().enumerate() {
|
|
1143
|
+
let i = array_arg[k];
|
|
1144
|
+
if (writable >> i) & 1 == 1 && entry_bool != ((bool_write_mask >> i) & 1 == 1) {
|
|
1145
|
+
return None;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// `as_mut_ptr`: #187 writable params store through this pointer, so it must carry mut provenance
|
|
1150
|
+
// (read-only params only load — harmless). Handles are built only AFTER `scratch` is fully
|
|
1151
|
+
// populated so its backing buffers never reallocate out from under a live pointer.
|
|
998
1152
|
let handles: Vec<crate::jit::ArrayHandle> = scratch
|
|
999
|
-
.
|
|
1153
|
+
.iter_mut()
|
|
1000
1154
|
.map(|buf| crate::jit::ArrayHandle {
|
|
1001
|
-
ptr: buf.
|
|
1155
|
+
ptr: buf.as_mut_ptr(),
|
|
1002
1156
|
len: buf.len(),
|
|
1003
1157
|
})
|
|
1004
1158
|
.collect();
|
|
1005
|
-
|
|
1159
|
+
// #187: a self-recursive array-mode function (queens' `place`) recurses on the NATIVE stack, so
|
|
1160
|
+
// pass a RecurGuard (like #381) — its entry SP-bail turns unbounded recursion into a catchable
|
|
1161
|
+
// RangeError instead of a SIGSEGV. Non-recursive array fns pass a null guard (unchanged ABI).
|
|
1162
|
+
let (res, deopt, tripped) = if nf.recur_guarded() {
|
|
1163
|
+
let anchor = 0u8;
|
|
1164
|
+
let current_sp = &anchor as *const u8 as usize;
|
|
1165
|
+
let stack_limit = match stacker::remaining_stack() {
|
|
1166
|
+
Some(rem) => {
|
|
1167
|
+
let margin = RECUR_STACK_MARGIN.min(rem / 2);
|
|
1168
|
+
current_sp.saturating_sub(rem).saturating_add(margin)
|
|
1169
|
+
}
|
|
1170
|
+
None => 0,
|
|
1171
|
+
};
|
|
1172
|
+
let mut guard = crate::jit::RecurGuard {
|
|
1173
|
+
stack_limit,
|
|
1174
|
+
tripped: 0,
|
|
1175
|
+
};
|
|
1176
|
+
let (res, deopt) = nf.call_arrays_guarded(&numeric, &handles, &mut guard);
|
|
1177
|
+
(res, deopt, guard.tripped != 0)
|
|
1178
|
+
} else {
|
|
1179
|
+
let (res, deopt) = nf.call_arrays(&numeric, &handles);
|
|
1180
|
+
(res, deopt, false)
|
|
1181
|
+
};
|
|
1006
1182
|
if deopt {
|
|
1007
|
-
return None; // OOB access → re-run interpreter (
|
|
1183
|
+
return None; // OOB access → re-run interpreter (the discarded scratch writes never escaped)
|
|
1184
|
+
}
|
|
1185
|
+
// #187: copy each WRITTEN array param's (possibly-mutated) scratch back into its `Value::Array`. The
|
|
1186
|
+
// length is unchanged (`SetIndex` can't grow; an OOB write deopts above), so this is a same-length
|
|
1187
|
+
// element overwrite. On a normal return this reflects the final state; on a recursion-guard TRIP
|
|
1188
|
+
// (below) it reflects the partial in-place mutations made before the overflow — matching the
|
|
1189
|
+
// interpreter/node, which likewise leak whatever was mutated when a deep recursion throws.
|
|
1190
|
+
if writable != 0 {
|
|
1191
|
+
for (k, buf) in scratch.iter().enumerate() {
|
|
1192
|
+
let i = array_arg[k];
|
|
1193
|
+
if (writable >> i) & 1 == 1 {
|
|
1194
|
+
if let Value::Array(a) = &args[i] {
|
|
1195
|
+
let bool_arr = is_bool_arr[k]; // #187: re-box a bool array's elements as Bool
|
|
1196
|
+
let mut b = a.borrow_mut();
|
|
1197
|
+
for (j, &v) in buf.iter().enumerate() {
|
|
1198
|
+
if let Some(slot) = b.get_mut(j) {
|
|
1199
|
+
*slot = if bool_arr {
|
|
1200
|
+
Value::Bool(v != 0.0)
|
|
1201
|
+
} else {
|
|
1202
|
+
Value::Number(v)
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// #187: a self-recursive array-mode fn that overflowed the native stack raises the same catchable
|
|
1211
|
+
// RangeError as every other tier (#381). The writeback above already flushed the partial mutations,
|
|
1212
|
+
// so the caller's arrays reflect the same "leaked" state the interpreter/node leave on a deep throw.
|
|
1213
|
+
if tripped {
|
|
1214
|
+
set_pending_throw(stack_overflow_error());
|
|
1215
|
+
return Some(Value::Null);
|
|
1216
|
+
}
|
|
1217
|
+
// #187: a VOID function (side-effect writer, e.g. `multiplyAv`) returns the implicit `null` — its
|
|
1218
|
+
// `f64` result is a dummy. Return `Value::Null` to match the interpreter; the real effect is the
|
|
1219
|
+
// array writeback above.
|
|
1220
|
+
if nf.returns_void() {
|
|
1221
|
+
return Some(Value::Null);
|
|
1008
1222
|
}
|
|
1009
1223
|
Some(Value::Number(res))
|
|
1010
1224
|
}
|
|
1011
1225
|
|
|
1226
|
+
/// #187 native HOF fusion. When `arr.map/filter/reduce/forEach(cb)` has a JIT-compiled pure-numeric
|
|
1227
|
+
/// callback (`cb` is a [`VmClosure`] with a plain register-`f64` [`crate::jit::NumericFn`]) AND the
|
|
1228
|
+
/// array is all-numeric, the whole higher-order call runs as a tight native loop calling the
|
|
1229
|
+
/// `NumericFn` per element — skipping the per-element boxed `Callable::call` dispatch AND the
|
|
1230
|
+
/// `Value` (un)boxing of each element that the generic `arr_builtins` path pays.
|
|
1231
|
+
///
|
|
1232
|
+
/// This is PURELY ADDITIVE: every helper returns `None` to fall back to the existing generic path
|
|
1233
|
+
/// (which is byte-identical to the interpreter), so a bail is always sound. The gate is deliberately
|
|
1234
|
+
/// narrow — see [`fused_numeric_fn`] and the per-helper arity/`result_bool` conditions.
|
|
1235
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1236
|
+
mod hof_fusion {
|
|
1237
|
+
use super::VmClosure;
|
|
1238
|
+
use tishlang_core::{Value, VmRef};
|
|
1239
|
+
|
|
1240
|
+
/// Extract the callback's fusable [`crate::jit::NumericFn`], if any. Fires ONLY for a `VmClosure`
|
|
1241
|
+
/// whose JIT is a plain pure-numeric register-`f64` function:
|
|
1242
|
+
/// * `array_param_mask() == 0` — not an array-mode ABI (that path reads `arr[i]`, needs handles);
|
|
1243
|
+
/// * `!is_jv()` — no function-local JIT arrays (those signal a deopt via a per-thread flag we'd
|
|
1244
|
+
/// have to poll — the generic path handles them correctly, so bail);
|
|
1245
|
+
/// * `!recur_guarded()` — not self-recursive (would need the `RecurGuard` trailing-ptr ABI; a
|
|
1246
|
+
/// lambda callback is never self-recursive, so this only ever excludes pathological input).
|
|
1247
|
+
///
|
|
1248
|
+
/// Any other callable (builtin native fn, non-JIT closure, array-mode/JV/recursive fn) → `None`.
|
|
1249
|
+
#[inline]
|
|
1250
|
+
pub(super) fn fused_numeric_fn(cb: &Value) -> Option<crate::jit::NumericFn> {
|
|
1251
|
+
let Value::Function(f) = cb else { return None };
|
|
1252
|
+
let vc = f.as_any().downcast_ref::<VmClosure>()?;
|
|
1253
|
+
let nf = vc.jit_fn?;
|
|
1254
|
+
if nf.array_param_mask() != 0 || nf.is_jv() || nf.recur_guarded() {
|
|
1255
|
+
return None;
|
|
1256
|
+
}
|
|
1257
|
+
Some(nf)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/// Snapshot an all-numeric array as a `Vec<f64>`. Accepts a packed [`Value::NumberArray`] and a
|
|
1261
|
+
/// boxed [`Value::Array`] whose every element is a [`Value::Number`]. Returns `None` for any other
|
|
1262
|
+
/// shape (mixed/boxed non-number element, a non-array) so the caller falls back to the generic
|
|
1263
|
+
/// path. Snapshots (clones) so — exactly like the generic `arr_builtins` path (#382) — a callback
|
|
1264
|
+
/// that re-enters the same array observes the pre-call contents and can never deadlock the borrow.
|
|
1265
|
+
#[inline]
|
|
1266
|
+
fn numeric_snapshot(arr: &Value) -> Option<Vec<f64>> {
|
|
1267
|
+
match arr {
|
|
1268
|
+
Value::NumberArray(a) => Some(a.borrow().clone()),
|
|
1269
|
+
Value::Array(a) => {
|
|
1270
|
+
let b = a.borrow();
|
|
1271
|
+
let mut out = Vec::with_capacity(b.len());
|
|
1272
|
+
for v in b.iter() {
|
|
1273
|
+
match v {
|
|
1274
|
+
Value::Number(n) => out.push(*n),
|
|
1275
|
+
_ => return None,
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
Some(out)
|
|
1279
|
+
}
|
|
1280
|
+
_ => None,
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/// Box a `NumericFn` f64 result the SAME way the interpreter/generic path does: as `Value::Bool`
|
|
1285
|
+
/// when the callback's result is a comparison (`result_is_bool`), else `Value::Number`. A bool
|
|
1286
|
+
/// `NumericFn` returns exactly `0.0`/`1.0`, so `r != 0.0` is exact.
|
|
1287
|
+
#[inline]
|
|
1288
|
+
fn box_result(nf: &crate::jit::NumericFn, r: f64) -> Value {
|
|
1289
|
+
if nf.result_is_bool() {
|
|
1290
|
+
Value::Bool(r != 0.0)
|
|
1291
|
+
} else {
|
|
1292
|
+
Value::Number(r)
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/// Truthiness of a `NumericFn` result under tish's rules — matches `Value::is_truthy` on the
|
|
1297
|
+
/// boxed form: a number is truthy iff `!= 0` and not NaN; a bool-result fn returns `0.0`/`1.0`
|
|
1298
|
+
/// (never NaN) so the same test is exact for it too.
|
|
1299
|
+
#[inline]
|
|
1300
|
+
fn result_truthy(r: f64) -> bool {
|
|
1301
|
+
r != 0.0 && !r.is_nan()
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/// Fused `map`. Fires when the callback is a fusable numeric fn of arity ≤ 2 (it may read the
|
|
1305
|
+
/// element and the index; a 3rd `array` param can't be a number, so arity 3+ bails). Returns the
|
|
1306
|
+
/// SAME `Value` variant the generic path would: a numeric-returning map over a `NumberArray` yields
|
|
1307
|
+
/// a packed `NumberArray` (empty → boxed empty `Array`, matching `packed_or_empty`); a
|
|
1308
|
+
/// bool-returning map, or any map over a boxed `Array`, yields a boxed `Value::Array`.
|
|
1309
|
+
pub(super) fn map(arr: &Value, cb: &Value) -> Option<Value> {
|
|
1310
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1311
|
+
if nf.arity() > 2 {
|
|
1312
|
+
return None; // 3rd param is the array arg — not a number; bail (sound).
|
|
1313
|
+
}
|
|
1314
|
+
let data = numeric_snapshot(arr)?;
|
|
1315
|
+
let arity = nf.arity();
|
|
1316
|
+
let packed_input = matches!(arr, Value::NumberArray(_));
|
|
1317
|
+
// A numeric-returning map over a packed input keeps its result packed (byte-identical to
|
|
1318
|
+
// `packed_or_empty`); every other case (bool result, or a boxed `Array` input) boxes.
|
|
1319
|
+
if !nf.result_is_bool() && packed_input {
|
|
1320
|
+
let mut out: Vec<f64> = Vec::with_capacity(data.len());
|
|
1321
|
+
let mut args = [0.0f64; 2];
|
|
1322
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1323
|
+
args[0] = n;
|
|
1324
|
+
args[1] = i as f64;
|
|
1325
|
+
out.push(nf.call(&args[..arity]));
|
|
1326
|
+
}
|
|
1327
|
+
// Empty → boxed empty `Array`, matching `packed_or_empty`.
|
|
1328
|
+
if out.is_empty() {
|
|
1329
|
+
return Some(Value::Array(VmRef::new(Vec::new())));
|
|
1330
|
+
}
|
|
1331
|
+
return Some(Value::number_array(out));
|
|
1332
|
+
}
|
|
1333
|
+
let mut out: Vec<Value> = Vec::with_capacity(data.len());
|
|
1334
|
+
let mut args = [0.0f64; 2];
|
|
1335
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1336
|
+
args[0] = n;
|
|
1337
|
+
args[1] = i as f64;
|
|
1338
|
+
out.push(box_result(&nf, nf.call(&args[..arity])));
|
|
1339
|
+
}
|
|
1340
|
+
Some(Value::Array(VmRef::new(out)))
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/// Fused `filter`. Callback is a predicate of arity ≤ 2. Keeps the ORIGINAL element (not the
|
|
1344
|
+
/// callback result) when the result is truthy — so the output is always a subset of the numeric
|
|
1345
|
+
/// input. Packed input → packed output (`packed_or_empty`); boxed `Array` input → boxed `Array`.
|
|
1346
|
+
pub(super) fn filter(arr: &Value, cb: &Value) -> Option<Value> {
|
|
1347
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1348
|
+
if nf.arity() > 2 {
|
|
1349
|
+
return None;
|
|
1350
|
+
}
|
|
1351
|
+
let data = numeric_snapshot(arr)?;
|
|
1352
|
+
let arity = nf.arity();
|
|
1353
|
+
let packed_input = matches!(arr, Value::NumberArray(_));
|
|
1354
|
+
let mut args = [0.0f64; 2];
|
|
1355
|
+
if packed_input {
|
|
1356
|
+
let mut out: Vec<f64> = Vec::new();
|
|
1357
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1358
|
+
args[0] = n;
|
|
1359
|
+
args[1] = i as f64;
|
|
1360
|
+
if result_truthy(nf.call(&args[..arity])) {
|
|
1361
|
+
out.push(n);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if out.is_empty() {
|
|
1365
|
+
return Some(Value::Array(VmRef::new(Vec::new())));
|
|
1366
|
+
}
|
|
1367
|
+
return Some(Value::number_array(out));
|
|
1368
|
+
}
|
|
1369
|
+
let mut out: Vec<Value> = Vec::new();
|
|
1370
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1371
|
+
args[0] = n;
|
|
1372
|
+
args[1] = i as f64;
|
|
1373
|
+
if result_truthy(nf.call(&args[..arity])) {
|
|
1374
|
+
out.push(Value::Number(n));
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
Some(Value::Array(VmRef::new(out)))
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/// Fused `reduce`. Callback is `(acc, element, index)` — arity ≤ 3 (a 4th `array` param bails).
|
|
1381
|
+
/// The accumulator stays an `f64` across the whole fold, so the callback result must NOT be a bool
|
|
1382
|
+
/// (`!result_is_bool()`): a bool acc fed back into the numeric fn would diverge from the interpreter
|
|
1383
|
+
/// (which would pass a `Value::Bool`, not a number). No-initial-value semantics match the generic
|
|
1384
|
+
/// path: absent init (`Value::Null`) with a non-empty array seeds `acc = data[0]` and scans from
|
|
1385
|
+
/// index 1; an empty array with no init throws (bail to the generic path, which raises it).
|
|
1386
|
+
pub(super) fn reduce(arr: &Value, cb: &Value, init: &Value) -> Option<Value> {
|
|
1387
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1388
|
+
if nf.arity() > 3 || nf.result_is_bool() {
|
|
1389
|
+
return None;
|
|
1390
|
+
}
|
|
1391
|
+
let data = numeric_snapshot(arr)?;
|
|
1392
|
+
let arity = nf.arity();
|
|
1393
|
+
// Determine seed. Reduce with no initial value AND an empty array is a TypeError in JS — let
|
|
1394
|
+
// the generic path produce that exact throw rather than replicate it here.
|
|
1395
|
+
let (start, mut acc) = match init {
|
|
1396
|
+
Value::Null if !data.is_empty() => (1usize, data[0]),
|
|
1397
|
+
Value::Null => return None, // empty + no init → generic path throws
|
|
1398
|
+
Value::Number(n) => (0usize, *n),
|
|
1399
|
+
_ => return None, // non-numeric explicit init → generic path (acc wouldn't stay f64)
|
|
1400
|
+
};
|
|
1401
|
+
let mut args = [0.0f64; 3];
|
|
1402
|
+
for (i, &x) in data.iter().enumerate().skip(start) {
|
|
1403
|
+
args[0] = acc;
|
|
1404
|
+
args[1] = x;
|
|
1405
|
+
args[2] = i as f64;
|
|
1406
|
+
acc = nf.call(&args[..arity]);
|
|
1407
|
+
}
|
|
1408
|
+
Some(Value::Number(acc))
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/// Fused `forEach`. Callback arity ≤ 2; result discarded; always returns `Value::Null`. Only worth
|
|
1412
|
+
/// fusing when the callback is side-effect-free numeric arithmetic — but a `NumericFn` is pure by
|
|
1413
|
+
/// construction (no calls/member/throw), so a fused `forEach` is observably a no-op EXCEPT for its
|
|
1414
|
+
/// (absent) side effects: it computes and discards. Kept for completeness / uniformity; it can
|
|
1415
|
+
/// never diverge because there is nothing observable to diverge on.
|
|
1416
|
+
pub(super) fn for_each(arr: &Value, cb: &Value) -> Option<Value> {
|
|
1417
|
+
let nf = fused_numeric_fn(cb)?;
|
|
1418
|
+
if nf.arity() > 2 {
|
|
1419
|
+
return None;
|
|
1420
|
+
}
|
|
1421
|
+
let data = numeric_snapshot(arr)?;
|
|
1422
|
+
let arity = nf.arity();
|
|
1423
|
+
let mut args = [0.0f64; 2];
|
|
1424
|
+
for (i, &n) in data.iter().enumerate() {
|
|
1425
|
+
args[0] = n;
|
|
1426
|
+
args[1] = i as f64;
|
|
1427
|
+
let _ = nf.call(&args[..arity]);
|
|
1428
|
+
}
|
|
1429
|
+
Some(Value::Null)
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1012
1433
|
struct VmClosure {
|
|
1013
1434
|
chunk: Arc<Chunk>,
|
|
1014
1435
|
/// Whether this closure can run on the frame stack — computed ONCE at creation (eligibility is an
|
|
@@ -1044,12 +1465,66 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1044
1465
|
}
|
|
1045
1466
|
}
|
|
1046
1467
|
if all_numbers {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1468
|
+
// #189: a JV function has function-local `f64` arrays lowered to
|
|
1469
|
+
// `tish_jv_*` calls over a per-thread arena. An out-of-bounds access (or a
|
|
1470
|
+
// non-numeric return) sets a per-thread deopt flag; we discard the numeric
|
|
1471
|
+
// result and re-run the interpreter. Sound because JV arrays never escape
|
|
1472
|
+
// the function — nothing observable was mutated, so re-execution
|
|
1473
|
+
// reproduces identical behaviour.
|
|
1474
|
+
if nf.is_jv() {
|
|
1475
|
+
crate::jit::jv_reset_deopt();
|
|
1476
|
+
let r = nf.call(&nums[..arity]);
|
|
1477
|
+
if !crate::jit::jv_take_deopt() {
|
|
1478
|
+
return if nf.result_is_bool() {
|
|
1479
|
+
Value::Bool(r != 0.0)
|
|
1480
|
+
} else {
|
|
1481
|
+
Value::Number(r)
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
// deopt ⇒ fall through to the interpreter below.
|
|
1050
1485
|
} else {
|
|
1051
|
-
|
|
1052
|
-
|
|
1486
|
+
// #381: a self-recursive JIT'd function carries a RecurGuard so it bails
|
|
1487
|
+
// (rather than overflowing the native stack) when the recursion nears the
|
|
1488
|
+
// real remaining stack. On a bail we raise the same catchable RangeError as
|
|
1489
|
+
// the non-JIT paths — tish's deopt tier producing the throw, not the JIT.
|
|
1490
|
+
let res = if nf.recur_guarded() {
|
|
1491
|
+
let anchor = 0u8;
|
|
1492
|
+
let current_sp = &anchor as *const u8 as usize;
|
|
1493
|
+
// `stack_limit` = the stack address below which we bail, leaving a
|
|
1494
|
+
// headroom margin. If the remaining stack is unknown we pass 0 (SP is
|
|
1495
|
+
// never below 0 → never trips), i.e. behave as before.
|
|
1496
|
+
let stack_limit = match stacker::remaining_stack() {
|
|
1497
|
+
Some(rem) => {
|
|
1498
|
+
// Cap the margin at half of what's left: when a JIT'd function
|
|
1499
|
+
// is first entered on an already-deep stack (rem < margin),
|
|
1500
|
+
// `bottom + margin` would sit ABOVE the current SP and trip on
|
|
1501
|
+
// the very first call — a false positive on shallow recursion.
|
|
1502
|
+
// Capping keeps the limit strictly below SP for any rem, while
|
|
1503
|
+
// still bailing with real headroom to spare.
|
|
1504
|
+
let margin = RECUR_STACK_MARGIN.min(rem / 2);
|
|
1505
|
+
current_sp.saturating_sub(rem).saturating_add(margin)
|
|
1506
|
+
}
|
|
1507
|
+
None => 0,
|
|
1508
|
+
};
|
|
1509
|
+
let mut guard = crate::jit::RecurGuard {
|
|
1510
|
+
stack_limit,
|
|
1511
|
+
tripped: 0,
|
|
1512
|
+
};
|
|
1513
|
+
let r = nf.call_guarded(&nums[..arity], &mut guard);
|
|
1514
|
+
if guard.tripped != 0 {
|
|
1515
|
+
set_pending_throw(stack_overflow_error());
|
|
1516
|
+
return Value::Null;
|
|
1517
|
+
}
|
|
1518
|
+
r
|
|
1519
|
+
} else {
|
|
1520
|
+
nf.call(&nums[..arity])
|
|
1521
|
+
};
|
|
1522
|
+
return if nf.result_is_bool() {
|
|
1523
|
+
Value::Bool(res != 0.0)
|
|
1524
|
+
} else {
|
|
1525
|
+
Value::Number(res)
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1053
1528
|
}
|
|
1054
1529
|
} else if let Some(v) = try_call_array_jit(&nf, args, arity, mask) {
|
|
1055
1530
|
// Array-mode path: succeeded (all-numeric arrays, in-bounds). On any bail
|
|
@@ -1060,6 +1535,16 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1060
1535
|
}
|
|
1061
1536
|
}
|
|
1062
1537
|
}
|
|
1538
|
+
// #381: bound recursion so a runaway recursive closure throws a catchable RangeError rather
|
|
1539
|
+
// than overflowing the native stack (this recursive re-entry is the DEFAULT `tish run` path).
|
|
1540
|
+
// The counter is thread-local because each closure call builds a fresh `Vm`; the parked throw
|
|
1541
|
+
// is picked up by the caller's `take_pending_throw()` check (Call/SelfCall post-call).
|
|
1542
|
+
let depth = tishlang_core::inc_call_depth();
|
|
1543
|
+
if depth > max_call_depth() {
|
|
1544
|
+
tishlang_core::dec_call_depth();
|
|
1545
|
+
set_pending_throw(stack_overflow_error());
|
|
1546
|
+
return Value::Null;
|
|
1547
|
+
}
|
|
1063
1548
|
let mut vm = Vm {
|
|
1064
1549
|
stack: Vec::new(),
|
|
1065
1550
|
scope: ObjectMap::default(),
|
|
@@ -1069,17 +1554,19 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1069
1554
|
native_modules: self.native_modules.clone(),
|
|
1070
1555
|
};
|
|
1071
1556
|
#[cfg(not(target_arch = "wasm32"))]
|
|
1072
|
-
{
|
|
1557
|
+
let result = {
|
|
1073
1558
|
stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
1074
1559
|
vm.run_chunk(self.chunk.as_ref(), &self.chunk.nested, args, false)
|
|
1075
1560
|
.unwrap_or(Value::Null)
|
|
1076
1561
|
})
|
|
1077
|
-
}
|
|
1562
|
+
};
|
|
1078
1563
|
#[cfg(target_arch = "wasm32")]
|
|
1079
|
-
{
|
|
1564
|
+
let result = {
|
|
1080
1565
|
vm.run_chunk(&self.chunk, &self.chunk.nested, args, false)
|
|
1081
1566
|
.unwrap_or(Value::Null)
|
|
1082
|
-
}
|
|
1567
|
+
};
|
|
1568
|
+
tishlang_core::dec_call_depth();
|
|
1569
|
+
result
|
|
1083
1570
|
}
|
|
1084
1571
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
1085
1572
|
self
|
|
@@ -1170,6 +1657,12 @@ impl Vm {
|
|
|
1170
1657
|
|
|
1171
1658
|
/// Run a chunk using this VM's capability set. `repl_mode` persists top-level `let` across REPL lines.
|
|
1172
1659
|
pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
|
|
1660
|
+
// #187: the directly-callable-callee registry is scoped to ONE top-level program run. Clear it
|
|
1661
|
+
// here (the outermost entry — closures run via `run_chunk`, not this) so a long-lived process
|
|
1662
|
+
// (REPL / embedder) running a NEW program never resolves a stale callee registered by a prior
|
|
1663
|
+
// one. A cross-caller and its callee always compile within the same run, so this is sufficient.
|
|
1664
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1665
|
+
crate::jit::reset_callees();
|
|
1173
1666
|
let result = self.run_chunk(chunk, &chunk.nested, &[], repl_mode);
|
|
1174
1667
|
// A throw that escaped every `catch` reaches here as the pending-throw sentinel; turn the
|
|
1175
1668
|
// parked value into the conventional uncaught-error message (issue #60).
|
|
@@ -1192,7 +1685,11 @@ impl Vm {
|
|
|
1192
1685
|
// regression to the DEFAULT path — caching makes the flag-off check a single atomic load.
|
|
1193
1686
|
use std::sync::OnceLock;
|
|
1194
1687
|
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
1195
|
-
*ENABLED.get_or_init(||
|
|
1688
|
+
*ENABLED.get_or_init(|| {
|
|
1689
|
+
std::env::var("TISH_FRAME_VM")
|
|
1690
|
+
.map(|v| v == "1")
|
|
1691
|
+
.unwrap_or(false)
|
|
1692
|
+
})
|
|
1196
1693
|
}
|
|
1197
1694
|
|
|
1198
1695
|
/// A `VmClosure` runs on the frame stack iff its chunk is frame-eligible AND it has no numeric
|
|
@@ -1245,6 +1742,64 @@ impl Vm {
|
|
|
1245
1742
|
true
|
|
1246
1743
|
}
|
|
1247
1744
|
|
|
1745
|
+
/// On-stack replacement (#190): run one hot loop region natively, or report why we can't.
|
|
1746
|
+
///
|
|
1747
|
+
/// Called from the frame VM's `JumpBack` handler once a loop's back-edge counter trips. Compiles
|
|
1748
|
+
/// (cached) the region `[header_ip, region_end)`; if it is a pure-numeric slot loop AND every live
|
|
1749
|
+
/// slot currently holds a `Number` (else the native f64 math would diverge from the interpreter),
|
|
1750
|
+
/// copies the live-ins into an f64 buffer, runs the whole remaining loop natively, writes the
|
|
1751
|
+
/// live-outs back as `Number`s, and returns the chunk `ip` of the loop exit to resume dispatch at.
|
|
1752
|
+
///
|
|
1753
|
+
/// Soundness: the v1 region whitelist is pure slot/stack arithmetic — no calls, no member/index,
|
|
1754
|
+
/// no object or array mutation — so nothing observable happens inside the region, and the
|
|
1755
|
+
/// interpreter running the same bytecode from the same numeric live-ins produces bit-identical
|
|
1756
|
+
/// slots (the `emit_simple_op` lowering matches `eval_binop`). A non-numeric live-in is the deopt
|
|
1757
|
+
/// case: we simply don't OSR and keep interpreting, so state is never corrupted.
|
|
1758
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1759
|
+
fn run_osr(
|
|
1760
|
+
chunk: &Chunk,
|
|
1761
|
+
header_ip: usize,
|
|
1762
|
+
region_end: usize,
|
|
1763
|
+
slots: &mut [Value],
|
|
1764
|
+
slot_base: usize,
|
|
1765
|
+
) -> OsrResult {
|
|
1766
|
+
let loopfn = match crate::jit::try_compile_loop(chunk, header_ip, region_end) {
|
|
1767
|
+
Some(lf) => lf,
|
|
1768
|
+
None => return OsrResult::NotCompilable,
|
|
1769
|
+
};
|
|
1770
|
+
let mut buf: Vec<f64> = Vec::with_capacity(loopfn.used_slots.len());
|
|
1771
|
+
for &slot in &loopfn.used_slots {
|
|
1772
|
+
match slots.get(slot_base + slot as usize) {
|
|
1773
|
+
Some(Value::Number(n)) => buf.push(*n),
|
|
1774
|
+
_ => return OsrResult::LiveInMiss, // a live slot is non-numeric → deopt, keep interpreting
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
let mut deopt: u8 = 0;
|
|
1778
|
+
// SAFETY: `buf` is a valid `[f64; used_slots.len()]`; `deopt` is a valid `*mut u8`. The region
|
|
1779
|
+
// was compiled for exactly this chunk+header (fingerprint-checked in the cache).
|
|
1780
|
+
let exit_id = loopfn.call(&mut buf, &mut deopt);
|
|
1781
|
+
for (p, &slot) in loopfn.used_slots.iter().enumerate() {
|
|
1782
|
+
if let Some(d) = slots.get_mut(slot_base + slot as usize) {
|
|
1783
|
+
*d = Value::Number(buf[p]);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
match loopfn.exits.get(exit_id as usize) {
|
|
1787
|
+
Some(&ip) => OsrResult::Compiled(ip),
|
|
1788
|
+
// Out-of-range exit id is impossible (the region returns an in-range id), but if it ever
|
|
1789
|
+
// happened the slots are already a consistent post-loop state, so re-interpreting the
|
|
1790
|
+
// (now-false) loop header exits correctly.
|
|
1791
|
+
None => OsrResult::LiveInMiss,
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/// OSR is **default ON**; `TISH_OSR=0` disables it (escape hatch, mirrors `TISH_JIT_ARRAYS`).
|
|
1796
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1797
|
+
#[inline]
|
|
1798
|
+
fn osr_enabled() -> bool {
|
|
1799
|
+
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
|
1800
|
+
*ENABLED.get_or_init(|| std::env::var("TISH_OSR").map(|v| v != "0").unwrap_or(true))
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1248
1803
|
/// Iterative frame-stack execution of a frame-eligible `VmClosure` (the frame-VM, flag-on).
|
|
1249
1804
|
/// Returns `None` if the entry chunk is ineligible (caller falls back to `VmClosure::call`).
|
|
1250
1805
|
/// Calls + recursion run on the heap `frames` stack — no per-call `Vm`, no recursive `run_chunk`
|
|
@@ -1264,6 +1819,14 @@ impl Vm {
|
|
|
1264
1819
|
let mut slots: Vec<Value> = Vec::new();
|
|
1265
1820
|
let mut slot_base: usize = 0;
|
|
1266
1821
|
slots.resize(cur.num_slots as usize, Value::Null);
|
|
1822
|
+
// #190 OSR: per-`(chunk, loop header)` back-edge counters (`u32::MAX` = gave up), with a
|
|
1823
|
+
// single-entry fast slot for the loop currently spinning (see the `run_chunk` copy for the
|
|
1824
|
+
// rationale) so a resolved hot loop stays off the HashMap.
|
|
1825
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1826
|
+
let mut osr_counters: std::collections::HashMap<(usize, usize), u32> =
|
|
1827
|
+
std::collections::HashMap::new();
|
|
1828
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1829
|
+
let mut osr_hot: ((usize, usize), u32) = ((usize::MAX, usize::MAX), 0);
|
|
1267
1830
|
for i in 0..(cur.param_count as usize) {
|
|
1268
1831
|
if let Some(v) = args.get(i) {
|
|
1269
1832
|
if let Some(d) = slots.get_mut(slot_base + i) {
|
|
@@ -1339,7 +1902,9 @@ impl Vm {
|
|
|
1339
1902
|
let idx = Self::read_u16(code, &mut ip) as usize;
|
|
1340
1903
|
let v = match cur.constants.get(idx) {
|
|
1341
1904
|
Some(Constant::Number(n)) => Value::Number(*n),
|
|
1342
|
-
Some(Constant::String(s)) =>
|
|
1905
|
+
Some(Constant::String(s)) => {
|
|
1906
|
+
Value::String(tishlang_core::ArcStr::from(s.as_ref()))
|
|
1907
|
+
}
|
|
1343
1908
|
Some(Constant::Bool(b)) => Value::Bool(*b),
|
|
1344
1909
|
Some(Constant::Null) => Value::Null,
|
|
1345
1910
|
_ => ferr!("Ineligible constant {} in run_framed", idx),
|
|
@@ -1388,6 +1953,41 @@ impl Vm {
|
|
|
1388
1953
|
}
|
|
1389
1954
|
Opcode::JumpBack => {
|
|
1390
1955
|
let dist = Self::read_u16(code, &mut ip) as usize;
|
|
1956
|
+
// #190 OSR: `ip` now points just past the JumpBack (region end); the loop header is
|
|
1957
|
+
// `region_end - dist`. Once a loop is hot, try to run its remaining iterations in
|
|
1958
|
+
// native code (numeric slot loops only; anything else falls straight through).
|
|
1959
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1960
|
+
if Self::osr_enabled() {
|
|
1961
|
+
let region_end = ip;
|
|
1962
|
+
let header_ip = ip.saturating_sub(dist);
|
|
1963
|
+
let key = (Arc::as_ptr(&cur) as usize, header_ip);
|
|
1964
|
+
if key != osr_hot.0 {
|
|
1965
|
+
if osr_hot.0 .0 != usize::MAX {
|
|
1966
|
+
osr_counters.insert(osr_hot.0, osr_hot.1);
|
|
1967
|
+
}
|
|
1968
|
+
osr_hot = (key, osr_counters.get(&key).copied().unwrap_or(0));
|
|
1969
|
+
}
|
|
1970
|
+
if osr_hot.1 != u32::MAX {
|
|
1971
|
+
osr_hot.1 = osr_hot.1.saturating_add(1);
|
|
1972
|
+
if osr_hot.1 >= OSR_THRESHOLD
|
|
1973
|
+
&& osr_hot
|
|
1974
|
+
.1
|
|
1975
|
+
.saturating_sub(OSR_THRESHOLD)
|
|
1976
|
+
.is_multiple_of(OSR_RETRY)
|
|
1977
|
+
{
|
|
1978
|
+
match Self::run_osr(
|
|
1979
|
+
&cur, header_ip, region_end, &mut slots, slot_base,
|
|
1980
|
+
) {
|
|
1981
|
+
OsrResult::Compiled(exit_ip) => {
|
|
1982
|
+
ip = exit_ip;
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
OsrResult::LiveInMiss => {}
|
|
1986
|
+
OsrResult::NotCompilable => osr_hot.1 = u32::MAX,
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1391
1991
|
ip = ip.saturating_sub(dist);
|
|
1392
1992
|
}
|
|
1393
1993
|
Opcode::Pop => {
|
|
@@ -1400,6 +2000,12 @@ impl Vm {
|
|
|
1400
2000
|
call_args.push(fpop!());
|
|
1401
2001
|
}
|
|
1402
2002
|
call_args.reverse();
|
|
2003
|
+
// #381: bound the heap frame stack (the opt-in frame-VM path grows a Vec instead
|
|
2004
|
+
// of the native stack, so unbounded recursion here is OOM rather than overflow).
|
|
2005
|
+
if frames.len() >= max_call_depth() {
|
|
2006
|
+
set_pending_throw(stack_overflow_error());
|
|
2007
|
+
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
2008
|
+
}
|
|
1403
2009
|
frames.push((cur.clone(), ip, slot_base, stack_base, enclosing.clone()));
|
|
1404
2010
|
let new_base = slots.len();
|
|
1405
2011
|
slots.resize(new_base + cur.num_slots as usize, Value::Null);
|
|
@@ -1435,6 +2041,10 @@ impl Vm {
|
|
|
1435
2041
|
// refcounts are unchanged (the chunk heap data doesn't move, so the
|
|
1436
2042
|
// laundered `code` ptr stays valid until rebind below). Halves the
|
|
1437
2043
|
// per-call Arc traffic vs cloning for the push.
|
|
2044
|
+
if frames.len() >= max_call_depth() {
|
|
2045
|
+
set_pending_throw(stack_overflow_error());
|
|
2046
|
+
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
2047
|
+
}
|
|
1438
2048
|
frames.push((cur, ip, slot_base, stack_base, enclosing));
|
|
1439
2049
|
cur = next_chunk;
|
|
1440
2050
|
enclosing = next_enc;
|
|
@@ -1528,6 +2138,35 @@ impl Vm {
|
|
|
1528
2138
|
// frame indexed by slot — no per-call hashmap, no name lookups. Args bind
|
|
1529
2139
|
// to slots 0..param_count. Empty for name-based chunks.
|
|
1530
2140
|
let mut slot_locals: Vec<Value> = Vec::new();
|
|
2141
|
+
// #190 OSR: per-loop-header back-edge counters for this frame (`u32::MAX` = gave up). `osr_hot`
|
|
2142
|
+
// is a single-entry fast slot for the loop currently spinning — its header is constant across
|
|
2143
|
+
// iterations, so the common hot loop never touches the HashMap (which is hit only when the
|
|
2144
|
+
// active loop changes: entry, exit, or a nested-loop switch). Keeps the per-back-edge cost of a
|
|
2145
|
+
// resolved loop to two integer compares.
|
|
2146
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2147
|
+
let mut osr_counters: std::collections::HashMap<usize, u32> =
|
|
2148
|
+
std::collections::HashMap::new();
|
|
2149
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2150
|
+
let mut osr_hot: (usize, u32) = (usize::MAX, 0);
|
|
2151
|
+
// Frame-local string builder for `acc += x` on a slot local (Opcode::AppendLocal): keeps the
|
|
2152
|
+
// accumulator in a growable `sb_buf` so appends are amortized O(1) instead of reallocating the
|
|
2153
|
+
// whole string each time. `sb_slot` is the slot currently buffered (at most one). The slot's
|
|
2154
|
+
// own value is stale while buffered; `sb_flush!()` writes the buffer back. Slots are
|
|
2155
|
+
// frame-private (never captured by closures and invisible to callees), and every read of a
|
|
2156
|
+
// slot goes through `LoadLocal`, so flushing there is sufficient for soundness — no
|
|
2157
|
+
// cross-frame state. JS string immutability is preserved: reads always see the full string.
|
|
2158
|
+
let mut sb_slot: Option<usize> = None;
|
|
2159
|
+
let mut sb_buf = String::new();
|
|
2160
|
+
macro_rules! sb_flush {
|
|
2161
|
+
() => {{
|
|
2162
|
+
if let Some(s) = sb_slot.take() {
|
|
2163
|
+
if let Some(dst) = slot_locals.get_mut(s) {
|
|
2164
|
+
*dst = Value::String(std::mem::take(&mut sb_buf).into());
|
|
2165
|
+
}
|
|
2166
|
+
sb_buf.clear();
|
|
2167
|
+
}
|
|
2168
|
+
}};
|
|
2169
|
+
}
|
|
1531
2170
|
if chunk.slot_based {
|
|
1532
2171
|
slot_locals = vec![Value::Null; chunk.num_slots as usize];
|
|
1533
2172
|
let param_count = chunk.param_count as usize;
|
|
@@ -1620,6 +2259,10 @@ impl Vm {
|
|
|
1620
2259
|
Opcode::Nop => {}
|
|
1621
2260
|
Opcode::LoadLocal => {
|
|
1622
2261
|
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
2262
|
+
// Flush a pending string-builder for this slot so the read sees the full string.
|
|
2263
|
+
if sb_slot == Some(slot) {
|
|
2264
|
+
sb_flush!();
|
|
2265
|
+
}
|
|
1623
2266
|
let v = slot_locals
|
|
1624
2267
|
.get(slot)
|
|
1625
2268
|
.cloned()
|
|
@@ -1628,6 +2271,11 @@ impl Vm {
|
|
|
1628
2271
|
}
|
|
1629
2272
|
Opcode::StoreLocal => {
|
|
1630
2273
|
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
2274
|
+
// A plain store overwrites the slot — drop any builder buffering it.
|
|
2275
|
+
if sb_slot == Some(slot) {
|
|
2276
|
+
sb_slot = None;
|
|
2277
|
+
sb_buf.clear();
|
|
2278
|
+
}
|
|
1631
2279
|
let v = self
|
|
1632
2280
|
.stack
|
|
1633
2281
|
.pop()
|
|
@@ -1637,10 +2285,59 @@ impl Vm {
|
|
|
1637
2285
|
None => return Err(format!("Local slot out of bounds: {}", slot)),
|
|
1638
2286
|
}
|
|
1639
2287
|
}
|
|
2288
|
+
Opcode::AppendLocal => {
|
|
2289
|
+
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
2290
|
+
let rhs = self
|
|
2291
|
+
.stack
|
|
2292
|
+
.pop()
|
|
2293
|
+
.ok_or_else(|| "Stack underflow in AppendLocal".to_string())?;
|
|
2294
|
+
if sb_slot == Some(slot) {
|
|
2295
|
+
// Continue the active builder for this slot — amortized O(1) append.
|
|
2296
|
+
append_value_for_string_concat(&mut sb_buf, &rhs);
|
|
2297
|
+
} else {
|
|
2298
|
+
// Switching slots: flush the previous builder, then (re)start for this one.
|
|
2299
|
+
sb_flush!();
|
|
2300
|
+
match slot_locals.get(slot) {
|
|
2301
|
+
Some(Value::String(a)) => {
|
|
2302
|
+
sb_buf.clear();
|
|
2303
|
+
sb_buf.reserve(a.len() + estimate_string_concat_len(&rhs));
|
|
2304
|
+
sb_buf.push_str(a);
|
|
2305
|
+
append_value_for_string_concat(&mut sb_buf, &rhs);
|
|
2306
|
+
sb_slot = Some(slot);
|
|
2307
|
+
}
|
|
2308
|
+
_ => {
|
|
2309
|
+
// Non-string (or absent) accumulator: generic `+=` in place,
|
|
2310
|
+
// identical to LoadLocal; BinOp Add; StoreLocal.
|
|
2311
|
+
let current = slot_locals.get(slot).cloned().unwrap_or(Value::Null);
|
|
2312
|
+
let result = eval_binop(BinOp::Add, ¤t, &rhs)?;
|
|
2313
|
+
match slot_locals.get_mut(slot) {
|
|
2314
|
+
Some(dst) => *dst = result,
|
|
2315
|
+
None => {
|
|
2316
|
+
return Err(format!("Local slot out of bounds: {}", slot))
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
1640
2323
|
Opcode::LoadUpvalue | Opcode::StoreUpvalue => {
|
|
1641
2324
|
// Reserved for the linked-frame upvalue model (not emitted yet).
|
|
1642
2325
|
return Err("Upvalue opcodes not supported in this VM build".to_string());
|
|
1643
2326
|
}
|
|
2327
|
+
Opcode::MathUnary => {
|
|
2328
|
+
// #186 — `Math.<fn>(x)` on a number. The compiler only emits this when `Math` is
|
|
2329
|
+
// the global builtin, so it is behaviour-identical to the general call: a number
|
|
2330
|
+
// maps through `MathUnaryFn::apply`, a non-number coerces to NaN (matching the
|
|
2331
|
+
// builtin's `extract_num(..).unwrap_or(NaN)`).
|
|
2332
|
+
let id = Self::read_u16(code, &mut ip);
|
|
2333
|
+
let x = match self.stack.pop() {
|
|
2334
|
+
Some(Value::Number(n)) => n,
|
|
2335
|
+
Some(_) | None => f64::NAN,
|
|
2336
|
+
};
|
|
2337
|
+
let mfn = tishlang_bytecode::MathUnaryFn::from_u16(id)
|
|
2338
|
+
.ok_or_else(|| format!("Bad MathUnary id: {}", id))?;
|
|
2339
|
+
self.stack.push(Value::Number(mfn.apply(x)));
|
|
2340
|
+
}
|
|
1644
2341
|
Opcode::LoadConst => {
|
|
1645
2342
|
let idx = Self::read_u16(code, &mut ip);
|
|
1646
2343
|
let c = constants
|
|
@@ -1648,7 +2345,9 @@ impl Vm {
|
|
|
1648
2345
|
.ok_or_else(|| format!("Constant index out of bounds: {}", idx))?;
|
|
1649
2346
|
let v = match c {
|
|
1650
2347
|
Constant::Number(n) => Value::Number(*n),
|
|
1651
|
-
Constant::String(s) =>
|
|
2348
|
+
Constant::String(s) => {
|
|
2349
|
+
Value::String(tishlang_core::ArcStr::from(s.as_ref()))
|
|
2350
|
+
}
|
|
1652
2351
|
Constant::Bool(b) => Value::Bool(*b),
|
|
1653
2352
|
Constant::Null => Value::Null,
|
|
1654
2353
|
Constant::Closure(nested_idx) => {
|
|
@@ -1669,45 +2368,45 @@ impl Vm {
|
|
|
1669
2368
|
// A closure must capture a real scope (even if empty) so that, post-creation,
|
|
1670
2369
|
// the parent's name-based locals are visible. Materialise local_scope here.
|
|
1671
2370
|
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
|
-
|
|
2371
|
+
let enclosing_chain: SharedChain =
|
|
2372
|
+
SharedChain::new(if active_loop_vars.is_empty() {
|
|
2373
|
+
let mut chain = Vec::with_capacity(self.enclosing.len() + 1);
|
|
2374
|
+
chain.push(captured_scope.clone());
|
|
2375
|
+
chain.extend(self.enclosing.iter().cloned());
|
|
2376
|
+
chain
|
|
2377
|
+
} else {
|
|
2378
|
+
// Per-iteration `let`: freeze the loop var(s) into an overlay that
|
|
2379
|
+
// shadows the still-shared frame scope, then the inherited chain.
|
|
2380
|
+
let mut overlay = ObjectMap::default();
|
|
2381
|
+
{
|
|
2382
|
+
let ls = captured_scope.borrow();
|
|
2383
|
+
for n in &active_loop_vars {
|
|
2384
|
+
if let Some(v) = ls.get(n.as_ref()) {
|
|
2385
|
+
overlay.insert(Arc::clone(n), v.clone());
|
|
2386
|
+
}
|
|
1686
2387
|
}
|
|
1687
2388
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
});
|
|
2389
|
+
let mut chain = Vec::with_capacity(self.enclosing.len() + 2);
|
|
2390
|
+
chain.push(VmRef::new(overlay));
|
|
2391
|
+
chain.push(captured_scope.clone());
|
|
2392
|
+
chain.extend(self.enclosing.iter().cloned());
|
|
2393
|
+
chain
|
|
2394
|
+
});
|
|
1695
2395
|
let capabilities = Arc::clone(&self.capabilities);
|
|
1696
2396
|
let native_modules = self.native_modules.clone();
|
|
1697
2397
|
// Frame-eligibility is an O(chunk) bytecode scan; gate it behind the
|
|
1698
2398
|
// (cached) frame-VM flag so the DEFAULT path skips it entirely — flag-off
|
|
1699
2399
|
// closure creation pays nothing.
|
|
1700
|
-
let frameable = Vm::frame_vm_enabled()
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
};
|
|
2400
|
+
let frameable = Vm::frame_vm_enabled() && {
|
|
2401
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2402
|
+
{
|
|
2403
|
+
jit_fn.is_none() && Vm::chunk_frame_eligible(&inner_clone)
|
|
2404
|
+
}
|
|
2405
|
+
#[cfg(target_arch = "wasm32")]
|
|
2406
|
+
{
|
|
2407
|
+
Vm::chunk_frame_eligible(&inner_clone)
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
1711
2410
|
let vmclosure = VmClosure {
|
|
1712
2411
|
chunk: std::sync::Arc::new(inner_clone),
|
|
1713
2412
|
frameable,
|
|
@@ -1759,7 +2458,10 @@ impl Vm {
|
|
|
1759
2458
|
.pop()
|
|
1760
2459
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1761
2460
|
// Update innermost scope that has the variable (matches interpreter Scope.assign)
|
|
1762
|
-
if local_scope
|
|
2461
|
+
if local_scope
|
|
2462
|
+
.as_ref()
|
|
2463
|
+
.is_some_and(|ls| ls.borrow().contains_key(name.as_ref()))
|
|
2464
|
+
{
|
|
1763
2465
|
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1764
2466
|
} else if let Some(e) = self
|
|
1765
2467
|
.enclosing
|
|
@@ -1994,6 +2696,14 @@ impl Vm {
|
|
|
1994
2696
|
);
|
|
1995
2697
|
}
|
|
1996
2698
|
args.reverse();
|
|
2699
|
+
// #381: SelfCall is a second native recursive re-entry (a self-recursive function
|
|
2700
|
+
// re-enters run_chunk directly). Bound it with the same shared counter; `raise!` is
|
|
2701
|
+
// in scope here, so throw the catchable RangeError directly.
|
|
2702
|
+
let depth = tishlang_core::inc_call_depth();
|
|
2703
|
+
if depth > max_call_depth() {
|
|
2704
|
+
tishlang_core::dec_call_depth();
|
|
2705
|
+
raise!(stack_overflow_error());
|
|
2706
|
+
}
|
|
1997
2707
|
let mut vm = Vm {
|
|
1998
2708
|
stack: Vec::new(),
|
|
1999
2709
|
scope: ObjectMap::default(),
|
|
@@ -2008,7 +2718,10 @@ impl Vm {
|
|
|
2008
2718
|
.unwrap_or(Value::Null)
|
|
2009
2719
|
});
|
|
2010
2720
|
#[cfg(target_arch = "wasm32")]
|
|
2011
|
-
let result = vm
|
|
2721
|
+
let result = vm
|
|
2722
|
+
.run_chunk(chunk, nested, &args, false)
|
|
2723
|
+
.unwrap_or(Value::Null);
|
|
2724
|
+
tishlang_core::dec_call_depth();
|
|
2012
2725
|
if let Some(v) = take_pending_throw() {
|
|
2013
2726
|
raise!(v);
|
|
2014
2727
|
}
|
|
@@ -2040,8 +2753,7 @@ impl Vm {
|
|
|
2040
2753
|
let f = match &callee {
|
|
2041
2754
|
Value::Function(f) => f.clone(),
|
|
2042
2755
|
Value::Object(o) => {
|
|
2043
|
-
if let Some(Value::Function(call_fn)) =
|
|
2044
|
-
o.borrow().strings.get("__call")
|
|
2756
|
+
if let Some(Value::Function(call_fn)) = o.borrow().strings.get("__call")
|
|
2045
2757
|
{
|
|
2046
2758
|
call_fn.clone()
|
|
2047
2759
|
} else {
|
|
@@ -2131,6 +2843,49 @@ impl Vm {
|
|
|
2131
2843
|
}
|
|
2132
2844
|
Opcode::JumpBack => {
|
|
2133
2845
|
let dist = Self::read_u16(code, &mut ip) as usize;
|
|
2846
|
+
// #190 OSR: on a hot back-edge, try to finish the loop natively. Slot-based frames
|
|
2847
|
+
// only (`slot_locals` is the live frame); non-slot / non-numeric loops fail the
|
|
2848
|
+
// whitelist or the live-in check and fall straight through to the interpreter.
|
|
2849
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2850
|
+
if chunk.slot_based && Self::osr_enabled() {
|
|
2851
|
+
let region_end = ip;
|
|
2852
|
+
let header_ip = ip.saturating_sub(dist);
|
|
2853
|
+
// Switch the fast slot only when the active loop changes: stash the old count,
|
|
2854
|
+
// load this header's (default 0). The hot loop keeps `header_ip == osr_hot.0`.
|
|
2855
|
+
if header_ip != osr_hot.0 {
|
|
2856
|
+
if osr_hot.0 != usize::MAX {
|
|
2857
|
+
osr_counters.insert(osr_hot.0, osr_hot.1);
|
|
2858
|
+
}
|
|
2859
|
+
osr_hot = (
|
|
2860
|
+
header_ip,
|
|
2861
|
+
osr_counters.get(&header_ip).copied().unwrap_or(0),
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
if osr_hot.1 != u32::MAX {
|
|
2865
|
+
osr_hot.1 = osr_hot.1.saturating_add(1);
|
|
2866
|
+
if osr_hot.1 >= OSR_THRESHOLD
|
|
2867
|
+
&& osr_hot
|
|
2868
|
+
.1
|
|
2869
|
+
.saturating_sub(OSR_THRESHOLD)
|
|
2870
|
+
.is_multiple_of(OSR_RETRY)
|
|
2871
|
+
{
|
|
2872
|
+
match Self::run_osr(
|
|
2873
|
+
chunk,
|
|
2874
|
+
header_ip,
|
|
2875
|
+
region_end,
|
|
2876
|
+
&mut slot_locals,
|
|
2877
|
+
0,
|
|
2878
|
+
) {
|
|
2879
|
+
OsrResult::Compiled(exit_ip) => {
|
|
2880
|
+
ip = exit_ip;
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
OsrResult::LiveInMiss => {}
|
|
2884
|
+
OsrResult::NotCompilable => osr_hot.1 = u32::MAX,
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2134
2889
|
ip = ip.saturating_sub(dist);
|
|
2135
2890
|
}
|
|
2136
2891
|
Opcode::BinOp => {
|
|
@@ -2265,8 +3020,12 @@ impl Vm {
|
|
|
2265
3020
|
if let Some(nums) = elems.iter().try_fold(
|
|
2266
3021
|
Vec::<f64>::with_capacity(elems.len()),
|
|
2267
3022
|
|mut acc, v| {
|
|
2268
|
-
if let Value::Number(n) = v {
|
|
2269
|
-
|
|
3023
|
+
if let Value::Number(n) = v {
|
|
3024
|
+
acc.push(*n);
|
|
3025
|
+
Some(acc)
|
|
3026
|
+
} else {
|
|
3027
|
+
None
|
|
3028
|
+
}
|
|
2270
3029
|
},
|
|
2271
3030
|
) {
|
|
2272
3031
|
self.stack.push(Value::number_array(nums));
|
|
@@ -2289,10 +3048,8 @@ impl Vm {
|
|
|
2289
3048
|
let base = self.stack.len() - 2 * n;
|
|
2290
3049
|
let mut map = PropMap::with_capacity(n);
|
|
2291
3050
|
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);
|
|
3051
|
+
let key_val = std::mem::replace(&mut self.stack[base + 2 * i], Value::Null);
|
|
3052
|
+
let val = std::mem::replace(&mut self.stack[base + 2 * i + 1], Value::Null);
|
|
2296
3053
|
let key: Arc<str> = key_val.to_display_string().into();
|
|
2297
3054
|
map.insert(key, val);
|
|
2298
3055
|
}
|
|
@@ -2448,13 +3205,25 @@ impl Vm {
|
|
|
2448
3205
|
.iter()
|
|
2449
3206
|
.map(|&n| {
|
|
2450
3207
|
let elem = Value::Number(n);
|
|
2451
|
-
let (l, r) = if param_left {
|
|
3208
|
+
let (l, r) = if param_left {
|
|
3209
|
+
(elem, const_val.clone())
|
|
3210
|
+
} else {
|
|
3211
|
+
(const_val.clone(), elem)
|
|
3212
|
+
};
|
|
2452
3213
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
|
|
2453
3214
|
})
|
|
2454
3215
|
.collect();
|
|
2455
3216
|
// If every result is numeric, stay packed (the common case for x*2, x+1, etc).
|
|
2456
3217
|
if mapped.iter().all(|v| matches!(v, Value::Number(_))) {
|
|
2457
|
-
Value::number_array(
|
|
3218
|
+
Value::number_array(
|
|
3219
|
+
mapped
|
|
3220
|
+
.into_iter()
|
|
3221
|
+
.map(|v| match v {
|
|
3222
|
+
Value::Number(n) => n,
|
|
3223
|
+
_ => unreachable!(),
|
|
3224
|
+
})
|
|
3225
|
+
.collect(),
|
|
3226
|
+
)
|
|
2458
3227
|
} else {
|
|
2459
3228
|
Value::Array(VmRef::new(mapped))
|
|
2460
3229
|
}
|
|
@@ -2464,8 +3233,16 @@ impl Vm {
|
|
|
2464
3233
|
let mapped: Vec<Value> = arr_borrow
|
|
2465
3234
|
.iter()
|
|
2466
3235
|
.map(|v| {
|
|
2467
|
-
let l: Value = if param_left {
|
|
2468
|
-
|
|
3236
|
+
let l: Value = if param_left {
|
|
3237
|
+
(*v).clone()
|
|
3238
|
+
} else {
|
|
3239
|
+
const_val.clone()
|
|
3240
|
+
};
|
|
3241
|
+
let r: Value = if param_left {
|
|
3242
|
+
const_val.clone()
|
|
3243
|
+
} else {
|
|
3244
|
+
(*v).clone()
|
|
3245
|
+
};
|
|
2469
3246
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
|
|
2470
3247
|
})
|
|
2471
3248
|
.collect();
|
|
@@ -2499,7 +3276,11 @@ impl Vm {
|
|
|
2499
3276
|
.iter()
|
|
2500
3277
|
.filter(|&&n| {
|
|
2501
3278
|
let elem = Value::Number(n);
|
|
2502
|
-
let (l, r) = if param_left {
|
|
3279
|
+
let (l, r) = if param_left {
|
|
3280
|
+
(elem, const_val.clone())
|
|
3281
|
+
} else {
|
|
3282
|
+
(const_val.clone(), elem)
|
|
3283
|
+
};
|
|
2503
3284
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
2504
3285
|
})
|
|
2505
3286
|
.copied()
|
|
@@ -2511,7 +3292,11 @@ impl Vm {
|
|
|
2511
3292
|
let filtered: Vec<Value> = arr_borrow
|
|
2512
3293
|
.iter()
|
|
2513
3294
|
.filter(|v| {
|
|
2514
|
-
let (l, r) = if param_left {
|
|
3295
|
+
let (l, r) = if param_left {
|
|
3296
|
+
((*v).clone(), const_val.clone())
|
|
3297
|
+
} else {
|
|
3298
|
+
(const_val.clone(), (*v).clone())
|
|
3299
|
+
};
|
|
2515
3300
|
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
2516
3301
|
})
|
|
2517
3302
|
.cloned()
|
|
@@ -2581,15 +3366,14 @@ impl Vm {
|
|
|
2581
3366
|
// on the cranelift / llvm backends that want to expose
|
|
2582
3367
|
// `cargo:…` Rust crates should register the module's
|
|
2583
3368
|
// 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
|
-
};
|
|
3369
|
+
let from_registry: Option<Value> =
|
|
3370
|
+
if spec.starts_with("cargo:") || spec.starts_with("ffi:") {
|
|
3371
|
+
let regs = self.native_modules.borrow();
|
|
3372
|
+
regs.get(spec)
|
|
3373
|
+
.and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
|
|
3374
|
+
} else {
|
|
3375
|
+
None
|
|
3376
|
+
};
|
|
2593
3377
|
let v = from_registry
|
|
2594
3378
|
.or_else(|| get_builtin_export(self.capabilities.as_ref(), spec, export_name))
|
|
2595
3379
|
.ok_or_else(|| {
|
|
@@ -2772,7 +3556,12 @@ fn eval_unary(op: UnaryOp, o: &Value) -> Result<Value, String> {
|
|
|
2772
3556
|
/// error), refilling the cache when the object *does* have the property. Result-equivalent to
|
|
2773
3557
|
/// `get_member` — the cache only skips the lookup; the shape uniquely fixes the slot for a property.
|
|
2774
3558
|
#[inline]
|
|
2775
|
-
fn ic_get_member(
|
|
3559
|
+
fn ic_get_member(
|
|
3560
|
+
chunk: &Chunk,
|
|
3561
|
+
name_idx: u16,
|
|
3562
|
+
obj: &Value,
|
|
3563
|
+
key: &Arc<str>,
|
|
3564
|
+
) -> Result<Value, String> {
|
|
2776
3565
|
use std::sync::atomic::Ordering::Relaxed;
|
|
2777
3566
|
if let Value::Object(od) = obj {
|
|
2778
3567
|
let b = od.borrow();
|
|
@@ -2853,13 +3642,21 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2853
3642
|
let map = m.borrow();
|
|
2854
3643
|
// Reading a missing own property returns `null` (tish's nullish value), matching
|
|
2855
3644
|
// JS object semantics and the tree-walk interpreter — not a thrown error (#66).
|
|
2856
|
-
Ok(map
|
|
3645
|
+
Ok(map
|
|
3646
|
+
.strings
|
|
3647
|
+
.get(key.as_ref())
|
|
3648
|
+
.cloned()
|
|
3649
|
+
.unwrap_or(Value::Null))
|
|
2857
3650
|
}
|
|
2858
3651
|
Value::NumberArray(a) => {
|
|
2859
3652
|
let key_s = key.as_ref();
|
|
2860
3653
|
// Numeric index fast path.
|
|
2861
3654
|
if let Ok(idx) = key_s.parse::<usize>() {
|
|
2862
|
-
return Ok(a
|
|
3655
|
+
return Ok(a
|
|
3656
|
+
.borrow()
|
|
3657
|
+
.get(idx)
|
|
3658
|
+
.map(|&n| Value::Number(n))
|
|
3659
|
+
.unwrap_or(Value::Null));
|
|
2863
3660
|
}
|
|
2864
3661
|
if key_s == "length" {
|
|
2865
3662
|
return Ok(Value::Number(a.borrow().len() as f64));
|
|
@@ -2880,19 +3677,38 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2880
3677
|
Value::Number(arr.len() as f64)
|
|
2881
3678
|
}),
|
|
2882
3679
|
"pop" => make_native_fn(move |_: &[Value]| {
|
|
2883
|
-
a_clone
|
|
2884
|
-
.
|
|
3680
|
+
a_clone
|
|
3681
|
+
.borrow_mut()
|
|
3682
|
+
.pop()
|
|
3683
|
+
.map(|n| {
|
|
3684
|
+
if n.is_nan() {
|
|
3685
|
+
Value::Null
|
|
3686
|
+
} else {
|
|
3687
|
+
Value::Number(n)
|
|
3688
|
+
}
|
|
3689
|
+
})
|
|
2885
3690
|
.unwrap_or(Value::Null)
|
|
2886
3691
|
}),
|
|
2887
3692
|
"shift" => make_native_fn(move |_: &[Value]| {
|
|
2888
3693
|
let mut arr = a_clone.borrow_mut();
|
|
2889
|
-
if arr.is_empty() {
|
|
2890
|
-
|
|
3694
|
+
if arr.is_empty() {
|
|
3695
|
+
Value::Null
|
|
3696
|
+
} else {
|
|
3697
|
+
let n = arr.remove(0);
|
|
3698
|
+
if n.is_nan() {
|
|
3699
|
+
Value::Null
|
|
3700
|
+
} else {
|
|
3701
|
+
Value::Number(n)
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
2891
3704
|
}),
|
|
2892
3705
|
"unshift" => make_native_fn(move |args: &[Value]| {
|
|
2893
3706
|
let mut arr = a_clone.borrow_mut();
|
|
2894
3707
|
for (i, v) in args.iter().enumerate() {
|
|
2895
|
-
let n = match v {
|
|
3708
|
+
let n = match v {
|
|
3709
|
+
Value::Number(n) => *n,
|
|
3710
|
+
_ => f64::NAN,
|
|
3711
|
+
};
|
|
2896
3712
|
arr.insert(i, n);
|
|
2897
3713
|
}
|
|
2898
3714
|
Value::Number(arr.len() as f64)
|
|
@@ -2905,7 +3721,10 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2905
3721
|
let a2 = a_clone.clone();
|
|
2906
3722
|
make_native_fn(move |args: &[Value]| {
|
|
2907
3723
|
// Check if there are non-numeric items to insert (args[2..]).
|
|
2908
|
-
let has_non_numeric = args
|
|
3724
|
+
let has_non_numeric = args
|
|
3725
|
+
.get(2..)
|
|
3726
|
+
.unwrap_or(&[])
|
|
3727
|
+
.iter()
|
|
2909
3728
|
.any(|v| !matches!(v, Value::Number(_)));
|
|
2910
3729
|
if has_non_numeric {
|
|
2911
3730
|
// Deopt: materialise, splice on the boxed array, then write numeric
|
|
@@ -2913,18 +3732,37 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2913
3732
|
// identity for subsequent accesses. The array may have non-numeric
|
|
2914
3733
|
// elements after this splice — they become NaN holes in the VmRef.
|
|
2915
3734
|
let boxed = Value::materialize_number_array(&a2);
|
|
2916
|
-
let result = arr_builtins::splice(
|
|
3735
|
+
let result = arr_builtins::splice(
|
|
3736
|
+
&boxed,
|
|
3737
|
+
args.first().unwrap_or(&Value::Null),
|
|
3738
|
+
args.get(1),
|
|
3739
|
+
args.get(2..).unwrap_or(&[]),
|
|
3740
|
+
);
|
|
2917
3741
|
// Sync the modified boxed Vec back into the original VmRef.
|
|
2918
3742
|
if let Value::Array(boxed_vmref) = &boxed {
|
|
2919
3743
|
let mut packed = a2.borrow_mut();
|
|
2920
|
-
*packed = boxed_vmref
|
|
3744
|
+
*packed = boxed_vmref
|
|
3745
|
+
.borrow()
|
|
3746
|
+
.iter()
|
|
3747
|
+
.map(|v| match v {
|
|
3748
|
+
Value::Number(n) => *n,
|
|
3749
|
+
_ => f64::NAN,
|
|
3750
|
+
})
|
|
3751
|
+
.collect();
|
|
2921
3752
|
}
|
|
2922
3753
|
result
|
|
2923
3754
|
} else {
|
|
2924
3755
|
let mut arr = a2.borrow_mut();
|
|
2925
3756
|
let len = arr.len() as i64;
|
|
2926
3757
|
let start = match args.first() {
|
|
2927
|
-
Some(Value::Number(n)) => {
|
|
3758
|
+
Some(Value::Number(n)) => {
|
|
3759
|
+
let s = *n as i64;
|
|
3760
|
+
if s < 0 {
|
|
3761
|
+
(len + s).max(0) as usize
|
|
3762
|
+
} else {
|
|
3763
|
+
(s as usize).min(arr.len())
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
2928
3766
|
_ => 0,
|
|
2929
3767
|
};
|
|
2930
3768
|
let del = match args.get(1) {
|
|
@@ -2932,8 +3770,17 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2932
3770
|
_ => arr.len().saturating_sub(start),
|
|
2933
3771
|
};
|
|
2934
3772
|
let del = del.min(arr.len().saturating_sub(start));
|
|
2935
|
-
let new_nums: Vec<f64> = args
|
|
2936
|
-
|
|
3773
|
+
let new_nums: Vec<f64> = args
|
|
3774
|
+
.get(2..)
|
|
3775
|
+
.unwrap_or(&[])
|
|
3776
|
+
.iter()
|
|
3777
|
+
.map(|v| match v {
|
|
3778
|
+
Value::Number(n) => *n,
|
|
3779
|
+
_ => f64::NAN,
|
|
3780
|
+
})
|
|
3781
|
+
.collect();
|
|
3782
|
+
let removed: Vec<f64> =
|
|
3783
|
+
arr.splice(start..start + del, new_nums).collect();
|
|
2937
3784
|
Value::number_array(removed)
|
|
2938
3785
|
}
|
|
2939
3786
|
})
|
|
@@ -2956,29 +3803,111 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
2956
3803
|
let boxed = Value::materialize_number_array(&a_clone);
|
|
2957
3804
|
let bv = boxed.clone();
|
|
2958
3805
|
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
|
-
|
|
3806
|
+
"map" => make_native_fn(move |args| {
|
|
3807
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3808
|
+
// #187 fusion over the SAME boxed snapshot `bv` the generic path uses, so a
|
|
3809
|
+
// fused result is byte-identical (a boxed all-number `Array` → boxed output).
|
|
3810
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3811
|
+
if let Some(v) = hof_fusion::map(&bv, &cb) {
|
|
3812
|
+
return v;
|
|
3813
|
+
}
|
|
3814
|
+
arr_builtins::map(&bv, &cb)
|
|
3815
|
+
}),
|
|
3816
|
+
"filter" => make_native_fn(move |args| {
|
|
3817
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3818
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3819
|
+
if let Some(v) = hof_fusion::filter(&bv, &cb) {
|
|
3820
|
+
return v;
|
|
3821
|
+
}
|
|
3822
|
+
arr_builtins::filter(&bv, &cb)
|
|
3823
|
+
}),
|
|
3824
|
+
"reduce" => make_native_fn(move |args| {
|
|
3825
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3826
|
+
let init = args.get(1).cloned().unwrap_or(Value::Null);
|
|
3827
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3828
|
+
if let Some(v) = hof_fusion::reduce(&bv, &cb, &init) {
|
|
3829
|
+
return v;
|
|
3830
|
+
}
|
|
3831
|
+
arr_builtins::reduce(&bv, &cb, &init)
|
|
3832
|
+
}),
|
|
3833
|
+
"forEach" => make_native_fn(move |args| {
|
|
3834
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3835
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3836
|
+
if let Some(v) = hof_fusion::for_each(&bv, &cb) {
|
|
3837
|
+
return v;
|
|
3838
|
+
}
|
|
3839
|
+
arr_builtins::for_each(&bv, &cb)
|
|
3840
|
+
}),
|
|
3841
|
+
"find" => make_native_fn(move |args| {
|
|
3842
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3843
|
+
arr_builtins::find(&bv, &cb)
|
|
3844
|
+
}),
|
|
3845
|
+
"findIndex" => make_native_fn(move |args| {
|
|
3846
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3847
|
+
arr_builtins::find_index(&bv, &cb)
|
|
3848
|
+
}),
|
|
3849
|
+
"findLast" => make_native_fn(move |args| {
|
|
3850
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3851
|
+
arr_builtins::find_last(&bv, &cb)
|
|
3852
|
+
}),
|
|
3853
|
+
"findLastIndex" => make_native_fn(move |args| {
|
|
3854
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3855
|
+
arr_builtins::find_last_index(&bv, &cb)
|
|
3856
|
+
}),
|
|
3857
|
+
"at" => make_native_fn(move |args| {
|
|
3858
|
+
let i = args.first().cloned().unwrap_or(Value::Null);
|
|
3859
|
+
arr_builtins::at(&bv, &i)
|
|
3860
|
+
}),
|
|
3861
|
+
"some" => make_native_fn(move |args| {
|
|
3862
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3863
|
+
arr_builtins::some(&bv, &cb)
|
|
3864
|
+
}),
|
|
3865
|
+
"every" => make_native_fn(move |args| {
|
|
3866
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3867
|
+
arr_builtins::every(&bv, &cb)
|
|
3868
|
+
}),
|
|
3869
|
+
"join" => make_native_fn(move |args| {
|
|
3870
|
+
let sep = args.first().cloned().unwrap_or(Value::Null);
|
|
3871
|
+
arr_builtins::join(&bv, &sep)
|
|
3872
|
+
}),
|
|
3873
|
+
"flat" => make_native_fn(move |args| {
|
|
3874
|
+
let d = args.first().cloned().unwrap_or(Value::Number(1.0));
|
|
3875
|
+
arr_builtins::flat(&bv, &d)
|
|
3876
|
+
}),
|
|
3877
|
+
"flatMap" => make_native_fn(move |args| {
|
|
3878
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3879
|
+
arr_builtins::flat_map(&bv, &cb)
|
|
3880
|
+
}),
|
|
3881
|
+
"reverse" => make_native_fn(move |_| arr_builtins::reverse(&bv)),
|
|
3882
|
+
"fill" => make_native_fn(move |args| {
|
|
3883
|
+
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
3884
|
+
let s = args.get(1).cloned().unwrap_or(Value::Null);
|
|
3885
|
+
let e = args.get(2).cloned().unwrap_or(Value::Null);
|
|
3886
|
+
arr_builtins::fill(&bv, &v, &s, &e)
|
|
3887
|
+
}),
|
|
3888
|
+
"slice" => make_native_fn(move |args| {
|
|
3889
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
3890
|
+
let e = args.get(1).cloned().unwrap_or(Value::Null);
|
|
3891
|
+
arr_builtins::slice(&bv, &s, &e)
|
|
3892
|
+
}),
|
|
3893
|
+
"concat" => make_native_fn(move |args| arr_builtins::concat(&bv, args)),
|
|
3894
|
+
"indexOf" => make_native_fn(move |args| {
|
|
3895
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
3896
|
+
arr_builtins::index_of(&bv, &s)
|
|
3897
|
+
}),
|
|
3898
|
+
"includes" => make_native_fn(move |args| {
|
|
3899
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
3900
|
+
let f = args.get(1).cloned();
|
|
3901
|
+
arr_builtins::includes(&bv, &s, f.as_ref())
|
|
3902
|
+
}),
|
|
3903
|
+
"unshift" => make_native_fn(move |args| arr_builtins::unshift(&bv, args)),
|
|
3904
|
+
"shift" => make_native_fn(move |_| arr_builtins::shift(&bv)),
|
|
3905
|
+
"splice" => make_native_fn(move |args| {
|
|
3906
|
+
let s = args.first().cloned().unwrap_or(Value::Null);
|
|
3907
|
+
let dc = args.get(1).cloned();
|
|
3908
|
+
let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
|
|
3909
|
+
arr_builtins::splice(&bv, &s, dc.as_ref(), &items)
|
|
3910
|
+
}),
|
|
2982
3911
|
_ => return Err(format!("Property '{}' not found", key)),
|
|
2983
3912
|
}
|
|
2984
3913
|
}
|
|
@@ -3046,20 +3975,42 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3046
3975
|
}),
|
|
3047
3976
|
"map" => make_native_fn(move |args: &[Value]| {
|
|
3048
3977
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3049
|
-
|
|
3978
|
+
let arr = Value::Array(a_clone.clone());
|
|
3979
|
+
// #187 native HOF fusion — tight native loop over a JIT'd numeric callback; bails
|
|
3980
|
+
// (None) to the byte-identical generic path for any non-fusable callback/array.
|
|
3981
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3982
|
+
if let Some(v) = hof_fusion::map(&arr, &cb) {
|
|
3983
|
+
return v;
|
|
3984
|
+
}
|
|
3985
|
+
arr_builtins::map(&arr, &cb)
|
|
3050
3986
|
}),
|
|
3051
3987
|
"filter" => make_native_fn(move |args: &[Value]| {
|
|
3052
3988
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3053
|
-
|
|
3989
|
+
let arr = Value::Array(a_clone.clone());
|
|
3990
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
3991
|
+
if let Some(v) = hof_fusion::filter(&arr, &cb) {
|
|
3992
|
+
return v;
|
|
3993
|
+
}
|
|
3994
|
+
arr_builtins::filter(&arr, &cb)
|
|
3054
3995
|
}),
|
|
3055
3996
|
"reduce" => make_native_fn(move |args: &[Value]| {
|
|
3056
3997
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3057
3998
|
let init = args.get(1).cloned().unwrap_or(Value::Null);
|
|
3058
|
-
|
|
3999
|
+
let arr = Value::Array(a_clone.clone());
|
|
4000
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
4001
|
+
if let Some(v) = hof_fusion::reduce(&arr, &cb, &init) {
|
|
4002
|
+
return v;
|
|
4003
|
+
}
|
|
4004
|
+
arr_builtins::reduce(&arr, &cb, &init)
|
|
3059
4005
|
}),
|
|
3060
4006
|
"forEach" => make_native_fn(move |args: &[Value]| {
|
|
3061
4007
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3062
|
-
|
|
4008
|
+
let arr = Value::Array(a_clone.clone());
|
|
4009
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
4010
|
+
if let Some(v) = hof_fusion::for_each(&arr, &cb) {
|
|
4011
|
+
return v;
|
|
4012
|
+
}
|
|
4013
|
+
arr_builtins::for_each(&arr, &cb)
|
|
3063
4014
|
}),
|
|
3064
4015
|
"find" => make_native_fn(move |args: &[Value]| {
|
|
3065
4016
|
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
@@ -3126,13 +4077,13 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3126
4077
|
Value::String(s) => {
|
|
3127
4078
|
let key_s = key.as_ref();
|
|
3128
4079
|
if let Ok(idx) = key_s.parse::<usize>() {
|
|
3129
|
-
return match s
|
|
4080
|
+
return match str_builtins::nth_char(s, idx) {
|
|
3130
4081
|
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
3131
4082
|
None => Err("Index out of bounds".to_string()),
|
|
3132
4083
|
};
|
|
3133
4084
|
}
|
|
3134
4085
|
if key_s == "length" {
|
|
3135
|
-
return Ok(Value::Number(s
|
|
4086
|
+
return Ok(Value::Number(str_builtins::char_count(s) as f64));
|
|
3136
4087
|
}
|
|
3137
4088
|
let s_clone: tishlang_core::ArcStr = s.clone();
|
|
3138
4089
|
let method: ArrayMethodFn = match key_s {
|
|
@@ -3144,11 +4095,7 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3144
4095
|
"lastIndexOf" => make_native_fn(move |args: &[Value]| {
|
|
3145
4096
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3146
4097
|
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
|
-
)
|
|
4098
|
+
str_builtins::last_index_of(&Value::String(s_clone.clone()), search, &position)
|
|
3152
4099
|
}),
|
|
3153
4100
|
"includes" => make_native_fn(move |args: &[Value]| {
|
|
3154
4101
|
let search = args.first().unwrap_or(&Value::Null);
|
|
@@ -3221,11 +4168,7 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
3221
4168
|
"replaceAll" => make_native_fn(move |args: &[Value]| {
|
|
3222
4169
|
let search = args.first().unwrap_or(&Value::Null);
|
|
3223
4170
|
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
|
-
)
|
|
4171
|
+
str_builtins::replace_all(&Value::String(s_clone.clone()), search, replacement)
|
|
3229
4172
|
}),
|
|
3230
4173
|
#[cfg(feature = "regex")]
|
|
3231
4174
|
"match" => make_native_fn(move |args: &[Value]| {
|
|
@@ -3390,10 +4333,24 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3390
4333
|
Value::NumberArray(a) => {
|
|
3391
4334
|
let i = match idx {
|
|
3392
4335
|
Value::Number(n) => *n as usize,
|
|
3393
|
-
_ =>
|
|
4336
|
+
_ => {
|
|
4337
|
+
return Err(format!(
|
|
4338
|
+
"Array index must be number, got {}",
|
|
4339
|
+
idx.type_name()
|
|
4340
|
+
))
|
|
4341
|
+
}
|
|
3394
4342
|
};
|
|
3395
4343
|
// NaN is used as the hole marker (sparse-array positions); reads return Null.
|
|
3396
|
-
Ok(a.borrow()
|
|
4344
|
+
Ok(a.borrow()
|
|
4345
|
+
.get(i)
|
|
4346
|
+
.map(|&n| {
|
|
4347
|
+
if n.is_nan() {
|
|
4348
|
+
Value::Null
|
|
4349
|
+
} else {
|
|
4350
|
+
Value::Number(n)
|
|
4351
|
+
}
|
|
4352
|
+
})
|
|
4353
|
+
.unwrap_or(Value::Null))
|
|
3397
4354
|
}
|
|
3398
4355
|
Value::Array(a) => {
|
|
3399
4356
|
let i = match idx {
|
|
@@ -3405,11 +4362,7 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3405
4362
|
));
|
|
3406
4363
|
}
|
|
3407
4364
|
};
|
|
3408
|
-
Ok(a
|
|
3409
|
-
.borrow()
|
|
3410
|
-
.get(i)
|
|
3411
|
-
.cloned()
|
|
3412
|
-
.unwrap_or(Value::Null))
|
|
4365
|
+
Ok(a.borrow().get(i).cloned().unwrap_or(Value::Null))
|
|
3413
4366
|
}
|
|
3414
4367
|
Value::String(s) => {
|
|
3415
4368
|
let i = match idx {
|
|
@@ -3421,12 +4374,7 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3421
4374
|
n
|
|
3422
4375
|
));
|
|
3423
4376
|
}
|
|
3424
|
-
|
|
3425
|
-
let len = s.chars().count();
|
|
3426
|
-
if i >= len {
|
|
3427
|
-
return Err("Index out of bounds".to_string());
|
|
3428
|
-
}
|
|
3429
|
-
i
|
|
4377
|
+
n as usize
|
|
3430
4378
|
}
|
|
3431
4379
|
_ => {
|
|
3432
4380
|
return Err(format!(
|
|
@@ -3435,7 +4383,9 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3435
4383
|
));
|
|
3436
4384
|
}
|
|
3437
4385
|
};
|
|
3438
|
-
|
|
4386
|
+
// `nth_char` returns None past the end, so the cursor cache handles bounds — no separate
|
|
4387
|
+
// O(n) `chars().count()` pre-check (#203).
|
|
4388
|
+
match str_builtins::nth_char(s, i) {
|
|
3439
4389
|
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
3440
4390
|
None => Err("Index out of bounds".to_string()),
|
|
3441
4391
|
}
|
|
@@ -3455,7 +4405,7 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
3455
4405
|
}
|
|
3456
4406
|
};
|
|
3457
4407
|
get_member(obj, &key_arc)
|
|
3458
|
-
}
|
|
4408
|
+
}
|
|
3459
4409
|
_ => Err(format!(
|
|
3460
4410
|
"Cannot read property '{}' of {}",
|
|
3461
4411
|
idx.to_display_string(),
|
|
@@ -3497,7 +4447,12 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3497
4447
|
Value::NumberArray(a) => {
|
|
3498
4448
|
let i = match idx {
|
|
3499
4449
|
Value::Number(n) => *n as usize,
|
|
3500
|
-
_ =>
|
|
4450
|
+
_ => {
|
|
4451
|
+
return Err(format!(
|
|
4452
|
+
"Array index must be number, got {}",
|
|
4453
|
+
idx.type_name()
|
|
4454
|
+
))
|
|
4455
|
+
}
|
|
3501
4456
|
};
|
|
3502
4457
|
// In-bounds numeric assignment stays packed.
|
|
3503
4458
|
// Out-of-bounds or non-numeric falls through to the Array path by returning
|
|
@@ -3508,7 +4463,9 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3508
4463
|
Value::Number(n) => {
|
|
3509
4464
|
let mut arr = a.borrow_mut();
|
|
3510
4465
|
// Extend with NaN "holes" if needed (NaN = sparse hole; read back as Null).
|
|
3511
|
-
while arr.len() <= i {
|
|
4466
|
+
while arr.len() <= i {
|
|
4467
|
+
arr.push(f64::NAN);
|
|
4468
|
+
}
|
|
3512
4469
|
arr[i] = n;
|
|
3513
4470
|
}
|
|
3514
4471
|
// Non-numeric set: the Vec<f64> can't represent this type. Extend with NaN holes
|
|
@@ -3518,7 +4475,9 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
3518
4475
|
// numeric elements and Null for the NaN holes.
|
|
3519
4476
|
_ => {
|
|
3520
4477
|
let mut arr = a.borrow_mut();
|
|
3521
|
-
while arr.len() <= i {
|
|
4478
|
+
while arr.len() <= i {
|
|
4479
|
+
arr.push(f64::NAN);
|
|
4480
|
+
}
|
|
3522
4481
|
// arr[i] is already NaN (hole); we can't store the non-numeric value — acceptable
|
|
3523
4482
|
// for the experimental TISH_PACKED_ARRAYS path.
|
|
3524
4483
|
}
|
|
@@ -3558,3 +4517,89 @@ pub fn run_with_options(chunk: &Chunk, opts: VmRunOptions) -> Result<Value, Stri
|
|
|
3558
4517
|
let mut vm = Vm::with_capabilities(opts.capabilities);
|
|
3559
4518
|
vm.run_with_options(chunk, opts.repl_mode)
|
|
3560
4519
|
}
|
|
4520
|
+
|
|
4521
|
+
#[cfg(test)]
|
|
4522
|
+
mod recursion_limit_tests_381 {
|
|
4523
|
+
use tishlang_core::{set_max_call_depth_for_test, Value, DEFAULT_MAX_CALL_DEPTH};
|
|
4524
|
+
|
|
4525
|
+
fn run_src(src: &str) -> Result<Value, String> {
|
|
4526
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
4527
|
+
let chunk = tishlang_bytecode::compile(&program).expect("compile");
|
|
4528
|
+
super::run(&chunk)
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
#[test]
|
|
4532
|
+
fn deep_recursion_is_catchable_not_abort() {
|
|
4533
|
+
// Without the guard this infinite recursion aborts the process (SIGABRT). With it, the throw
|
|
4534
|
+
// is a catchable RangeError: try/catch recovers and the program returns normally.
|
|
4535
|
+
set_max_call_depth_for_test(300);
|
|
4536
|
+
// Object-returning recursion is NOT JIT-eligible, so it takes the guarded VM call path.
|
|
4537
|
+
let src = "let ok = false\n\
|
|
4538
|
+
fn rec(n) { return { v: rec(n + 1) } }\n\
|
|
4539
|
+
try { rec(0) } catch (e) { ok = true }\n\
|
|
4540
|
+
ok";
|
|
4541
|
+
let out = run_src(src);
|
|
4542
|
+
set_max_call_depth_for_test(DEFAULT_MAX_CALL_DEPTH);
|
|
4543
|
+
assert!(
|
|
4544
|
+
out.is_ok(),
|
|
4545
|
+
"deep recursion must be catchable, not abort/error: {out:?}"
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
#[test]
|
|
4550
|
+
fn uncaught_deep_recursion_surfaces_error_not_abort() {
|
|
4551
|
+
// An UNCAUGHT infinite recursion must surface as a returned error, never a SIGABRT.
|
|
4552
|
+
set_max_call_depth_for_test(300);
|
|
4553
|
+
let out = run_src("fn rec(n) { return { v: rec(n + 1) } }\nrec(0)");
|
|
4554
|
+
set_max_call_depth_for_test(DEFAULT_MAX_CALL_DEPTH);
|
|
4555
|
+
assert!(
|
|
4556
|
+
out.is_err(),
|
|
4557
|
+
"uncaught deep recursion must return an error, got {out:?}"
|
|
4558
|
+
);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
#[test]
|
|
4562
|
+
fn normal_recursion_is_unaffected() {
|
|
4563
|
+
set_max_call_depth_for_test(20_000);
|
|
4564
|
+
let out = run_src(
|
|
4565
|
+
"fn fib(n) { if (n < 2) { return n } return fib(n - 1) + fib(n - 2) }\nfib(15)",
|
|
4566
|
+
);
|
|
4567
|
+
set_max_call_depth_for_test(DEFAULT_MAX_CALL_DEPTH);
|
|
4568
|
+
assert!(
|
|
4569
|
+
out.is_ok(),
|
|
4570
|
+
"normal recursion must not be affected: {out:?}"
|
|
4571
|
+
);
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
// The core of #381 for the JIT tier: a pure-numeric self-recursive function JIT-compiles and
|
|
4575
|
+
// recurses on the native stack via SelfCall, BELOW the VM's depth counter (so `set_max_call_depth`
|
|
4576
|
+
// can't bound it). Without the guard this overflows the native stack — an uncatchable SIGSEGV/abort
|
|
4577
|
+
// that would kill the whole process. With it, the entry SP check turns the overflow into a
|
|
4578
|
+
// RangeError which, uncaught, must surface as a returned `Err`. Run on a bounded-stack thread so
|
|
4579
|
+
// the guard (or, on a regression, the overflow) is reached fast: a working guard lets the thread
|
|
4580
|
+
// `join` cleanly with the error flag; a broken one aborts the process (the loud regression signal).
|
|
4581
|
+
#[test]
|
|
4582
|
+
fn jit_deep_recursion_surfaces_error_not_abort() {
|
|
4583
|
+
// `Value` is `!Send` (holds `Rc`), so reduce to a Send bool inside the thread: true iff the
|
|
4584
|
+
// overflow surfaced as the catchable stack-overflow error rather than aborting.
|
|
4585
|
+
let handle = std::thread::Builder::new()
|
|
4586
|
+
.stack_size(1024 * 1024)
|
|
4587
|
+
.spawn(|| {
|
|
4588
|
+
matches!(
|
|
4589
|
+
run_src(
|
|
4590
|
+
"fn dive(n) { if (n <= 0.0) { return 0.0 } return dive(n - 1.0) + 1.0 }\n\
|
|
4591
|
+
dive(50000000.0)",
|
|
4592
|
+
),
|
|
4593
|
+
Err(ref e) if e.contains("call stack")
|
|
4594
|
+
)
|
|
4595
|
+
})
|
|
4596
|
+
.expect("spawn");
|
|
4597
|
+
let surfaced = handle
|
|
4598
|
+
.join()
|
|
4599
|
+
.expect("thread must not abort — the guard must catch the overflow");
|
|
4600
|
+
assert!(
|
|
4601
|
+
surfaced,
|
|
4602
|
+
"uncaught JIT deep recursion must surface as a RangeError, not abort"
|
|
4603
|
+
);
|
|
4604
|
+
}
|
|
4605
|
+
}
|