@tishlang/tish-format 1.0.12 → 2.0.1

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