@tishlang/tish-format 1.0.12 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,3536 @@
|
|
|
1
|
+
//! Stack-based bytecode VM.
|
|
2
|
+
|
|
3
|
+
use std::collections::{HashMap, HashSet};
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
#[cfg(not(feature = "send-values"))]
|
|
7
|
+
use std::rc::Rc;
|
|
8
|
+
use tishlang_core::VmRef;
|
|
9
|
+
|
|
10
|
+
use tishlang_ast::{BinOp, UnaryOp};
|
|
11
|
+
use tishlang_builtins::array as arr_builtins;
|
|
12
|
+
use tishlang_builtins::construct as construct_builtin;
|
|
13
|
+
use tishlang_builtins::globals as globals_builtins;
|
|
14
|
+
use tishlang_builtins::math as math_builtins;
|
|
15
|
+
use tishlang_builtins::number as num_builtins;
|
|
16
|
+
use tishlang_builtins::string as str_builtins;
|
|
17
|
+
use tishlang_bytecode::{u8_to_binop, u8_to_unaryop, Chunk, Constant, Opcode, NO_REST_PARAM};
|
|
18
|
+
use tishlang_core::{
|
|
19
|
+
merge_object_data, object_get, object_has, object_set, to_int32, to_uint32, NativeFn,
|
|
20
|
+
ObjectData, ObjectMap, PropMap, Value,
|
|
21
|
+
};
|
|
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
|
+
|
|
58
|
+
/// Wrap a closure in the right shared pointer for the current build.
|
|
59
|
+
/// Under `send-values` that's `Arc<dyn Fn + Send + Sync>`; otherwise it's
|
|
60
|
+
/// plain `Rc<dyn Fn>`. Call sites can stay ignorant of the distinction.
|
|
61
|
+
#[cfg(feature = "send-values")]
|
|
62
|
+
#[inline]
|
|
63
|
+
fn make_native_fn<F>(f: F) -> NativeFn
|
|
64
|
+
where
|
|
65
|
+
F: Fn(&[Value]) -> Value + Send + Sync + 'static,
|
|
66
|
+
{
|
|
67
|
+
tishlang_core::native_fn(f)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[cfg(not(feature = "send-values"))]
|
|
71
|
+
#[inline]
|
|
72
|
+
fn make_native_fn<F>(f: F) -> NativeFn
|
|
73
|
+
where
|
|
74
|
+
F: Fn(&[Value]) -> Value + 'static,
|
|
75
|
+
{
|
|
76
|
+
tishlang_core::native_fn(f)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Array / string / object methods have the same shape as `NativeFn`, which
|
|
80
|
+
// is already feature-gated (`Rc<dyn Fn>` vs `Arc<dyn Fn + Send + Sync>`).
|
|
81
|
+
// Alias to that so the VM picks the right pointer type automatically.
|
|
82
|
+
type ArrayMethodFn = NativeFn;
|
|
83
|
+
|
|
84
|
+
/// Feature names enabled for this VM run (`tish run --feature …`). `full` enables every optional capability.
|
|
85
|
+
#[cfg_attr(
|
|
86
|
+
not(any(
|
|
87
|
+
feature = "fs",
|
|
88
|
+
feature = "http",
|
|
89
|
+
feature = "promise",
|
|
90
|
+
feature = "timers",
|
|
91
|
+
feature = "process",
|
|
92
|
+
feature = "ws"
|
|
93
|
+
)),
|
|
94
|
+
allow(dead_code)
|
|
95
|
+
)]
|
|
96
|
+
#[inline]
|
|
97
|
+
fn value_object_from_map(m: ObjectMap) -> Value {
|
|
98
|
+
Value::Object(VmRef::new(ObjectData::from_strings(m)))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[cfg(any(
|
|
102
|
+
feature = "fs",
|
|
103
|
+
feature = "http",
|
|
104
|
+
feature = "promise",
|
|
105
|
+
feature = "timers",
|
|
106
|
+
feature = "process",
|
|
107
|
+
feature = "ws"
|
|
108
|
+
))]
|
|
109
|
+
#[inline]
|
|
110
|
+
fn cap_allows(enabled: &HashSet<String>, name: &str) -> bool {
|
|
111
|
+
enabled.contains("full") || enabled.contains(name)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Capabilities linked into this `tishlang_vm` binary (compile-time). Used by [`Vm::new`] and `run()`.
|
|
115
|
+
pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
116
|
+
#[allow(unused_mut)]
|
|
117
|
+
let mut s = HashSet::new();
|
|
118
|
+
#[cfg(feature = "http")]
|
|
119
|
+
s.insert("http".to_string());
|
|
120
|
+
#[cfg(feature = "promise")]
|
|
121
|
+
s.insert("promise".to_string());
|
|
122
|
+
#[cfg(feature = "timers")]
|
|
123
|
+
s.insert("timers".to_string());
|
|
124
|
+
#[cfg(feature = "fs")]
|
|
125
|
+
s.insert("fs".to_string());
|
|
126
|
+
#[cfg(feature = "process")]
|
|
127
|
+
s.insert("process".to_string());
|
|
128
|
+
#[cfg(feature = "regex")]
|
|
129
|
+
s.insert("regex".to_string());
|
|
130
|
+
#[cfg(feature = "ws")]
|
|
131
|
+
s.insert("ws".to_string());
|
|
132
|
+
#[cfg(feature = "tty")]
|
|
133
|
+
s.insert("tty".to_string());
|
|
134
|
+
s
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Look up built-in module export for LoadNativeExport. Returns None if unknown or feature disabled.
|
|
138
|
+
#[cfg_attr(
|
|
139
|
+
not(any(
|
|
140
|
+
feature = "fs",
|
|
141
|
+
feature = "http",
|
|
142
|
+
feature = "promise",
|
|
143
|
+
feature = "timers",
|
|
144
|
+
feature = "process",
|
|
145
|
+
feature = "ws",
|
|
146
|
+
feature = "tty"
|
|
147
|
+
)),
|
|
148
|
+
allow(unused_variables)
|
|
149
|
+
)]
|
|
150
|
+
fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str) -> Option<Value> {
|
|
151
|
+
#[cfg(feature = "fs")]
|
|
152
|
+
if spec == "tish:fs" && cap_allows(enabled, "fs") {
|
|
153
|
+
return match export_name {
|
|
154
|
+
"readFile" => Some(Value::native(|args: &[Value]| {
|
|
155
|
+
tishlang_runtime::read_file(args)
|
|
156
|
+
})),
|
|
157
|
+
"writeFile" => Some(Value::native(|args: &[Value]| {
|
|
158
|
+
tishlang_runtime::write_file(args)
|
|
159
|
+
})),
|
|
160
|
+
"fileExists" => Some(Value::native(|args: &[Value]| {
|
|
161
|
+
tishlang_runtime::file_exists(args)
|
|
162
|
+
})),
|
|
163
|
+
"isDir" => Some(Value::native(|args: &[Value]| {
|
|
164
|
+
tishlang_runtime::is_dir(args)
|
|
165
|
+
})),
|
|
166
|
+
"readDir" => Some(Value::native(|args: &[Value]| {
|
|
167
|
+
tishlang_runtime::read_dir(args)
|
|
168
|
+
})),
|
|
169
|
+
"mkdir" => Some(Value::native(|args: &[Value]| {
|
|
170
|
+
tishlang_runtime::mkdir(args)
|
|
171
|
+
})),
|
|
172
|
+
_ => None,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
#[cfg(feature = "http")]
|
|
176
|
+
if spec == "tish:http" && cap_allows(enabled, "http") {
|
|
177
|
+
return match export_name {
|
|
178
|
+
// Bytecode compiler lowers `await expr` to `tish:http.await(promise)` (see tish_bytecode compiler).
|
|
179
|
+
"await" => Some(Value::native(|args: &[Value]| {
|
|
180
|
+
tishlang_runtime::await_promise(args.first().cloned().unwrap_or(Value::Null))
|
|
181
|
+
})),
|
|
182
|
+
"fetch" => Some(Value::native(|args: &[Value]| {
|
|
183
|
+
tishlang_runtime::fetch_promise(args.to_vec())
|
|
184
|
+
})),
|
|
185
|
+
"fetchAll" => Some(Value::native(|args: &[Value]| {
|
|
186
|
+
tishlang_runtime::fetch_all_promise(args.to_vec())
|
|
187
|
+
})),
|
|
188
|
+
"Promise" => Some(tishlang_runtime::promise_object()),
|
|
189
|
+
"serve" => Some(Value::native(|args: &[Value]| {
|
|
190
|
+
// Phase-1 item 2: support `serve(port, { handler, onWorker })`
|
|
191
|
+
// in addition to `serve(port, handler)`. When an options
|
|
192
|
+
// object is given and onWorker is a function, invoke it with
|
|
193
|
+
// worker id 0 and expect it to return the request handler.
|
|
194
|
+
let raw = args.get(1).cloned().unwrap_or(Value::Null);
|
|
195
|
+
let handler_value = match raw {
|
|
196
|
+
Value::Function(_) => raw,
|
|
197
|
+
Value::Object(ref obj) => {
|
|
198
|
+
let obj_ref = obj.borrow();
|
|
199
|
+
if let Some(Value::Function(on_worker)) =
|
|
200
|
+
obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
|
|
201
|
+
{
|
|
202
|
+
let args_for_init = [Value::Number(0.0)];
|
|
203
|
+
on_worker.call(&args_for_init)
|
|
204
|
+
} else if let Some(h) =
|
|
205
|
+
obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
|
|
206
|
+
{
|
|
207
|
+
h
|
|
208
|
+
} else {
|
|
209
|
+
Value::Null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
_ => Value::Null,
|
|
213
|
+
};
|
|
214
|
+
if let Value::Function(f) = handler_value {
|
|
215
|
+
tishlang_runtime::http_serve(args, move |req_args| f.call(req_args))
|
|
216
|
+
} else {
|
|
217
|
+
Value::Null
|
|
218
|
+
}
|
|
219
|
+
})),
|
|
220
|
+
_ => None,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
#[cfg(all(feature = "promise", not(feature = "http")))]
|
|
224
|
+
if spec == "tish:http" && cap_allows(enabled, "promise") {
|
|
225
|
+
return match export_name {
|
|
226
|
+
"Promise" => Some(tishlang_runtime::promise_object()),
|
|
227
|
+
"await" => Some(Value::native(|args: &[Value]| {
|
|
228
|
+
tishlang_runtime::await_promise(args.first().cloned().unwrap_or(Value::Null))
|
|
229
|
+
})),
|
|
230
|
+
_ => None,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
#[cfg(feature = "timers")]
|
|
234
|
+
if spec == "tish:timers" && cap_allows(enabled, "timers") {
|
|
235
|
+
return match export_name {
|
|
236
|
+
"setTimeout" => Some(Value::native(|args: &[Value]| {
|
|
237
|
+
tishlang_runtime::timer_set_timeout(args)
|
|
238
|
+
})),
|
|
239
|
+
"setInterval" => Some(Value::native(|args: &[Value]| {
|
|
240
|
+
tishlang_runtime::timer_set_interval(args)
|
|
241
|
+
})),
|
|
242
|
+
"clearTimeout" => Some(Value::native(|args: &[Value]| {
|
|
243
|
+
tishlang_runtime::timer_clear_timeout(args)
|
|
244
|
+
})),
|
|
245
|
+
"clearInterval" => Some(Value::native(|args: &[Value]| {
|
|
246
|
+
tishlang_runtime::timer_clear_interval(args)
|
|
247
|
+
})),
|
|
248
|
+
_ => None,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
#[cfg(feature = "process")]
|
|
252
|
+
if spec == "tish:process" && cap_allows(enabled, "process") {
|
|
253
|
+
return match export_name {
|
|
254
|
+
"exit" => Some(Value::native(|args: &[Value]| {
|
|
255
|
+
tishlang_runtime::process_exit(args)
|
|
256
|
+
})),
|
|
257
|
+
"cwd" => Some(Value::native(|args: &[Value]| {
|
|
258
|
+
tishlang_runtime::process_cwd(args)
|
|
259
|
+
})),
|
|
260
|
+
"exec" => Some(Value::native(|args: &[Value]| {
|
|
261
|
+
tishlang_runtime::process_exec(args)
|
|
262
|
+
})),
|
|
263
|
+
"argv" => Some(Value::Array(VmRef::new(
|
|
264
|
+
std::env::args().map(|s| Value::String(s.into())).collect(),
|
|
265
|
+
))),
|
|
266
|
+
"env" => Some(value_object_from_map(
|
|
267
|
+
std::env::vars()
|
|
268
|
+
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
|
|
269
|
+
.collect(),
|
|
270
|
+
)),
|
|
271
|
+
"process" => {
|
|
272
|
+
let mut m = ObjectMap::default();
|
|
273
|
+
m.insert(
|
|
274
|
+
"exit".into(),
|
|
275
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exit(args)),
|
|
276
|
+
);
|
|
277
|
+
m.insert(
|
|
278
|
+
"cwd".into(),
|
|
279
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_cwd(args)),
|
|
280
|
+
);
|
|
281
|
+
m.insert(
|
|
282
|
+
"exec".into(),
|
|
283
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
|
|
284
|
+
);
|
|
285
|
+
m.insert(
|
|
286
|
+
"argv".into(),
|
|
287
|
+
Value::Array(VmRef::new(
|
|
288
|
+
std::env::args().map(|s| Value::String(s.into())).collect(),
|
|
289
|
+
)),
|
|
290
|
+
);
|
|
291
|
+
m.insert(
|
|
292
|
+
"env".into(),
|
|
293
|
+
value_object_from_map(
|
|
294
|
+
std::env::vars()
|
|
295
|
+
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
|
|
296
|
+
.collect(),
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
Some(value_object_from_map(m))
|
|
300
|
+
}
|
|
301
|
+
_ => None,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
#[cfg(feature = "ws")]
|
|
305
|
+
if spec == "tish:ws" && cap_allows(enabled, "ws") {
|
|
306
|
+
return match export_name {
|
|
307
|
+
"WebSocket" => Some(Value::native(|args: &[Value]| {
|
|
308
|
+
tishlang_runtime::web_socket_client(args)
|
|
309
|
+
})),
|
|
310
|
+
"Server" => Some(Value::native(|args: &[Value]| {
|
|
311
|
+
tishlang_runtime::web_socket_server_construct(args)
|
|
312
|
+
})),
|
|
313
|
+
"wsSend" => Some(Value::native(|args: &[Value]| {
|
|
314
|
+
Value::Bool(tishlang_runtime::ws_send_native(
|
|
315
|
+
args.first().unwrap_or(&Value::Null),
|
|
316
|
+
&args
|
|
317
|
+
.get(1)
|
|
318
|
+
.map(|v| v.to_display_string())
|
|
319
|
+
.unwrap_or_default(),
|
|
320
|
+
))
|
|
321
|
+
})),
|
|
322
|
+
"wsBroadcast" => Some(Value::native(|args: &[Value]| {
|
|
323
|
+
tishlang_runtime::ws_broadcast_native(args)
|
|
324
|
+
})),
|
|
325
|
+
_ => None,
|
|
326
|
+
};
|
|
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
|
+
}
|
|
349
|
+
None
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Console output: println! on native, web_sys::console on wasm
|
|
353
|
+
#[cfg(not(feature = "wasm"))]
|
|
354
|
+
fn vm_log(s: &str) {
|
|
355
|
+
println!("{}", s);
|
|
356
|
+
}
|
|
357
|
+
#[cfg(not(feature = "wasm"))]
|
|
358
|
+
fn vm_log_err(s: &str) {
|
|
359
|
+
eprintln!("{}", s);
|
|
360
|
+
}
|
|
361
|
+
#[cfg(feature = "wasm")]
|
|
362
|
+
fn vm_log(s: &str) {
|
|
363
|
+
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
364
|
+
extern "C" {
|
|
365
|
+
#[wasm_bindgen(js_namespace = console)]
|
|
366
|
+
fn log(s: &str);
|
|
367
|
+
}
|
|
368
|
+
log(s);
|
|
369
|
+
}
|
|
370
|
+
#[cfg(feature = "wasm")]
|
|
371
|
+
fn vm_log_err(s: &str) {
|
|
372
|
+
#[wasm_bindgen::prelude::wasm_bindgen]
|
|
373
|
+
extern "C" {
|
|
374
|
+
#[wasm_bindgen(js_namespace = console)]
|
|
375
|
+
fn error(s: &str);
|
|
376
|
+
}
|
|
377
|
+
error(s);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// Initialize default globals (console, Math, JSON, etc.)
|
|
381
|
+
#[allow(unused_variables)]
|
|
382
|
+
fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
383
|
+
let mut g = ObjectMap::default();
|
|
384
|
+
|
|
385
|
+
let mut console = ObjectMap::default();
|
|
386
|
+
console.insert(
|
|
387
|
+
"debug".into(),
|
|
388
|
+
Value::native(|args: &[Value]| {
|
|
389
|
+
let s =
|
|
390
|
+
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
|
|
391
|
+
vm_log(&s);
|
|
392
|
+
Value::Null
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
console.insert(
|
|
396
|
+
"log".into(),
|
|
397
|
+
Value::native(|args: &[Value]| {
|
|
398
|
+
let s =
|
|
399
|
+
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
|
|
400
|
+
vm_log(&s);
|
|
401
|
+
Value::Null
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
console.insert(
|
|
405
|
+
"info".into(),
|
|
406
|
+
Value::native(|args: &[Value]| {
|
|
407
|
+
let s =
|
|
408
|
+
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
|
|
409
|
+
vm_log(&s);
|
|
410
|
+
Value::Null
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
console.insert(
|
|
414
|
+
"warn".into(),
|
|
415
|
+
Value::native(|args: &[Value]| {
|
|
416
|
+
let s =
|
|
417
|
+
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
|
|
418
|
+
vm_log_err(&s);
|
|
419
|
+
Value::Null
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
console.insert(
|
|
423
|
+
"error".into(),
|
|
424
|
+
Value::native(|args: &[Value]| {
|
|
425
|
+
let s =
|
|
426
|
+
tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
|
|
427
|
+
vm_log_err(&s);
|
|
428
|
+
Value::Null
|
|
429
|
+
}),
|
|
430
|
+
);
|
|
431
|
+
g.insert("console".into(), value_object_from_map(console));
|
|
432
|
+
|
|
433
|
+
let mut math = ObjectMap::default();
|
|
434
|
+
math.insert(
|
|
435
|
+
"abs".into(),
|
|
436
|
+
Value::native(|args: &[Value]| {
|
|
437
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
438
|
+
Value::Number(n.abs())
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
math.insert(
|
|
442
|
+
"sqrt".into(),
|
|
443
|
+
Value::native(|args: &[Value]| {
|
|
444
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
445
|
+
Value::Number(n.sqrt())
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
math.insert(
|
|
449
|
+
"floor".into(),
|
|
450
|
+
Value::native(|args: &[Value]| {
|
|
451
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
452
|
+
Value::Number(n.floor())
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
math.insert(
|
|
456
|
+
"ceil".into(),
|
|
457
|
+
Value::native(|args: &[Value]| {
|
|
458
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
459
|
+
Value::Number(n.ceil())
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
math.insert(
|
|
463
|
+
"round".into(),
|
|
464
|
+
Value::native(|args: &[Value]| {
|
|
465
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
466
|
+
Value::Number(n.round())
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
math.insert(
|
|
470
|
+
"random".into(),
|
|
471
|
+
Value::native(|_| Value::Number(rand::random::<f64>())),
|
|
472
|
+
);
|
|
473
|
+
math.insert(
|
|
474
|
+
"min".into(),
|
|
475
|
+
Value::native(|args: &[Value]| {
|
|
476
|
+
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
|
|
477
|
+
Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.min(b)))
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
math.insert(
|
|
481
|
+
"max".into(),
|
|
482
|
+
Value::native(|args: &[Value]| {
|
|
483
|
+
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
|
|
484
|
+
Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.max(b)))
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
math.insert(
|
|
488
|
+
"pow".into(),
|
|
489
|
+
Value::native(|args: &[Value]| math_builtins::pow(args)),
|
|
490
|
+
);
|
|
491
|
+
math.insert(
|
|
492
|
+
"sin".into(),
|
|
493
|
+
Value::native(|args: &[Value]| math_builtins::sin(args)),
|
|
494
|
+
);
|
|
495
|
+
math.insert(
|
|
496
|
+
"cos".into(),
|
|
497
|
+
Value::native(|args: &[Value]| math_builtins::cos(args)),
|
|
498
|
+
);
|
|
499
|
+
math.insert(
|
|
500
|
+
"tan".into(),
|
|
501
|
+
Value::native(|args: &[Value]| math_builtins::tan(args)),
|
|
502
|
+
);
|
|
503
|
+
math.insert(
|
|
504
|
+
"log".into(),
|
|
505
|
+
Value::native(|args: &[Value]| math_builtins::log(args)),
|
|
506
|
+
);
|
|
507
|
+
math.insert(
|
|
508
|
+
"exp".into(),
|
|
509
|
+
Value::native(|args: &[Value]| math_builtins::exp(args)),
|
|
510
|
+
);
|
|
511
|
+
math.insert(
|
|
512
|
+
"sign".into(),
|
|
513
|
+
Value::native(|args: &[Value]| math_builtins::sign(args)),
|
|
514
|
+
);
|
|
515
|
+
math.insert(
|
|
516
|
+
"trunc".into(),
|
|
517
|
+
Value::native(|args: &[Value]| math_builtins::trunc(args)),
|
|
518
|
+
);
|
|
519
|
+
// Trig/hypot not covered by `math_builtins`; needed by the 3D engine's
|
|
520
|
+
// camera + character-controller math (atan2/hypot) on the wasm VM, where
|
|
521
|
+
// (unlike `--target js`) there is no host `Math` to fall through to.
|
|
522
|
+
math.insert(
|
|
523
|
+
"atan2".into(),
|
|
524
|
+
Value::native(|args: &[Value]| {
|
|
525
|
+
let y = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
526
|
+
let x = args.get(1).and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
527
|
+
Value::Number(y.atan2(x))
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
math.insert(
|
|
531
|
+
"atan".into(),
|
|
532
|
+
Value::native(|args: &[Value]| {
|
|
533
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
534
|
+
Value::Number(n.atan())
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
math.insert(
|
|
538
|
+
"asin".into(),
|
|
539
|
+
Value::native(|args: &[Value]| {
|
|
540
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
541
|
+
Value::Number(n.asin())
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
math.insert(
|
|
545
|
+
"acos".into(),
|
|
546
|
+
Value::native(|args: &[Value]| {
|
|
547
|
+
let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
|
|
548
|
+
Value::Number(n.acos())
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
math.insert(
|
|
552
|
+
"hypot".into(),
|
|
553
|
+
Value::native(|args: &[Value]| {
|
|
554
|
+
let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
|
|
555
|
+
let sum_sq: f64 = nums.iter().map(|n| n * n).sum();
|
|
556
|
+
Value::Number(sum_sq.sqrt())
|
|
557
|
+
}),
|
|
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);
|
|
582
|
+
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
|
|
583
|
+
math.insert("E".into(), Value::Number(std::f64::consts::E));
|
|
584
|
+
g.insert("Math".into(), value_object_from_map(math));
|
|
585
|
+
|
|
586
|
+
let mut json = ObjectMap::default();
|
|
587
|
+
json.insert(
|
|
588
|
+
"parse".into(),
|
|
589
|
+
Value::native(|args: &[Value]| {
|
|
590
|
+
let s = args
|
|
591
|
+
.first()
|
|
592
|
+
.map(|v| v.to_display_string())
|
|
593
|
+
.unwrap_or_default();
|
|
594
|
+
tishlang_core::json_parse(&s).unwrap_or(Value::Null)
|
|
595
|
+
}),
|
|
596
|
+
);
|
|
597
|
+
json.insert(
|
|
598
|
+
"stringify".into(),
|
|
599
|
+
Value::native(|args: &[Value]| {
|
|
600
|
+
let v = args.first().unwrap_or(&Value::Null);
|
|
601
|
+
Value::String(tishlang_core::json_stringify(v).into())
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
g.insert("JSON".into(), value_object_from_map(json));
|
|
605
|
+
|
|
606
|
+
g.insert(
|
|
607
|
+
"parseInt".into(),
|
|
608
|
+
Value::native(|args: &[Value]| globals_builtins::parse_int(args)),
|
|
609
|
+
);
|
|
610
|
+
g.insert(
|
|
611
|
+
"parseFloat".into(),
|
|
612
|
+
Value::native(|args: &[Value]| globals_builtins::parse_float(args)),
|
|
613
|
+
);
|
|
614
|
+
g.insert(
|
|
615
|
+
"encodeURI".into(),
|
|
616
|
+
Value::native(|args: &[Value]| globals_builtins::encode_uri(args)),
|
|
617
|
+
);
|
|
618
|
+
g.insert(
|
|
619
|
+
"decodeURI".into(),
|
|
620
|
+
Value::native(|args: &[Value]| globals_builtins::decode_uri(args)),
|
|
621
|
+
);
|
|
622
|
+
g.insert(
|
|
623
|
+
"htmlEscape".into(),
|
|
624
|
+
Value::native(|args: &[Value]| {
|
|
625
|
+
tishlang_builtins::string::escape_html(args.first().unwrap_or(&Value::Null))
|
|
626
|
+
}),
|
|
627
|
+
);
|
|
628
|
+
g.insert(
|
|
629
|
+
"Boolean".into(),
|
|
630
|
+
Value::native(|args: &[Value]| globals_builtins::boolean(args)),
|
|
631
|
+
);
|
|
632
|
+
g.insert(
|
|
633
|
+
"isFinite".into(),
|
|
634
|
+
Value::native(|args: &[Value]| globals_builtins::is_finite(args)),
|
|
635
|
+
);
|
|
636
|
+
g.insert(
|
|
637
|
+
"isNaN".into(),
|
|
638
|
+
Value::native(|args: &[Value]| globals_builtins::is_nan(args)),
|
|
639
|
+
);
|
|
640
|
+
g.insert("Infinity".into(), Value::Number(f64::INFINITY));
|
|
641
|
+
g.insert("NaN".into(), Value::Number(f64::NAN));
|
|
642
|
+
g.insert(
|
|
643
|
+
"typeof".into(),
|
|
644
|
+
Value::native(|args: &[Value]| {
|
|
645
|
+
let v = args.first().unwrap_or(&Value::Null);
|
|
646
|
+
Value::String(v.type_name().into())
|
|
647
|
+
}),
|
|
648
|
+
);
|
|
649
|
+
g.insert(
|
|
650
|
+
"Symbol".into(),
|
|
651
|
+
tishlang_builtins::symbol::symbol_object(),
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Date - full constructor (new Date(...)) plus statics now()/parse()/UTC().
|
|
655
|
+
g.insert(
|
|
656
|
+
"Date".into(),
|
|
657
|
+
tishlang_builtins::date::date_constructor_value(),
|
|
658
|
+
);
|
|
659
|
+
g.insert(
|
|
660
|
+
"Set".into(),
|
|
661
|
+
tishlang_builtins::collections::set_constructor_value(),
|
|
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
|
+
}
|
|
684
|
+
g.insert(
|
|
685
|
+
"AudioContext".into(),
|
|
686
|
+
construct_builtin::audio_context_constructor_value(),
|
|
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
|
+
}
|
|
692
|
+
|
|
693
|
+
// Object methods - delegate to tishlang_builtins::globals
|
|
694
|
+
let mut object_methods = ObjectMap::default();
|
|
695
|
+
object_methods.insert(
|
|
696
|
+
"assign".into(),
|
|
697
|
+
Value::native(|args: &[Value]| globals_builtins::object_assign(args)),
|
|
698
|
+
);
|
|
699
|
+
object_methods.insert(
|
|
700
|
+
"fromEntries".into(),
|
|
701
|
+
Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
|
|
702
|
+
);
|
|
703
|
+
object_methods.insert(
|
|
704
|
+
"keys".into(),
|
|
705
|
+
Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
|
|
706
|
+
);
|
|
707
|
+
object_methods.insert(
|
|
708
|
+
"values".into(),
|
|
709
|
+
Value::native(|args: &[Value]| globals_builtins::object_values(args)),
|
|
710
|
+
);
|
|
711
|
+
object_methods.insert(
|
|
712
|
+
"entries".into(),
|
|
713
|
+
Value::native(|args: &[Value]| globals_builtins::object_entries(args)),
|
|
714
|
+
);
|
|
715
|
+
g.insert("Object".into(), value_object_from_map(object_methods));
|
|
716
|
+
|
|
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`.
|
|
719
|
+
let mut array_static = ObjectMap::default();
|
|
720
|
+
array_static.insert(
|
|
721
|
+
"isArray".into(),
|
|
722
|
+
Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
|
|
723
|
+
);
|
|
724
|
+
array_static.insert(
|
|
725
|
+
Arc::from("__call"),
|
|
726
|
+
Value::native(|args: &[Value]| construct_builtin::array_construct(args)),
|
|
727
|
+
);
|
|
728
|
+
g.insert("Array".into(), value_object_from_map(array_static));
|
|
729
|
+
|
|
730
|
+
// String(value) as callable + String.fromCharCode
|
|
731
|
+
let string_convert_fn = Value::native(|args: &[Value]| globals_builtins::string_convert(args));
|
|
732
|
+
let mut string_static = ObjectMap::default();
|
|
733
|
+
string_static.insert(
|
|
734
|
+
"fromCharCode".into(),
|
|
735
|
+
Value::native(|args: &[Value]| globals_builtins::string_from_char_code(args)),
|
|
736
|
+
);
|
|
737
|
+
string_static.insert(Arc::from("__call"), string_convert_fn);
|
|
738
|
+
g.insert("String".into(), value_object_from_map(string_static));
|
|
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
|
+
|
|
748
|
+
// JSX / Lattish: stubs for bytecode VM when no DOM (e.g. console). Override via set_global in browser.
|
|
749
|
+
g.insert("h".into(), Value::native(|_args: &[Value]| Value::Null));
|
|
750
|
+
g.insert(
|
|
751
|
+
"Fragment".into(),
|
|
752
|
+
value_object_from_map(ObjectMap::default()),
|
|
753
|
+
);
|
|
754
|
+
g.insert(
|
|
755
|
+
"createRoot".into(),
|
|
756
|
+
Value::native(|_args: &[Value]| {
|
|
757
|
+
let mut render_obj = ObjectMap::default();
|
|
758
|
+
render_obj.insert(
|
|
759
|
+
"render".into(),
|
|
760
|
+
Value::native(|_args: &[Value]| Value::Null),
|
|
761
|
+
);
|
|
762
|
+
value_object_from_map(render_obj)
|
|
763
|
+
}),
|
|
764
|
+
);
|
|
765
|
+
g.insert(
|
|
766
|
+
"useState".into(),
|
|
767
|
+
Value::native(|args: &[Value]| {
|
|
768
|
+
let init = args.first().cloned().unwrap_or(Value::Null);
|
|
769
|
+
let arr = vec![init, Value::native(|_| Value::Null)];
|
|
770
|
+
Value::Array(VmRef::new(arr))
|
|
771
|
+
}),
|
|
772
|
+
);
|
|
773
|
+
let mut document_obj = ObjectMap::default();
|
|
774
|
+
document_obj.insert("body".into(), Value::Null);
|
|
775
|
+
g.insert("document".into(), value_object_from_map(document_obj));
|
|
776
|
+
|
|
777
|
+
#[cfg(feature = "process")]
|
|
778
|
+
if cap_allows(enabled, "process") {
|
|
779
|
+
let mut process_obj = ObjectMap::default();
|
|
780
|
+
process_obj.insert(
|
|
781
|
+
"exit".into(),
|
|
782
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exit(args)),
|
|
783
|
+
);
|
|
784
|
+
process_obj.insert(
|
|
785
|
+
"cwd".into(),
|
|
786
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_cwd(args)),
|
|
787
|
+
);
|
|
788
|
+
process_obj.insert(
|
|
789
|
+
"exec".into(),
|
|
790
|
+
Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
|
|
791
|
+
);
|
|
792
|
+
process_obj.insert(
|
|
793
|
+
"argv".into(),
|
|
794
|
+
Value::Array(VmRef::new(
|
|
795
|
+
std::env::args().map(|s| Value::String(s.into())).collect(),
|
|
796
|
+
)),
|
|
797
|
+
);
|
|
798
|
+
process_obj.insert(
|
|
799
|
+
"env".into(),
|
|
800
|
+
value_object_from_map(
|
|
801
|
+
std::env::vars()
|
|
802
|
+
.map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
|
|
803
|
+
.collect(),
|
|
804
|
+
),
|
|
805
|
+
);
|
|
806
|
+
g.insert("process".into(), value_object_from_map(process_obj));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
#[cfg(feature = "timers")]
|
|
810
|
+
if cap_allows(enabled, "timers") {
|
|
811
|
+
g.insert(
|
|
812
|
+
"setTimeout".into(),
|
|
813
|
+
Value::native(|args: &[Value]| tishlang_runtime::timer_set_timeout(args)),
|
|
814
|
+
);
|
|
815
|
+
g.insert(
|
|
816
|
+
"clearTimeout".into(),
|
|
817
|
+
Value::native(|args: &[Value]| tishlang_runtime::timer_clear_timeout(args)),
|
|
818
|
+
);
|
|
819
|
+
g.insert(
|
|
820
|
+
"setInterval".into(),
|
|
821
|
+
Value::native(|args: &[Value]| tishlang_runtime::timer_set_interval(args)),
|
|
822
|
+
);
|
|
823
|
+
g.insert(
|
|
824
|
+
"clearInterval".into(),
|
|
825
|
+
Value::native(|args: &[Value]| tishlang_runtime::timer_clear_interval(args)),
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[cfg(feature = "http")]
|
|
830
|
+
if cap_allows(enabled, "http") {
|
|
831
|
+
g.insert(
|
|
832
|
+
"fetch".into(),
|
|
833
|
+
Value::native(|args: &[Value]| tishlang_runtime::fetch_promise(args.to_vec())),
|
|
834
|
+
);
|
|
835
|
+
g.insert(
|
|
836
|
+
"fetchAll".into(),
|
|
837
|
+
Value::native(|args: &[Value]| tishlang_runtime::fetch_all_promise(args.to_vec())),
|
|
838
|
+
);
|
|
839
|
+
g.insert(
|
|
840
|
+
"registerStaticRoute".into(),
|
|
841
|
+
Value::native(|args: &[Value]| {
|
|
842
|
+
let path = match args.first() {
|
|
843
|
+
Some(Value::String(s)) => s.to_string(),
|
|
844
|
+
_ => return Value::Null,
|
|
845
|
+
};
|
|
846
|
+
let body = match args.get(1) {
|
|
847
|
+
Some(Value::String(s)) => s.as_bytes().to_vec(),
|
|
848
|
+
_ => return Value::Null,
|
|
849
|
+
};
|
|
850
|
+
let ct = match args.get(2) {
|
|
851
|
+
Some(Value::String(s)) => s.to_string(),
|
|
852
|
+
_ => "application/octet-stream".to_string(),
|
|
853
|
+
};
|
|
854
|
+
tishlang_runtime::register_static_route(&path, &body, &ct);
|
|
855
|
+
Value::Null
|
|
856
|
+
}),
|
|
857
|
+
);
|
|
858
|
+
g.insert(
|
|
859
|
+
"serve".into(),
|
|
860
|
+
Value::native(|args: &[Value]| {
|
|
861
|
+
// Phase-1 item 2 (see tish:http.serve above for full docs).
|
|
862
|
+
let raw = args.get(1).cloned().unwrap_or(Value::Null);
|
|
863
|
+
let handler_value = match raw {
|
|
864
|
+
Value::Function(_) => raw,
|
|
865
|
+
Value::Object(ref obj) => {
|
|
866
|
+
let obj_ref = obj.borrow();
|
|
867
|
+
if let Some(Value::Function(on_worker)) =
|
|
868
|
+
obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
|
|
869
|
+
{
|
|
870
|
+
let args_for_init = [Value::Number(0.0)];
|
|
871
|
+
on_worker.call(&args_for_init)
|
|
872
|
+
} else if let Some(h) =
|
|
873
|
+
obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
|
|
874
|
+
{
|
|
875
|
+
h
|
|
876
|
+
} else {
|
|
877
|
+
Value::Null
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
_ => Value::Null,
|
|
881
|
+
};
|
|
882
|
+
if let Value::Function(f) = handler_value {
|
|
883
|
+
tishlang_runtime::http_serve(args, move |req_args| f.call(req_args))
|
|
884
|
+
} else {
|
|
885
|
+
Value::Null
|
|
886
|
+
}
|
|
887
|
+
}),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
892
|
+
if cap_allows(enabled, "http") || cap_allows(enabled, "promise") {
|
|
893
|
+
g.insert("Promise".into(), tishlang_runtime::promise_object());
|
|
894
|
+
}
|
|
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
|
+
|
|
905
|
+
g
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/// Shared scope for closure capture (parent frame's locals).
|
|
909
|
+
type ScopeMap = VmRef<ObjectMap>;
|
|
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
|
+
|
|
922
|
+
/// Options for the convenience [`run_with_options`] helper (one-shot VM run from the CLI).
|
|
923
|
+
#[derive(Clone, Debug, Default)]
|
|
924
|
+
pub struct VmRunOptions {
|
|
925
|
+
/// When true and not inside a nested chunk (`enclosing` is `None`), top-level [`Opcode::DeclareVar`]
|
|
926
|
+
/// also writes to globals so the REPL keeps bindings across input lines.
|
|
927
|
+
pub repl_mode: bool,
|
|
928
|
+
/// Enabled capabilities for this run (e.g. `fs`, `http`, `full`). Empty = none (secure default).
|
|
929
|
+
pub capabilities: HashSet<String>,
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
pub struct Vm {
|
|
933
|
+
stack: Vec<Value>,
|
|
934
|
+
scope: ObjectMap,
|
|
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,
|
|
945
|
+
globals: VmRef<ObjectMap>,
|
|
946
|
+
/// Capabilities for `LoadNativeExport` and globals such as `process` / `serve`.
|
|
947
|
+
capabilities: Arc<HashSet<String>>,
|
|
948
|
+
/// Externally registered native modules, keyed by import spec (e.g.
|
|
949
|
+
/// `"cargo:tish_pg"`). Populated by embedders before `run` (see
|
|
950
|
+
/// [`register_native_module`]). Phase-2 item 11: unblocks `cargo:`
|
|
951
|
+
/// imports on the cranelift and llvm backends which run this VM.
|
|
952
|
+
native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
|
|
953
|
+
}
|
|
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
|
+
|
|
1093
|
+
impl Vm {
|
|
1094
|
+
/// VM with every capability that exists in this `tishlang_vm` build (embedders, tests, `run()`).
|
|
1095
|
+
pub fn new() -> Self {
|
|
1096
|
+
Self::with_capabilities_arc(Arc::new(all_compiled_capabilities()))
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/// VM with an explicit capability set (e.g. from `tish run --feature …`).
|
|
1100
|
+
pub fn with_capabilities(capabilities: HashSet<String>) -> Self {
|
|
1101
|
+
Self::with_capabilities_arc(Arc::new(capabilities))
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
fn with_capabilities_arc(capabilities: Arc<HashSet<String>>) -> Self {
|
|
1105
|
+
Self {
|
|
1106
|
+
stack: Vec::new(),
|
|
1107
|
+
scope: ObjectMap::default(),
|
|
1108
|
+
enclosing: SharedChain::new(Vec::new()),
|
|
1109
|
+
globals: VmRef::new(init_globals(capabilities.as_ref())),
|
|
1110
|
+
capabilities,
|
|
1111
|
+
native_modules: VmRef::new(HashMap::new()),
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/// Register an externally-supplied native module under a `cargo:`-style
|
|
1116
|
+
/// spec (e.g. `"cargo:tish_pg"`). The `exports` map is what
|
|
1117
|
+
/// `LoadNativeExport` will index into when user code imports from this
|
|
1118
|
+
/// spec. Intended to be called by the `tishlang_cranelift_runtime` /
|
|
1119
|
+
/// `tishlang_llvm` link step, or by external embedders that want to
|
|
1120
|
+
/// expose Rust crates to `.tish` programs running on the bytecode VM.
|
|
1121
|
+
pub fn register_native_module(&mut self, spec: impl Into<String>, exports: ObjectMap) {
|
|
1122
|
+
self.native_modules
|
|
1123
|
+
.borrow_mut()
|
|
1124
|
+
.insert(spec.into(), VmRef::new(exports));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
pub fn get_global(&self, name: &str) -> Option<Value> {
|
|
1128
|
+
self.globals.borrow().get(name).cloned()
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
pub fn set_global(&mut self, name: Arc<str>, value: Value) {
|
|
1132
|
+
self.globals.borrow_mut().insert(name, value);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/// Names of all globals (for REPL bare-word tab completion).
|
|
1136
|
+
pub fn global_names(&self) -> Vec<String> {
|
|
1137
|
+
self.globals
|
|
1138
|
+
.borrow()
|
|
1139
|
+
.keys()
|
|
1140
|
+
.map(|k| k.as_ref().to_string())
|
|
1141
|
+
.collect()
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
fn read_u16(code: &[u8], ip: &mut usize) -> u16 {
|
|
1145
|
+
let a = code[*ip] as u16;
|
|
1146
|
+
let b = code[*ip + 1] as u16;
|
|
1147
|
+
*ip += 2;
|
|
1148
|
+
(a << 8) | b
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
fn read_i16(code: &[u8], ip: &mut usize) -> i16 {
|
|
1152
|
+
Self::read_u16(code, ip) as i16
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/// Pop innermost try handler, truncate stack, push thrown value, jump to catch.
|
|
1156
|
+
fn unwind_throw(
|
|
1157
|
+
try_handlers: &mut Vec<(usize, usize)>,
|
|
1158
|
+
stack: &mut Vec<Value>,
|
|
1159
|
+
ip: &mut usize,
|
|
1160
|
+
v: Value,
|
|
1161
|
+
) -> Result<(), String> {
|
|
1162
|
+
let (catch_ip, stack_len) = try_handlers
|
|
1163
|
+
.pop()
|
|
1164
|
+
.ok_or_else(|| format!("Uncaught throw: {}", v.to_display_string()))?;
|
|
1165
|
+
stack.truncate(stack_len);
|
|
1166
|
+
stack.push(v);
|
|
1167
|
+
*ip = catch_ip;
|
|
1168
|
+
Ok(())
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
pub fn run(&mut self, chunk: &Chunk) -> Result<Value, String> {
|
|
1172
|
+
self.run_with_options(chunk, false)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/// Run a chunk using this VM's capability set. `repl_mode` persists top-level `let` across REPL lines.
|
|
1176
|
+
pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
|
|
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
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
fn run_chunk(
|
|
1505
|
+
&mut self,
|
|
1506
|
+
chunk: &Chunk,
|
|
1507
|
+
nested: &[Chunk],
|
|
1508
|
+
args: &[Value],
|
|
1509
|
+
repl_mode: bool,
|
|
1510
|
+
) -> Result<Value, String> {
|
|
1511
|
+
let code = &chunk.code;
|
|
1512
|
+
let constants = &chunk.constants;
|
|
1513
|
+
let names = &chunk.names;
|
|
1514
|
+
|
|
1515
|
+
let mut ip = 0;
|
|
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();
|
|
1547
|
+
let param_count = chunk.param_count as usize;
|
|
1548
|
+
if chunk.rest_param_index != NO_REST_PARAM {
|
|
1549
|
+
let ri = chunk.rest_param_index as usize;
|
|
1550
|
+
for (i, name) in chunk.names.iter().take(param_count).enumerate() {
|
|
1551
|
+
if i < ri {
|
|
1552
|
+
let v = args.get(i).cloned().unwrap_or(Value::Null);
|
|
1553
|
+
ls.insert(Arc::clone(name), v);
|
|
1554
|
+
} else if i == ri {
|
|
1555
|
+
let rest_arr: Vec<Value> = args.iter().skip(ri).cloned().collect();
|
|
1556
|
+
ls.insert(Arc::clone(name), Value::Array(VmRef::new(rest_arr)));
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
} else {
|
|
1560
|
+
for (i, name) in chunk.names.iter().take(param_count).enumerate() {
|
|
1561
|
+
if let Some(v) = args.get(i) {
|
|
1562
|
+
ls.insert(Arc::clone(name), v.clone());
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
let mut try_handlers: Vec<(usize, usize)> = vec![];
|
|
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
|
+
}
|
|
1609
|
+
|
|
1610
|
+
loop {
|
|
1611
|
+
if ip >= code.len() {
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1614
|
+
// Offset of the instruction about to execute (read by the error macros, #74).
|
|
1615
|
+
instr_off = ip;
|
|
1616
|
+
let op = code[ip];
|
|
1617
|
+
ip += 1;
|
|
1618
|
+
if op == Opcode::Nop as u8 {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
let opcode = Opcode::from_u8(op).ok_or_else(|| format!("Unknown opcode: {}", op))?;
|
|
1622
|
+
|
|
1623
|
+
match opcode {
|
|
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
|
+
}
|
|
1648
|
+
Opcode::LoadConst => {
|
|
1649
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1650
|
+
let c = constants
|
|
1651
|
+
.get(idx as usize)
|
|
1652
|
+
.ok_or_else(|| format!("Constant index out of bounds: {}", idx))?;
|
|
1653
|
+
let v = match c {
|
|
1654
|
+
Constant::Number(n) => Value::Number(*n),
|
|
1655
|
+
Constant::String(s) => Value::String(tishlang_core::ArcStr::from(s.as_ref())),
|
|
1656
|
+
Constant::Bool(b) => Value::Bool(*b),
|
|
1657
|
+
Constant::Null => Value::Null,
|
|
1658
|
+
Constant::Closure(nested_idx) => {
|
|
1659
|
+
let inner = nested
|
|
1660
|
+
.get(*nested_idx)
|
|
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);
|
|
1668
|
+
let inner_clone = inner.clone();
|
|
1669
|
+
let globals = self.globals.clone();
|
|
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
|
+
});
|
|
1699
|
+
let capabilities = Arc::clone(&self.capabilities);
|
|
1700
|
+
let native_modules = self.native_modules.clone();
|
|
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
|
+
}
|
|
1714
|
+
};
|
|
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
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
self.stack.push(v);
|
|
1736
|
+
}
|
|
1737
|
+
Opcode::LoadVar => {
|
|
1738
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1739
|
+
let name = names
|
|
1740
|
+
.get(idx as usize)
|
|
1741
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1742
|
+
let v = local_scope
|
|
1743
|
+
.as_ref()
|
|
1744
|
+
.and_then(|ls| ls.borrow().get(name.as_ref()).cloned())
|
|
1745
|
+
.or_else(|| {
|
|
1746
|
+
// Walk the captured lexical chain, innermost first.
|
|
1747
|
+
self.enclosing
|
|
1748
|
+
.iter()
|
|
1749
|
+
.find_map(|e| e.borrow().get(name.as_ref()).cloned())
|
|
1750
|
+
})
|
|
1751
|
+
.or_else(|| self.scope.get(name.as_ref()).cloned())
|
|
1752
|
+
.or_else(|| self.globals.borrow().get(name.as_ref()).cloned())
|
|
1753
|
+
.ok_or_else(|| format!("Undefined variable: {}", name))?;
|
|
1754
|
+
self.stack.push(v);
|
|
1755
|
+
}
|
|
1756
|
+
Opcode::StoreVar => {
|
|
1757
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1758
|
+
let name = names
|
|
1759
|
+
.get(idx as usize)
|
|
1760
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1761
|
+
let v = self
|
|
1762
|
+
.stack
|
|
1763
|
+
.pop()
|
|
1764
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1765
|
+
// Update innermost scope that has the variable (matches interpreter Scope.assign)
|
|
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
|
|
1769
|
+
.enclosing
|
|
1770
|
+
.iter()
|
|
1771
|
+
.find(|e| e.borrow().contains_key(name.as_ref()))
|
|
1772
|
+
{
|
|
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);
|
|
1776
|
+
} else if self.scope.contains_key(name.as_ref()) {
|
|
1777
|
+
self.scope.insert(Arc::clone(name), v);
|
|
1778
|
+
} else if self.globals.borrow().contains_key(name.as_ref()) {
|
|
1779
|
+
self.globals.borrow_mut().insert(Arc::clone(name), v);
|
|
1780
|
+
} else {
|
|
1781
|
+
// New variable: at top level (no enclosing) store in globals so REPL persists across lines
|
|
1782
|
+
if self.enclosing.is_empty() {
|
|
1783
|
+
self.globals.borrow_mut().insert(Arc::clone(name), v);
|
|
1784
|
+
} else {
|
|
1785
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
Opcode::DeclareVar => {
|
|
1790
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1791
|
+
let name = names
|
|
1792
|
+
.get(idx as usize)
|
|
1793
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1794
|
+
let v = self
|
|
1795
|
+
.stack
|
|
1796
|
+
.pop()
|
|
1797
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1798
|
+
if let Some(frame) = block_undo_stack.last_mut() {
|
|
1799
|
+
let old = local_scope
|
|
1800
|
+
.as_ref()
|
|
1801
|
+
.and_then(|ls| ls.borrow().get(name.as_ref()).cloned());
|
|
1802
|
+
frame.push((Arc::clone(name), old));
|
|
1803
|
+
}
|
|
1804
|
+
// REPL: persist top-level bindings only (not block-locals shadowing globals).
|
|
1805
|
+
if repl_mode && self.enclosing.is_empty() && block_undo_stack.is_empty() {
|
|
1806
|
+
self.globals
|
|
1807
|
+
.borrow_mut()
|
|
1808
|
+
.insert(Arc::clone(name), v.clone());
|
|
1809
|
+
}
|
|
1810
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1811
|
+
}
|
|
1812
|
+
Opcode::DeclareVarPlain => {
|
|
1813
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1814
|
+
let name = names
|
|
1815
|
+
.get(idx as usize)
|
|
1816
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1817
|
+
let v = self
|
|
1818
|
+
.stack
|
|
1819
|
+
.pop()
|
|
1820
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1821
|
+
if repl_mode && self.enclosing.is_empty() && block_undo_stack.is_empty() {
|
|
1822
|
+
self.globals
|
|
1823
|
+
.borrow_mut()
|
|
1824
|
+
.insert(Arc::clone(name), v.clone());
|
|
1825
|
+
}
|
|
1826
|
+
ls_get_or_init!().borrow_mut().insert(Arc::clone(name), v);
|
|
1827
|
+
}
|
|
1828
|
+
Opcode::EnterBlock => {
|
|
1829
|
+
block_undo_stack.push(Vec::new());
|
|
1830
|
+
}
|
|
1831
|
+
Opcode::ExitBlock => {
|
|
1832
|
+
let frame = block_undo_stack
|
|
1833
|
+
.pop()
|
|
1834
|
+
.ok_or_else(|| "ExitBlock without matching EnterBlock".to_string())?;
|
|
1835
|
+
for (name, old) in frame.into_iter().rev() {
|
|
1836
|
+
let mut ls = ls_get_or_init!().borrow_mut();
|
|
1837
|
+
match old {
|
|
1838
|
+
Some(prev) => {
|
|
1839
|
+
ls.insert(name, prev);
|
|
1840
|
+
}
|
|
1841
|
+
None => {
|
|
1842
|
+
ls.remove(name.as_ref());
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
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
|
+
}
|
|
1864
|
+
Opcode::LoadGlobal => {
|
|
1865
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1866
|
+
let name = names
|
|
1867
|
+
.get(idx as usize)
|
|
1868
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1869
|
+
let v = self
|
|
1870
|
+
.globals
|
|
1871
|
+
.borrow()
|
|
1872
|
+
.get(name.as_ref())
|
|
1873
|
+
.cloned()
|
|
1874
|
+
.ok_or_else(|| format!("Undefined global: {}", name))?;
|
|
1875
|
+
self.stack.push(v);
|
|
1876
|
+
}
|
|
1877
|
+
Opcode::StoreGlobal => {
|
|
1878
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
1879
|
+
let name = names
|
|
1880
|
+
.get(idx as usize)
|
|
1881
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
1882
|
+
let v = self
|
|
1883
|
+
.stack
|
|
1884
|
+
.pop()
|
|
1885
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1886
|
+
self.globals.borrow_mut().insert(Arc::clone(name), v);
|
|
1887
|
+
}
|
|
1888
|
+
Opcode::Pop => {
|
|
1889
|
+
self.stack
|
|
1890
|
+
.pop()
|
|
1891
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1892
|
+
}
|
|
1893
|
+
Opcode::PopN => {
|
|
1894
|
+
let n = Self::read_u16(code, &mut ip) as usize;
|
|
1895
|
+
for _ in 0..n {
|
|
1896
|
+
self.stack
|
|
1897
|
+
.pop()
|
|
1898
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
Opcode::Dup => {
|
|
1902
|
+
let v = self
|
|
1903
|
+
.stack
|
|
1904
|
+
.last()
|
|
1905
|
+
.ok_or_else(|| "Stack underflow".to_string())?
|
|
1906
|
+
.clone();
|
|
1907
|
+
self.stack.push(v);
|
|
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
|
+
}
|
|
1922
|
+
Opcode::Call => {
|
|
1923
|
+
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
1924
|
+
let mut args = Vec::with_capacity(argc);
|
|
1925
|
+
for _ in 0..argc {
|
|
1926
|
+
args.push(
|
|
1927
|
+
self.stack
|
|
1928
|
+
.pop()
|
|
1929
|
+
.ok_or_else(|| "Stack underflow in call".to_string())?,
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
args.reverse();
|
|
1933
|
+
let callee = self
|
|
1934
|
+
.stack
|
|
1935
|
+
.pop()
|
|
1936
|
+
.ok_or_else(|| "Stack underflow: no callee".to_string())?;
|
|
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
|
+
}
|
|
1959
|
+
} else {
|
|
1960
|
+
f.call(&args)
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
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)
|
|
1972
|
+
}
|
|
1973
|
+
_ => raise!(construct_builtin::error_object(
|
|
1974
|
+
"TypeError",
|
|
1975
|
+
&format!("Call of non-function: {}", callee.type_name())
|
|
1976
|
+
)),
|
|
1977
|
+
};
|
|
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
|
+
}
|
|
2019
|
+
self.stack.push(result);
|
|
2020
|
+
}
|
|
2021
|
+
Opcode::CallSpread => {
|
|
2022
|
+
let callee = self
|
|
2023
|
+
.stack
|
|
2024
|
+
.pop()
|
|
2025
|
+
.ok_or_else(|| "Stack underflow: no callee in CallSpread".to_string())?;
|
|
2026
|
+
let args_array = self
|
|
2027
|
+
.stack
|
|
2028
|
+
.pop()
|
|
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
|
+
};
|
|
2035
|
+
let args: Vec<Value> = match &args_array {
|
|
2036
|
+
Value::Array(a) => a.borrow().clone(),
|
|
2037
|
+
_ => {
|
|
2038
|
+
return Err(format!(
|
|
2039
|
+
"CallSpread: args must be array, got {}",
|
|
2040
|
+
args_array.to_display_string()
|
|
2041
|
+
));
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
let f = match &callee {
|
|
2045
|
+
Value::Function(f) => f.clone(),
|
|
2046
|
+
Value::Object(o) => {
|
|
2047
|
+
if let Some(Value::Function(call_fn)) =
|
|
2048
|
+
o.borrow().strings.get("__call")
|
|
2049
|
+
{
|
|
2050
|
+
call_fn.clone()
|
|
2051
|
+
} else {
|
|
2052
|
+
return Err(format!(
|
|
2053
|
+
"Call of non-function: {}",
|
|
2054
|
+
callee.type_name()
|
|
2055
|
+
));
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
_ => {
|
|
2059
|
+
return Err(format!("Call of non-function: {}", callee.type_name()));
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
let result = f.call(&args);
|
|
2063
|
+
if let Some(v) = take_pending_throw() {
|
|
2064
|
+
raise!(v);
|
|
2065
|
+
}
|
|
2066
|
+
self.stack.push(result);
|
|
2067
|
+
}
|
|
2068
|
+
Opcode::Construct => {
|
|
2069
|
+
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
2070
|
+
let mut args = Vec::with_capacity(argc);
|
|
2071
|
+
for _ in 0..argc {
|
|
2072
|
+
args.push(
|
|
2073
|
+
self.stack
|
|
2074
|
+
.pop()
|
|
2075
|
+
.ok_or_else(|| "Stack underflow in construct".to_string())?,
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
args.reverse();
|
|
2079
|
+
let callee = self
|
|
2080
|
+
.stack
|
|
2081
|
+
.pop()
|
|
2082
|
+
.ok_or_else(|| "Stack underflow: no callee for construct".to_string())?;
|
|
2083
|
+
let result = construct_builtin::construct(&callee, &args);
|
|
2084
|
+
if let Some(v) = take_pending_throw() {
|
|
2085
|
+
raise!(v);
|
|
2086
|
+
}
|
|
2087
|
+
self.stack.push(result);
|
|
2088
|
+
}
|
|
2089
|
+
Opcode::ConstructSpread => {
|
|
2090
|
+
let callee = self
|
|
2091
|
+
.stack
|
|
2092
|
+
.pop()
|
|
2093
|
+
.ok_or_else(|| "Stack underflow: callee in ConstructSpread".to_string())?;
|
|
2094
|
+
let args_array = self
|
|
2095
|
+
.stack
|
|
2096
|
+
.pop()
|
|
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
|
+
};
|
|
2103
|
+
let args: Vec<Value> = match &args_array {
|
|
2104
|
+
Value::Array(a) => a.borrow().clone(),
|
|
2105
|
+
_ => {
|
|
2106
|
+
return Err(format!(
|
|
2107
|
+
"ConstructSpread: args must be array, got {}",
|
|
2108
|
+
args_array.to_display_string()
|
|
2109
|
+
));
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
let result = construct_builtin::construct(&callee, &args);
|
|
2113
|
+
if let Some(v) = take_pending_throw() {
|
|
2114
|
+
raise!(v);
|
|
2115
|
+
}
|
|
2116
|
+
self.stack.push(result);
|
|
2117
|
+
}
|
|
2118
|
+
Opcode::Return => {
|
|
2119
|
+
let v = self.stack.pop().unwrap_or(Value::Null);
|
|
2120
|
+
return Ok(v);
|
|
2121
|
+
}
|
|
2122
|
+
Opcode::Jump => {
|
|
2123
|
+
let offset = Self::read_i16(code, &mut ip) as isize;
|
|
2124
|
+
ip = (ip as isize + offset).max(0) as usize;
|
|
2125
|
+
}
|
|
2126
|
+
Opcode::JumpIfFalse => {
|
|
2127
|
+
let offset = Self::read_i16(code, &mut ip) as isize;
|
|
2128
|
+
let v = self
|
|
2129
|
+
.stack
|
|
2130
|
+
.pop()
|
|
2131
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2132
|
+
if !v.is_truthy() {
|
|
2133
|
+
ip = (ip as isize + offset).max(0) as usize;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
Opcode::JumpBack => {
|
|
2137
|
+
let dist = Self::read_u16(code, &mut ip) as usize;
|
|
2138
|
+
ip = ip.saturating_sub(dist);
|
|
2139
|
+
}
|
|
2140
|
+
Opcode::BinOp => {
|
|
2141
|
+
let op_u8 = Self::read_u16(code, &mut ip) as u8;
|
|
2142
|
+
let r = self
|
|
2143
|
+
.stack
|
|
2144
|
+
.pop()
|
|
2145
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2146
|
+
let l = self
|
|
2147
|
+
.stack
|
|
2148
|
+
.pop()
|
|
2149
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2150
|
+
let op =
|
|
2151
|
+
u8_to_binop(op_u8).ok_or_else(|| format!("Unknown binop: {}", op_u8))?;
|
|
2152
|
+
let result = eval_binop(op, &l, &r)?;
|
|
2153
|
+
self.stack.push(result);
|
|
2154
|
+
}
|
|
2155
|
+
Opcode::UnaryOp => {
|
|
2156
|
+
let op_u8 = Self::read_u16(code, &mut ip) as u8;
|
|
2157
|
+
let o = self
|
|
2158
|
+
.stack
|
|
2159
|
+
.pop()
|
|
2160
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2161
|
+
let op = u8_to_unaryop(op_u8)
|
|
2162
|
+
.ok_or_else(|| format!("Unknown unary op: {}", op_u8))?;
|
|
2163
|
+
let result = eval_unary(op, &o)?;
|
|
2164
|
+
self.stack.push(result);
|
|
2165
|
+
}
|
|
2166
|
+
Opcode::GetMember => {
|
|
2167
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
2168
|
+
let key = names
|
|
2169
|
+
.get(idx as usize)
|
|
2170
|
+
.ok_or_else(|| "Name index out of bounds".to_string())?;
|
|
2171
|
+
let obj = self
|
|
2172
|
+
.stack
|
|
2173
|
+
.pop()
|
|
2174
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2175
|
+
let v = catchable!(ic_get_member(chunk, idx, &obj, key));
|
|
2176
|
+
self.stack.push(v);
|
|
2177
|
+
}
|
|
2178
|
+
Opcode::GetMemberOptional => {
|
|
2179
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
2180
|
+
let key = names
|
|
2181
|
+
.get(idx as usize)
|
|
2182
|
+
.ok_or_else(|| "Name index out of bounds".to_string())?;
|
|
2183
|
+
let obj = self
|
|
2184
|
+
.stack
|
|
2185
|
+
.pop()
|
|
2186
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2187
|
+
let v = ic_get_member(chunk, idx, &obj, key).unwrap_or(Value::Null);
|
|
2188
|
+
self.stack.push(v);
|
|
2189
|
+
}
|
|
2190
|
+
Opcode::SetMember => {
|
|
2191
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
2192
|
+
let key = names
|
|
2193
|
+
.get(idx as usize)
|
|
2194
|
+
.ok_or_else(|| "Name index out of bounds".to_string())?;
|
|
2195
|
+
let val = self
|
|
2196
|
+
.stack
|
|
2197
|
+
.pop()
|
|
2198
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2199
|
+
let obj = self
|
|
2200
|
+
.stack
|
|
2201
|
+
.pop()
|
|
2202
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2203
|
+
catchable!(ic_set_member(chunk, idx, &obj, key, val.clone()));
|
|
2204
|
+
self.stack.push(val); // assignment yields value
|
|
2205
|
+
}
|
|
2206
|
+
Opcode::GetIndex => {
|
|
2207
|
+
let idx_val = self
|
|
2208
|
+
.stack
|
|
2209
|
+
.pop()
|
|
2210
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2211
|
+
let obj = self
|
|
2212
|
+
.stack
|
|
2213
|
+
.pop()
|
|
2214
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2215
|
+
let v = catchable!(get_index(&obj, &idx_val));
|
|
2216
|
+
self.stack.push(v);
|
|
2217
|
+
}
|
|
2218
|
+
Opcode::SetIndex => {
|
|
2219
|
+
// Stack: [obj, idx, val, val] (Dup of val for expression result).
|
|
2220
|
+
// Pop val (dup), val, idx, obj; use (obj, idx, val) for set_index; leave val on stack.
|
|
2221
|
+
let dup_val = self
|
|
2222
|
+
.stack
|
|
2223
|
+
.pop()
|
|
2224
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2225
|
+
let val = self
|
|
2226
|
+
.stack
|
|
2227
|
+
.pop()
|
|
2228
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2229
|
+
let idx_val = self
|
|
2230
|
+
.stack
|
|
2231
|
+
.pop()
|
|
2232
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2233
|
+
let obj = self
|
|
2234
|
+
.stack
|
|
2235
|
+
.pop()
|
|
2236
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2237
|
+
catchable!(set_index(&obj, &idx_val, val.clone()));
|
|
2238
|
+
self.stack.push(dup_val); // assignment yields the assigned value
|
|
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
|
+
}
|
|
2253
|
+
Opcode::NewArray => {
|
|
2254
|
+
let n = Self::read_u16(code, &mut ip) as usize;
|
|
2255
|
+
let mut elems = Vec::with_capacity(n);
|
|
2256
|
+
for _ in 0..n {
|
|
2257
|
+
elems.push(
|
|
2258
|
+
self.stack
|
|
2259
|
+
.pop()
|
|
2260
|
+
.ok_or_else(|| "Stack underflow".to_string())?,
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
elems.reverse();
|
|
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
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
Opcode::NewObject => {
|
|
2285
|
+
let n = Self::read_u16(code, &mut ip) as usize;
|
|
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();
|
|
2301
|
+
map.insert(key, val);
|
|
2302
|
+
}
|
|
2303
|
+
self.stack.truncate(base);
|
|
2304
|
+
self.stack.push(Value::Object(VmRef::new(ObjectData {
|
|
2305
|
+
strings: map,
|
|
2306
|
+
symbols: None,
|
|
2307
|
+
})));
|
|
2308
|
+
}
|
|
2309
|
+
Opcode::EnterTry => {
|
|
2310
|
+
let offset = Self::read_u16(code, &mut ip) as usize;
|
|
2311
|
+
let catch_ip = ip + offset;
|
|
2312
|
+
try_handlers.push((catch_ip, self.stack.len()));
|
|
2313
|
+
}
|
|
2314
|
+
Opcode::ExitTry => {
|
|
2315
|
+
try_handlers.pop();
|
|
2316
|
+
}
|
|
2317
|
+
Opcode::ConcatArray => {
|
|
2318
|
+
let right = self
|
|
2319
|
+
.stack
|
|
2320
|
+
.pop()
|
|
2321
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2322
|
+
let left = self
|
|
2323
|
+
.stack
|
|
2324
|
+
.pop()
|
|
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
|
+
};
|
|
2338
|
+
let (mut a, b) = (
|
|
2339
|
+
match &left {
|
|
2340
|
+
Value::Array(arr) => arr.borrow().clone(),
|
|
2341
|
+
_ => {
|
|
2342
|
+
return Err(format!(
|
|
2343
|
+
"ConcatArray: left must be array, got {}",
|
|
2344
|
+
left.to_display_string()
|
|
2345
|
+
));
|
|
2346
|
+
}
|
|
2347
|
+
},
|
|
2348
|
+
match &right {
|
|
2349
|
+
Value::Array(arr) => arr.borrow().clone(),
|
|
2350
|
+
_ => {
|
|
2351
|
+
return Err(format!(
|
|
2352
|
+
"ConcatArray: right must be array, got {}",
|
|
2353
|
+
right.to_display_string()
|
|
2354
|
+
));
|
|
2355
|
+
}
|
|
2356
|
+
},
|
|
2357
|
+
);
|
|
2358
|
+
a.extend(b);
|
|
2359
|
+
self.stack.push(Value::Array(VmRef::new(a)));
|
|
2360
|
+
}
|
|
2361
|
+
Opcode::MergeObject => {
|
|
2362
|
+
let right = self
|
|
2363
|
+
.stack
|
|
2364
|
+
.pop()
|
|
2365
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2366
|
+
let left = self
|
|
2367
|
+
.stack
|
|
2368
|
+
.pop()
|
|
2369
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2370
|
+
match (&left, &right) {
|
|
2371
|
+
(Value::Object(l), Value::Object(r)) => {
|
|
2372
|
+
let merged = merge_object_data(l, r);
|
|
2373
|
+
self.stack.push(Value::Object(VmRef::new(merged)));
|
|
2374
|
+
}
|
|
2375
|
+
_ => {
|
|
2376
|
+
return Err(format!(
|
|
2377
|
+
"MergeObject: expected two objects, got {} and {}",
|
|
2378
|
+
left.to_display_string(),
|
|
2379
|
+
right.to_display_string()
|
|
2380
|
+
));
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
Opcode::ArraySortNumeric => {
|
|
2385
|
+
let operand = Self::read_u16(code, &mut ip);
|
|
2386
|
+
let asc = operand == 0;
|
|
2387
|
+
let arr = self
|
|
2388
|
+
.stack
|
|
2389
|
+
.pop()
|
|
2390
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2391
|
+
let result = if asc {
|
|
2392
|
+
arr_builtins::sort_numeric_asc(&arr)
|
|
2393
|
+
} else {
|
|
2394
|
+
arr_builtins::sort_numeric_desc(&arr)
|
|
2395
|
+
};
|
|
2396
|
+
self.stack.push(result);
|
|
2397
|
+
}
|
|
2398
|
+
Opcode::ArraySortByProperty => {
|
|
2399
|
+
let prop_idx = Self::read_u16(code, &mut ip);
|
|
2400
|
+
let asc = Self::read_u16(code, &mut ip) == 0;
|
|
2401
|
+
let arr = self
|
|
2402
|
+
.stack
|
|
2403
|
+
.pop()
|
|
2404
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2405
|
+
let prop = constants
|
|
2406
|
+
.get(prop_idx as usize)
|
|
2407
|
+
.and_then(|c| {
|
|
2408
|
+
if let Constant::String(s) = c {
|
|
2409
|
+
Some(s.as_ref())
|
|
2410
|
+
} else {
|
|
2411
|
+
None
|
|
2412
|
+
}
|
|
2413
|
+
})
|
|
2414
|
+
.unwrap_or("");
|
|
2415
|
+
let result = arr_builtins::sort_by_property_numeric(&arr, prop, asc);
|
|
2416
|
+
self.stack.push(result);
|
|
2417
|
+
}
|
|
2418
|
+
Opcode::ArrayMapIdentity => {
|
|
2419
|
+
let arr = self
|
|
2420
|
+
.stack
|
|
2421
|
+
.pop()
|
|
2422
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2423
|
+
let result = match &arr {
|
|
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())),
|
|
2427
|
+
_ => Value::Null,
|
|
2428
|
+
};
|
|
2429
|
+
self.stack.push(result);
|
|
2430
|
+
}
|
|
2431
|
+
Opcode::ArrayMapBinOp => {
|
|
2432
|
+
let binop_u8 = code[ip];
|
|
2433
|
+
ip += 1;
|
|
2434
|
+
let const_idx = Self::read_u16(code, &mut ip);
|
|
2435
|
+
let param_left = code[ip] == 0; // 0 = param on left (x op const), 1 = param on right (const op x)
|
|
2436
|
+
ip += 1;
|
|
2437
|
+
let binop = u8_to_binop(binop_u8)
|
|
2438
|
+
.ok_or_else(|| format!("Unknown binop in ArrayMapBinOp: {}", binop_u8))?;
|
|
2439
|
+
let arr = self
|
|
2440
|
+
.stack
|
|
2441
|
+
.pop()
|
|
2442
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2443
|
+
let const_val = constants
|
|
2444
|
+
.get(const_idx as usize)
|
|
2445
|
+
.map(|c| c.to_value())
|
|
2446
|
+
.unwrap_or(Value::Null);
|
|
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,
|
|
2479
|
+
};
|
|
2480
|
+
self.stack.push(result);
|
|
2481
|
+
}
|
|
2482
|
+
Opcode::ArrayFilterBinOp => {
|
|
2483
|
+
let binop_u8 = code[ip];
|
|
2484
|
+
ip += 1;
|
|
2485
|
+
let const_idx = Self::read_u16(code, &mut ip);
|
|
2486
|
+
let param_left = code[ip] == 0; // 0 = param on left (x op const), 1 = param on right (const op x)
|
|
2487
|
+
ip += 1;
|
|
2488
|
+
let binop = u8_to_binop(binop_u8).ok_or_else(|| {
|
|
2489
|
+
format!("Unknown binop in ArrayFilterBinOp: {}", binop_u8)
|
|
2490
|
+
})?;
|
|
2491
|
+
let arr = self
|
|
2492
|
+
.stack
|
|
2493
|
+
.pop()
|
|
2494
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2495
|
+
let const_val = constants
|
|
2496
|
+
.get(const_idx as usize)
|
|
2497
|
+
.map(|c| c.to_value())
|
|
2498
|
+
.unwrap_or(Value::Null);
|
|
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,
|
|
2526
|
+
};
|
|
2527
|
+
self.stack.push(result);
|
|
2528
|
+
}
|
|
2529
|
+
Opcode::Throw => {
|
|
2530
|
+
let v = self
|
|
2531
|
+
.stack
|
|
2532
|
+
.pop()
|
|
2533
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
2534
|
+
raise!(v);
|
|
2535
|
+
}
|
|
2536
|
+
Opcode::AwaitPromise => {
|
|
2537
|
+
let v = self
|
|
2538
|
+
.stack
|
|
2539
|
+
.pop()
|
|
2540
|
+
.ok_or_else(|| "Stack underflow in AwaitPromise".to_string())?;
|
|
2541
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
2542
|
+
{
|
|
2543
|
+
use tishlang_core::Value as V;
|
|
2544
|
+
match v {
|
|
2545
|
+
V::Promise(p) => match p.block_until_settled() {
|
|
2546
|
+
Ok(val) => self.stack.push(val),
|
|
2547
|
+
Err(rej) => {
|
|
2548
|
+
Self::unwind_throw(
|
|
2549
|
+
&mut try_handlers,
|
|
2550
|
+
&mut self.stack,
|
|
2551
|
+
&mut ip,
|
|
2552
|
+
rej,
|
|
2553
|
+
)?;
|
|
2554
|
+
}
|
|
2555
|
+
},
|
|
2556
|
+
other => self.stack.push(tishlang_runtime::await_promise(other)),
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
#[cfg(not(any(feature = "http", feature = "promise")))]
|
|
2560
|
+
{
|
|
2561
|
+
self.stack.push(v);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
Opcode::LoadNativeExport => {
|
|
2565
|
+
let spec_idx = Self::read_u16(code, &mut ip);
|
|
2566
|
+
let export_idx = Self::read_u16(code, &mut ip);
|
|
2567
|
+
let spec = match constants.get(spec_idx as usize) {
|
|
2568
|
+
Some(Constant::String(s)) => s.as_ref(),
|
|
2569
|
+
_ => {
|
|
2570
|
+
return Err(
|
|
2571
|
+
"LoadNativeExport: spec constant out of bounds or not string"
|
|
2572
|
+
.to_string(),
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
let export_name = match constants.get(export_idx as usize) {
|
|
2577
|
+
Some(Constant::String(s)) => s.as_ref(),
|
|
2578
|
+
_ => {
|
|
2579
|
+
return Err("LoadNativeExport: export_name constant out of bounds or not string".to_string());
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
// Phase-2 item 11: consult externally registered native
|
|
2583
|
+
// modules (populated via `Vm::register_native_module`)
|
|
2584
|
+
// before falling through to the built-in lookup. Embedders
|
|
2585
|
+
// on the cranelift / llvm backends that want to expose
|
|
2586
|
+
// `cargo:…` Rust crates should register the module's
|
|
2587
|
+
// exports map before calling `vm.run(chunk)`.
|
|
2588
|
+
let from_registry: Option<Value> = if spec.starts_with("cargo:")
|
|
2589
|
+
|| spec.starts_with("ffi:")
|
|
2590
|
+
{
|
|
2591
|
+
let regs = self.native_modules.borrow();
|
|
2592
|
+
regs.get(spec)
|
|
2593
|
+
.and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
|
|
2594
|
+
} else {
|
|
2595
|
+
None
|
|
2596
|
+
};
|
|
2597
|
+
let v = from_registry
|
|
2598
|
+
.or_else(|| get_builtin_export(self.capabilities.as_ref(), spec, export_name))
|
|
2599
|
+
.ok_or_else(|| {
|
|
2600
|
+
if spec.starts_with("cargo:") {
|
|
2601
|
+
format!(
|
|
2602
|
+
"cargo:{} is not registered on the bytecode VM. Embedders must call Vm::register_native_module before run(). Spec: {} export: {}",
|
|
2603
|
+
spec.trim_start_matches("cargo:"),
|
|
2604
|
+
spec,
|
|
2605
|
+
export_name,
|
|
2606
|
+
)
|
|
2607
|
+
} else {
|
|
2608
|
+
format!(
|
|
2609
|
+
"Built-in module '{}' does not export '{}' or capability not enabled for this run. Use e.g. tish run --feature fs (or full). The tish binary must also be built with that capability linked in.",
|
|
2610
|
+
spec, export_name
|
|
2611
|
+
)
|
|
2612
|
+
}
|
|
2613
|
+
})?;
|
|
2614
|
+
self.stack.push(v);
|
|
2615
|
+
}
|
|
2616
|
+
Opcode::Closure | Opcode::LoadThis => {
|
|
2617
|
+
return Err(format!("Unhandled opcode: {:?}", opcode));
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
#[cfg(feature = "timers")]
|
|
2623
|
+
if cap_allows(self.capabilities.as_ref(), "timers") {
|
|
2624
|
+
tishlang_runtime::drain_timers();
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
Ok(self.stack.pop().unwrap_or(Value::Null))
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
impl Default for Vm {
|
|
2632
|
+
fn default() -> Self {
|
|
2633
|
+
Self::new()
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
/// Rough byte capacity for string coercion (matches hot paths like `"x" + n + "ms"`).
|
|
2638
|
+
fn estimate_string_concat_len(v: &Value) -> usize {
|
|
2639
|
+
match v {
|
|
2640
|
+
Value::String(s) => s.len(),
|
|
2641
|
+
Value::Number(_) => 24,
|
|
2642
|
+
Value::Bool(_) => 5,
|
|
2643
|
+
Value::Null => 4,
|
|
2644
|
+
_ => 32,
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
/// Append JS-style string conversion without an intermediate `String` per operand (unlike
|
|
2649
|
+
/// `format!("{}{}", a.to_display_string(), b.to_display_string())`, which triple-allocates).
|
|
2650
|
+
fn append_value_for_string_concat(out: &mut String, v: &Value) {
|
|
2651
|
+
match v {
|
|
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)),
|
|
2655
|
+
Value::String(s) => out.push_str(s.as_ref()),
|
|
2656
|
+
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
|
2657
|
+
Value::Null => out.push_str("null"),
|
|
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()),
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
|
|
2665
|
+
use tishlang_ast::BinOp::*;
|
|
2666
|
+
use tishlang_core::Value::*;
|
|
2667
|
+
let ln = l.as_number().unwrap_or(f64::NAN);
|
|
2668
|
+
let rn = r.as_number().unwrap_or(f64::NAN);
|
|
2669
|
+
match op {
|
|
2670
|
+
Add => {
|
|
2671
|
+
if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) {
|
|
2672
|
+
let cap = estimate_string_concat_len(l) + estimate_string_concat_len(r);
|
|
2673
|
+
let mut buf = std::string::String::with_capacity(cap);
|
|
2674
|
+
append_value_for_string_concat(&mut buf, l);
|
|
2675
|
+
append_value_for_string_concat(&mut buf, r);
|
|
2676
|
+
Ok(String(buf.into()))
|
|
2677
|
+
} else {
|
|
2678
|
+
Ok(Number(ln + rn))
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
Sub => Ok(Number(ln - rn)),
|
|
2682
|
+
Mul => Ok(Number(ln * rn)),
|
|
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)),
|
|
2690
|
+
Pow => Ok(Number(ln.powf(rn))),
|
|
2691
|
+
Eq => Ok(Bool(l.strict_eq(r))),
|
|
2692
|
+
Ne => Ok(Bool(!l.strict_eq(r))),
|
|
2693
|
+
StrictEq => Ok(Bool(l.strict_eq(r))),
|
|
2694
|
+
StrictNe => Ok(Bool(!l.strict_eq(r))),
|
|
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
|
+
})),
|
|
2714
|
+
And => Ok(Bool(l.is_truthy() && r.is_truthy())),
|
|
2715
|
+
Or => Ok(Bool(l.is_truthy() || r.is_truthy())),
|
|
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)),
|
|
2726
|
+
In => Ok(Bool(match r {
|
|
2727
|
+
Value::Object(_) => object_has(r, l),
|
|
2728
|
+
Value::Array(a) => {
|
|
2729
|
+
let key_s: Arc<str> = match l {
|
|
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()),
|
|
2745
|
+
Value::Number(n) => n.to_string().into(),
|
|
2746
|
+
_ => l.to_display_string().into(),
|
|
2747
|
+
};
|
|
2748
|
+
if key_s.as_ref() == "length" {
|
|
2749
|
+
true
|
|
2750
|
+
} else if let Ok(idx) = key_s.parse::<usize>() {
|
|
2751
|
+
idx < a.borrow().len()
|
|
2752
|
+
} else {
|
|
2753
|
+
false
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
_ => false,
|
|
2757
|
+
})),
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
fn eval_unary(op: UnaryOp, o: &Value) -> Result<Value, String> {
|
|
2762
|
+
use tishlang_ast::UnaryOp::*;
|
|
2763
|
+
use tishlang_core::Value::*;
|
|
2764
|
+
match op {
|
|
2765
|
+
Not => Ok(Bool(!o.is_truthy())),
|
|
2766
|
+
Neg => Ok(Number(-o.as_number().unwrap_or(f64::NAN))),
|
|
2767
|
+
Pos => Ok(Number(o.as_number().unwrap_or(f64::NAN))),
|
|
2768
|
+
BitNot => Ok(Number(!to_int32(o.as_number().unwrap_or(0.0)) as f64)),
|
|
2769
|
+
Void => Ok(Null),
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
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
|
+
|
|
2848
|
+
fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
2849
|
+
match obj {
|
|
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
|
+
}
|
|
2857
|
+
let map = m.borrow();
|
|
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))
|
|
2988
|
+
}
|
|
2989
|
+
Value::Array(a) => {
|
|
2990
|
+
let key_s = key.as_ref();
|
|
2991
|
+
if let Ok(idx) = key_s.parse::<usize>() {
|
|
2992
|
+
let arr = a.borrow();
|
|
2993
|
+
return arr
|
|
2994
|
+
.get(idx)
|
|
2995
|
+
.cloned()
|
|
2996
|
+
.ok_or_else(|| "Index out of bounds".to_string());
|
|
2997
|
+
}
|
|
2998
|
+
if key_s == "length" {
|
|
2999
|
+
return Ok(Value::Number(a.borrow().len() as f64));
|
|
3000
|
+
}
|
|
3001
|
+
let a_clone = a.clone();
|
|
3002
|
+
let method: ArrayMethodFn = match key_s {
|
|
3003
|
+
"push" => make_native_fn(move |args: &[Value]| {
|
|
3004
|
+
arr_builtins::push(&Value::Array(a_clone.clone()), args)
|
|
3005
|
+
}),
|
|
3006
|
+
"pop" => make_native_fn(move |_args: &[Value]| {
|
|
3007
|
+
arr_builtins::pop(&Value::Array(a_clone.clone()))
|
|
3008
|
+
}),
|
|
3009
|
+
"shift" => make_native_fn(move |_args: &[Value]| {
|
|
3010
|
+
arr_builtins::shift(&Value::Array(a_clone.clone()))
|
|
3011
|
+
}),
|
|
3012
|
+
"unshift" => make_native_fn(move |args: &[Value]| {
|
|
3013
|
+
arr_builtins::unshift(&Value::Array(a_clone.clone()), args)
|
|
3014
|
+
}),
|
|
3015
|
+
"reverse" => make_native_fn(move |_args: &[Value]| {
|
|
3016
|
+
arr_builtins::reverse(&Value::Array(a_clone.clone()))
|
|
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
|
+
}),
|
|
3024
|
+
"shuffle" => make_native_fn(move |_args: &[Value]| {
|
|
3025
|
+
arr_builtins::shuffle(&Value::Array(a_clone.clone()))
|
|
3026
|
+
}),
|
|
3027
|
+
"slice" => make_native_fn(move |args: &[Value]| {
|
|
3028
|
+
let start = args.first().unwrap_or(&Value::Null);
|
|
3029
|
+
let end = args.get(1).unwrap_or(&Value::Null);
|
|
3030
|
+
arr_builtins::slice(&Value::Array(a_clone.clone()), start, end)
|
|
3031
|
+
}),
|
|
3032
|
+
"concat" => make_native_fn(move |args: &[Value]| {
|
|
3033
|
+
arr_builtins::concat(&Value::Array(a_clone.clone()), args)
|
|
3034
|
+
}),
|
|
3035
|
+
"join" => make_native_fn(move |args: &[Value]| {
|
|
3036
|
+
let sep = args.first().unwrap_or(&Value::Null);
|
|
3037
|
+
arr_builtins::join(&Value::Array(a_clone.clone()), sep)
|
|
3038
|
+
}),
|
|
3039
|
+
"indexOf" => make_native_fn(move |args: &[Value]| {
|
|
3040
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3041
|
+
arr_builtins::index_of(&Value::Array(a_clone.clone()), search)
|
|
3042
|
+
}),
|
|
3043
|
+
"includes" => make_native_fn(move |args: &[Value]| {
|
|
3044
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3045
|
+
let from = args.get(1);
|
|
3046
|
+
arr_builtins::includes(&Value::Array(a_clone.clone()), search, from)
|
|
3047
|
+
}),
|
|
3048
|
+
"map" => make_native_fn(move |args: &[Value]| {
|
|
3049
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3050
|
+
arr_builtins::map(&Value::Array(a_clone.clone()), &cb)
|
|
3051
|
+
}),
|
|
3052
|
+
"filter" => make_native_fn(move |args: &[Value]| {
|
|
3053
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3054
|
+
arr_builtins::filter(&Value::Array(a_clone.clone()), &cb)
|
|
3055
|
+
}),
|
|
3056
|
+
"reduce" => make_native_fn(move |args: &[Value]| {
|
|
3057
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3058
|
+
let init = args.get(1).cloned().unwrap_or(Value::Null);
|
|
3059
|
+
arr_builtins::reduce(&Value::Array(a_clone.clone()), &cb, &init)
|
|
3060
|
+
}),
|
|
3061
|
+
"forEach" => make_native_fn(move |args: &[Value]| {
|
|
3062
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3063
|
+
arr_builtins::for_each(&Value::Array(a_clone.clone()), &cb)
|
|
3064
|
+
}),
|
|
3065
|
+
"find" => make_native_fn(move |args: &[Value]| {
|
|
3066
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3067
|
+
arr_builtins::find(&Value::Array(a_clone.clone()), &cb)
|
|
3068
|
+
}),
|
|
3069
|
+
"findIndex" => make_native_fn(move |args: &[Value]| {
|
|
3070
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3071
|
+
arr_builtins::find_index(&Value::Array(a_clone.clone()), &cb)
|
|
3072
|
+
}),
|
|
3073
|
+
"some" => make_native_fn(move |args: &[Value]| {
|
|
3074
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3075
|
+
arr_builtins::some(&Value::Array(a_clone.clone()), &cb)
|
|
3076
|
+
}),
|
|
3077
|
+
"every" => make_native_fn(move |args: &[Value]| {
|
|
3078
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3079
|
+
arr_builtins::every(&Value::Array(a_clone.clone()), &cb)
|
|
3080
|
+
}),
|
|
3081
|
+
"flat" => make_native_fn(move |args: &[Value]| {
|
|
3082
|
+
let depth = args.first().unwrap_or(&Value::Number(1.0));
|
|
3083
|
+
arr_builtins::flat(&Value::Array(a_clone.clone()), depth)
|
|
3084
|
+
}),
|
|
3085
|
+
"flatMap" => make_native_fn(move |args: &[Value]| {
|
|
3086
|
+
let cb = args.first().cloned().unwrap_or(Value::Null);
|
|
3087
|
+
arr_builtins::flat_map(&Value::Array(a_clone.clone()), &cb)
|
|
3088
|
+
}),
|
|
3089
|
+
"sort" => make_native_fn(move |args: &[Value]| {
|
|
3090
|
+
let cmp = args.first();
|
|
3091
|
+
if let Some(Value::Function(_)) = cmp {
|
|
3092
|
+
arr_builtins::sort_with_comparator(
|
|
3093
|
+
&Value::Array(a_clone.clone()),
|
|
3094
|
+
cmp.unwrap(),
|
|
3095
|
+
)
|
|
3096
|
+
} else {
|
|
3097
|
+
arr_builtins::sort_default(&Value::Array(a_clone.clone()))
|
|
3098
|
+
}
|
|
3099
|
+
}),
|
|
3100
|
+
"splice" => make_native_fn(move |args: &[Value]| {
|
|
3101
|
+
let start = args.first().unwrap_or(&Value::Null);
|
|
3102
|
+
let delete_count = args.get(1).map(|v| v as &Value);
|
|
3103
|
+
let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
|
|
3104
|
+
arr_builtins::splice(
|
|
3105
|
+
&Value::Array(a_clone.clone()),
|
|
3106
|
+
start,
|
|
3107
|
+
delete_count,
|
|
3108
|
+
&items,
|
|
3109
|
+
)
|
|
3110
|
+
}),
|
|
3111
|
+
_ => return Err(format!("Property '{}' not found", key)),
|
|
3112
|
+
};
|
|
3113
|
+
Ok(Value::Function(method))
|
|
3114
|
+
}
|
|
3115
|
+
Value::String(s) => {
|
|
3116
|
+
let key_s = key.as_ref();
|
|
3117
|
+
if let Ok(idx) = key_s.parse::<usize>() {
|
|
3118
|
+
return match s.chars().nth(idx) {
|
|
3119
|
+
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
3120
|
+
None => Err("Index out of bounds".to_string()),
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
if key_s == "length" {
|
|
3124
|
+
return Ok(Value::Number(s.chars().count() as f64));
|
|
3125
|
+
}
|
|
3126
|
+
let s_clone: tishlang_core::ArcStr = s.clone();
|
|
3127
|
+
let method: ArrayMethodFn = match key_s {
|
|
3128
|
+
"indexOf" => make_native_fn(move |args: &[Value]| {
|
|
3129
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3130
|
+
let from = args.get(1);
|
|
3131
|
+
str_builtins::index_of(&Value::String(s_clone.clone()), search, from)
|
|
3132
|
+
}),
|
|
3133
|
+
"lastIndexOf" => make_native_fn(move |args: &[Value]| {
|
|
3134
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3135
|
+
let position = args.get(1).cloned().unwrap_or(Value::Number(f64::INFINITY));
|
|
3136
|
+
str_builtins::last_index_of(
|
|
3137
|
+
&Value::String(s_clone.clone()),
|
|
3138
|
+
search,
|
|
3139
|
+
&position,
|
|
3140
|
+
)
|
|
3141
|
+
}),
|
|
3142
|
+
"includes" => make_native_fn(move |args: &[Value]| {
|
|
3143
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3144
|
+
let from = args.get(1);
|
|
3145
|
+
str_builtins::includes(&Value::String(s_clone.clone()), search, from)
|
|
3146
|
+
}),
|
|
3147
|
+
"slice" => make_native_fn(move |args: &[Value]| {
|
|
3148
|
+
let start = args.first().unwrap_or(&Value::Null);
|
|
3149
|
+
let end = args.get(1).unwrap_or(&Value::Null);
|
|
3150
|
+
str_builtins::slice(&Value::String(s_clone.clone()), start, end)
|
|
3151
|
+
}),
|
|
3152
|
+
"substring" => make_native_fn(move |args: &[Value]| {
|
|
3153
|
+
let start = args.first().unwrap_or(&Value::Null);
|
|
3154
|
+
let end = args.get(1).unwrap_or(&Value::Null);
|
|
3155
|
+
str_builtins::substring(&Value::String(s_clone.clone()), start, end)
|
|
3156
|
+
}),
|
|
3157
|
+
"split" => make_native_fn(move |args: &[Value]| {
|
|
3158
|
+
let sep = args.first().unwrap_or(&Value::Null);
|
|
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)
|
|
3168
|
+
}),
|
|
3169
|
+
"trim" => make_native_fn(move |_args: &[Value]| {
|
|
3170
|
+
str_builtins::trim(&Value::String(s_clone.clone()))
|
|
3171
|
+
}),
|
|
3172
|
+
"toUpperCase" => make_native_fn(move |_args: &[Value]| {
|
|
3173
|
+
str_builtins::to_upper_case(&Value::String(s_clone.clone()))
|
|
3174
|
+
}),
|
|
3175
|
+
"toLowerCase" => make_native_fn(move |_args: &[Value]| {
|
|
3176
|
+
str_builtins::to_lower_case(&Value::String(s_clone.clone()))
|
|
3177
|
+
}),
|
|
3178
|
+
"startsWith" => make_native_fn(move |args: &[Value]| {
|
|
3179
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3180
|
+
str_builtins::starts_with(&Value::String(s_clone.clone()), search)
|
|
3181
|
+
}),
|
|
3182
|
+
"endsWith" => make_native_fn(move |args: &[Value]| {
|
|
3183
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3184
|
+
str_builtins::ends_with(&Value::String(s_clone.clone()), search)
|
|
3185
|
+
}),
|
|
3186
|
+
"replace" => make_native_fn(move |args: &[Value]| {
|
|
3187
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3188
|
+
let replacement = args.get(1).unwrap_or(&Value::Null);
|
|
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)
|
|
3200
|
+
}),
|
|
3201
|
+
"replaceAll" => make_native_fn(move |args: &[Value]| {
|
|
3202
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
3203
|
+
let replacement = args.get(1).unwrap_or(&Value::Null);
|
|
3204
|
+
str_builtins::replace_all(
|
|
3205
|
+
&Value::String(s_clone.clone()),
|
|
3206
|
+
search,
|
|
3207
|
+
replacement,
|
|
3208
|
+
)
|
|
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
|
+
}),
|
|
3220
|
+
"charAt" => make_native_fn(move |args: &[Value]| {
|
|
3221
|
+
let idx = args.first().unwrap_or(&Value::Null);
|
|
3222
|
+
str_builtins::char_at(&Value::String(s_clone.clone()), idx)
|
|
3223
|
+
}),
|
|
3224
|
+
"charCodeAt" => make_native_fn(move |args: &[Value]| {
|
|
3225
|
+
let idx = args.first().unwrap_or(&Value::Null);
|
|
3226
|
+
str_builtins::char_code_at(&Value::String(s_clone.clone()), idx)
|
|
3227
|
+
}),
|
|
3228
|
+
"repeat" => make_native_fn(move |args: &[Value]| {
|
|
3229
|
+
let count = args.first().unwrap_or(&Value::Null);
|
|
3230
|
+
str_builtins::repeat(&Value::String(s_clone.clone()), count)
|
|
3231
|
+
}),
|
|
3232
|
+
"padStart" => make_native_fn(move |args: &[Value]| {
|
|
3233
|
+
let target_len = args.first().unwrap_or(&Value::Null);
|
|
3234
|
+
let pad = args.get(1).unwrap_or(&Value::Null);
|
|
3235
|
+
str_builtins::pad_start(&Value::String(s_clone.clone()), target_len, pad)
|
|
3236
|
+
}),
|
|
3237
|
+
"padEnd" => make_native_fn(move |args: &[Value]| {
|
|
3238
|
+
let target_len = args.first().unwrap_or(&Value::Null);
|
|
3239
|
+
let pad = args.get(1).unwrap_or(&Value::Null);
|
|
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)
|
|
3258
|
+
}),
|
|
3259
|
+
_ => return Err(format!("Property '{}' not found", key)),
|
|
3260
|
+
};
|
|
3261
|
+
Ok(Value::Function(method))
|
|
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
|
+
},
|
|
3293
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
3294
|
+
Value::Promise(p) => match key.as_ref() {
|
|
3295
|
+
"then" => {
|
|
3296
|
+
let pc = Arc::clone(p);
|
|
3297
|
+
Ok(Value::native(move |args| {
|
|
3298
|
+
tishlang_runtime::promise_instance_then(&pc, args)
|
|
3299
|
+
}))
|
|
3300
|
+
}
|
|
3301
|
+
"catch" => {
|
|
3302
|
+
let pc = Arc::clone(p);
|
|
3303
|
+
Ok(Value::native(move |args| {
|
|
3304
|
+
tishlang_runtime::promise_instance_catch(&pc, args)
|
|
3305
|
+
}))
|
|
3306
|
+
}
|
|
3307
|
+
_ => Err(format!("Property '{}' not found", key)),
|
|
3308
|
+
},
|
|
3309
|
+
_ => Err(format!(
|
|
3310
|
+
"Cannot read property '{}' of {}",
|
|
3311
|
+
key,
|
|
3312
|
+
obj.type_name()
|
|
3313
|
+
)),
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
|
|
3318
|
+
match obj {
|
|
3319
|
+
Value::Object(m) => {
|
|
3320
|
+
m.borrow_mut().strings.insert(Arc::clone(key), val);
|
|
3321
|
+
Ok(())
|
|
3322
|
+
}
|
|
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
|
+
}
|
|
3331
|
+
let idx: usize = key.as_ref().parse().unwrap_or(0);
|
|
3332
|
+
let mut arr = a.borrow_mut();
|
|
3333
|
+
if idx < arr.len() {
|
|
3334
|
+
arr[idx] = val;
|
|
3335
|
+
} else {
|
|
3336
|
+
arr.resize(idx + 1, Value::Null);
|
|
3337
|
+
arr[idx] = val;
|
|
3338
|
+
}
|
|
3339
|
+
Ok(())
|
|
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
|
+
}
|
|
3350
|
+
_ => Err(format!("Cannot set property of {}", obj.type_name())),
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
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
|
+
|
|
3364
|
+
fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
|
|
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
|
+
}
|
|
3374
|
+
Value::Array(a) => {
|
|
3375
|
+
let i = match idx {
|
|
3376
|
+
Value::Number(n) => *n as usize,
|
|
3377
|
+
_ => {
|
|
3378
|
+
return Err(format!(
|
|
3379
|
+
"Array index must be number, got {}",
|
|
3380
|
+
idx.type_name()
|
|
3381
|
+
));
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
Ok(a
|
|
3385
|
+
.borrow()
|
|
3386
|
+
.get(i)
|
|
3387
|
+
.cloned()
|
|
3388
|
+
.unwrap_or(Value::Null))
|
|
3389
|
+
}
|
|
3390
|
+
Value::String(s) => {
|
|
3391
|
+
let i = match idx {
|
|
3392
|
+
Value::Number(n) => {
|
|
3393
|
+
let n = *n;
|
|
3394
|
+
if n < 0.0 || n.fract() != 0.0 {
|
|
3395
|
+
return Err(format!(
|
|
3396
|
+
"String index must be non-negative integer, got {}",
|
|
3397
|
+
n
|
|
3398
|
+
));
|
|
3399
|
+
}
|
|
3400
|
+
let i = n as usize;
|
|
3401
|
+
let len = s.chars().count();
|
|
3402
|
+
if i >= len {
|
|
3403
|
+
return Err("Index out of bounds".to_string());
|
|
3404
|
+
}
|
|
3405
|
+
i
|
|
3406
|
+
}
|
|
3407
|
+
_ => {
|
|
3408
|
+
return Err(format!(
|
|
3409
|
+
"String index must be number, got {}",
|
|
3410
|
+
idx.type_name()
|
|
3411
|
+
));
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
match s.chars().nth(i) {
|
|
3415
|
+
Some(c) => Ok(Value::String(tishlang_core::ArcStr::from(c.to_string()))),
|
|
3416
|
+
None => Err("Index out of bounds".to_string()),
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
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)),
|
|
3422
|
+
#[cfg(any(feature = "http", feature = "promise"))]
|
|
3423
|
+
Value::Promise(_) => {
|
|
3424
|
+
let key_arc: std::sync::Arc<str> = match idx {
|
|
3425
|
+
Value::String(s) => std::sync::Arc::from(s.as_str()),
|
|
3426
|
+
_ => {
|
|
3427
|
+
return Err(format!(
|
|
3428
|
+
"Promise bracket access requires a string key, got {}",
|
|
3429
|
+
idx.type_name()
|
|
3430
|
+
));
|
|
3431
|
+
}
|
|
3432
|
+
};
|
|
3433
|
+
get_member(obj, &key_arc)
|
|
3434
|
+
},
|
|
3435
|
+
_ => Err(format!(
|
|
3436
|
+
"Cannot read property '{}' of {}",
|
|
3437
|
+
idx.to_display_string(),
|
|
3438
|
+
obj.type_name()
|
|
3439
|
+
)),
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
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
|
+
|
|
3471
|
+
fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
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
|
+
}
|
|
3504
|
+
Value::Array(a) => {
|
|
3505
|
+
let i = match idx {
|
|
3506
|
+
Value::Number(n) => *n as usize,
|
|
3507
|
+
_ => {
|
|
3508
|
+
return Err(format!(
|
|
3509
|
+
"Array index must be number, got {}",
|
|
3510
|
+
idx.type_name()
|
|
3511
|
+
));
|
|
3512
|
+
}
|
|
3513
|
+
};
|
|
3514
|
+
let mut arr = a.borrow_mut();
|
|
3515
|
+
while arr.len() <= i {
|
|
3516
|
+
arr.push(Value::Null);
|
|
3517
|
+
}
|
|
3518
|
+
arr[i] = val;
|
|
3519
|
+
Ok(())
|
|
3520
|
+
}
|
|
3521
|
+
Value::Object(_) => object_set(obj, idx, val),
|
|
3522
|
+
_ => Err(format!("Cannot set property of {}", obj.type_name())),
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
/// Run a chunk with every capability linked into this `tishlang_vm` build (tests, embedders).
|
|
3527
|
+
pub fn run(chunk: &Chunk) -> Result<Value, String> {
|
|
3528
|
+
let mut vm = Vm::new();
|
|
3529
|
+
vm.run_with_options(chunk, false)
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
/// Run a chunk with options (e.g. REPL persistence for top-level declarations).
|
|
3533
|
+
pub fn run_with_options(chunk: &Chunk, opts: VmRunOptions) -> Result<Value, String> {
|
|
3534
|
+
let mut vm = Vm::with_capabilities(opts.capabilities);
|
|
3535
|
+
vm.run_with_options(chunk, opts.repl_mode)
|
|
3536
|
+
}
|