@tishlang/tish 2.12.0 → 2.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- 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
|
@@ -11,23 +11,77 @@ use tishlang_builtins::helpers::make_error_value;
|
|
|
11
11
|
|
|
12
12
|
pub use tishlang_builtins::symbol::symbol_object;
|
|
13
13
|
pub use tishlang_core::ObjectMap;
|
|
14
|
+
// `ObjectData`/`PropMap` are the concrete object representation. Re-exported so the
|
|
15
|
+
// native codegen can build an object literal's `PropMap` directly (pre-sized, single
|
|
16
|
+
// pass) instead of materializing an intermediate `AHashMap` and rebuilding from it.
|
|
17
|
+
pub use tishlang_core::{ObjectData, PropMap};
|
|
14
18
|
pub use tishlang_core::Value;
|
|
15
19
|
pub use tishlang_core::ArcStr;
|
|
16
20
|
/// Used by native codegen for `f()` / `obj()` dispatch (`Value::Function` or `__call` on objects).
|
|
17
|
-
|
|
21
|
+
/// Wraps `tishlang_core::value_call` with a pending-throw suppression check (#381): while a thrown
|
|
22
|
+
/// value is unwinding toward its checkpoint, calling anything would run side effects JS semantics
|
|
23
|
+
/// say must not happen after a throw (e.g. `console.log(f(x))` printing the NaN sentinel a tripped
|
|
24
|
+
/// recursion guard unwound with). One thread-local read on a path already dominated by boxing.
|
|
25
|
+
pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
26
|
+
if has_pending_throw() {
|
|
27
|
+
return Value::Null;
|
|
28
|
+
}
|
|
29
|
+
tishlang_core::value_call(callee, args)
|
|
30
|
+
}
|
|
18
31
|
/// JS ToInt32/ToUint32 for the emitted bitwise/shift code (modulo 2³², NaN/±Infinity → 0).
|
|
19
32
|
pub use tishlang_core::{
|
|
20
33
|
to_int32, to_int32_value, to_number_value, to_uint32, to_uint32_value,
|
|
21
34
|
};
|
|
35
|
+
/// ECMAScript `Number::toString`, appended straight into a buffer — for in-place string building
|
|
36
|
+
/// (`s += n`) in emitted code, so a number append needs no throwaway `String`.
|
|
37
|
+
pub use tishlang_core::js_number_to_string_into;
|
|
38
|
+
|
|
39
|
+
/// Append `v`'s JS string-concatenation form directly to `buf` (no intermediate `String` for the
|
|
40
|
+
/// common scalar cases). Used by emitted `s += rhs` / `s = s + rhs` so a string-builder loop stays
|
|
41
|
+
/// allocation-light. Numbers go through `js_number_to_string_into` (JS-correct, ryu-backed), so a
|
|
42
|
+
/// concatenated float matches Node (`String(1e21)` → `"1e+21"`), which the old `n.to_string()`
|
|
43
|
+
/// Display path did not.
|
|
44
|
+
#[inline]
|
|
45
|
+
pub fn push_value_str(buf: &mut String, v: &Value) {
|
|
46
|
+
match v {
|
|
47
|
+
Value::String(s) => buf.push_str(s),
|
|
48
|
+
Value::Number(n) => js_number_to_string_into(buf, *n),
|
|
49
|
+
Value::Bool(b) => buf.push_str(if *b { "true" } else { "false" }),
|
|
50
|
+
Value::Null => buf.push_str("null"),
|
|
51
|
+
other => buf.push_str(&other.to_js_string()),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
22
54
|
// Re-export the shared-mutable wrapper so the Rust code emitted by
|
|
23
55
|
// `tishlang_compile::codegen` can write `VmRef::new(...)` without needing
|
|
24
56
|
// a direct dependency on `tishlang_core` from the generated crate.
|
|
25
57
|
pub use tishlang_core::{VmReadGuard, VmRef, VmWriteGuard};
|
|
26
58
|
|
|
59
|
+
/// #218 — read a captured `VmRef` cell's value, releasing the lock guard BEFORE returning.
|
|
60
|
+
///
|
|
61
|
+
/// Generated native code reads captured cells in *expression position* (e.g. `current.dim +
|
|
62
|
+
/// current.fg` lowers to two reads of `current` inside one `+`). The old inline form
|
|
63
|
+
/// `(*cell.borrow()).clone()` puts the `borrow()` guard in a *temporary*, whose lifetime extends to
|
|
64
|
+
/// the end of the enclosing statement — so two reads of the SAME cell in one expression hold two
|
|
65
|
+
/// guards at once. Under the `send-values` build a `VmRef` is an `Arc<Mutex<T>>` and `borrow()` is a
|
|
66
|
+
/// non-reentrant `Mutex::lock()`, so that second lock self-deadlocks (the process sleeps at 0% CPU).
|
|
67
|
+
/// Cloning inside this fn drops the guard at the `return`, so repeated reads of one cell lock
|
|
68
|
+
/// strictly sequentially. Behaviour is identical to `(*cell.borrow()).clone()` otherwise.
|
|
69
|
+
#[inline]
|
|
70
|
+
pub fn vm_read<T: Clone>(cell: &VmRef<T>) -> T {
|
|
71
|
+
(*cell.borrow()).clone()
|
|
72
|
+
}
|
|
73
|
+
|
|
27
74
|
/// `for…of` iterable normalization for the native backend: a JS iterator object (one with a
|
|
28
75
|
/// callable `next()` returning `{ value, done }`, e.g. a `Map`/`Set` `.values()` result) is
|
|
29
76
|
/// drained into a `Value::Array`; arrays, strings, and everything else pass through unchanged.
|
|
30
77
|
pub fn normalize_for_of(v: Value) -> Value {
|
|
78
|
+
// A packed `NumberArray` (e.g. a module-const f64 array) must box to a plain `Value::Array` so the
|
|
79
|
+
// single-variant `if let Value::Array` in the spread / for-of codegen binds — otherwise it spreads
|
|
80
|
+
// to zero elements.
|
|
81
|
+
if let Value::NumberArray(arr) = &v {
|
|
82
|
+
let items: Vec<Value> = arr.borrow().iter().map(|n| Value::Number(*n)).collect();
|
|
83
|
+
return Value::Array(VmRef::new(items));
|
|
84
|
+
}
|
|
31
85
|
match tishlang_core::drain_iterator(&v) {
|
|
32
86
|
Some(items) => Value::Array(VmRef::new(items)),
|
|
33
87
|
None => v,
|
|
@@ -53,8 +107,8 @@ pub use tishlang_builtins::typedarrays::{
|
|
|
53
107
|
};
|
|
54
108
|
pub use tishlang_builtins::date::date_constructor_value as tish_date_constructor;
|
|
55
109
|
pub use tishlang_builtins::collections::{
|
|
56
|
-
collection_size, map_constructor_value as tish_map_constructor,
|
|
57
|
-
set_constructor_value as tish_set_constructor,
|
|
110
|
+
collection_size, map_constructor_value as tish_map_constructor, map_get,
|
|
111
|
+
map_has, map_set, map_values, set_constructor_value as tish_set_constructor,
|
|
58
112
|
};
|
|
59
113
|
|
|
60
114
|
// Re-export array methods from tishlang_builtins
|
|
@@ -65,9 +119,10 @@ pub use tishlang_builtins::array::{
|
|
|
65
119
|
for_each as array_for_each, includes as array_includes_impl, index_of as array_index_of_impl,
|
|
66
120
|
join as array_join_impl, map as array_map, pop as array_pop, push as array_push_impl,
|
|
67
121
|
fill as array_fill, reduce as array_reduce, reverse as array_reverse, shift as array_shift,
|
|
68
|
-
shuffle as array_shuffle, slice as array_slice_impl,
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
shuffle as array_shuffle, slice as array_slice_impl, snapshot_values as array_snapshot_values,
|
|
123
|
+
as_f64_snapshot as array_as_f64_snapshot, some as array_some,
|
|
124
|
+
sort_by_keys as array_sort_by_keys, sort_default as array_sort_default,
|
|
125
|
+
sort_numeric_asc as array_sort_numeric_asc, sort_numeric_desc as array_sort_numeric_desc,
|
|
71
126
|
sort_with_comparator as array_sort_with_comparator, splice as array_splice_impl,
|
|
72
127
|
unshift as array_unshift_impl,
|
|
73
128
|
};
|
|
@@ -195,6 +250,126 @@ pub fn value_at(recv: &Value, idx: &Value) -> Value {
|
|
|
195
250
|
pub fn string_char_code_at(s: &Value, idx: &Value) -> Value {
|
|
196
251
|
string_char_code_at_impl(s, idx)
|
|
197
252
|
}
|
|
253
|
+
|
|
254
|
+
// ── #317: typed-`String` (RustType::String) receiver fast paths ───────────────────────────────
|
|
255
|
+
//
|
|
256
|
+
// When native codegen knows the receiver of `s.charCodeAt(i)` / `s.charAt(i)` / `s.at(i)` / `s[i]`
|
|
257
|
+
// / `s.length` is a Rust `String` local (the `TISH_NATIVE_OPT` default), it can BORROW the string
|
|
258
|
+
// (`s.as_str()`) instead of deep-cloning it into a fresh `Value::String(ArcStr)` on every call. The
|
|
259
|
+
// boxed path's `s.clone().into()` is an O(n) copy of the whole string per call — O(n²) in a strided
|
|
260
|
+
// scan loop. These `&str` entry points eliminate that per-call copy.
|
|
261
|
+
//
|
|
262
|
+
// Indexing is by Unicode SCALAR (code point), byte-identical to `s.chars().nth(i)` /
|
|
263
|
+
// `s.chars().count()` — the same semantics as the boxed builtins (`tishlang_builtins::string`).
|
|
264
|
+
// `chars().nth(idx)` is O(idx) per call (acceptable: the win is removing the O(n) copy), versus the
|
|
265
|
+
// boxed cursor cache's O(1)-for-ASCII; correctness is identical either way.
|
|
266
|
+
|
|
267
|
+
#[inline]
|
|
268
|
+
fn idx_as_usize(idx: &Value) -> usize {
|
|
269
|
+
match idx {
|
|
270
|
+
Value::Number(n) => *n as usize,
|
|
271
|
+
_ => 0,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// `&str` charCodeAt — code point at `idx` as f64; OOB → NaN. Mirrors `string::char_code_at`.
|
|
276
|
+
#[inline]
|
|
277
|
+
pub fn str_char_code_at(s: &str, idx: &Value) -> Value {
|
|
278
|
+
match s.chars().nth(idx_as_usize(idx)) {
|
|
279
|
+
Some(c) => Value::Number(c as u32 as f64),
|
|
280
|
+
None => Value::Number(f64::NAN),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// `&str` charAt — 1-char string at `idx`; OOB → `""`. Mirrors `string::char_at`.
|
|
285
|
+
#[inline]
|
|
286
|
+
pub fn str_char_at(s: &str, idx: &Value) -> Value {
|
|
287
|
+
match s.chars().nth(idx_as_usize(idx)) {
|
|
288
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
289
|
+
None => Value::String("".into()),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// `&str` String.prototype.at — negative `idx` counts from the end; OOB → null. Mirrors `string::at`.
|
|
294
|
+
#[inline]
|
|
295
|
+
pub fn str_at(s: &str, idx: &Value) -> Value {
|
|
296
|
+
let i = match idx {
|
|
297
|
+
Value::Number(n) => *n as i64,
|
|
298
|
+
_ => 0,
|
|
299
|
+
};
|
|
300
|
+
let resolved = if i < 0 {
|
|
301
|
+
s.chars().count() as i64 + i
|
|
302
|
+
} else {
|
|
303
|
+
i
|
|
304
|
+
};
|
|
305
|
+
if resolved >= 0 {
|
|
306
|
+
if let Some(c) = s.chars().nth(resolved as usize) {
|
|
307
|
+
return Value::String(c.to_string().into());
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
Value::Null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── O(1) char-slice forms: the loop-hoisted scan path ─────────────────────────────────────────
|
|
314
|
+
//
|
|
315
|
+
// `str_char_code_at` & friends index a `&str` via `chars().nth(i)` = O(i), so a strided scan
|
|
316
|
+
// (`for i in 0..s.length { s.charCodeAt(i) }`) is O(n²). When native codegen proves the String is
|
|
317
|
+
// loop-invariant it collects it ONCE into a `Vec<char>` and routes per-iteration accesses here, so
|
|
318
|
+
// each index is O(1) (scan → O(n)). Semantics are byte-identical to the `&str` forms: same
|
|
319
|
+
// `idx_as_usize`, `chars.len()` == `s.chars().count()`, `chars.get(i)` == `s.chars().nth(i)`.
|
|
320
|
+
|
|
321
|
+
/// O(1) char-slice `charCodeAt` — loop-hoisted [`str_char_code_at`]. OOB → NaN.
|
|
322
|
+
#[inline]
|
|
323
|
+
pub fn slice_char_code_at(chars: &[char], idx: &Value) -> Value {
|
|
324
|
+
match chars.get(idx_as_usize(idx)) {
|
|
325
|
+
Some(c) => Value::Number(*c as u32 as f64),
|
|
326
|
+
None => Value::Number(f64::NAN),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// O(1) char-slice `charAt` — loop-hoisted [`str_char_at`]. OOB → `""`.
|
|
331
|
+
#[inline]
|
|
332
|
+
pub fn slice_char_at(chars: &[char], idx: &Value) -> Value {
|
|
333
|
+
match chars.get(idx_as_usize(idx)) {
|
|
334
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
335
|
+
None => Value::String("".into()),
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// O(1) char-slice `at` (negative-index aware) — loop-hoisted [`str_at`]. OOB → null.
|
|
340
|
+
#[inline]
|
|
341
|
+
pub fn slice_at(chars: &[char], idx: &Value) -> Value {
|
|
342
|
+
let i = match idx {
|
|
343
|
+
Value::Number(n) => *n as i64,
|
|
344
|
+
_ => 0,
|
|
345
|
+
};
|
|
346
|
+
let resolved = if i < 0 { chars.len() as i64 + i } else { i };
|
|
347
|
+
if resolved >= 0 {
|
|
348
|
+
if let Some(c) = chars.get(resolved as usize) {
|
|
349
|
+
return Value::String(c.to_string().into());
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
Value::Null
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// `&str` `s[i]` — char at a non-negative integer `idx`; non-int / negative / OOB → null.
|
|
356
|
+
/// Mirrors the `Value::String` arm of [`get_index`].
|
|
357
|
+
#[inline]
|
|
358
|
+
pub fn str_index(s: &str, idx: &Value) -> Value {
|
|
359
|
+
match idx {
|
|
360
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => match s.chars().nth(*n as usize) {
|
|
361
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
362
|
+
None => Value::Null,
|
|
363
|
+
},
|
|
364
|
+
_ => Value::Null,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/// `&str` `.length` — Unicode scalar (code point) count as f64. Mirrors `string::char_count`.
|
|
369
|
+
#[inline]
|
|
370
|
+
pub fn str_char_count(s: &str) -> f64 {
|
|
371
|
+
s.chars().count() as f64
|
|
372
|
+
}
|
|
198
373
|
pub fn string_repeat(s: &Value, count: &Value) -> Value {
|
|
199
374
|
string_repeat_impl(s, count)
|
|
200
375
|
}
|
|
@@ -233,69 +408,72 @@ pub mod ops {
|
|
|
233
408
|
use tishlang_core::Value;
|
|
234
409
|
|
|
235
410
|
#[inline]
|
|
236
|
-
|
|
411
|
+
// Arithmetic ops return a bare `Value` (not `Result`): they NEVER error — a non-number
|
|
412
|
+
// operand coerces to NaN, matching the VM's `eval_binop`. The former `Result<Value, Box<dyn
|
|
413
|
+
// Error>>` was vestigial and cost ~4× on the boxed path (memory-return + caller `.unwrap_or`;
|
|
414
|
+
// #201 measurement). Callers use the value directly (no suffix — see `ops_result_suffix`).
|
|
415
|
+
pub fn add(left: &Value, right: &Value) -> Value {
|
|
237
416
|
match (left, right) {
|
|
238
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
417
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a + b),
|
|
239
418
|
(Value::String(a), Value::String(b)) => {
|
|
240
419
|
let mut s = String::with_capacity(a.len() + b.len());
|
|
241
420
|
s.push_str(a);
|
|
242
421
|
s.push_str(b);
|
|
243
|
-
|
|
422
|
+
Value::String(s.into())
|
|
244
423
|
}
|
|
245
424
|
(Value::String(a), b) => {
|
|
246
425
|
let b_str = b.to_js_string();
|
|
247
426
|
let mut s = String::with_capacity(a.len() + b_str.len());
|
|
248
427
|
s.push_str(a);
|
|
249
428
|
s.push_str(&b_str);
|
|
250
|
-
|
|
429
|
+
Value::String(s.into())
|
|
251
430
|
}
|
|
252
431
|
(a, Value::String(b)) => {
|
|
253
432
|
let a_str = a.to_js_string();
|
|
254
433
|
let mut s = String::with_capacity(a_str.len() + b.len());
|
|
255
434
|
s.push_str(&a_str);
|
|
256
435
|
s.push_str(b);
|
|
257
|
-
|
|
436
|
+
Value::String(s.into())
|
|
258
437
|
}
|
|
259
|
-
// Neither operand is a string
|
|
260
|
-
// (`as_number().unwrap_or(NaN)`): a null/bool/object operand (e.g. an out-of-bounds
|
|
261
|
-
// read) coerces to NaN, so `number + null` is NaN
|
|
262
|
-
|
|
263
|
-
(a, b) => Ok(Value::Number(
|
|
438
|
+
// Neither operand is a string ⇒ numeric coercion, matching the VM's `eval_binop`
|
|
439
|
+
// (`as_number().unwrap_or(NaN)`): a null/bool/object operand (e.g. an out-of-bounds
|
|
440
|
+
// array read) coerces to NaN, so `number + null` is NaN. Never an error.
|
|
441
|
+
(a, b) => Value::Number(
|
|
264
442
|
a.as_number().unwrap_or(f64::NAN) + b.as_number().unwrap_or(f64::NAN),
|
|
265
|
-
)
|
|
443
|
+
),
|
|
266
444
|
}
|
|
267
445
|
}
|
|
268
446
|
|
|
269
447
|
#[inline]
|
|
270
|
-
pub fn sub(left: &Value, right: &Value) ->
|
|
448
|
+
pub fn sub(left: &Value, right: &Value) -> Value {
|
|
271
449
|
match (left, right) {
|
|
272
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
450
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a - b),
|
|
273
451
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
274
|
-
(a, b) =>
|
|
452
|
+
(a, b) => Value::Number(
|
|
275
453
|
a.as_number().unwrap_or(f64::NAN) - b.as_number().unwrap_or(f64::NAN),
|
|
276
|
-
)
|
|
454
|
+
),
|
|
277
455
|
}
|
|
278
456
|
}
|
|
279
457
|
|
|
280
458
|
#[inline]
|
|
281
|
-
pub fn mul(left: &Value, right: &Value) ->
|
|
459
|
+
pub fn mul(left: &Value, right: &Value) -> Value {
|
|
282
460
|
match (left, right) {
|
|
283
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
461
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a * b),
|
|
284
462
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
285
|
-
(a, b) =>
|
|
463
|
+
(a, b) => Value::Number(
|
|
286
464
|
a.as_number().unwrap_or(f64::NAN) * b.as_number().unwrap_or(f64::NAN),
|
|
287
|
-
)
|
|
465
|
+
),
|
|
288
466
|
}
|
|
289
467
|
}
|
|
290
468
|
|
|
291
469
|
#[inline]
|
|
292
|
-
pub fn div(left: &Value, right: &Value) ->
|
|
470
|
+
pub fn div(left: &Value, right: &Value) -> Value {
|
|
293
471
|
match (left, right) {
|
|
294
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
472
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a / b),
|
|
295
473
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
296
|
-
(a, b) =>
|
|
474
|
+
(a, b) => Value::Number(
|
|
297
475
|
a.as_number().unwrap_or(f64::NAN) / b.as_number().unwrap_or(f64::NAN),
|
|
298
|
-
)
|
|
476
|
+
),
|
|
299
477
|
}
|
|
300
478
|
}
|
|
301
479
|
|
|
@@ -341,13 +519,13 @@ pub mod ops {
|
|
|
341
519
|
}
|
|
342
520
|
|
|
343
521
|
#[inline]
|
|
344
|
-
pub fn modulo(left: &Value, right: &Value) ->
|
|
522
|
+
pub fn modulo(left: &Value, right: &Value) -> Value {
|
|
345
523
|
match (left, right) {
|
|
346
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
524
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a % b),
|
|
347
525
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
348
|
-
(a, b) =>
|
|
526
|
+
(a, b) => Value::Number(
|
|
349
527
|
a.as_number().unwrap_or(f64::NAN) % b.as_number().unwrap_or(f64::NAN),
|
|
350
|
-
)
|
|
528
|
+
),
|
|
351
529
|
}
|
|
352
530
|
}
|
|
353
531
|
}
|
|
@@ -370,7 +548,12 @@ use tishlang_core::{json_parse as core_json_parse, json_stringify as core_json_s
|
|
|
370
548
|
/// from the runtime keeps the generated source decoupled from
|
|
371
549
|
/// `tishlang_core` — generated code only ever names `tishlang_runtime`.
|
|
372
550
|
pub mod json {
|
|
551
|
+
pub use tishlang_core::json_parse;
|
|
373
552
|
pub use tishlang_core::json_stringify_into as stringify_into;
|
|
553
|
+
/// JS-correct number→JSON writer (NaN/Inf→`null`, integer fast path, else the
|
|
554
|
+
/// ECMAScript `Number::toString`). Used by codegen-emitted per-struct serialisers
|
|
555
|
+
/// (#315) for `number` fields, so their output matches `json_stringify_into` exactly.
|
|
556
|
+
pub use tishlang_core::write_json_number;
|
|
374
557
|
/// Append the JSON-escaped contents of `s` (without surrounding
|
|
375
558
|
/// quotes) to `buf`. Used by typed-struct serialisers for `String`
|
|
376
559
|
/// fields. Falls through to `tishlang_core::json_stringify_into`'s
|
|
@@ -431,14 +614,107 @@ impl fmt::Display for TishError {
|
|
|
431
614
|
|
|
432
615
|
impl std::error::Error for TishError {}
|
|
433
616
|
|
|
617
|
+
// #303 — the pending-throw slot lives in `tishlang_core` so the shared array builtins
|
|
618
|
+
// (`tishlang_builtins::array`) can poll it without a `builtins -> runtime` dependency cycle, and so
|
|
619
|
+
// the VM shares the same slot. Re-export the accessors so the Rust emitted by `tishlang_compile`
|
|
620
|
+
// keeps calling `tishlang_runtime::{set,has,take}_pending_throw`. See `tishlang_core` for the docs.
|
|
621
|
+
pub use tishlang_core::{has_pending_throw, set_pending_throw, take_pending_throw};
|
|
622
|
+
|
|
623
|
+
// #381 — the recursion-guard plumbing for generated native code. The depth counter lives in
|
|
624
|
+
// `tishlang_core` beside the pending-throw slot; the entry point generated code uses is the
|
|
625
|
+
// wrapper below, which adds the stack-pressure check the counter alone can't provide.
|
|
626
|
+
pub use tishlang_core::{stack_overflow_error, CallDepthGuard};
|
|
627
|
+
|
|
628
|
+
/// #381 — enter a boxed user-fn call frame, or trip the recursion guard. Emitted at the top of
|
|
629
|
+
/// every generated user-fn closure. Trips on EITHER limit: the counted ceiling
|
|
630
|
+
/// (`TISH_MAX_CALL_DEPTH`, default 20000 — parity with the interp/VM guards) or real stack
|
|
631
|
+
/// pressure ([`stack_low`]) — boxed native frames are large enough that 20000 of them can exceed
|
|
632
|
+
/// the stack before the counter does (the VM sidesteps this with `stacker::maybe_grow`; generated
|
|
633
|
+
/// code has no stack growth, so pressure must be its own trigger). On trip: parks the catchable
|
|
634
|
+
/// `RangeError` and returns `None`; the closure returns its dummy `Value::Null` and the throw
|
|
635
|
+
/// surfaces at the caller's pending-throw checkpoint.
|
|
636
|
+
#[inline]
|
|
637
|
+
pub fn enter_call_guarded() -> Option<CallDepthGuard> {
|
|
638
|
+
if stack_low() {
|
|
639
|
+
set_pending_throw(stack_overflow_error());
|
|
640
|
+
return None;
|
|
641
|
+
}
|
|
642
|
+
tishlang_core::enter_call_guarded()
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
#[cfg(not(target_family = "wasm"))]
|
|
646
|
+
thread_local! {
|
|
647
|
+
// The current thread's bail floor for guarded self-recursive native fns: the stack address below
|
|
648
|
+
// which recursion must stop (real stack bottom + headroom margin). 0 = not yet initialized.
|
|
649
|
+
// Thread-local because every thread's stack occupies a different address range. #381
|
|
650
|
+
static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/// Headroom left below the deepest allowed recursion level (#381): must cover the levels emitted
|
|
654
|
+
/// between two guard checks (the rotation window), the bail path, and building/raising the
|
|
655
|
+
/// `RangeError`. Mirrors the VM JIT tier's margin (`RECUR_STACK_MARGIN`).
|
|
656
|
+
#[cfg(not(target_family = "wasm"))]
|
|
657
|
+
const RECUR_STACK_MARGIN: usize = 256 * 1024;
|
|
658
|
+
|
|
659
|
+
/// #381 — is the current thread's stack nearly exhausted? Emitted by the native backend at the
|
|
660
|
+
/// entry of self-recursive typed fns (every K-th rotation copy), so unbounded numeric recursion
|
|
661
|
+
/// bails into a catchable `RangeError` instead of overflowing the stack (an uncatchable abort).
|
|
662
|
+
/// One thread-local read + pointer compare after first init; the margin is capped at half the
|
|
663
|
+
/// remaining stack so a first call on an already-deep stack can't false-trip (limit stays below SP).
|
|
664
|
+
#[cfg(not(target_family = "wasm"))]
|
|
665
|
+
#[inline]
|
|
666
|
+
pub fn stack_low() -> bool {
|
|
667
|
+
let anchor = 0u8;
|
|
668
|
+
let sp = &anchor as *const u8 as usize;
|
|
669
|
+
STACK_FLOOR.with(|c| {
|
|
670
|
+
let mut floor = c.get();
|
|
671
|
+
if floor == 0 {
|
|
672
|
+
// `None` (unknown bounds) → floor 1: SP can never be below it, guard never trips —
|
|
673
|
+
// same do-no-harm fallback as the VM JIT tier.
|
|
674
|
+
floor = match stacker::remaining_stack() {
|
|
675
|
+
Some(rem) => {
|
|
676
|
+
let margin = RECUR_STACK_MARGIN.min(rem / 2);
|
|
677
|
+
sp.saturating_sub(rem).saturating_add(margin).max(1)
|
|
678
|
+
}
|
|
679
|
+
None => 1,
|
|
680
|
+
};
|
|
681
|
+
c.set(floor);
|
|
682
|
+
}
|
|
683
|
+
sp < floor
|
|
684
|
+
})
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/// Wasm: the sandbox traps on overflow (contained by design), and `stacker` has no wasm support —
|
|
688
|
+
/// the guard compiles to a constant `false` so the emitted check folds away.
|
|
689
|
+
#[cfg(target_family = "wasm")]
|
|
690
|
+
#[inline]
|
|
691
|
+
pub fn stack_low() -> bool {
|
|
692
|
+
false
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/// #381 — the bail path for a tripped typed-fn recursion guard: park the catchable `RangeError`
|
|
696
|
+
/// and return the NaN sentinel the f64 frame unwinds with. Typed native fns are pure numeric
|
|
697
|
+
/// (no side effects), so the NaN propagates harmlessly until the first `Value`/`Result` frame's
|
|
698
|
+
/// pending-throw checkpoint raises the error.
|
|
699
|
+
#[cold]
|
|
700
|
+
#[inline(never)]
|
|
701
|
+
pub fn recursion_tripped_f64() -> f64 {
|
|
702
|
+
set_pending_throw(stack_overflow_error());
|
|
703
|
+
f64::NAN
|
|
704
|
+
}
|
|
705
|
+
|
|
434
706
|
/// Function-boundary unwind: convert a completion that escaped a function body's `Result`-closure
|
|
435
|
-
/// back into the function's `Value`. A `return v` yields `v`; an uncaught `throw`
|
|
436
|
-
///
|
|
707
|
+
/// back into the function's `Value`. A `return v` yields `v`; an uncaught `throw` is stored in the
|
|
708
|
+
/// pending-throw slot and the fn escapes with a dummy `Value` so the throw keeps propagating to the
|
|
709
|
+
/// caller's post-call check (#303); any other error panics.
|
|
437
710
|
pub fn fn_unwind(e: Box<dyn std::error::Error>) -> Value {
|
|
438
711
|
match e.downcast::<TishError>() {
|
|
439
712
|
Ok(te) => match *te {
|
|
440
713
|
TishError::Return(v) => v,
|
|
441
|
-
TishError::Throw(v) =>
|
|
714
|
+
TishError::Throw(v) => {
|
|
715
|
+
set_pending_throw(v);
|
|
716
|
+
Value::Null
|
|
717
|
+
}
|
|
442
718
|
},
|
|
443
719
|
Err(orig) => panic!("error in native Tish: {:?}", orig),
|
|
444
720
|
}
|
|
@@ -696,6 +972,48 @@ pub fn process_exec(args: &[Value]) -> Value {
|
|
|
696
972
|
}
|
|
697
973
|
}
|
|
698
974
|
|
|
975
|
+
/// `process.execFile(program, [args])` — run a program directly, WITHOUT a shell (no `sh -c`). Each
|
|
976
|
+
/// argument is passed to the program verbatim, so shell metacharacters in untrusted argument data are
|
|
977
|
+
/// never interpreted — the safe counterpart to `exec` when arguments derive from input (#384). Returns
|
|
978
|
+
/// the exit code, like `exec`.
|
|
979
|
+
#[cfg(feature = "process")]
|
|
980
|
+
pub fn process_exec_file(args: &[Value]) -> Value {
|
|
981
|
+
use std::process::Command;
|
|
982
|
+
let program = args
|
|
983
|
+
.first()
|
|
984
|
+
.map(|v| v.to_display_string())
|
|
985
|
+
.unwrap_or_default();
|
|
986
|
+
if program.is_empty() {
|
|
987
|
+
return Value::Number(0.0);
|
|
988
|
+
}
|
|
989
|
+
let argv: Vec<String> = match args.get(1) {
|
|
990
|
+
Some(Value::Array(a)) => a.borrow().iter().map(|v| v.to_display_string()).collect(),
|
|
991
|
+
_ => Vec::new(),
|
|
992
|
+
};
|
|
993
|
+
match Command::new(&program).args(&argv).status() {
|
|
994
|
+
Ok(status) => Value::Number(status.code().unwrap_or(1) as f64),
|
|
995
|
+
Err(_) => Value::Number(1.0),
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
#[cfg(all(test, feature = "process", unix))]
|
|
1000
|
+
mod execfile_tests_384 {
|
|
1001
|
+
use super::process_exec_file;
|
|
1002
|
+
use tishlang_core::{Value, VmRef};
|
|
1003
|
+
|
|
1004
|
+
#[test]
|
|
1005
|
+
fn execfile_runs_without_shell_and_returns_exit_code() {
|
|
1006
|
+
// Runs the program directly (no `sh -c`); `true`/`false`/`echo` exist on unix.
|
|
1007
|
+
assert!(matches!(process_exec_file(&[Value::String("true".into())]), Value::Number(n) if n == 0.0));
|
|
1008
|
+
assert!(matches!(process_exec_file(&[Value::String("false".into())]), Value::Number(n) if n == 1.0));
|
|
1009
|
+
// args are passed verbatim as a Value::Array.
|
|
1010
|
+
let args = Value::Array(VmRef::new(vec![Value::String("ok".into())]));
|
|
1011
|
+
assert!(matches!(process_exec_file(&[Value::String("echo".into()), args]), Value::Number(n) if n == 0.0));
|
|
1012
|
+
// empty program is a no-op returning 0, matching `exec`.
|
|
1013
|
+
assert!(matches!(process_exec_file(&[]), Value::Number(n) if n == 0.0));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
699
1017
|
#[cfg(feature = "fs")]
|
|
700
1018
|
pub fn read_file(args: &[Value]) -> Value {
|
|
701
1019
|
let path = args
|
|
@@ -791,6 +1109,92 @@ pub fn mkdir(args: &[Value]) -> Value {
|
|
|
791
1109
|
|
|
792
1110
|
use std::sync::Arc;
|
|
793
1111
|
|
|
1112
|
+
/// Per-site polymorphic inline cache for `obj.<literal key>` reads in generated native code (#179).
|
|
1113
|
+
///
|
|
1114
|
+
/// The native backend otherwise pays a full dynamic lookup (RefCell/Mutex borrow + linear key scan)
|
|
1115
|
+
/// for every member read, even at sites that only ever see a few object shapes. `PropMap` already
|
|
1116
|
+
/// tracks a hidden-class [`ShapeId`] per object and exposes slot-indexed access, so once a
|
|
1117
|
+
/// `(shape, slot)` pair is observed at a site, a later object of the same shape resolves the property
|
|
1118
|
+
/// with one integer compare + a direct slot load instead of a key scan.
|
|
1119
|
+
///
|
|
1120
|
+
/// Each of the 8 entries packs `(shape: u32) << 32 | (slot: u32)` into a single `AtomicU64`, so the
|
|
1121
|
+
/// pair is read and written atomically — no torn `(shape, slot)` under concurrent access (e.g. an
|
|
1122
|
+
/// HTTP handler shared across worker threads). A 9th distinct shape evicts an entry round-robin. An
|
|
1123
|
+
/// empty entry is `0`; a real object never has `EMPTY_SHAPE` (0), so `0` can't be a false hit.
|
|
1124
|
+
pub struct PropIC {
|
|
1125
|
+
entries: [std::sync::atomic::AtomicU64; 8],
|
|
1126
|
+
next: std::sync::atomic::AtomicU32,
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
impl PropIC {
|
|
1130
|
+
#[allow(clippy::declare_interior_mutable_const)] // each `static IC: PropIC = PropIC::new()` site needs its own cell
|
|
1131
|
+
pub const fn new() -> Self {
|
|
1132
|
+
use std::sync::atomic::{AtomicU32, AtomicU64};
|
|
1133
|
+
PropIC {
|
|
1134
|
+
entries: [
|
|
1135
|
+
AtomicU64::new(0),
|
|
1136
|
+
AtomicU64::new(0),
|
|
1137
|
+
AtomicU64::new(0),
|
|
1138
|
+
AtomicU64::new(0),
|
|
1139
|
+
AtomicU64::new(0),
|
|
1140
|
+
AtomicU64::new(0),
|
|
1141
|
+
AtomicU64::new(0),
|
|
1142
|
+
AtomicU64::new(0),
|
|
1143
|
+
],
|
|
1144
|
+
next: AtomicU32::new(0),
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
impl Default for PropIC {
|
|
1150
|
+
fn default() -> Self {
|
|
1151
|
+
Self::new()
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/// Cached `obj.key` read — see [`PropIC`]. Behaviour is identical to [`get_prop`]: only objects with
|
|
1156
|
+
/// a stable hidden class (non-empty, non-dictionary) take the cached fast path; empty/dictionary
|
|
1157
|
+
/// objects, non-objects, and the special `size` key all fall through to `get_prop` (the borrow is
|
|
1158
|
+
/// released first, so the fallback's re-borrow can't self-deadlock the send-values `Mutex`). The
|
|
1159
|
+
/// caller (codegen) never emits this for the `size` key, but the fallback covers it regardless.
|
|
1160
|
+
#[inline]
|
|
1161
|
+
pub fn get_prop_ic(obj: &Value, key: &str, ic: &PropIC) -> Value {
|
|
1162
|
+
use std::sync::atomic::Ordering;
|
|
1163
|
+
if key != "size" {
|
|
1164
|
+
if let Value::Object(map) = obj {
|
|
1165
|
+
// Scope the borrow so it is released before the `get_prop` fallback below.
|
|
1166
|
+
{
|
|
1167
|
+
let b = map.borrow();
|
|
1168
|
+
let shape = b.strings.shape();
|
|
1169
|
+
if shape != tishlang_core::EMPTY_SHAPE && shape != tishlang_core::DICT_SHAPE {
|
|
1170
|
+
let shape_hi = (shape as u64) << 32;
|
|
1171
|
+
// Fast path: a cached entry whose shape matches gives the slot directly.
|
|
1172
|
+
for e in ic.entries.iter() {
|
|
1173
|
+
let packed = e.load(Ordering::Relaxed);
|
|
1174
|
+
if packed >> 32 == shape as u64 {
|
|
1175
|
+
if let Some(v) = b.strings.value_at_index((packed & 0xFFFF_FFFF) as usize)
|
|
1176
|
+
{
|
|
1177
|
+
return v.clone();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
// Miss: resolve once, fill an entry round-robin with the packed (shape, slot).
|
|
1182
|
+
return match b.strings.get_with_index(key) {
|
|
1183
|
+
Some((v, i)) => {
|
|
1184
|
+
let k = (ic.next.fetch_add(1, Ordering::Relaxed) % 8) as usize;
|
|
1185
|
+
ic.entries[k].store(shape_hi | i as u64, Ordering::Relaxed);
|
|
1186
|
+
v.clone()
|
|
1187
|
+
}
|
|
1188
|
+
None => Value::Null,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
// empty / dictionary object → fall through (borrow dropped here)
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
get_prop(obj, key)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
794
1198
|
#[inline]
|
|
795
1199
|
pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
796
1200
|
let key = key.as_ref();
|
|
@@ -832,7 +1236,7 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
|
832
1236
|
}
|
|
833
1237
|
Value::String(s) => {
|
|
834
1238
|
if key == "length" {
|
|
835
|
-
Value::Number(s
|
|
1239
|
+
Value::Number(tishlang_builtins::string::char_count(s) as f64)
|
|
836
1240
|
} else {
|
|
837
1241
|
Value::Null
|
|
838
1242
|
}
|
|
@@ -885,6 +1289,15 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
|
885
1289
|
}
|
|
886
1290
|
_ => Value::Null,
|
|
887
1291
|
},
|
|
1292
|
+
// Reading a property of the nullish value is a JS TypeError. PARK a catchable throw (#425)
|
|
1293
|
+
// and return the null sentinel; it surfaces at the next pending-throw checkpoint. This is the
|
|
1294
|
+
// ONLY receiver that throws — a number/bool/function with no such property reads back `null`
|
|
1295
|
+
// (JS `undefined`), matching the VM/interpreter/node. Free for valid reads: `Object` is matched
|
|
1296
|
+
// first, so this cold arm never runs on the hot path.
|
|
1297
|
+
Value::Null => {
|
|
1298
|
+
tishlang_core::set_pending_throw(tishlang_core::cannot_read_property_error(key));
|
|
1299
|
+
Value::Null
|
|
1300
|
+
}
|
|
888
1301
|
_ => Value::Null,
|
|
889
1302
|
}
|
|
890
1303
|
}
|
|
@@ -910,11 +1323,11 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
910
1323
|
// `str[i]` returns the character at index `i` (issue #17) — matches the VM /
|
|
911
1324
|
// interpreter; out-of-bounds / negative / non-integer indices yield null.
|
|
912
1325
|
Value::String(s) => match index {
|
|
913
|
-
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 =>
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1326
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => {
|
|
1327
|
+
tishlang_builtins::string::nth_char(s, *n as usize)
|
|
1328
|
+
.map(|c| Value::String(c.to_string().into()))
|
|
1329
|
+
.unwrap_or(Value::Null)
|
|
1330
|
+
}
|
|
918
1331
|
_ => Value::Null,
|
|
919
1332
|
},
|
|
920
1333
|
Value::Object(_) => tishlang_core::object_get(obj, index).unwrap_or(Value::Null),
|
|
@@ -925,6 +1338,16 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
925
1338
|
Value::String(k) => get_prop(obj, k.as_str()),
|
|
926
1339
|
_ => Value::Null,
|
|
927
1340
|
},
|
|
1341
|
+
// Indexing the nullish value throws a catchable TypeError (#425), like `get_prop` above —
|
|
1342
|
+
// parked and surfaced at the next checkpoint. Every other receiver reads back `null`.
|
|
1343
|
+
Value::Null => {
|
|
1344
|
+
let key = match index {
|
|
1345
|
+
Value::String(s) => s.to_string(),
|
|
1346
|
+
other => other.to_js_string(),
|
|
1347
|
+
};
|
|
1348
|
+
tishlang_core::set_pending_throw(tishlang_core::cannot_read_property_error(&key));
|
|
1349
|
+
Value::Null
|
|
1350
|
+
}
|
|
928
1351
|
_ => Value::Null,
|
|
929
1352
|
}
|
|
930
1353
|
}
|
|
@@ -1502,3 +1925,61 @@ pub fn string_search_regex(s: &Value, regexp: &Value) -> Value {
|
|
|
1502
1925
|
_ => Value::Number(-1.0),
|
|
1503
1926
|
}
|
|
1504
1927
|
}
|
|
1928
|
+
|
|
1929
|
+
#[cfg(test)]
|
|
1930
|
+
mod null_read_parking_tests_425 {
|
|
1931
|
+
// Reading a property/index of the nullish value PARKS a catchable TypeError (#425) instead of
|
|
1932
|
+
// silently reading back `null` — the native/runtime read paths. The throw surfaces at the caller's
|
|
1933
|
+
// next pending-throw checkpoint. Every other receiver (number/bool/valid object/array) reads back
|
|
1934
|
+
// a value with NO parked throw.
|
|
1935
|
+
use super::{get_index, get_prop};
|
|
1936
|
+
use tishlang_core::{has_pending_throw, take_pending_throw, Value, VmRef};
|
|
1937
|
+
|
|
1938
|
+
fn parked_name() -> Option<String> {
|
|
1939
|
+
take_pending_throw().and_then(|v| {
|
|
1940
|
+
if let Value::Object(o) = v {
|
|
1941
|
+
if let Some(Value::String(s)) = o.borrow().strings.get("name") {
|
|
1942
|
+
return Some(s.to_string());
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
None
|
|
1946
|
+
})
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
#[test]
|
|
1950
|
+
fn get_prop_on_null_parks_type_error() {
|
|
1951
|
+
let _ = take_pending_throw();
|
|
1952
|
+
let r = get_prop(&Value::Null, "length");
|
|
1953
|
+
assert!(matches!(r, Value::Null));
|
|
1954
|
+
assert!(has_pending_throw());
|
|
1955
|
+
assert_eq!(parked_name().as_deref(), Some("TypeError"));
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
#[test]
|
|
1959
|
+
fn get_index_on_null_parks_type_error() {
|
|
1960
|
+
let _ = take_pending_throw();
|
|
1961
|
+
let r = get_index(&Value::Null, &Value::Number(0.0));
|
|
1962
|
+
assert!(matches!(r, Value::Null));
|
|
1963
|
+
assert!(has_pending_throw());
|
|
1964
|
+
assert_eq!(parked_name().as_deref(), Some("TypeError"));
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
#[test]
|
|
1968
|
+
fn valid_reads_do_not_park() {
|
|
1969
|
+
let _ = take_pending_throw();
|
|
1970
|
+
// object property, array index, and array length must NOT park a throw.
|
|
1971
|
+
let obj = Value::object({
|
|
1972
|
+
let mut m = tishlang_core::ObjectMap::default();
|
|
1973
|
+
m.insert(std::sync::Arc::from("a"), Value::Number(42.0));
|
|
1974
|
+
m
|
|
1975
|
+
});
|
|
1976
|
+
assert!(matches!(get_prop(&obj, "a"), Value::Number(n) if n == 42.0));
|
|
1977
|
+
assert!(!has_pending_throw(), "valid property read must not park");
|
|
1978
|
+
let arr = Value::Array(VmRef::new(vec![Value::Number(7.0)]));
|
|
1979
|
+
assert!(matches!(get_index(&arr, &Value::Number(0.0)), Value::Number(n) if n == 7.0));
|
|
1980
|
+
assert!(!has_pending_throw(), "valid index read must not park");
|
|
1981
|
+
// a MISSING object property reads back null WITHOUT parking (JS `undefined`, not a throw).
|
|
1982
|
+
assert!(matches!(get_prop(&obj, "missing"), Value::Null));
|
|
1983
|
+
assert!(!has_pending_throw(), "missing property must not park");
|
|
1984
|
+
}
|
|
1985
|
+
}
|