@tishlang/tish-format 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,2192 @@
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::string as str_builtins;
16
+ use tishlang_bytecode::{u8_to_binop, u8_to_unaryop, Chunk, Constant, Opcode, NO_REST_PARAM};
17
+ use tishlang_core::{
18
+ merge_object_data, object_get, object_has, object_set, NativeFn, ObjectData, ObjectMap, Value,
19
+ };
20
+
21
+ /// Wrap a closure in the right shared pointer for the current build.
22
+ /// Under `send-values` that's `Arc<dyn Fn + Send + Sync>`; otherwise it's
23
+ /// plain `Rc<dyn Fn>`. Call sites can stay ignorant of the distinction.
24
+ #[cfg(feature = "send-values")]
25
+ #[inline]
26
+ fn make_native_fn<F>(f: F) -> NativeFn
27
+ where
28
+ F: Fn(&[Value]) -> Value + Send + Sync + 'static,
29
+ {
30
+ Arc::new(f)
31
+ }
32
+
33
+ #[cfg(not(feature = "send-values"))]
34
+ #[inline]
35
+ fn make_native_fn<F>(f: F) -> NativeFn
36
+ where
37
+ F: Fn(&[Value]) -> Value + 'static,
38
+ {
39
+ Rc::new(f)
40
+ }
41
+
42
+ // Array / string / object methods have the same shape as `NativeFn`, which
43
+ // is already feature-gated (`Rc<dyn Fn>` vs `Arc<dyn Fn + Send + Sync>`).
44
+ // Alias to that so the VM picks the right pointer type automatically.
45
+ type ArrayMethodFn = NativeFn;
46
+
47
+ /// Feature names enabled for this VM run (`tish run --feature …`). `full` enables every optional capability.
48
+ #[cfg_attr(
49
+ not(any(
50
+ feature = "fs",
51
+ feature = "http",
52
+ feature = "promise",
53
+ feature = "timers",
54
+ feature = "process",
55
+ feature = "ws"
56
+ )),
57
+ allow(dead_code)
58
+ )]
59
+ #[inline]
60
+ fn value_object_from_map(m: ObjectMap) -> Value {
61
+ Value::Object(VmRef::new(ObjectData::from_strings(m)))
62
+ }
63
+
64
+ #[cfg(any(
65
+ feature = "fs",
66
+ feature = "http",
67
+ feature = "promise",
68
+ feature = "timers",
69
+ feature = "process",
70
+ feature = "ws"
71
+ ))]
72
+ #[inline]
73
+ fn cap_allows(enabled: &HashSet<String>, name: &str) -> bool {
74
+ enabled.contains("full") || enabled.contains(name)
75
+ }
76
+
77
+ /// Capabilities linked into this `tishlang_vm` binary (compile-time). Used by [`Vm::new`] and `run()`.
78
+ pub fn all_compiled_capabilities() -> HashSet<String> {
79
+ #[allow(unused_mut)]
80
+ let mut s = HashSet::new();
81
+ #[cfg(feature = "http")]
82
+ s.insert("http".to_string());
83
+ #[cfg(feature = "promise")]
84
+ s.insert("promise".to_string());
85
+ #[cfg(feature = "timers")]
86
+ s.insert("timers".to_string());
87
+ #[cfg(feature = "fs")]
88
+ s.insert("fs".to_string());
89
+ #[cfg(feature = "process")]
90
+ s.insert("process".to_string());
91
+ #[cfg(feature = "regex")]
92
+ s.insert("regex".to_string());
93
+ #[cfg(feature = "ws")]
94
+ s.insert("ws".to_string());
95
+ s
96
+ }
97
+
98
+ /// Look up built-in module export for LoadNativeExport. Returns None if unknown or feature disabled.
99
+ #[cfg_attr(
100
+ not(any(
101
+ feature = "fs",
102
+ feature = "http",
103
+ feature = "promise",
104
+ feature = "timers",
105
+ feature = "process",
106
+ feature = "ws"
107
+ )),
108
+ allow(unused_variables)
109
+ )]
110
+ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str) -> Option<Value> {
111
+ #[cfg(feature = "fs")]
112
+ if spec == "tish:fs" && cap_allows(enabled, "fs") {
113
+ return match export_name {
114
+ "readFile" => Some(Value::native(|args: &[Value]| {
115
+ tishlang_runtime::read_file(args)
116
+ })),
117
+ "writeFile" => Some(Value::native(|args: &[Value]| {
118
+ tishlang_runtime::write_file(args)
119
+ })),
120
+ "fileExists" => Some(Value::native(|args: &[Value]| {
121
+ tishlang_runtime::file_exists(args)
122
+ })),
123
+ "isDir" => Some(Value::native(|args: &[Value]| {
124
+ tishlang_runtime::is_dir(args)
125
+ })),
126
+ "readDir" => Some(Value::native(|args: &[Value]| {
127
+ tishlang_runtime::read_dir(args)
128
+ })),
129
+ "mkdir" => Some(Value::native(|args: &[Value]| {
130
+ tishlang_runtime::mkdir(args)
131
+ })),
132
+ _ => None,
133
+ };
134
+ }
135
+ #[cfg(feature = "http")]
136
+ if spec == "tish:http" && cap_allows(enabled, "http") {
137
+ return match export_name {
138
+ // Bytecode compiler lowers `await expr` to `tish:http.await(promise)` (see tish_bytecode compiler).
139
+ "await" => Some(Value::native(|args: &[Value]| {
140
+ tishlang_runtime::await_promise(args.first().cloned().unwrap_or(Value::Null))
141
+ })),
142
+ "fetch" => Some(Value::native(|args: &[Value]| {
143
+ tishlang_runtime::fetch_promise(args.to_vec())
144
+ })),
145
+ "fetchAll" => Some(Value::native(|args: &[Value]| {
146
+ tishlang_runtime::fetch_all_promise(args.to_vec())
147
+ })),
148
+ "Promise" => Some(tishlang_runtime::promise_object()),
149
+ "serve" => Some(Value::native(|args: &[Value]| {
150
+ // Phase-1 item 2: support `serve(port, { handler, onWorker })`
151
+ // in addition to `serve(port, handler)`. When an options
152
+ // object is given and onWorker is a function, invoke it with
153
+ // worker id 0 and expect it to return the request handler.
154
+ let raw = args.get(1).cloned().unwrap_or(Value::Null);
155
+ let handler_value = match raw {
156
+ Value::Function(_) => raw,
157
+ Value::Object(ref obj) => {
158
+ let obj_ref = obj.borrow();
159
+ if let Some(Value::Function(on_worker)) =
160
+ obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
161
+ {
162
+ let args_for_init = [Value::Number(0.0)];
163
+ on_worker(&args_for_init)
164
+ } else if let Some(h) =
165
+ obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
166
+ {
167
+ h
168
+ } else {
169
+ Value::Null
170
+ }
171
+ }
172
+ _ => Value::Null,
173
+ };
174
+ if let Value::Function(f) = handler_value {
175
+ tishlang_runtime::http_serve(args, move |req_args| f(req_args))
176
+ } else {
177
+ Value::Null
178
+ }
179
+ })),
180
+ _ => None,
181
+ };
182
+ }
183
+ #[cfg(all(feature = "promise", not(feature = "http")))]
184
+ if spec == "tish:http" && cap_allows(enabled, "promise") {
185
+ return match export_name {
186
+ "Promise" => Some(tishlang_runtime::promise_object()),
187
+ "await" => Some(Value::native(|args: &[Value]| {
188
+ tishlang_runtime::await_promise(args.first().cloned().unwrap_or(Value::Null))
189
+ })),
190
+ _ => None,
191
+ };
192
+ }
193
+ #[cfg(feature = "timers")]
194
+ if spec == "tish:timers" && cap_allows(enabled, "timers") {
195
+ return match export_name {
196
+ "setTimeout" => Some(Value::native(|args: &[Value]| {
197
+ tishlang_runtime::timer_set_timeout(args)
198
+ })),
199
+ "setInterval" => Some(Value::native(|args: &[Value]| {
200
+ tishlang_runtime::timer_set_interval(args)
201
+ })),
202
+ "clearTimeout" => Some(Value::native(|args: &[Value]| {
203
+ tishlang_runtime::timer_clear_timeout(args)
204
+ })),
205
+ "clearInterval" => Some(Value::native(|args: &[Value]| {
206
+ tishlang_runtime::timer_clear_interval(args)
207
+ })),
208
+ _ => None,
209
+ };
210
+ }
211
+ #[cfg(feature = "process")]
212
+ if spec == "tish:process" && cap_allows(enabled, "process") {
213
+ return match export_name {
214
+ "exit" => Some(Value::native(|args: &[Value]| {
215
+ tishlang_runtime::process_exit(args)
216
+ })),
217
+ "cwd" => Some(Value::native(|args: &[Value]| {
218
+ tishlang_runtime::process_cwd(args)
219
+ })),
220
+ "exec" => Some(Value::native(|args: &[Value]| {
221
+ tishlang_runtime::process_exec(args)
222
+ })),
223
+ "argv" => Some(Value::Array(VmRef::new(
224
+ std::env::args().map(|s| Value::String(s.into())).collect(),
225
+ ))),
226
+ "env" => Some(value_object_from_map(
227
+ std::env::vars()
228
+ .map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
229
+ .collect(),
230
+ )),
231
+ "process" => {
232
+ let mut m = ObjectMap::default();
233
+ m.insert(
234
+ "exit".into(),
235
+ Value::native(|args: &[Value]| tishlang_runtime::process_exit(args)),
236
+ );
237
+ m.insert(
238
+ "cwd".into(),
239
+ Value::native(|args: &[Value]| tishlang_runtime::process_cwd(args)),
240
+ );
241
+ m.insert(
242
+ "exec".into(),
243
+ Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
244
+ );
245
+ m.insert(
246
+ "argv".into(),
247
+ Value::Array(VmRef::new(
248
+ std::env::args().map(|s| Value::String(s.into())).collect(),
249
+ )),
250
+ );
251
+ m.insert(
252
+ "env".into(),
253
+ value_object_from_map(
254
+ std::env::vars()
255
+ .map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
256
+ .collect(),
257
+ ),
258
+ );
259
+ Some(value_object_from_map(m))
260
+ }
261
+ _ => None,
262
+ };
263
+ }
264
+ #[cfg(feature = "ws")]
265
+ if spec == "tish:ws" && cap_allows(enabled, "ws") {
266
+ return match export_name {
267
+ "WebSocket" => Some(Value::native(|args: &[Value]| {
268
+ tishlang_runtime::web_socket_client(args)
269
+ })),
270
+ "Server" => Some(Value::native(|args: &[Value]| {
271
+ tishlang_runtime::web_socket_server_construct(args)
272
+ })),
273
+ "wsSend" => Some(Value::native(|args: &[Value]| {
274
+ Value::Bool(tishlang_runtime::ws_send_native(
275
+ args.first().unwrap_or(&Value::Null),
276
+ &args
277
+ .get(1)
278
+ .map(|v| v.to_display_string())
279
+ .unwrap_or_default(),
280
+ ))
281
+ })),
282
+ "wsBroadcast" => Some(Value::native(|args: &[Value]| {
283
+ tishlang_runtime::ws_broadcast_native(args)
284
+ })),
285
+ _ => None,
286
+ };
287
+ }
288
+ None
289
+ }
290
+
291
+ /// Console output: println! on native, web_sys::console on wasm
292
+ #[cfg(not(feature = "wasm"))]
293
+ fn vm_log(s: &str) {
294
+ println!("{}", s);
295
+ }
296
+ #[cfg(not(feature = "wasm"))]
297
+ fn vm_log_err(s: &str) {
298
+ eprintln!("{}", s);
299
+ }
300
+ #[cfg(feature = "wasm")]
301
+ fn vm_log(s: &str) {
302
+ #[wasm_bindgen::prelude::wasm_bindgen]
303
+ extern "C" {
304
+ #[wasm_bindgen(js_namespace = console)]
305
+ fn log(s: &str);
306
+ }
307
+ log(s);
308
+ }
309
+ #[cfg(feature = "wasm")]
310
+ fn vm_log_err(s: &str) {
311
+ #[wasm_bindgen::prelude::wasm_bindgen]
312
+ extern "C" {
313
+ #[wasm_bindgen(js_namespace = console)]
314
+ fn error(s: &str);
315
+ }
316
+ error(s);
317
+ }
318
+
319
+ /// Initialize default globals (console, Math, JSON, etc.)
320
+ #[allow(unused_variables)]
321
+ fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
322
+ let mut g = ObjectMap::default();
323
+
324
+ let mut console = ObjectMap::default();
325
+ console.insert(
326
+ "debug".into(),
327
+ Value::native(|args: &[Value]| {
328
+ let s =
329
+ tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
330
+ vm_log(&s);
331
+ Value::Null
332
+ }),
333
+ );
334
+ console.insert(
335
+ "log".into(),
336
+ Value::native(|args: &[Value]| {
337
+ let s =
338
+ tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
339
+ vm_log(&s);
340
+ Value::Null
341
+ }),
342
+ );
343
+ console.insert(
344
+ "info".into(),
345
+ Value::native(|args: &[Value]| {
346
+ let s =
347
+ tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
348
+ vm_log(&s);
349
+ Value::Null
350
+ }),
351
+ );
352
+ console.insert(
353
+ "warn".into(),
354
+ Value::native(|args: &[Value]| {
355
+ let s =
356
+ tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
357
+ vm_log_err(&s);
358
+ Value::Null
359
+ }),
360
+ );
361
+ console.insert(
362
+ "error".into(),
363
+ Value::native(|args: &[Value]| {
364
+ let s =
365
+ tishlang_core::format_values_for_console(args, tishlang_core::use_console_colors());
366
+ vm_log_err(&s);
367
+ Value::Null
368
+ }),
369
+ );
370
+ g.insert("console".into(), value_object_from_map(console));
371
+
372
+ let mut math = ObjectMap::default();
373
+ math.insert(
374
+ "abs".into(),
375
+ Value::native(|args: &[Value]| {
376
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
377
+ Value::Number(n.abs())
378
+ }),
379
+ );
380
+ math.insert(
381
+ "sqrt".into(),
382
+ Value::native(|args: &[Value]| {
383
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
384
+ Value::Number(n.sqrt())
385
+ }),
386
+ );
387
+ math.insert(
388
+ "floor".into(),
389
+ Value::native(|args: &[Value]| {
390
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
391
+ Value::Number(n.floor())
392
+ }),
393
+ );
394
+ math.insert(
395
+ "ceil".into(),
396
+ Value::native(|args: &[Value]| {
397
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
398
+ Value::Number(n.ceil())
399
+ }),
400
+ );
401
+ math.insert(
402
+ "round".into(),
403
+ Value::native(|args: &[Value]| {
404
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
405
+ Value::Number(n.round())
406
+ }),
407
+ );
408
+ math.insert(
409
+ "random".into(),
410
+ Value::native(|_| Value::Number(rand::random::<f64>())),
411
+ );
412
+ math.insert(
413
+ "min".into(),
414
+ Value::native(|args: &[Value]| {
415
+ let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
416
+ Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.min(b)))
417
+ }),
418
+ );
419
+ math.insert(
420
+ "max".into(),
421
+ Value::native(|args: &[Value]| {
422
+ let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
423
+ Value::Number(nums.into_iter().fold(f64::NAN, |a, b| a.max(b)))
424
+ }),
425
+ );
426
+ math.insert(
427
+ "pow".into(),
428
+ Value::native(|args: &[Value]| math_builtins::pow(args)),
429
+ );
430
+ math.insert(
431
+ "sin".into(),
432
+ Value::native(|args: &[Value]| math_builtins::sin(args)),
433
+ );
434
+ math.insert(
435
+ "cos".into(),
436
+ Value::native(|args: &[Value]| math_builtins::cos(args)),
437
+ );
438
+ math.insert(
439
+ "tan".into(),
440
+ Value::native(|args: &[Value]| math_builtins::tan(args)),
441
+ );
442
+ math.insert(
443
+ "log".into(),
444
+ Value::native(|args: &[Value]| math_builtins::log(args)),
445
+ );
446
+ math.insert(
447
+ "exp".into(),
448
+ Value::native(|args: &[Value]| math_builtins::exp(args)),
449
+ );
450
+ math.insert(
451
+ "sign".into(),
452
+ Value::native(|args: &[Value]| math_builtins::sign(args)),
453
+ );
454
+ math.insert(
455
+ "trunc".into(),
456
+ Value::native(|args: &[Value]| math_builtins::trunc(args)),
457
+ );
458
+ // Trig/hypot not covered by `math_builtins`; needed by the 3D engine's
459
+ // camera + character-controller math (atan2/hypot) on the wasm VM, where
460
+ // (unlike `--target js`) there is no host `Math` to fall through to.
461
+ math.insert(
462
+ "atan2".into(),
463
+ Value::native(|args: &[Value]| {
464
+ let y = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
465
+ let x = args.get(1).and_then(|v| v.as_number()).unwrap_or(f64::NAN);
466
+ Value::Number(y.atan2(x))
467
+ }),
468
+ );
469
+ math.insert(
470
+ "atan".into(),
471
+ Value::native(|args: &[Value]| {
472
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
473
+ Value::Number(n.atan())
474
+ }),
475
+ );
476
+ math.insert(
477
+ "asin".into(),
478
+ Value::native(|args: &[Value]| {
479
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
480
+ Value::Number(n.asin())
481
+ }),
482
+ );
483
+ math.insert(
484
+ "acos".into(),
485
+ Value::native(|args: &[Value]| {
486
+ let n = args.first().and_then(|v| v.as_number()).unwrap_or(f64::NAN);
487
+ Value::Number(n.acos())
488
+ }),
489
+ );
490
+ math.insert(
491
+ "hypot".into(),
492
+ Value::native(|args: &[Value]| {
493
+ let nums: Vec<f64> = args.iter().filter_map(|v| v.as_number()).collect();
494
+ let sum_sq: f64 = nums.iter().map(|n| n * n).sum();
495
+ Value::Number(sum_sq.sqrt())
496
+ }),
497
+ );
498
+ math.insert("PI".into(), Value::Number(std::f64::consts::PI));
499
+ math.insert("E".into(), Value::Number(std::f64::consts::E));
500
+ g.insert("Math".into(), value_object_from_map(math));
501
+
502
+ let mut json = ObjectMap::default();
503
+ json.insert(
504
+ "parse".into(),
505
+ Value::native(|args: &[Value]| {
506
+ let s = args
507
+ .first()
508
+ .map(|v| v.to_display_string())
509
+ .unwrap_or_default();
510
+ tishlang_core::json_parse(&s).unwrap_or(Value::Null)
511
+ }),
512
+ );
513
+ json.insert(
514
+ "stringify".into(),
515
+ Value::native(|args: &[Value]| {
516
+ let v = args.first().unwrap_or(&Value::Null);
517
+ Value::String(tishlang_core::json_stringify(v).into())
518
+ }),
519
+ );
520
+ g.insert("JSON".into(), value_object_from_map(json));
521
+
522
+ g.insert(
523
+ "parseInt".into(),
524
+ Value::native(|args: &[Value]| globals_builtins::parse_int(args)),
525
+ );
526
+ g.insert(
527
+ "parseFloat".into(),
528
+ Value::native(|args: &[Value]| globals_builtins::parse_float(args)),
529
+ );
530
+ g.insert(
531
+ "encodeURI".into(),
532
+ Value::native(|args: &[Value]| globals_builtins::encode_uri(args)),
533
+ );
534
+ g.insert(
535
+ "decodeURI".into(),
536
+ Value::native(|args: &[Value]| globals_builtins::decode_uri(args)),
537
+ );
538
+ g.insert(
539
+ "htmlEscape".into(),
540
+ Value::native(|args: &[Value]| {
541
+ tishlang_builtins::string::escape_html(args.first().unwrap_or(&Value::Null))
542
+ }),
543
+ );
544
+ g.insert(
545
+ "Boolean".into(),
546
+ Value::native(|args: &[Value]| globals_builtins::boolean(args)),
547
+ );
548
+ g.insert(
549
+ "isFinite".into(),
550
+ Value::native(|args: &[Value]| globals_builtins::is_finite(args)),
551
+ );
552
+ g.insert(
553
+ "isNaN".into(),
554
+ Value::native(|args: &[Value]| globals_builtins::is_nan(args)),
555
+ );
556
+ g.insert("Infinity".into(), Value::Number(f64::INFINITY));
557
+ g.insert("NaN".into(), Value::Number(f64::NAN));
558
+ g.insert(
559
+ "typeof".into(),
560
+ Value::native(|args: &[Value]| {
561
+ let v = args.first().unwrap_or(&Value::Null);
562
+ Value::String(v.type_name().into())
563
+ }),
564
+ );
565
+ g.insert(
566
+ "Symbol".into(),
567
+ tishlang_builtins::symbol::symbol_object(),
568
+ );
569
+
570
+ // Date - at minimum Date.now() for timing
571
+ let mut date = ObjectMap::default();
572
+ date.insert(
573
+ "now".into(),
574
+ Value::native(|_args: &[Value]| {
575
+ let ms = std::time::SystemTime::now()
576
+ .duration_since(std::time::UNIX_EPOCH)
577
+ .unwrap_or_default()
578
+ .as_millis() as f64;
579
+ Value::Number(ms)
580
+ }),
581
+ );
582
+ g.insert("Date".into(), value_object_from_map(date));
583
+
584
+ g.insert(
585
+ "Uint8Array".into(),
586
+ construct_builtin::uint8_array_constructor_value(),
587
+ );
588
+ g.insert(
589
+ "AudioContext".into(),
590
+ construct_builtin::audio_context_constructor_value(),
591
+ );
592
+
593
+ // Object methods - delegate to tishlang_builtins::globals
594
+ let mut object_methods = ObjectMap::default();
595
+ object_methods.insert(
596
+ "assign".into(),
597
+ Value::native(|args: &[Value]| globals_builtins::object_assign(args)),
598
+ );
599
+ object_methods.insert(
600
+ "fromEntries".into(),
601
+ Value::native(|args: &[Value]| globals_builtins::object_from_entries(args)),
602
+ );
603
+ object_methods.insert(
604
+ "keys".into(),
605
+ Value::native(|args: &[Value]| globals_builtins::object_keys(args)),
606
+ );
607
+ object_methods.insert(
608
+ "values".into(),
609
+ Value::native(|args: &[Value]| globals_builtins::object_values(args)),
610
+ );
611
+ object_methods.insert(
612
+ "entries".into(),
613
+ Value::native(|args: &[Value]| globals_builtins::object_entries(args)),
614
+ );
615
+ g.insert("Object".into(), value_object_from_map(object_methods));
616
+
617
+ // Array.isArray
618
+ let mut array_static = ObjectMap::default();
619
+ array_static.insert(
620
+ "isArray".into(),
621
+ Value::native(|args: &[Value]| globals_builtins::array_is_array(args)),
622
+ );
623
+ g.insert("Array".into(), value_object_from_map(array_static));
624
+
625
+ // String(value) as callable + String.fromCharCode
626
+ let string_convert_fn = Value::native(|args: &[Value]| globals_builtins::string_convert(args));
627
+ let mut string_static = ObjectMap::default();
628
+ string_static.insert(
629
+ "fromCharCode".into(),
630
+ Value::native(|args: &[Value]| globals_builtins::string_from_char_code(args)),
631
+ );
632
+ string_static.insert(Arc::from("__call"), string_convert_fn);
633
+ g.insert("String".into(), value_object_from_map(string_static));
634
+
635
+ // JSX / Lattish: stubs for bytecode VM when no DOM (e.g. console). Override via set_global in browser.
636
+ g.insert("h".into(), Value::native(|_args: &[Value]| Value::Null));
637
+ g.insert(
638
+ "Fragment".into(),
639
+ value_object_from_map(ObjectMap::default()),
640
+ );
641
+ g.insert(
642
+ "createRoot".into(),
643
+ Value::native(|_args: &[Value]| {
644
+ let mut render_obj = ObjectMap::default();
645
+ render_obj.insert(
646
+ "render".into(),
647
+ Value::native(|_args: &[Value]| Value::Null),
648
+ );
649
+ value_object_from_map(render_obj)
650
+ }),
651
+ );
652
+ g.insert(
653
+ "useState".into(),
654
+ Value::native(|args: &[Value]| {
655
+ let init = args.first().cloned().unwrap_or(Value::Null);
656
+ let arr = vec![init, Value::native(|_| Value::Null)];
657
+ Value::Array(VmRef::new(arr))
658
+ }),
659
+ );
660
+ let mut document_obj = ObjectMap::default();
661
+ document_obj.insert("body".into(), Value::Null);
662
+ g.insert("document".into(), value_object_from_map(document_obj));
663
+
664
+ #[cfg(feature = "process")]
665
+ if cap_allows(enabled, "process") {
666
+ let mut process_obj = ObjectMap::default();
667
+ process_obj.insert(
668
+ "exit".into(),
669
+ Value::native(|args: &[Value]| tishlang_runtime::process_exit(args)),
670
+ );
671
+ process_obj.insert(
672
+ "cwd".into(),
673
+ Value::native(|args: &[Value]| tishlang_runtime::process_cwd(args)),
674
+ );
675
+ process_obj.insert(
676
+ "exec".into(),
677
+ Value::native(|args: &[Value]| tishlang_runtime::process_exec(args)),
678
+ );
679
+ process_obj.insert(
680
+ "argv".into(),
681
+ Value::Array(VmRef::new(
682
+ std::env::args().map(|s| Value::String(s.into())).collect(),
683
+ )),
684
+ );
685
+ process_obj.insert(
686
+ "env".into(),
687
+ value_object_from_map(
688
+ std::env::vars()
689
+ .map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
690
+ .collect(),
691
+ ),
692
+ );
693
+ g.insert("process".into(), value_object_from_map(process_obj));
694
+ }
695
+
696
+ #[cfg(feature = "timers")]
697
+ if cap_allows(enabled, "timers") {
698
+ g.insert(
699
+ "setTimeout".into(),
700
+ Value::native(|args: &[Value]| tishlang_runtime::timer_set_timeout(args)),
701
+ );
702
+ g.insert(
703
+ "clearTimeout".into(),
704
+ Value::native(|args: &[Value]| tishlang_runtime::timer_clear_timeout(args)),
705
+ );
706
+ g.insert(
707
+ "setInterval".into(),
708
+ Value::native(|args: &[Value]| tishlang_runtime::timer_set_interval(args)),
709
+ );
710
+ g.insert(
711
+ "clearInterval".into(),
712
+ Value::native(|args: &[Value]| tishlang_runtime::timer_clear_interval(args)),
713
+ );
714
+ }
715
+
716
+ #[cfg(feature = "http")]
717
+ if cap_allows(enabled, "http") {
718
+ g.insert(
719
+ "fetch".into(),
720
+ Value::native(|args: &[Value]| tishlang_runtime::fetch_promise(args.to_vec())),
721
+ );
722
+ g.insert(
723
+ "fetchAll".into(),
724
+ Value::native(|args: &[Value]| tishlang_runtime::fetch_all_promise(args.to_vec())),
725
+ );
726
+ g.insert(
727
+ "registerStaticRoute".into(),
728
+ Value::native(|args: &[Value]| {
729
+ let path = match args.first() {
730
+ Some(Value::String(s)) => s.to_string(),
731
+ _ => return Value::Null,
732
+ };
733
+ let body = match args.get(1) {
734
+ Some(Value::String(s)) => s.as_bytes().to_vec(),
735
+ _ => return Value::Null,
736
+ };
737
+ let ct = match args.get(2) {
738
+ Some(Value::String(s)) => s.to_string(),
739
+ _ => "application/octet-stream".to_string(),
740
+ };
741
+ tishlang_runtime::register_static_route(&path, &body, &ct);
742
+ Value::Null
743
+ }),
744
+ );
745
+ g.insert(
746
+ "serve".into(),
747
+ Value::native(|args: &[Value]| {
748
+ // Phase-1 item 2 (see tish:http.serve above for full docs).
749
+ let raw = args.get(1).cloned().unwrap_or(Value::Null);
750
+ let handler_value = match raw {
751
+ Value::Function(_) => raw,
752
+ Value::Object(ref obj) => {
753
+ let obj_ref = obj.borrow();
754
+ if let Some(Value::Function(on_worker)) =
755
+ obj_ref.strings.get(&std::sync::Arc::from("onWorker")).cloned()
756
+ {
757
+ let args_for_init = [Value::Number(0.0)];
758
+ on_worker(&args_for_init)
759
+ } else if let Some(h) =
760
+ obj_ref.strings.get(&std::sync::Arc::from("handler")).cloned()
761
+ {
762
+ h
763
+ } else {
764
+ Value::Null
765
+ }
766
+ }
767
+ _ => Value::Null,
768
+ };
769
+ if let Value::Function(f) = handler_value {
770
+ tishlang_runtime::http_serve(args, move |req_args| f(req_args))
771
+ } else {
772
+ Value::Null
773
+ }
774
+ }),
775
+ );
776
+ }
777
+
778
+ #[cfg(any(feature = "http", feature = "promise"))]
779
+ if cap_allows(enabled, "http") || cap_allows(enabled, "promise") {
780
+ g.insert("Promise".into(), tishlang_runtime::promise_object());
781
+ }
782
+
783
+ g
784
+ }
785
+
786
+ /// Shared scope for closure capture (parent frame's locals).
787
+ type ScopeMap = VmRef<ObjectMap>;
788
+
789
+ /// Options for the convenience [`run_with_options`] helper (one-shot VM run from the CLI).
790
+ #[derive(Clone, Debug, Default)]
791
+ pub struct VmRunOptions {
792
+ /// When true and not inside a nested chunk (`enclosing` is `None`), top-level [`Opcode::DeclareVar`]
793
+ /// also writes to globals so the REPL keeps bindings across input lines.
794
+ pub repl_mode: bool,
795
+ /// Enabled capabilities for this run (e.g. `fs`, `http`, `full`). Empty = none (secure default).
796
+ pub capabilities: HashSet<String>,
797
+ }
798
+
799
+ pub struct Vm {
800
+ stack: Vec<Value>,
801
+ scope: ObjectMap,
802
+ /// Enclosing scope for closures (captured parent frame locals).
803
+ enclosing: Option<ScopeMap>,
804
+ globals: VmRef<ObjectMap>,
805
+ /// Capabilities for `LoadNativeExport` and globals such as `process` / `serve`.
806
+ capabilities: Arc<HashSet<String>>,
807
+ /// Externally registered native modules, keyed by import spec (e.g.
808
+ /// `"cargo:tish_pg"`). Populated by embedders before `run` (see
809
+ /// [`register_native_module`]). Phase-2 item 11: unblocks `cargo:`
810
+ /// imports on the cranelift and llvm backends which run this VM.
811
+ native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
812
+ }
813
+
814
+ impl Vm {
815
+ /// VM with every capability that exists in this `tishlang_vm` build (embedders, tests, `run()`).
816
+ pub fn new() -> Self {
817
+ Self::with_capabilities_arc(Arc::new(all_compiled_capabilities()))
818
+ }
819
+
820
+ /// VM with an explicit capability set (e.g. from `tish run --feature …`).
821
+ pub fn with_capabilities(capabilities: HashSet<String>) -> Self {
822
+ Self::with_capabilities_arc(Arc::new(capabilities))
823
+ }
824
+
825
+ fn with_capabilities_arc(capabilities: Arc<HashSet<String>>) -> Self {
826
+ Self {
827
+ stack: Vec::new(),
828
+ scope: ObjectMap::default(),
829
+ enclosing: None,
830
+ globals: VmRef::new(init_globals(capabilities.as_ref())),
831
+ capabilities,
832
+ native_modules: VmRef::new(HashMap::new()),
833
+ }
834
+ }
835
+
836
+ /// Register an externally-supplied native module under a `cargo:`-style
837
+ /// spec (e.g. `"cargo:tish_pg"`). The `exports` map is what
838
+ /// `LoadNativeExport` will index into when user code imports from this
839
+ /// spec. Intended to be called by the `tishlang_cranelift_runtime` /
840
+ /// `tishlang_llvm` link step, or by external embedders that want to
841
+ /// expose Rust crates to `.tish` programs running on the bytecode VM.
842
+ pub fn register_native_module(&mut self, spec: impl Into<String>, exports: ObjectMap) {
843
+ self.native_modules
844
+ .borrow_mut()
845
+ .insert(spec.into(), VmRef::new(exports));
846
+ }
847
+
848
+ pub fn get_global(&self, name: &str) -> Option<Value> {
849
+ self.globals.borrow().get(name).cloned()
850
+ }
851
+
852
+ pub fn set_global(&mut self, name: Arc<str>, value: Value) {
853
+ self.globals.borrow_mut().insert(name, value);
854
+ }
855
+
856
+ /// Names of all globals (for REPL bare-word tab completion).
857
+ pub fn global_names(&self) -> Vec<String> {
858
+ self.globals
859
+ .borrow()
860
+ .keys()
861
+ .map(|k| k.as_ref().to_string())
862
+ .collect()
863
+ }
864
+
865
+ fn read_u16(code: &[u8], ip: &mut usize) -> u16 {
866
+ let a = code[*ip] as u16;
867
+ let b = code[*ip + 1] as u16;
868
+ *ip += 2;
869
+ (a << 8) | b
870
+ }
871
+
872
+ fn read_i16(code: &[u8], ip: &mut usize) -> i16 {
873
+ Self::read_u16(code, ip) as i16
874
+ }
875
+
876
+ /// Pop innermost try handler, truncate stack, push thrown value, jump to catch.
877
+ fn unwind_throw(
878
+ try_handlers: &mut Vec<(usize, usize)>,
879
+ stack: &mut Vec<Value>,
880
+ ip: &mut usize,
881
+ v: Value,
882
+ ) -> Result<(), String> {
883
+ let (catch_ip, stack_len) = try_handlers
884
+ .pop()
885
+ .ok_or_else(|| format!("Uncaught throw: {}", v.to_display_string()))?;
886
+ stack.truncate(stack_len);
887
+ stack.push(v);
888
+ *ip = catch_ip;
889
+ Ok(())
890
+ }
891
+
892
+ pub fn run(&mut self, chunk: &Chunk) -> Result<Value, String> {
893
+ self.run_with_options(chunk, false)
894
+ }
895
+
896
+ /// Run a chunk using this VM's capability set. `repl_mode` persists top-level `let` across REPL lines.
897
+ pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
898
+ self.run_chunk(chunk, &chunk.nested, &[], repl_mode)
899
+ }
900
+
901
+ fn run_chunk(
902
+ &mut self,
903
+ chunk: &Chunk,
904
+ nested: &[Chunk],
905
+ args: &[Value],
906
+ repl_mode: bool,
907
+ ) -> Result<Value, String> {
908
+ let code = &chunk.code;
909
+ let constants = &chunk.constants;
910
+ let names = &chunk.names;
911
+
912
+ let mut ip = 0;
913
+ let local_scope: ScopeMap = VmRef::new(ObjectMap::default());
914
+ {
915
+ let mut ls = local_scope.borrow_mut();
916
+ let param_count = chunk.param_count as usize;
917
+ if chunk.rest_param_index != NO_REST_PARAM {
918
+ let ri = chunk.rest_param_index as usize;
919
+ for (i, name) in chunk.names.iter().take(param_count).enumerate() {
920
+ if i < ri {
921
+ let v = args.get(i).cloned().unwrap_or(Value::Null);
922
+ ls.insert(Arc::clone(name), v);
923
+ } else if i == ri {
924
+ let rest_arr: Vec<Value> = args.iter().skip(ri).cloned().collect();
925
+ ls.insert(Arc::clone(name), Value::Array(VmRef::new(rest_arr)));
926
+ }
927
+ }
928
+ } else {
929
+ for (i, name) in chunk.names.iter().take(param_count).enumerate() {
930
+ if let Some(v) = args.get(i) {
931
+ ls.insert(Arc::clone(name), v.clone());
932
+ }
933
+ }
934
+ }
935
+ }
936
+ let mut try_handlers: Vec<(usize, usize)> = vec![];
937
+ let mut block_undo_stack: Vec<Vec<(Arc<str>, Option<Value>)>> = vec![];
938
+
939
+ loop {
940
+ if ip >= code.len() {
941
+ break;
942
+ }
943
+ let op = code[ip];
944
+ ip += 1;
945
+ if op == Opcode::Nop as u8 {
946
+ continue;
947
+ }
948
+ let opcode = Opcode::from_u8(op).ok_or_else(|| format!("Unknown opcode: {}", op))?;
949
+
950
+ match opcode {
951
+ Opcode::Nop => {}
952
+ Opcode::LoadConst => {
953
+ let idx = Self::read_u16(code, &mut ip);
954
+ let c = constants
955
+ .get(idx as usize)
956
+ .ok_or_else(|| format!("Constant index out of bounds: {}", idx))?;
957
+ let v = match c {
958
+ Constant::Number(n) => Value::Number(*n),
959
+ Constant::String(s) => Value::String(Arc::clone(s)),
960
+ Constant::Bool(b) => Value::Bool(*b),
961
+ Constant::Null => Value::Null,
962
+ Constant::Closure(nested_idx) => {
963
+ let inner = nested
964
+ .get(*nested_idx)
965
+ .ok_or_else(|| "Nested chunk index out of bounds".to_string())?;
966
+ let inner_clone = inner.clone();
967
+ let globals = self.globals.clone();
968
+ let enclosing = Some(local_scope.clone());
969
+ let capabilities = Arc::clone(&self.capabilities);
970
+ let native_modules = self.native_modules.clone();
971
+ Value::native(move |args: &[Value]| {
972
+ let mut vm = Vm {
973
+ stack: Vec::new(),
974
+ scope: ObjectMap::default(),
975
+ enclosing: enclosing.clone(),
976
+ globals: globals.clone(),
977
+ capabilities: Arc::clone(&capabilities),
978
+ native_modules: native_modules.clone(),
979
+ };
980
+ vm.run_chunk(&inner_clone, &inner_clone.nested, args, false)
981
+ .unwrap_or(Value::Null)
982
+ })
983
+ }
984
+ };
985
+ self.stack.push(v);
986
+ }
987
+ Opcode::LoadVar => {
988
+ let idx = Self::read_u16(code, &mut ip);
989
+ let name = names
990
+ .get(idx as usize)
991
+ .ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
992
+ let v = local_scope
993
+ .borrow()
994
+ .get(name.as_ref())
995
+ .cloned()
996
+ .or_else(|| {
997
+ self.enclosing
998
+ .as_ref()
999
+ .and_then(|e| e.borrow().get(name.as_ref()).cloned())
1000
+ })
1001
+ .or_else(|| self.scope.get(name.as_ref()).cloned())
1002
+ .or_else(|| self.globals.borrow().get(name.as_ref()).cloned())
1003
+ .ok_or_else(|| format!("Undefined variable: {}", name))?;
1004
+ self.stack.push(v);
1005
+ }
1006
+ Opcode::StoreVar => {
1007
+ let idx = Self::read_u16(code, &mut ip);
1008
+ let name = names
1009
+ .get(idx as usize)
1010
+ .ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
1011
+ let v = self
1012
+ .stack
1013
+ .pop()
1014
+ .ok_or_else(|| "Stack underflow".to_string())?;
1015
+ // Update innermost scope that has the variable (matches interpreter Scope.assign)
1016
+ if local_scope.borrow().contains_key(name.as_ref()) {
1017
+ local_scope.borrow_mut().insert(Arc::clone(name), v);
1018
+ } else if self
1019
+ .enclosing
1020
+ .as_ref()
1021
+ .map(|e| e.borrow().contains_key(name.as_ref()))
1022
+ .unwrap_or(false)
1023
+ {
1024
+ let en = self.enclosing.as_ref().unwrap();
1025
+ en.borrow_mut().insert(Arc::clone(name), v);
1026
+ } else if self.scope.contains_key(name.as_ref()) {
1027
+ self.scope.insert(Arc::clone(name), v);
1028
+ } else if self.globals.borrow().contains_key(name.as_ref()) {
1029
+ self.globals.borrow_mut().insert(Arc::clone(name), v);
1030
+ } else {
1031
+ // New variable: at top level (no enclosing) store in globals so REPL persists across lines
1032
+ if self.enclosing.is_none() {
1033
+ self.globals.borrow_mut().insert(Arc::clone(name), v);
1034
+ } else {
1035
+ local_scope.borrow_mut().insert(Arc::clone(name), v);
1036
+ }
1037
+ }
1038
+ }
1039
+ Opcode::DeclareVar => {
1040
+ let idx = Self::read_u16(code, &mut ip);
1041
+ let name = names
1042
+ .get(idx as usize)
1043
+ .ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
1044
+ let v = self
1045
+ .stack
1046
+ .pop()
1047
+ .ok_or_else(|| "Stack underflow".to_string())?;
1048
+ if let Some(frame) = block_undo_stack.last_mut() {
1049
+ let old = local_scope.borrow().get(name.as_ref()).cloned();
1050
+ frame.push((Arc::clone(name), old));
1051
+ }
1052
+ // REPL: persist top-level bindings only (not block-locals shadowing globals).
1053
+ if repl_mode && self.enclosing.is_none() && block_undo_stack.is_empty() {
1054
+ self.globals
1055
+ .borrow_mut()
1056
+ .insert(Arc::clone(name), v.clone());
1057
+ }
1058
+ local_scope.borrow_mut().insert(Arc::clone(name), v);
1059
+ }
1060
+ Opcode::DeclareVarPlain => {
1061
+ let idx = Self::read_u16(code, &mut ip);
1062
+ let name = names
1063
+ .get(idx as usize)
1064
+ .ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
1065
+ let v = self
1066
+ .stack
1067
+ .pop()
1068
+ .ok_or_else(|| "Stack underflow".to_string())?;
1069
+ if repl_mode && self.enclosing.is_none() && block_undo_stack.is_empty() {
1070
+ self.globals
1071
+ .borrow_mut()
1072
+ .insert(Arc::clone(name), v.clone());
1073
+ }
1074
+ local_scope.borrow_mut().insert(Arc::clone(name), v);
1075
+ }
1076
+ Opcode::EnterBlock => {
1077
+ block_undo_stack.push(Vec::new());
1078
+ }
1079
+ Opcode::ExitBlock => {
1080
+ let frame = block_undo_stack
1081
+ .pop()
1082
+ .ok_or_else(|| "ExitBlock without matching EnterBlock".to_string())?;
1083
+ for (name, old) in frame.into_iter().rev() {
1084
+ let mut ls = local_scope.borrow_mut();
1085
+ match old {
1086
+ Some(prev) => {
1087
+ ls.insert(name, prev);
1088
+ }
1089
+ None => {
1090
+ ls.remove(name.as_ref());
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ Opcode::LoadGlobal => {
1096
+ let idx = Self::read_u16(code, &mut ip);
1097
+ let name = names
1098
+ .get(idx as usize)
1099
+ .ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
1100
+ let v = self
1101
+ .globals
1102
+ .borrow()
1103
+ .get(name.as_ref())
1104
+ .cloned()
1105
+ .ok_or_else(|| format!("Undefined global: {}", name))?;
1106
+ self.stack.push(v);
1107
+ }
1108
+ Opcode::StoreGlobal => {
1109
+ let idx = Self::read_u16(code, &mut ip);
1110
+ let name = names
1111
+ .get(idx as usize)
1112
+ .ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
1113
+ let v = self
1114
+ .stack
1115
+ .pop()
1116
+ .ok_or_else(|| "Stack underflow".to_string())?;
1117
+ self.globals.borrow_mut().insert(Arc::clone(name), v);
1118
+ }
1119
+ Opcode::Pop => {
1120
+ self.stack
1121
+ .pop()
1122
+ .ok_or_else(|| "Stack underflow".to_string())?;
1123
+ }
1124
+ Opcode::PopN => {
1125
+ let n = Self::read_u16(code, &mut ip) as usize;
1126
+ for _ in 0..n {
1127
+ self.stack
1128
+ .pop()
1129
+ .ok_or_else(|| "Stack underflow".to_string())?;
1130
+ }
1131
+ }
1132
+ Opcode::Dup => {
1133
+ let v = self
1134
+ .stack
1135
+ .last()
1136
+ .ok_or_else(|| "Stack underflow".to_string())?
1137
+ .clone();
1138
+ self.stack.push(v);
1139
+ }
1140
+ Opcode::Call => {
1141
+ let argc = Self::read_u16(code, &mut ip) as usize;
1142
+ let mut args = Vec::with_capacity(argc);
1143
+ for _ in 0..argc {
1144
+ args.push(
1145
+ self.stack
1146
+ .pop()
1147
+ .ok_or_else(|| "Stack underflow in call".to_string())?,
1148
+ );
1149
+ }
1150
+ args.reverse();
1151
+ let callee = self
1152
+ .stack
1153
+ .pop()
1154
+ .ok_or_else(|| "Stack underflow: no callee".to_string())?;
1155
+ let f = match &callee {
1156
+ Value::Function(f) => f.clone(),
1157
+ Value::Object(o) => {
1158
+ if let Some(Value::Function(call_fn)) =
1159
+ o.borrow().strings.get("__call")
1160
+ {
1161
+ call_fn.clone()
1162
+ } else {
1163
+ return Err(format!(
1164
+ "Call of non-function: {}",
1165
+ callee.type_name()
1166
+ ));
1167
+ }
1168
+ }
1169
+ _ => {
1170
+ return Err(format!("Call of non-function: {}", callee.type_name()));
1171
+ }
1172
+ };
1173
+ let result = f(&args);
1174
+ self.stack.push(result);
1175
+ }
1176
+ Opcode::CallSpread => {
1177
+ let callee = self
1178
+ .stack
1179
+ .pop()
1180
+ .ok_or_else(|| "Stack underflow: no callee in CallSpread".to_string())?;
1181
+ let args_array = self
1182
+ .stack
1183
+ .pop()
1184
+ .ok_or_else(|| "Stack underflow in CallSpread".to_string())?;
1185
+ let args: Vec<Value> = match &args_array {
1186
+ Value::Array(a) => a.borrow().clone(),
1187
+ _ => {
1188
+ return Err(format!(
1189
+ "CallSpread: args must be array, got {}",
1190
+ args_array.to_display_string()
1191
+ ));
1192
+ }
1193
+ };
1194
+ let f = match &callee {
1195
+ Value::Function(f) => f.clone(),
1196
+ Value::Object(o) => {
1197
+ if let Some(Value::Function(call_fn)) =
1198
+ o.borrow().strings.get("__call")
1199
+ {
1200
+ call_fn.clone()
1201
+ } else {
1202
+ return Err(format!(
1203
+ "Call of non-function: {}",
1204
+ callee.type_name()
1205
+ ));
1206
+ }
1207
+ }
1208
+ _ => {
1209
+ return Err(format!("Call of non-function: {}", callee.type_name()));
1210
+ }
1211
+ };
1212
+ let result = f(&args);
1213
+ self.stack.push(result);
1214
+ }
1215
+ Opcode::Construct => {
1216
+ let argc = Self::read_u16(code, &mut ip) as usize;
1217
+ let mut args = Vec::with_capacity(argc);
1218
+ for _ in 0..argc {
1219
+ args.push(
1220
+ self.stack
1221
+ .pop()
1222
+ .ok_or_else(|| "Stack underflow in construct".to_string())?,
1223
+ );
1224
+ }
1225
+ args.reverse();
1226
+ let callee = self
1227
+ .stack
1228
+ .pop()
1229
+ .ok_or_else(|| "Stack underflow: no callee for construct".to_string())?;
1230
+ let result = construct_builtin::construct(&callee, &args);
1231
+ self.stack.push(result);
1232
+ }
1233
+ Opcode::ConstructSpread => {
1234
+ let callee = self
1235
+ .stack
1236
+ .pop()
1237
+ .ok_or_else(|| "Stack underflow: callee in ConstructSpread".to_string())?;
1238
+ let args_array = self
1239
+ .stack
1240
+ .pop()
1241
+ .ok_or_else(|| "Stack underflow in ConstructSpread".to_string())?;
1242
+ let args: Vec<Value> = match &args_array {
1243
+ Value::Array(a) => a.borrow().clone(),
1244
+ _ => {
1245
+ return Err(format!(
1246
+ "ConstructSpread: args must be array, got {}",
1247
+ args_array.to_display_string()
1248
+ ));
1249
+ }
1250
+ };
1251
+ let result = construct_builtin::construct(&callee, &args);
1252
+ self.stack.push(result);
1253
+ }
1254
+ Opcode::Return => {
1255
+ let v = self.stack.pop().unwrap_or(Value::Null);
1256
+ return Ok(v);
1257
+ }
1258
+ Opcode::Jump => {
1259
+ let offset = Self::read_i16(code, &mut ip) as isize;
1260
+ ip = (ip as isize + offset).max(0) as usize;
1261
+ }
1262
+ Opcode::JumpIfFalse => {
1263
+ let offset = Self::read_i16(code, &mut ip) as isize;
1264
+ let v = self
1265
+ .stack
1266
+ .pop()
1267
+ .ok_or_else(|| "Stack underflow".to_string())?;
1268
+ if !v.is_truthy() {
1269
+ ip = (ip as isize + offset).max(0) as usize;
1270
+ }
1271
+ }
1272
+ Opcode::JumpBack => {
1273
+ let dist = Self::read_u16(code, &mut ip) as usize;
1274
+ ip = ip.saturating_sub(dist);
1275
+ }
1276
+ Opcode::BinOp => {
1277
+ let op_u8 = Self::read_u16(code, &mut ip) as u8;
1278
+ let r = self
1279
+ .stack
1280
+ .pop()
1281
+ .ok_or_else(|| "Stack underflow".to_string())?;
1282
+ let l = self
1283
+ .stack
1284
+ .pop()
1285
+ .ok_or_else(|| "Stack underflow".to_string())?;
1286
+ let op =
1287
+ u8_to_binop(op_u8).ok_or_else(|| format!("Unknown binop: {}", op_u8))?;
1288
+ let result = eval_binop(op, &l, &r)?;
1289
+ self.stack.push(result);
1290
+ }
1291
+ Opcode::UnaryOp => {
1292
+ let op_u8 = Self::read_u16(code, &mut ip) as u8;
1293
+ let o = self
1294
+ .stack
1295
+ .pop()
1296
+ .ok_or_else(|| "Stack underflow".to_string())?;
1297
+ let op = u8_to_unaryop(op_u8)
1298
+ .ok_or_else(|| format!("Unknown unary op: {}", op_u8))?;
1299
+ let result = eval_unary(op, &o)?;
1300
+ self.stack.push(result);
1301
+ }
1302
+ Opcode::GetMember => {
1303
+ let idx = Self::read_u16(code, &mut ip);
1304
+ let key = names
1305
+ .get(idx as usize)
1306
+ .ok_or_else(|| "Name index out of bounds".to_string())?;
1307
+ let obj = self
1308
+ .stack
1309
+ .pop()
1310
+ .ok_or_else(|| "Stack underflow".to_string())?;
1311
+ let v = get_member(&obj, key)?;
1312
+ self.stack.push(v);
1313
+ }
1314
+ Opcode::GetMemberOptional => {
1315
+ let idx = Self::read_u16(code, &mut ip);
1316
+ let key = names
1317
+ .get(idx as usize)
1318
+ .ok_or_else(|| "Name index out of bounds".to_string())?;
1319
+ let obj = self
1320
+ .stack
1321
+ .pop()
1322
+ .ok_or_else(|| "Stack underflow".to_string())?;
1323
+ let v = get_member(&obj, key).unwrap_or(Value::Null);
1324
+ self.stack.push(v);
1325
+ }
1326
+ Opcode::SetMember => {
1327
+ let idx = Self::read_u16(code, &mut ip);
1328
+ let key = names
1329
+ .get(idx as usize)
1330
+ .ok_or_else(|| "Name index out of bounds".to_string())?;
1331
+ let val = self
1332
+ .stack
1333
+ .pop()
1334
+ .ok_or_else(|| "Stack underflow".to_string())?;
1335
+ let obj = self
1336
+ .stack
1337
+ .pop()
1338
+ .ok_or_else(|| "Stack underflow".to_string())?;
1339
+ set_member(&obj, key, val.clone())?;
1340
+ self.stack.push(val); // assignment yields value
1341
+ }
1342
+ Opcode::GetIndex => {
1343
+ let idx_val = self
1344
+ .stack
1345
+ .pop()
1346
+ .ok_or_else(|| "Stack underflow".to_string())?;
1347
+ let obj = self
1348
+ .stack
1349
+ .pop()
1350
+ .ok_or_else(|| "Stack underflow".to_string())?;
1351
+ let v = get_index(&obj, &idx_val)?;
1352
+ self.stack.push(v);
1353
+ }
1354
+ Opcode::SetIndex => {
1355
+ // Stack: [obj, idx, val, val] (Dup of val for expression result).
1356
+ // Pop val (dup), val, idx, obj; use (obj, idx, val) for set_index; leave val on stack.
1357
+ let dup_val = self
1358
+ .stack
1359
+ .pop()
1360
+ .ok_or_else(|| "Stack underflow".to_string())?;
1361
+ let val = self
1362
+ .stack
1363
+ .pop()
1364
+ .ok_or_else(|| "Stack underflow".to_string())?;
1365
+ let idx_val = self
1366
+ .stack
1367
+ .pop()
1368
+ .ok_or_else(|| "Stack underflow".to_string())?;
1369
+ let obj = self
1370
+ .stack
1371
+ .pop()
1372
+ .ok_or_else(|| "Stack underflow".to_string())?;
1373
+ set_index(&obj, &idx_val, val.clone())?;
1374
+ self.stack.push(dup_val); // assignment yields the assigned value
1375
+ }
1376
+ Opcode::NewArray => {
1377
+ let n = Self::read_u16(code, &mut ip) as usize;
1378
+ let mut elems = Vec::with_capacity(n);
1379
+ for _ in 0..n {
1380
+ elems.push(
1381
+ self.stack
1382
+ .pop()
1383
+ .ok_or_else(|| "Stack underflow".to_string())?,
1384
+ );
1385
+ }
1386
+ elems.reverse();
1387
+ self.stack.push(Value::Array(VmRef::new(elems)));
1388
+ }
1389
+ Opcode::NewObject => {
1390
+ let n = Self::read_u16(code, &mut ip) as usize;
1391
+ let mut map = ObjectMap::with_capacity(n.max(1));
1392
+ for _ in 0..n {
1393
+ let val = self
1394
+ .stack
1395
+ .pop()
1396
+ .ok_or_else(|| "Stack underflow".to_string())?;
1397
+ let key_val = self
1398
+ .stack
1399
+ .pop()
1400
+ .ok_or_else(|| "Stack underflow".to_string())?;
1401
+ let key = key_val.to_display_string().into();
1402
+ map.insert(key, val);
1403
+ }
1404
+ self.stack.push(value_object_from_map(map));
1405
+ }
1406
+ Opcode::EnterTry => {
1407
+ let offset = Self::read_u16(code, &mut ip) as usize;
1408
+ let catch_ip = ip + offset;
1409
+ try_handlers.push((catch_ip, self.stack.len()));
1410
+ }
1411
+ Opcode::ExitTry => {
1412
+ try_handlers.pop();
1413
+ }
1414
+ Opcode::ConcatArray => {
1415
+ let right = self
1416
+ .stack
1417
+ .pop()
1418
+ .ok_or_else(|| "Stack underflow".to_string())?;
1419
+ let left = self
1420
+ .stack
1421
+ .pop()
1422
+ .ok_or_else(|| "Stack underflow".to_string())?;
1423
+ let (mut a, b) = (
1424
+ match &left {
1425
+ Value::Array(arr) => arr.borrow().clone(),
1426
+ _ => {
1427
+ return Err(format!(
1428
+ "ConcatArray: left must be array, got {}",
1429
+ left.to_display_string()
1430
+ ));
1431
+ }
1432
+ },
1433
+ match &right {
1434
+ Value::Array(arr) => arr.borrow().clone(),
1435
+ _ => {
1436
+ return Err(format!(
1437
+ "ConcatArray: right must be array, got {}",
1438
+ right.to_display_string()
1439
+ ));
1440
+ }
1441
+ },
1442
+ );
1443
+ a.extend(b);
1444
+ self.stack.push(Value::Array(VmRef::new(a)));
1445
+ }
1446
+ Opcode::MergeObject => {
1447
+ let right = self
1448
+ .stack
1449
+ .pop()
1450
+ .ok_or_else(|| "Stack underflow".to_string())?;
1451
+ let left = self
1452
+ .stack
1453
+ .pop()
1454
+ .ok_or_else(|| "Stack underflow".to_string())?;
1455
+ match (&left, &right) {
1456
+ (Value::Object(l), Value::Object(r)) => {
1457
+ let merged = merge_object_data(l, r);
1458
+ self.stack.push(Value::Object(VmRef::new(merged)));
1459
+ }
1460
+ _ => {
1461
+ return Err(format!(
1462
+ "MergeObject: expected two objects, got {} and {}",
1463
+ left.to_display_string(),
1464
+ right.to_display_string()
1465
+ ));
1466
+ }
1467
+ }
1468
+ }
1469
+ Opcode::ArraySortNumeric => {
1470
+ let operand = Self::read_u16(code, &mut ip);
1471
+ let asc = operand == 0;
1472
+ let arr = self
1473
+ .stack
1474
+ .pop()
1475
+ .ok_or_else(|| "Stack underflow".to_string())?;
1476
+ let result = if asc {
1477
+ arr_builtins::sort_numeric_asc(&arr)
1478
+ } else {
1479
+ arr_builtins::sort_numeric_desc(&arr)
1480
+ };
1481
+ self.stack.push(result);
1482
+ }
1483
+ Opcode::ArraySortByProperty => {
1484
+ let prop_idx = Self::read_u16(code, &mut ip);
1485
+ let asc = Self::read_u16(code, &mut ip) == 0;
1486
+ let arr = self
1487
+ .stack
1488
+ .pop()
1489
+ .ok_or_else(|| "Stack underflow".to_string())?;
1490
+ let prop = constants
1491
+ .get(prop_idx as usize)
1492
+ .and_then(|c| {
1493
+ if let Constant::String(s) = c {
1494
+ Some(s.as_ref())
1495
+ } else {
1496
+ None
1497
+ }
1498
+ })
1499
+ .unwrap_or("");
1500
+ let result = arr_builtins::sort_by_property_numeric(&arr, prop, asc);
1501
+ self.stack.push(result);
1502
+ }
1503
+ Opcode::ArrayMapIdentity => {
1504
+ let arr = self
1505
+ .stack
1506
+ .pop()
1507
+ .ok_or_else(|| "Stack underflow".to_string())?;
1508
+ let result = match &arr {
1509
+ Value::Array(a) => Value::Array(VmRef::new(a.borrow().clone())),
1510
+ _ => Value::Null,
1511
+ };
1512
+ self.stack.push(result);
1513
+ }
1514
+ Opcode::ArrayMapBinOp => {
1515
+ let binop_u8 = code[ip];
1516
+ ip += 1;
1517
+ let const_idx = Self::read_u16(code, &mut ip);
1518
+ let param_left = code[ip] == 0; // 0 = param on left (x op const), 1 = param on right (const op x)
1519
+ ip += 1;
1520
+ let binop = u8_to_binop(binop_u8)
1521
+ .ok_or_else(|| format!("Unknown binop in ArrayMapBinOp: {}", binop_u8))?;
1522
+ let arr = self
1523
+ .stack
1524
+ .pop()
1525
+ .ok_or_else(|| "Stack underflow".to_string())?;
1526
+ let const_val = constants
1527
+ .get(const_idx as usize)
1528
+ .map(|c| c.to_value())
1529
+ .unwrap_or(Value::Null);
1530
+ let result = if let Value::Array(a) = &arr {
1531
+ let arr_borrow = a.borrow();
1532
+ let mapped: Vec<Value> = arr_borrow
1533
+ .iter()
1534
+ .map(|v| {
1535
+ let l: Value = if param_left {
1536
+ (*v).clone()
1537
+ } else {
1538
+ const_val.clone()
1539
+ };
1540
+ let r: Value = if param_left {
1541
+ const_val.clone()
1542
+ } else {
1543
+ (*v).clone()
1544
+ };
1545
+ eval_binop(binop, &l, &r).unwrap_or(Value::Null)
1546
+ })
1547
+ .collect();
1548
+ Value::Array(VmRef::new(mapped))
1549
+ } else {
1550
+ Value::Null
1551
+ };
1552
+ self.stack.push(result);
1553
+ }
1554
+ Opcode::ArrayFilterBinOp => {
1555
+ let binop_u8 = code[ip];
1556
+ ip += 1;
1557
+ let const_idx = Self::read_u16(code, &mut ip);
1558
+ let param_left = code[ip] == 0; // 0 = param on left (x op const), 1 = param on right (const op x)
1559
+ ip += 1;
1560
+ let binop = u8_to_binop(binop_u8).ok_or_else(|| {
1561
+ format!("Unknown binop in ArrayFilterBinOp: {}", binop_u8)
1562
+ })?;
1563
+ let arr = self
1564
+ .stack
1565
+ .pop()
1566
+ .ok_or_else(|| "Stack underflow".to_string())?;
1567
+ let const_val = constants
1568
+ .get(const_idx as usize)
1569
+ .map(|c| c.to_value())
1570
+ .unwrap_or(Value::Null);
1571
+ let result = if let Value::Array(a) = &arr {
1572
+ let arr_borrow = a.borrow();
1573
+ let filtered: Vec<Value> = arr_borrow
1574
+ .iter()
1575
+ .filter(|v| {
1576
+ let l: Value = if param_left {
1577
+ (*v).clone()
1578
+ } else {
1579
+ const_val.clone()
1580
+ };
1581
+ let r: Value = if param_left {
1582
+ const_val.clone()
1583
+ } else {
1584
+ (*v).clone()
1585
+ };
1586
+ let b = eval_binop(binop, &l, &r).unwrap_or(Value::Null);
1587
+ b.is_truthy()
1588
+ })
1589
+ .cloned()
1590
+ .collect();
1591
+ Value::Array(VmRef::new(filtered))
1592
+ } else {
1593
+ Value::Null
1594
+ };
1595
+ self.stack.push(result);
1596
+ }
1597
+ Opcode::Throw => {
1598
+ let v = self
1599
+ .stack
1600
+ .pop()
1601
+ .ok_or_else(|| "Stack underflow".to_string())?;
1602
+ Self::unwind_throw(&mut try_handlers, &mut self.stack, &mut ip, v)?;
1603
+ }
1604
+ Opcode::AwaitPromise => {
1605
+ let v = self
1606
+ .stack
1607
+ .pop()
1608
+ .ok_or_else(|| "Stack underflow in AwaitPromise".to_string())?;
1609
+ #[cfg(any(feature = "http", feature = "promise"))]
1610
+ {
1611
+ use tishlang_core::Value as V;
1612
+ match v {
1613
+ V::Promise(p) => match p.block_until_settled() {
1614
+ Ok(val) => self.stack.push(val),
1615
+ Err(rej) => {
1616
+ Self::unwind_throw(
1617
+ &mut try_handlers,
1618
+ &mut self.stack,
1619
+ &mut ip,
1620
+ rej,
1621
+ )?;
1622
+ }
1623
+ },
1624
+ other => self.stack.push(tishlang_runtime::await_promise(other)),
1625
+ }
1626
+ }
1627
+ #[cfg(not(any(feature = "http", feature = "promise")))]
1628
+ {
1629
+ self.stack.push(v);
1630
+ }
1631
+ }
1632
+ Opcode::LoadNativeExport => {
1633
+ let spec_idx = Self::read_u16(code, &mut ip);
1634
+ let export_idx = Self::read_u16(code, &mut ip);
1635
+ let spec = match constants.get(spec_idx as usize) {
1636
+ Some(Constant::String(s)) => s.as_ref(),
1637
+ _ => {
1638
+ return Err(
1639
+ "LoadNativeExport: spec constant out of bounds or not string"
1640
+ .to_string(),
1641
+ );
1642
+ }
1643
+ };
1644
+ let export_name = match constants.get(export_idx as usize) {
1645
+ Some(Constant::String(s)) => s.as_ref(),
1646
+ _ => {
1647
+ return Err("LoadNativeExport: export_name constant out of bounds or not string".to_string());
1648
+ }
1649
+ };
1650
+ // Phase-2 item 11: consult externally registered native
1651
+ // modules (populated via `Vm::register_native_module`)
1652
+ // before falling through to the built-in lookup. Embedders
1653
+ // on the cranelift / llvm backends that want to expose
1654
+ // `cargo:…` Rust crates should register the module's
1655
+ // exports map before calling `vm.run(chunk)`.
1656
+ let from_registry: Option<Value> = if spec.starts_with("cargo:") {
1657
+ let regs = self.native_modules.borrow();
1658
+ regs.get(spec)
1659
+ .and_then(|m| m.borrow().get(&Arc::from(export_name)).cloned())
1660
+ } else {
1661
+ None
1662
+ };
1663
+ let v = from_registry
1664
+ .or_else(|| get_builtin_export(self.capabilities.as_ref(), spec, export_name))
1665
+ .ok_or_else(|| {
1666
+ if spec.starts_with("cargo:") {
1667
+ format!(
1668
+ "cargo:{} is not registered on the bytecode VM. Embedders must call Vm::register_native_module before run(). Spec: {} export: {}",
1669
+ spec.trim_start_matches("cargo:"),
1670
+ spec,
1671
+ export_name,
1672
+ )
1673
+ } else {
1674
+ format!(
1675
+ "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.",
1676
+ spec, export_name
1677
+ )
1678
+ }
1679
+ })?;
1680
+ self.stack.push(v);
1681
+ }
1682
+ Opcode::Closure | Opcode::LoadThis => {
1683
+ return Err(format!("Unhandled opcode: {:?}", opcode));
1684
+ }
1685
+ }
1686
+ }
1687
+
1688
+ #[cfg(feature = "timers")]
1689
+ if cap_allows(self.capabilities.as_ref(), "timers") {
1690
+ tishlang_runtime::drain_timers();
1691
+ }
1692
+
1693
+ Ok(self.stack.pop().unwrap_or(Value::Null))
1694
+ }
1695
+ }
1696
+
1697
+ impl Default for Vm {
1698
+ fn default() -> Self {
1699
+ Self::new()
1700
+ }
1701
+ }
1702
+
1703
+ /// Rough byte capacity for string coercion (matches hot paths like `"x" + n + "ms"`).
1704
+ fn estimate_string_concat_len(v: &Value) -> usize {
1705
+ match v {
1706
+ Value::String(s) => s.len(),
1707
+ Value::Number(_) => 24,
1708
+ Value::Bool(_) => 5,
1709
+ Value::Null => 4,
1710
+ _ => 32,
1711
+ }
1712
+ }
1713
+
1714
+ /// Append JS-style string conversion without an intermediate `String` per operand (unlike
1715
+ /// `format!("{}{}", a.to_display_string(), b.to_display_string())`, which triple-allocates).
1716
+ fn append_value_for_string_concat(out: &mut String, v: &Value) {
1717
+ use std::fmt::Write;
1718
+ match v {
1719
+ Value::Number(n) => {
1720
+ if n.is_nan() {
1721
+ out.push_str("NaN");
1722
+ } else if *n == f64::INFINITY {
1723
+ out.push_str("Infinity");
1724
+ } else if *n == f64::NEG_INFINITY {
1725
+ out.push_str("-Infinity");
1726
+ } else {
1727
+ let _ = write!(out, "{n}");
1728
+ }
1729
+ }
1730
+ Value::String(s) => out.push_str(s.as_ref()),
1731
+ Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
1732
+ Value::Null => out.push_str("null"),
1733
+ _ => out.push_str(&v.to_display_string()),
1734
+ }
1735
+ }
1736
+
1737
+ fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
1738
+ use tishlang_ast::BinOp::*;
1739
+ use tishlang_core::Value::*;
1740
+ let ln = l.as_number().unwrap_or(f64::NAN);
1741
+ let rn = r.as_number().unwrap_or(f64::NAN);
1742
+ match op {
1743
+ Add => {
1744
+ if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) {
1745
+ let cap = estimate_string_concat_len(l) + estimate_string_concat_len(r);
1746
+ let mut buf = std::string::String::with_capacity(cap);
1747
+ append_value_for_string_concat(&mut buf, l);
1748
+ append_value_for_string_concat(&mut buf, r);
1749
+ Ok(String(buf.into()))
1750
+ } else {
1751
+ Ok(Number(ln + rn))
1752
+ }
1753
+ }
1754
+ Sub => Ok(Number(ln - rn)),
1755
+ Mul => Ok(Number(ln * rn)),
1756
+ Div => Ok(Number(if rn == 0.0 { f64::NAN } else { ln / rn })),
1757
+ Mod => Ok(Number(if rn == 0.0 { f64::NAN } else { ln % rn })),
1758
+ Pow => Ok(Number(ln.powf(rn))),
1759
+ Eq => Ok(Bool(l.strict_eq(r))),
1760
+ Ne => Ok(Bool(!l.strict_eq(r))),
1761
+ StrictEq => Ok(Bool(l.strict_eq(r))),
1762
+ StrictNe => Ok(Bool(!l.strict_eq(r))),
1763
+ Lt => Ok(Bool(ln < rn)),
1764
+ Le => Ok(Bool(ln <= rn)),
1765
+ Gt => Ok(Bool(ln > rn)),
1766
+ Ge => Ok(Bool(ln >= rn)),
1767
+ And => Ok(Bool(l.is_truthy() && r.is_truthy())),
1768
+ Or => Ok(Bool(l.is_truthy() || r.is_truthy())),
1769
+ BitAnd => Ok(Number((ln as i32 & rn as i32) as f64)),
1770
+ BitOr => Ok(Number((ln as i32 | rn as i32) as f64)),
1771
+ BitXor => Ok(Number((ln as i32 ^ rn as i32) as f64)),
1772
+ Shl => Ok(Number(((ln as i32) << (rn as i32)) as f64)),
1773
+ Shr => Ok(Number(((ln as i32) >> (rn as i32)) as f64)),
1774
+ In => Ok(Bool(match r {
1775
+ Value::Object(_) => object_has(r, l),
1776
+ Value::Array(a) => {
1777
+ let key_s: Arc<str> = match l {
1778
+ Value::String(s) => Arc::clone(s),
1779
+ Value::Number(n) => n.to_string().into(),
1780
+ _ => l.to_display_string().into(),
1781
+ };
1782
+ if key_s.as_ref() == "length" {
1783
+ true
1784
+ } else if let Ok(idx) = key_s.parse::<usize>() {
1785
+ idx < a.borrow().len()
1786
+ } else {
1787
+ false
1788
+ }
1789
+ }
1790
+ _ => false,
1791
+ })),
1792
+ }
1793
+ }
1794
+
1795
+ fn eval_unary(op: UnaryOp, o: &Value) -> Result<Value, String> {
1796
+ use tishlang_ast::UnaryOp::*;
1797
+ use tishlang_core::Value::*;
1798
+ match op {
1799
+ Not => Ok(Bool(!o.is_truthy())),
1800
+ Neg => Ok(Number(-o.as_number().unwrap_or(f64::NAN))),
1801
+ Pos => Ok(Number(o.as_number().unwrap_or(f64::NAN))),
1802
+ BitNot => Ok(Number(!(o.as_number().unwrap_or(0.0) as i32) as f64)),
1803
+ Void => Ok(Null),
1804
+ }
1805
+ }
1806
+
1807
+ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
1808
+ match obj {
1809
+ Value::Object(m) => {
1810
+ let map = m.borrow();
1811
+ map.strings
1812
+ .get(key.as_ref())
1813
+ .cloned()
1814
+ .ok_or_else(|| format!("Property '{}' not found", key))
1815
+ }
1816
+ Value::Array(a) => {
1817
+ let key_s = key.as_ref();
1818
+ if let Ok(idx) = key_s.parse::<usize>() {
1819
+ let arr = a.borrow();
1820
+ return arr
1821
+ .get(idx)
1822
+ .cloned()
1823
+ .ok_or_else(|| "Index out of bounds".to_string());
1824
+ }
1825
+ if key_s == "length" {
1826
+ return Ok(Value::Number(a.borrow().len() as f64));
1827
+ }
1828
+ let a_clone = a.clone();
1829
+ let method: ArrayMethodFn = match key_s {
1830
+ "push" => make_native_fn(move |args: &[Value]| {
1831
+ arr_builtins::push(&Value::Array(a_clone.clone()), args)
1832
+ }),
1833
+ "pop" => make_native_fn(move |_args: &[Value]| {
1834
+ arr_builtins::pop(&Value::Array(a_clone.clone()))
1835
+ }),
1836
+ "shift" => make_native_fn(move |_args: &[Value]| {
1837
+ arr_builtins::shift(&Value::Array(a_clone.clone()))
1838
+ }),
1839
+ "unshift" => make_native_fn(move |args: &[Value]| {
1840
+ arr_builtins::unshift(&Value::Array(a_clone.clone()), args)
1841
+ }),
1842
+ "reverse" => make_native_fn(move |_args: &[Value]| {
1843
+ arr_builtins::reverse(&Value::Array(a_clone.clone()))
1844
+ }),
1845
+ "shuffle" => make_native_fn(move |_args: &[Value]| {
1846
+ arr_builtins::shuffle(&Value::Array(a_clone.clone()))
1847
+ }),
1848
+ "slice" => make_native_fn(move |args: &[Value]| {
1849
+ let start = args.first().unwrap_or(&Value::Null);
1850
+ let end = args.get(1).unwrap_or(&Value::Null);
1851
+ arr_builtins::slice(&Value::Array(a_clone.clone()), start, end)
1852
+ }),
1853
+ "concat" => make_native_fn(move |args: &[Value]| {
1854
+ arr_builtins::concat(&Value::Array(a_clone.clone()), args)
1855
+ }),
1856
+ "join" => make_native_fn(move |args: &[Value]| {
1857
+ let sep = args.first().unwrap_or(&Value::Null);
1858
+ arr_builtins::join(&Value::Array(a_clone.clone()), sep)
1859
+ }),
1860
+ "indexOf" => make_native_fn(move |args: &[Value]| {
1861
+ let search = args.first().unwrap_or(&Value::Null);
1862
+ arr_builtins::index_of(&Value::Array(a_clone.clone()), search)
1863
+ }),
1864
+ "includes" => make_native_fn(move |args: &[Value]| {
1865
+ let search = args.first().unwrap_or(&Value::Null);
1866
+ let from = args.get(1);
1867
+ arr_builtins::includes(&Value::Array(a_clone.clone()), search, from)
1868
+ }),
1869
+ "map" => make_native_fn(move |args: &[Value]| {
1870
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1871
+ arr_builtins::map(&Value::Array(a_clone.clone()), &cb)
1872
+ }),
1873
+ "filter" => make_native_fn(move |args: &[Value]| {
1874
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1875
+ arr_builtins::filter(&Value::Array(a_clone.clone()), &cb)
1876
+ }),
1877
+ "reduce" => make_native_fn(move |args: &[Value]| {
1878
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1879
+ let init = args.get(1).cloned().unwrap_or(Value::Null);
1880
+ arr_builtins::reduce(&Value::Array(a_clone.clone()), &cb, &init)
1881
+ }),
1882
+ "forEach" => make_native_fn(move |args: &[Value]| {
1883
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1884
+ arr_builtins::for_each(&Value::Array(a_clone.clone()), &cb)
1885
+ }),
1886
+ "find" => make_native_fn(move |args: &[Value]| {
1887
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1888
+ arr_builtins::find(&Value::Array(a_clone.clone()), &cb)
1889
+ }),
1890
+ "findIndex" => make_native_fn(move |args: &[Value]| {
1891
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1892
+ arr_builtins::find_index(&Value::Array(a_clone.clone()), &cb)
1893
+ }),
1894
+ "some" => make_native_fn(move |args: &[Value]| {
1895
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1896
+ arr_builtins::some(&Value::Array(a_clone.clone()), &cb)
1897
+ }),
1898
+ "every" => make_native_fn(move |args: &[Value]| {
1899
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1900
+ arr_builtins::every(&Value::Array(a_clone.clone()), &cb)
1901
+ }),
1902
+ "flat" => make_native_fn(move |args: &[Value]| {
1903
+ let depth = args.first().unwrap_or(&Value::Number(1.0));
1904
+ arr_builtins::flat(&Value::Array(a_clone.clone()), depth)
1905
+ }),
1906
+ "flatMap" => make_native_fn(move |args: &[Value]| {
1907
+ let cb = args.first().cloned().unwrap_or(Value::Null);
1908
+ arr_builtins::flat_map(&Value::Array(a_clone.clone()), &cb)
1909
+ }),
1910
+ "sort" => make_native_fn(move |args: &[Value]| {
1911
+ let cmp = args.first();
1912
+ if let Some(Value::Function(_)) = cmp {
1913
+ arr_builtins::sort_with_comparator(
1914
+ &Value::Array(a_clone.clone()),
1915
+ cmp.unwrap(),
1916
+ )
1917
+ } else {
1918
+ arr_builtins::sort_default(&Value::Array(a_clone.clone()))
1919
+ }
1920
+ }),
1921
+ "splice" => make_native_fn(move |args: &[Value]| {
1922
+ let start = args.first().unwrap_or(&Value::Null);
1923
+ let delete_count = args.get(1).map(|v| v as &Value);
1924
+ let items: Vec<Value> = args.get(2..).unwrap_or(&[]).to_vec();
1925
+ arr_builtins::splice(
1926
+ &Value::Array(a_clone.clone()),
1927
+ start,
1928
+ delete_count,
1929
+ &items,
1930
+ )
1931
+ }),
1932
+ _ => return Err(format!("Property '{}' not found", key)),
1933
+ };
1934
+ Ok(Value::Function(method))
1935
+ }
1936
+ Value::String(s) => {
1937
+ let key_s = key.as_ref();
1938
+ if let Ok(idx) = key_s.parse::<usize>() {
1939
+ return match s.chars().nth(idx) {
1940
+ Some(c) => Ok(Value::String(Arc::from(c.to_string()))),
1941
+ None => Err("Index out of bounds".to_string()),
1942
+ };
1943
+ }
1944
+ if key_s == "length" {
1945
+ return Ok(Value::Number(s.chars().count() as f64));
1946
+ }
1947
+ let s_clone: Arc<str> = Arc::clone(s);
1948
+ let method: ArrayMethodFn = match key_s {
1949
+ "indexOf" => make_native_fn(move |args: &[Value]| {
1950
+ let search = args.first().unwrap_or(&Value::Null);
1951
+ let from = args.get(1);
1952
+ str_builtins::index_of(&Value::String(Arc::clone(&s_clone)), search, from)
1953
+ }),
1954
+ "lastIndexOf" => make_native_fn(move |args: &[Value]| {
1955
+ let search = args.first().unwrap_or(&Value::Null);
1956
+ let position = args.get(1).cloned().unwrap_or(Value::Number(f64::INFINITY));
1957
+ str_builtins::last_index_of(
1958
+ &Value::String(Arc::clone(&s_clone)),
1959
+ search,
1960
+ &position,
1961
+ )
1962
+ }),
1963
+ "includes" => make_native_fn(move |args: &[Value]| {
1964
+ let search = args.first().unwrap_or(&Value::Null);
1965
+ let from = args.get(1);
1966
+ str_builtins::includes(&Value::String(Arc::clone(&s_clone)), search, from)
1967
+ }),
1968
+ "slice" => make_native_fn(move |args: &[Value]| {
1969
+ let start = args.first().unwrap_or(&Value::Null);
1970
+ let end = args.get(1).unwrap_or(&Value::Null);
1971
+ str_builtins::slice(&Value::String(Arc::clone(&s_clone)), start, end)
1972
+ }),
1973
+ "substring" => make_native_fn(move |args: &[Value]| {
1974
+ let start = args.first().unwrap_or(&Value::Null);
1975
+ let end = args.get(1).unwrap_or(&Value::Null);
1976
+ str_builtins::substring(&Value::String(Arc::clone(&s_clone)), start, end)
1977
+ }),
1978
+ "split" => make_native_fn(move |args: &[Value]| {
1979
+ let sep = args.first().unwrap_or(&Value::Null);
1980
+ str_builtins::split(&Value::String(Arc::clone(&s_clone)), sep)
1981
+ }),
1982
+ "trim" => make_native_fn(move |_args: &[Value]| {
1983
+ str_builtins::trim(&Value::String(Arc::clone(&s_clone)))
1984
+ }),
1985
+ "toUpperCase" => make_native_fn(move |_args: &[Value]| {
1986
+ str_builtins::to_upper_case(&Value::String(Arc::clone(&s_clone)))
1987
+ }),
1988
+ "toLowerCase" => make_native_fn(move |_args: &[Value]| {
1989
+ str_builtins::to_lower_case(&Value::String(Arc::clone(&s_clone)))
1990
+ }),
1991
+ "startsWith" => make_native_fn(move |args: &[Value]| {
1992
+ let search = args.first().unwrap_or(&Value::Null);
1993
+ str_builtins::starts_with(&Value::String(Arc::clone(&s_clone)), search)
1994
+ }),
1995
+ "endsWith" => make_native_fn(move |args: &[Value]| {
1996
+ let search = args.first().unwrap_or(&Value::Null);
1997
+ str_builtins::ends_with(&Value::String(Arc::clone(&s_clone)), search)
1998
+ }),
1999
+ "replace" => make_native_fn(move |args: &[Value]| {
2000
+ let search = args.first().unwrap_or(&Value::Null);
2001
+ let replacement = args.get(1).unwrap_or(&Value::Null);
2002
+ str_builtins::replace(&Value::String(Arc::clone(&s_clone)), search, replacement)
2003
+ }),
2004
+ "replaceAll" => make_native_fn(move |args: &[Value]| {
2005
+ let search = args.first().unwrap_or(&Value::Null);
2006
+ let replacement = args.get(1).unwrap_or(&Value::Null);
2007
+ str_builtins::replace_all(
2008
+ &Value::String(Arc::clone(&s_clone)),
2009
+ search,
2010
+ replacement,
2011
+ )
2012
+ }),
2013
+ "charAt" => make_native_fn(move |args: &[Value]| {
2014
+ let idx = args.first().unwrap_or(&Value::Null);
2015
+ str_builtins::char_at(&Value::String(Arc::clone(&s_clone)), idx)
2016
+ }),
2017
+ "charCodeAt" => make_native_fn(move |args: &[Value]| {
2018
+ let idx = args.first().unwrap_or(&Value::Null);
2019
+ str_builtins::char_code_at(&Value::String(Arc::clone(&s_clone)), idx)
2020
+ }),
2021
+ "repeat" => make_native_fn(move |args: &[Value]| {
2022
+ let count = args.first().unwrap_or(&Value::Null);
2023
+ str_builtins::repeat(&Value::String(Arc::clone(&s_clone)), count)
2024
+ }),
2025
+ "padStart" => make_native_fn(move |args: &[Value]| {
2026
+ let target_len = args.first().unwrap_or(&Value::Null);
2027
+ let pad = args.get(1).unwrap_or(&Value::Null);
2028
+ str_builtins::pad_start(&Value::String(Arc::clone(&s_clone)), target_len, pad)
2029
+ }),
2030
+ "padEnd" => make_native_fn(move |args: &[Value]| {
2031
+ let target_len = args.first().unwrap_or(&Value::Null);
2032
+ let pad = args.get(1).unwrap_or(&Value::Null);
2033
+ str_builtins::pad_end(&Value::String(Arc::clone(&s_clone)), target_len, pad)
2034
+ }),
2035
+ _ => return Err(format!("Property '{}' not found", key)),
2036
+ };
2037
+ Ok(Value::Function(method))
2038
+ }
2039
+ #[cfg(any(feature = "http", feature = "promise"))]
2040
+ Value::Promise(p) => match key.as_ref() {
2041
+ "then" => {
2042
+ let pc = Arc::clone(p);
2043
+ Ok(Value::native(move |args| {
2044
+ tishlang_runtime::promise_instance_then(&pc, args)
2045
+ }))
2046
+ }
2047
+ "catch" => {
2048
+ let pc = Arc::clone(p);
2049
+ Ok(Value::native(move |args| {
2050
+ tishlang_runtime::promise_instance_catch(&pc, args)
2051
+ }))
2052
+ }
2053
+ _ => Err(format!("Property '{}' not found", key)),
2054
+ },
2055
+ _ => Err(format!(
2056
+ "Cannot read property '{}' of {}",
2057
+ key,
2058
+ obj.type_name()
2059
+ )),
2060
+ }
2061
+ }
2062
+
2063
+ fn set_member(obj: &Value, key: &Arc<str>, val: Value) -> Result<(), String> {
2064
+ match obj {
2065
+ Value::Object(m) => {
2066
+ m.borrow_mut().strings.insert(Arc::clone(key), val);
2067
+ Ok(())
2068
+ }
2069
+ Value::Array(a) => {
2070
+ let idx: usize = key.as_ref().parse().unwrap_or(0);
2071
+ let mut arr = a.borrow_mut();
2072
+ if idx < arr.len() {
2073
+ arr[idx] = val;
2074
+ } else {
2075
+ arr.resize(idx + 1, Value::Null);
2076
+ arr[idx] = val;
2077
+ }
2078
+ Ok(())
2079
+ }
2080
+ _ => Err(format!("Cannot set property of {}", obj.type_name())),
2081
+ }
2082
+ }
2083
+
2084
+ fn get_index(obj: &Value, idx: &Value) -> Result<Value, String> {
2085
+ match obj {
2086
+ Value::Array(a) => {
2087
+ let i = match idx {
2088
+ Value::Number(n) => *n as usize,
2089
+ _ => {
2090
+ return Err(format!(
2091
+ "Array index must be number, got {}",
2092
+ idx.type_name()
2093
+ ));
2094
+ }
2095
+ };
2096
+ Ok(a
2097
+ .borrow()
2098
+ .get(i)
2099
+ .cloned()
2100
+ .unwrap_or(Value::Null))
2101
+ }
2102
+ Value::String(s) => {
2103
+ let i = match idx {
2104
+ Value::Number(n) => {
2105
+ let n = *n;
2106
+ if n < 0.0 || n.fract() != 0.0 {
2107
+ return Err(format!(
2108
+ "String index must be non-negative integer, got {}",
2109
+ n
2110
+ ));
2111
+ }
2112
+ let i = n as usize;
2113
+ let len = s.chars().count();
2114
+ if i >= len {
2115
+ return Err("Index out of bounds".to_string());
2116
+ }
2117
+ i
2118
+ }
2119
+ _ => {
2120
+ return Err(format!(
2121
+ "String index must be number, got {}",
2122
+ idx.type_name()
2123
+ ));
2124
+ }
2125
+ };
2126
+ match s.chars().nth(i) {
2127
+ Some(c) => Ok(Value::String(Arc::from(c.to_string()))),
2128
+ None => Err("Index out of bounds".to_string()),
2129
+ }
2130
+ }
2131
+ Value::Object(_) => object_get(obj, idx).ok_or_else(|| {
2132
+ format!(
2133
+ "Property '{}' not found",
2134
+ idx.to_display_string()
2135
+ )
2136
+ }),
2137
+ #[cfg(any(feature = "http", feature = "promise"))]
2138
+ Value::Promise(_) => {
2139
+ let key_arc: std::sync::Arc<str> = match idx {
2140
+ Value::String(s) => std::sync::Arc::clone(s),
2141
+ _ => {
2142
+ return Err(format!(
2143
+ "Promise bracket access requires a string key, got {}",
2144
+ idx.type_name()
2145
+ ));
2146
+ }
2147
+ };
2148
+ get_member(obj, &key_arc)
2149
+ },
2150
+ _ => Err(format!(
2151
+ "Cannot read property '{}' of {}",
2152
+ idx.to_display_string(),
2153
+ obj.type_name()
2154
+ )),
2155
+ }
2156
+ }
2157
+
2158
+ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
2159
+ match obj {
2160
+ Value::Array(a) => {
2161
+ let i = match idx {
2162
+ Value::Number(n) => *n as usize,
2163
+ _ => {
2164
+ return Err(format!(
2165
+ "Array index must be number, got {}",
2166
+ idx.type_name()
2167
+ ));
2168
+ }
2169
+ };
2170
+ let mut arr = a.borrow_mut();
2171
+ while arr.len() <= i {
2172
+ arr.push(Value::Null);
2173
+ }
2174
+ arr[i] = val;
2175
+ Ok(())
2176
+ }
2177
+ Value::Object(_) => object_set(obj, idx, val),
2178
+ _ => Err(format!("Cannot set property of {}", obj.type_name())),
2179
+ }
2180
+ }
2181
+
2182
+ /// Run a chunk with every capability linked into this `tishlang_vm` build (tests, embedders).
2183
+ pub fn run(chunk: &Chunk) -> Result<Value, String> {
2184
+ let mut vm = Vm::new();
2185
+ vm.run_with_options(chunk, false)
2186
+ }
2187
+
2188
+ /// Run a chunk with options (e.g. REPL persistence for top-level declarations).
2189
+ pub fn run_with_options(chunk: &Chunk, opts: VmRunOptions) -> Result<Value, String> {
2190
+ let mut vm = Vm::with_capabilities(opts.capabilities);
2191
+ vm.run_with_options(chunk, opts.repl_mode)
2192
+ }