@tishlang/tish-format 1.0.13 → 2.0.2

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
@@ -12,16 +12,47 @@ use tishlang_builtins::helpers::make_error_value;
12
12
  pub use tishlang_builtins::symbol::symbol_object;
13
13
  pub use tishlang_core::ObjectMap;
14
14
  pub use tishlang_core::Value;
15
+ pub use tishlang_core::ArcStr;
15
16
  /// Used by native codegen for `f()` / `obj()` dispatch (`Value::Function` or `__call` on objects).
16
17
  pub use tishlang_core::value_call;
18
+ /// JS ToInt32/ToUint32 for the emitted bitwise/shift code (modulo 2³², NaN/±Infinity → 0).
19
+ pub use tishlang_core::{to_int32, to_uint32};
17
20
  // Re-export the shared-mutable wrapper so the Rust code emitted by
18
21
  // `tishlang_compile::codegen` can write `VmRef::new(...)` without needing
19
22
  // a direct dependency on `tishlang_core` from the generated crate.
20
23
  pub use tishlang_core::{VmReadGuard, VmRef, VmWriteGuard};
21
24
 
25
+ /// `for…of` iterable normalization for the native backend: a JS iterator object (one with a
26
+ /// callable `next()` returning `{ value, done }`, e.g. a `Map`/`Set` `.values()` result) is
27
+ /// drained into a `Value::Array`; arrays, strings, and everything else pass through unchanged.
28
+ pub fn normalize_for_of(v: Value) -> Value {
29
+ match tishlang_core::drain_iterator(&v) {
30
+ Some(items) => Value::Array(VmRef::new(items)),
31
+ None => v,
32
+ }
33
+ }
34
+
35
+ pub use tishlang_builtins::construct::array_construct;
36
+ pub use tishlang_builtins::construct::error_constructor_value as tish_error_constructor;
22
37
  pub use tishlang_builtins::construct::{
23
38
  audio_context_constructor_value as tish_audio_context_constructor, construct as tish_construct,
39
+ };
40
+ pub use tishlang_builtins::typedarrays::{
41
+ float32_array_constructor_value as tish_float32_array_constructor,
42
+ float64_array_constructor_value as tish_float64_array_constructor,
43
+ float64_array_packed,
44
+ int16_array_constructor_value as tish_int16_array_constructor,
45
+ int32_array_constructor_value as tish_int32_array_constructor,
46
+ int8_array_constructor_value as tish_int8_array_constructor,
47
+ uint16_array_constructor_value as tish_uint16_array_constructor,
48
+ uint32_array_constructor_value as tish_uint32_array_constructor,
24
49
  uint8_array_constructor_value as tish_uint8_array_constructor,
50
+ uint8_clamped_array_constructor_value as tish_uint8_clamped_array_constructor,
51
+ };
52
+ pub use tishlang_builtins::date::date_constructor_value as tish_date_constructor;
53
+ pub use tishlang_builtins::collections::{
54
+ collection_size, map_constructor_value as tish_map_constructor,
55
+ set_constructor_value as tish_set_constructor,
25
56
  };
26
57
 
27
58
  // Re-export array methods from tishlang_builtins
@@ -30,7 +61,7 @@ pub use tishlang_builtins::array::{
30
61
  find_index as array_find_index, flat as array_flat_impl, flat_map as array_flat_map,
31
62
  for_each as array_for_each, includes as array_includes_impl, index_of as array_index_of_impl,
32
63
  join as array_join_impl, map as array_map, pop as array_pop, push as array_push_impl,
33
- reduce as array_reduce, reverse as array_reverse, shift as array_shift,
64
+ fill as array_fill, reduce as array_reduce, reverse as array_reverse, shift as array_shift,
34
65
  shuffle as array_shuffle, slice as array_slice_impl, some as array_some,
35
66
  sort_default as array_sort_default, sort_numeric_asc as array_sort_numeric_asc,
36
67
  sort_numeric_desc as array_sort_numeric_desc,
@@ -110,6 +141,12 @@ pub fn string_substr(s: &Value, start: &Value, length: &Value) -> Value {
110
141
  string_substr_impl(s, start, length)
111
142
  }
112
143
  pub fn string_split(s: &Value, sep: &Value) -> Value {
144
+ // A RegExp separator routes to the regex splitter (matches string_replace's regex handling
145
+ // and the interpreter/VM), so `"a1b2c".split(RegExp("\\d",""))` works on the rust backend.
146
+ #[cfg(feature = "regex")]
147
+ if matches!(sep, Value::RegExp(_)) {
148
+ return string_split_regex(s, sep, None);
149
+ }
113
150
  string_split_impl(s, sep)
114
151
  }
115
152
  pub fn string_starts_with(s: &Value, search: &Value) -> Value {
@@ -148,16 +185,23 @@ pub fn string_last_index_of(s: &Value, search: &Value, position: &Value) -> Valu
148
185
  }
149
186
 
150
187
  /// Number.prototype.toFixed(digits) - format number with fixed decimal places (0-20)
188
+ ///
189
+ /// Delegates to the single source of truth in `tishlang_builtins::number` so the rust
190
+ /// backend, the bytecode VM, and the interpreter stay byte-identical. See
191
+ /// `tish/docs/full-backend-parity-plan.md` (Workstream A).
151
192
  pub fn number_to_fixed(n: &Value, digits: &Value) -> Value {
152
- let num = match n {
153
- Value::Number(x) => *x,
154
- _ => f64::NAN,
155
- };
156
- let d = match digits {
157
- Value::Number(x) => (*x as i32).clamp(0, 20),
158
- _ => 0,
159
- };
160
- Value::String(format!("{:.*}", d as usize, num).into())
193
+ tishlang_builtins::number::to_fixed(n, digits)
194
+ }
195
+
196
+ /// `.toString([radix])` for the compiled backend (issue #59). A number receiver uses the
197
+ /// shared radix formatter so it stays byte-identical with the VM / interpreter; any other
198
+ /// receiver falls back to its normal JS string, so `[1,2].toString()` / `obj.toString()`
199
+ /// keep working.
200
+ pub fn number_to_string(n: &Value, radix: &Value) -> Value {
201
+ match n {
202
+ Value::Number(_) => tishlang_builtins::number::to_string(n, radix),
203
+ other => Value::String(other.to_js_string().into()),
204
+ }
161
205
  }
162
206
 
163
207
  /// Operators module for compound assignment operations
@@ -175,20 +219,26 @@ pub mod ops {
175
219
  Ok(Value::String(s.into()))
176
220
  }
177
221
  (Value::String(a), b) => {
178
- let b_str = b.to_display_string();
222
+ let b_str = b.to_js_string();
179
223
  let mut s = String::with_capacity(a.len() + b_str.len());
180
224
  s.push_str(a);
181
225
  s.push_str(&b_str);
182
226
  Ok(Value::String(s.into()))
183
227
  }
184
228
  (a, Value::String(b)) => {
185
- let a_str = a.to_display_string();
229
+ let a_str = a.to_js_string();
186
230
  let mut s = String::with_capacity(a_str.len() + b.len());
187
231
  s.push_str(&a_str);
188
232
  s.push_str(b);
189
233
  Ok(Value::String(s.into()))
190
234
  }
191
- _ => Err(format!("Cannot add {:?} and {:?}", left, right).into()),
235
+ // Neither operand is a string here ⇒ numeric coercion, matching the VM's `eval_binop`
236
+ // (`as_number().unwrap_or(NaN)`): a null/bool/object operand (e.g. an out-of-bounds array
237
+ // read) coerces to NaN, so `number + null` is NaN — NOT an error that the codegen's
238
+ // `.unwrap_or(Value::Null)` would silently turn into `null` (the old rust-AOT divergence).
239
+ (a, b) => Ok(Value::Number(
240
+ a.as_number().unwrap_or(f64::NAN) + b.as_number().unwrap_or(f64::NAN),
241
+ )),
192
242
  }
193
243
  }
194
244
 
@@ -196,7 +246,10 @@ pub mod ops {
196
246
  pub fn sub(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
197
247
  match (left, right) {
198
248
  (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a - b)),
199
- _ => Err(format!("Cannot subtract {:?} from {:?}", right, left).into()),
249
+ // VM-parity numeric coercion (null/non-number NaN), see `add`.
250
+ (a, b) => Ok(Value::Number(
251
+ a.as_number().unwrap_or(f64::NAN) - b.as_number().unwrap_or(f64::NAN),
252
+ )),
200
253
  }
201
254
  }
202
255
 
@@ -204,7 +257,10 @@ pub mod ops {
204
257
  pub fn mul(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
205
258
  match (left, right) {
206
259
  (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a * b)),
207
- _ => Err(format!("Cannot multiply {:?} and {:?}", left, right).into()),
260
+ // VM-parity numeric coercion (null/non-number NaN), see `add`.
261
+ (a, b) => Ok(Value::Number(
262
+ a.as_number().unwrap_or(f64::NAN) * b.as_number().unwrap_or(f64::NAN),
263
+ )),
208
264
  }
209
265
  }
210
266
 
@@ -212,7 +268,10 @@ pub mod ops {
212
268
  pub fn div(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
213
269
  match (left, right) {
214
270
  (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a / b)),
215
- _ => Err(format!("Cannot divide {:?} by {:?}", left, right).into()),
271
+ // VM-parity numeric coercion (null/non-number NaN), see `add`.
272
+ (a, b) => Ok(Value::Number(
273
+ a.as_number().unwrap_or(f64::NAN) / b.as_number().unwrap_or(f64::NAN),
274
+ )),
216
275
  }
217
276
  }
218
277
 
@@ -221,7 +280,7 @@ pub mod ops {
221
280
  pub fn lt(left: &Value, right: &Value) -> Value {
222
281
  let b = match (left, right) {
223
282
  (Value::Number(a), Value::Number(b)) => a < b,
224
- (Value::String(a), Value::String(b)) => a.as_ref() < b.as_ref(),
283
+ (Value::String(a), Value::String(b)) => a.as_str() < b.as_str(),
225
284
  _ => false,
226
285
  };
227
286
  Value::Bool(b)
@@ -231,7 +290,7 @@ pub mod ops {
231
290
  pub fn le(left: &Value, right: &Value) -> Value {
232
291
  let b = match (left, right) {
233
292
  (Value::Number(a), Value::Number(b)) => a <= b,
234
- (Value::String(a), Value::String(b)) => a.as_ref() <= b.as_ref(),
293
+ (Value::String(a), Value::String(b)) => a.as_str() <= b.as_str(),
235
294
  _ => false,
236
295
  };
237
296
  Value::Bool(b)
@@ -241,7 +300,7 @@ pub mod ops {
241
300
  pub fn gt(left: &Value, right: &Value) -> Value {
242
301
  let b = match (left, right) {
243
302
  (Value::Number(a), Value::Number(b)) => a > b,
244
- (Value::String(a), Value::String(b)) => a.as_ref() > b.as_ref(),
303
+ (Value::String(a), Value::String(b)) => a.as_str() > b.as_str(),
245
304
  _ => false,
246
305
  };
247
306
  Value::Bool(b)
@@ -251,7 +310,7 @@ pub mod ops {
251
310
  pub fn ge(left: &Value, right: &Value) -> Value {
252
311
  let b = match (left, right) {
253
312
  (Value::Number(a), Value::Number(b)) => a >= b,
254
- (Value::String(a), Value::String(b)) => a.as_ref() >= b.as_ref(),
313
+ (Value::String(a), Value::String(b)) => a.as_str() >= b.as_str(),
255
314
  _ => false,
256
315
  };
257
316
  Value::Bool(b)
@@ -261,7 +320,10 @@ pub mod ops {
261
320
  pub fn modulo(left: &Value, right: &Value) -> Result<Value, Box<dyn std::error::Error>> {
262
321
  match (left, right) {
263
322
  (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a % b)),
264
- _ => Err(format!("Cannot modulo {:?} by {:?}", left, right).into()),
323
+ // VM-parity numeric coercion (null/non-number NaN), see `add`.
324
+ (a, b) => Ok(Value::Number(
325
+ a.as_number().unwrap_or(f64::NAN) % b.as_number().unwrap_or(f64::NAN),
326
+ )),
265
327
  }
266
328
  }
267
329
  }
@@ -273,6 +335,8 @@ use tishlang_builtins::globals::{
273
335
  object_assign as builtins_object_assign, object_entries as builtins_object_entries,
274
336
  object_from_entries as builtins_object_from_entries, object_keys as builtins_object_keys,
275
337
  object_values as builtins_object_values,
338
+ number_convert as builtins_number_convert,
339
+ string_convert as builtins_string_convert,
276
340
  string_from_char_code as builtins_string_from_char_code,
277
341
  };
278
342
  use tishlang_core::{json_parse as core_json_parse, json_stringify as core_json_stringify};
@@ -321,22 +385,41 @@ pub mod json {
321
385
  }
322
386
  }
323
387
 
324
- /// Error type for Tish throw/catch.
388
+ /// Error type for Tish throw/catch + non-local control flow (used to model `return`/`throw`
389
+ /// escaping `try`/`finally` in the Rust backend, which has no native exceptions).
325
390
  #[derive(Debug, Clone)]
326
391
  pub enum TishError {
392
+ /// A JS `throw` — catchable by `catch`.
327
393
  Throw(Value),
394
+ /// A JS `return value` that must escape an enclosing `try`/`finally` and unwind to the
395
+ /// function boundary (running each `finally` on the way out). Never caught by `catch`.
396
+ Return(Value),
328
397
  }
329
398
 
330
399
  impl fmt::Display for TishError {
331
400
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332
401
  match self {
333
402
  TishError::Throw(v) => write!(f, "{}", v.to_display_string()),
403
+ TishError::Return(v) => write!(f, "return {}", v.to_display_string()),
334
404
  }
335
405
  }
336
406
  }
337
407
 
338
408
  impl std::error::Error for TishError {}
339
409
 
410
+ /// Function-boundary unwind: convert a completion that escaped a function body's `Result`-closure
411
+ /// back into the function's `Value`. A `return v` yields `v`; an uncaught `throw` panics (matching
412
+ /// the behavior of a throw with no enclosing `try`); any other error panics.
413
+ pub fn fn_unwind(e: Box<dyn std::error::Error>) -> Value {
414
+ match e.downcast::<TishError>() {
415
+ Ok(te) => match *te {
416
+ TishError::Return(v) => v,
417
+ TishError::Throw(v) => panic!("uncaught throw: {}", v.to_display_string()),
418
+ },
419
+ Err(orig) => panic!("error in native Tish: {:?}", orig),
420
+ }
421
+ }
422
+
340
423
  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
341
424
  enum LogLevel {
342
425
  Debug = 0,
@@ -483,6 +566,26 @@ pub fn math_log(args: &[Value]) -> Value {
483
566
  Value::Number(n.ln())
484
567
  }
485
568
 
569
+ // Hyperbolic / inverse-hyperbolic / cbrt / base-2/10 logs (issue #61). Compiled backends
570
+ // (native/cranelift/wasi) share this runtime, so wiring them here resolves all of them.
571
+ macro_rules! runtime_math_unary {
572
+ ($name:ident, $method:ident) => {
573
+ pub fn $name(args: &[Value]) -> Value {
574
+ let n = extract_num(args.first()).unwrap_or(f64::NAN);
575
+ Value::Number(n.$method())
576
+ }
577
+ };
578
+ }
579
+ runtime_math_unary!(math_sinh, sinh);
580
+ runtime_math_unary!(math_cosh, cosh);
581
+ runtime_math_unary!(math_tanh, tanh);
582
+ runtime_math_unary!(math_asinh, asinh);
583
+ runtime_math_unary!(math_acosh, acosh);
584
+ runtime_math_unary!(math_atanh, atanh);
585
+ runtime_math_unary!(math_cbrt, cbrt);
586
+ runtime_math_unary!(math_log2, log2);
587
+ runtime_math_unary!(math_log10, log10);
588
+
486
589
  pub fn json_stringify(args: &[Value]) -> Value {
487
590
  let v = args.first().cloned().unwrap_or(Value::Null);
488
591
  Value::String(core_json_stringify(&v).into())
@@ -496,14 +599,6 @@ pub fn json_parse(args: &[Value]) -> Value {
496
599
  core_json_parse(&s).unwrap_or(Value::Null)
497
600
  }
498
601
 
499
- pub fn date_now(_args: &[Value]) -> Value {
500
- use std::time::{SystemTime, UNIX_EPOCH};
501
- let now = SystemTime::now()
502
- .duration_since(UNIX_EPOCH)
503
- .map(|d| d.as_millis() as f64)
504
- .unwrap_or(0.0);
505
- Value::Number(now)
506
- }
507
602
 
508
603
  pub fn array_is_array(args: &[Value]) -> Value {
509
604
  builtins_array_is_array(args)
@@ -513,6 +608,16 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
513
608
  builtins_string_from_char_code(args)
514
609
  }
515
610
 
611
+ /// `String(value)` as a function (JS `ToString`). Wired into the codegen `String`
612
+ /// global as `__call` so compiled `String(x)` matches the VM/interp.
613
+ pub fn string_convert(args: &[Value]) -> Value {
614
+ builtins_string_convert(args)
615
+ }
616
+
617
+ pub fn number_convert(args: &[Value]) -> Value {
618
+ builtins_number_convert(args)
619
+ }
620
+
516
621
  #[cfg(feature = "process")]
517
622
  pub fn process_exit(args: &[Value]) -> Value {
518
623
  let code = args
@@ -632,6 +737,13 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
632
737
  let key = key.as_ref();
633
738
  match obj {
634
739
  Value::Object(map) => {
740
+ // `Set`/`Map` instances expose a computed `.size` (the backing store has no real
741
+ // `size` key); `collection_size` returns `None` for any other object.
742
+ if key == "size" {
743
+ if let Some(n) = collection_size(obj) {
744
+ return Value::Number(n);
745
+ }
746
+ }
635
747
  // The map's key type is `Arc<str>`, which implements
636
748
  // `Borrow<str>` — so we can look up with a borrowed `&str`
637
749
  // directly. Previously we allocated a fresh `Arc<str>` on
@@ -648,6 +760,17 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
648
760
  Value::Null
649
761
  }
650
762
  }
763
+ // Packed `Float64Array` (`TISH_PACKED_ARRAYS`): `.length` and numeric-key reads, mirroring
764
+ // the boxed `Array` arm. Methods (`reduce`/`map`/…) materialise via `as_boxed_array`.
765
+ Value::NumberArray(arr) => {
766
+ if key == "length" {
767
+ Value::Number(arr.borrow().len() as f64)
768
+ } else if let Ok(idx) = key.parse::<usize>() {
769
+ arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
770
+ } else {
771
+ Value::Null
772
+ }
773
+ }
651
774
  Value::String(s) => {
652
775
  if key == "length" {
653
776
  Value::Number(s.chars().count() as f64)
@@ -656,26 +779,53 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
656
779
  }
657
780
  }
658
781
  #[cfg(feature = "regex")]
659
- Value::RegExp(re) => {
660
- let re = re.clone();
661
- if key == "exec" {
782
+ Value::RegExp(re) => match key {
783
+ "exec" => {
784
+ let rc = re.clone();
662
785
  Value::native(move |args: &[Value]| {
663
786
  let input = args.first().unwrap_or(&Value::Null);
664
- regexp_exec(&Value::RegExp(re.clone()), input)
787
+ regexp_exec(&Value::RegExp(rc.clone()), input)
665
788
  })
666
- } else if key == "test" {
789
+ }
790
+ "test" => {
791
+ let rc = re.clone();
667
792
  Value::native(move |args: &[Value]| {
668
793
  let input = args.first().unwrap_or(&Value::Null);
669
- regexp_test(&Value::RegExp(re.clone()), input)
794
+ regexp_test(&Value::RegExp(rc.clone()), input)
670
795
  })
671
- } else {
672
- Value::Null
673
796
  }
674
- }
797
+ // Properties — mirror the interpreter + bytecode VM so all backends agree.
798
+ "source" => Value::String(re.borrow().source.clone().into()),
799
+ "flags" => Value::String(re.borrow().flags_string().into()),
800
+ "lastIndex" => Value::Number(re.borrow().last_index as f64),
801
+ "global" => Value::Bool(re.borrow().flags.global),
802
+ "ignoreCase" => Value::Bool(re.borrow().flags.ignore_case),
803
+ "multiline" => Value::Bool(re.borrow().flags.multiline),
804
+ "dotAll" => Value::Bool(re.borrow().flags.dot_all),
805
+ "unicode" => Value::Bool(re.borrow().flags.unicode),
806
+ "sticky" => Value::Bool(re.borrow().flags.sticky),
807
+ _ => Value::Null,
808
+ },
675
809
  Value::Opaque(o) => o
676
810
  .get_method(key)
677
811
  .map(Value::Function)
678
812
  .unwrap_or(Value::Null),
813
+ // Promise instance methods (`.then`/`.catch`), bound to this promise. Returning a
814
+ // callable here makes the rust backend match the VM family (interp/vm/cranelift/wasi),
815
+ // which expose these via `GetMember`. Both `p.then(cb)` (member) and `p["catch"](cb)`
816
+ // (index, used because `catch` is reserved) route through here / `get_index`.
817
+ #[cfg(any(feature = "http", feature = "promise"))]
818
+ Value::Promise(p) => match key {
819
+ "then" => {
820
+ let pc = p.clone();
821
+ Value::native(move |args: &[Value]| promise_instance_then(&pc, args))
822
+ }
823
+ "catch" => {
824
+ let pc = p.clone();
825
+ Value::native(move |args: &[Value]| promise_instance_catch(&pc, args))
826
+ }
827
+ _ => Value::Null,
828
+ },
679
829
  _ => Value::Null,
680
830
  }
681
831
  }
@@ -690,11 +840,65 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
690
840
  };
691
841
  arr.borrow().get(idx).cloned().unwrap_or(Value::Null)
692
842
  }
843
+ // Packed `Float64Array` indexing (`TISH_PACKED_ARRAYS`); mirrors the boxed `Array` arm.
844
+ Value::NumberArray(arr) => {
845
+ let idx = match index {
846
+ Value::Number(n) => *n as usize,
847
+ _ => return Value::Null,
848
+ };
849
+ arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
850
+ }
851
+ // `str[i]` returns the character at index `i` (issue #17) — matches the VM /
852
+ // interpreter; out-of-bounds / negative / non-integer indices yield null.
853
+ Value::String(s) => match index {
854
+ Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => s
855
+ .chars()
856
+ .nth(*n as usize)
857
+ .map(|c| Value::String(c.to_string().into()))
858
+ .unwrap_or(Value::Null),
859
+ _ => Value::Null,
860
+ },
693
861
  Value::Object(_) => tishlang_core::object_get(obj, index).unwrap_or(Value::Null),
862
+ // `promise["then"|"catch"]` — string-keyed access mirrors `get_prop` (bracket form
863
+ // is required for `catch`, a reserved word). Keeps the rust backend on par with the VM.
864
+ #[cfg(any(feature = "http", feature = "promise"))]
865
+ Value::Promise(_) => match index {
866
+ Value::String(k) => get_prop(obj, k.as_str()),
867
+ _ => Value::Null,
868
+ },
694
869
  _ => Value::Null,
695
870
  }
696
871
  }
697
872
 
873
+ /// `delete obj[key]` / `delete obj.prop` (issue #40). Objects drop the string key; arrays
874
+ /// clear a numeric index to a `null` hole (length preserved). Always evaluates to `true`.
875
+ #[inline]
876
+ pub fn delete_property(obj: &Value, key: &Value) -> Value {
877
+ match obj {
878
+ Value::Object(m) => {
879
+ let key_s = match key {
880
+ Value::String(s) => s.to_string(),
881
+ other => other.to_js_string(),
882
+ };
883
+ m.borrow_mut().strings.remove(key_s.as_str());
884
+ }
885
+ Value::Array(arr) => {
886
+ if let Value::Number(n) = key {
887
+ let n = *n;
888
+ if n >= 0.0 && n.fract() == 0.0 {
889
+ let i = n as usize;
890
+ let mut a = arr.borrow_mut();
891
+ if i < a.len() {
892
+ a[i] = Value::Null;
893
+ }
894
+ }
895
+ }
896
+ }
897
+ _ => {}
898
+ }
899
+ Value::Bool(true)
900
+ }
901
+
698
902
  #[inline]
699
903
  pub fn set_prop(obj: &Value, key: &str, val: Value) -> Value {
700
904
  match obj {
@@ -710,6 +914,16 @@ pub fn set_prop(obj: &Value, key: &str, val: Value) -> Value {
710
914
  }
711
915
  val
712
916
  }
917
+ // `arr.length = k` truncates / grows the array (holes read back as Null), matching
918
+ // JS and the VM/interpreter (issue #62).
919
+ Value::Array(arr) if key == "length" => {
920
+ let n = extract_num(Some(&val)).unwrap_or(f64::NAN);
921
+ if n.is_nan() || n < 0.0 || n.fract() != 0.0 || n > 4_294_967_295.0 {
922
+ panic!("Invalid array length");
923
+ }
924
+ arr.borrow_mut().resize(n as usize, Value::Null);
925
+ val
926
+ }
713
927
  _ => panic!("Cannot assign property on non-object"),
714
928
  }
715
929
  }
@@ -729,6 +943,22 @@ pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
729
943
  arr_mut[index] = val.clone();
730
944
  val
731
945
  }
946
+ // Packed `Float64Array` write (`TISH_PACKED_ARRAYS`). On the native path a `NumberArray` is
947
+ // always a `Float64Array`, so storing the f64 (non-numeric → `NaN`) is the correct view
948
+ // semantics — and unlike the boxed v1 backing, it actually coerces the write to the element
949
+ // type. Out-of-range index zero-fills, matching the boxed grow-with-Null behaviour.
950
+ Value::NumberArray(arr) => {
951
+ let index = match idx {
952
+ Value::Number(n) => *n as usize,
953
+ _ => panic!("Array index must be number"),
954
+ };
955
+ let mut arr_mut = arr.borrow_mut();
956
+ while arr_mut.len() <= index {
957
+ arr_mut.push(0.0);
958
+ }
959
+ arr_mut[index] = val.as_number().unwrap_or(f64::NAN);
960
+ val
961
+ }
732
962
  Value::Object(_) => {
733
963
  tishlang_core::object_set(obj, idx, val.clone()).expect("object set");
734
964
  val
@@ -742,7 +972,22 @@ pub fn in_operator(key: &Value, obj: &Value) -> Value {
742
972
  Value::Object(_) => Value::Bool(tishlang_core::object_has(obj, key)),
743
973
  Value::Array(arr) => {
744
974
  let key_str: Arc<str> = match key {
745
- Value::String(s) => Arc::clone(s),
975
+ Value::String(s) => Arc::from(s.as_str()),
976
+ Value::Number(n) => n.to_string().into(),
977
+ _ => return Value::Bool(false),
978
+ };
979
+ let result = key_str.as_ref() == "length"
980
+ || key_str
981
+ .parse::<usize>()
982
+ .ok()
983
+ .map(|i| i < arr.borrow().len())
984
+ .unwrap_or(false);
985
+ Value::Bool(result)
986
+ }
987
+ // Packed `Float64Array` (`TISH_PACKED_ARRAYS`); same key set as the boxed `Array` arm.
988
+ Value::NumberArray(arr) => {
989
+ let key_str: Arc<str> = match key {
990
+ Value::String(s) => Arc::from(s.as_str()),
746
991
  Value::Number(n) => n.to_string().into(),
747
992
  _ => return Value::Bool(false),
748
993
  };
@@ -809,6 +1054,13 @@ mod native_promise;
809
1054
  #[cfg(feature = "ws")]
810
1055
  mod ws;
811
1056
 
1057
+ #[cfg(feature = "tty")]
1058
+ pub mod tty;
1059
+ #[cfg(feature = "tty")]
1060
+ pub use tty::{
1061
+ tty_enter_alt_screen, tty_is_tty, tty_leave_alt_screen, tty_read, tty_read_line, tty_set_raw_mode, tty_size,
1062
+ };
1063
+
812
1064
  #[cfg(feature = "ws")]
813
1065
  pub use ws::{
814
1066
  web_socket_client, web_socket_server_accept, web_socket_server_construct,
@@ -868,7 +1120,7 @@ pub fn http_serve_per_worker(
868
1120
  };
869
1121
  let factory: tishlang_core::NativeFn = factory;
870
1122
  http::serve_per_worker(args, move |worker_id| {
871
- let handler_val = factory(&[Value::Number(worker_id as f64)]);
1123
+ let handler_val = factory.call(&[Value::Number(worker_id as f64)]);
872
1124
  match handler_val {
873
1125
  Value::Function(f) => f,
874
1126
  _ => panic!(
@@ -885,7 +1137,10 @@ pub use timers::{
885
1137
  };
886
1138
 
887
1139
  #[cfg(any(feature = "http", feature = "promise"))]
888
- pub use promise::{await_promise, promise_instance_catch, promise_instance_then, promise_object};
1140
+ pub use promise::{
1141
+ await_promise, await_promise_throw, promise_instance_catch, promise_instance_then,
1142
+ promise_object, promise_spawn as promise_spawn_value,
1143
+ };
889
1144
 
890
1145
  #[cfg(feature = "http")]
891
1146
  pub use native_promise::{fetch_all_promise, fetch_async_promise, fetch_promise};
@@ -1040,7 +1295,7 @@ pub fn string_split_regex(s: &Value, separator: &Value, limit: Option<usize>) ->
1040
1295
  }
1041
1296
  Value::String(sep) => {
1042
1297
  let parts: Vec<Value> = input
1043
- .splitn(max, sep.as_ref())
1298
+ .splitn(max, sep.as_str())
1044
1299
  .map(|s| Value::String(s.into()))
1045
1300
  .collect();
1046
1301
  Value::Array(VmRef::new(parts))
@@ -1134,7 +1389,7 @@ fn string_replace_regex_or_callback(s: &Value, search: &Value, replacement: &Val
1134
1389
  args.push(Value::Number(char_index as f64));
1135
1390
  args.push(Value::String(input.into()));
1136
1391
 
1137
- let repl_val = cb(&args);
1392
+ let repl_val = cb.call(&args);
1138
1393
  let repl_str = repl_val.to_display_string();
1139
1394
  result.push_str(&input[last_end..byte_start]);
1140
1395
  result.push_str(&repl_str);