@tishlang/tish-format 2.12.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -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 +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- 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 +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- 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/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +94 -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_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -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_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.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
|
|
@@ -64,17 +118,23 @@ pub use tishlang_builtins::array::{
|
|
|
64
118
|
find_last_index as array_find_last_index, flat as array_flat_impl, flat_map as array_flat_map,
|
|
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
|
-
fill as array_fill,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
fill as array_fill, last_index_of as array_last_index_of, copy_within as array_copy_within,
|
|
122
|
+
reduce as array_reduce, reduce_right as array_reduce_right, reverse as array_reverse,
|
|
123
|
+
keys as array_keys, values as array_values, entries as array_entries, shift as array_shift,
|
|
124
|
+
shuffle as array_shuffle, slice as array_slice_impl, snapshot_values as array_snapshot_values,
|
|
125
|
+
as_f64_snapshot as array_as_f64_snapshot, some as array_some,
|
|
126
|
+
sort_by_keys as array_sort_by_keys, sort_default as array_sort_default,
|
|
127
|
+
sort_numeric_asc as array_sort_numeric_asc, sort_numeric_desc as array_sort_numeric_desc,
|
|
71
128
|
sort_with_comparator as array_sort_with_comparator, splice as array_splice_impl,
|
|
129
|
+
to_reversed as array_to_reversed, to_sorted as array_to_sorted,
|
|
130
|
+
to_spliced as array_to_spliced, with as array_with,
|
|
72
131
|
unshift as array_unshift_impl,
|
|
73
132
|
};
|
|
74
133
|
|
|
75
134
|
// Re-export string methods from tishlang_builtins
|
|
76
135
|
pub use tishlang_builtins::string::{
|
|
77
136
|
at as string_at_impl, char_at as string_char_at_impl, char_code_at as string_char_code_at_impl,
|
|
137
|
+
code_point_at as string_code_point_at,
|
|
78
138
|
ends_with as string_ends_with_impl, escape_html as string_escape_html_impl,
|
|
79
139
|
includes as string_includes_impl, index_of as string_index_of_impl,
|
|
80
140
|
last_index_of as string_last_index_of_impl, pad_end as string_pad_end_impl,
|
|
@@ -83,8 +143,9 @@ pub use tishlang_builtins::string::{
|
|
|
83
143
|
slice as string_slice_impl, split as string_split_impl, split_limit as string_split_limit_impl,
|
|
84
144
|
starts_with as string_starts_with_impl, substr as string_substr_impl,
|
|
85
145
|
substring as string_substring_impl,
|
|
86
|
-
|
|
87
|
-
trim as string_trim,
|
|
146
|
+
normalize as string_normalize, to_lower_case as string_to_lower_case,
|
|
147
|
+
to_upper_case as string_to_upper_case, trim as string_trim, trim_end as string_trim_end,
|
|
148
|
+
trim_start as string_trim_start,
|
|
88
149
|
};
|
|
89
150
|
|
|
90
151
|
// Wrapper functions to maintain API compatibility
|
|
@@ -94,8 +155,8 @@ pub fn array_push(arr: &Value, args: &[Value]) -> Value {
|
|
|
94
155
|
pub fn array_unshift(arr: &Value, args: &[Value]) -> Value {
|
|
95
156
|
array_unshift_impl(arr, args)
|
|
96
157
|
}
|
|
97
|
-
pub fn array_index_of(arr: &Value, search: &Value) -> Value {
|
|
98
|
-
array_index_of_impl(arr, search)
|
|
158
|
+
pub fn array_index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
|
|
159
|
+
array_index_of_impl(arr, search, from)
|
|
99
160
|
}
|
|
100
161
|
pub fn array_includes(arr: &Value, search: &Value, from: &Value) -> Value {
|
|
101
162
|
array_includes_impl(arr, search, Some(from))
|
|
@@ -162,11 +223,11 @@ pub fn string_split_limit(s: &Value, sep: &Value, limit: &Value) -> Value {
|
|
|
162
223
|
}
|
|
163
224
|
string_split_limit_impl(s, sep, max)
|
|
164
225
|
}
|
|
165
|
-
pub fn string_starts_with(s: &Value, search: &Value) -> Value {
|
|
166
|
-
string_starts_with_impl(s, search)
|
|
226
|
+
pub fn string_starts_with(s: &Value, search: &Value, position: Option<&Value>) -> Value {
|
|
227
|
+
string_starts_with_impl(s, search, position)
|
|
167
228
|
}
|
|
168
|
-
pub fn string_ends_with(s: &Value, search: &Value) -> Value {
|
|
169
|
-
string_ends_with_impl(s, search)
|
|
229
|
+
pub fn string_ends_with(s: &Value, search: &Value, end_position: Option<&Value>) -> Value {
|
|
230
|
+
string_ends_with_impl(s, search, end_position)
|
|
170
231
|
}
|
|
171
232
|
pub fn string_replace(s: &Value, search: &Value, replacement: &Value) -> Value {
|
|
172
233
|
#[cfg(feature = "regex")]
|
|
@@ -195,6 +256,126 @@ pub fn value_at(recv: &Value, idx: &Value) -> Value {
|
|
|
195
256
|
pub fn string_char_code_at(s: &Value, idx: &Value) -> Value {
|
|
196
257
|
string_char_code_at_impl(s, idx)
|
|
197
258
|
}
|
|
259
|
+
|
|
260
|
+
// ── #317: typed-`String` (RustType::String) receiver fast paths ───────────────────────────────
|
|
261
|
+
//
|
|
262
|
+
// When native codegen knows the receiver of `s.charCodeAt(i)` / `s.charAt(i)` / `s.at(i)` / `s[i]`
|
|
263
|
+
// / `s.length` is a Rust `String` local (the `TISH_NATIVE_OPT` default), it can BORROW the string
|
|
264
|
+
// (`s.as_str()`) instead of deep-cloning it into a fresh `Value::String(ArcStr)` on every call. The
|
|
265
|
+
// boxed path's `s.clone().into()` is an O(n) copy of the whole string per call — O(n²) in a strided
|
|
266
|
+
// scan loop. These `&str` entry points eliminate that per-call copy.
|
|
267
|
+
//
|
|
268
|
+
// Indexing is by Unicode SCALAR (code point), byte-identical to `s.chars().nth(i)` /
|
|
269
|
+
// `s.chars().count()` — the same semantics as the boxed builtins (`tishlang_builtins::string`).
|
|
270
|
+
// `chars().nth(idx)` is O(idx) per call (acceptable: the win is removing the O(n) copy), versus the
|
|
271
|
+
// boxed cursor cache's O(1)-for-ASCII; correctness is identical either way.
|
|
272
|
+
|
|
273
|
+
#[inline]
|
|
274
|
+
fn idx_as_usize(idx: &Value) -> usize {
|
|
275
|
+
match idx {
|
|
276
|
+
Value::Number(n) => *n as usize,
|
|
277
|
+
_ => 0,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// `&str` charCodeAt — code point at `idx` as f64; OOB → NaN. Mirrors `string::char_code_at`.
|
|
282
|
+
#[inline]
|
|
283
|
+
pub fn str_char_code_at(s: &str, idx: &Value) -> Value {
|
|
284
|
+
match s.chars().nth(idx_as_usize(idx)) {
|
|
285
|
+
Some(c) => Value::Number(c as u32 as f64),
|
|
286
|
+
None => Value::Number(f64::NAN),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// `&str` charAt — 1-char string at `idx`; OOB → `""`. Mirrors `string::char_at`.
|
|
291
|
+
#[inline]
|
|
292
|
+
pub fn str_char_at(s: &str, idx: &Value) -> Value {
|
|
293
|
+
match s.chars().nth(idx_as_usize(idx)) {
|
|
294
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
295
|
+
None => Value::String("".into()),
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// `&str` String.prototype.at — negative `idx` counts from the end; OOB → null. Mirrors `string::at`.
|
|
300
|
+
#[inline]
|
|
301
|
+
pub fn str_at(s: &str, idx: &Value) -> Value {
|
|
302
|
+
let i = match idx {
|
|
303
|
+
Value::Number(n) => *n as i64,
|
|
304
|
+
_ => 0,
|
|
305
|
+
};
|
|
306
|
+
let resolved = if i < 0 {
|
|
307
|
+
s.chars().count() as i64 + i
|
|
308
|
+
} else {
|
|
309
|
+
i
|
|
310
|
+
};
|
|
311
|
+
if resolved >= 0 {
|
|
312
|
+
if let Some(c) = s.chars().nth(resolved as usize) {
|
|
313
|
+
return Value::String(c.to_string().into());
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
Value::Null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── O(1) char-slice forms: the loop-hoisted scan path ─────────────────────────────────────────
|
|
320
|
+
//
|
|
321
|
+
// `str_char_code_at` & friends index a `&str` via `chars().nth(i)` = O(i), so a strided scan
|
|
322
|
+
// (`for i in 0..s.length { s.charCodeAt(i) }`) is O(n²). When native codegen proves the String is
|
|
323
|
+
// loop-invariant it collects it ONCE into a `Vec<char>` and routes per-iteration accesses here, so
|
|
324
|
+
// each index is O(1) (scan → O(n)). Semantics are byte-identical to the `&str` forms: same
|
|
325
|
+
// `idx_as_usize`, `chars.len()` == `s.chars().count()`, `chars.get(i)` == `s.chars().nth(i)`.
|
|
326
|
+
|
|
327
|
+
/// O(1) char-slice `charCodeAt` — loop-hoisted [`str_char_code_at`]. OOB → NaN.
|
|
328
|
+
#[inline]
|
|
329
|
+
pub fn slice_char_code_at(chars: &[char], idx: &Value) -> Value {
|
|
330
|
+
match chars.get(idx_as_usize(idx)) {
|
|
331
|
+
Some(c) => Value::Number(*c as u32 as f64),
|
|
332
|
+
None => Value::Number(f64::NAN),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// O(1) char-slice `charAt` — loop-hoisted [`str_char_at`]. OOB → `""`.
|
|
337
|
+
#[inline]
|
|
338
|
+
pub fn slice_char_at(chars: &[char], idx: &Value) -> Value {
|
|
339
|
+
match chars.get(idx_as_usize(idx)) {
|
|
340
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
341
|
+
None => Value::String("".into()),
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// O(1) char-slice `at` (negative-index aware) — loop-hoisted [`str_at`]. OOB → null.
|
|
346
|
+
#[inline]
|
|
347
|
+
pub fn slice_at(chars: &[char], idx: &Value) -> Value {
|
|
348
|
+
let i = match idx {
|
|
349
|
+
Value::Number(n) => *n as i64,
|
|
350
|
+
_ => 0,
|
|
351
|
+
};
|
|
352
|
+
let resolved = if i < 0 { chars.len() as i64 + i } else { i };
|
|
353
|
+
if resolved >= 0 {
|
|
354
|
+
if let Some(c) = chars.get(resolved as usize) {
|
|
355
|
+
return Value::String(c.to_string().into());
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
Value::Null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// `&str` `s[i]` — char at a non-negative integer `idx`; non-int / negative / OOB → null.
|
|
362
|
+
/// Mirrors the `Value::String` arm of [`get_index`].
|
|
363
|
+
#[inline]
|
|
364
|
+
pub fn str_index(s: &str, idx: &Value) -> Value {
|
|
365
|
+
match idx {
|
|
366
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => match s.chars().nth(*n as usize) {
|
|
367
|
+
Some(c) => Value::String(c.to_string().into()),
|
|
368
|
+
None => Value::Null,
|
|
369
|
+
},
|
|
370
|
+
_ => Value::Null,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// `&str` `.length` — Unicode scalar (code point) count as f64. Mirrors `string::char_count`.
|
|
375
|
+
#[inline]
|
|
376
|
+
pub fn str_char_count(s: &str) -> f64 {
|
|
377
|
+
s.chars().count() as f64
|
|
378
|
+
}
|
|
198
379
|
pub fn string_repeat(s: &Value, count: &Value) -> Value {
|
|
199
380
|
string_repeat_impl(s, count)
|
|
200
381
|
}
|
|
@@ -216,6 +397,12 @@ pub fn string_last_index_of(s: &Value, search: &Value, position: &Value) -> Valu
|
|
|
216
397
|
pub fn number_to_fixed(n: &Value, digits: &Value) -> Value {
|
|
217
398
|
tishlang_builtins::number::to_fixed(n, digits)
|
|
218
399
|
}
|
|
400
|
+
pub fn number_to_exponential(n: &Value, digits: &Value) -> Value {
|
|
401
|
+
tishlang_builtins::number::to_exponential(n, digits)
|
|
402
|
+
}
|
|
403
|
+
pub fn number_to_precision(n: &Value, precision: &Value) -> Value {
|
|
404
|
+
tishlang_builtins::number::to_precision(n, precision)
|
|
405
|
+
}
|
|
219
406
|
|
|
220
407
|
/// `.toString([radix])` for the compiled backend (issue #59). A number receiver uses the
|
|
221
408
|
/// shared radix formatter so it stays byte-identical with the VM / interpreter; any other
|
|
@@ -233,69 +420,72 @@ pub mod ops {
|
|
|
233
420
|
use tishlang_core::Value;
|
|
234
421
|
|
|
235
422
|
#[inline]
|
|
236
|
-
|
|
423
|
+
// Arithmetic ops return a bare `Value` (not `Result`): they NEVER error — a non-number
|
|
424
|
+
// operand coerces to NaN, matching the VM's `eval_binop`. The former `Result<Value, Box<dyn
|
|
425
|
+
// Error>>` was vestigial and cost ~4× on the boxed path (memory-return + caller `.unwrap_or`;
|
|
426
|
+
// #201 measurement). Callers use the value directly (no suffix — see `ops_result_suffix`).
|
|
427
|
+
pub fn add(left: &Value, right: &Value) -> Value {
|
|
237
428
|
match (left, right) {
|
|
238
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
429
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a + b),
|
|
239
430
|
(Value::String(a), Value::String(b)) => {
|
|
240
431
|
let mut s = String::with_capacity(a.len() + b.len());
|
|
241
432
|
s.push_str(a);
|
|
242
433
|
s.push_str(b);
|
|
243
|
-
|
|
434
|
+
Value::String(s.into())
|
|
244
435
|
}
|
|
245
436
|
(Value::String(a), b) => {
|
|
246
437
|
let b_str = b.to_js_string();
|
|
247
438
|
let mut s = String::with_capacity(a.len() + b_str.len());
|
|
248
439
|
s.push_str(a);
|
|
249
440
|
s.push_str(&b_str);
|
|
250
|
-
|
|
441
|
+
Value::String(s.into())
|
|
251
442
|
}
|
|
252
443
|
(a, Value::String(b)) => {
|
|
253
444
|
let a_str = a.to_js_string();
|
|
254
445
|
let mut s = String::with_capacity(a_str.len() + b.len());
|
|
255
446
|
s.push_str(&a_str);
|
|
256
447
|
s.push_str(b);
|
|
257
|
-
|
|
448
|
+
Value::String(s.into())
|
|
258
449
|
}
|
|
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(
|
|
450
|
+
// Neither operand is a string ⇒ numeric coercion, matching the VM's `eval_binop`
|
|
451
|
+
// (`as_number().unwrap_or(NaN)`): a null/bool/object operand (e.g. an out-of-bounds
|
|
452
|
+
// array read) coerces to NaN, so `number + null` is NaN. Never an error.
|
|
453
|
+
(a, b) => Value::Number(
|
|
264
454
|
a.as_number().unwrap_or(f64::NAN) + b.as_number().unwrap_or(f64::NAN),
|
|
265
|
-
)
|
|
455
|
+
),
|
|
266
456
|
}
|
|
267
457
|
}
|
|
268
458
|
|
|
269
459
|
#[inline]
|
|
270
|
-
pub fn sub(left: &Value, right: &Value) ->
|
|
460
|
+
pub fn sub(left: &Value, right: &Value) -> Value {
|
|
271
461
|
match (left, right) {
|
|
272
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
462
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a - b),
|
|
273
463
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
274
|
-
(a, b) =>
|
|
464
|
+
(a, b) => Value::Number(
|
|
275
465
|
a.as_number().unwrap_or(f64::NAN) - b.as_number().unwrap_or(f64::NAN),
|
|
276
|
-
)
|
|
466
|
+
),
|
|
277
467
|
}
|
|
278
468
|
}
|
|
279
469
|
|
|
280
470
|
#[inline]
|
|
281
|
-
pub fn mul(left: &Value, right: &Value) ->
|
|
471
|
+
pub fn mul(left: &Value, right: &Value) -> Value {
|
|
282
472
|
match (left, right) {
|
|
283
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
473
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a * b),
|
|
284
474
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
285
|
-
(a, b) =>
|
|
475
|
+
(a, b) => Value::Number(
|
|
286
476
|
a.as_number().unwrap_or(f64::NAN) * b.as_number().unwrap_or(f64::NAN),
|
|
287
|
-
)
|
|
477
|
+
),
|
|
288
478
|
}
|
|
289
479
|
}
|
|
290
480
|
|
|
291
481
|
#[inline]
|
|
292
|
-
pub fn div(left: &Value, right: &Value) ->
|
|
482
|
+
pub fn div(left: &Value, right: &Value) -> Value {
|
|
293
483
|
match (left, right) {
|
|
294
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
484
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a / b),
|
|
295
485
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
296
|
-
(a, b) =>
|
|
486
|
+
(a, b) => Value::Number(
|
|
297
487
|
a.as_number().unwrap_or(f64::NAN) / b.as_number().unwrap_or(f64::NAN),
|
|
298
|
-
)
|
|
488
|
+
),
|
|
299
489
|
}
|
|
300
490
|
}
|
|
301
491
|
|
|
@@ -341,13 +531,13 @@ pub mod ops {
|
|
|
341
531
|
}
|
|
342
532
|
|
|
343
533
|
#[inline]
|
|
344
|
-
pub fn modulo(left: &Value, right: &Value) ->
|
|
534
|
+
pub fn modulo(left: &Value, right: &Value) -> Value {
|
|
345
535
|
match (left, right) {
|
|
346
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
536
|
+
(Value::Number(a), Value::Number(b)) => Value::Number(a % b),
|
|
347
537
|
// VM-parity numeric coercion (null/non-number → NaN), see `add`.
|
|
348
|
-
(a, b) =>
|
|
538
|
+
(a, b) => Value::Number(
|
|
349
539
|
a.as_number().unwrap_or(f64::NAN) % b.as_number().unwrap_or(f64::NAN),
|
|
350
|
-
)
|
|
540
|
+
),
|
|
351
541
|
}
|
|
352
542
|
}
|
|
353
543
|
}
|
|
@@ -370,7 +560,12 @@ use tishlang_core::{json_parse as core_json_parse, json_stringify as core_json_s
|
|
|
370
560
|
/// from the runtime keeps the generated source decoupled from
|
|
371
561
|
/// `tishlang_core` — generated code only ever names `tishlang_runtime`.
|
|
372
562
|
pub mod json {
|
|
563
|
+
pub use tishlang_core::json_parse;
|
|
373
564
|
pub use tishlang_core::json_stringify_into as stringify_into;
|
|
565
|
+
/// JS-correct number→JSON writer (NaN/Inf→`null`, integer fast path, else the
|
|
566
|
+
/// ECMAScript `Number::toString`). Used by codegen-emitted per-struct serialisers
|
|
567
|
+
/// (#315) for `number` fields, so their output matches `json_stringify_into` exactly.
|
|
568
|
+
pub use tishlang_core::write_json_number;
|
|
374
569
|
/// Append the JSON-escaped contents of `s` (without surrounding
|
|
375
570
|
/// quotes) to `buf`. Used by typed-struct serialisers for `String`
|
|
376
571
|
/// fields. Falls through to `tishlang_core::json_stringify_into`'s
|
|
@@ -431,14 +626,107 @@ impl fmt::Display for TishError {
|
|
|
431
626
|
|
|
432
627
|
impl std::error::Error for TishError {}
|
|
433
628
|
|
|
629
|
+
// #303 — the pending-throw slot lives in `tishlang_core` so the shared array builtins
|
|
630
|
+
// (`tishlang_builtins::array`) can poll it without a `builtins -> runtime` dependency cycle, and so
|
|
631
|
+
// the VM shares the same slot. Re-export the accessors so the Rust emitted by `tishlang_compile`
|
|
632
|
+
// keeps calling `tishlang_runtime::{set,has,take}_pending_throw`. See `tishlang_core` for the docs.
|
|
633
|
+
pub use tishlang_core::{has_pending_throw, set_pending_throw, take_pending_throw};
|
|
634
|
+
|
|
635
|
+
// #381 — the recursion-guard plumbing for generated native code. The depth counter lives in
|
|
636
|
+
// `tishlang_core` beside the pending-throw slot; the entry point generated code uses is the
|
|
637
|
+
// wrapper below, which adds the stack-pressure check the counter alone can't provide.
|
|
638
|
+
pub use tishlang_core::{stack_overflow_error, CallDepthGuard};
|
|
639
|
+
|
|
640
|
+
/// #381 — enter a boxed user-fn call frame, or trip the recursion guard. Emitted at the top of
|
|
641
|
+
/// every generated user-fn closure. Trips on EITHER limit: the counted ceiling
|
|
642
|
+
/// (`TISH_MAX_CALL_DEPTH`, default 20000 — parity with the interp/VM guards) or real stack
|
|
643
|
+
/// pressure ([`stack_low`]) — boxed native frames are large enough that 20000 of them can exceed
|
|
644
|
+
/// the stack before the counter does (the VM sidesteps this with `stacker::maybe_grow`; generated
|
|
645
|
+
/// code has no stack growth, so pressure must be its own trigger). On trip: parks the catchable
|
|
646
|
+
/// `RangeError` and returns `None`; the closure returns its dummy `Value::Null` and the throw
|
|
647
|
+
/// surfaces at the caller's pending-throw checkpoint.
|
|
648
|
+
#[inline]
|
|
649
|
+
pub fn enter_call_guarded() -> Option<CallDepthGuard> {
|
|
650
|
+
if stack_low() {
|
|
651
|
+
set_pending_throw(stack_overflow_error());
|
|
652
|
+
return None;
|
|
653
|
+
}
|
|
654
|
+
tishlang_core::enter_call_guarded()
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[cfg(not(target_family = "wasm"))]
|
|
658
|
+
thread_local! {
|
|
659
|
+
// The current thread's bail floor for guarded self-recursive native fns: the stack address below
|
|
660
|
+
// which recursion must stop (real stack bottom + headroom margin). 0 = not yet initialized.
|
|
661
|
+
// Thread-local because every thread's stack occupies a different address range. #381
|
|
662
|
+
static STACK_FLOOR: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// Headroom left below the deepest allowed recursion level (#381): must cover the levels emitted
|
|
666
|
+
/// between two guard checks (the rotation window), the bail path, and building/raising the
|
|
667
|
+
/// `RangeError`. Mirrors the VM JIT tier's margin (`RECUR_STACK_MARGIN`).
|
|
668
|
+
#[cfg(not(target_family = "wasm"))]
|
|
669
|
+
const RECUR_STACK_MARGIN: usize = 256 * 1024;
|
|
670
|
+
|
|
671
|
+
/// #381 — is the current thread's stack nearly exhausted? Emitted by the native backend at the
|
|
672
|
+
/// entry of self-recursive typed fns (every K-th rotation copy), so unbounded numeric recursion
|
|
673
|
+
/// bails into a catchable `RangeError` instead of overflowing the stack (an uncatchable abort).
|
|
674
|
+
/// One thread-local read + pointer compare after first init; the margin is capped at half the
|
|
675
|
+
/// remaining stack so a first call on an already-deep stack can't false-trip (limit stays below SP).
|
|
676
|
+
#[cfg(not(target_family = "wasm"))]
|
|
677
|
+
#[inline]
|
|
678
|
+
pub fn stack_low() -> bool {
|
|
679
|
+
let anchor = 0u8;
|
|
680
|
+
let sp = &anchor as *const u8 as usize;
|
|
681
|
+
STACK_FLOOR.with(|c| {
|
|
682
|
+
let mut floor = c.get();
|
|
683
|
+
if floor == 0 {
|
|
684
|
+
// `None` (unknown bounds) → floor 1: SP can never be below it, guard never trips —
|
|
685
|
+
// same do-no-harm fallback as the VM JIT tier.
|
|
686
|
+
floor = match stacker::remaining_stack() {
|
|
687
|
+
Some(rem) => {
|
|
688
|
+
let margin = RECUR_STACK_MARGIN.min(rem / 2);
|
|
689
|
+
sp.saturating_sub(rem).saturating_add(margin).max(1)
|
|
690
|
+
}
|
|
691
|
+
None => 1,
|
|
692
|
+
};
|
|
693
|
+
c.set(floor);
|
|
694
|
+
}
|
|
695
|
+
sp < floor
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/// Wasm: the sandbox traps on overflow (contained by design), and `stacker` has no wasm support —
|
|
700
|
+
/// the guard compiles to a constant `false` so the emitted check folds away.
|
|
701
|
+
#[cfg(target_family = "wasm")]
|
|
702
|
+
#[inline]
|
|
703
|
+
pub fn stack_low() -> bool {
|
|
704
|
+
false
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/// #381 — the bail path for a tripped typed-fn recursion guard: park the catchable `RangeError`
|
|
708
|
+
/// and return the NaN sentinel the f64 frame unwinds with. Typed native fns are pure numeric
|
|
709
|
+
/// (no side effects), so the NaN propagates harmlessly until the first `Value`/`Result` frame's
|
|
710
|
+
/// pending-throw checkpoint raises the error.
|
|
711
|
+
#[cold]
|
|
712
|
+
#[inline(never)]
|
|
713
|
+
pub fn recursion_tripped_f64() -> f64 {
|
|
714
|
+
set_pending_throw(stack_overflow_error());
|
|
715
|
+
f64::NAN
|
|
716
|
+
}
|
|
717
|
+
|
|
434
718
|
/// 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
|
-
///
|
|
719
|
+
/// back into the function's `Value`. A `return v` yields `v`; an uncaught `throw` is stored in the
|
|
720
|
+
/// pending-throw slot and the fn escapes with a dummy `Value` so the throw keeps propagating to the
|
|
721
|
+
/// caller's post-call check (#303); any other error panics.
|
|
437
722
|
pub fn fn_unwind(e: Box<dyn std::error::Error>) -> Value {
|
|
438
723
|
match e.downcast::<TishError>() {
|
|
439
724
|
Ok(te) => match *te {
|
|
440
725
|
TishError::Return(v) => v,
|
|
441
|
-
TishError::Throw(v) =>
|
|
726
|
+
TishError::Throw(v) => {
|
|
727
|
+
set_pending_throw(v);
|
|
728
|
+
Value::Null
|
|
729
|
+
}
|
|
442
730
|
},
|
|
443
731
|
Err(orig) => panic!("error in native Tish: {:?}", orig),
|
|
444
732
|
}
|
|
@@ -524,6 +812,15 @@ pub fn decode_uri(args: &[Value]) -> Value {
|
|
|
524
812
|
pub fn encode_uri(args: &[Value]) -> Value {
|
|
525
813
|
builtins_encode_uri(args)
|
|
526
814
|
}
|
|
815
|
+
pub fn encode_uri_component(args: &[Value]) -> Value {
|
|
816
|
+
tishlang_builtins::globals::encode_uri_component(args)
|
|
817
|
+
}
|
|
818
|
+
pub fn decode_uri_component(args: &[Value]) -> Value {
|
|
819
|
+
tishlang_builtins::globals::decode_uri_component(args)
|
|
820
|
+
}
|
|
821
|
+
pub fn structured_clone(args: &[Value]) -> Value {
|
|
822
|
+
tishlang_builtins::globals::structured_clone(args)
|
|
823
|
+
}
|
|
527
824
|
|
|
528
825
|
// Math functions - use tishlang_builtins::math
|
|
529
826
|
pub use tishlang_builtins::math::{
|
|
@@ -627,6 +924,14 @@ runtime_math_unary!(math_atanh, atanh);
|
|
|
627
924
|
runtime_math_unary!(math_cbrt, cbrt);
|
|
628
925
|
runtime_math_unary!(math_log2, log2);
|
|
629
926
|
runtime_math_unary!(math_log10, log10);
|
|
927
|
+
runtime_math_unary!(math_expm1, exp_m1);
|
|
928
|
+
runtime_math_unary!(math_log1p, ln_1p);
|
|
929
|
+
pub fn math_clz32(args: &[Value]) -> Value {
|
|
930
|
+
tishlang_builtins::math::clz32(args)
|
|
931
|
+
}
|
|
932
|
+
pub fn math_fround(args: &[Value]) -> Value {
|
|
933
|
+
tishlang_builtins::math::fround(args)
|
|
934
|
+
}
|
|
630
935
|
|
|
631
936
|
pub fn json_stringify(args: &[Value]) -> Value {
|
|
632
937
|
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
@@ -645,6 +950,18 @@ pub fn json_parse(args: &[Value]) -> Value {
|
|
|
645
950
|
pub fn array_is_array(args: &[Value]) -> Value {
|
|
646
951
|
builtins_array_is_array(args)
|
|
647
952
|
}
|
|
953
|
+
pub fn array_of(args: &[Value]) -> Value {
|
|
954
|
+
tishlang_builtins::globals::array_of(args)
|
|
955
|
+
}
|
|
956
|
+
pub fn array_from(args: &[Value]) -> Value {
|
|
957
|
+
tishlang_builtins::globals::array_from(args)
|
|
958
|
+
}
|
|
959
|
+
pub fn object_is(args: &[Value]) -> Value {
|
|
960
|
+
tishlang_builtins::globals::object_is(args)
|
|
961
|
+
}
|
|
962
|
+
pub fn object_has_own(args: &[Value]) -> Value {
|
|
963
|
+
tishlang_builtins::globals::object_has_own(args)
|
|
964
|
+
}
|
|
648
965
|
|
|
649
966
|
pub fn string_from_char_code(args: &[Value]) -> Value {
|
|
650
967
|
builtins_string_from_char_code(args)
|
|
@@ -659,6 +976,18 @@ pub fn string_convert(args: &[Value]) -> Value {
|
|
|
659
976
|
pub fn number_convert(args: &[Value]) -> Value {
|
|
660
977
|
builtins_number_convert(args)
|
|
661
978
|
}
|
|
979
|
+
pub fn number_is_integer(args: &[Value]) -> Value {
|
|
980
|
+
tishlang_builtins::globals::number_is_integer(args)
|
|
981
|
+
}
|
|
982
|
+
pub fn number_is_safe_integer(args: &[Value]) -> Value {
|
|
983
|
+
tishlang_builtins::globals::number_is_safe_integer(args)
|
|
984
|
+
}
|
|
985
|
+
pub fn number_is_nan(args: &[Value]) -> Value {
|
|
986
|
+
tishlang_builtins::globals::number_is_nan(args)
|
|
987
|
+
}
|
|
988
|
+
pub fn number_is_finite(args: &[Value]) -> Value {
|
|
989
|
+
tishlang_builtins::globals::number_is_finite(args)
|
|
990
|
+
}
|
|
662
991
|
|
|
663
992
|
#[cfg(feature = "process")]
|
|
664
993
|
pub fn process_exit(args: &[Value]) -> Value {
|
|
@@ -696,6 +1025,48 @@ pub fn process_exec(args: &[Value]) -> Value {
|
|
|
696
1025
|
}
|
|
697
1026
|
}
|
|
698
1027
|
|
|
1028
|
+
/// `process.execFile(program, [args])` — run a program directly, WITHOUT a shell (no `sh -c`). Each
|
|
1029
|
+
/// argument is passed to the program verbatim, so shell metacharacters in untrusted argument data are
|
|
1030
|
+
/// never interpreted — the safe counterpart to `exec` when arguments derive from input (#384). Returns
|
|
1031
|
+
/// the exit code, like `exec`.
|
|
1032
|
+
#[cfg(feature = "process")]
|
|
1033
|
+
pub fn process_exec_file(args: &[Value]) -> Value {
|
|
1034
|
+
use std::process::Command;
|
|
1035
|
+
let program = args
|
|
1036
|
+
.first()
|
|
1037
|
+
.map(|v| v.to_display_string())
|
|
1038
|
+
.unwrap_or_default();
|
|
1039
|
+
if program.is_empty() {
|
|
1040
|
+
return Value::Number(0.0);
|
|
1041
|
+
}
|
|
1042
|
+
let argv: Vec<String> = match args.get(1) {
|
|
1043
|
+
Some(Value::Array(a)) => a.borrow().iter().map(|v| v.to_display_string()).collect(),
|
|
1044
|
+
_ => Vec::new(),
|
|
1045
|
+
};
|
|
1046
|
+
match Command::new(&program).args(&argv).status() {
|
|
1047
|
+
Ok(status) => Value::Number(status.code().unwrap_or(1) as f64),
|
|
1048
|
+
Err(_) => Value::Number(1.0),
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
#[cfg(all(test, feature = "process", unix))]
|
|
1053
|
+
mod execfile_tests_384 {
|
|
1054
|
+
use super::process_exec_file;
|
|
1055
|
+
use tishlang_core::{Value, VmRef};
|
|
1056
|
+
|
|
1057
|
+
#[test]
|
|
1058
|
+
fn execfile_runs_without_shell_and_returns_exit_code() {
|
|
1059
|
+
// Runs the program directly (no `sh -c`); `true`/`false`/`echo` exist on unix.
|
|
1060
|
+
assert!(matches!(process_exec_file(&[Value::String("true".into())]), Value::Number(n) if n == 0.0));
|
|
1061
|
+
assert!(matches!(process_exec_file(&[Value::String("false".into())]), Value::Number(n) if n == 1.0));
|
|
1062
|
+
// args are passed verbatim as a Value::Array.
|
|
1063
|
+
let args = Value::Array(VmRef::new(vec![Value::String("ok".into())]));
|
|
1064
|
+
assert!(matches!(process_exec_file(&[Value::String("echo".into()), args]), Value::Number(n) if n == 0.0));
|
|
1065
|
+
// empty program is a no-op returning 0, matching `exec`.
|
|
1066
|
+
assert!(matches!(process_exec_file(&[]), Value::Number(n) if n == 0.0));
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
699
1070
|
#[cfg(feature = "fs")]
|
|
700
1071
|
pub fn read_file(args: &[Value]) -> Value {
|
|
701
1072
|
let path = args
|
|
@@ -791,6 +1162,92 @@ pub fn mkdir(args: &[Value]) -> Value {
|
|
|
791
1162
|
|
|
792
1163
|
use std::sync::Arc;
|
|
793
1164
|
|
|
1165
|
+
/// Per-site polymorphic inline cache for `obj.<literal key>` reads in generated native code (#179).
|
|
1166
|
+
///
|
|
1167
|
+
/// The native backend otherwise pays a full dynamic lookup (RefCell/Mutex borrow + linear key scan)
|
|
1168
|
+
/// for every member read, even at sites that only ever see a few object shapes. `PropMap` already
|
|
1169
|
+
/// tracks a hidden-class [`ShapeId`] per object and exposes slot-indexed access, so once a
|
|
1170
|
+
/// `(shape, slot)` pair is observed at a site, a later object of the same shape resolves the property
|
|
1171
|
+
/// with one integer compare + a direct slot load instead of a key scan.
|
|
1172
|
+
///
|
|
1173
|
+
/// Each of the 8 entries packs `(shape: u32) << 32 | (slot: u32)` into a single `AtomicU64`, so the
|
|
1174
|
+
/// pair is read and written atomically — no torn `(shape, slot)` under concurrent access (e.g. an
|
|
1175
|
+
/// HTTP handler shared across worker threads). A 9th distinct shape evicts an entry round-robin. An
|
|
1176
|
+
/// empty entry is `0`; a real object never has `EMPTY_SHAPE` (0), so `0` can't be a false hit.
|
|
1177
|
+
pub struct PropIC {
|
|
1178
|
+
entries: [std::sync::atomic::AtomicU64; 8],
|
|
1179
|
+
next: std::sync::atomic::AtomicU32,
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
impl PropIC {
|
|
1183
|
+
#[allow(clippy::declare_interior_mutable_const)] // each `static IC: PropIC = PropIC::new()` site needs its own cell
|
|
1184
|
+
pub const fn new() -> Self {
|
|
1185
|
+
use std::sync::atomic::{AtomicU32, AtomicU64};
|
|
1186
|
+
PropIC {
|
|
1187
|
+
entries: [
|
|
1188
|
+
AtomicU64::new(0),
|
|
1189
|
+
AtomicU64::new(0),
|
|
1190
|
+
AtomicU64::new(0),
|
|
1191
|
+
AtomicU64::new(0),
|
|
1192
|
+
AtomicU64::new(0),
|
|
1193
|
+
AtomicU64::new(0),
|
|
1194
|
+
AtomicU64::new(0),
|
|
1195
|
+
AtomicU64::new(0),
|
|
1196
|
+
],
|
|
1197
|
+
next: AtomicU32::new(0),
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
impl Default for PropIC {
|
|
1203
|
+
fn default() -> Self {
|
|
1204
|
+
Self::new()
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/// Cached `obj.key` read — see [`PropIC`]. Behaviour is identical to [`get_prop`]: only objects with
|
|
1209
|
+
/// a stable hidden class (non-empty, non-dictionary) take the cached fast path; empty/dictionary
|
|
1210
|
+
/// objects, non-objects, and the special `size` key all fall through to `get_prop` (the borrow is
|
|
1211
|
+
/// released first, so the fallback's re-borrow can't self-deadlock the send-values `Mutex`). The
|
|
1212
|
+
/// caller (codegen) never emits this for the `size` key, but the fallback covers it regardless.
|
|
1213
|
+
#[inline]
|
|
1214
|
+
pub fn get_prop_ic(obj: &Value, key: &str, ic: &PropIC) -> Value {
|
|
1215
|
+
use std::sync::atomic::Ordering;
|
|
1216
|
+
if key != "size" {
|
|
1217
|
+
if let Value::Object(map) = obj {
|
|
1218
|
+
// Scope the borrow so it is released before the `get_prop` fallback below.
|
|
1219
|
+
{
|
|
1220
|
+
let b = map.borrow();
|
|
1221
|
+
let shape = b.strings.shape();
|
|
1222
|
+
if shape != tishlang_core::EMPTY_SHAPE && shape != tishlang_core::DICT_SHAPE {
|
|
1223
|
+
let shape_hi = (shape as u64) << 32;
|
|
1224
|
+
// Fast path: a cached entry whose shape matches gives the slot directly.
|
|
1225
|
+
for e in ic.entries.iter() {
|
|
1226
|
+
let packed = e.load(Ordering::Relaxed);
|
|
1227
|
+
if packed >> 32 == shape as u64 {
|
|
1228
|
+
if let Some(v) = b.strings.value_at_index((packed & 0xFFFF_FFFF) as usize)
|
|
1229
|
+
{
|
|
1230
|
+
return v.clone();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
// Miss: resolve once, fill an entry round-robin with the packed (shape, slot).
|
|
1235
|
+
return match b.strings.get_with_index(key) {
|
|
1236
|
+
Some((v, i)) => {
|
|
1237
|
+
let k = (ic.next.fetch_add(1, Ordering::Relaxed) % 8) as usize;
|
|
1238
|
+
ic.entries[k].store(shape_hi | i as u64, Ordering::Relaxed);
|
|
1239
|
+
v.clone()
|
|
1240
|
+
}
|
|
1241
|
+
None => Value::Null,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
// empty / dictionary object → fall through (borrow dropped here)
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
get_prop(obj, key)
|
|
1249
|
+
}
|
|
1250
|
+
|
|
794
1251
|
#[inline]
|
|
795
1252
|
pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
796
1253
|
let key = key.as_ref();
|
|
@@ -832,7 +1289,7 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
|
832
1289
|
}
|
|
833
1290
|
Value::String(s) => {
|
|
834
1291
|
if key == "length" {
|
|
835
|
-
Value::Number(s
|
|
1292
|
+
Value::Number(tishlang_builtins::string::char_count(s) as f64)
|
|
836
1293
|
} else {
|
|
837
1294
|
Value::Null
|
|
838
1295
|
}
|
|
@@ -885,6 +1342,15 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
|
|
|
885
1342
|
}
|
|
886
1343
|
_ => Value::Null,
|
|
887
1344
|
},
|
|
1345
|
+
// Reading a property of the nullish value is a JS TypeError. PARK a catchable throw (#425)
|
|
1346
|
+
// and return the null sentinel; it surfaces at the next pending-throw checkpoint. This is the
|
|
1347
|
+
// ONLY receiver that throws — a number/bool/function with no such property reads back `null`
|
|
1348
|
+
// (JS `undefined`), matching the VM/interpreter/node. Free for valid reads: `Object` is matched
|
|
1349
|
+
// first, so this cold arm never runs on the hot path.
|
|
1350
|
+
Value::Null => {
|
|
1351
|
+
tishlang_core::set_pending_throw(tishlang_core::cannot_read_property_error(key));
|
|
1352
|
+
Value::Null
|
|
1353
|
+
}
|
|
888
1354
|
_ => Value::Null,
|
|
889
1355
|
}
|
|
890
1356
|
}
|
|
@@ -895,6 +1361,11 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
895
1361
|
Value::Array(arr) => {
|
|
896
1362
|
let idx = match index {
|
|
897
1363
|
Value::Number(n) => *n as usize,
|
|
1364
|
+
// Canonical integer string key ("0","1",…) indexes the array (#432); other keys → Null.
|
|
1365
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
1366
|
+
Some(i) => i,
|
|
1367
|
+
None => return Value::Null,
|
|
1368
|
+
},
|
|
898
1369
|
_ => return Value::Null,
|
|
899
1370
|
};
|
|
900
1371
|
arr.borrow().get(idx).cloned().unwrap_or(Value::Null)
|
|
@@ -903,6 +1374,10 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
903
1374
|
Value::NumberArray(arr) => {
|
|
904
1375
|
let idx = match index {
|
|
905
1376
|
Value::Number(n) => *n as usize,
|
|
1377
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
1378
|
+
Some(i) => i,
|
|
1379
|
+
None => return Value::Null,
|
|
1380
|
+
},
|
|
906
1381
|
_ => return Value::Null,
|
|
907
1382
|
};
|
|
908
1383
|
arr.borrow().get(idx).copied().map(Value::Number).unwrap_or(Value::Null)
|
|
@@ -910,11 +1385,11 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
910
1385
|
// `str[i]` returns the character at index `i` (issue #17) — matches the VM /
|
|
911
1386
|
// interpreter; out-of-bounds / negative / non-integer indices yield null.
|
|
912
1387
|
Value::String(s) => match index {
|
|
913
|
-
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 =>
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1388
|
+
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => {
|
|
1389
|
+
tishlang_builtins::string::nth_char(s, *n as usize)
|
|
1390
|
+
.map(|c| Value::String(c.to_string().into()))
|
|
1391
|
+
.unwrap_or(Value::Null)
|
|
1392
|
+
}
|
|
918
1393
|
_ => Value::Null,
|
|
919
1394
|
},
|
|
920
1395
|
Value::Object(_) => tishlang_core::object_get(obj, index).unwrap_or(Value::Null),
|
|
@@ -925,6 +1400,16 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
|
|
|
925
1400
|
Value::String(k) => get_prop(obj, k.as_str()),
|
|
926
1401
|
_ => Value::Null,
|
|
927
1402
|
},
|
|
1403
|
+
// Indexing the nullish value throws a catchable TypeError (#425), like `get_prop` above —
|
|
1404
|
+
// parked and surfaced at the next checkpoint. Every other receiver reads back `null`.
|
|
1405
|
+
Value::Null => {
|
|
1406
|
+
let key = match index {
|
|
1407
|
+
Value::String(s) => s.to_string(),
|
|
1408
|
+
other => other.to_js_string(),
|
|
1409
|
+
};
|
|
1410
|
+
tishlang_core::set_pending_throw(tishlang_core::cannot_read_property_error(&key));
|
|
1411
|
+
Value::Null
|
|
1412
|
+
}
|
|
928
1413
|
_ => Value::Null,
|
|
929
1414
|
}
|
|
930
1415
|
}
|
|
@@ -993,6 +1478,11 @@ pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
|
|
|
993
1478
|
Value::Array(arr) => {
|
|
994
1479
|
let index = match idx {
|
|
995
1480
|
Value::Number(n) => *n as usize,
|
|
1481
|
+
// Canonical integer string key → index (#432); a non-index string key is a no-op write.
|
|
1482
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
1483
|
+
Some(i) => i,
|
|
1484
|
+
None => return val,
|
|
1485
|
+
},
|
|
996
1486
|
_ => panic!("Array index must be number"),
|
|
997
1487
|
};
|
|
998
1488
|
let mut arr_mut = arr.borrow_mut();
|
|
@@ -1009,6 +1499,11 @@ pub fn set_index(obj: &Value, idx: &Value, val: Value) -> Value {
|
|
|
1009
1499
|
Value::NumberArray(arr) => {
|
|
1010
1500
|
let index = match idx {
|
|
1011
1501
|
Value::Number(n) => *n as usize,
|
|
1502
|
+
// Canonical integer string key → index (#432); a non-index string key is a no-op write.
|
|
1503
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
1504
|
+
Some(i) => i,
|
|
1505
|
+
None => return val,
|
|
1506
|
+
},
|
|
1012
1507
|
_ => panic!("Array index must be number"),
|
|
1013
1508
|
};
|
|
1014
1509
|
let mut arr_mut = arr.borrow_mut();
|
|
@@ -1107,6 +1602,11 @@ mod timers;
|
|
|
1107
1602
|
#[cfg(any(feature = "http", feature = "promise"))]
|
|
1108
1603
|
mod promise;
|
|
1109
1604
|
|
|
1605
|
+
/// Node-compatible fs surface (sync + `fs/promises`). Referenced by the backends via
|
|
1606
|
+
/// `tishlang_runtime::fs_ext::*`. See issue #122.
|
|
1607
|
+
#[cfg(feature = "fs")]
|
|
1608
|
+
pub mod fs_ext;
|
|
1609
|
+
|
|
1110
1610
|
#[cfg(feature = "http")]
|
|
1111
1611
|
mod native_promise;
|
|
1112
1612
|
|
|
@@ -1407,6 +1907,48 @@ pub fn string_match_regex(s: &Value, regexp: &Value) -> Value {
|
|
|
1407
1907
|
}
|
|
1408
1908
|
}
|
|
1409
1909
|
|
|
1910
|
+
/// `String.prototype.matchAll(regexp)` — an ITERATOR over every match, each an exec-style match object
|
|
1911
|
+
/// (`{ "0", ...groups, index, input, groups }`). Per ECMA the regexp must be global, else a `TypeError`
|
|
1912
|
+
/// is parked; a non-RegExp argument is coerced to a global regex. Reuses `regexp_exec_impl` so the
|
|
1913
|
+
/// match objects are byte-identical to `exec`/`match`, then wraps them in the shared iterator.
|
|
1914
|
+
#[cfg(feature = "regex")]
|
|
1915
|
+
pub fn string_match_all_regex(s: &Value, regexp: &Value) -> Value {
|
|
1916
|
+
let input = match s {
|
|
1917
|
+
Value::String(s) => s.as_ref(),
|
|
1918
|
+
_ => return Value::Null,
|
|
1919
|
+
};
|
|
1920
|
+
let re_cell = match regexp {
|
|
1921
|
+
Value::RegExp(re) => re.clone(),
|
|
1922
|
+
other => {
|
|
1923
|
+
let pattern = other.to_display_string();
|
|
1924
|
+
match tishlang_core::TishRegExp::new(&pattern, "g") {
|
|
1925
|
+
Ok(re) => tishlang_core::VmRef::new(re),
|
|
1926
|
+
Err(_) => return Value::Null,
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
if !re_cell.borrow().flags.global {
|
|
1931
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
1932
|
+
"String.prototype.matchAll called with a non-global RegExp argument",
|
|
1933
|
+
));
|
|
1934
|
+
return Value::Null;
|
|
1935
|
+
}
|
|
1936
|
+
let mut results: Vec<Value> = Vec::new();
|
|
1937
|
+
{
|
|
1938
|
+
let mut re = re_cell.borrow_mut();
|
|
1939
|
+
re.last_index = 0;
|
|
1940
|
+
loop {
|
|
1941
|
+
let m = regexp_exec_impl(&mut re, input);
|
|
1942
|
+
if matches!(m, Value::Null) {
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
results.push(m);
|
|
1946
|
+
}
|
|
1947
|
+
re.last_index = 0;
|
|
1948
|
+
}
|
|
1949
|
+
tishlang_builtins::iterator::array_iterator(results)
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1410
1952
|
#[cfg(feature = "regex")]
|
|
1411
1953
|
fn string_replace_regex_or_callback(s: &Value, search: &Value, replacement: &Value) -> Value {
|
|
1412
1954
|
let input = match s {
|
|
@@ -1502,3 +2044,61 @@ pub fn string_search_regex(s: &Value, regexp: &Value) -> Value {
|
|
|
1502
2044
|
_ => Value::Number(-1.0),
|
|
1503
2045
|
}
|
|
1504
2046
|
}
|
|
2047
|
+
|
|
2048
|
+
#[cfg(test)]
|
|
2049
|
+
mod null_read_parking_tests_425 {
|
|
2050
|
+
// Reading a property/index of the nullish value PARKS a catchable TypeError (#425) instead of
|
|
2051
|
+
// silently reading back `null` — the native/runtime read paths. The throw surfaces at the caller's
|
|
2052
|
+
// next pending-throw checkpoint. Every other receiver (number/bool/valid object/array) reads back
|
|
2053
|
+
// a value with NO parked throw.
|
|
2054
|
+
use super::{get_index, get_prop};
|
|
2055
|
+
use tishlang_core::{has_pending_throw, take_pending_throw, Value, VmRef};
|
|
2056
|
+
|
|
2057
|
+
fn parked_name() -> Option<String> {
|
|
2058
|
+
take_pending_throw().and_then(|v| {
|
|
2059
|
+
if let Value::Object(o) = v {
|
|
2060
|
+
if let Some(Value::String(s)) = o.borrow().strings.get("name") {
|
|
2061
|
+
return Some(s.to_string());
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
None
|
|
2065
|
+
})
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
#[test]
|
|
2069
|
+
fn get_prop_on_null_parks_type_error() {
|
|
2070
|
+
let _ = take_pending_throw();
|
|
2071
|
+
let r = get_prop(&Value::Null, "length");
|
|
2072
|
+
assert!(matches!(r, Value::Null));
|
|
2073
|
+
assert!(has_pending_throw());
|
|
2074
|
+
assert_eq!(parked_name().as_deref(), Some("TypeError"));
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
#[test]
|
|
2078
|
+
fn get_index_on_null_parks_type_error() {
|
|
2079
|
+
let _ = take_pending_throw();
|
|
2080
|
+
let r = get_index(&Value::Null, &Value::Number(0.0));
|
|
2081
|
+
assert!(matches!(r, Value::Null));
|
|
2082
|
+
assert!(has_pending_throw());
|
|
2083
|
+
assert_eq!(parked_name().as_deref(), Some("TypeError"));
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
#[test]
|
|
2087
|
+
fn valid_reads_do_not_park() {
|
|
2088
|
+
let _ = take_pending_throw();
|
|
2089
|
+
// object property, array index, and array length must NOT park a throw.
|
|
2090
|
+
let obj = Value::object({
|
|
2091
|
+
let mut m = tishlang_core::ObjectMap::default();
|
|
2092
|
+
m.insert(std::sync::Arc::from("a"), Value::Number(42.0));
|
|
2093
|
+
m
|
|
2094
|
+
});
|
|
2095
|
+
assert!(matches!(get_prop(&obj, "a"), Value::Number(n) if n == 42.0));
|
|
2096
|
+
assert!(!has_pending_throw(), "valid property read must not park");
|
|
2097
|
+
let arr = Value::Array(VmRef::new(vec![Value::Number(7.0)]));
|
|
2098
|
+
assert!(matches!(get_index(&arr, &Value::Number(0.0)), Value::Number(n) if n == 7.0));
|
|
2099
|
+
assert!(!has_pending_throw(), "valid index read must not park");
|
|
2100
|
+
// a MISSING object property reads back null WITHOUT parking (JS `undefined`, not a throw).
|
|
2101
|
+
assert!(matches!(get_prop(&obj, "missing"), Value::Null));
|
|
2102
|
+
assert!(!has_pending_throw(), "missing property must not park");
|
|
2103
|
+
}
|
|
2104
|
+
}
|