@tishlang/tish-format 1.0.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. package/README.md +0 -138
@@ -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: PropMap,
26
- consts: std::collections::HashSet<Arc<str>>,
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: PropMap::default(),
34
- consts: std::collections::HashSet::new(),
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: PropMap::default(),
42
- consts: std::collections::HashSet::new(),
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(1);
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
- let mut string_obj = PropMap::with_capacity(1);
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
- let mut date = PropMap::with_capacity(1);
202
- date.insert("now".into(), Value::Native(natives::date_now));
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
- Value::object(date),
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
- "Symbol".into(),
211
- crate::value_convert::core_to_eval(tishlang_builtins::symbol::symbol_object()),
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
- "Uint8Array".into(),
255
+ "Map".into(),
216
256
  crate::value_convert::core_to_eval(
217
- tishlang_builtins::construct::uint8_array_constructor_value(),
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
- return Err(EvalError::Error(format!(
401
- "for-of requires iterable (array or string), got {}",
402
- iter_val
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
- self.scope.borrow_mut().set(Arc::clone(name), elem, true);
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) => return Err(e),
500
+ Err(e) => {
501
+ ret = Err(e);
502
+ break;
503
+ }
413
504
  }
414
505
  }
415
- Ok(Value::Null)
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
- let cond_ok = cond
429
- .as_ref()
430
- .map(|c| self.eval_expr(c).map(|v| v.is_truthy()))
431
- .transpose()?
432
- .unwrap_or(true);
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
- match self.eval_statement(body) {
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
- if let Some(u) = update {
441
- self.eval_expr(u)?;
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
- Ok(Value::Null)
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
- let result = match try_result {
566
- Ok(v) => Ok(v),
567
- Err(EvalError::Throw(thrown)) => {
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
- Err(EvalError::Throw(thrown))
730
+ // No catch clause — re-raise the original error after `finally`.
731
+ try_result
581
732
  }
582
733
  }
583
- Err(e) => Err(e),
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
- return Ok(Value::object(exports));
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
- return Ok(Value::object(exports));
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
- return Ok(Value::object(exports));
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
- return Ok(Value::object(exports));
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
- return Ok(Value::object(exports));
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
- return Err(EvalError::Error(format!(
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
- let parts: Vec<String> = arr_borrow.iter().map(|v| v.to_string()).collect();
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.to_string());
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.to_string();
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.to_string();
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
- _ => Err(format!("Cannot add {:?} and {:?}", l, r)),
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
- BinOp::Lt => self.binop_number(l, r, |a, b| Value::Bool(a < b)),
2072
- BinOp::Le => self.binop_number(l, r, |a, b| Value::Bool(a <= b)),
2073
- BinOp::Gt => self.binop_number(l, r, |a, b| Value::Bool(a > b)),
2074
- BinOp::Ge => self.binop_number(l, r, |a, b| Value::Bool(a >= b)),
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
- BinOp::Shl => self.binop_int32(l, r, |a, b| Value::Number((a << b) as f64)),
2081
- BinOp::Shr => self.binop_int32(l, r, |a, b| Value::Number((a >> b) as f64)),
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
- BinOp::Eq | BinOp::Ne => Err("Loose equality not supported".to_string()),
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
- fn to_int32(v: &Value) -> Result<i32, String> {
2176
- match v {
2177
- Value::Number(n) => Ok(*n as i32),
2178
- _ => Err(format!("Bitwise operands must be numbers, got {:?}", v)),
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
- let a = Self::to_int32(l)?;
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
- match (l, r) {
2196
- (Value::Number(a), Value::Number(b)) => Ok(f(*a, *b)),
2197
- _ => Err(format!("Expected numbers, got {:?} and {:?}", l, r)),
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
- let scope = Scope::child(Rc::clone(&self.scope));
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 = self.eval_expr(default_expr)?;
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
- let mut eval = Evaluator {
2581
- scope,
2582
- module_cache: Rc::clone(&self.module_cache),
2583
- current_dir: RefCell::new(self.current_dir.borrow().clone()),
2584
- virtual_builtins: Rc::clone(&self.virtual_builtins),
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 eval.eval_statement(body) {
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
- let mut count = 0usize;
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 += 1;
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) => Ok(map
3009
- .borrow()
3010
- .strings
3011
- .get(key)
3012
- .cloned()
3013
- .unwrap_or(Value::Null)),
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
- let mut entries: Vec<_> = map
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
- k.as_ref().to_string(),
3326
- format!(
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
- entries.sort_by(|a, b| a.0.cmp(&b.0));
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();