@tishlang/tish 1.13.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -12,12 +12,49 @@ use tishlang_builtins::array as arr_builtins;
|
|
|
12
12
|
use tishlang_builtins::construct as construct_builtin;
|
|
13
13
|
use tishlang_builtins::globals as globals_builtins;
|
|
14
14
|
use tishlang_builtins::math as math_builtins;
|
|
15
|
+
use tishlang_builtins::number as num_builtins;
|
|
15
16
|
use tishlang_builtins::string as str_builtins;
|
|
16
17
|
use tishlang_bytecode::{u8_to_binop, u8_to_unaryop, Chunk, Constant, Opcode, NO_REST_PARAM};
|
|
17
18
|
use tishlang_core::{
|
|
18
|
-
merge_object_data, object_get, object_has, object_set,
|
|
19
|
+
merge_object_data, object_get, object_has, object_set, to_int32, to_uint32, NativeFn,
|
|
20
|
+
ObjectData, ObjectMap, PropMap, Value,
|
|
19
21
|
};
|
|
20
22
|
|
|
23
|
+
/// Error string returned by `run_chunk`/`run_framed` to mean "a thrown value is parked in
|
|
24
|
+
/// [`VM_PENDING_THROW`]; keep unwinding toward an enclosing `catch`" (issue #60). The leading
|
|
25
|
+
/// control char makes it unmistakable for a real diagnostic. `Callable::call` returns a bare
|
|
26
|
+
/// `Value`, so the thrown *value* can't ride the `Result`; it travels in this thread-local
|
|
27
|
+
/// instead and is picked up at the next call site (or the top-level boundary).
|
|
28
|
+
const PENDING_THROW_SENTINEL: &str = "\u{1}__tish_pending_throw__";
|
|
29
|
+
|
|
30
|
+
thread_local! {
|
|
31
|
+
static VM_PENDING_THROW: std::cell::RefCell<Option<Value>> =
|
|
32
|
+
const { std::cell::RefCell::new(None) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn set_pending_throw(v: Value) {
|
|
36
|
+
VM_PENDING_THROW.with(|c| *c.borrow_mut() = Some(v));
|
|
37
|
+
}
|
|
38
|
+
fn take_pending_throw() -> Option<Value> {
|
|
39
|
+
VM_PENDING_THROW.with(|c| c.borrow_mut().take())
|
|
40
|
+
}
|
|
41
|
+
fn pending_throw_is_set() -> bool {
|
|
42
|
+
VM_PENDING_THROW.with(|c| c.borrow().is_some())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Append the source location of the instruction at `off` to a runtime-error message, e.g.
|
|
46
|
+
/// `Cannot read property 'x' of null (at app.tish:4)` (issue #74). No-ops when the chunk
|
|
47
|
+
/// carries no line table (e.g. deserialized bytecode).
|
|
48
|
+
fn locate_error(chunk: &Chunk, off: usize, msg: &str) -> String {
|
|
49
|
+
match chunk.line_at(off) {
|
|
50
|
+
Some(line) => match &chunk.source {
|
|
51
|
+
Some(src) => format!("{msg} (at {src}:{line})"),
|
|
52
|
+
None => format!("{msg} (at line {line})"),
|
|
53
|
+
},
|
|
54
|
+
None => msg.to_string(),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
/// Wrap a closure in the right shared pointer for the current build.
|
|
22
59
|
/// Under `send-values` that's `Arc<dyn Fn + Send + Sync>`; otherwise it's
|
|
23
60
|
/// plain `Rc<dyn Fn>`. Call sites can stay ignorant of the distinction.
|
|
@@ -27,7 +64,7 @@ fn make_native_fn<F>(f: F) -> NativeFn
|
|
|
27
64
|
where
|
|
28
65
|
F: Fn(&[Value]) -> Value + Send + Sync + 'static,
|
|
29
66
|
{
|
|
30
|
-
|
|
67
|
+
tishlang_core::native_fn(f)
|
|
31
68
|
}
|
|
32
69
|
|
|
33
70
|
#[cfg(not(feature = "send-values"))]
|
|
@@ -36,7 +73,7 @@ fn make_native_fn<F>(f: F) -> NativeFn
|
|
|
36
73
|
where
|
|
37
74
|
F: Fn(&[Value]) -> Value + 'static,
|
|
38
75
|
{
|
|
39
|
-
|
|
76
|
+
tishlang_core::native_fn(f)
|
|
40
77
|
}
|
|
41
78
|
|
|
42
79
|
// Array / string / object methods have the same shape as `NativeFn`, which
|
|
@@ -92,6 +129,8 @@ pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
|
92
129
|
s.insert("regex".to_string());
|
|
93
130
|
#[cfg(feature = "ws")]
|
|
94
131
|
s.insert("ws".to_string());
|
|
132
|
+
#[cfg(feature = "tty")]
|
|
133
|
+
s.insert("tty".to_string());
|
|
95
134
|
s
|
|
96
135
|
}
|
|
97
136
|
|
|
@@ -103,7 +142,8 @@ pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
|
103
142
|
feature = "promise",
|
|
104
143
|
feature = "timers",
|
|
105
144
|
feature = "process",
|
|
106
|
-
feature = "ws"
|
|
145
|
+
feature = "ws",
|
|
146
|
+
feature = "tty"
|
|
107
147
|
)),
|
|
108
148
|
allow(unused_variables)
|
|
109
149
|
)]
|
|
@@ -160,7 +200,7 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
160
200
|
obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
|
|
161
201
|
{
|
|
162
202
|
let args_for_init = [Value::Number(0.0)];
|
|
163
|
-
on_worker(&args_for_init)
|
|
203
|
+
on_worker.call(&args_for_init)
|
|
164
204
|
} else if let Some(h) =
|
|
165
205
|
obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
|
|
166
206
|
{
|
|
@@ -172,7 +212,7 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
172
212
|
_ => Value::Null,
|
|
173
213
|
};
|
|
174
214
|
if let Value::Function(f) = handler_value {
|
|
175
|
-
tishlang_runtime::http_serve(args, move |req_args| f(req_args))
|
|
215
|
+
tishlang_runtime::http_serve(args, move |req_args| f.call(req_args))
|
|
176
216
|
} else {
|
|
177
217
|
Value::Null
|
|
178
218
|
}
|
|
@@ -285,6 +325,27 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
285
325
|
_ => None,
|
|
286
326
|
};
|
|
287
327
|
}
|
|
328
|
+
#[cfg(feature = "tty")]
|
|
329
|
+
if spec == "tish:tty" && cap_allows(enabled, "tty") {
|
|
330
|
+
return match export_name {
|
|
331
|
+
"size" => Some(Value::native(|args: &[Value]| tishlang_runtime::tty_size(args))),
|
|
332
|
+
"isTTY" => Some(Value::native(|args: &[Value]| tishlang_runtime::tty_is_tty(args))),
|
|
333
|
+
"setRawMode" => Some(Value::native(|args: &[Value]| {
|
|
334
|
+
tishlang_runtime::tty_set_raw_mode(args)
|
|
335
|
+
})),
|
|
336
|
+
"enterAltScreen" => Some(Value::native(|args: &[Value]| {
|
|
337
|
+
tishlang_runtime::tty_enter_alt_screen(args)
|
|
338
|
+
})),
|
|
339
|
+
"leaveAltScreen" => Some(Value::native(|args: &[Value]| {
|
|
340
|
+
tishlang_runtime::tty_leave_alt_screen(args)
|
|
341
|
+
})),
|
|
342
|
+
"read" => Some(Value::native(|args: &[Value]| tishlang_runtime::tty_read(args))),
|
|
343
|
+
"readLine" => Some(Value::native(|args: &[Value]| {
|
|
344
|
+
tishlang_runtime::tty_read_line(args)
|
|
345
|
+
})),
|
|
346
|
+
_ => None,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
288
349
|
None
|
|
289
350
|
}
|
|
290
351
|
|
|
@@ -495,6 +556,29 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
495
556
|
Value::Number(sum_sq.sqrt())
|
|
496
557
|
}),
|
|
497
558
|
);
|
|
559
|
+
// Hyperbolic, inverse-hyperbolic, cbrt and base-2/10 logs. Like the trig block above
|
|
560
|
+
// these aren't in `math_builtins`, and on the wasm/native VM there is no host `Math`
|
|
561
|
+
// to fall through to, so they previously returned `undefined` (issue #61).
|
|
562
|
+
macro_rules! math_unary {
|
|
563
|
+
($name:literal, $method:ident) => {
|
|
564
|
+
math.insert(
|
|
565
|
+
$name.into(),
|
|
566
|
+
Value::native(|args: &[Value]| {
|
|
567
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
568
|
+
Value::Number(n.$method())
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
math_unary!("sinh", sinh);
|
|
574
|
+
math_unary!("cosh", cosh);
|
|
575
|
+
math_unary!("tanh", tanh);
|
|
576
|
+
math_unary!("asinh", asinh);
|
|
577
|
+
math_unary!("acosh", acosh);
|
|
578
|
+
math_unary!("atanh", atanh);
|
|
579
|
+
math_unary!("cbrt", cbrt);
|
|
580
|
+
math_unary!("log2", log2);
|
|
581
|
+
math_unary!("log10", log10);
|
|
498
582
|
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
|
|
499
583
|
math.insert("E".into(), Value::Number(std::f64::consts::E));
|
|
500
584
|
g.insert("Math".into(), value_object_from_map(math));
|
|
@@ -567,28 +651,44 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
567
651
|
tishlang_builtins::symbol::symbol_object(),
|
|
568
652
|
);
|
|
569
653
|
|
|
570
|
-
// Date -
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
Value::native(|_args: &[Value]| {
|
|
575
|
-
let ms = std::time::SystemTime::now()
|
|
576
|
-
.duration_since(std::time::UNIX_EPOCH)
|
|
577
|
-
.unwrap_or_default()
|
|
578
|
-
.as_millis() as f64;
|
|
579
|
-
Value::Number(ms)
|
|
580
|
-
}),
|
|
654
|
+
// Date - full constructor (new Date(...)) plus statics now()/parse()/UTC().
|
|
655
|
+
g.insert(
|
|
656
|
+
"Date".into(),
|
|
657
|
+
tishlang_builtins::date::date_constructor_value(),
|
|
581
658
|
);
|
|
582
|
-
g.insert("Date".into(), value_object_from_map(date));
|
|
583
|
-
|
|
584
659
|
g.insert(
|
|
585
|
-
"
|
|
586
|
-
|
|
660
|
+
"Set".into(),
|
|
661
|
+
tishlang_builtins::collections::set_constructor_value(),
|
|
587
662
|
);
|
|
663
|
+
g.insert(
|
|
664
|
+
"Map".into(),
|
|
665
|
+
tishlang_builtins::collections::map_constructor_value(),
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
for (name, ctor) in [
|
|
669
|
+
(
|
|
670
|
+
"Float64Array",
|
|
671
|
+
tishlang_builtins::typedarrays::float64_array_constructor_value as fn() -> Value,
|
|
672
|
+
),
|
|
673
|
+
("Float32Array", tishlang_builtins::typedarrays::float32_array_constructor_value),
|
|
674
|
+
("Int8Array", tishlang_builtins::typedarrays::int8_array_constructor_value),
|
|
675
|
+
("Uint8Array", tishlang_builtins::typedarrays::uint8_array_constructor_value),
|
|
676
|
+
("Uint8ClampedArray", tishlang_builtins::typedarrays::uint8_clamped_array_constructor_value),
|
|
677
|
+
("Int16Array", tishlang_builtins::typedarrays::int16_array_constructor_value),
|
|
678
|
+
("Uint16Array", tishlang_builtins::typedarrays::uint16_array_constructor_value),
|
|
679
|
+
("Int32Array", tishlang_builtins::typedarrays::int32_array_constructor_value),
|
|
680
|
+
("Uint32Array", tishlang_builtins::typedarrays::uint32_array_constructor_value),
|
|
681
|
+
] {
|
|
682
|
+
g.insert(name.into(), ctor());
|
|
683
|
+
}
|
|
588
684
|
g.insert(
|
|
589
685
|
"AudioContext".into(),
|
|
590
686
|
construct_builtin::audio_context_constructor_value(),
|
|
591
687
|
);
|
|
688
|
+
// Error constructors (issue #60): `new Error(msg)` / `Error(msg)` → `{ name, message }`.
|
|
689
|
+
for name in ["Error", "TypeError", "RangeError", "SyntaxError"] {
|
|
690
|
+
g.insert(name.into(), construct_builtin::error_constructor_value(name));
|
|
691
|
+
}
|
|
592
692
|
|
|
593
693
|
// Object methods - delegate to tishlang_builtins::globals
|
|
594
694
|
let mut object_methods = ObjectMap::default();
|
|
@@ -614,12 +714,17 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
614
714
|
);
|
|
615
715
|
g.insert("Object".into(), value_object_from_map(object_methods));
|
|
616
716
|
|
|
617
|
-
// Array.isArray
|
|
717
|
+
// Array.isArray + the `Array(n)` / `new Array(n)` constructor (issue #72). `__call`
|
|
718
|
+
// serves both forms — `construct()` falls back to `__call` when there's no `__construct`.
|
|
618
719
|
let mut array_static = ObjectMap::default();
|
|
619
720
|
array_static.insert(
|
|
620
721
|
"isArray".into(),
|
|
621
722
|
Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
|
|
622
723
|
);
|
|
724
|
+
array_static.insert(
|
|
725
|
+
Arc::from("__call"),
|
|
726
|
+
Value::native(|args: &[Value]| construct_builtin::array_construct(args)),
|
|
727
|
+
);
|
|
623
728
|
g.insert("Array".into(), value_object_from_map(array_static));
|
|
624
729
|
|
|
625
730
|
// String(value) as callable + String.fromCharCode
|
|
@@ -632,6 +737,14 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
632
737
|
string_static.insert(Arc::from("__call"), string_convert_fn);
|
|
633
738
|
g.insert("String".into(), value_object_from_map(string_static));
|
|
634
739
|
|
|
740
|
+
// Number(value) coercion as a callable global (issue #36).
|
|
741
|
+
let mut number_static = ObjectMap::default();
|
|
742
|
+
number_static.insert(
|
|
743
|
+
Arc::from("__call"),
|
|
744
|
+
Value::native(|args: &[Value]| globals_builtins::number_convert(args)),
|
|
745
|
+
);
|
|
746
|
+
g.insert("Number".into(), value_object_from_map(number_static));
|
|
747
|
+
|
|
635
748
|
// JSX / Lattish: stubs for bytecode VM when no DOM (e.g. console). Override via set_global in browser.
|
|
636
749
|
g.insert("h".into(), Value::native(|_args: &[Value]| Value::Null));
|
|
637
750
|
g.insert(
|
|
@@ -755,7 +868,7 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
755
868
|
obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
|
|
756
869
|
{
|
|
757
870
|
let args_for_init = [Value::Number(0.0)];
|
|
758
|
-
on_worker(&args_for_init)
|
|
871
|
+
on_worker.call(&args_for_init)
|
|
759
872
|
} else if let Some(h) =
|
|
760
873
|
obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
|
|
761
874
|
{
|
|
@@ -767,7 +880,7 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
767
880
|
_ => Value::Null,
|
|
768
881
|
};
|
|
769
882
|
if let Value::Function(f) = handler_value {
|
|
770
|
-
tishlang_runtime::http_serve(args, move |req_args| f(req_args))
|
|
883
|
+
tishlang_runtime::http_serve(args, move |req_args| f.call(req_args))
|
|
771
884
|
} else {
|
|
772
885
|
Value::Null
|
|
773
886
|
}
|
|
@@ -780,12 +893,32 @@ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
|
780
893
|
g.insert("Promise".into(), tishlang_runtime::promise_object());
|
|
781
894
|
}
|
|
782
895
|
|
|
896
|
+
// `RegExp(pattern, flags)` constructor. A language feature (not a sandboxed capability),
|
|
897
|
+
// so it's available whenever the `regex` feature is compiled — matching the interpreter.
|
|
898
|
+
// Routes to the same `regexp_new` the rust backend uses (full-backend-parity-plan.md).
|
|
899
|
+
#[cfg(feature = "regex")]
|
|
900
|
+
g.insert(
|
|
901
|
+
"RegExp".into(),
|
|
902
|
+
Value::native(|args: &[Value]| tishlang_runtime::regexp_new(args)),
|
|
903
|
+
);
|
|
904
|
+
|
|
783
905
|
g
|
|
784
906
|
}
|
|
785
907
|
|
|
786
908
|
/// Shared scope for closure capture (parent frame's locals).
|
|
787
909
|
type ScopeMap = VmRef<ObjectMap>;
|
|
788
910
|
|
|
911
|
+
/// The captured lexical chain for closures. Shared immutably (never mutated after a closure is
|
|
912
|
+
/// built — `run_chunk` only reads it: `.len()`/`.iter()`/`.is_empty()`), so it lives behind an
|
|
913
|
+
/// `Rc`/`Arc` instead of a `Vec` that would be deep-cloned on every call. This makes the per-call
|
|
914
|
+
/// `enclosing` propagation a single refcount bump rather than a `Vec` allocation + N element clones
|
|
915
|
+
/// — a direct cut to function-call overhead. `Arc` under `send-values` (closures must be `Send`),
|
|
916
|
+
/// `Rc` otherwise.
|
|
917
|
+
#[cfg(feature = "send-values")]
|
|
918
|
+
type SharedChain = std::sync::Arc<Vec<ScopeMap>>;
|
|
919
|
+
#[cfg(not(feature = "send-values"))]
|
|
920
|
+
type SharedChain = std::rc::Rc<Vec<ScopeMap>>;
|
|
921
|
+
|
|
789
922
|
/// Options for the convenience [`run_with_options`] helper (one-shot VM run from the CLI).
|
|
790
923
|
#[derive(Clone, Debug, Default)]
|
|
791
924
|
pub struct VmRunOptions {
|
|
@@ -799,8 +932,16 @@ pub struct VmRunOptions {
|
|
|
799
932
|
pub struct Vm {
|
|
800
933
|
stack: Vec<Value>,
|
|
801
934
|
scope: ObjectMap,
|
|
802
|
-
///
|
|
803
|
-
|
|
935
|
+
/// Captured enclosing scopes for closures, **innermost first**. A free variable resolves by
|
|
936
|
+
/// walking `local_scope` → each entry here in order → `scope` → `globals`. This is the full
|
|
937
|
+
/// lexical chain: a closure captures its defining frame's scope *plus that frame's own
|
|
938
|
+
/// enclosing chain*, so a function nested N levels deep still sees every ancestor's locals
|
|
939
|
+
/// (was a fixed `enclosing` + `enclosing2`, which silently lost captures >2 levels deep — see
|
|
940
|
+
/// `nested_complex`). Per-iteration `let`: a fresh frozen overlay of the loop var(s) is
|
|
941
|
+
/// prepended as the innermost entry, shadowing the still-shared frame scope that follows it,
|
|
942
|
+
/// so the loop var is frozen per-iteration while everything else stays live. Empty at top level.
|
|
943
|
+
/// Shared via `SharedChain` (Rc/Arc) so per-call propagation is a refcount bump, not a Vec clone.
|
|
944
|
+
enclosing: SharedChain,
|
|
804
945
|
globals: VmRef<ObjectMap>,
|
|
805
946
|
/// Capabilities for `LoadNativeExport` and globals such as `process` / `serve`.
|
|
806
947
|
capabilities: Arc<HashSet<String>>,
|
|
@@ -811,6 +952,144 @@ pub struct Vm {
|
|
|
811
952
|
native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
|
|
812
953
|
}
|
|
813
954
|
|
|
955
|
+
/// A bytecode-VM closure: a compiled chunk plus its captured lexical chain and shared VM state.
|
|
956
|
+
/// Implements [`tishlang_core::Callable`] so it lives in `Value::Function` like any callable, but
|
|
957
|
+
/// the `Call` opcode can `as_any`-downcast to it to run the call on the VM's explicit frame stack
|
|
958
|
+
/// (the frame-VM, task #39) instead of recursively re-entering `run_chunk`. `call()` is the
|
|
959
|
+
/// fallback path (builtin callbacks, and any not-yet-framed call) — byte-identical to the former
|
|
960
|
+
/// inline `Value::native` closure, so building these instead of raw closures changes nothing on
|
|
961
|
+
/// its own; the behavioural win comes when `Call` starts using the downcast + frame stack.
|
|
962
|
+
/// Try the array-mode JIT for `nf` (`array_param_mask != 0`). Splits `args` into numeric `f64`s and
|
|
963
|
+
/// flat [`crate::jit::ArrayHandle`]s — extracting all-numeric `Value::Array`s into scratch `Vec<f64>`s
|
|
964
|
+
/// that outlive the call. Returns `None` (caller falls back to the interpreter, so behaviour is always
|
|
965
|
+
/// correct) when an array arg is not an all-numeric `Value::Array` (covers `NumberArray`, whose
|
|
966
|
+
/// NaN-hole semantics differ), a numeric arg isn't a `Number`, or the JIT signals an OOB deopt.
|
|
967
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
968
|
+
fn try_call_array_jit(
|
|
969
|
+
nf: &crate::jit::NumericFn,
|
|
970
|
+
args: &[Value],
|
|
971
|
+
arity: usize,
|
|
972
|
+
mask: u8,
|
|
973
|
+
) -> Option<Value> {
|
|
974
|
+
let mut numeric: Vec<f64> = Vec::with_capacity(arity);
|
|
975
|
+
// `scratch` OWNS the extracted f64 data; handles point into it. Build handles only AFTER scratch is
|
|
976
|
+
// fully populated so its backing buffers never reallocate out from under a live pointer.
|
|
977
|
+
let mut scratch: Vec<Vec<f64>> = Vec::new();
|
|
978
|
+
#[allow(clippy::needless_range_loop)] // `i` drives bit-mask math (`mask >> i`), not just indexing
|
|
979
|
+
for i in 0..arity {
|
|
980
|
+
if (mask >> i) & 1 == 1 {
|
|
981
|
+
match &args[i] {
|
|
982
|
+
Value::Array(a) => {
|
|
983
|
+
let b = a.borrow();
|
|
984
|
+
let mut buf: Vec<f64> = Vec::with_capacity(b.len());
|
|
985
|
+
for el in b.iter() {
|
|
986
|
+
match el {
|
|
987
|
+
Value::Number(n) => buf.push(*n),
|
|
988
|
+
_ => return None, // non-numeric element → interpreter
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
scratch.push(buf);
|
|
992
|
+
}
|
|
993
|
+
_ => return None, // NumberArray / non-array → interpreter
|
|
994
|
+
}
|
|
995
|
+
} else {
|
|
996
|
+
match &args[i] {
|
|
997
|
+
Value::Number(n) => numeric.push(*n),
|
|
998
|
+
_ => return None,
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
let handles: Vec<crate::jit::ArrayHandle> = scratch
|
|
1003
|
+
.iter()
|
|
1004
|
+
.map(|buf| crate::jit::ArrayHandle {
|
|
1005
|
+
ptr: buf.as_ptr(),
|
|
1006
|
+
len: buf.len(),
|
|
1007
|
+
})
|
|
1008
|
+
.collect();
|
|
1009
|
+
let (res, deopt) = nf.call_arrays(&numeric, &handles);
|
|
1010
|
+
if deopt {
|
|
1011
|
+
return None; // OOB access → re-run interpreter (OOB reads coerce as Value::Null)
|
|
1012
|
+
}
|
|
1013
|
+
Some(Value::Number(res))
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
struct VmClosure {
|
|
1017
|
+
chunk: Arc<Chunk>,
|
|
1018
|
+
/// Whether this closure can run on the frame stack — computed ONCE at creation (eligibility is an
|
|
1019
|
+
/// O(chunk) bytecode scan; doing it per call regressed perf). `true` iff the chunk is frame-eligible
|
|
1020
|
+
/// and there is no numeric JIT for it.
|
|
1021
|
+
frameable: bool,
|
|
1022
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1023
|
+
jit_fn: Option<crate::jit::NumericFn>,
|
|
1024
|
+
enclosing: SharedChain,
|
|
1025
|
+
globals: VmRef<ObjectMap>,
|
|
1026
|
+
capabilities: Arc<HashSet<String>>,
|
|
1027
|
+
native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
impl tishlang_core::Callable for VmClosure {
|
|
1031
|
+
fn call(&self, args: &[Value]) -> Value {
|
|
1032
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1033
|
+
{
|
|
1034
|
+
if let Some(nf) = self.jit_fn {
|
|
1035
|
+
let arity = nf.arity();
|
|
1036
|
+
if args.len() >= arity {
|
|
1037
|
+
let mask = nf.array_param_mask();
|
|
1038
|
+
if mask == 0 {
|
|
1039
|
+
// Pure-numeric register-f64 path.
|
|
1040
|
+
let mut nums = [0f64; 8];
|
|
1041
|
+
let mut all_numbers = true;
|
|
1042
|
+
for i in 0..arity {
|
|
1043
|
+
if let Value::Number(n) = &args[i] {
|
|
1044
|
+
nums[i] = *n;
|
|
1045
|
+
} else {
|
|
1046
|
+
all_numbers = false;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if all_numbers {
|
|
1051
|
+
let res = nf.call(&nums[..arity]);
|
|
1052
|
+
return if nf.result_is_bool() {
|
|
1053
|
+
Value::Bool(res != 0.0)
|
|
1054
|
+
} else {
|
|
1055
|
+
Value::Number(res)
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
} else if let Some(v) = try_call_array_jit(&nf, args, arity, mask) {
|
|
1059
|
+
// Array-mode path: succeeded (all-numeric arrays, in-bounds). On any bail
|
|
1060
|
+
// (non-numeric element, NumberArray, OOB deopt) this returns None and we fall
|
|
1061
|
+
// through to the interpreter — so behaviour is always correct.
|
|
1062
|
+
return v;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
let mut vm = Vm {
|
|
1068
|
+
stack: Vec::new(),
|
|
1069
|
+
scope: ObjectMap::default(),
|
|
1070
|
+
enclosing: self.enclosing.clone(),
|
|
1071
|
+
globals: self.globals.clone(),
|
|
1072
|
+
capabilities: Arc::clone(&self.capabilities),
|
|
1073
|
+
native_modules: self.native_modules.clone(),
|
|
1074
|
+
};
|
|
1075
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1076
|
+
{
|
|
1077
|
+
stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
1078
|
+
vm.run_chunk(self.chunk.as_ref(), &self.chunk.nested, args, false)
|
|
1079
|
+
.unwrap_or(Value::Null)
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
#[cfg(target_arch = "wasm32")]
|
|
1083
|
+
{
|
|
1084
|
+
vm.run_chunk(&self.chunk, &self.chunk.nested, args, false)
|
|
1085
|
+
.unwrap_or(Value::Null)
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
fn as_any(&self) -> &dyn std::any::Any {
|
|
1089
|
+
self
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
814
1093
|
impl Vm {
|
|
815
1094
|
/// VM with every capability that exists in this `tishlang_vm` build (embedders, tests, `run()`).
|
|
816
1095
|
pub fn new() -> Self {
|
|
@@ -826,7 +1105,7 @@ impl Vm {
|
|
|
826
1105
|
Self {
|
|
827
1106
|
stack: Vec::new(),
|
|
828
1107
|
scope: ObjectMap::default(),
|
|
829
|
-
enclosing:
|
|
1108
|
+
enclosing: SharedChain::new(Vec::new()),
|
|
830
1109
|
globals: VmRef::new(init_globals(capabilities.as_ref())),
|
|
831
1110
|
capabilities,
|
|
832
1111
|
native_modules: VmRef::new(HashMap::new()),
|
|
@@ -895,7 +1174,331 @@ impl Vm {
|
|
|
895
1174
|
|
|
896
1175
|
/// Run a chunk using this VM's capability set. `repl_mode` persists top-level `let` across REPL lines.
|
|
897
1176
|
pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
|
|
898
|
-
self.run_chunk(chunk, &chunk.nested, &[], repl_mode)
|
|
1177
|
+
let result = self.run_chunk(chunk, &chunk.nested, &[], repl_mode);
|
|
1178
|
+
// A throw that escaped every `catch` reaches here as the pending-throw sentinel; turn the
|
|
1179
|
+
// parked value into the conventional uncaught-error message (issue #60).
|
|
1180
|
+
if let Err(e) = &result {
|
|
1181
|
+
if e == PENDING_THROW_SENTINEL {
|
|
1182
|
+
let v = take_pending_throw().unwrap_or(Value::Null);
|
|
1183
|
+
return Err(format!("Uncaught {}", v.to_display_string()));
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
result
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/// Whether the experimental frame-VM path is on (`TISH_FRAME_VM=1`). Flag-off (default) is
|
|
1190
|
+
/// byte-identical to the recursive `run_chunk` model — every `Value::Function` call goes through
|
|
1191
|
+
/// `VmClosure::call` exactly as before.
|
|
1192
|
+
#[inline]
|
|
1193
|
+
fn frame_vm_enabled() -> bool {
|
|
1194
|
+
// Read the env var ONCE and cache it. This is checked on the hot path (every Call opcode +
|
|
1195
|
+
// every closure creation), so a per-call `std::env::var` (a lock + String alloc) is a severe
|
|
1196
|
+
// regression to the DEFAULT path — caching makes the flag-off check a single atomic load.
|
|
1197
|
+
use std::sync::OnceLock;
|
|
1198
|
+
static ENABLED: OnceLock<bool> = OnceLock::new();
|
|
1199
|
+
*ENABLED.get_or_init(|| std::env::var("TISH_FRAME_VM").map(|v| v == "1").unwrap_or(false))
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/// A `VmClosure` runs on the frame stack iff its chunk is frame-eligible AND it has no numeric
|
|
1203
|
+
/// JIT (jit'd functions stay on the faster native path via `VmClosure::call`; the frame loop's
|
|
1204
|
+
/// niche is non-jit'd call-heavy / mutually-recursive functions + wasi where there is no JIT).
|
|
1205
|
+
fn vmclosure_frameable(vc: &VmClosure) -> bool {
|
|
1206
|
+
vc.frameable
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/// A chunk is frame-eligible iff slot-based and every opcode is one `run_framed` handles.
|
|
1210
|
+
/// `LoadConst` of a nested `Closure` is excluded (closure creation needs the full `run_chunk`).
|
|
1211
|
+
fn chunk_frame_eligible(chunk: &Chunk) -> bool {
|
|
1212
|
+
if !chunk.slot_based {
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
let code = &chunk.code;
|
|
1216
|
+
let mut ip = 0usize;
|
|
1217
|
+
while ip < code.len() {
|
|
1218
|
+
let op = match Opcode::from_u8(code[ip]) {
|
|
1219
|
+
Some(o) => o,
|
|
1220
|
+
None => return false,
|
|
1221
|
+
};
|
|
1222
|
+
match op {
|
|
1223
|
+
Opcode::Nop
|
|
1224
|
+
| Opcode::LoadLocal
|
|
1225
|
+
| Opcode::StoreLocal
|
|
1226
|
+
| Opcode::LoadVar
|
|
1227
|
+
| Opcode::BinOp
|
|
1228
|
+
| Opcode::Jump
|
|
1229
|
+
| Opcode::JumpIfFalse
|
|
1230
|
+
| Opcode::JumpBack
|
|
1231
|
+
| Opcode::Pop
|
|
1232
|
+
| Opcode::Call
|
|
1233
|
+
| Opcode::SelfCall
|
|
1234
|
+
| Opcode::Return => {}
|
|
1235
|
+
Opcode::LoadConst => {
|
|
1236
|
+
let idx = (((*code.get(ip + 1).unwrap_or(&0)) as usize) << 8)
|
|
1237
|
+
| ((*code.get(ip + 2).unwrap_or(&0)) as usize);
|
|
1238
|
+
if matches!(chunk.constants.get(idx), Some(Constant::Closure(_))) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
_ => return false,
|
|
1243
|
+
}
|
|
1244
|
+
ip += match op.instruction_size(code, ip) {
|
|
1245
|
+
Some(s) => s,
|
|
1246
|
+
None => return false,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
true
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/// Iterative frame-stack execution of a frame-eligible `VmClosure` (the frame-VM, flag-on).
|
|
1253
|
+
/// Returns `None` if the entry chunk is ineligible (caller falls back to `VmClosure::call`).
|
|
1254
|
+
/// Calls + recursion run on the heap `frames` stack — no per-call `Vm`, no recursive `run_chunk`
|
|
1255
|
+
/// re-entry, so deep + mutual recursion can't overflow and it works on wasi (no JIT there).
|
|
1256
|
+
fn run_framed(&mut self, top: &VmClosure, args: &[Value]) -> Option<Result<Value, String>> {
|
|
1257
|
+
if !Self::vmclosure_frameable(top) {
|
|
1258
|
+
return None;
|
|
1259
|
+
}
|
|
1260
|
+
let mut cur: Arc<Chunk> = top.chunk.clone();
|
|
1261
|
+
let mut enclosing: SharedChain = top.enclosing.clone();
|
|
1262
|
+
let mut ip: usize = 0;
|
|
1263
|
+
let mut stack_base: usize = self.stack.len();
|
|
1264
|
+
// Slot-region pooling: ALL frames' locals share one `slots` Vec; each frame occupies
|
|
1265
|
+
// `slots[slot_base .. slot_base + num_slots]`. A call does `resize` (amortized, no per-call
|
|
1266
|
+
// heap alloc — unlike `run_chunk` which `vec!`s a fresh `slot_locals` every call); a return
|
|
1267
|
+
// does `truncate`. This is what makes the frame loop cheaper than the recursive path.
|
|
1268
|
+
let mut slots: Vec<Value> = Vec::new();
|
|
1269
|
+
let mut slot_base: usize = 0;
|
|
1270
|
+
slots.resize(cur.num_slots as usize, Value::Null);
|
|
1271
|
+
for i in 0..(cur.param_count as usize) {
|
|
1272
|
+
if let Some(v) = args.get(i) {
|
|
1273
|
+
if let Some(d) = slots.get_mut(slot_base + i) {
|
|
1274
|
+
*d = v.clone();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// Suspended callers: (chunk, return ip, caller slot_base, caller stack_base, enclosing).
|
|
1279
|
+
let mut frames: Vec<(Arc<Chunk>, usize, usize, usize, SharedChain)> = Vec::new();
|
|
1280
|
+
|
|
1281
|
+
macro_rules! ferr {
|
|
1282
|
+
($($t:tt)*) => {
|
|
1283
|
+
return Some(Err(format!($($t)*)))
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
macro_rules! fpop {
|
|
1287
|
+
() => {
|
|
1288
|
+
match self.stack.pop() {
|
|
1289
|
+
Some(v) => v,
|
|
1290
|
+
None => ferr!("Stack underflow in run_framed"),
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// SAFETY: `code` aliases the current frame's chunk bytecode. The chunk is kept alive by `cur`
|
|
1296
|
+
// (and suspended-frame chunks by `frames`), so the slice stays valid for as long as we read
|
|
1297
|
+
// it; it is re-derived via `rebind_code!()` after every frame switch (Call/Return/end).
|
|
1298
|
+
// Laundering the borrow lets the hot opcode path index `code[ip]` directly with no per-opcode
|
|
1299
|
+
// Arc deref — matching run_chunk (the per-opcode Arc deref was a measured ~10% shallow-call regression).
|
|
1300
|
+
let mut code: &[u8] = unsafe { &*(cur.code.as_slice() as *const [u8]) };
|
|
1301
|
+
|
|
1302
|
+
loop {
|
|
1303
|
+
if ip >= code.len() {
|
|
1304
|
+
self.stack.truncate(stack_base);
|
|
1305
|
+
slots.truncate(slot_base);
|
|
1306
|
+
match frames.pop() {
|
|
1307
|
+
Some((c, rip, sbase, sb, enc)) => {
|
|
1308
|
+
cur = c;
|
|
1309
|
+
ip = rip;
|
|
1310
|
+
slot_base = sbase;
|
|
1311
|
+
stack_base = sb;
|
|
1312
|
+
enclosing = enc;
|
|
1313
|
+
code = unsafe { &*(cur.code.as_slice() as *const [u8]) };
|
|
1314
|
+
self.stack.push(Value::Null);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
None => return Some(Ok(Value::Null)),
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
let op = match Opcode::from_u8(code[ip]) {
|
|
1321
|
+
Some(o) => o,
|
|
1322
|
+
None => ferr!("Bad opcode {} in run_framed", code[ip]),
|
|
1323
|
+
};
|
|
1324
|
+
ip += 1;
|
|
1325
|
+
match op {
|
|
1326
|
+
Opcode::Nop => {}
|
|
1327
|
+
Opcode::LoadLocal => {
|
|
1328
|
+
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
1329
|
+
match slots.get(slot_base + slot) {
|
|
1330
|
+
Some(v) => self.stack.push(v.clone()),
|
|
1331
|
+
None => ferr!("Local slot out of bounds: {}", slot),
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
Opcode::StoreLocal => {
|
|
1335
|
+
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
1336
|
+
let v = fpop!();
|
|
1337
|
+
match slots.get_mut(slot_base + slot) {
|
|
1338
|
+
Some(d) => *d = v,
|
|
1339
|
+
None => ferr!("Local slot out of bounds: {}", slot),
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
Opcode::LoadConst => {
|
|
1343
|
+
let idx = Self::read_u16(code, &mut ip) as usize;
|
|
1344
|
+
let v = match cur.constants.get(idx) {
|
|
1345
|
+
Some(Constant::Number(n)) => Value::Number(*n),
|
|
1346
|
+
Some(Constant::String(s)) => Value::String(tishlang_core::ArcStr::from(s.as_ref())),
|
|
1347
|
+
Some(Constant::Bool(b)) => Value::Bool(*b),
|
|
1348
|
+
Some(Constant::Null) => Value::Null,
|
|
1349
|
+
_ => ferr!("Ineligible constant {} in run_framed", idx),
|
|
1350
|
+
};
|
|
1351
|
+
self.stack.push(v);
|
|
1352
|
+
}
|
|
1353
|
+
Opcode::LoadVar => {
|
|
1354
|
+
let idx = Self::read_u16(code, &mut ip) as usize;
|
|
1355
|
+
let name = match cur.names.get(idx) {
|
|
1356
|
+
Some(n) => n.clone(),
|
|
1357
|
+
None => ferr!("Name index out of bounds: {}", idx),
|
|
1358
|
+
};
|
|
1359
|
+
let v = enclosing
|
|
1360
|
+
.iter()
|
|
1361
|
+
.find_map(|e| e.borrow().get(name.as_ref()).cloned())
|
|
1362
|
+
.or_else(|| self.scope.get(name.as_ref()).cloned())
|
|
1363
|
+
.or_else(|| self.globals.borrow().get(name.as_ref()).cloned());
|
|
1364
|
+
match v {
|
|
1365
|
+
Some(v) => self.stack.push(v),
|
|
1366
|
+
None => ferr!("Undefined variable: {}", name),
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
Opcode::BinOp => {
|
|
1370
|
+
let op_u8 = Self::read_u16(code, &mut ip) as u8;
|
|
1371
|
+
let r = fpop!();
|
|
1372
|
+
let l = fpop!();
|
|
1373
|
+
let bop = match u8_to_binop(op_u8) {
|
|
1374
|
+
Some(b) => b,
|
|
1375
|
+
None => ferr!("Unknown binop: {}", op_u8),
|
|
1376
|
+
};
|
|
1377
|
+
match eval_binop(bop, &l, &r) {
|
|
1378
|
+
Ok(res) => self.stack.push(res),
|
|
1379
|
+
Err(e) => return Some(Err(e)),
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
Opcode::Jump => {
|
|
1383
|
+
let offset = Self::read_i16(code, &mut ip) as isize;
|
|
1384
|
+
ip = (ip as isize + offset).max(0) as usize;
|
|
1385
|
+
}
|
|
1386
|
+
Opcode::JumpIfFalse => {
|
|
1387
|
+
let offset = Self::read_i16(code, &mut ip) as isize;
|
|
1388
|
+
let v = fpop!();
|
|
1389
|
+
if !v.is_truthy() {
|
|
1390
|
+
ip = (ip as isize + offset).max(0) as usize;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
Opcode::JumpBack => {
|
|
1394
|
+
let dist = Self::read_u16(code, &mut ip) as usize;
|
|
1395
|
+
ip = ip.saturating_sub(dist);
|
|
1396
|
+
}
|
|
1397
|
+
Opcode::Pop => {
|
|
1398
|
+
let _ = fpop!();
|
|
1399
|
+
}
|
|
1400
|
+
Opcode::SelfCall => {
|
|
1401
|
+
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
1402
|
+
let mut call_args = Vec::with_capacity(argc);
|
|
1403
|
+
for _ in 0..argc {
|
|
1404
|
+
call_args.push(fpop!());
|
|
1405
|
+
}
|
|
1406
|
+
call_args.reverse();
|
|
1407
|
+
frames.push((cur.clone(), ip, slot_base, stack_base, enclosing.clone()));
|
|
1408
|
+
let new_base = slots.len();
|
|
1409
|
+
slots.resize(new_base + cur.num_slots as usize, Value::Null);
|
|
1410
|
+
slot_base = new_base;
|
|
1411
|
+
ip = 0;
|
|
1412
|
+
stack_base = self.stack.len();
|
|
1413
|
+
for i in 0..(cur.param_count as usize) {
|
|
1414
|
+
if let Some(v) = call_args.get(i) {
|
|
1415
|
+
if let Some(d) = slots.get_mut(slot_base + i) {
|
|
1416
|
+
*d = v.clone();
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
Opcode::Call => {
|
|
1422
|
+
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
1423
|
+
let mut call_args = Vec::with_capacity(argc);
|
|
1424
|
+
for _ in 0..argc {
|
|
1425
|
+
call_args.push(fpop!());
|
|
1426
|
+
}
|
|
1427
|
+
call_args.reverse();
|
|
1428
|
+
let callee = fpop!();
|
|
1429
|
+
match &callee {
|
|
1430
|
+
Value::Function(f) => {
|
|
1431
|
+
let framed = f
|
|
1432
|
+
.as_any()
|
|
1433
|
+
.downcast_ref::<VmClosure>()
|
|
1434
|
+
.filter(|vc| Self::vmclosure_frameable(vc));
|
|
1435
|
+
if let Some(vc) = framed {
|
|
1436
|
+
let next_chunk = vc.chunk.clone();
|
|
1437
|
+
let next_enc = vc.enclosing.clone();
|
|
1438
|
+
// Move (not clone) the caller's chunk+chain into the frame; the Arc
|
|
1439
|
+
// refcounts are unchanged (the chunk heap data doesn't move, so the
|
|
1440
|
+
// laundered `code` ptr stays valid until rebind below). Halves the
|
|
1441
|
+
// per-call Arc traffic vs cloning for the push.
|
|
1442
|
+
frames.push((cur, ip, slot_base, stack_base, enclosing));
|
|
1443
|
+
cur = next_chunk;
|
|
1444
|
+
enclosing = next_enc;
|
|
1445
|
+
code = unsafe { &*(cur.code.as_slice() as *const [u8]) };
|
|
1446
|
+
let new_base = slots.len();
|
|
1447
|
+
slots.resize(new_base + cur.num_slots as usize, Value::Null);
|
|
1448
|
+
slot_base = new_base;
|
|
1449
|
+
ip = 0;
|
|
1450
|
+
stack_base = self.stack.len();
|
|
1451
|
+
for i in 0..(cur.param_count as usize) {
|
|
1452
|
+
if let Some(v) = call_args.get(i) {
|
|
1453
|
+
if let Some(d) = slots.get_mut(slot_base + i) {
|
|
1454
|
+
*d = v.clone();
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
let r = f.call(&call_args);
|
|
1460
|
+
// A throw escaping the callee can't be caught here (frameable
|
|
1461
|
+
// chunks have no `try`); bubble it to an enclosing frame (#60).
|
|
1462
|
+
if pending_throw_is_set() {
|
|
1463
|
+
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
1464
|
+
}
|
|
1465
|
+
self.stack.push(r);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
Value::Object(o) => {
|
|
1469
|
+
let cf = match o.borrow().strings.get("__call") {
|
|
1470
|
+
Some(Value::Function(cf)) => cf.clone(),
|
|
1471
|
+
_ => ferr!("Call of non-function: {}", callee.type_name()),
|
|
1472
|
+
};
|
|
1473
|
+
let r = cf.call(&call_args);
|
|
1474
|
+
if pending_throw_is_set() {
|
|
1475
|
+
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
1476
|
+
}
|
|
1477
|
+
self.stack.push(r);
|
|
1478
|
+
}
|
|
1479
|
+
_ => ferr!("Call of non-function: {}", callee.type_name()),
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
Opcode::Return => {
|
|
1483
|
+
let result = self.stack.pop().unwrap_or(Value::Null);
|
|
1484
|
+
self.stack.truncate(stack_base);
|
|
1485
|
+
slots.truncate(slot_base);
|
|
1486
|
+
match frames.pop() {
|
|
1487
|
+
Some((c, rip, sbase, sb, enc)) => {
|
|
1488
|
+
cur = c;
|
|
1489
|
+
ip = rip;
|
|
1490
|
+
slot_base = sbase;
|
|
1491
|
+
stack_base = sb;
|
|
1492
|
+
enclosing = enc;
|
|
1493
|
+
code = unsafe { &*(cur.code.as_slice() as *const [u8]) };
|
|
1494
|
+
self.stack.push(result);
|
|
1495
|
+
}
|
|
1496
|
+
None => return Some(Ok(result)),
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
other => ferr!("Unhandled opcode {:?} in run_framed", other),
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
899
1502
|
}
|
|
900
1503
|
|
|
901
1504
|
fn run_chunk(
|
|
@@ -910,9 +1513,37 @@ impl Vm {
|
|
|
910
1513
|
let names = &chunk.names;
|
|
911
1514
|
|
|
912
1515
|
let mut ip = 0;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1516
|
+
// Lazily allocated name-keyed scope. Slot-based chunks never WRITE it (params + body locals
|
|
1517
|
+
// live in `slot_locals`; `StoreVar` checks-then-falls-through to globals; a slot-based chunk
|
|
1518
|
+
// has no captured locals by construction), so on the hot slot-based call path we skip the
|
|
1519
|
+
// `VmRef::new(Arc<Mutex<HashMap>>)` box entirely. Non-slot chunks need it eagerly for params.
|
|
1520
|
+
// `ls_get_or_init!()` lazily creates it on the first write/capture; reads treat `None` as empty.
|
|
1521
|
+
let mut local_scope: Option<ScopeMap> = if chunk.slot_based {
|
|
1522
|
+
None
|
|
1523
|
+
} else {
|
|
1524
|
+
Some(VmRef::new(ObjectMap::default()))
|
|
1525
|
+
};
|
|
1526
|
+
macro_rules! ls_get_or_init {
|
|
1527
|
+
() => {{
|
|
1528
|
+
local_scope.get_or_insert_with(|| VmRef::new(ObjectMap::default()))
|
|
1529
|
+
}};
|
|
1530
|
+
}
|
|
1531
|
+
// Slot-based chunks (self-contained functions) use a bare `Vec<Value>`
|
|
1532
|
+
// frame indexed by slot — no per-call hashmap, no name lookups. Args bind
|
|
1533
|
+
// to slots 0..param_count. Empty for name-based chunks.
|
|
1534
|
+
let mut slot_locals: Vec<Value> = Vec::new();
|
|
1535
|
+
if chunk.slot_based {
|
|
1536
|
+
slot_locals = vec![Value::Null; chunk.num_slots as usize];
|
|
1537
|
+
let param_count = chunk.param_count as usize;
|
|
1538
|
+
for i in 0..param_count {
|
|
1539
|
+
if let Some(v) = args.get(i) {
|
|
1540
|
+
if let Some(dst) = slot_locals.get_mut(i) {
|
|
1541
|
+
*dst = v.clone();
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
} else {
|
|
1546
|
+
let mut ls = ls_get_or_init!().borrow_mut();
|
|
916
1547
|
let param_count = chunk.param_count as usize;
|
|
917
1548
|
if chunk.rest_param_index != NO_REST_PARAM {
|
|
918
1549
|
let ri = chunk.rest_param_index as usize;
|
|
@@ -935,11 +1566,53 @@ impl Vm {
|
|
|
935
1566
|
}
|
|
936
1567
|
let mut try_handlers: Vec<(usize, usize)> = vec![];
|
|
937
1568
|
let mut block_undo_stack: Vec<Vec<(Arc<str>, Option<Value>)>> = vec![];
|
|
1569
|
+
// Names of loop variables currently in a per-iteration binding region (ES `let` semantics).
|
|
1570
|
+
// A closure created while this is non-empty snapshots these into a fresh overlay so it
|
|
1571
|
+
// captures the loop variable's value for THIS iteration. Pushed/popped by LoopVarsBegin/End.
|
|
1572
|
+
let mut active_loop_vars: Vec<Arc<str>> = Vec::new();
|
|
1573
|
+
// Offset of the instruction currently executing — updated each iteration, read by the
|
|
1574
|
+
// error macros to attach a source location (issue #74). Declared here (not in the loop)
|
|
1575
|
+
// so it's in scope where `catchable!` is defined (macro hygiene).
|
|
1576
|
+
let mut instr_off = 0usize;
|
|
1577
|
+
|
|
1578
|
+
// Throw `$v` to the nearest enclosing handler (issue #60): if this frame has a live
|
|
1579
|
+
// `try`, jump to its `catch` with `$v` on the stack; otherwise park `$v` in the
|
|
1580
|
+
// thread-local and bubble the sentinel so an enclosing frame's catch can take it.
|
|
1581
|
+
macro_rules! raise {
|
|
1582
|
+
($v:expr) => {{
|
|
1583
|
+
let __thrown = $v;
|
|
1584
|
+
if let Some((catch_ip, stack_len)) = try_handlers.pop() {
|
|
1585
|
+
self.stack.truncate(stack_len);
|
|
1586
|
+
self.stack.push(__thrown);
|
|
1587
|
+
ip = catch_ip;
|
|
1588
|
+
continue;
|
|
1589
|
+
} else {
|
|
1590
|
+
set_pending_throw(__thrown);
|
|
1591
|
+
return Err(PENDING_THROW_SENTINEL.to_string());
|
|
1592
|
+
}
|
|
1593
|
+
}};
|
|
1594
|
+
}
|
|
1595
|
+
// Evaluate a fallible, JS-throwable opcode helper: on `Err(msg)` the message becomes a
|
|
1596
|
+
// catchable `TypeError` (`x.foo()` on null, calling a non-function, …) routed through
|
|
1597
|
+
// `raise!` instead of aborting the whole VM.
|
|
1598
|
+
macro_rules! catchable {
|
|
1599
|
+
($expr:expr) => {
|
|
1600
|
+
match $expr {
|
|
1601
|
+
Ok(v) => v,
|
|
1602
|
+
Err(msg) => raise!(construct_builtin::error_object(
|
|
1603
|
+
"TypeError",
|
|
1604
|
+
&locate_error(chunk, instr_off, &msg)
|
|
1605
|
+
)),
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
938
1609
|
|
|
939
1610
|
loop {
|
|
940
1611
|
if ip >= code.len() {
|
|
941
1612
|
break;
|
|
942
1613
|
}
|
|
1614
|
+
// Offset of the instruction about to execute (read by the error macros, #74).
|
|
1615
|
+
instr_off = ip;
|
|
943
1616
|
let op = code[ip];
|
|
944
1617
|
ip += 1;
|
|
945
1618
|
if op == Opcode::Nop as u8 {
|
|
@@ -949,6 +1622,29 @@ impl Vm {
|
|
|
949
1622
|
|
|
950
1623
|
match opcode {
|
|
951
1624
|
Opcode::Nop => {}
|
|
1625
|
+
Opcode::LoadLocal => {
|
|
1626
|
+
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
1627
|
+
let v = slot_locals
|
|
1628
|
+
.get(slot)
|
|
1629
|
+
.cloned()
|
|
1630
|
+
.ok_or_else(|| format!("Local slot out of bounds: {}", slot))?;
|
|
1631
|
+
self.stack.push(v);
|
|
1632
|
+
}
|
|
1633
|
+
Opcode::StoreLocal => {
|
|
1634
|
+
let slot = Self::read_u16(code, &mut ip) as usize;
|
|
1635
|
+
let v = self
|
|
1636
|
+
.stack
|
|
1637
|
+
.pop()
|
|
1638
|
+
.ok_or_else(|| "Stack underflow in StoreLocal".to_string())?;
|
|
1639
|
+
match slot_locals.get_mut(slot) {
|
|
1640
|
+
Some(dst) => *dst = v,
|
|
1641
|
+
None => return Err(format!("Local slot out of bounds: {}", slot)),
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
Opcode::LoadUpvalue | Opcode::StoreUpvalue => {
|
|
1645
|
+
// Reserved for the linked-frame upvalue model (not emitted yet).
|
|
1646
|
+
return Err("Upvalue opcodes not supported in this VM build".to_string());
|
|
1647
|
+
}
|
|
952
1648
|
Opcode::LoadConst => {
|
|
953
1649
|
let idx = Self::read_u16(code, &mut ip);
|
|
954
1650
|
let c = constants
|
|
@@ -956,30 +1652,84 @@ impl Vm {
|
|
|
956
1652
|
.ok_or_else(|| format!("Constant index out of bounds: {}", idx))?;
|
|
957
1653
|
let v = match c {
|
|
958
1654
|
Constant::Number(n) => Value::Number(*n),
|
|
959
|
-
Constant::String(s) => Value::String(
|
|
1655
|
+
Constant::String(s) => Value::String(tishlang_core::ArcStr::from(s.as_ref())),
|
|
960
1656
|
Constant::Bool(b) => Value::Bool(*b),
|
|
961
1657
|
Constant::Null => Value::Null,
|
|
962
1658
|
Constant::Closure(nested_idx) => {
|
|
963
1659
|
let inner = nested
|
|
964
1660
|
.get(*nested_idx)
|
|
965
1661
|
.ok_or_else(|| "Nested chunk index out of bounds".to_string())?;
|
|
1662
|
+
// Numeric JIT fast path (native codegen, non-wasm): if this is a
|
|
1663
|
+
// straight-line numeric function, compile it once (cached per chunk)
|
|
1664
|
+
// and call native code when all args are numbers; else fall back to
|
|
1665
|
+
// the interpreter below. Purely additive — can't change behaviour.
|
|
1666
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1667
|
+
let jit_fn = crate::jit::try_compile_numeric(inner);
|
|
966
1668
|
let inner_clone = inner.clone();
|
|
967
1669
|
let globals = self.globals.clone();
|
|
968
|
-
|
|
1670
|
+
// The closure captures its defining frame's scope PLUS that frame's own
|
|
1671
|
+
// enclosing chain, so functions nested arbitrarily deep still resolve
|
|
1672
|
+
// every ancestor's locals (innermost first).
|
|
1673
|
+
// A closure must capture a real scope (even if empty) so that, post-creation,
|
|
1674
|
+
// the parent's name-based locals are visible. Materialise local_scope here.
|
|
1675
|
+
let captured_scope: ScopeMap = ls_get_or_init!().clone();
|
|
1676
|
+
let enclosing_chain: SharedChain = SharedChain::new(if active_loop_vars.is_empty() {
|
|
1677
|
+
let mut chain = Vec::with_capacity(self.enclosing.len() + 1);
|
|
1678
|
+
chain.push(captured_scope.clone());
|
|
1679
|
+
chain.extend(self.enclosing.iter().cloned());
|
|
1680
|
+
chain
|
|
1681
|
+
} else {
|
|
1682
|
+
// Per-iteration `let`: freeze the loop var(s) into an overlay that
|
|
1683
|
+
// shadows the still-shared frame scope, then the inherited chain.
|
|
1684
|
+
let mut overlay = ObjectMap::default();
|
|
1685
|
+
{
|
|
1686
|
+
let ls = captured_scope.borrow();
|
|
1687
|
+
for n in &active_loop_vars {
|
|
1688
|
+
if let Some(v) = ls.get(n.as_ref()) {
|
|
1689
|
+
overlay.insert(Arc::clone(n), v.clone());
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
let mut chain = Vec::with_capacity(self.enclosing.len() + 2);
|
|
1694
|
+
chain.push(VmRef::new(overlay));
|
|
1695
|
+
chain.push(captured_scope.clone());
|
|
1696
|
+
chain.extend(self.enclosing.iter().cloned());
|
|
1697
|
+
chain
|
|
1698
|
+
});
|
|
969
1699
|
let capabilities = Arc::clone(&self.capabilities);
|
|
970
1700
|
let native_modules = self.native_modules.clone();
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1701
|
+
// Frame-eligibility is an O(chunk) bytecode scan; gate it behind the
|
|
1702
|
+
// (cached) frame-VM flag so the DEFAULT path skips it entirely — flag-off
|
|
1703
|
+
// closure creation pays nothing.
|
|
1704
|
+
let frameable = Vm::frame_vm_enabled()
|
|
1705
|
+
&& {
|
|
1706
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1707
|
+
{
|
|
1708
|
+
jit_fn.is_none() && Vm::chunk_frame_eligible(&inner_clone)
|
|
1709
|
+
}
|
|
1710
|
+
#[cfg(target_arch = "wasm32")]
|
|
1711
|
+
{
|
|
1712
|
+
Vm::chunk_frame_eligible(&inner_clone)
|
|
1713
|
+
}
|
|
979
1714
|
};
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1715
|
+
let vmclosure = VmClosure {
|
|
1716
|
+
chunk: std::sync::Arc::new(inner_clone),
|
|
1717
|
+
frameable,
|
|
1718
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
1719
|
+
jit_fn,
|
|
1720
|
+
enclosing: enclosing_chain,
|
|
1721
|
+
globals,
|
|
1722
|
+
capabilities,
|
|
1723
|
+
native_modules,
|
|
1724
|
+
};
|
|
1725
|
+
#[cfg(feature = "send-values")]
|
|
1726
|
+
{
|
|
1727
|
+
Value::Function(std::sync::Arc::new(vmclosure))
|
|
1728
|
+
}
|
|
1729
|
+
#[cfg(not(feature = "send-values"))]
|
|
1730
|
+
{
|
|
1731
|
+
Value::Function(std::rc::Rc::new(vmclosure))
|
|
1732
|
+
}
|
|
983
1733
|
}
|
|
984
1734
|
};
|
|
985
1735
|
self.stack.push(v);
|
|
@@ -990,13 +1740,13 @@ impl Vm {
|
|
|
990
1740
|
.get(idx as usize)
|
|
991
1741
|
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
992
1742
|
let v = local_scope
|
|
993
|
-
.
|
|
994
|
-
.get(name.as_ref())
|
|
995
|
-
.cloned()
|
|
1743
|
+
.as_ref()
|
|
1744
|
+
.and_then(|ls| ls.borrow().get(name.as_ref()).cloned())
|
|
996
1745
|
.or_else(|| {
|
|
1746
|
+
// Walk the captured lexical chain, innermost first.
|
|
997
1747
|
self.enclosing
|
|
998
|
-
.
|
|
999
|
-
.
|
|
1748
|
+
.iter()
|
|
1749
|
+
.find_map(|e| e.borrow().get(name.as_ref()).cloned())
|
|
1000
1750
|
})
|
|
1001
1751
|
.or_else(|| self.scope.get(name.as_ref()).cloned())
|
|
1002
1752
|
.or_else(|| self.globals.borrow().get(name.as_ref()).cloned())
|
|
@@ -1013,26 +1763,26 @@ impl Vm {
|
|
|
1013
1763
|
.pop()
|
|
1014
1764
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1015
1765
|
// Update innermost scope that has the variable (matches interpreter Scope.assign)
|
|
1016
|
-
if local_scope.borrow().contains_key(name.as_ref()) {
|
|
1017
|
-
|
|
1018
|
-
} else if self
|
|
1766
|
+
if local_scope.as_ref().is_some_and(|ls| ls.borrow().contains_key(name.as_ref())) {
|
|
1767
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1768
|
+
} else if let Some(e) = self
|
|
1019
1769
|
.enclosing
|
|
1020
|
-
.
|
|
1021
|
-
.
|
|
1022
|
-
.unwrap_or(false)
|
|
1770
|
+
.iter()
|
|
1771
|
+
.find(|e| e.borrow().contains_key(name.as_ref()))
|
|
1023
1772
|
{
|
|
1024
|
-
|
|
1025
|
-
|
|
1773
|
+
// Innermost captured scope that already binds the name (matches the
|
|
1774
|
+
// interpreter's Scope.assign walking the lexical chain).
|
|
1775
|
+
e.borrow_mut().insert(Arc::clone(name), v);
|
|
1026
1776
|
} else if self.scope.contains_key(name.as_ref()) {
|
|
1027
1777
|
self.scope.insert(Arc::clone(name), v);
|
|
1028
1778
|
} else if self.globals.borrow().contains_key(name.as_ref()) {
|
|
1029
1779
|
self.globals.borrow_mut().insert(Arc::clone(name), v);
|
|
1030
1780
|
} else {
|
|
1031
1781
|
// New variable: at top level (no enclosing) store in globals so REPL persists across lines
|
|
1032
|
-
if self.enclosing.
|
|
1782
|
+
if self.enclosing.is_empty() {
|
|
1033
1783
|
self.globals.borrow_mut().insert(Arc::clone(name), v);
|
|
1034
1784
|
} else {
|
|
1035
|
-
|
|
1785
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1036
1786
|
}
|
|
1037
1787
|
}
|
|
1038
1788
|
}
|
|
@@ -1046,16 +1796,18 @@ impl Vm {
|
|
|
1046
1796
|
.pop()
|
|
1047
1797
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1048
1798
|
if let Some(frame) = block_undo_stack.last_mut() {
|
|
1049
|
-
let old = local_scope
|
|
1799
|
+
let old = local_scope
|
|
1800
|
+
.as_ref()
|
|
1801
|
+
.and_then(|ls| ls.borrow().get(name.as_ref()).cloned());
|
|
1050
1802
|
frame.push((Arc::clone(name), old));
|
|
1051
1803
|
}
|
|
1052
1804
|
// REPL: persist top-level bindings only (not block-locals shadowing globals).
|
|
1053
|
-
if repl_mode && self.enclosing.
|
|
1805
|
+
if repl_mode && self.enclosing.is_empty() && block_undo_stack.is_empty() {
|
|
1054
1806
|
self.globals
|
|
1055
1807
|
.borrow_mut()
|
|
1056
1808
|
.insert(Arc::clone(name), v.clone());
|
|
1057
1809
|
}
|
|
1058
|
-
|
|
1810
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1059
1811
|
}
|
|
1060
1812
|
Opcode::DeclareVarPlain => {
|
|
1061
1813
|
let idx = Self::read_u16(code, &mut ip);
|
|
@@ -1066,12 +1818,12 @@ impl Vm {
|
|
|
1066
1818
|
.stack
|
|
1067
1819
|
.pop()
|
|
1068
1820
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1069
|
-
if repl_mode && self.enclosing.
|
|
1821
|
+
if repl_mode && self.enclosing.is_empty() && block_undo_stack.is_empty() {
|
|
1070
1822
|
self.globals
|
|
1071
1823
|
.borrow_mut()
|
|
1072
1824
|
.insert(Arc::clone(name), v.clone());
|
|
1073
1825
|
}
|
|
1074
|
-
|
|
1826
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1075
1827
|
}
|
|
1076
1828
|
Opcode::EnterBlock => {
|
|
1077
1829
|
block_undo_stack.push(Vec::new());
|
|
@@ -1081,7 +1833,7 @@ impl Vm {
|
|
|
1081
1833
|
.pop()
|
|
1082
1834
|
.ok_or_else(|| "ExitBlock without matching EnterBlock".to_string())?;
|
|
1083
1835
|
for (name, old) in frame.into_iter().rev() {
|
|
1084
|
-
let mut ls =
|
|
1836
|
+
let mut ls = ls_get_or_init!().borrow_mut();
|
|
1085
1837
|
match old {
|
|
1086
1838
|
Some(prev) => {
|
|
1087
1839
|
ls.insert(name, prev);
|
|
@@ -1092,6 +1844,23 @@ impl Vm {
|
|
|
1092
1844
|
}
|
|
1093
1845
|
}
|
|
1094
1846
|
}
|
|
1847
|
+
Opcode::LoopVarsBegin => {
|
|
1848
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1849
|
+
let name = names
|
|
1850
|
+
.get(idx as usize)
|
|
1851
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1852
|
+
active_loop_vars.push(Arc::clone(name));
|
|
1853
|
+
}
|
|
1854
|
+
Opcode::LoopVarsEnd => {
|
|
1855
|
+
active_loop_vars.pop();
|
|
1856
|
+
}
|
|
1857
|
+
Opcode::ArgMissing => {
|
|
1858
|
+
// True iff the positional arg at `idx` was not supplied → the function
|
|
1859
|
+
// prologue applies the param's default. Matches the interpreter: an
|
|
1860
|
+
// explicit `null` arg is "supplied" and keeps the `null`.
|
|
1861
|
+
let idx = Self::read_u16(code, &mut ip) as usize;
|
|
1862
|
+
self.stack.push(Value::Bool(idx >= args.len()));
|
|
1863
|
+
}
|
|
1095
1864
|
Opcode::LoadGlobal => {
|
|
1096
1865
|
let idx = Self::read_u16(code, &mut ip);
|
|
1097
1866
|
let name = names
|
|
@@ -1137,6 +1906,19 @@ impl Vm {
|
|
|
1137
1906
|
.clone();
|
|
1138
1907
|
self.stack.push(v);
|
|
1139
1908
|
}
|
|
1909
|
+
Opcode::IterNormalize => {
|
|
1910
|
+
// `for…of`: turn a JS iterator object (callable `next()` → `{ value, done }`,
|
|
1911
|
+
// e.g. a Map/Set `.values()` result) into an array so the index loop iterates
|
|
1912
|
+
// it. Arrays/strings/everything else pass through unchanged.
|
|
1913
|
+
let v = self
|
|
1914
|
+
.stack
|
|
1915
|
+
.last()
|
|
1916
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1917
|
+
if let Some(items) = tishlang_core::drain_iterator(v) {
|
|
1918
|
+
self.stack.pop();
|
|
1919
|
+
self.stack.push(Value::Array(VmRef::new(items)));
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1140
1922
|
Opcode::Call => {
|
|
1141
1923
|
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
1142
1924
|
let mut args = Vec::with_capacity(argc);
|
|
@@ -1152,25 +1934,88 @@ impl Vm {
|
|
|
1152
1934
|
.stack
|
|
1153
1935
|
.pop()
|
|
1154
1936
|
.ok_or_else(|| "Stack underflow: no callee".to_string())?;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1937
|
+
// Call the function in place — no `Arc` clone on the hot direct-call path. The
|
|
1938
|
+
// immutable borrow of `callee` is held only across the call, which never touches it.
|
|
1939
|
+
let result = match &callee {
|
|
1940
|
+
Value::Function(f) => {
|
|
1941
|
+
// Frame-VM (flag-on): a frameable VmClosure runs on the heap frame stack
|
|
1942
|
+
// (iterative, no per-call Vm / native recursion). Else the normal path.
|
|
1943
|
+
if Self::frame_vm_enabled() {
|
|
1944
|
+
match f.as_any().downcast_ref::<VmClosure>() {
|
|
1945
|
+
Some(vc) if Self::vmclosure_frameable(vc) => {
|
|
1946
|
+
match self.run_framed(vc, &args) {
|
|
1947
|
+
// A pending throw is handled by the post-call check
|
|
1948
|
+
// below (issue #60); a real fatal error propagates.
|
|
1949
|
+
Some(Ok(v)) => v,
|
|
1950
|
+
Some(Err(e)) if e == PENDING_THROW_SENTINEL => {
|
|
1951
|
+
Value::Null
|
|
1952
|
+
}
|
|
1953
|
+
Some(Err(e)) => return Err(e),
|
|
1954
|
+
None => f.call(&args),
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
_ => f.call(&args),
|
|
1958
|
+
}
|
|
1162
1959
|
} else {
|
|
1163
|
-
|
|
1164
|
-
"Call of non-function: {}",
|
|
1165
|
-
callee.type_name()
|
|
1166
|
-
));
|
|
1960
|
+
f.call(&args)
|
|
1167
1961
|
}
|
|
1168
1962
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1963
|
+
Value::Object(o) => {
|
|
1964
|
+
let call_fn = match o.borrow().strings.get("__call") {
|
|
1965
|
+
Some(Value::Function(cf)) => cf.clone(),
|
|
1966
|
+
_ => raise!(construct_builtin::error_object(
|
|
1967
|
+
"TypeError",
|
|
1968
|
+
&format!("Call of non-function: {}", callee.type_name())
|
|
1969
|
+
)),
|
|
1970
|
+
};
|
|
1971
|
+
call_fn.call(&args)
|
|
1171
1972
|
}
|
|
1973
|
+
_ => raise!(construct_builtin::error_object(
|
|
1974
|
+
"TypeError",
|
|
1975
|
+
&format!("Call of non-function: {}", callee.type_name())
|
|
1976
|
+
)),
|
|
1172
1977
|
};
|
|
1173
|
-
|
|
1978
|
+
// A throw that escaped the callee's own `catch` is parked in the thread-local;
|
|
1979
|
+
// surface it here so this frame's `try` (if any) can catch it (issue #60).
|
|
1980
|
+
if let Some(v) = take_pending_throw() {
|
|
1981
|
+
raise!(v);
|
|
1982
|
+
}
|
|
1983
|
+
self.stack.push(result);
|
|
1984
|
+
}
|
|
1985
|
+
Opcode::SelfCall => {
|
|
1986
|
+
// Direct recursive call to the CURRENT function (`chunk`). The compiler emits
|
|
1987
|
+
// this only when the function's own name is provably stable, so the callee is
|
|
1988
|
+
// implicitly `chunk` — no callee on the stack, no name lookup, no closure
|
|
1989
|
+
// dispatch. Behaviour matches `LoadVar name; Call argc` (a closure call that
|
|
1990
|
+
// swallows errors to Null), and uses the SAME captured `enclosing`.
|
|
1991
|
+
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
1992
|
+
let mut args = Vec::with_capacity(argc);
|
|
1993
|
+
for _ in 0..argc {
|
|
1994
|
+
args.push(
|
|
1995
|
+
self.stack
|
|
1996
|
+
.pop()
|
|
1997
|
+
.ok_or_else(|| "Stack underflow in self-call".to_string())?,
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
args.reverse();
|
|
2001
|
+
let mut vm = Vm {
|
|
2002
|
+
stack: Vec::new(),
|
|
2003
|
+
scope: ObjectMap::default(),
|
|
2004
|
+
enclosing: self.enclosing.clone(),
|
|
2005
|
+
globals: self.globals.clone(),
|
|
2006
|
+
capabilities: Arc::clone(&self.capabilities),
|
|
2007
|
+
native_modules: self.native_modules.clone(),
|
|
2008
|
+
};
|
|
2009
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2010
|
+
let result = stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
2011
|
+
vm.run_chunk(chunk, nested, &args, false)
|
|
2012
|
+
.unwrap_or(Value::Null)
|
|
2013
|
+
});
|
|
2014
|
+
#[cfg(target_arch = "wasm32")]
|
|
2015
|
+
let result = vm.run_chunk(chunk, nested, &args, false).unwrap_or(Value::Null);
|
|
2016
|
+
if let Some(v) = take_pending_throw() {
|
|
2017
|
+
raise!(v);
|
|
2018
|
+
}
|
|
1174
2019
|
self.stack.push(result);
|
|
1175
2020
|
}
|
|
1176
2021
|
Opcode::CallSpread => {
|
|
@@ -1182,6 +2027,11 @@ impl Vm {
|
|
|
1182
2027
|
.stack
|
|
1183
2028
|
.pop()
|
|
1184
2029
|
.ok_or_else(|| "Stack underflow in CallSpread".to_string())?;
|
|
2030
|
+
// A lone iterator spread (`f(...m.values())`) — drain to an array.
|
|
2031
|
+
let args_array = match tishlang_core::drain_iterator(&args_array) {
|
|
2032
|
+
Some(items) => Value::Array(VmRef::new(items)),
|
|
2033
|
+
None => args_array,
|
|
2034
|
+
};
|
|
1185
2035
|
let args: Vec<Value> = match &args_array {
|
|
1186
2036
|
Value::Array(a) => a.borrow().clone(),
|
|
1187
2037
|
_ => {
|
|
@@ -1209,7 +2059,10 @@ impl Vm {
|
|
|
1209
2059
|
return Err(format!("Call of non-function: {}", callee.type_name()));
|
|
1210
2060
|
}
|
|
1211
2061
|
};
|
|
1212
|
-
let result = f(&args);
|
|
2062
|
+
let result = f.call(&args);
|
|
2063
|
+
if let Some(v) = take_pending_throw() {
|
|
2064
|
+
raise!(v);
|
|
2065
|
+
}
|
|
1213
2066
|
self.stack.push(result);
|
|
1214
2067
|
}
|
|
1215
2068
|
Opcode::Construct => {
|
|
@@ -1228,6 +2081,9 @@ impl Vm {
|
|
|
1228
2081
|
.pop()
|
|
1229
2082
|
.ok_or_else(|| "Stack underflow: no callee for construct".to_string())?;
|
|
1230
2083
|
let result = construct_builtin::construct(&callee, &args);
|
|
2084
|
+
if let Some(v) = take_pending_throw() {
|
|
2085
|
+
raise!(v);
|
|
2086
|
+
}
|
|
1231
2087
|
self.stack.push(result);
|
|
1232
2088
|
}
|
|
1233
2089
|
Opcode::ConstructSpread => {
|
|
@@ -1239,6 +2095,11 @@ impl Vm {
|
|
|
1239
2095
|
.stack
|
|
1240
2096
|
.pop()
|
|
1241
2097
|
.ok_or_else(|| "Stack underflow in ConstructSpread".to_string())?;
|
|
2098
|
+
// A lone iterator spread (`new X(...m.values())`) — drain to an array.
|
|
2099
|
+
let args_array = match tishlang_core::drain_iterator(&args_array) {
|
|
2100
|
+
Some(items) => Value::Array(VmRef::new(items)),
|
|
2101
|
+
None => args_array,
|
|
2102
|
+
};
|
|
1242
2103
|
let args: Vec<Value> = match &args_array {
|
|
1243
2104
|
Value::Array(a) => a.borrow().clone(),
|
|
1244
2105
|
_ => {
|
|
@@ -1249,6 +2110,9 @@ impl Vm {
|
|
|
1249
2110
|
}
|
|
1250
2111
|
};
|
|
1251
2112
|
let result = construct_builtin::construct(&callee, &args);
|
|
2113
|
+
if let Some(v) = take_pending_throw() {
|
|
2114
|
+
raise!(v);
|
|
2115
|
+
}
|
|
1252
2116
|
self.stack.push(result);
|
|
1253
2117
|
}
|
|
1254
2118
|
Opcode::Return => {
|
|
@@ -1308,7 +2172,7 @@ impl Vm {
|
|
|
1308
2172
|
.stack
|
|
1309
2173
|
.pop()
|
|
1310
2174
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1311
|
-
let v =
|
|
2175
|
+
let v = catchable!(ic_get_member(chunk, idx, &obj, key));
|
|
1312
2176
|
self.stack.push(v);
|
|
1313
2177
|
}
|
|
1314
2178
|
Opcode::GetMemberOptional => {
|
|
@@ -1320,7 +2184,7 @@ impl Vm {
|
|
|
1320
2184
|
.stack
|
|
1321
2185
|
.pop()
|
|
1322
2186
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1323
|
-
let v =
|
|
2187
|
+
let v = ic_get_member(chunk, idx, &obj, key).unwrap_or(Value::Null);
|
|
1324
2188
|
self.stack.push(v);
|
|
1325
2189
|
}
|
|
1326
2190
|
Opcode::SetMember => {
|
|
@@ -1336,7 +2200,7 @@ impl Vm {
|
|
|
1336
2200
|
.stack
|
|
1337
2201
|
.pop()
|
|
1338
2202
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1339
|
-
|
|
2203
|
+
catchable!(ic_set_member(chunk, idx, &obj, key, val.clone()));
|
|
1340
2204
|
self.stack.push(val); // assignment yields value
|
|
1341
2205
|
}
|
|
1342
2206
|
Opcode::GetIndex => {
|
|
@@ -1348,7 +2212,7 @@ impl Vm {
|
|
|
1348
2212
|
.stack
|
|
1349
2213
|
.pop()
|
|
1350
2214
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1351
|
-
let v = get_index(&obj, &idx_val)
|
|
2215
|
+
let v = catchable!(get_index(&obj, &idx_val));
|
|
1352
2216
|
self.stack.push(v);
|
|
1353
2217
|
}
|
|
1354
2218
|
Opcode::SetIndex => {
|
|
@@ -1370,9 +2234,22 @@ impl Vm {
|
|
|
1370
2234
|
.stack
|
|
1371
2235
|
.pop()
|
|
1372
2236
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1373
|
-
set_index(&obj, &idx_val, val.clone())
|
|
2237
|
+
catchable!(set_index(&obj, &idx_val, val.clone()));
|
|
1374
2238
|
self.stack.push(dup_val); // assignment yields the assigned value
|
|
1375
2239
|
}
|
|
2240
|
+
Opcode::DeleteIndex => {
|
|
2241
|
+
// `delete obj[key]` / `delete obj.prop`: pop [obj, key], remove, push true.
|
|
2242
|
+
let key = self
|
|
2243
|
+
.stack
|
|
2244
|
+
.pop()
|
|
2245
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2246
|
+
let obj = self
|
|
2247
|
+
.stack
|
|
2248
|
+
.pop()
|
|
2249
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2250
|
+
delete_index(&obj, &key);
|
|
2251
|
+
self.stack.push(Value::Bool(true));
|
|
2252
|
+
}
|
|
1376
2253
|
Opcode::NewArray => {
|
|
1377
2254
|
let n = Self::read_u16(code, &mut ip) as usize;
|
|
1378
2255
|
let mut elems = Vec::with_capacity(n);
|
|
@@ -1384,24 +2261,50 @@ impl Vm {
|
|
|
1384
2261
|
);
|
|
1385
2262
|
}
|
|
1386
2263
|
elems.reverse();
|
|
1387
|
-
|
|
2264
|
+
// Packed-array fast path: if every element is a number AND there is at
|
|
2265
|
+
// least one element, store as Vec<f64>. Empty arrays stay as Value::Array
|
|
2266
|
+
// because they are commonly used as general-purpose containers (the type
|
|
2267
|
+
// can't be inferred from zero elements).
|
|
2268
|
+
if Value::packed_arrays_enabled() && !elems.is_empty() {
|
|
2269
|
+
if let Some(nums) = elems.iter().try_fold(
|
|
2270
|
+
Vec::<f64>::with_capacity(elems.len()),
|
|
2271
|
+
|mut acc, v| {
|
|
2272
|
+
if let Value::Number(n) = v { acc.push(*n); Some(acc) }
|
|
2273
|
+
else { None }
|
|
2274
|
+
},
|
|
2275
|
+
) {
|
|
2276
|
+
self.stack.push(Value::number_array(nums));
|
|
2277
|
+
} else {
|
|
2278
|
+
self.stack.push(Value::Array(VmRef::new(elems)));
|
|
2279
|
+
}
|
|
2280
|
+
} else {
|
|
2281
|
+
self.stack.push(Value::Array(VmRef::new(elems)));
|
|
2282
|
+
}
|
|
1388
2283
|
}
|
|
1389
2284
|
Opcode::NewObject => {
|
|
1390
2285
|
let n = Self::read_u16(code, &mut ip) as usize;
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
let
|
|
2286
|
+
if self.stack.len() < 2 * n {
|
|
2287
|
+
return Err("Stack underflow".to_string());
|
|
2288
|
+
}
|
|
2289
|
+
// Pairs sit on the stack in source order: key1,val1,…,keyN,valN. Read them
|
|
2290
|
+
// in place into the PropMap (insertion order = JS order) and drop them in
|
|
2291
|
+
// one truncate — no intermediate Vec per object literal (a hot path: every
|
|
2292
|
+
// `{...}` and every HTTP JSON response).
|
|
2293
|
+
let base = self.stack.len() - 2 * n;
|
|
2294
|
+
let mut map = PropMap::with_capacity(n);
|
|
2295
|
+
for i in 0..n {
|
|
2296
|
+
let key_val =
|
|
2297
|
+
std::mem::replace(&mut self.stack[base + 2 * i], Value::Null);
|
|
2298
|
+
let val =
|
|
2299
|
+
std::mem::replace(&mut self.stack[base + 2 * i + 1], Value::Null);
|
|
2300
|
+
let key: Arc<str> = key_val.to_display_string().into();
|
|
1402
2301
|
map.insert(key, val);
|
|
1403
2302
|
}
|
|
1404
|
-
self.stack.
|
|
2303
|
+
self.stack.truncate(base);
|
|
2304
|
+
self.stack.push(Value::Object(VmRef::new(ObjectData {
|
|
2305
|
+
strings: map,
|
|
2306
|
+
symbols: None,
|
|
2307
|
+
})));
|
|
1405
2308
|
}
|
|
1406
2309
|
Opcode::EnterTry => {
|
|
1407
2310
|
let offset = Self::read_u16(code, &mut ip) as usize;
|
|
@@ -1420,6 +2323,18 @@ impl Vm {
|
|
|
1420
2323
|
.stack
|
|
1421
2324
|
.pop()
|
|
1422
2325
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2326
|
+
// Materialise NumberArray on either side before concatenation.
|
|
2327
|
+
let left = left.coerce_number_array();
|
|
2328
|
+
let right = right.coerce_number_array();
|
|
2329
|
+
// Spread of a Map/Set iterator (`[...m.values()]`): drain to an array.
|
|
2330
|
+
let left = match tishlang_core::drain_iterator(&left) {
|
|
2331
|
+
Some(items) => Value::Array(VmRef::new(items)),
|
|
2332
|
+
None => left,
|
|
2333
|
+
};
|
|
2334
|
+
let right = match tishlang_core::drain_iterator(&right) {
|
|
2335
|
+
Some(items) => Value::Array(VmRef::new(items)),
|
|
2336
|
+
None => right,
|
|
2337
|
+
};
|
|
1423
2338
|
let (mut a, b) = (
|
|
1424
2339
|
match &left {
|
|
1425
2340
|
Value::Array(arr) => arr.borrow().clone(),
|
|
@@ -1507,6 +2422,8 @@ impl Vm {
|
|
|
1507
2422
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1508
2423
|
let result = match &arr {
|
|
1509
2424
|
Value::Array(a) => Value::Array(VmRef::new(a.borrow().clone())),
|
|
2425
|
+
// Identity map on a NumberArray = clone the packed vec (stays packed).
|
|
2426
|
+
Value::NumberArray(a) => Value::NumberArray(VmRef::new(a.borrow().clone())),
|
|
1510
2427
|
_ => Value::Null,
|
|
1511
2428
|
};
|
|
1512
2429
|
self.stack.push(result);
|
|
@@ -1527,27 +2444,38 @@ impl Vm {
|
|
|
1527
2444
|
.get(const_idx as usize)
|
|
1528
2445
|
.map(|c| c.to_value())
|
|
1529
2446
|
.unwrap_or(Value::Null);
|
|
1530
|
-
let result =
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
.
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
const_val.clone()
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2447
|
+
let result = match &arr {
|
|
2448
|
+
Value::NumberArray(a) => {
|
|
2449
|
+
// All-numeric fast path: operate on raw f64, no boxing/unboxing.
|
|
2450
|
+
let arr_borrow = a.borrow();
|
|
2451
|
+
let mapped: Vec<Value> = arr_borrow
|
|
2452
|
+
.iter()
|
|
2453
|
+
.map(|&n| {
|
|
2454
|
+
let elem = Value::Number(n);
|
|
2455
|
+
let (l, r) = if param_left { (elem, const_val.clone()) } else { (const_val.clone(), elem) };
|
|
2456
|
+
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
|
|
2457
|
+
})
|
|
2458
|
+
.collect();
|
|
2459
|
+
// If every result is numeric, stay packed (the common case for x*2, x+1, etc).
|
|
2460
|
+
if mapped.iter().all(|v| matches!(v, Value::Number(_))) {
|
|
2461
|
+
Value::number_array(mapped.into_iter().map(|v| match v { Value::Number(n) => n, _ => unreachable!() }).collect())
|
|
2462
|
+
} else {
|
|
2463
|
+
Value::Array(VmRef::new(mapped))
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
Value::Array(a) => {
|
|
2467
|
+
let arr_borrow = a.borrow();
|
|
2468
|
+
let mapped: Vec<Value> = arr_borrow
|
|
2469
|
+
.iter()
|
|
2470
|
+
.map(|v| {
|
|
2471
|
+
let l: Value = if param_left { (*v).clone() } else { const_val.clone() };
|
|
2472
|
+
let r: Value = if param_left { const_val.clone() } else { (*v).clone() };
|
|
2473
|
+
eval_binop(binop, &l, &r).unwrap_or(Value::Null)
|
|
2474
|
+
})
|
|
2475
|
+
.collect();
|
|
2476
|
+
Value::Array(VmRef::new(mapped))
|
|
2477
|
+
}
|
|
2478
|
+
_ => Value::Null,
|
|
1551
2479
|
};
|
|
1552
2480
|
self.stack.push(result);
|
|
1553
2481
|
}
|
|
@@ -1568,29 +2496,33 @@ impl Vm {
|
|
|
1568
2496
|
.get(const_idx as usize)
|
|
1569
2497
|
.map(|c| c.to_value())
|
|
1570
2498
|
.unwrap_or(Value::Null);
|
|
1571
|
-
let result =
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
(
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2499
|
+
let result = match &arr {
|
|
2500
|
+
Value::NumberArray(a) => {
|
|
2501
|
+
let arr_borrow = a.borrow();
|
|
2502
|
+
let filtered: Vec<f64> = arr_borrow
|
|
2503
|
+
.iter()
|
|
2504
|
+
.filter(|&&n| {
|
|
2505
|
+
let elem = Value::Number(n);
|
|
2506
|
+
let (l, r) = if param_left { (elem, const_val.clone()) } else { (const_val.clone(), elem) };
|
|
2507
|
+
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
2508
|
+
})
|
|
2509
|
+
.copied()
|
|
2510
|
+
.collect();
|
|
2511
|
+
Value::number_array(filtered)
|
|
2512
|
+
}
|
|
2513
|
+
Value::Array(a) => {
|
|
2514
|
+
let arr_borrow = a.borrow();
|
|
2515
|
+
let filtered: Vec<Value> = arr_borrow
|
|
2516
|
+
.iter()
|
|
2517
|
+
.filter(|v| {
|
|
2518
|
+
let (l, r) = if param_left { ((*v).clone(), const_val.clone()) } else { (const_val.clone(), (*v).clone()) };
|
|
2519
|
+
eval_binop(binop, &l, &r).unwrap_or(Value::Null).is_truthy()
|
|
2520
|
+
})
|
|
2521
|
+
.cloned()
|
|
2522
|
+
.collect();
|
|
2523
|
+
Value::Array(VmRef::new(filtered))
|
|
2524
|
+
}
|
|
2525
|
+
_ => Value::Null,
|
|
1594
2526
|
};
|
|
1595
2527
|
self.stack.push(result);
|
|
1596
2528
|
}
|
|
@@ -1599,7 +2531,7 @@ impl Vm {
|
|
|
1599
2531
|
.stack
|
|
1600
2532
|
.pop()
|
|
1601
2533
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1602
|
-
|
|
2534
|
+
raise!(v);
|
|
1603
2535
|
}
|
|
1604
2536
|
Opcode::AwaitPromise => {
|
|
1605
2537
|
let v = self
|
|
@@ -1653,7 +2585,9 @@ impl Vm {
|
|
|
1653
2585
|
// on the cranelift / llvm backends that want to expose
|
|
1654
2586
|
// `cargo:…` Rust crates should register the module's
|
|
1655
2587
|
// exports map before calling `vm.run(chunk)`.
|
|
1656
|
-
let from_registry: Option<Value> = if spec.starts_with("cargo:")
|
|
2588
|
+
let from_registry: Option<Value> = if spec.starts_with("cargo:")
|
|
2589
|
+
|| spec.starts_with("ffi:")
|
|
2590
|
+
{
|
|
1657
2591
|
let regs = self.native_modules.borrow();
|
|
1658
2592
|
regs.get(spec)
|
|
1659
2593
|
.and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
|
|
@@ -1714,23 +2648,16 @@ fn estimate_string_concat_len(v: &Value) -> usize {
|
|
|
1714
2648
|
/// Append JS-style string conversion without an intermediate `String` per operand (unlike
|
|
1715
2649
|
/// `format!("{}{}", a.to_display_string(), b.to_display_string())`, which triple-allocates).
|
|
1716
2650
|
fn append_value_for_string_concat(out: &mut String, v: &Value) {
|
|
1717
|
-
use std::fmt::Write;
|
|
1718
2651
|
match v {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
} else if *n == f64::INFINITY {
|
|
1723
|
-
out.push_str("Infinity");
|
|
1724
|
-
} else if *n == f64::NEG_INFINITY {
|
|
1725
|
-
out.push_str("-Infinity");
|
|
1726
|
-
} else {
|
|
1727
|
-
let _ = write!(out, "{n}");
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
2652
|
+
// JS `Number.prototype.toString` (exponential past digit 21 / before −6), shared
|
|
2653
|
+
// with `console.log` so `"" + n` and `` `${n}` `` match Node exactly.
|
|
2654
|
+
Value::Number(n) => out.push_str(&tishlang_core::js_number_to_string(*n)),
|
|
1730
2655
|
Value::String(s) => out.push_str(s.as_ref()),
|
|
1731
2656
|
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
|
1732
2657
|
Value::Null => out.push_str("null"),
|
|
1733
|
-
|
|
2658
|
+
// Arrays/objects use JS `ToString` (recursive comma-join / "[object Object]"),
|
|
2659
|
+
// not the inspect form, so `"" + [1,[2,3]]` and templates match Node.
|
|
2660
|
+
_ => out.push_str(&v.to_js_string()),
|
|
1734
2661
|
}
|
|
1735
2662
|
}
|
|
1736
2663
|
|
|
@@ -1753,29 +2680,68 @@ fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
|
|
|
1753
2680
|
}
|
|
1754
2681
|
Sub => Ok(Number(ln - rn)),
|
|
1755
2682
|
Mul => Ok(Number(ln * rn)),
|
|
1756
|
-
|
|
1757
|
-
|
|
2683
|
+
// IEEE division/remainder, matching JS (and the interp + rust-AOT backends): `5/0` → Infinity,
|
|
2684
|
+
// `-5/0` → -Infinity, `0/0` → NaN, `5%0` → NaN. The former `if rn==0 { NaN }` special-case made
|
|
2685
|
+
// the VM the only backend that returned NaN for `n/0` at runtime (literals were masked by
|
|
2686
|
+
// constant-folding) — a cross-backend divergence. Null/non-number operands already coerce to
|
|
2687
|
+
// NaN via `as_number().unwrap_or(NaN)` above, so `5/null` stays NaN (tish's null-coercion).
|
|
2688
|
+
Div => Ok(Number(ln / rn)),
|
|
2689
|
+
Mod => Ok(Number(ln % rn)),
|
|
1758
2690
|
Pow => Ok(Number(ln.powf(rn))),
|
|
1759
2691
|
Eq => Ok(Bool(l.strict_eq(r))),
|
|
1760
2692
|
Ne => Ok(Bool(!l.strict_eq(r))),
|
|
1761
2693
|
StrictEq => Ok(Bool(l.strict_eq(r))),
|
|
1762
2694
|
StrictNe => Ok(Bool(!l.strict_eq(r))),
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
2695
|
+
// Relational operators: when BOTH operands are strings, compare them
|
|
2696
|
+
// lexicographically (JS semantics). Otherwise coerce to numbers — a string
|
|
2697
|
+
// mixed with a number still falls through to numeric coercion (NaN → false).
|
|
2698
|
+
Lt => Ok(Bool(match (l, r) {
|
|
2699
|
+
(String(a), String(b)) => a.as_str() < b.as_str(),
|
|
2700
|
+
_ => ln < rn,
|
|
2701
|
+
})),
|
|
2702
|
+
Le => Ok(Bool(match (l, r) {
|
|
2703
|
+
(String(a), String(b)) => a.as_str() <= b.as_str(),
|
|
2704
|
+
_ => ln <= rn,
|
|
2705
|
+
})),
|
|
2706
|
+
Gt => Ok(Bool(match (l, r) {
|
|
2707
|
+
(String(a), String(b)) => a.as_str() > b.as_str(),
|
|
2708
|
+
_ => ln > rn,
|
|
2709
|
+
})),
|
|
2710
|
+
Ge => Ok(Bool(match (l, r) {
|
|
2711
|
+
(String(a), String(b)) => a.as_str() >= b.as_str(),
|
|
2712
|
+
_ => ln >= rn,
|
|
2713
|
+
})),
|
|
1767
2714
|
And => Ok(Bool(l.is_truthy() && r.is_truthy())),
|
|
1768
2715
|
Or => Ok(Bool(l.is_truthy() || r.is_truthy())),
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2716
|
+
// `to_int32`/`to_uint32` = JS ToInt32/ToUint32 (modulo 2³², NaN/±Infinity → 0); not a
|
|
2717
|
+
// saturating cast, so out-of-range operands wrap exactly like JS instead of clamping.
|
|
2718
|
+
BitAnd => Ok(Number((to_int32(ln) & to_int32(rn)) as f64)),
|
|
2719
|
+
BitOr => Ok(Number((to_int32(ln) | to_int32(rn)) as f64)),
|
|
2720
|
+
BitXor => Ok(Number((to_int32(ln) ^ to_int32(rn)) as f64)),
|
|
2721
|
+
// JS shifts mask the count to 5 bits; `wrapping_sh*` matches that and avoids
|
|
2722
|
+
// the debug-mode panic that plain `<<`/`>>` raise for a count of 32+.
|
|
2723
|
+
Shl => Ok(Number(to_int32(ln).wrapping_shl(to_uint32(rn)) as f64)),
|
|
2724
|
+
Shr => Ok(Number(to_int32(ln).wrapping_shr(to_uint32(rn)) as f64)),
|
|
2725
|
+
UShr => Ok(Number(to_uint32(ln).wrapping_shr(to_uint32(rn)) as f64)),
|
|
1774
2726
|
In => Ok(Bool(match r {
|
|
1775
2727
|
Value::Object(_) => object_has(r, l),
|
|
1776
2728
|
Value::Array(a) => {
|
|
1777
2729
|
let key_s: Arc<str> = match l {
|
|
1778
|
-
Value::String(s) => Arc::
|
|
2730
|
+
Value::String(s) => Arc::from(s.as_str()),
|
|
2731
|
+
Value::Number(n) => n.to_string().into(),
|
|
2732
|
+
_ => l.to_display_string().into(),
|
|
2733
|
+
};
|
|
2734
|
+
if key_s.as_ref() == "length" {
|
|
2735
|
+
true
|
|
2736
|
+
} else if let Ok(idx) = key_s.parse::<usize>() {
|
|
2737
|
+
idx < a.borrow().len()
|
|
2738
|
+
} else {
|
|
2739
|
+
false
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
Value::NumberArray(a) => {
|
|
2743
|
+
let key_s: Arc<str> = match l {
|
|
2744
|
+
Value::String(s) => Arc::from(s.as_str()),
|
|
1779
2745
|
Value::Number(n) => n.to_string().into(),
|
|
1780
2746
|
_ => l.to_display_string().into(),
|
|
1781
2747
|
};
|
|
@@ -1799,19 +2765,226 @@ fn eval_unary(op: UnaryOp, o: &Value) -> Result<Value, String> {
|
|
|
1799
2765
|
Not => Ok(Bool(!o.is_truthy())),
|
|
1800
2766
|
Neg => Ok(Number(-o.as_number().unwrap_or(f64::NAN))),
|
|
1801
2767
|
Pos => Ok(Number(o.as_number().unwrap_or(f64::NAN))),
|
|
1802
|
-
BitNot => Ok(Number(!(o.as_number().unwrap_or(0.0)
|
|
2768
|
+
BitNot => Ok(Number(!to_int32(o.as_number().unwrap_or(0.0)) as f64)),
|
|
1803
2769
|
Void => Ok(Null),
|
|
1804
2770
|
}
|
|
1805
2771
|
}
|
|
1806
2772
|
|
|
2773
|
+
/// `GetMember` with the per-name inline cache (JSC-style, Phase 1a). On a shape hit the property is at
|
|
2774
|
+
/// a cached slot index → a direct load, no key hash/compare. A miss (or a non-plain-object, or a
|
|
2775
|
+
/// `DICT_SHAPE` object) falls to [`get_member`] (arrays/strings/`length`/methods/missing-property
|
|
2776
|
+
/// error), refilling the cache when the object *does* have the property. Result-equivalent to
|
|
2777
|
+
/// `get_member` — the cache only skips the lookup; the shape uniquely fixes the slot for a property.
|
|
2778
|
+
#[inline]
|
|
2779
|
+
fn ic_get_member(chunk: &Chunk, name_idx: u16, obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
2780
|
+
use std::sync::atomic::Ordering::Relaxed;
|
|
2781
|
+
if let Value::Object(od) = obj {
|
|
2782
|
+
let b = od.borrow();
|
|
2783
|
+
let shape = b.strings.shape();
|
|
2784
|
+
if shape != tishlang_core::DICT_SHAPE {
|
|
2785
|
+
if let Some(cell) = chunk.inline_caches.0.get(name_idx as usize) {
|
|
2786
|
+
let ic = cell.load(Relaxed);
|
|
2787
|
+
let cached_shape = (ic >> 32) as u32; // 0 == uncached
|
|
2788
|
+
if cached_shape != 0 && cached_shape == shape {
|
|
2789
|
+
if let Some(v) = b.strings.value_at_index((ic & 0xffff_ffff) as usize) {
|
|
2790
|
+
return Ok(v.clone());
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
// Miss: do the real lookup once, and if the property exists, cache its slot.
|
|
2794
|
+
if let Some((v, i)) = b.strings.get_with_index(key.as_ref()) {
|
|
2795
|
+
cell.store(((shape as u64) << 32) | i as u64, Relaxed);
|
|
2796
|
+
return Ok(v.clone());
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
// `b` drops at the end of this block → safe to re-borrow `obj` in `get_member` below.
|
|
2801
|
+
}
|
|
2802
|
+
get_member(obj, key)
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
/// `SetMember` with the per-name inline cache. On a shape hit for an existing property → an in-place
|
|
2806
|
+
/// store at the cached slot (no key lookup, no shape change). Otherwise the slow path inserts (a new
|
|
2807
|
+
/// key transitions the shape) and refills the cache. Non-objects fall to [`set_member`].
|
|
2808
|
+
#[inline]
|
|
2809
|
+
fn ic_set_member(
|
|
2810
|
+
chunk: &Chunk,
|
|
2811
|
+
name_idx: u16,
|
|
2812
|
+
obj: &Value,
|
|
2813
|
+
key: &Arc<str>,
|
|
2814
|
+
val: Value,
|
|
2815
|
+
) -> Result<(), String> {
|
|
2816
|
+
use std::sync::atomic::Ordering::Relaxed;
|
|
2817
|
+
if let Value::Object(od) = obj {
|
|
2818
|
+
let mut b = od.borrow_mut();
|
|
2819
|
+
let shape = b.strings.shape();
|
|
2820
|
+
let cell = chunk.inline_caches.0.get(name_idx as usize);
|
|
2821
|
+
if shape != tishlang_core::DICT_SHAPE {
|
|
2822
|
+
if let Some(c) = cell {
|
|
2823
|
+
let ic = c.load(Relaxed);
|
|
2824
|
+
let cached_shape = (ic >> 32) as u32;
|
|
2825
|
+
if cached_shape != 0 && cached_shape == shape {
|
|
2826
|
+
if let Some(slot) = b.strings.value_at_index_mut((ic & 0xffff_ffff) as usize) {
|
|
2827
|
+
*slot = val; // existing property, same shape → in-place update
|
|
2828
|
+
return Ok(());
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
// Slow path: insert (a new key transitions the shape) + refill the cache for next time.
|
|
2834
|
+
b.strings.insert(Arc::clone(key), val);
|
|
2835
|
+
if let Some(c) = cell {
|
|
2836
|
+
let ns = b.strings.shape();
|
|
2837
|
+
if ns != tishlang_core::DICT_SHAPE {
|
|
2838
|
+
if let Some((_, i)) = b.strings.get_with_index(key.as_ref()) {
|
|
2839
|
+
c.store(((ns as u64) << 32) | i as u64, Relaxed);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return Ok(());
|
|
2844
|
+
}
|
|
2845
|
+
set_member(obj, key, val)
|
|
2846
|
+
}
|
|
2847
|
+
|
|
1807
2848
|
fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
1808
2849
|
match obj {
|
|
1809
2850
|
Value::Object(m) => {
|
|
2851
|
+
// `Set`/`Map` instances expose a computed `.size` (via a hidden `SizeProbe` opaque).
|
|
2852
|
+
if key.as_ref() == "size" {
|
|
2853
|
+
if let Some(n) = tishlang_builtins::collections::collection_size(obj) {
|
|
2854
|
+
return Ok(Value::Number(n));
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
1810
2857
|
let map = m.borrow();
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
2858
|
+
// Reading a missing own property returns `null` (tish's nullish value), matching
|
|
2859
|
+
// JS object semantics and the tree-walk interpreter — not a thrown error (#66).
|
|
2860
|
+
Ok(map.strings.get(key.as_ref()).cloned().unwrap_or(Value::Null))
|
|
2861
|
+
}
|
|
2862
|
+
Value::NumberArray(a) => {
|
|
2863
|
+
let key_s = key.as_ref();
|
|
2864
|
+
// Numeric index fast path.
|
|
2865
|
+
if let Ok(idx) = key_s.parse::<usize>() {
|
|
2866
|
+
return Ok(a.borrow().get(idx).map(|&n| Value::Number(n)).unwrap_or(Value::Null));
|
|
2867
|
+
}
|
|
2868
|
+
if key_s == "length" {
|
|
2869
|
+
return Ok(Value::Number(a.borrow().len() as f64));
|
|
2870
|
+
}
|
|
2871
|
+
// push/pop/sort — stay packed; everything else materialise + delegate.
|
|
2872
|
+
let a_clone = a.clone();
|
|
2873
|
+
let method: ArrayMethodFn = match key_s {
|
|
2874
|
+
"push" => make_native_fn(move |args: &[Value]| {
|
|
2875
|
+
let mut arr = a_clone.borrow_mut();
|
|
2876
|
+
for v in args {
|
|
2877
|
+
match v {
|
|
2878
|
+
Value::Number(n) => arr.push(*n),
|
|
2879
|
+
_ => {
|
|
2880
|
+
arr.push(f64::NAN); // hole-marker for non-numeric
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
Value::Number(arr.len() as f64)
|
|
2885
|
+
}),
|
|
2886
|
+
"pop" => make_native_fn(move |_: &[Value]| {
|
|
2887
|
+
a_clone.borrow_mut().pop()
|
|
2888
|
+
.map(|n| if n.is_nan() { Value::Null } else { Value::Number(n) })
|
|
2889
|
+
.unwrap_or(Value::Null)
|
|
2890
|
+
}),
|
|
2891
|
+
"shift" => make_native_fn(move |_: &[Value]| {
|
|
2892
|
+
let mut arr = a_clone.borrow_mut();
|
|
2893
|
+
if arr.is_empty() { Value::Null }
|
|
2894
|
+
else { let n = arr.remove(0); if n.is_nan() { Value::Null } else { Value::Number(n) } }
|
|
2895
|
+
}),
|
|
2896
|
+
"unshift" => make_native_fn(move |args: &[Value]| {
|
|
2897
|
+
let mut arr = a_clone.borrow_mut();
|
|
2898
|
+
for (i, v) in args.iter().enumerate() {
|
|
2899
|
+
let n = match v { Value::Number(n) => *n, _ => f64::NAN };
|
|
2900
|
+
arr.insert(i, n);
|
|
2901
|
+
}
|
|
2902
|
+
Value::Number(arr.len() as f64)
|
|
2903
|
+
}),
|
|
2904
|
+
"reverse" => make_native_fn(move |_: &[Value]| {
|
|
2905
|
+
a_clone.borrow_mut().reverse();
|
|
2906
|
+
Value::NumberArray(a_clone.clone())
|
|
2907
|
+
}),
|
|
2908
|
+
"splice" => {
|
|
2909
|
+
let a2 = a_clone.clone();
|
|
2910
|
+
make_native_fn(move |args: &[Value]| {
|
|
2911
|
+
// Check if there are non-numeric items to insert (args[2..]).
|
|
2912
|
+
let has_non_numeric = args.get(2..).unwrap_or(&[]).iter()
|
|
2913
|
+
.any(|v| !matches!(v, Value::Number(_)));
|
|
2914
|
+
if has_non_numeric {
|
|
2915
|
+
// Deopt: materialise, splice on the boxed array, then write numeric
|
|
2916
|
+
// elements back to the original Vec<f64>. This preserves the VmRef
|
|
2917
|
+
// identity for subsequent accesses. The array may have non-numeric
|
|
2918
|
+
// elements after this splice — they become NaN holes in the VmRef.
|
|
2919
|
+
let boxed = Value::materialize_number_array(&a2);
|
|
2920
|
+
let result = arr_builtins::splice(&boxed, args.first().unwrap_or(&Value::Null), args.get(1), args.get(2..).unwrap_or(&[]));
|
|
2921
|
+
// Sync the modified boxed Vec back into the original VmRef.
|
|
2922
|
+
if let Value::Array(boxed_vmref) = &boxed {
|
|
2923
|
+
let mut packed = a2.borrow_mut();
|
|
2924
|
+
*packed = boxed_vmref.borrow().iter().map(|v| match v { Value::Number(n) => *n, _ => f64::NAN }).collect();
|
|
2925
|
+
}
|
|
2926
|
+
result
|
|
2927
|
+
} else {
|
|
2928
|
+
let mut arr = a2.borrow_mut();
|
|
2929
|
+
let len = arr.len() as i64;
|
|
2930
|
+
let start = match args.first() {
|
|
2931
|
+
Some(Value::Number(n)) => { let s = *n as i64; if s < 0 { (len + s).max(0) as usize } else { (s as usize).min(arr.len()) } }
|
|
2932
|
+
_ => 0,
|
|
2933
|
+
};
|
|
2934
|
+
let del = match args.get(1) {
|
|
2935
|
+
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
2936
|
+
_ => arr.len().saturating_sub(start),
|
|
2937
|
+
};
|
|
2938
|
+
let del = del.min(arr.len().saturating_sub(start));
|
|
2939
|
+
let new_nums: Vec<f64> = args.get(2..).unwrap_or(&[]).iter().map(|v| match v { Value::Number(n) => *n, _ => f64::NAN }).collect();
|
|
2940
|
+
let removed: Vec<f64> = arr.splice(start..start + del, new_nums).collect();
|
|
2941
|
+
Value::number_array(removed)
|
|
2942
|
+
}
|
|
2943
|
+
})
|
|
2944
|
+
}
|
|
2945
|
+
"sort" => make_native_fn(move |args: &[Value]| {
|
|
2946
|
+
let arr_val = Value::NumberArray(a_clone.clone());
|
|
2947
|
+
let cmp = args.first();
|
|
2948
|
+
if let Some(Value::Function(_)) = cmp {
|
|
2949
|
+
// Comparator sort: materialise first (comparator may return non-numeric).
|
|
2950
|
+
let boxed = Value::materialize_number_array(&a_clone);
|
|
2951
|
+
arr_builtins::sort_with_comparator(&boxed, cmp.unwrap())
|
|
2952
|
+
} else {
|
|
2953
|
+
arr_builtins::sort_numeric_asc(&arr_val)
|
|
2954
|
+
}
|
|
2955
|
+
}),
|
|
2956
|
+
_ => {
|
|
2957
|
+
// All other methods: materialise to a boxed Array and delegate.
|
|
2958
|
+
// The a_clone is the original NumberArray VmRef; we materialise once per
|
|
2959
|
+
// method lookup (not per call) so the closure captures a stable boxed Array.
|
|
2960
|
+
let boxed = Value::materialize_number_array(&a_clone);
|
|
2961
|
+
let bv = boxed.clone();
|
|
2962
|
+
match key_s {
|
|
2963
|
+
"map" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::map(&bv, &cb) }),
|
|
2964
|
+
"filter" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::filter(&bv, &cb) }),
|
|
2965
|
+
"reduce" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); let init = args.get(1).cloned().unwrap_or(Value::Null); arr_builtins::reduce(&bv, &cb, &init) }),
|
|
2966
|
+
"forEach" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::for_each(&bv, &cb) }),
|
|
2967
|
+
"find" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::find(&bv, &cb) }),
|
|
2968
|
+
"findIndex" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::find_index(&bv, &cb) }),
|
|
2969
|
+
"some" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::some(&bv, &cb) }),
|
|
2970
|
+
"every" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::every(&bv, &cb) }),
|
|
2971
|
+
"join" => make_native_fn(move |args| { let sep = args.first().cloned().unwrap_or(Value::Null); arr_builtins::join(&bv, &sep) }),
|
|
2972
|
+
"flat" => make_native_fn(move |args| { let d = args.first().cloned().unwrap_or(Value::Number(1.0)); arr_builtins::flat(&bv, &d) }),
|
|
2973
|
+
"flatMap" => make_native_fn(move |args| { let cb = args.first().cloned().unwrap_or(Value::Null); arr_builtins::flat_map(&bv, &cb) }),
|
|
2974
|
+
"reverse" => make_native_fn(move |_| arr_builtins::reverse(&bv)),
|
|
2975
|
+
"fill" => make_native_fn(move |args| { let v = args.first().cloned().unwrap_or(Value::Null); let s = args.get(1).cloned().unwrap_or(Value::Null); let e = args.get(2).cloned().unwrap_or(Value::Null); arr_builtins::fill(&bv, &v, &s, &e) }),
|
|
2976
|
+
"slice" => make_native_fn(move |args| { let s = args.first().cloned().unwrap_or(Value::Null); let e = args.get(1).cloned().unwrap_or(Value::Null); arr_builtins::slice(&bv, &s, &e) }),
|
|
2977
|
+
"concat" => make_native_fn(move |args| arr_builtins::concat(&bv, args)),
|
|
2978
|
+
"indexOf" => make_native_fn(move |args| { let s = args.first().cloned().unwrap_or(Value::Null); arr_builtins::index_of(&bv, &s) }),
|
|
2979
|
+
"includes" => make_native_fn(move |args| { let s = args.first().cloned().unwrap_or(Value::Null); let f = args.get(1).cloned(); arr_builtins::includes(&bv, &s, f.as_ref()) }),
|
|
2980
|
+
"unshift" => make_native_fn(move |args| arr_builtins::unshift(&bv, args)),
|
|
2981
|
+
"shift" => make_native_fn(move |_| arr_builtins::shift(&bv)),
|
|
2982
|
+
"splice" => make_native_fn(move |args| { let s = args.first().cloned().unwrap_or(Value::Null); let dc = args.get(1).cloned(); let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec(); arr_builtins::splice(&bv, &s, dc.as_ref(), &items) }),
|
|
2983
|
+
_ => return Err(format!("Property '{}' not found", key)),
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
};
|
|
2987
|
+
Ok(Value::Function(method))
|
|
1815
2988
|
}
|
|
1816
2989
|
Value::Array(a) => {
|
|
1817
2990
|
let key_s = key.as_ref();
|
|
@@ -1842,6 +3015,12 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
1842
3015
|
"reverse" => make_native_fn(move |_args: &[Value]| {
|
|
1843
3016
|
arr_builtins::reverse(&Value::Array(a_clone.clone()))
|
|
1844
3017
|
}),
|
|
3018
|
+
"fill" => make_native_fn(move |args: &[Value]| {
|
|
3019
|
+
let value = args.first().unwrap_or(&Value::Null);
|
|
3020
|
+
let start = args.get(1).unwrap_or(&Value::Null);
|
|
3021
|
+
let end = args.get(2).unwrap_or(&Value::Null);
|
|
3022
|
+
arr_builtins::fill(&Value::Array(a_clone.clone()), value, start, end)
|
|
3023
|
+
}),
|
|
1845
3024
|
"shuffle" => make_native_fn(move |_args: &[Value]| {
|
|
1846
3025
|
arr_builtins::shuffle(&Value::Array(a_clone.clone()))
|
|
1847
3026
|
}),
|
|
@@ -1937,25 +3116,25 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
1937
3116
|
let key_s = key.as_ref();
|
|
1938
3117
|
if let Ok(idx) = key_s.parse::<usize>() {
|
|
1939
3118
|
return match s.chars().nth(idx) {
|
|
1940
|
-
Some(c) => Ok(Value::String(
|
|
3119
|
+
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
1941
3120
|
None => Err("Index out of bounds".to_string()),
|
|
1942
3121
|
};
|
|
1943
3122
|
}
|
|
1944
3123
|
if key_s == "length" {
|
|
1945
3124
|
return Ok(Value::Number(s.chars().count() as f64));
|
|
1946
3125
|
}
|
|
1947
|
-
let s_clone:
|
|
3126
|
+
let s_clone: tishlang_core::ArcStr = s.clone();
|
|
1948
3127
|
let method: ArrayMethodFn = match key_s {
|
|
1949
3128
|
"indexOf" => make_native_fn(move |args: &[Value]| {
|
|
1950
3129
|
let search = args.first().unwrap_or(&Value::Null);
|
|
1951
3130
|
let from = args.get(1);
|
|
1952
|
-
str_builtins::index_of(&Value::String(
|
|
3131
|
+
str_builtins::index_of(&Value::String(s_clone.clone()), search, from)
|
|
1953
3132
|
}),
|
|
1954
3133
|
"lastIndexOf" => make_native_fn(move |args: &[Value]| {
|
|
1955
3134
|
let search = args.first().unwrap_or(&Value::Null);
|
|
1956
3135
|
let position = args.get(1).cloned().unwrap_or(Value::Number(f64::INFINITY));
|
|
1957
3136
|
str_builtins::last_index_of(
|
|
1958
|
-
&Value::String(
|
|
3137
|
+
&Value::String(s_clone.clone()),
|
|
1959
3138
|
search,
|
|
1960
3139
|
&position,
|
|
1961
3140
|
)
|
|
@@ -1963,79 +3142,154 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
1963
3142
|
"includes" => make_native_fn(move |args: &[Value]| {
|
|
1964
3143
|
let search = args.first().unwrap_or(&Value::Null);
|
|
1965
3144
|
let from = args.get(1);
|
|
1966
|
-
str_builtins::includes(&Value::String(
|
|
3145
|
+
str_builtins::includes(&Value::String(s_clone.clone()), search, from)
|
|
1967
3146
|
}),
|
|
1968
3147
|
"slice" => make_native_fn(move |args: &[Value]| {
|
|
1969
3148
|
let start = args.first().unwrap_or(&Value::Null);
|
|
1970
3149
|
let end = args.get(1).unwrap_or(&Value::Null);
|
|
1971
|
-
str_builtins::slice(&Value::String(
|
|
3150
|
+
str_builtins::slice(&Value::String(s_clone.clone()), start, end)
|
|
1972
3151
|
}),
|
|
1973
3152
|
"substring" => make_native_fn(move |args: &[Value]| {
|
|
1974
3153
|
let start = args.first().unwrap_or(&Value::Null);
|
|
1975
3154
|
let end = args.get(1).unwrap_or(&Value::Null);
|
|
1976
|
-
str_builtins::substring(&Value::String(
|
|
3155
|
+
str_builtins::substring(&Value::String(s_clone.clone()), start, end)
|
|
1977
3156
|
}),
|
|
1978
3157
|
"split" => make_native_fn(move |args: &[Value]| {
|
|
1979
3158
|
let sep = args.first().unwrap_or(&Value::Null);
|
|
1980
|
-
|
|
3159
|
+
#[cfg(feature = "regex")]
|
|
3160
|
+
if matches!(sep, Value::RegExp(_)) {
|
|
3161
|
+
return tishlang_runtime::string_split_regex(
|
|
3162
|
+
&Value::String(s_clone.clone()),
|
|
3163
|
+
sep,
|
|
3164
|
+
None,
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
str_builtins::split(&Value::String(s_clone.clone()), sep)
|
|
1981
3168
|
}),
|
|
1982
3169
|
"trim" => make_native_fn(move |_args: &[Value]| {
|
|
1983
|
-
str_builtins::trim(&Value::String(
|
|
3170
|
+
str_builtins::trim(&Value::String(s_clone.clone()))
|
|
1984
3171
|
}),
|
|
1985
3172
|
"toUpperCase" => make_native_fn(move |_args: &[Value]| {
|
|
1986
|
-
str_builtins::to_upper_case(&Value::String(
|
|
3173
|
+
str_builtins::to_upper_case(&Value::String(s_clone.clone()))
|
|
1987
3174
|
}),
|
|
1988
3175
|
"toLowerCase" => make_native_fn(move |_args: &[Value]| {
|
|
1989
|
-
str_builtins::to_lower_case(&Value::String(
|
|
3176
|
+
str_builtins::to_lower_case(&Value::String(s_clone.clone()))
|
|
1990
3177
|
}),
|
|
1991
3178
|
"startsWith" => make_native_fn(move |args: &[Value]| {
|
|
1992
3179
|
let search = args.first().unwrap_or(&Value::Null);
|
|
1993
|
-
str_builtins::starts_with(&Value::String(
|
|
3180
|
+
str_builtins::starts_with(&Value::String(s_clone.clone()), search)
|
|
1994
3181
|
}),
|
|
1995
3182
|
"endsWith" => make_native_fn(move |args: &[Value]| {
|
|
1996
3183
|
let search = args.first().unwrap_or(&Value::Null);
|
|
1997
|
-
str_builtins::ends_with(&Value::String(
|
|
3184
|
+
str_builtins::ends_with(&Value::String(s_clone.clone()), search)
|
|
1998
3185
|
}),
|
|
1999
3186
|
"replace" => make_native_fn(move |args: &[Value]| {
|
|
2000
3187
|
let search = args.first().unwrap_or(&Value::Null);
|
|
2001
3188
|
let replacement = args.get(1).unwrap_or(&Value::Null);
|
|
2002
|
-
|
|
3189
|
+
// RegExp search (incl. global flag + function replacer) routes to the runtime's
|
|
3190
|
+
// regex-aware string_replace, identical to the rust backend.
|
|
3191
|
+
#[cfg(feature = "regex")]
|
|
3192
|
+
if matches!(search, Value::RegExp(_)) {
|
|
3193
|
+
return tishlang_runtime::string_replace(
|
|
3194
|
+
&Value::String(s_clone.clone()),
|
|
3195
|
+
search,
|
|
3196
|
+
replacement,
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
str_builtins::replace(&Value::String(s_clone.clone()), search, replacement)
|
|
2003
3200
|
}),
|
|
2004
3201
|
"replaceAll" => make_native_fn(move |args: &[Value]| {
|
|
2005
3202
|
let search = args.first().unwrap_or(&Value::Null);
|
|
2006
3203
|
let replacement = args.get(1).unwrap_or(&Value::Null);
|
|
2007
3204
|
str_builtins::replace_all(
|
|
2008
|
-
&Value::String(
|
|
3205
|
+
&Value::String(s_clone.clone()),
|
|
2009
3206
|
search,
|
|
2010
3207
|
replacement,
|
|
2011
3208
|
)
|
|
2012
3209
|
}),
|
|
3210
|
+
#[cfg(feature = "regex")]
|
|
3211
|
+
"match" => make_native_fn(move |args: &[Value]| {
|
|
3212
|
+
let re = args.first().unwrap_or(&Value::Null);
|
|
3213
|
+
tishlang_runtime::string_match_regex(&Value::String(s_clone.clone()), re)
|
|
3214
|
+
}),
|
|
3215
|
+
#[cfg(feature = "regex")]
|
|
3216
|
+
"search" => make_native_fn(move |args: &[Value]| {
|
|
3217
|
+
let re = args.first().unwrap_or(&Value::Null);
|
|
3218
|
+
tishlang_runtime::string_search_regex(&Value::String(s_clone.clone()), re)
|
|
3219
|
+
}),
|
|
2013
3220
|
"charAt" => make_native_fn(move |args: &[Value]| {
|
|
2014
3221
|
let idx = args.first().unwrap_or(&Value::Null);
|
|
2015
|
-
str_builtins::char_at(&Value::String(
|
|
3222
|
+
str_builtins::char_at(&Value::String(s_clone.clone()), idx)
|
|
2016
3223
|
}),
|
|
2017
3224
|
"charCodeAt" => make_native_fn(move |args: &[Value]| {
|
|
2018
3225
|
let idx = args.first().unwrap_or(&Value::Null);
|
|
2019
|
-
str_builtins::char_code_at(&Value::String(
|
|
3226
|
+
str_builtins::char_code_at(&Value::String(s_clone.clone()), idx)
|
|
2020
3227
|
}),
|
|
2021
3228
|
"repeat" => make_native_fn(move |args: &[Value]| {
|
|
2022
3229
|
let count = args.first().unwrap_or(&Value::Null);
|
|
2023
|
-
str_builtins::repeat(&Value::String(
|
|
3230
|
+
str_builtins::repeat(&Value::String(s_clone.clone()), count)
|
|
2024
3231
|
}),
|
|
2025
3232
|
"padStart" => make_native_fn(move |args: &[Value]| {
|
|
2026
3233
|
let target_len = args.first().unwrap_or(&Value::Null);
|
|
2027
3234
|
let pad = args.get(1).unwrap_or(&Value::Null);
|
|
2028
|
-
str_builtins::pad_start(&Value::String(
|
|
3235
|
+
str_builtins::pad_start(&Value::String(s_clone.clone()), target_len, pad)
|
|
2029
3236
|
}),
|
|
2030
3237
|
"padEnd" => make_native_fn(move |args: &[Value]| {
|
|
2031
3238
|
let target_len = args.first().unwrap_or(&Value::Null);
|
|
2032
3239
|
let pad = args.get(1).unwrap_or(&Value::Null);
|
|
2033
|
-
str_builtins::pad_end(&Value::String(
|
|
3240
|
+
str_builtins::pad_end(&Value::String(s_clone.clone()), target_len, pad)
|
|
3241
|
+
}),
|
|
3242
|
+
_ => return Err(format!("Property '{}' not found", key)),
|
|
3243
|
+
};
|
|
3244
|
+
Ok(Value::Function(method))
|
|
3245
|
+
}
|
|
3246
|
+
Value::Number(n) => {
|
|
3247
|
+
// Number.prototype methods. Shared impls live in tishlang_builtins::number so
|
|
3248
|
+
// the VM, rust runtime, and interpreter stay byte-identical (full-backend-parity-plan.md).
|
|
3249
|
+
let n_val = *n;
|
|
3250
|
+
let method: ArrayMethodFn = match key.as_ref() {
|
|
3251
|
+
"toFixed" => make_native_fn(move |args: &[Value]| {
|
|
3252
|
+
let digits = args.first().unwrap_or(&Value::Null);
|
|
3253
|
+
num_builtins::to_fixed(&Value::Number(n_val), digits)
|
|
3254
|
+
}),
|
|
3255
|
+
"toString" => make_native_fn(move |args: &[Value]| {
|
|
3256
|
+
let radix = args.first().unwrap_or(&Value::Null);
|
|
3257
|
+
num_builtins::to_string(&Value::Number(n_val), radix)
|
|
2034
3258
|
}),
|
|
2035
3259
|
_ => return Err(format!("Property '{}' not found", key)),
|
|
2036
3260
|
};
|
|
2037
3261
|
Ok(Value::Function(method))
|
|
2038
3262
|
}
|
|
3263
|
+
#[cfg(feature = "regex")]
|
|
3264
|
+
Value::RegExp(re) => match key.as_ref() {
|
|
3265
|
+
// `test`/`exec` route to the same runtime impls the rust backend uses, so the match
|
|
3266
|
+
// object shape (keys "0".."n" + "index") and lastIndex advancement are identical.
|
|
3267
|
+
"test" => {
|
|
3268
|
+
let rc = re.clone();
|
|
3269
|
+
Ok(Value::native(move |args: &[Value]| {
|
|
3270
|
+
let input = args.first().unwrap_or(&Value::Null);
|
|
3271
|
+
tishlang_runtime::regexp_test(&Value::RegExp(rc.clone()), input)
|
|
3272
|
+
}))
|
|
3273
|
+
}
|
|
3274
|
+
"exec" => {
|
|
3275
|
+
let rc = re.clone();
|
|
3276
|
+
Ok(Value::native(move |args: &[Value]| {
|
|
3277
|
+
let input = args.first().unwrap_or(&Value::Null);
|
|
3278
|
+
tishlang_runtime::regexp_exec(&Value::RegExp(rc.clone()), input)
|
|
3279
|
+
}))
|
|
3280
|
+
}
|
|
3281
|
+
// Properties mirror the interpreter (eval.rs get_prop RegExp arm) exactly.
|
|
3282
|
+
"source" => Ok(Value::String(re.borrow().source.clone().into())),
|
|
3283
|
+
"flags" => Ok(Value::String(re.borrow().flags_string().into())),
|
|
3284
|
+
"lastIndex" => Ok(Value::Number(re.borrow().last_index as f64)),
|
|
3285
|
+
"global" => Ok(Value::Bool(re.borrow().flags.global)),
|
|
3286
|
+
"ignoreCase" => Ok(Value::Bool(re.borrow().flags.ignore_case)),
|
|
3287
|
+
"multiline" => Ok(Value::Bool(re.borrow().flags.multiline)),
|
|
3288
|
+
"dotAll" => Ok(Value::Bool(re.borrow().flags.dot_all)),
|
|
3289
|
+
"unicode" => Ok(Value::Bool(re.borrow().flags.unicode)),
|
|
3290
|
+
"sticky" => Ok(Value::Bool(re.borrow().flags.sticky)),
|
|
3291
|
+
_ => Err(format!("Property '{}' not found", key)),
|
|
3292
|
+
},
|
|
2039
3293
|
#[cfg(any(feature = "http", feature = "promise"))]
|
|
2040
3294
|
Value::Promise(p) => match key.as_ref() {
|
|
2041
3295
|
"then" => {
|
|
@@ -2067,6 +3321,13 @@ fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
|
|
|
2067
3321
|
Ok(())
|
|
2068
3322
|
}
|
|
2069
3323
|
Value::Array(a) => {
|
|
3324
|
+
if key.as_ref() == "length" {
|
|
3325
|
+
// `arr.length = k` truncates or grows (holes read back as Null), JS-style.
|
|
3326
|
+
let new_len = array_length_arg(&val)?;
|
|
3327
|
+
let mut arr = a.borrow_mut();
|
|
3328
|
+
arr.resize(new_len, Value::Null);
|
|
3329
|
+
return Ok(());
|
|
3330
|
+
}
|
|
2070
3331
|
let idx: usize = key.as_ref().parse().unwrap_or(0);
|
|
2071
3332
|
let mut arr = a.borrow_mut();
|
|
2072
3333
|
if idx < arr.len() {
|
|
@@ -2077,12 +3338,39 @@ fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
|
|
|
2077
3338
|
}
|
|
2078
3339
|
Ok(())
|
|
2079
3340
|
}
|
|
3341
|
+
Value::NumberArray(a) => {
|
|
3342
|
+
if key.as_ref() == "length" {
|
|
3343
|
+
let new_len = array_length_arg(&val)?;
|
|
3344
|
+
// NaN is the packed-array hole marker (read back as Null), matching get_index.
|
|
3345
|
+
a.borrow_mut().resize(new_len, f64::NAN);
|
|
3346
|
+
return Ok(());
|
|
3347
|
+
}
|
|
3348
|
+
Err(format!("Cannot set property of {}", obj.type_name()))
|
|
3349
|
+
}
|
|
2080
3350
|
_ => Err(format!("Cannot set property of {}", obj.type_name())),
|
|
2081
3351
|
}
|
|
2082
3352
|
}
|
|
2083
3353
|
|
|
3354
|
+
/// JS `arr.length = v`: `v` is coerced to a number and must be a valid array length —
|
|
3355
|
+
/// a non-negative integer below 2³². Anything else is a RangeError ("Invalid array length").
|
|
3356
|
+
fn array_length_arg(val: &Value) -> Result<usize, String> {
|
|
3357
|
+
let n = val.as_number().unwrap_or(f64::NAN);
|
|
3358
|
+
if n.is_nan() || n < 0.0 || n.fract() != 0.0 || n > 4_294_967_295.0 {
|
|
3359
|
+
return Err("Invalid array length".to_string());
|
|
3360
|
+
}
|
|
3361
|
+
Ok(n as usize)
|
|
3362
|
+
}
|
|
3363
|
+
|
|
2084
3364
|
fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
2085
3365
|
match obj {
|
|
3366
|
+
Value::NumberArray(a) => {
|
|
3367
|
+
let i = match idx {
|
|
3368
|
+
Value::Number(n) => *n as usize,
|
|
3369
|
+
_ => return Err(format!("Array index must be number, got {}", idx.type_name())),
|
|
3370
|
+
};
|
|
3371
|
+
// NaN is used as the hole marker (sparse-array positions); reads return Null.
|
|
3372
|
+
Ok(a.borrow().get(i).map(|&n| if n.is_nan() { Value::Null } else { Value::Number(n) }).unwrap_or(Value::Null))
|
|
3373
|
+
}
|
|
2086
3374
|
Value::Array(a) => {
|
|
2087
3375
|
let i = match idx {
|
|
2088
3376
|
Value::Number(n) => *n as usize,
|
|
@@ -2124,20 +3412,17 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
2124
3412
|
}
|
|
2125
3413
|
};
|
|
2126
3414
|
match s.chars().nth(i) {
|
|
2127
|
-
Some(c) => Ok(Value::String(
|
|
3415
|
+
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
2128
3416
|
None => Err("Index out of bounds".to_string()),
|
|
2129
3417
|
}
|
|
2130
3418
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
idx.to_display_string()
|
|
2135
|
-
)
|
|
2136
|
-
}),
|
|
3419
|
+
// A missing own property returns `null`, not a thrown error — matching dot reads
|
|
3420
|
+
// (#66) and JS object semantics. Keeps `obj[key]` and `obj.key` in lockstep (#113).
|
|
3421
|
+
Value::Object(_) => Ok(object_get(obj, idx).unwrap_or(Value::Null)),
|
|
2137
3422
|
#[cfg(any(feature = "http", feature = "promise"))]
|
|
2138
3423
|
Value::Promise(_) => {
|
|
2139
3424
|
let key_arc: std::sync::Arc<str> = match idx {
|
|
2140
|
-
Value::String(s) => std::sync::Arc::
|
|
3425
|
+
Value::String(s) => std::sync::Arc::from(s.as_str()),
|
|
2141
3426
|
_ => {
|
|
2142
3427
|
return Err(format!(
|
|
2143
3428
|
"Promise bracket access requires a string key, got {}",
|
|
@@ -2155,8 +3440,67 @@ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
|
2155
3440
|
}
|
|
2156
3441
|
}
|
|
2157
3442
|
|
|
3443
|
+
/// `delete obj[key]` semantics (issue #40). Objects drop the string key; arrays clear the
|
|
3444
|
+
/// element at a numeric index to a `null` hole (length is preserved, JS-style). Anything else
|
|
3445
|
+
/// is a no-op. The operator always evaluates to `true` (handled by the caller).
|
|
3446
|
+
fn delete_index(obj: &Value, key: &Value) {
|
|
3447
|
+
match obj {
|
|
3448
|
+
Value::Object(m) => {
|
|
3449
|
+
let key_s: Arc<str> = match key {
|
|
3450
|
+
Value::String(s) => Arc::from(s.as_str()),
|
|
3451
|
+
other => Arc::from(other.to_display_string().as_str()),
|
|
3452
|
+
};
|
|
3453
|
+
m.borrow_mut().strings.remove(key_s.as_ref());
|
|
3454
|
+
}
|
|
3455
|
+
Value::Array(a) => {
|
|
3456
|
+
if let Value::Number(n) = key {
|
|
3457
|
+
let n = *n;
|
|
3458
|
+
if n >= 0.0 && n.fract() == 0.0 {
|
|
3459
|
+
let i = n as usize;
|
|
3460
|
+
let mut arr = a.borrow_mut();
|
|
3461
|
+
if i < arr.len() {
|
|
3462
|
+
arr[i] = Value::Null;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
_ => {}
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
|
|
2158
3471
|
fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
2159
3472
|
match obj {
|
|
3473
|
+
Value::NumberArray(a) => {
|
|
3474
|
+
let i = match idx {
|
|
3475
|
+
Value::Number(n) => *n as usize,
|
|
3476
|
+
_ => return Err(format!("Array index must be number, got {}", idx.type_name())),
|
|
3477
|
+
};
|
|
3478
|
+
// In-bounds numeric assignment stays packed.
|
|
3479
|
+
// Out-of-bounds or non-numeric falls through to the Array path by returning
|
|
3480
|
+
// a sentinel error — the caller (SetIndex opcode) does NOT handle deopt.
|
|
3481
|
+
// Instead we only do in-bounds-or-next-element numeric assignments here;
|
|
3482
|
+
// anything that creates holes (i > len) or sets a non-number is unsupported.
|
|
3483
|
+
match val {
|
|
3484
|
+
Value::Number(n) => {
|
|
3485
|
+
let mut arr = a.borrow_mut();
|
|
3486
|
+
// Extend with NaN "holes" if needed (NaN = sparse hole; read back as Null).
|
|
3487
|
+
while arr.len() <= i { arr.push(f64::NAN); }
|
|
3488
|
+
arr[i] = n;
|
|
3489
|
+
}
|
|
3490
|
+
// Non-numeric set: the Vec<f64> can't represent this type. Extend with NaN holes
|
|
3491
|
+
// up to the index, then leave the slot as NaN (the value is lost). This is a
|
|
3492
|
+
// known limitation of NumberArray; the uncommon mixed-type path should not produce
|
|
3493
|
+
// a NumberArray in the first place. The caller will see the correct index reads for
|
|
3494
|
+
// numeric elements and Null for the NaN holes.
|
|
3495
|
+
_ => {
|
|
3496
|
+
let mut arr = a.borrow_mut();
|
|
3497
|
+
while arr.len() <= i { arr.push(f64::NAN); }
|
|
3498
|
+
// arr[i] is already NaN (hole); we can't store the non-numeric value — acceptable
|
|
3499
|
+
// for the experimental TISH_PACKED_ARRAYS path.
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
Ok(())
|
|
3503
|
+
}
|
|
2160
3504
|
Value::Array(a) => {
|
|
2161
3505
|
let i = match idx {
|
|
2162
3506
|
Value::Number(n) => *n as usize,
|