@tishlang/tish 1.13.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -21,25 +21,32 @@ use crate::value::{
|
|
|
21
21
|
eval_object_get, eval_object_has, eval_object_set, EvalObjectData, PropMap, Value,
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
struct Scope {
|
|
25
|
-
vars:
|
|
26
|
-
|
|
24
|
+
pub struct Scope {
|
|
25
|
+
// Scope vars: order is never observed (no Object.keys over a scope), so use a fast
|
|
26
|
+
// unordered aHash map — NOT the object-strings PropMap (an insertion-ordered IndexMap),
|
|
27
|
+
// which would pay SipHash + ordered-bucket overhead on every variable lookup.
|
|
28
|
+
vars: AHashMap<Arc<str>, Value>,
|
|
29
|
+
consts: ahash::AHashSet<Arc<str>>,
|
|
27
30
|
parent: Option<Rc<std::cell::RefCell<Scope>>>,
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/// A reference-counted lexical scope. A `Value::Function` captures one of these at creation
|
|
34
|
+
/// (the *defining* scope) so calls resolve free variables lexically — real closures.
|
|
35
|
+
pub type ScopeRef = Rc<std::cell::RefCell<Scope>>;
|
|
36
|
+
|
|
30
37
|
impl Scope {
|
|
31
38
|
fn new() -> Rc<std::cell::RefCell<Self>> {
|
|
32
39
|
Rc::new(std::cell::RefCell::new(Self {
|
|
33
|
-
vars:
|
|
34
|
-
consts:
|
|
40
|
+
vars: AHashMap::default(),
|
|
41
|
+
consts: ahash::AHashSet::default(),
|
|
35
42
|
parent: None,
|
|
36
43
|
}))
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
fn child(parent: Rc<std::cell::RefCell<Scope>>) -> Rc<std::cell::RefCell<Self>> {
|
|
40
47
|
Rc::new(std::cell::RefCell::new(Self {
|
|
41
|
-
vars:
|
|
42
|
-
consts:
|
|
48
|
+
vars: AHashMap::default(),
|
|
49
|
+
consts: ahash::AHashSet::default(),
|
|
43
50
|
parent: Some(parent),
|
|
44
51
|
}))
|
|
45
52
|
}
|
|
@@ -144,6 +151,15 @@ impl Evaluator {
|
|
|
144
151
|
math.insert("exp".into(), Value::Native(natives::math_exp));
|
|
145
152
|
math.insert("sign".into(), Value::Native(natives::math_sign));
|
|
146
153
|
math.insert("trunc".into(), Value::Native(natives::math_trunc));
|
|
154
|
+
math.insert("sinh".into(), Value::Native(natives::math_sinh));
|
|
155
|
+
math.insert("cosh".into(), Value::Native(natives::math_cosh));
|
|
156
|
+
math.insert("tanh".into(), Value::Native(natives::math_tanh));
|
|
157
|
+
math.insert("asinh".into(), Value::Native(natives::math_asinh));
|
|
158
|
+
math.insert("acosh".into(), Value::Native(natives::math_acosh));
|
|
159
|
+
math.insert("atanh".into(), Value::Native(natives::math_atanh));
|
|
160
|
+
math.insert("cbrt".into(), Value::Native(natives::math_cbrt));
|
|
161
|
+
math.insert("log2".into(), Value::Native(natives::math_log2));
|
|
162
|
+
math.insert("log10".into(), Value::Native(natives::math_log10));
|
|
147
163
|
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
|
|
148
164
|
math.insert("E".into(), Value::Number(std::f64::consts::E));
|
|
149
165
|
s.set(
|
|
@@ -179,45 +195,95 @@ impl Evaluator {
|
|
|
179
195
|
true,
|
|
180
196
|
);
|
|
181
197
|
|
|
182
|
-
let mut array_obj = PropMap::with_capacity(
|
|
198
|
+
let mut array_obj = PropMap::with_capacity(3);
|
|
183
199
|
array_obj.insert("isArray".into(), Value::Native(natives::array_is_array));
|
|
200
|
+
// `Array(n)` and `new Array(n)` constructor (issue #72).
|
|
201
|
+
array_obj.insert("__call".into(), Value::Native(natives::array_construct));
|
|
202
|
+
array_obj.insert("__construct".into(), Value::Native(natives::array_construct));
|
|
184
203
|
s.set(
|
|
185
204
|
"Array".into(),
|
|
186
205
|
Value::object(array_obj),
|
|
187
206
|
true,
|
|
188
207
|
);
|
|
189
208
|
|
|
190
|
-
|
|
209
|
+
// Error constructors (issue #60): callable + constructable via __call/__construct.
|
|
210
|
+
for (name, ctor) in [
|
|
211
|
+
("Error", natives::error_construct as fn(&[Value]) -> Result<Value, String>),
|
|
212
|
+
("TypeError", natives::type_error_construct),
|
|
213
|
+
("RangeError", natives::range_error_construct),
|
|
214
|
+
("SyntaxError", natives::syntax_error_construct),
|
|
215
|
+
] {
|
|
216
|
+
let mut err_obj = PropMap::with_capacity(2);
|
|
217
|
+
err_obj.insert("__call".into(), Value::Native(ctor));
|
|
218
|
+
err_obj.insert("__construct".into(), Value::Native(ctor));
|
|
219
|
+
s.set(name.into(), Value::object(err_obj), true);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let mut string_obj = PropMap::with_capacity(2);
|
|
191
223
|
string_obj.insert(
|
|
192
224
|
"fromCharCode".into(),
|
|
193
225
|
Value::Native(natives::string_from_char_code),
|
|
194
226
|
);
|
|
227
|
+
// `String(value)` callable: dispatched via `__call` in `call_func`, like `Symbol`.
|
|
228
|
+
string_obj.insert("__call".into(), Value::Native(natives::string_convert));
|
|
195
229
|
s.set(
|
|
196
230
|
"String".into(),
|
|
197
231
|
Value::object(string_obj),
|
|
198
232
|
true,
|
|
199
233
|
);
|
|
200
234
|
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
// `Number(value)` coercion as a callable global (issue #36).
|
|
236
|
+
let mut number_obj = PropMap::with_capacity(1);
|
|
237
|
+
number_obj.insert("__call".into(), Value::Native(natives::number_convert));
|
|
238
|
+
s.set("Number".into(), Value::object(number_obj), true);
|
|
239
|
+
|
|
203
240
|
s.set(
|
|
204
241
|
"Date".into(),
|
|
205
|
-
|
|
242
|
+
crate::value_convert::core_to_eval(
|
|
243
|
+
tishlang_builtins::date::date_constructor_value(),
|
|
244
|
+
),
|
|
206
245
|
true,
|
|
207
246
|
);
|
|
208
|
-
|
|
209
247
|
s.set(
|
|
210
|
-
"
|
|
211
|
-
crate::value_convert::core_to_eval(
|
|
248
|
+
"Set".into(),
|
|
249
|
+
crate::value_convert::core_to_eval(
|
|
250
|
+
tishlang_builtins::collections::set_constructor_value(),
|
|
251
|
+
),
|
|
212
252
|
true,
|
|
213
253
|
);
|
|
214
254
|
s.set(
|
|
215
|
-
"
|
|
255
|
+
"Map".into(),
|
|
216
256
|
crate::value_convert::core_to_eval(
|
|
217
|
-
tishlang_builtins::
|
|
257
|
+
tishlang_builtins::collections::map_constructor_value(),
|
|
218
258
|
),
|
|
219
259
|
true,
|
|
220
260
|
);
|
|
261
|
+
|
|
262
|
+
s.set(
|
|
263
|
+
"Symbol".into(),
|
|
264
|
+
crate::value_convert::core_to_eval(tishlang_builtins::symbol::symbol_object()),
|
|
265
|
+
true,
|
|
266
|
+
);
|
|
267
|
+
for (name, ctor) in [
|
|
268
|
+
(
|
|
269
|
+
"Float64Array",
|
|
270
|
+
tishlang_builtins::typedarrays::float64_array_constructor_value
|
|
271
|
+
as fn() -> tishlang_core::Value,
|
|
272
|
+
),
|
|
273
|
+
("Float32Array", tishlang_builtins::typedarrays::float32_array_constructor_value),
|
|
274
|
+
("Int8Array", tishlang_builtins::typedarrays::int8_array_constructor_value),
|
|
275
|
+
("Uint8Array", tishlang_builtins::typedarrays::uint8_array_constructor_value),
|
|
276
|
+
(
|
|
277
|
+
"Uint8ClampedArray",
|
|
278
|
+
tishlang_builtins::typedarrays::uint8_clamped_array_constructor_value,
|
|
279
|
+
),
|
|
280
|
+
("Int16Array", tishlang_builtins::typedarrays::int16_array_constructor_value),
|
|
281
|
+
("Uint16Array", tishlang_builtins::typedarrays::uint16_array_constructor_value),
|
|
282
|
+
("Int32Array", tishlang_builtins::typedarrays::int32_array_constructor_value),
|
|
283
|
+
("Uint32Array", tishlang_builtins::typedarrays::uint32_array_constructor_value),
|
|
284
|
+
] {
|
|
285
|
+
s.set(name.into(), crate::value_convert::core_to_eval(ctor()), true);
|
|
286
|
+
}
|
|
221
287
|
s.set(
|
|
222
288
|
"AudioContext".into(),
|
|
223
289
|
crate::value_convert::core_to_eval(
|
|
@@ -325,6 +391,15 @@ impl Evaluator {
|
|
|
325
391
|
self.scope = prev;
|
|
326
392
|
Ok(last)
|
|
327
393
|
}
|
|
394
|
+
// Comma-declarators: a transparent group — evaluate each declarator in
|
|
395
|
+
// the *current* scope (no child scope).
|
|
396
|
+
Statement::Multi { statements, .. } => {
|
|
397
|
+
let mut last = Value::Null;
|
|
398
|
+
for s in statements {
|
|
399
|
+
last = self.eval_statement(s)?;
|
|
400
|
+
}
|
|
401
|
+
Ok(last)
|
|
402
|
+
}
|
|
328
403
|
Statement::VarDecl {
|
|
329
404
|
name,
|
|
330
405
|
mutable,
|
|
@@ -396,23 +471,40 @@ impl Evaluator {
|
|
|
396
471
|
.chars()
|
|
397
472
|
.map(|c| crate::value::Value::String(Arc::from(c.to_string())))
|
|
398
473
|
.collect::<Vec<_>>(),
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
474
|
+
// Iterator protocol: an object with a callable `next()` returning
|
|
475
|
+
// `{ value, done }` — e.g. a Map/Set iterator from `.values()` /
|
|
476
|
+
// `.keys()` / `.entries()`. Drain it ONCE (draining advances the
|
|
477
|
+
// iterator's shared position, so it must not be re-run).
|
|
478
|
+
_ => match self.drain_eval_iterator(&iter_val) {
|
|
479
|
+
Some(elems) => elems,
|
|
480
|
+
None => {
|
|
481
|
+
return Err(EvalError::Error(format!(
|
|
482
|
+
"for-of requires iterable (array, string, or iterator), got {}",
|
|
483
|
+
iter_val
|
|
484
|
+
)));
|
|
485
|
+
}
|
|
486
|
+
},
|
|
405
487
|
};
|
|
488
|
+
// Each element gets a FRESH per-iteration binding (ES6 `for (let v of …)`), so a
|
|
489
|
+
// closure created in the body captures that element, not the last one.
|
|
490
|
+
let outer = Rc::clone(&self.scope);
|
|
491
|
+
let mut ret = Ok(Value::Null);
|
|
406
492
|
for elem in elements {
|
|
407
|
-
|
|
493
|
+
let iter_env = Scope::child(Rc::clone(&outer));
|
|
494
|
+
iter_env.borrow_mut().set(Arc::clone(name), elem, true);
|
|
495
|
+
self.scope = Rc::clone(&iter_env);
|
|
408
496
|
match self.eval_statement(body) {
|
|
409
497
|
Ok(_) => {}
|
|
410
498
|
Err(EvalError::Break) => break,
|
|
411
499
|
Err(EvalError::Continue) => continue,
|
|
412
|
-
Err(e) =>
|
|
500
|
+
Err(e) => {
|
|
501
|
+
ret = Err(e);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
413
504
|
}
|
|
414
505
|
}
|
|
415
|
-
|
|
506
|
+
self.scope = outer;
|
|
507
|
+
ret
|
|
416
508
|
}
|
|
417
509
|
Statement::For {
|
|
418
510
|
init,
|
|
@@ -421,34 +513,76 @@ impl Evaluator {
|
|
|
421
513
|
body,
|
|
422
514
|
..
|
|
423
515
|
} => {
|
|
516
|
+
// `let`/`const` declared in `init` get a FRESH per-iteration binding (ES6), so a
|
|
517
|
+
// closure created in the body captures THAT iteration's value, not the final one.
|
|
518
|
+
// The canonical values live in `loop_env`; each iteration's body runs in a fresh
|
|
519
|
+
// `iter_env` copy, and mutations are copied back for the next test/update.
|
|
520
|
+
let outer = Rc::clone(&self.scope);
|
|
521
|
+
let loop_env = Scope::child(Rc::clone(&outer));
|
|
522
|
+
self.scope = Rc::clone(&loop_env);
|
|
424
523
|
if let Some(i) = init {
|
|
425
|
-
self.eval_statement(i)
|
|
524
|
+
if let Err(e) = self.eval_statement(i) {
|
|
525
|
+
self.scope = outer;
|
|
526
|
+
return Err(e);
|
|
527
|
+
}
|
|
426
528
|
}
|
|
529
|
+
let per_iter: Vec<Arc<str>> = loop_env.borrow().vars.keys().cloned().collect();
|
|
530
|
+
let copy_vars = |from: &ScopeRef, to: &ScopeRef, names: &[Arc<str>]| {
|
|
531
|
+
let src = from.borrow();
|
|
532
|
+
let mut dst = to.borrow_mut();
|
|
533
|
+
for n in names {
|
|
534
|
+
if let Some(v) = src.vars.get(n.as_ref()) {
|
|
535
|
+
dst.set(Arc::clone(n), v.clone(), true);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
let mut ret = Ok(Value::Null);
|
|
427
540
|
loop {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
541
|
+
self.scope = Rc::clone(&loop_env);
|
|
542
|
+
let cond_ok = match cond.as_ref() {
|
|
543
|
+
Some(c) => match self.eval_expr(c) {
|
|
544
|
+
Ok(v) => v.is_truthy(),
|
|
545
|
+
Err(e) => {
|
|
546
|
+
ret = Err(e);
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
None => true,
|
|
551
|
+
};
|
|
433
552
|
if !cond_ok {
|
|
434
553
|
break;
|
|
435
554
|
}
|
|
436
|
-
|
|
555
|
+
let iter_env = if per_iter.is_empty() {
|
|
556
|
+
Rc::clone(&loop_env)
|
|
557
|
+
} else {
|
|
558
|
+
let e = Scope::child(Rc::clone(&outer));
|
|
559
|
+
copy_vars(&loop_env, &e, &per_iter);
|
|
560
|
+
e
|
|
561
|
+
};
|
|
562
|
+
self.scope = Rc::clone(&iter_env);
|
|
563
|
+
let flow = self.eval_statement(body);
|
|
564
|
+
if !per_iter.is_empty() {
|
|
565
|
+
copy_vars(&iter_env, &loop_env, &per_iter);
|
|
566
|
+
}
|
|
567
|
+
match flow {
|
|
437
568
|
Ok(_) => {}
|
|
438
569
|
Err(EvalError::Break) => break,
|
|
439
|
-
Err(EvalError::Continue) => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
continue;
|
|
570
|
+
Err(EvalError::Continue) => {}
|
|
571
|
+
Err(e) => {
|
|
572
|
+
ret = Err(e);
|
|
573
|
+
break;
|
|
444
574
|
}
|
|
445
|
-
Err(e) => return Err(e),
|
|
446
575
|
}
|
|
576
|
+
self.scope = Rc::clone(&loop_env);
|
|
447
577
|
if let Some(u) = update {
|
|
448
|
-
self.eval_expr(u)
|
|
578
|
+
if let Err(e) = self.eval_expr(u) {
|
|
579
|
+
ret = Err(e);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
449
582
|
}
|
|
450
583
|
}
|
|
451
|
-
|
|
584
|
+
self.scope = outer;
|
|
585
|
+
ret
|
|
452
586
|
}
|
|
453
587
|
Statement::Return { value, .. } => {
|
|
454
588
|
let v = value
|
|
@@ -474,6 +608,9 @@ impl Evaluator {
|
|
|
474
608
|
formals,
|
|
475
609
|
rest_param: rest_param_name,
|
|
476
610
|
body,
|
|
611
|
+
// Capture the defining scope. It's the SAME Rc we insert into below, so the
|
|
612
|
+
// function sees itself → recursion works.
|
|
613
|
+
env: Rc::clone(&self.scope),
|
|
477
614
|
};
|
|
478
615
|
self.scope.borrow_mut().set(Arc::clone(name), func, true);
|
|
479
616
|
Ok(Value::Null)
|
|
@@ -562,9 +699,22 @@ impl Evaluator {
|
|
|
562
699
|
} => {
|
|
563
700
|
let try_result = self.eval_statement(body);
|
|
564
701
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
702
|
+
// Both a user `throw` and a runtime error (`null.foo()`, "not a function", …)
|
|
703
|
+
// are catchable (issue #60); a runtime error is boxed as a `{ name, message }`
|
|
704
|
+
// object so `catch (e) { e.message }` works. Break/Continue/Return propagate.
|
|
705
|
+
let caught: Option<Value> = match &try_result {
|
|
706
|
+
Err(EvalError::Throw(v)) => Some(v.clone()),
|
|
707
|
+
Err(EvalError::Error(msg)) => {
|
|
708
|
+
let mut m = crate::value::PropMap::with_capacity(2);
|
|
709
|
+
m.insert("name".into(), Value::String("TypeError".into()));
|
|
710
|
+
m.insert("message".into(), Value::String(msg.as_str().into()));
|
|
711
|
+
Some(Value::object(m))
|
|
712
|
+
}
|
|
713
|
+
_ => None,
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
let result = match caught {
|
|
717
|
+
Some(thrown) => {
|
|
568
718
|
if let Some(catch_stmt) = catch_body {
|
|
569
719
|
if let Some(param) = catch_param {
|
|
570
720
|
let scope = Scope::child(Rc::clone(&self.scope));
|
|
@@ -577,13 +727,20 @@ impl Evaluator {
|
|
|
577
727
|
self.eval_statement(catch_stmt)
|
|
578
728
|
}
|
|
579
729
|
} else {
|
|
580
|
-
|
|
730
|
+
// No catch clause — re-raise the original error after `finally`.
|
|
731
|
+
try_result
|
|
581
732
|
}
|
|
582
733
|
}
|
|
583
|
-
|
|
734
|
+
None => try_result,
|
|
584
735
|
};
|
|
585
736
|
|
|
586
737
|
if let Some(finally_stmt) = finally_body {
|
|
738
|
+
// KNOWN BUG (shared with the VM/compiled backends): a throw/return/
|
|
739
|
+
// break/continue inside `finally` should supersede the try/catch
|
|
740
|
+
// outcome (JS completion semantics) but is swallowed here. Fixing it
|
|
741
|
+
// in the interp alone (`?`) breaks interp==vm parity because the VM has
|
|
742
|
+
// the same bug, and the VM fix is a bytecode-compiler-level
|
|
743
|
+
// finally-completion change. Deferred as a coordinated cross-backend fix.
|
|
587
744
|
let _ = self.eval_statement(finally_stmt);
|
|
588
745
|
}
|
|
589
746
|
|
|
@@ -760,7 +917,7 @@ impl Evaluator {
|
|
|
760
917
|
exports.insert("isDir".into(), Value::Native(natives::is_dir));
|
|
761
918
|
exports.insert("readDir".into(), Value::Native(natives::read_dir));
|
|
762
919
|
exports.insert("mkdir".into(), Value::Native(natives::mkdir));
|
|
763
|
-
|
|
920
|
+
Ok(Value::object(exports))
|
|
764
921
|
}
|
|
765
922
|
#[cfg(not(feature = "fs"))]
|
|
766
923
|
{
|
|
@@ -777,7 +934,7 @@ impl Evaluator {
|
|
|
777
934
|
exports.insert("fetchAll".into(), Value::Native(Self::fetch_all_native));
|
|
778
935
|
exports.insert("serve".into(), Value::Serve);
|
|
779
936
|
exports.insert("Promise".into(), Value::PromiseConstructor);
|
|
780
|
-
|
|
937
|
+
Ok(Value::object(exports))
|
|
781
938
|
}
|
|
782
939
|
#[cfg(not(feature = "http"))]
|
|
783
940
|
{
|
|
@@ -806,7 +963,7 @@ impl Evaluator {
|
|
|
806
963
|
"clearInterval".into(),
|
|
807
964
|
Value::Native(Self::clear_interval_native),
|
|
808
965
|
);
|
|
809
|
-
|
|
966
|
+
Ok(Value::object(exports))
|
|
810
967
|
}
|
|
811
968
|
#[cfg(not(feature = "timers"))]
|
|
812
969
|
{
|
|
@@ -829,7 +986,7 @@ impl Evaluator {
|
|
|
829
986
|
"wsBroadcast".into(),
|
|
830
987
|
Value::Native(Self::ws_broadcast_native),
|
|
831
988
|
);
|
|
832
|
-
|
|
989
|
+
Ok(Value::object(exports))
|
|
833
990
|
}
|
|
834
991
|
#[cfg(not(feature = "ws"))]
|
|
835
992
|
{
|
|
@@ -838,6 +995,32 @@ impl Evaluator {
|
|
|
838
995
|
));
|
|
839
996
|
}
|
|
840
997
|
}
|
|
998
|
+
"tish:tty" => {
|
|
999
|
+
#[cfg(feature = "tty")]
|
|
1000
|
+
{
|
|
1001
|
+
let mut exports: PropMap = PropMap::default();
|
|
1002
|
+
exports.insert("size".into(), Value::Native(natives::tty_size));
|
|
1003
|
+
exports.insert("isTTY".into(), Value::Native(natives::tty_is_tty));
|
|
1004
|
+
exports.insert("setRawMode".into(), Value::Native(natives::tty_set_raw_mode));
|
|
1005
|
+
exports.insert(
|
|
1006
|
+
"enterAltScreen".into(),
|
|
1007
|
+
Value::Native(natives::tty_enter_alt_screen),
|
|
1008
|
+
);
|
|
1009
|
+
exports.insert(
|
|
1010
|
+
"leaveAltScreen".into(),
|
|
1011
|
+
Value::Native(natives::tty_leave_alt_screen),
|
|
1012
|
+
);
|
|
1013
|
+
exports.insert("read".into(), Value::Native(natives::tty_read));
|
|
1014
|
+
exports.insert("readLine".into(), Value::Native(natives::tty_read_line));
|
|
1015
|
+
Ok(Value::object(exports))
|
|
1016
|
+
}
|
|
1017
|
+
#[cfg(not(feature = "tty"))]
|
|
1018
|
+
{
|
|
1019
|
+
return Err(EvalError::Error(
|
|
1020
|
+
"tish:tty requires the tty feature. Rebuild with: cargo build -p tishlang --features tty".into(),
|
|
1021
|
+
));
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
841
1024
|
"tish:process" => {
|
|
842
1025
|
#[cfg(feature = "process")]
|
|
843
1026
|
{
|
|
@@ -868,7 +1051,7 @@ impl Evaluator {
|
|
|
868
1051
|
"process".into(),
|
|
869
1052
|
Value::object(process_obj),
|
|
870
1053
|
);
|
|
871
|
-
|
|
1054
|
+
Ok(Value::object(exports))
|
|
872
1055
|
}
|
|
873
1056
|
#[cfg(not(feature = "process"))]
|
|
874
1057
|
{
|
|
@@ -878,10 +1061,10 @@ impl Evaluator {
|
|
|
878
1061
|
}
|
|
879
1062
|
}
|
|
880
1063
|
_ => {
|
|
881
|
-
|
|
1064
|
+
Err(EvalError::Error(format!(
|
|
882
1065
|
"Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:timers, tish:process, tish:ws (plus any registered by native modules)",
|
|
883
1066
|
spec
|
|
884
|
-
)))
|
|
1067
|
+
)))
|
|
885
1068
|
}
|
|
886
1069
|
}
|
|
887
1070
|
}
|
|
@@ -998,13 +1181,45 @@ impl Evaluator {
|
|
|
998
1181
|
_ => ",".to_string(),
|
|
999
1182
|
};
|
|
1000
1183
|
let arr_borrow = arr.borrow();
|
|
1001
|
-
|
|
1184
|
+
// JS join: null/undefined → "", else JS ToString (nested arrays
|
|
1185
|
+
// recurse to a comma-join, objects → "[object Object]").
|
|
1186
|
+
let parts: Vec<String> = arr_borrow
|
|
1187
|
+
.iter()
|
|
1188
|
+
.map(|v| match v {
|
|
1189
|
+
Value::Null => String::new(),
|
|
1190
|
+
other => other.to_js_string(),
|
|
1191
|
+
})
|
|
1192
|
+
.collect();
|
|
1002
1193
|
return Ok(Value::String(parts.join(&sep).into()));
|
|
1003
1194
|
}
|
|
1004
1195
|
"reverse" => {
|
|
1005
1196
|
arr.borrow_mut().reverse();
|
|
1006
1197
|
return Ok(obj.clone());
|
|
1007
1198
|
}
|
|
1199
|
+
"fill" => {
|
|
1200
|
+
// Array.prototype.fill(value, start?, end?) — in place (issue #76).
|
|
1201
|
+
let value = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1202
|
+
let mut arr_mut = arr.borrow_mut();
|
|
1203
|
+
let len = arr_mut.len() as i64;
|
|
1204
|
+
let norm = |v: Option<&Value>, dflt: usize| -> usize {
|
|
1205
|
+
match v {
|
|
1206
|
+
Some(Value::Number(n)) => {
|
|
1207
|
+
let n = *n as i64;
|
|
1208
|
+
if n < 0 { (len + n).max(0) as usize } else { (n as usize).min(len as usize) }
|
|
1209
|
+
}
|
|
1210
|
+
_ => dflt,
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
let start = norm(arg_vals.get(1), 0);
|
|
1214
|
+
let end = norm(arg_vals.get(2), len as usize);
|
|
1215
|
+
let mut i = start;
|
|
1216
|
+
while i < end && i < arr_mut.len() {
|
|
1217
|
+
arr_mut[i] = value.clone();
|
|
1218
|
+
i += 1;
|
|
1219
|
+
}
|
|
1220
|
+
drop(arr_mut);
|
|
1221
|
+
return Ok(obj.clone());
|
|
1222
|
+
}
|
|
1008
1223
|
"shuffle" => {
|
|
1009
1224
|
let mut v = arr.borrow().clone();
|
|
1010
1225
|
use rand::seq::SliceRandom;
|
|
@@ -1652,6 +1867,24 @@ impl Evaluator {
|
|
|
1652
1867
|
let formatted = format!("{:.*}", digits as usize, n);
|
|
1653
1868
|
return Ok(Value::String(formatted.into()));
|
|
1654
1869
|
}
|
|
1870
|
+
if method_name.as_ref() == "toString" {
|
|
1871
|
+
// Shares the VM/native formatting via the backend-agnostic helper
|
|
1872
|
+
// (issue #59). Radix defaults to 10; 2–36 supported, else RangeError.
|
|
1873
|
+
let radix = arg_vals
|
|
1874
|
+
.first()
|
|
1875
|
+
.and_then(|v| match v {
|
|
1876
|
+
Value::Number(d) => Some(*d as i64),
|
|
1877
|
+
_ => None,
|
|
1878
|
+
})
|
|
1879
|
+
.unwrap_or(10);
|
|
1880
|
+
return match tishlang_builtins::number::number_to_string_radix(*n, radix)
|
|
1881
|
+
{
|
|
1882
|
+
Some(s) => Ok(Value::String(s.into())),
|
|
1883
|
+
None => Err(EvalError::Error(
|
|
1884
|
+
"toString() radix must be between 2 and 36".to_string(),
|
|
1885
|
+
)),
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1655
1888
|
}
|
|
1656
1889
|
|
|
1657
1890
|
// RegExp methods
|
|
@@ -1759,8 +1992,11 @@ impl Evaluator {
|
|
|
1759
1992
|
}
|
|
1760
1993
|
tishlang_ast::ArrayElement::Spread(e) => {
|
|
1761
1994
|
let spread_val = self.eval_expr(e)?;
|
|
1762
|
-
if let Value::Array(arr) = spread_val {
|
|
1995
|
+
if let Value::Array(arr) = &spread_val {
|
|
1763
1996
|
vals.extend(arr.borrow().iter().cloned());
|
|
1997
|
+
} else if let Some(items) = self.drain_eval_iterator(&spread_val) {
|
|
1998
|
+
// Spread a Map/Set iterator (`[...m.values()]`).
|
|
1999
|
+
vals.extend(items);
|
|
1764
2000
|
}
|
|
1765
2001
|
}
|
|
1766
2002
|
}
|
|
@@ -1847,6 +2083,52 @@ impl Evaluator {
|
|
|
1847
2083
|
Value::OpaqueMethod(_, _) => "function".into(),
|
|
1848
2084
|
}))
|
|
1849
2085
|
}
|
|
2086
|
+
// `delete obj.prop` / `delete obj[key]` (issue #40): remove the property and
|
|
2087
|
+
// evaluate to `true`. Objects drop the key; arrays clear a numeric index to a
|
|
2088
|
+
// null hole (length preserved). Deleting a non-reference is a no-op (still `true`).
|
|
2089
|
+
Expr::Delete { target, .. } => {
|
|
2090
|
+
// Resolve the target to (object value, key value); then remove the key.
|
|
2091
|
+
let resolved = match target.as_ref() {
|
|
2092
|
+
Expr::Member { object, prop: MemberProp::Name { name, .. }, .. } => {
|
|
2093
|
+
Some((self.eval_expr(object)?, Value::String(name.as_ref().into())))
|
|
2094
|
+
}
|
|
2095
|
+
Expr::Member { object, prop: MemberProp::Expr(key), .. } => {
|
|
2096
|
+
Some((self.eval_expr(object)?, self.eval_expr(key)?))
|
|
2097
|
+
}
|
|
2098
|
+
Expr::Index { object, index, .. } => {
|
|
2099
|
+
Some((self.eval_expr(object)?, self.eval_expr(index)?))
|
|
2100
|
+
}
|
|
2101
|
+
_ => None,
|
|
2102
|
+
};
|
|
2103
|
+
if let Some((obj, key)) = resolved {
|
|
2104
|
+
match &obj {
|
|
2105
|
+
Value::Object(map) => {
|
|
2106
|
+
let key_s = match &key {
|
|
2107
|
+
Value::String(s) => s.to_string(),
|
|
2108
|
+
Value::Number(n) => n.to_string(),
|
|
2109
|
+
other => other.to_string(),
|
|
2110
|
+
};
|
|
2111
|
+
// shift_remove preserves the insertion order of the remaining keys
|
|
2112
|
+
// (JS delete semantics); plain remove() is deprecated on IndexMap.
|
|
2113
|
+
map.borrow_mut().strings.shift_remove(key_s.as_str());
|
|
2114
|
+
}
|
|
2115
|
+
Value::Array(arr) => {
|
|
2116
|
+
if let Value::Number(n) = &key {
|
|
2117
|
+
let n = *n;
|
|
2118
|
+
if n >= 0.0 && n.fract() == 0.0 {
|
|
2119
|
+
let i = n as usize;
|
|
2120
|
+
let mut a = arr.borrow_mut();
|
|
2121
|
+
if i < a.len() {
|
|
2122
|
+
a[i] = Value::Null;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
_ => {}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
Ok(Value::Bool(true))
|
|
2131
|
+
}
|
|
1850
2132
|
Expr::PostfixInc { name, .. } => {
|
|
1851
2133
|
let v = self.scope.borrow().get(name.as_ref())
|
|
1852
2134
|
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
@@ -1963,6 +2245,19 @@ impl Evaluator {
|
|
|
1963
2245
|
.insert(Arc::clone(prop), val.clone());
|
|
1964
2246
|
Ok(val)
|
|
1965
2247
|
}
|
|
2248
|
+
// `arr.length = k` truncates / grows the array (holes read back as Null),
|
|
2249
|
+
// matching JS and the bytecode VM (issue #62).
|
|
2250
|
+
Value::Array(arr) if prop.as_ref() == "length" => {
|
|
2251
|
+
let n = match &val {
|
|
2252
|
+
Value::Number(n) => *n,
|
|
2253
|
+
_ => f64::NAN,
|
|
2254
|
+
};
|
|
2255
|
+
if n.is_nan() || n < 0.0 || n.fract() != 0.0 || n > 4_294_967_295.0 {
|
|
2256
|
+
return Err(EvalError::Error("Invalid array length".to_string()));
|
|
2257
|
+
}
|
|
2258
|
+
arr.borrow_mut().resize(n as usize, Value::Null);
|
|
2259
|
+
Ok(val)
|
|
2260
|
+
}
|
|
1966
2261
|
_ => Err(EvalError::Error(format!(
|
|
1967
2262
|
"Cannot assign property '{}' on non-object: {:?}",
|
|
1968
2263
|
prop, obj_val
|
|
@@ -2018,6 +2313,7 @@ impl Evaluator {
|
|
|
2018
2313
|
formals,
|
|
2019
2314
|
rest_param: None,
|
|
2020
2315
|
body: Arc::new(body_stmt),
|
|
2316
|
+
env: Rc::clone(&self.scope),
|
|
2021
2317
|
})
|
|
2022
2318
|
}
|
|
2023
2319
|
Expr::TemplateLiteral { quasis, exprs, .. } => {
|
|
@@ -2027,7 +2323,7 @@ impl Evaluator {
|
|
|
2027
2323
|
result.push_str(quasi);
|
|
2028
2324
|
if i < exprs.len() {
|
|
2029
2325
|
let val = self.eval_expr(&exprs[i])?;
|
|
2030
|
-
result.push_str(&val.
|
|
2326
|
+
result.push_str(&val.to_js_string());
|
|
2031
2327
|
}
|
|
2032
2328
|
}
|
|
2033
2329
|
Ok(Value::String(result.into()))
|
|
@@ -2046,20 +2342,26 @@ impl Evaluator {
|
|
|
2046
2342
|
Ok(Value::String(s.into()))
|
|
2047
2343
|
}
|
|
2048
2344
|
(Value::String(a), b) => {
|
|
2049
|
-
let b_str = b.
|
|
2345
|
+
let b_str = b.to_js_string();
|
|
2050
2346
|
let mut s = String::with_capacity(a.len() + b_str.len());
|
|
2051
2347
|
s.push_str(a);
|
|
2052
2348
|
s.push_str(&b_str);
|
|
2053
2349
|
Ok(Value::String(s.into()))
|
|
2054
2350
|
}
|
|
2055
2351
|
(a, Value::String(b)) => {
|
|
2056
|
-
let a_str = a.
|
|
2352
|
+
let a_str = a.to_js_string();
|
|
2057
2353
|
let mut s = String::with_capacity(a_str.len() + b.len());
|
|
2058
2354
|
s.push_str(&a_str);
|
|
2059
2355
|
s.push_str(b);
|
|
2060
2356
|
Ok(Value::String(s.into()))
|
|
2061
2357
|
}
|
|
2062
|
-
|
|
2358
|
+
// Neither operand is a string: numeric add, coercing non-numbers
|
|
2359
|
+
// (Null/Bool/Object/…) to NaN exactly like the VM's
|
|
2360
|
+
// `as_number().unwrap_or(NaN)` (vm.rs eval_binop). e.g. an out-of-bounds
|
|
2361
|
+
// array read is `Null` (JS `undefined`), so `15 + arr[oob]` → NaN, not an error.
|
|
2362
|
+
_ => Ok(Value::Number(
|
|
2363
|
+
l.as_number().unwrap_or(f64::NAN) + r.as_number().unwrap_or(f64::NAN),
|
|
2364
|
+
)),
|
|
2063
2365
|
},
|
|
2064
2366
|
BinOp::Sub => self.binop_number(l, r, |a, b| Value::Number(a - b)),
|
|
2065
2367
|
BinOp::Mul => self.binop_number(l, r, |a, b| Value::Number(a * b)),
|
|
@@ -2068,17 +2370,29 @@ impl Evaluator {
|
|
|
2068
2370
|
BinOp::Pow => self.binop_number(l, r, |a, b| Value::Number(a.powf(b))),
|
|
2069
2371
|
BinOp::StrictEq => Ok(Value::Bool(l.strict_eq(r))),
|
|
2070
2372
|
BinOp::StrictNe => Ok(Value::Bool(!l.strict_eq(r))),
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
BinOp::
|
|
2074
|
-
BinOp::
|
|
2373
|
+
// Relational ops compare strings lexicographically when BOTH operands
|
|
2374
|
+
// are strings (JS semantics); otherwise coerce to numbers via binop_number.
|
|
2375
|
+
BinOp::Lt => self.binop_relational(l, r, |o| o.is_lt()),
|
|
2376
|
+
BinOp::Le => self.binop_relational(l, r, |o| o.is_le()),
|
|
2377
|
+
BinOp::Gt => self.binop_relational(l, r, |o| o.is_gt()),
|
|
2378
|
+
BinOp::Ge => self.binop_relational(l, r, |o| o.is_ge()),
|
|
2075
2379
|
BinOp::And => Ok(Value::Bool(l.is_truthy() && r.is_truthy())),
|
|
2076
2380
|
BinOp::Or => Ok(Value::Bool(l.is_truthy() || r.is_truthy())),
|
|
2077
2381
|
BinOp::BitAnd => self.binop_int32(l, r, |a, b| Value::Number((a & b) as f64)),
|
|
2078
2382
|
BinOp::BitOr => self.binop_int32(l, r, |a, b| Value::Number((a | b) as f64)),
|
|
2079
2383
|
BinOp::BitXor => self.binop_int32(l, r, |a, b| Value::Number((a ^ b) as f64)),
|
|
2080
|
-
|
|
2081
|
-
|
|
2384
|
+
// JS shifts mask the count to the low 5 bits; `wrapping_sh*` does exactly
|
|
2385
|
+
// that and never panics (plain `<<`/`>>` panic in debug for count >= 32).
|
|
2386
|
+
BinOp::Shl => {
|
|
2387
|
+
self.binop_int32(l, r, |a, b| Value::Number(a.wrapping_shl(b as u32) as f64))
|
|
2388
|
+
}
|
|
2389
|
+
BinOp::Shr => {
|
|
2390
|
+
self.binop_int32(l, r, |a, b| Value::Number(a.wrapping_shr(b as u32) as f64))
|
|
2391
|
+
}
|
|
2392
|
+
// `>>>` — unsigned (logical) right shift: ToUint32(a) >>> (b & 31).
|
|
2393
|
+
BinOp::UShr => self.binop_int32(l, r, |a, b| {
|
|
2394
|
+
Value::Number((a as u32).wrapping_shr(b as u32) as f64)
|
|
2395
|
+
}),
|
|
2082
2396
|
BinOp::In => {
|
|
2083
2397
|
let ok = match r {
|
|
2084
2398
|
Value::Object(_) => eval_object_has(r, l),
|
|
@@ -2104,7 +2418,10 @@ impl Evaluator {
|
|
|
2104
2418
|
};
|
|
2105
2419
|
Ok(Value::Bool(ok))
|
|
2106
2420
|
}
|
|
2107
|
-
|
|
2421
|
+
// Loose ==/!= : match the VM (vm.rs maps Eq/Ne to strict_eq) so interp == vm ==
|
|
2422
|
+
// compiled. Previously the interpreter alone errored on `==`.
|
|
2423
|
+
BinOp::Eq => Ok(Value::Bool(l.strict_eq(r))),
|
|
2424
|
+
BinOp::Ne => Ok(Value::Bool(!l.strict_eq(r))),
|
|
2108
2425
|
}
|
|
2109
2426
|
}
|
|
2110
2427
|
|
|
@@ -2172,10 +2489,19 @@ impl Evaluator {
|
|
|
2172
2489
|
false
|
|
2173
2490
|
}
|
|
2174
2491
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2492
|
+
/// JS ToInt32 coercion. Non-numbers coerce to NaN → 0. Going through `i64`
|
|
2493
|
+
/// (not a direct `as i32`) gives modulo-2³² truncation instead of a saturating
|
|
2494
|
+
/// cast, so out-of-i32-range values (e.g. a `0..2³²` hash) wrap exactly like JS:
|
|
2495
|
+
/// `4294967295 | 0 === -1`, not the saturated `i32::MAX`. Realistic values are
|
|
2496
|
+
/// `< 2⁵³` so they fit `i64` exactly; the two casts stay cheap.
|
|
2497
|
+
fn to_int32(v: &Value) -> i32 {
|
|
2498
|
+
// NaN / ±Infinity → 0 (the `is_finite` guard): `f64 as i64` *saturates* (`+∞ → i64::MAX
|
|
2499
|
+
// → -1`), which is not the JS ToInt32 result. Finite values use the cheap modulo cast.
|
|
2500
|
+
let x = v.as_number().unwrap_or(f64::NAN);
|
|
2501
|
+
if x.is_finite() {
|
|
2502
|
+
x as i64 as i32
|
|
2503
|
+
} else {
|
|
2504
|
+
0
|
|
2179
2505
|
}
|
|
2180
2506
|
}
|
|
2181
2507
|
|
|
@@ -2183,19 +2509,39 @@ impl Evaluator {
|
|
|
2183
2509
|
where
|
|
2184
2510
|
F: FnOnce(i32, i32) -> Value,
|
|
2185
2511
|
{
|
|
2186
|
-
|
|
2187
|
-
let b = Self::to_int32(r)?;
|
|
2188
|
-
Ok(f(a, b))
|
|
2512
|
+
Ok(f(Self::to_int32(l), Self::to_int32(r)))
|
|
2189
2513
|
}
|
|
2190
2514
|
|
|
2515
|
+
/// Numeric binop, coercing each operand to a number the way the VM does
|
|
2516
|
+
/// (`as_number().unwrap_or(NaN)`): non-numbers (Null/Bool/Object/…) become NaN rather
|
|
2517
|
+
/// than erroring. Keeps the interpreter in parity with the VM and Node on out-of-bounds
|
|
2518
|
+
/// reads and other `undefined`-like operands.
|
|
2191
2519
|
fn binop_number<F>(&self, l: &Value, r: &Value, f: F) -> Result<Value, String>
|
|
2192
2520
|
where
|
|
2193
2521
|
F: FnOnce(f64, f64) -> Value,
|
|
2194
2522
|
{
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2523
|
+
let a = l.as_number().unwrap_or(f64::NAN);
|
|
2524
|
+
let b = r.as_number().unwrap_or(f64::NAN);
|
|
2525
|
+
Ok(f(a, b))
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
/// Relational comparison (`<` `<=` `>` `>=`). When both operands are strings,
|
|
2529
|
+
/// compare lexicographically; otherwise coerce to numbers. `pred` maps the
|
|
2530
|
+
/// resulting `Ordering` to a bool. A NaN-involved numeric comparison yields no
|
|
2531
|
+
/// ordering and is always `false`, matching JS (`NaN < 5` → false).
|
|
2532
|
+
fn binop_relational<F>(&self, l: &Value, r: &Value, pred: F) -> Result<Value, String>
|
|
2533
|
+
where
|
|
2534
|
+
F: FnOnce(std::cmp::Ordering) -> bool,
|
|
2535
|
+
{
|
|
2536
|
+
let ord = match (l, r) {
|
|
2537
|
+
(Value::String(a), Value::String(b)) => Some(a.as_ref().cmp(b.as_ref())),
|
|
2538
|
+
_ => {
|
|
2539
|
+
let a = l.as_number().unwrap_or(f64::NAN);
|
|
2540
|
+
let b = r.as_number().unwrap_or(f64::NAN);
|
|
2541
|
+
a.partial_cmp(&b)
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
Ok(Value::Bool(ord.map(pred).unwrap_or(false)))
|
|
2199
2545
|
}
|
|
2200
2546
|
|
|
2201
2547
|
fn eval_unary(&self, op: UnaryOp, v: &Value) -> Result<Value, String> {
|
|
@@ -2210,7 +2556,7 @@ impl Evaluator {
|
|
|
2210
2556
|
_ => Err(format!("Cannot apply unary + to {:?}", v)),
|
|
2211
2557
|
},
|
|
2212
2558
|
UnaryOp::BitNot => {
|
|
2213
|
-
let n = Self::to_int32(v)
|
|
2559
|
+
let n = Self::to_int32(v);
|
|
2214
2560
|
Ok(Value::Number((!n) as f64))
|
|
2215
2561
|
}
|
|
2216
2562
|
UnaryOp::Void => Ok(Value::Null),
|
|
@@ -2504,7 +2850,7 @@ impl Evaluator {
|
|
|
2504
2850
|
.map(crate::value_convert::eval_to_core)
|
|
2505
2851
|
.collect();
|
|
2506
2852
|
let ca = ca.map_err(EvalError::Error)?;
|
|
2507
|
-
Ok(crate::value_convert::core_to_eval(f(&ca)))
|
|
2853
|
+
Ok(crate::value_convert::core_to_eval(f.call(&ca)))
|
|
2508
2854
|
}
|
|
2509
2855
|
#[cfg(feature = "regex")]
|
|
2510
2856
|
Value::RegExp(_) => Err(EvalError::Error("RegExp is not callable".to_string())),
|
|
@@ -2527,15 +2873,30 @@ impl Evaluator {
|
|
|
2527
2873
|
.map(crate::value_convert::eval_to_core)
|
|
2528
2874
|
.collect();
|
|
2529
2875
|
let core_args = core_args.map_err(EvalError::Error)?;
|
|
2530
|
-
let result = method(&core_args);
|
|
2876
|
+
let result = method.call(&core_args);
|
|
2531
2877
|
Ok(crate::value_convert::core_to_eval(result))
|
|
2532
2878
|
}
|
|
2533
2879
|
Value::Function {
|
|
2534
2880
|
formals,
|
|
2535
2881
|
rest_param,
|
|
2536
2882
|
body,
|
|
2883
|
+
env,
|
|
2537
2884
|
} => {
|
|
2538
|
-
|
|
2885
|
+
// A real closure: the call frame's parent is the function's DEFINING scope (env),
|
|
2886
|
+
// not the call site — so free variables resolve lexically.
|
|
2887
|
+
let scope = Scope::child(Rc::clone(env));
|
|
2888
|
+
// The call-frame evaluator, built up front so default-parameter expressions
|
|
2889
|
+
// evaluate in this *call* scope — where earlier params are already bound (so a
|
|
2890
|
+
// default like `b = a + 1` can see `a`) and free vars still resolve lexically
|
|
2891
|
+
// through the closure's `env`. Evaluating against `self.scope` (the call *site*)
|
|
2892
|
+
// would see neither, matching the bytecode VM's ArgMissing prologue, which runs
|
|
2893
|
+
// defaults in the frame after the supplied args are bound.
|
|
2894
|
+
let mut eval = Evaluator {
|
|
2895
|
+
scope: Rc::clone(&scope),
|
|
2896
|
+
module_cache: Rc::clone(&self.module_cache),
|
|
2897
|
+
current_dir: RefCell::new(self.current_dir.borrow().clone()),
|
|
2898
|
+
virtual_builtins: Rc::clone(&self.virtual_builtins),
|
|
2899
|
+
};
|
|
2539
2900
|
{
|
|
2540
2901
|
let mut s = scope.borrow_mut();
|
|
2541
2902
|
for (i, formal) in formals.iter().enumerate() {
|
|
@@ -2548,7 +2909,7 @@ impl Evaluator {
|
|
|
2548
2909
|
};
|
|
2549
2910
|
if let Some(default_expr) = def {
|
|
2550
2911
|
drop(s);
|
|
2551
|
-
let default_val =
|
|
2912
|
+
let default_val = eval.eval_expr(default_expr)?;
|
|
2552
2913
|
s = scope.borrow_mut();
|
|
2553
2914
|
default_val
|
|
2554
2915
|
} else {
|
|
@@ -2577,13 +2938,33 @@ impl Evaluator {
|
|
|
2577
2938
|
);
|
|
2578
2939
|
}
|
|
2579
2940
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2941
|
+
// Grow the native stack on demand so deep (non-tail) recursion doesn't overflow
|
|
2942
|
+
// the OS thread stack — same idea as the bytecode VM's `stacker::maybe_grow` around
|
|
2943
|
+
// recursive `run_chunk` (vm.rs:1138). Without it the tree-walker aborts (SIGABRT,
|
|
2944
|
+
// "stack overflow") on deep recursion, which the cross-backend parity run surfaced
|
|
2945
|
+
// on `recursion_stress`. This is the recursion ACCUMULATOR (every user-function call
|
|
2946
|
+
// lands here); the per-element HOF path (`call_with_scope`) is deliberately NOT
|
|
2947
|
+
// guarded — it never nests deeply, so it avoids the per-call check on hot map/filter.
|
|
2948
|
+
//
|
|
2949
|
+
// Red zone = 1 MiB, NOT the VM's 128 KiB: one tree-walker recursion level spans a
|
|
2950
|
+
// long eval chain (eval_statement → eval_expr(if) → eval_expr(binary) → eval_expr(call)
|
|
2951
|
+
// → eval_call_args → call_func → …), each frame large — far more per level than the
|
|
2952
|
+
// VM's single `run_chunk` re-entry. 128 KiB is smaller than one level's chain, so the
|
|
2953
|
+
// stack overflows BETWEEN checks; 1 MiB comfortably covers a level (verified to depth
|
|
2954
|
+
// 20000 in both debug and release). 16 MiB segments keep grow frequency low.
|
|
2955
|
+
let body_result = {
|
|
2956
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
2957
|
+
{
|
|
2958
|
+
stacker::maybe_grow(1024 * 1024, 16 * 1024 * 1024, || {
|
|
2959
|
+
eval.eval_statement(body)
|
|
2960
|
+
})
|
|
2961
|
+
}
|
|
2962
|
+
#[cfg(target_arch = "wasm32")]
|
|
2963
|
+
{
|
|
2964
|
+
eval.eval_statement(body)
|
|
2965
|
+
}
|
|
2585
2966
|
};
|
|
2586
|
-
match
|
|
2967
|
+
match body_result {
|
|
2587
2968
|
Ok(v) => Ok(v),
|
|
2588
2969
|
Err(EvalError::Return(v)) => Ok(v),
|
|
2589
2970
|
Err(EvalError::Throw(v)) => Err(EvalError::Throw(v)),
|
|
@@ -2828,7 +3209,6 @@ impl Evaluator {
|
|
|
2828
3209
|
println!("Server listening on http://0.0.0.0:{}", port);
|
|
2829
3210
|
|
|
2830
3211
|
if max_requests == Some(1) {
|
|
2831
|
-
let port = port;
|
|
2832
3212
|
std::thread::spawn(move || {
|
|
2833
3213
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
2834
3214
|
if let Ok(mut stream) = std::net::TcpStream::connect(format!("127.0.0.1:{}", port))
|
|
@@ -2841,8 +3221,7 @@ impl Evaluator {
|
|
|
2841
3221
|
});
|
|
2842
3222
|
}
|
|
2843
3223
|
|
|
2844
|
-
|
|
2845
|
-
for mut request in server.incoming_requests() {
|
|
3224
|
+
for (count, mut request) in server.incoming_requests().enumerate() {
|
|
2846
3225
|
let req_value = crate::http::request_to_value(&mut request);
|
|
2847
3226
|
|
|
2848
3227
|
let response_value = match self.call_func(&handler, &[req_value]) {
|
|
@@ -2869,8 +3248,7 @@ impl Evaluator {
|
|
|
2869
3248
|
let (status, headers, body) = crate::http::value_to_response(&response_value);
|
|
2870
3249
|
crate::http::send_response(request, status, headers, body);
|
|
2871
3250
|
}
|
|
2872
|
-
count
|
|
2873
|
-
if max_requests.map(|m| count >= m).unwrap_or(false) {
|
|
3251
|
+
if max_requests.map(|m| count + 1 >= m).unwrap_or(false) {
|
|
2874
3252
|
break;
|
|
2875
3253
|
}
|
|
2876
3254
|
}
|
|
@@ -2887,8 +3265,11 @@ impl Evaluator {
|
|
|
2887
3265
|
}
|
|
2888
3266
|
tishlang_ast::CallArg::Spread(e) => {
|
|
2889
3267
|
let spread_val = self.eval_expr(e)?;
|
|
2890
|
-
if let Value::Array(arr) = spread_val {
|
|
3268
|
+
if let Value::Array(arr) = &spread_val {
|
|
2891
3269
|
result.extend(arr.borrow().iter().cloned());
|
|
3270
|
+
} else if let Some(items) = self.drain_eval_iterator(&spread_val) {
|
|
3271
|
+
// Spread a Map/Set iterator into call args (`f(...m.values())`).
|
|
3272
|
+
result.extend(items);
|
|
2892
3273
|
}
|
|
2893
3274
|
}
|
|
2894
3275
|
}
|
|
@@ -3003,14 +3384,55 @@ impl Evaluator {
|
|
|
3003
3384
|
}
|
|
3004
3385
|
}
|
|
3005
3386
|
|
|
3387
|
+
/// Drain a JS iterator object — one whose `next()` is a bridged core fn (`CoreFn`)
|
|
3388
|
+
/// returning `{ value, done }`, e.g. a Map/Set iterator from `.values()` / `.keys()` /
|
|
3389
|
+
/// `.entries()` — into a `Vec` by calling `next()` until `done`. Returns `None` when `v`
|
|
3390
|
+
/// is not such an object. Shared by `for…of` and spread so both treat iterators like JS.
|
|
3391
|
+
fn drain_eval_iterator(&self, v: &Value) -> Option<Vec<Value>> {
|
|
3392
|
+
if !matches!(v, Value::Object(_)) {
|
|
3393
|
+
return None;
|
|
3394
|
+
}
|
|
3395
|
+
// Fast path: tish's Map/Set iterators expose `__drain__`, returning all remaining items as
|
|
3396
|
+
// one array — skips the per-element bridge + `{ value, done }` alloc of the generic loop.
|
|
3397
|
+
if let Ok(Value::CoreFn(drain)) = self.get_prop(v, "__drain__") {
|
|
3398
|
+
if let Value::Array(arr) = crate::value_convert::core_to_eval(drain.call(&[])) {
|
|
3399
|
+
return Some(arr.borrow().clone());
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
let Ok(Value::CoreFn(next)) = self.get_prop(v, "next") else {
|
|
3403
|
+
return None;
|
|
3404
|
+
};
|
|
3405
|
+
let mut out = Vec::new();
|
|
3406
|
+
loop {
|
|
3407
|
+
let res = crate::value_convert::core_to_eval(next.call(&[]));
|
|
3408
|
+
let done = self
|
|
3409
|
+
.get_prop(&res, "done")
|
|
3410
|
+
.map(|x| x.is_truthy())
|
|
3411
|
+
.unwrap_or(true);
|
|
3412
|
+
if done {
|
|
3413
|
+
break;
|
|
3414
|
+
}
|
|
3415
|
+
out.push(self.get_prop(&res, "value").unwrap_or(Value::Null));
|
|
3416
|
+
}
|
|
3417
|
+
Some(out)
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3006
3420
|
fn get_prop(&self, obj: &Value, key: &str) -> Result<Value, String> {
|
|
3007
3421
|
match obj {
|
|
3008
|
-
Value::Object(map) =>
|
|
3009
|
-
|
|
3010
|
-
.
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3422
|
+
Value::Object(map) => {
|
|
3423
|
+
// `Set`/`Map` instances expose a computed `.size` via a hidden `SizeProbe` opaque
|
|
3424
|
+
// (shared, not copied, across the value bridge — so it reflects the live store).
|
|
3425
|
+
if key == "size" {
|
|
3426
|
+
if let Some(Value::Opaque(op)) =
|
|
3427
|
+
map.borrow().strings.get(tishlang_builtins::collections::SIZE_SLOT)
|
|
3428
|
+
{
|
|
3429
|
+
if let Some(n) = tishlang_builtins::collections::size_probe_len(op.as_ref()) {
|
|
3430
|
+
return Ok(Value::Number(n));
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
Ok(map.borrow().strings.get(key).cloned().unwrap_or(Value::Null))
|
|
3435
|
+
}
|
|
3014
3436
|
Value::Array(arr) => {
|
|
3015
3437
|
if key == "length" {
|
|
3016
3438
|
Ok(Value::Number(arr.borrow().len() as f64))
|
|
@@ -3051,6 +3473,9 @@ impl Evaluator {
|
|
|
3051
3473
|
"reject" => Ok(Value::Native(Self::promise_reject)),
|
|
3052
3474
|
"all" => Ok(Value::Native(Self::promise_all)),
|
|
3053
3475
|
"race" => Ok(Value::Native(Self::promise_race)),
|
|
3476
|
+
"any" => Ok(Value::Native(Self::promise_any)),
|
|
3477
|
+
"allSettled" => Ok(Value::Native(Self::promise_all_settled)),
|
|
3478
|
+
"spawn" => Ok(Value::Native(Self::promise_spawn_interp)),
|
|
3054
3479
|
_ => Ok(Value::Null),
|
|
3055
3480
|
},
|
|
3056
3481
|
Value::Opaque(o) => {
|
|
@@ -3089,6 +3514,20 @@ impl Evaluator {
|
|
|
3089
3514
|
};
|
|
3090
3515
|
Ok(arr.borrow().get(idx).cloned().unwrap_or(Value::Null))
|
|
3091
3516
|
}
|
|
3517
|
+
// `str[i]` returns the character at index `i` (issue #17). The VM already does
|
|
3518
|
+
// this; the interpreter previously fell through to `null`, a silent divergence.
|
|
3519
|
+
// Out-of-bounds / negative / non-integer indices yield tish's nullish value.
|
|
3520
|
+
Value::String(s) => {
|
|
3521
|
+
let idx = match index {
|
|
3522
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => *n as usize,
|
|
3523
|
+
_ => return Ok(Value::Null),
|
|
3524
|
+
};
|
|
3525
|
+
Ok(s
|
|
3526
|
+
.chars()
|
|
3527
|
+
.nth(idx)
|
|
3528
|
+
.map(|c| Value::String(c.to_string().into()))
|
|
3529
|
+
.unwrap_or(Value::Null))
|
|
3530
|
+
}
|
|
3092
3531
|
Value::Object(_) => Ok(eval_object_get(obj, index).unwrap_or(Value::Null)),
|
|
3093
3532
|
#[cfg(feature = "http")]
|
|
3094
3533
|
Value::Promise(_) | Value::CorePromise(_) => {
|
|
@@ -3316,30 +3755,21 @@ impl Evaluator {
|
|
|
3316
3755
|
format!("[{}]", inner.join(","))
|
|
3317
3756
|
}
|
|
3318
3757
|
Value::Object(map) => {
|
|
3319
|
-
|
|
3758
|
+
// Insertion order (PropMap is an IndexMap) — matches JS/Node and the
|
|
3759
|
+
// VM/rust backends. No key sort.
|
|
3760
|
+
let entries: Vec<String> = map
|
|
3320
3761
|
.borrow()
|
|
3321
3762
|
.strings
|
|
3322
3763
|
.iter()
|
|
3323
3764
|
.map(|(k, v)| {
|
|
3324
|
-
(
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
k.replace('\\', "\\\\").replace('"', "\\\""),
|
|
3329
|
-
Self::json_stringify_value(v)
|
|
3330
|
-
),
|
|
3765
|
+
format!(
|
|
3766
|
+
"\"{}\":{}",
|
|
3767
|
+
k.replace('\\', "\\\\").replace('"', "\\\""),
|
|
3768
|
+
Self::json_stringify_value(v)
|
|
3331
3769
|
)
|
|
3332
3770
|
})
|
|
3333
3771
|
.collect();
|
|
3334
|
-
|
|
3335
|
-
format!(
|
|
3336
|
-
"{{{}}}",
|
|
3337
|
-
entries
|
|
3338
|
-
.into_iter()
|
|
3339
|
-
.map(|(_, s)| s)
|
|
3340
|
-
.collect::<Vec<_>>()
|
|
3341
|
-
.join(",")
|
|
3342
|
-
)
|
|
3772
|
+
format!("{{{}}}", entries.join(","))
|
|
3343
3773
|
}
|
|
3344
3774
|
Value::Symbol(_) => "null".to_string(),
|
|
3345
3775
|
Value::Function { .. } | Value::Native(_) => "null".to_string(),
|
|
@@ -3597,6 +4027,124 @@ impl Evaluator {
|
|
|
3597
4027
|
Err("Promise.race requires at least one promise".to_string())
|
|
3598
4028
|
}
|
|
3599
4029
|
|
|
4030
|
+
/// Helper: settle a new promise fulfilled with `v` (interp Value).
|
|
4031
|
+
#[cfg(feature = "http")]
|
|
4032
|
+
fn eval_fulfilled(v: Value) -> Result<Value, String> {
|
|
4033
|
+
let (promise, resolve_val, reject_val) = crate::promise::create_promise();
|
|
4034
|
+
let (resolve, _) = crate::promise::extract_resolvers(&resolve_val, &reject_val);
|
|
4035
|
+
crate::promise::settle_promise(&resolve, v, true)?;
|
|
4036
|
+
Ok(promise)
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
/// Helper: settle a new promise rejected with `v` (interp Value).
|
|
4040
|
+
#[cfg(feature = "http")]
|
|
4041
|
+
fn eval_rejected(v: Value) -> Result<Value, String> {
|
|
4042
|
+
let (promise, resolve_val, reject_val) = crate::promise::create_promise();
|
|
4043
|
+
let (_, reject) = crate::promise::extract_resolvers(&resolve_val, &reject_val);
|
|
4044
|
+
crate::promise::settle_promise(&reject, v, false)?;
|
|
4045
|
+
Ok(promise)
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
/// Await one interp promise/core-promise/value → `Result<Value, Value>`.
|
|
4049
|
+
#[cfg(feature = "http")]
|
|
4050
|
+
fn settle_one(v: Value) -> Result<Value, Value> {
|
|
4051
|
+
match v {
|
|
4052
|
+
Value::Promise(ref p) => match crate::promise::block_until_settled(p) {
|
|
4053
|
+
crate::promise::PromiseAwaitResult::Fulfilled(x) => Ok(x),
|
|
4054
|
+
crate::promise::PromiseAwaitResult::Rejected(x) => Err(x),
|
|
4055
|
+
crate::promise::PromiseAwaitResult::Error(e) => {
|
|
4056
|
+
Err(Value::String(e.into()))
|
|
4057
|
+
}
|
|
4058
|
+
},
|
|
4059
|
+
Value::CorePromise(ref p) => match p.block_until_settled() {
|
|
4060
|
+
Ok(x) => Ok(crate::value_convert::core_to_eval(x)),
|
|
4061
|
+
Err(x) => Err(crate::value_convert::core_to_eval(x)),
|
|
4062
|
+
},
|
|
4063
|
+
other => Ok(other),
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
/// `Promise.any(iterable)` — first fulfilled wins; rejects with array of reasons if all reject.
|
|
4068
|
+
#[cfg(feature = "http")]
|
|
4069
|
+
fn promise_any(args: &[Value]) -> Result<Value, String> {
|
|
4070
|
+
let iterable = args
|
|
4071
|
+
.first()
|
|
4072
|
+
.ok_or_else(|| "Promise.any requires an iterable".to_string())?;
|
|
4073
|
+
let values: Vec<Value> = match iterable {
|
|
4074
|
+
Value::Array(arr) => arr.borrow().clone(),
|
|
4075
|
+
_ => return Err("Promise.any requires an array".to_string()),
|
|
4076
|
+
};
|
|
4077
|
+
let n = values.len();
|
|
4078
|
+
if n == 0 {
|
|
4079
|
+
return Self::eval_rejected(Value::Array(Rc::new(RefCell::new(vec![]))));
|
|
4080
|
+
}
|
|
4081
|
+
let mut errors = Vec::with_capacity(n);
|
|
4082
|
+
for v in values {
|
|
4083
|
+
match Self::settle_one(v) {
|
|
4084
|
+
Ok(x) => return Self::eval_fulfilled(x),
|
|
4085
|
+
Err(e) => errors.push(e),
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
Self::eval_rejected(Value::Array(Rc::new(RefCell::new(errors))))
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
/// `Promise.allSettled(iterable)` — always fulfills with array of `{status,value|reason}`.
|
|
4092
|
+
#[cfg(feature = "http")]
|
|
4093
|
+
fn promise_all_settled(args: &[Value]) -> Result<Value, String> {
|
|
4094
|
+
use crate::value::EvalObjectData;
|
|
4095
|
+
let iterable = args
|
|
4096
|
+
.first()
|
|
4097
|
+
.ok_or_else(|| "Promise.allSettled requires an iterable".to_string())?;
|
|
4098
|
+
let values: Vec<Value> = match iterable {
|
|
4099
|
+
Value::Array(arr) => arr.borrow().clone(),
|
|
4100
|
+
_ => return Err("Promise.allSettled requires an array".to_string()),
|
|
4101
|
+
};
|
|
4102
|
+
let mut out = Vec::with_capacity(values.len());
|
|
4103
|
+
for v in values {
|
|
4104
|
+
let r = Self::settle_one(v);
|
|
4105
|
+
let mut data = EvalObjectData::default();
|
|
4106
|
+
match r {
|
|
4107
|
+
Ok(x) => {
|
|
4108
|
+
data.strings.insert(std::sync::Arc::from("status"), Value::String("fulfilled".into()));
|
|
4109
|
+
data.strings.insert(std::sync::Arc::from("value"), x);
|
|
4110
|
+
}
|
|
4111
|
+
Err(e) => {
|
|
4112
|
+
data.strings.insert(std::sync::Arc::from("status"), Value::String("rejected".into()));
|
|
4113
|
+
data.strings.insert(std::sync::Arc::from("reason"), e);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
out.push(Value::Object(Rc::new(RefCell::new(data))));
|
|
4117
|
+
}
|
|
4118
|
+
Self::eval_fulfilled(Value::Array(Rc::new(RefCell::new(out))))
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
/// `Promise.spawn(fn)` — on the interpreter, runs the function synchronously and wraps
|
|
4122
|
+
/// the result in an immediate promise. The interpreter uses `Rc<RefCell<…>>` for closures,
|
|
4123
|
+
/// which is `!Send`, so we cannot move the function to a background thread here. Real
|
|
4124
|
+
/// cross-thread parallelism via spawn is available on the bytecode VM (which uses the
|
|
4125
|
+
/// `send-values` / Arc path for the shipped `full` build). For the interpreter, `any` and
|
|
4126
|
+
/// `race` over spawn-created promises still work correctly — they just don't run concurrently.
|
|
4127
|
+
#[cfg(feature = "http")]
|
|
4128
|
+
fn promise_spawn_interp(args: &[Value]) -> Result<Value, String> {
|
|
4129
|
+
let callable = match args.first() {
|
|
4130
|
+
Some(v @ (Value::Native(_) | Value::Function { .. })) => v.clone(),
|
|
4131
|
+
_ => return Err("Promise.spawn: expected a function argument".to_string()),
|
|
4132
|
+
};
|
|
4133
|
+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
4134
|
+
match &callable {
|
|
4135
|
+
Value::Native(f) => f(&[]).map_err(|e| e.to_string()),
|
|
4136
|
+
// Interpreter closures (Value::Function) can't be called from a static native fn
|
|
4137
|
+
// (no evaluator state / Rc captures). Use the VM backend for concurrent CPU spawn.
|
|
4138
|
+
_ => Err("Promise.spawn: tish closures are not supported on the interpreter backend; use the vm backend (tish run) or pass a native module function".to_string()),
|
|
4139
|
+
}
|
|
4140
|
+
}));
|
|
4141
|
+
match result {
|
|
4142
|
+
Ok(Ok(v)) => Self::eval_fulfilled(v),
|
|
4143
|
+
Ok(Err(e)) => Self::eval_rejected(Value::String(e.into())),
|
|
4144
|
+
Err(_) => Self::eval_rejected(Value::String("Promise.spawn: task panicked".into())),
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
|
|
3600
4148
|
#[cfg(feature = "ws")]
|
|
3601
4149
|
fn ws_web_socket_native(args: &[Value]) -> Result<Value, String> {
|
|
3602
4150
|
let mut cv = Vec::new();
|