@tishlang/tish-lint 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-lint +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/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/Cargo.toml +1 -1
- 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-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#![allow(clippy::type_complexity, clippy::cloned_ref_to_slice_refs)]
|
|
4
4
|
|
|
5
|
+
use std::cell::Cell;
|
|
5
6
|
use std::cell::RefCell;
|
|
6
7
|
use std::collections::HashMap;
|
|
7
8
|
use std::path::{Path, PathBuf};
|
|
@@ -21,6 +22,74 @@ use crate::value::{
|
|
|
21
22
|
eval_object_get, eval_object_has, eval_object_set, EvalObjectData, PropMap, Value,
|
|
22
23
|
};
|
|
23
24
|
|
|
25
|
+
// #203: cursor cache for O(1)/near-O(1) character indexing in the interpreter. Twin of the one in
|
|
26
|
+
// `tishlang_builtins::string` (used by the native/VM backends); duplicated here because the
|
|
27
|
+
// interpreter has its own `Value`/`Arc<str>` string type rather than `tishlang_core`'s `ArcStr`. tish
|
|
28
|
+
// strings are UTF-8, so `chars().nth(i)` is O(i) — turning indexed/strided scans into O(n^2). For each
|
|
29
|
+
// recently-indexed string we cache whether it is all-ASCII (then a character index equals a byte index
|
|
30
|
+
// → O(1)) plus a forward cursor (so non-ASCII sequential/strided scans advance from the last position).
|
|
31
|
+
// Safety: the entry holds an `Arc<str>` CLONE, keeping the allocation alive so its data pointer can't
|
|
32
|
+
// be reused while cached (no ABA), and since strings are immutable the cached ASCII flag stays valid.
|
|
33
|
+
struct EvalCharCursor {
|
|
34
|
+
s: Arc<str>,
|
|
35
|
+
ascii: bool,
|
|
36
|
+
len_chars: usize,
|
|
37
|
+
char_idx: usize,
|
|
38
|
+
byte_off: usize,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
thread_local! {
|
|
42
|
+
static EVAL_INDEX_CURSOR: RefCell<Option<EvalCharCursor>> = const { RefCell::new(None) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn with_eval_cursor<R>(s: &Arc<str>, f: impl FnOnce(&mut EvalCharCursor, &Arc<str>) -> R) -> R {
|
|
46
|
+
EVAL_INDEX_CURSOR.with(|cell| {
|
|
47
|
+
let mut slot = cell.borrow_mut();
|
|
48
|
+
let hit = matches!(slot.as_ref(), Some(c)
|
|
49
|
+
if std::ptr::eq(c.s.as_bytes().as_ptr(), s.as_bytes().as_ptr()) && c.s.len() == s.len());
|
|
50
|
+
if !hit {
|
|
51
|
+
let ascii = s.as_bytes().is_ascii();
|
|
52
|
+
let len_chars = if ascii { s.len() } else { s.chars().count() };
|
|
53
|
+
*slot = Some(EvalCharCursor {
|
|
54
|
+
s: Arc::clone(s),
|
|
55
|
+
ascii,
|
|
56
|
+
len_chars,
|
|
57
|
+
char_idx: 0,
|
|
58
|
+
byte_off: 0,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
f(slot.as_mut().unwrap(), s)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Character (Unicode scalar) at index `idx` via the cursor cache. Identical results to
|
|
66
|
+
/// `s.chars().nth(idx)`, but O(1) for ASCII and near-O(1) for forward/strided scans.
|
|
67
|
+
fn nth_char_cached(s: &Arc<str>, idx: usize) -> Option<char> {
|
|
68
|
+
with_eval_cursor(s, |c, s| {
|
|
69
|
+
if c.ascii {
|
|
70
|
+
return s.as_bytes().get(idx).map(|&b| b as char);
|
|
71
|
+
}
|
|
72
|
+
let (base_idx, base_off) = if idx >= c.char_idx {
|
|
73
|
+
(c.char_idx, c.byte_off)
|
|
74
|
+
} else {
|
|
75
|
+
(0, 0)
|
|
76
|
+
};
|
|
77
|
+
match s[base_off..].char_indices().nth(idx - base_idx) {
|
|
78
|
+
Some((rel_off, ch)) => {
|
|
79
|
+
c.char_idx = idx;
|
|
80
|
+
c.byte_off = base_off + rel_off;
|
|
81
|
+
Some(ch)
|
|
82
|
+
}
|
|
83
|
+
None => None,
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Character (Unicode scalar) count via the cursor cache — O(1) after the first call on a string.
|
|
89
|
+
fn char_count_cached(s: &Arc<str>) -> usize {
|
|
90
|
+
with_eval_cursor(s, |c, _| c.len_chars)
|
|
91
|
+
}
|
|
92
|
+
|
|
24
93
|
pub struct Scope {
|
|
25
94
|
// Scope vars: order is never observed (no Object.keys over a scope), so use a fast
|
|
26
95
|
// unordered aHash map — NOT the object-strings PropMap (an insertion-ordered IndexMap),
|
|
@@ -30,6 +99,31 @@ pub struct Scope {
|
|
|
30
99
|
parent: Option<Rc<std::cell::RefCell<Scope>>>,
|
|
31
100
|
}
|
|
32
101
|
|
|
102
|
+
// #186 / string_build: amortized O(1) `acc += x`. tish strings are an immutable `Arc<str>`, so the
|
|
103
|
+
// generic `acc = acc + x` allocates a fresh String and copies the whole accumulator each time → O(n^2)
|
|
104
|
+
// over a build loop. Instead we keep the accumulator in a growable `String` ("the pending builder")
|
|
105
|
+
// and `push_str` onto it in O(1), writing it back to the scope slot ("flushing") the moment the
|
|
106
|
+
// variable is observed. JS strings are immutable VALUES, so soundness requires that any read sees the
|
|
107
|
+
// full current string: every read flushes first (see `flush_pending_for` at `Expr::Ident` and the
|
|
108
|
+
// other read sites). The builder is keyed by the EXACT owning scope (captured `Rc`) plus name, so a
|
|
109
|
+
// shadowing inner `acc` never appends onto an outer `acc`'s buffer. Shared across nested evaluators
|
|
110
|
+
// (function calls / closures) via `Rc` so a callee that reads the variable flushes the same buffer.
|
|
111
|
+
struct PendingAppend {
|
|
112
|
+
/// The exact scope that owns the accumulator variable (not necessarily the current scope).
|
|
113
|
+
scope: Rc<std::cell::RefCell<Scope>>,
|
|
114
|
+
name: Arc<str>,
|
|
115
|
+
/// The true current value of the accumulator while buffered; the scope slot is stale until flush.
|
|
116
|
+
buf: String,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[derive(Default)]
|
|
120
|
+
struct StringBuilderState {
|
|
121
|
+
/// Fast-path guard: a single `Cell<bool>` load lets the hot identifier-read path skip the
|
|
122
|
+
/// `RefCell` borrow entirely when no builder is active (the case for all non-building programs).
|
|
123
|
+
active: Cell<bool>,
|
|
124
|
+
pending: RefCell<Option<PendingAppend>>,
|
|
125
|
+
}
|
|
126
|
+
|
|
33
127
|
/// A reference-counted lexical scope. A `Value::Function` captures one of these at creation
|
|
34
128
|
/// (the *defining* scope) so calls resolve free variables lexically — real closures.
|
|
35
129
|
pub type ScopeRef = Rc<std::cell::RefCell<Scope>>;
|
|
@@ -91,6 +185,30 @@ pub struct Evaluator {
|
|
|
91
185
|
current_dir: RefCell<Option<PathBuf>>,
|
|
92
186
|
/// Extra `tish:*` builtins from `TishNativeModule::virtual_builtin_modules` (shared across nested evaluators).
|
|
93
187
|
virtual_builtins: Rc<RefCell<HashMap<Arc<str>, Value>>>,
|
|
188
|
+
/// String-builder state for amortized O(1) `acc += x` (see [`StringBuilderState`]). Shared across
|
|
189
|
+
/// nested evaluators so a called function/closure that reads the accumulator flushes the buffer.
|
|
190
|
+
string_builder: Rc<StringBuilderState>,
|
|
191
|
+
/// Current user-function call depth, SHARED (`Rc`) across every nested call-frame evaluator so it
|
|
192
|
+
/// tracks total recursion. Past [`Evaluator::max_call_depth`] a call throws a catchable
|
|
193
|
+
/// `RangeError` instead of growing the stack toward OOM/abort (#381).
|
|
194
|
+
call_depth: Rc<std::cell::Cell<usize>>,
|
|
195
|
+
/// Recursion ceiling: past this many nested user-function frames a call throws a catchable
|
|
196
|
+
/// `RangeError('Maximum call stack size exceeded')`, matching JS, rather than aborting the process.
|
|
197
|
+
/// Defaults to [`DEFAULT_MAX_CALL_DEPTH`], overridable via `TISH_MAX_CALL_DEPTH`.
|
|
198
|
+
max_call_depth: usize,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Default recursion ceiling: far deeper than any real non-pathological recursion, yet below where
|
|
202
|
+
/// `stacker`'s growth would exhaust memory (the accompanying `stacker::maybe_grow` was verified safe
|
|
203
|
+
/// to depth 20000). Override with `TISH_MAX_CALL_DEPTH`.
|
|
204
|
+
const DEFAULT_MAX_CALL_DEPTH: usize = 20_000;
|
|
205
|
+
|
|
206
|
+
fn env_max_call_depth() -> usize {
|
|
207
|
+
std::env::var("TISH_MAX_CALL_DEPTH")
|
|
208
|
+
.ok()
|
|
209
|
+
.and_then(|v| v.parse().ok())
|
|
210
|
+
.filter(|&n| n > 0)
|
|
211
|
+
.unwrap_or(DEFAULT_MAX_CALL_DEPTH)
|
|
94
212
|
}
|
|
95
213
|
|
|
96
214
|
impl Evaluator {
|
|
@@ -120,6 +238,9 @@ impl Evaluator {
|
|
|
120
238
|
);
|
|
121
239
|
s.set("decodeURI".into(), Value::Native(natives::decode_uri), true);
|
|
122
240
|
s.set("encodeURI".into(), Value::Native(natives::encode_uri), true);
|
|
241
|
+
s.set("encodeURIComponent".into(), Value::Native(natives::encode_uri_component), true);
|
|
242
|
+
s.set("decodeURIComponent".into(), Value::Native(natives::decode_uri_component), true);
|
|
243
|
+
s.set("structuredClone".into(), Value::Native(natives::structured_clone), true);
|
|
123
244
|
s.set(
|
|
124
245
|
"htmlEscape".into(),
|
|
125
246
|
Value::Native(natives::html_escape),
|
|
@@ -144,6 +265,7 @@ impl Evaluator {
|
|
|
144
265
|
math.insert("round".into(), Value::Native(natives::math_round));
|
|
145
266
|
math.insert("random".into(), Value::Native(natives::math_random));
|
|
146
267
|
math.insert("pow".into(), Value::Native(natives::math_pow));
|
|
268
|
+
math.insert("imul".into(), Value::Native(natives::math_imul));
|
|
147
269
|
math.insert("hypot".into(), Value::Native(natives::math_hypot));
|
|
148
270
|
math.insert("asin".into(), Value::Native(natives::math_asin));
|
|
149
271
|
math.insert("acos".into(), Value::Native(natives::math_acos));
|
|
@@ -163,6 +285,10 @@ impl Evaluator {
|
|
|
163
285
|
math.insert("acosh".into(), Value::Native(natives::math_acosh));
|
|
164
286
|
math.insert("atanh".into(), Value::Native(natives::math_atanh));
|
|
165
287
|
math.insert("cbrt".into(), Value::Native(natives::math_cbrt));
|
|
288
|
+
math.insert("expm1".into(), Value::Native(natives::math_expm1));
|
|
289
|
+
math.insert("log1p".into(), Value::Native(natives::math_log1p));
|
|
290
|
+
math.insert("clz32".into(), Value::Native(natives::math_clz32));
|
|
291
|
+
math.insert("fround".into(), Value::Native(natives::math_fround));
|
|
166
292
|
math.insert("log2".into(), Value::Native(natives::math_log2));
|
|
167
293
|
math.insert("log10".into(), Value::Native(natives::math_log10));
|
|
168
294
|
math.insert("PI".into(), Value::Number(std::f64::consts::PI));
|
|
@@ -193,6 +319,7 @@ impl Evaluator {
|
|
|
193
319
|
process_obj.insert("exit".into(), Value::Native(natives::process_exit));
|
|
194
320
|
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
195
321
|
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
322
|
+
process_obj.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
196
323
|
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
197
324
|
.into_iter()
|
|
198
325
|
.map(|s| Value::String(s.into()))
|
|
@@ -210,6 +337,10 @@ impl Evaluator {
|
|
|
210
337
|
object.insert("values".into(), Value::Native(Self::object_values));
|
|
211
338
|
object.insert("entries".into(), Value::Native(Self::object_entries));
|
|
212
339
|
object.insert("assign".into(), Value::Native(Self::object_assign));
|
|
340
|
+
object.insert("is".into(), Value::Native(natives::object_is));
|
|
341
|
+
object.insert("hasOwn".into(), Value::Native(natives::object_has_own));
|
|
342
|
+
// getOwnPropertyNames ≈ keys for tish's plain objects (own string keys, insertion order).
|
|
343
|
+
object.insert("getOwnPropertyNames".into(), Value::Native(Self::object_keys));
|
|
213
344
|
object.insert(
|
|
214
345
|
"fromEntries".into(),
|
|
215
346
|
Value::Native(Self::object_from_entries),
|
|
@@ -222,6 +353,7 @@ impl Evaluator {
|
|
|
222
353
|
|
|
223
354
|
let mut array_obj = PropMap::with_capacity(3);
|
|
224
355
|
array_obj.insert("isArray".into(), Value::Native(natives::array_is_array));
|
|
356
|
+
array_obj.insert("of".into(), Value::Native(natives::array_of));
|
|
225
357
|
// `Array(n)` and `new Array(n)` constructor (issue #72).
|
|
226
358
|
array_obj.insert("__call".into(), Value::Native(natives::array_construct));
|
|
227
359
|
array_obj.insert("__construct".into(), Value::Native(natives::array_construct));
|
|
@@ -257,9 +389,23 @@ impl Evaluator {
|
|
|
257
389
|
true,
|
|
258
390
|
);
|
|
259
391
|
|
|
260
|
-
// `Number(value)` coercion as a callable global (issue #36).
|
|
261
|
-
let mut number_obj = PropMap::with_capacity(
|
|
392
|
+
// `Number(value)` coercion as a callable global (issue #36) + the `Number.*` statics.
|
|
393
|
+
let mut number_obj = PropMap::with_capacity(15);
|
|
262
394
|
number_obj.insert("__call".into(), Value::Native(natives::number_convert));
|
|
395
|
+
number_obj.insert("isInteger".into(), Value::Native(natives::number_is_integer));
|
|
396
|
+
number_obj.insert("isSafeInteger".into(), Value::Native(natives::number_is_safe_integer));
|
|
397
|
+
number_obj.insert("isNaN".into(), Value::Native(natives::number_is_nan));
|
|
398
|
+
number_obj.insert("isFinite".into(), Value::Native(natives::number_is_finite));
|
|
399
|
+
number_obj.insert("parseInt".into(), Value::Native(natives::parse_int));
|
|
400
|
+
number_obj.insert("parseFloat".into(), Value::Native(natives::parse_float));
|
|
401
|
+
number_obj.insert("MAX_SAFE_INTEGER".into(), Value::Number(9_007_199_254_740_991.0));
|
|
402
|
+
number_obj.insert("MIN_SAFE_INTEGER".into(), Value::Number(-9_007_199_254_740_991.0));
|
|
403
|
+
number_obj.insert("EPSILON".into(), Value::Number(f64::EPSILON));
|
|
404
|
+
number_obj.insert("MAX_VALUE".into(), Value::Number(f64::MAX));
|
|
405
|
+
number_obj.insert("MIN_VALUE".into(), Value::Number(5e-324));
|
|
406
|
+
number_obj.insert("POSITIVE_INFINITY".into(), Value::Number(f64::INFINITY));
|
|
407
|
+
number_obj.insert("NEGATIVE_INFINITY".into(), Value::Number(f64::NEG_INFINITY));
|
|
408
|
+
number_obj.insert("NaN".into(), Value::Number(f64::NAN));
|
|
263
409
|
s.set("Number".into(), Value::object(number_obj), true);
|
|
264
410
|
|
|
265
411
|
s.set(
|
|
@@ -367,6 +513,9 @@ impl Evaluator {
|
|
|
367
513
|
module_cache: Rc::new(RefCell::new(HashMap::new())),
|
|
368
514
|
current_dir: RefCell::new(None),
|
|
369
515
|
virtual_builtins: Rc::new(RefCell::new(HashMap::new())),
|
|
516
|
+
string_builder: Rc::new(StringBuilderState::default()),
|
|
517
|
+
call_depth: Rc::new(std::cell::Cell::new(0)),
|
|
518
|
+
max_call_depth: env_max_call_depth(),
|
|
370
519
|
}
|
|
371
520
|
}
|
|
372
521
|
|
|
@@ -401,6 +550,9 @@ impl Evaluator {
|
|
|
401
550
|
for stmt in &program.statements {
|
|
402
551
|
last = self.eval_statement(stmt).map_err(|e| e.to_string())?;
|
|
403
552
|
}
|
|
553
|
+
// Flush any still-buffered string accumulator so the variable's slot is correct for any
|
|
554
|
+
// post-run observation (timers, REPL, embedders reading scope state).
|
|
555
|
+
self.flush_pending();
|
|
404
556
|
Ok(last)
|
|
405
557
|
}
|
|
406
558
|
|
|
@@ -451,7 +603,11 @@ impl Evaluator {
|
|
|
451
603
|
self.bind_destruct_pattern(pattern, &value, *mutable)?;
|
|
452
604
|
Ok(Value::Null)
|
|
453
605
|
}
|
|
454
|
-
Statement::ExprStmt { expr, .. } =>
|
|
606
|
+
Statement::ExprStmt { expr, .. } => {
|
|
607
|
+
// Statement position: route through the path that keeps statement-position
|
|
608
|
+
// `acc += x` O(1) (no result materialization) while preserving values otherwise.
|
|
609
|
+
self.eval_expr_discard(expr)
|
|
610
|
+
}
|
|
455
611
|
Statement::If {
|
|
456
612
|
cond,
|
|
457
613
|
then_branch,
|
|
@@ -531,6 +687,51 @@ impl Evaluator {
|
|
|
531
687
|
self.scope = outer;
|
|
532
688
|
ret
|
|
533
689
|
}
|
|
690
|
+
Statement::ForIn {
|
|
691
|
+
name,
|
|
692
|
+
object,
|
|
693
|
+
body,
|
|
694
|
+
..
|
|
695
|
+
} => {
|
|
696
|
+
let obj_val = self.eval_expr(object)?;
|
|
697
|
+
// for-in enumerates OWN ENUMERABLE KEYS as strings (JS semantics): objects yield keys
|
|
698
|
+
// in insertion order, arrays yield index strings "0".."len-1", and a non-object
|
|
699
|
+
// (incl. null) yields nothing — a no-op, matching JS `for (k in null)`.
|
|
700
|
+
let keys: Vec<crate::value::Value> = match &obj_val {
|
|
701
|
+
crate::value::Value::Object(obj) => obj
|
|
702
|
+
.borrow()
|
|
703
|
+
.strings
|
|
704
|
+
.keys()
|
|
705
|
+
.map(|k| crate::value::Value::String(Arc::clone(k)))
|
|
706
|
+
.collect(),
|
|
707
|
+
crate::value::Value::Array(arr) => {
|
|
708
|
+
let len = arr.borrow().len();
|
|
709
|
+
(0..len)
|
|
710
|
+
.map(|i| crate::value::Value::String(Arc::from(i.to_string())))
|
|
711
|
+
.collect()
|
|
712
|
+
}
|
|
713
|
+
_ => Vec::new(),
|
|
714
|
+
};
|
|
715
|
+
// Fresh per-iteration binding (ES6 `for (let k in …)`), like the for-of arm above.
|
|
716
|
+
let outer = Rc::clone(&self.scope);
|
|
717
|
+
let mut ret = Ok(Value::Null);
|
|
718
|
+
for key in keys {
|
|
719
|
+
let iter_env = Scope::child(Rc::clone(&outer));
|
|
720
|
+
iter_env.borrow_mut().set(Arc::clone(name), key, true);
|
|
721
|
+
self.scope = Rc::clone(&iter_env);
|
|
722
|
+
match self.eval_statement(body) {
|
|
723
|
+
Ok(_) => {}
|
|
724
|
+
Err(EvalError::Break) => break,
|
|
725
|
+
Err(EvalError::Continue) => continue,
|
|
726
|
+
Err(e) => {
|
|
727
|
+
ret = Err(e);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
self.scope = outer;
|
|
733
|
+
ret
|
|
734
|
+
}
|
|
534
735
|
Statement::For {
|
|
535
736
|
init,
|
|
536
737
|
cond,
|
|
@@ -815,6 +1016,38 @@ impl Evaluator {
|
|
|
815
1016
|
let v = self.eval_expr(e)?;
|
|
816
1017
|
self.scope.borrow_mut().set(Arc::from("default"), v, false);
|
|
817
1018
|
}
|
|
1019
|
+
// #305: re-export in the running program — load the dep and bind the names locally.
|
|
1020
|
+
ExportDeclaration::ReExport {
|
|
1021
|
+
specifiers,
|
|
1022
|
+
all,
|
|
1023
|
+
from: Some(from),
|
|
1024
|
+
..
|
|
1025
|
+
} => {
|
|
1026
|
+
let dep_val = self.load_module(from.as_ref())?;
|
|
1027
|
+
let dep = match &dep_val {
|
|
1028
|
+
Value::Object(m) => m.borrow().clone(),
|
|
1029
|
+
_ => return Err(EvalError::Error("Module exports must be object".to_string())),
|
|
1030
|
+
};
|
|
1031
|
+
if *all {
|
|
1032
|
+
for (k, v) in dep.strings.iter() {
|
|
1033
|
+
self.scope.borrow_mut().set(Arc::from(k.as_ref()), v.clone(), false);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
for spec in specifiers {
|
|
1037
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
1038
|
+
let v = dep.strings.get(name.as_ref()).ok_or_else(|| {
|
|
1039
|
+
EvalError::Error(format!("Module does not export '{}'", name))
|
|
1040
|
+
})?;
|
|
1041
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1042
|
+
self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// #415 local named export (`export { a, b as c }`, no `from`): a no-op in the
|
|
1047
|
+
// running (entry) program — the bindings already exist, and an export alias is
|
|
1048
|
+
// visible only to importers, never as a local variable (JS semantics). The module
|
|
1049
|
+
// export table is populated by the module-evaluation path, not here.
|
|
1050
|
+
ExportDeclaration::ReExport { from: None, .. } => {}
|
|
818
1051
|
}
|
|
819
1052
|
Ok(Value::Null)
|
|
820
1053
|
}
|
|
@@ -880,6 +1113,54 @@ impl Evaluator {
|
|
|
880
1113
|
self.scope.borrow_mut().set(Arc::from("default"), v, false);
|
|
881
1114
|
export_names.push("default".to_string());
|
|
882
1115
|
}
|
|
1116
|
+
// #305: re-export — load the dep, pull the requested (or all) exports into this
|
|
1117
|
+
// module's scope, and re-expose them in `export_names`.
|
|
1118
|
+
ExportDeclaration::ReExport {
|
|
1119
|
+
specifiers,
|
|
1120
|
+
all,
|
|
1121
|
+
from: Some(from),
|
|
1122
|
+
..
|
|
1123
|
+
} => {
|
|
1124
|
+
let dep_val = self.load_module(from.as_ref())?;
|
|
1125
|
+
let dep = match &dep_val {
|
|
1126
|
+
Value::Object(m) => m.borrow().clone(),
|
|
1127
|
+
_ => return Err(EvalError::Error("Module exports must be object".to_string())),
|
|
1128
|
+
};
|
|
1129
|
+
if *all {
|
|
1130
|
+
for (k, v) in dep.strings.iter() {
|
|
1131
|
+
self.scope.borrow_mut().set(Arc::from(k.as_ref()), v.clone(), false);
|
|
1132
|
+
export_names.push(k.to_string());
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
for spec in specifiers {
|
|
1136
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
1137
|
+
let v = dep.strings.get(name.as_ref()).ok_or_else(|| {
|
|
1138
|
+
EvalError::Error(format!("Module does not export '{}'", name))
|
|
1139
|
+
})?;
|
|
1140
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1141
|
+
self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
|
|
1142
|
+
export_names.push(bind.to_string());
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// #415 local named export (`export { a, b as c }`, no `from`): the bindings are
|
|
1147
|
+
// already declared in this module's scope; expose each under its export name.
|
|
1148
|
+
ExportDeclaration::ReExport {
|
|
1149
|
+
specifiers,
|
|
1150
|
+
from: None,
|
|
1151
|
+
..
|
|
1152
|
+
} => {
|
|
1153
|
+
for spec in specifiers {
|
|
1154
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
1155
|
+
let v = self.scope.borrow().get(name.as_ref()).ok_or_else(|| {
|
|
1156
|
+
EvalError::Error(format!("Export '{}' is not defined", name))
|
|
1157
|
+
})?;
|
|
1158
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1159
|
+
self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
|
|
1160
|
+
export_names.push(bind.to_string());
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
883
1164
|
}
|
|
884
1165
|
} else {
|
|
885
1166
|
let _ = self.eval_statement(stmt);
|
|
@@ -946,6 +1227,44 @@ impl Evaluator {
|
|
|
946
1227
|
Value::Native(natives::read_file_bytes),
|
|
947
1228
|
);
|
|
948
1229
|
exports.insert("mkdir".into(), Value::Native(natives::mkdir));
|
|
1230
|
+
// Node-compatible names + new methods (sync) — issue #122
|
|
1231
|
+
use natives::fsx;
|
|
1232
|
+
exports.insert("readFileSync".into(), Value::Native(fsx::read_file));
|
|
1233
|
+
exports.insert("readFileBytesSync".into(), Value::Native(fsx::read_file_bytes));
|
|
1234
|
+
exports.insert("writeFileSync".into(), Value::Native(fsx::write_file));
|
|
1235
|
+
exports.insert("appendFile".into(), Value::Native(fsx::append_file));
|
|
1236
|
+
exports.insert("appendFileSync".into(), Value::Native(fsx::append_file));
|
|
1237
|
+
exports.insert("existsSync".into(), Value::Native(fsx::exists));
|
|
1238
|
+
exports.insert("mkdirSync".into(), Value::Native(fsx::mkdir));
|
|
1239
|
+
exports.insert("readdir".into(), Value::Native(fsx::readdir));
|
|
1240
|
+
exports.insert("readdirSync".into(), Value::Native(fsx::readdir));
|
|
1241
|
+
exports.insert("stat".into(), Value::Native(fsx::stat));
|
|
1242
|
+
exports.insert("statSync".into(), Value::Native(fsx::stat));
|
|
1243
|
+
exports.insert("lstat".into(), Value::Native(fsx::lstat));
|
|
1244
|
+
exports.insert("lstatSync".into(), Value::Native(fsx::lstat));
|
|
1245
|
+
exports.insert("rm".into(), Value::Native(fsx::rm));
|
|
1246
|
+
exports.insert("rmSync".into(), Value::Native(fsx::rm));
|
|
1247
|
+
exports.insert("rmdir".into(), Value::Native(fsx::rmdir));
|
|
1248
|
+
exports.insert("rmdirSync".into(), Value::Native(fsx::rmdir));
|
|
1249
|
+
exports.insert("unlink".into(), Value::Native(fsx::unlink));
|
|
1250
|
+
exports.insert("unlinkSync".into(), Value::Native(fsx::unlink));
|
|
1251
|
+
exports.insert("rename".into(), Value::Native(fsx::rename));
|
|
1252
|
+
exports.insert("renameSync".into(), Value::Native(fsx::rename));
|
|
1253
|
+
exports.insert("copyFile".into(), Value::Native(fsx::copy_file));
|
|
1254
|
+
exports.insert("copyFileSync".into(), Value::Native(fsx::copy_file));
|
|
1255
|
+
exports.insert("realpath".into(), Value::Native(fsx::realpath));
|
|
1256
|
+
exports.insert("realpathSync".into(), Value::Native(fsx::realpath));
|
|
1257
|
+
exports.insert("readlink".into(), Value::Native(fsx::readlink));
|
|
1258
|
+
exports.insert("readlinkSync".into(), Value::Native(fsx::readlink));
|
|
1259
|
+
exports.insert("truncate".into(), Value::Native(fsx::truncate));
|
|
1260
|
+
exports.insert("truncateSync".into(), Value::Native(fsx::truncate));
|
|
1261
|
+
exports.insert("mkdtemp".into(), Value::Native(fsx::mkdtemp));
|
|
1262
|
+
exports.insert("mkdtempSync".into(), Value::Native(fsx::mkdtemp));
|
|
1263
|
+
exports.insert("cp".into(), Value::Native(fsx::cp));
|
|
1264
|
+
exports.insert("cpSync".into(), Value::Native(fsx::cp));
|
|
1265
|
+
exports.insert("access".into(), Value::Native(fsx::access));
|
|
1266
|
+
exports.insert("accessSync".into(), Value::Native(fsx::access));
|
|
1267
|
+
exports.insert("constants".into(), fsx::constants());
|
|
949
1268
|
Ok(Value::object(exports))
|
|
950
1269
|
}
|
|
951
1270
|
#[cfg(not(feature = "fs"))]
|
|
@@ -955,6 +1274,41 @@ impl Evaluator {
|
|
|
955
1274
|
));
|
|
956
1275
|
}
|
|
957
1276
|
}
|
|
1277
|
+
"tish:fs/promises" => {
|
|
1278
|
+
#[cfg(feature = "fs")]
|
|
1279
|
+
{
|
|
1280
|
+
use natives::fsx;
|
|
1281
|
+
let mut exports: PropMap = PropMap::default();
|
|
1282
|
+
exports.insert("readFile".into(), Value::Native(fsx::read_file_p));
|
|
1283
|
+
exports.insert("readFileBytes".into(), Value::Native(fsx::read_file_bytes_p));
|
|
1284
|
+
exports.insert("writeFile".into(), Value::Native(fsx::write_file_p));
|
|
1285
|
+
exports.insert("appendFile".into(), Value::Native(fsx::append_file_p));
|
|
1286
|
+
exports.insert("exists".into(), Value::Native(fsx::exists_p));
|
|
1287
|
+
exports.insert("mkdir".into(), Value::Native(fsx::mkdir_p));
|
|
1288
|
+
exports.insert("readdir".into(), Value::Native(fsx::readdir_p));
|
|
1289
|
+
exports.insert("stat".into(), Value::Native(fsx::stat_p));
|
|
1290
|
+
exports.insert("lstat".into(), Value::Native(fsx::lstat_p));
|
|
1291
|
+
exports.insert("rm".into(), Value::Native(fsx::rm_p));
|
|
1292
|
+
exports.insert("rmdir".into(), Value::Native(fsx::rmdir_p));
|
|
1293
|
+
exports.insert("unlink".into(), Value::Native(fsx::unlink_p));
|
|
1294
|
+
exports.insert("rename".into(), Value::Native(fsx::rename_p));
|
|
1295
|
+
exports.insert("copyFile".into(), Value::Native(fsx::copy_file_p));
|
|
1296
|
+
exports.insert("realpath".into(), Value::Native(fsx::realpath_p));
|
|
1297
|
+
exports.insert("readlink".into(), Value::Native(fsx::readlink_p));
|
|
1298
|
+
exports.insert("truncate".into(), Value::Native(fsx::truncate_p));
|
|
1299
|
+
exports.insert("mkdtemp".into(), Value::Native(fsx::mkdtemp_p));
|
|
1300
|
+
exports.insert("cp".into(), Value::Native(fsx::cp_p));
|
|
1301
|
+
exports.insert("access".into(), Value::Native(fsx::access_p));
|
|
1302
|
+
exports.insert("constants".into(), fsx::constants());
|
|
1303
|
+
Ok(Value::object(exports))
|
|
1304
|
+
}
|
|
1305
|
+
#[cfg(not(feature = "fs"))]
|
|
1306
|
+
{
|
|
1307
|
+
return Err(EvalError::Error(
|
|
1308
|
+
"tish:fs/promises requires the fs feature. Rebuild with: cargo build -p tishlang --features fs".into(),
|
|
1309
|
+
));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
958
1312
|
"tish:http" => {
|
|
959
1313
|
#[cfg(feature = "http")]
|
|
960
1314
|
{
|
|
@@ -1057,6 +1411,7 @@ impl Evaluator {
|
|
|
1057
1411
|
exports.insert("exit".into(), Value::Native(natives::process_exit));
|
|
1058
1412
|
exports.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1059
1413
|
exports.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1414
|
+
exports.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
1060
1415
|
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
1061
1416
|
.into_iter()
|
|
1062
1417
|
.map(|s| Value::String(s.into()))
|
|
@@ -1076,6 +1431,7 @@ impl Evaluator {
|
|
|
1076
1431
|
process_obj.insert("exit".into(), Value::Native(natives::process_exit));
|
|
1077
1432
|
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1078
1433
|
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1434
|
+
process_obj.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
1079
1435
|
process_obj.insert("argv".into(), Value::Array(Rc::new(RefCell::new(argv))));
|
|
1080
1436
|
process_obj.insert("env".into(), Value::object(env_obj));
|
|
1081
1437
|
exports.insert(
|
|
@@ -1115,6 +1471,177 @@ impl Evaluator {
|
|
|
1115
1471
|
})
|
|
1116
1472
|
}
|
|
1117
1473
|
|
|
1474
|
+
// --- string-builder helpers (#186 / string_build): amortized O(1) `acc += x` ---
|
|
1475
|
+
|
|
1476
|
+
/// Append a value to a builder buffer using the exact JS coercion of `eval_binop`'s `+` (string
|
|
1477
|
+
/// operands push their raw chars; everything else goes through `to_js_string`).
|
|
1478
|
+
fn append_value_to_buf(buf: &mut String, v: &Value) {
|
|
1479
|
+
match v {
|
|
1480
|
+
Value::String(s) => buf.push_str(s),
|
|
1481
|
+
other => buf.push_str(&other.to_js_string()),
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/// Walk the scope chain from the current scope and return the exact scope that owns `name`.
|
|
1486
|
+
fn find_var_scope(&self, name: &str) -> Option<Rc<std::cell::RefCell<Scope>>> {
|
|
1487
|
+
let mut cur = Rc::clone(&self.scope);
|
|
1488
|
+
loop {
|
|
1489
|
+
if cur.borrow().vars.contains_key(name) {
|
|
1490
|
+
return Some(cur);
|
|
1491
|
+
}
|
|
1492
|
+
let parent = cur.borrow().parent.clone();
|
|
1493
|
+
match parent {
|
|
1494
|
+
Some(p) => cur = p,
|
|
1495
|
+
None => return None,
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/// Flush the active builder (if any) back into its owning scope slot, restoring the invariant
|
|
1501
|
+
/// that the slot holds the variable's true value. No-op when no builder is active.
|
|
1502
|
+
fn flush_pending(&self) {
|
|
1503
|
+
if !self.string_builder.active.get() {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if let Some(p) = self.string_builder.pending.borrow_mut().take() {
|
|
1507
|
+
p.scope
|
|
1508
|
+
.borrow_mut()
|
|
1509
|
+
.vars
|
|
1510
|
+
.insert(Arc::clone(&p.name), Value::String(p.buf.into()));
|
|
1511
|
+
}
|
|
1512
|
+
self.string_builder.active.set(false);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/// Flush the builder iff it is buffering `name` (which is about to be read). The `active` guard
|
|
1516
|
+
/// keeps this ~free on the hot identifier-read path when no builder exists.
|
|
1517
|
+
#[inline]
|
|
1518
|
+
fn flush_pending_for(&self, name: &str) {
|
|
1519
|
+
if !self.string_builder.active.get() {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
let hit = self
|
|
1523
|
+
.string_builder
|
|
1524
|
+
.pending
|
|
1525
|
+
.borrow()
|
|
1526
|
+
.as_ref()
|
|
1527
|
+
.is_some_and(|p| p.name.as_ref() == name);
|
|
1528
|
+
if hit {
|
|
1529
|
+
self.flush_pending();
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/// Discard the builder iff it is buffering `name` (which is about to be overwritten by a plain
|
|
1534
|
+
/// assignment) — avoids an unnecessary O(n) flush of a value that is about to be replaced.
|
|
1535
|
+
#[inline]
|
|
1536
|
+
fn discard_pending_for(&self, name: &str) {
|
|
1537
|
+
if !self.string_builder.active.get() {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
let hit = self
|
|
1541
|
+
.string_builder
|
|
1542
|
+
.pending
|
|
1543
|
+
.borrow()
|
|
1544
|
+
.as_ref()
|
|
1545
|
+
.is_some_and(|p| p.name.as_ref() == name);
|
|
1546
|
+
if hit {
|
|
1547
|
+
*self.string_builder.pending.borrow_mut() = None;
|
|
1548
|
+
self.string_builder.active.set(false);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/// Try to handle `name += rhs` as an amortized-O(1) string append. Returns `true` if it was a
|
|
1553
|
+
/// string append (buffer updated); `false` if `name` is not a (mutable) string accumulator, so
|
|
1554
|
+
/// the caller falls back to the generic `+=`. Any builder for a DIFFERENT slot is flushed first;
|
|
1555
|
+
/// keying by the exact owning scope means a shadowing inner `name` never appends onto an outer
|
|
1556
|
+
/// `name`'s buffer.
|
|
1557
|
+
fn try_string_append(&self, name: &Arc<str>, rhs: &Value) -> bool {
|
|
1558
|
+
let owner = match self.find_var_scope(name) {
|
|
1559
|
+
Some(s) => s,
|
|
1560
|
+
None => return false, // undefined → let the generic path raise the error
|
|
1561
|
+
};
|
|
1562
|
+
// Continue an existing builder for this exact slot.
|
|
1563
|
+
if self.string_builder.active.get() {
|
|
1564
|
+
let same = self
|
|
1565
|
+
.string_builder
|
|
1566
|
+
.pending
|
|
1567
|
+
.borrow()
|
|
1568
|
+
.as_ref()
|
|
1569
|
+
.is_some_and(|p| p.name == *name && Rc::ptr_eq(&p.scope, &owner));
|
|
1570
|
+
if same {
|
|
1571
|
+
if let Some(p) = self.string_builder.pending.borrow_mut().as_mut() {
|
|
1572
|
+
Self::append_value_to_buf(&mut p.buf, rhs);
|
|
1573
|
+
}
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
// A different slot is buffered — flush it before starting a new one.
|
|
1577
|
+
self.flush_pending();
|
|
1578
|
+
}
|
|
1579
|
+
// Start a new builder only if the accumulator currently holds a mutable string.
|
|
1580
|
+
let start = {
|
|
1581
|
+
let owner_ref = owner.borrow();
|
|
1582
|
+
if owner_ref.consts.contains(name.as_ref()) {
|
|
1583
|
+
return false; // `const acc += x` must raise the same error as the generic path
|
|
1584
|
+
}
|
|
1585
|
+
match owner_ref.vars.get(name.as_ref()) {
|
|
1586
|
+
Some(Value::String(a)) => Some(a.clone()),
|
|
1587
|
+
_ => None, // non-string accumulator → numeric/other `+=`
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
match start {
|
|
1591
|
+
Some(a) => {
|
|
1592
|
+
let mut buf = String::with_capacity(a.len() + 16);
|
|
1593
|
+
buf.push_str(&a);
|
|
1594
|
+
Self::append_value_to_buf(&mut buf, rhs);
|
|
1595
|
+
*self.string_builder.pending.borrow_mut() = Some(PendingAppend {
|
|
1596
|
+
scope: owner,
|
|
1597
|
+
name: Arc::clone(name),
|
|
1598
|
+
buf,
|
|
1599
|
+
});
|
|
1600
|
+
self.string_builder.active.set(true);
|
|
1601
|
+
true
|
|
1602
|
+
}
|
|
1603
|
+
None => false,
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/// Evaluate `expr` in statement position. Special-cases `acc += rhs` so the string-builder never
|
|
1608
|
+
/// has to materialize the assignment's result value — the key to keeping the append O(1). In that
|
|
1609
|
+
/// one case the (discarded) statement value is `null`; every other expression returns its real
|
|
1610
|
+
/// value, preserving block/program last-value semantics.
|
|
1611
|
+
fn eval_expr_discard(&self, expr: &Expr) -> Result<Value, EvalError> {
|
|
1612
|
+
if let Expr::CompoundAssign {
|
|
1613
|
+
name,
|
|
1614
|
+
op: tishlang_ast::CompoundOp::Add,
|
|
1615
|
+
value,
|
|
1616
|
+
..
|
|
1617
|
+
} = expr
|
|
1618
|
+
{
|
|
1619
|
+
// Evaluate rhs FIRST — it may read `name`, which flushes any active builder.
|
|
1620
|
+
let rhs = self.eval_expr(value)?;
|
|
1621
|
+
if self.try_string_append(name, &rhs) {
|
|
1622
|
+
// Statement-position result is discarded; returning null avoids the O(n) flatten.
|
|
1623
|
+
return Ok(Value::Null);
|
|
1624
|
+
}
|
|
1625
|
+
// Not a string accumulator: generic `+=` (numeric/other) — return its real value.
|
|
1626
|
+
self.flush_pending_for(name);
|
|
1627
|
+
let current = self
|
|
1628
|
+
.scope
|
|
1629
|
+
.borrow()
|
|
1630
|
+
.get(name.as_ref())
|
|
1631
|
+
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
1632
|
+
let result = self
|
|
1633
|
+
.eval_binop(¤t, BinOp::Add, &rhs)
|
|
1634
|
+
.map_err(EvalError::Error)?;
|
|
1635
|
+
match self.scope.borrow_mut().assign(name.as_ref(), result.clone()) {
|
|
1636
|
+
Ok(true) => Ok(result),
|
|
1637
|
+
Ok(false) => Err(EvalError::Error(format!("Undefined variable: {}", name))),
|
|
1638
|
+
Err(e) => Err(EvalError::Error(e)),
|
|
1639
|
+
}
|
|
1640
|
+
} else {
|
|
1641
|
+
self.eval_expr(expr)
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1118
1645
|
fn eval_expr(&self, expr: &Expr) -> Result<Value, EvalError> {
|
|
1119
1646
|
match expr {
|
|
1120
1647
|
Expr::Literal { value, .. } => Ok(match value {
|
|
@@ -1123,11 +1650,14 @@ impl Evaluator {
|
|
|
1123
1650
|
Literal::Bool(b) => Value::Bool(*b),
|
|
1124
1651
|
Literal::Null => Value::Null,
|
|
1125
1652
|
}),
|
|
1126
|
-
Expr::Ident { name, .. } =>
|
|
1127
|
-
.
|
|
1128
|
-
.
|
|
1129
|
-
.
|
|
1130
|
-
|
|
1653
|
+
Expr::Ident { name, .. } => {
|
|
1654
|
+
// Flush any string-builder buffering this variable so the read sees the full string.
|
|
1655
|
+
self.flush_pending_for(name.as_ref());
|
|
1656
|
+
self.scope
|
|
1657
|
+
.borrow()
|
|
1658
|
+
.get(name.as_ref())
|
|
1659
|
+
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))
|
|
1660
|
+
}
|
|
1131
1661
|
Expr::Binary {
|
|
1132
1662
|
left,
|
|
1133
1663
|
op,
|
|
@@ -1176,7 +1706,58 @@ impl Evaluator {
|
|
|
1176
1706
|
{
|
|
1177
1707
|
let obj = self.eval_expr(object)?;
|
|
1178
1708
|
let arg_vals = self.eval_call_args(args)?;
|
|
1179
|
-
|
|
1709
|
+
|
|
1710
|
+
// `Array.from(source, mapFn?)` — handled inline so the mapFn callback runs with the
|
|
1711
|
+
// interpreter context (a plain native has no `self`). Covers array / string / Set /
|
|
1712
|
+
// Map / iterator (via `__drain__`) and array-like `{ length }`. (#440)
|
|
1713
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
1714
|
+
if name.as_ref() == "Array" && method_name.as_ref() == "from" {
|
|
1715
|
+
let source = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1716
|
+
let items: Vec<Value> = match &source {
|
|
1717
|
+
Value::Array(a) => a.borrow().clone(),
|
|
1718
|
+
Value::String(s) => s
|
|
1719
|
+
.chars()
|
|
1720
|
+
.map(|c| Value::String(std::sync::Arc::from(c.to_string())))
|
|
1721
|
+
.collect(),
|
|
1722
|
+
_ => {
|
|
1723
|
+
if let Some(d) = self.drain_eval_iterator(&source) {
|
|
1724
|
+
d
|
|
1725
|
+
} else if matches!(&source, Value::Object(_)) {
|
|
1726
|
+
let len = self
|
|
1727
|
+
.get_prop(&source, "length")
|
|
1728
|
+
.ok()
|
|
1729
|
+
.and_then(|v| match v {
|
|
1730
|
+
Value::Number(n) if n.is_finite() && n >= 0.0 => {
|
|
1731
|
+
Some(n as usize)
|
|
1732
|
+
}
|
|
1733
|
+
_ => None,
|
|
1734
|
+
})
|
|
1735
|
+
.unwrap_or(0);
|
|
1736
|
+
(0..len)
|
|
1737
|
+
.map(|i| {
|
|
1738
|
+
self.get_prop(&source, &i.to_string())
|
|
1739
|
+
.unwrap_or(Value::Null)
|
|
1740
|
+
})
|
|
1741
|
+
.collect()
|
|
1742
|
+
} else {
|
|
1743
|
+
Vec::new()
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
let out = match arg_vals.get(1) {
|
|
1748
|
+
Some(f) if !matches!(f, Value::Null) => {
|
|
1749
|
+
let mut o = Vec::with_capacity(items.len());
|
|
1750
|
+
for (i, item) in items.into_iter().enumerate() {
|
|
1751
|
+
o.push(self.call_func(f, &[item, Value::Number(i as f64)])?);
|
|
1752
|
+
}
|
|
1753
|
+
o
|
|
1754
|
+
}
|
|
1755
|
+
_ => items,
|
|
1756
|
+
};
|
|
1757
|
+
return Ok(Value::array(out));
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1180
1761
|
// Array methods
|
|
1181
1762
|
if let Value::Array(arr) = &obj {
|
|
1182
1763
|
match method_name.as_ref() {
|
|
@@ -1207,13 +1788,66 @@ impl Evaluator {
|
|
|
1207
1788
|
"indexOf" => {
|
|
1208
1789
|
let search = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1209
1790
|
let arr_borrow = arr.borrow();
|
|
1210
|
-
|
|
1791
|
+
// fromIndex: positive clamps to [0, len]; negative counts from the end.
|
|
1792
|
+
let len = arr_borrow.len() as i64;
|
|
1793
|
+
let start = match arg_vals.get(1) {
|
|
1794
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
|
|
1795
|
+
Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
|
|
1796
|
+
_ => 0,
|
|
1797
|
+
};
|
|
1798
|
+
for (i, v) in arr_borrow.iter().enumerate().skip(start) {
|
|
1211
1799
|
if v.strict_eq(&search) {
|
|
1212
1800
|
return Ok(Value::Number(i as f64));
|
|
1213
1801
|
}
|
|
1214
1802
|
}
|
|
1215
1803
|
return Ok(Value::Number(-1.0));
|
|
1216
1804
|
}
|
|
1805
|
+
"lastIndexOf" => {
|
|
1806
|
+
let search = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1807
|
+
let arr_borrow = arr.borrow();
|
|
1808
|
+
let len = arr_borrow.len() as i64;
|
|
1809
|
+
let start = match arg_vals.get(1) {
|
|
1810
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len - 1),
|
|
1811
|
+
Some(Value::Number(n)) => len + *n as i64,
|
|
1812
|
+
_ => len - 1,
|
|
1813
|
+
};
|
|
1814
|
+
let mut i = start;
|
|
1815
|
+
while i >= 0 {
|
|
1816
|
+
if let Some(v) = arr_borrow.get(i as usize) {
|
|
1817
|
+
if v.strict_eq(&search) {
|
|
1818
|
+
return Ok(Value::Number(i as f64));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
i -= 1;
|
|
1822
|
+
}
|
|
1823
|
+
return Ok(Value::Number(-1.0));
|
|
1824
|
+
}
|
|
1825
|
+
"copyWithin" => {
|
|
1826
|
+
let mut v = arr.borrow_mut();
|
|
1827
|
+
let len = v.len() as i64;
|
|
1828
|
+
let norm = |x: Option<&Value>, dflt: i64| -> usize {
|
|
1829
|
+
match x {
|
|
1830
|
+
Some(Value::Number(n)) => {
|
|
1831
|
+
let n = *n as i64;
|
|
1832
|
+
(if n < 0 { len + n } else { n }).clamp(0, len) as usize
|
|
1833
|
+
}
|
|
1834
|
+
_ => dflt.clamp(0, len) as usize,
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
let t = norm(arg_vals.first(), 0);
|
|
1838
|
+
let s = norm(arg_vals.get(1), 0);
|
|
1839
|
+
let e = norm(arg_vals.get(2), len);
|
|
1840
|
+
let lu = len as usize;
|
|
1841
|
+
if s < e && t < lu {
|
|
1842
|
+
let count = (e - s).min(lu - t);
|
|
1843
|
+
let block: Vec<Value> = v[s..s + count].to_vec();
|
|
1844
|
+
for (k, val) in block.into_iter().enumerate() {
|
|
1845
|
+
v[t + k] = val;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
drop(v);
|
|
1849
|
+
return Ok(Value::Array(arr.clone()));
|
|
1850
|
+
}
|
|
1217
1851
|
"includes" => {
|
|
1218
1852
|
let search = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1219
1853
|
let arr_borrow = arr.borrow();
|
|
@@ -1255,6 +1889,91 @@ impl Evaluator {
|
|
|
1255
1889
|
arr.borrow_mut().reverse();
|
|
1256
1890
|
return Ok(obj.clone());
|
|
1257
1891
|
}
|
|
1892
|
+
"toReversed" => {
|
|
1893
|
+
// ES2023: a reversed COPY; the receiver is untouched.
|
|
1894
|
+
let mut v = arr.borrow().clone();
|
|
1895
|
+
v.reverse();
|
|
1896
|
+
return Ok(Value::Array(Rc::new(RefCell::new(v))));
|
|
1897
|
+
}
|
|
1898
|
+
"toSorted" => {
|
|
1899
|
+
// ES2023: a sorted COPY. Uses the general comparator/default path (the
|
|
1900
|
+
// numeric fast-paths in `sort` are speed-only and give identical order).
|
|
1901
|
+
let comparator = arg_vals.into_iter().next();
|
|
1902
|
+
let arr_values: Vec<Value> = arr.borrow().clone();
|
|
1903
|
+
let len = arr_values.len();
|
|
1904
|
+
let mut indices: Vec<usize> = (0..len).collect();
|
|
1905
|
+
if let Some(cmp_fn) = comparator {
|
|
1906
|
+
let mut pending: Option<EvalError> = None;
|
|
1907
|
+
indices.sort_by(|&i, &j| {
|
|
1908
|
+
if pending.is_some() {
|
|
1909
|
+
return std::cmp::Ordering::Equal;
|
|
1910
|
+
}
|
|
1911
|
+
match self.call_func(&cmp_fn, &[arr_values[i].clone(), arr_values[j].clone()]) {
|
|
1912
|
+
Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
|
|
1913
|
+
Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
|
|
1914
|
+
Ok(_) => std::cmp::Ordering::Equal,
|
|
1915
|
+
Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
if let Some(e) = pending {
|
|
1919
|
+
return Err(e);
|
|
1920
|
+
}
|
|
1921
|
+
} else {
|
|
1922
|
+
indices.sort_by(|&i, &j| {
|
|
1923
|
+
arr_values[i].to_string().cmp(&arr_values[j].to_string())
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
let sorted: Vec<Value> =
|
|
1927
|
+
indices.into_iter().map(|i| arr_values[i].clone()).collect();
|
|
1928
|
+
return Ok(Value::Array(Rc::new(RefCell::new(sorted))));
|
|
1929
|
+
}
|
|
1930
|
+
"with" => {
|
|
1931
|
+
// ES2023: a COPY with one index replaced; negative index counts from the
|
|
1932
|
+
// end; out-of-range throws a catchable RangeError (matches vm/native/node).
|
|
1933
|
+
let index = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1934
|
+
let value = arg_vals.get(1).cloned().unwrap_or(Value::Null);
|
|
1935
|
+
let mut v = arr.borrow().clone();
|
|
1936
|
+
let len = v.len() as i64;
|
|
1937
|
+
let rel = match index {
|
|
1938
|
+
Value::Number(n) if n.is_finite() => n as i64,
|
|
1939
|
+
_ => 0,
|
|
1940
|
+
};
|
|
1941
|
+
let actual = if rel >= 0 { rel } else { len + rel };
|
|
1942
|
+
if actual < 0 || actual >= len {
|
|
1943
|
+
let err = crate::natives::range_error_construct(&[Value::String(
|
|
1944
|
+
format!("Invalid index : {}", rel).into(),
|
|
1945
|
+
)])
|
|
1946
|
+
.unwrap_or(Value::Null);
|
|
1947
|
+
return Err(EvalError::Throw(err));
|
|
1948
|
+
}
|
|
1949
|
+
v[actual as usize] = value;
|
|
1950
|
+
return Ok(Value::Array(Rc::new(RefCell::new(v))));
|
|
1951
|
+
}
|
|
1952
|
+
"toSpliced" => {
|
|
1953
|
+
// ES2023: a COPY with the splice applied; returns the resulting array
|
|
1954
|
+
// (splice returns the removed elements). Same normalization as `splice`.
|
|
1955
|
+
let mut result = arr.borrow().clone();
|
|
1956
|
+
let len = result.len() as i64;
|
|
1957
|
+
let start = match arg_vals.first() {
|
|
1958
|
+
Some(Value::Number(n)) => {
|
|
1959
|
+
let n = *n as i64;
|
|
1960
|
+
if n < 0 { (len + n).max(0) as usize } else { n.min(len) as usize }
|
|
1961
|
+
}
|
|
1962
|
+
_ => 0,
|
|
1963
|
+
};
|
|
1964
|
+
let delete_count = match arg_vals.get(1) {
|
|
1965
|
+
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
1966
|
+
_ => (len as usize).saturating_sub(start),
|
|
1967
|
+
};
|
|
1968
|
+
let actual_delete = delete_count.min(result.len().saturating_sub(start));
|
|
1969
|
+
result.drain(start..start + actual_delete);
|
|
1970
|
+
if arg_vals.len() > 2 {
|
|
1971
|
+
for (i, item) in arg_vals[2..].iter().cloned().enumerate() {
|
|
1972
|
+
result.insert(start + i, item);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return Ok(Value::Array(Rc::new(RefCell::new(result))));
|
|
1976
|
+
}
|
|
1258
1977
|
"fill" => {
|
|
1259
1978
|
// Array.prototype.fill(value, start?, end?) — in place (issue #76).
|
|
1260
1979
|
let value = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
@@ -1313,27 +2032,45 @@ impl Evaluator {
|
|
|
1313
2032
|
let len = arr_mut.len();
|
|
1314
2033
|
let mut indices: Vec<usize> = (0..len).collect();
|
|
1315
2034
|
let arr_values: Vec<Value> = std::mem::take(&mut *arr_mut);
|
|
2035
|
+
// A `throw` from the comparator must propagate (be catchable) and
|
|
2036
|
+
// must NOT corrupt the array. Previously the `_ => Equal` arm
|
|
2037
|
+
// swallowed the `Err` and wrote a bogus reordering back; now we
|
|
2038
|
+
// capture the throw, stop comparing, and restore the original order
|
|
2039
|
+
// — matching the vm/native backends and node.
|
|
2040
|
+
let mut pending: Option<EvalError> = None;
|
|
1316
2041
|
|
|
1317
2042
|
if let Some((scope, params, body)) = self.create_callback_scope(&cmp_fn) {
|
|
1318
2043
|
indices.sort_by(|&i, &j| {
|
|
1319
|
-
|
|
1320
|
-
|
|
2044
|
+
if pending.is_some() {
|
|
2045
|
+
return std::cmp::Ordering::Equal;
|
|
2046
|
+
}
|
|
2047
|
+
match self.call_with_scope(&scope, ¶ms, &body, &[arr_values[i].clone(), arr_values[j].clone()]) {
|
|
1321
2048
|
Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
|
|
1322
2049
|
Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
|
|
1323
|
-
_ => std::cmp::Ordering::Equal,
|
|
2050
|
+
Ok(_) => std::cmp::Ordering::Equal,
|
|
2051
|
+
Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
|
|
1324
2052
|
}
|
|
1325
2053
|
});
|
|
1326
2054
|
} else {
|
|
1327
2055
|
indices.sort_by(|&i, &j| {
|
|
1328
|
-
|
|
1329
|
-
|
|
2056
|
+
if pending.is_some() {
|
|
2057
|
+
return std::cmp::Ordering::Equal;
|
|
2058
|
+
}
|
|
2059
|
+
match self.call_func(&cmp_fn, &[arr_values[i].clone(), arr_values[j].clone()]) {
|
|
1330
2060
|
Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
|
|
1331
2061
|
Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
|
|
1332
|
-
_ => std::cmp::Ordering::Equal,
|
|
2062
|
+
Ok(_) => std::cmp::Ordering::Equal,
|
|
2063
|
+
Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
|
|
1333
2064
|
}
|
|
1334
2065
|
});
|
|
1335
2066
|
}
|
|
1336
2067
|
|
|
2068
|
+
if let Some(e) = pending {
|
|
2069
|
+
// Comparator threw: leave the array untouched and re-raise.
|
|
2070
|
+
*arr_mut = arr_values;
|
|
2071
|
+
drop(arr_mut);
|
|
2072
|
+
return Err(e);
|
|
2073
|
+
}
|
|
1337
2074
|
*arr_mut = indices.into_iter().map(|i| arr_values[i].clone()).collect();
|
|
1338
2075
|
}
|
|
1339
2076
|
} else {
|
|
@@ -1416,7 +2153,11 @@ impl Evaluator {
|
|
|
1416
2153
|
}
|
|
1417
2154
|
"map" => {
|
|
1418
2155
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1419
|
-
|
|
2156
|
+
// #382-style snapshot: own the elements and DROP the array borrow before
|
|
2157
|
+
// any callback runs, so a callback that mutates the array (via the `array`
|
|
2158
|
+
// arg or a capture) can't RefCell-panic. `arr_value` is the JS 3rd arg.
|
|
2159
|
+
let arr_borrow = arr.borrow().clone();
|
|
2160
|
+
let arr_value = Value::Array(arr.clone());
|
|
1420
2161
|
let mut result = Vec::with_capacity(arr_borrow.len());
|
|
1421
2162
|
// Try fastest path: simple single-expression callbacks
|
|
1422
2163
|
let first_result = self.eval_simple_callback(&callback, &[arr_borrow.first().cloned().unwrap_or(Value::Null)]);
|
|
@@ -1433,21 +2174,49 @@ impl Evaluator {
|
|
|
1433
2174
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1434
2175
|
// Reusable scope path
|
|
1435
2176
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1436
|
-
let mapped = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2177
|
+
let mapped = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1437
2178
|
result.push(mapped);
|
|
1438
2179
|
}
|
|
1439
2180
|
} else {
|
|
1440
2181
|
// Full call_func path
|
|
1441
2182
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1442
|
-
let mapped = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2183
|
+
let mapped = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1443
2184
|
result.push(mapped);
|
|
1444
2185
|
}
|
|
1445
2186
|
}
|
|
1446
2187
|
return Ok(Value::Array(Rc::new(RefCell::new(result))));
|
|
1447
2188
|
}
|
|
2189
|
+
"flatMap" => {
|
|
2190
|
+
// map + flatten one level (Array.prototype.flatMap). A callback `throw`
|
|
2191
|
+
// propagates via `?` (catchable), matching vm/native/node — previously
|
|
2192
|
+
// interp lacked flatMap entirely ("Not a function").
|
|
2193
|
+
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
2194
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2195
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
2196
|
+
let mut result: Vec<Value> = Vec::with_capacity(arr_borrow.len());
|
|
2197
|
+
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
2198
|
+
for (i, v) in arr_borrow.iter().enumerate() {
|
|
2199
|
+
let mapped = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
2200
|
+
match mapped {
|
|
2201
|
+
Value::Array(inner) => result.extend(inner.borrow().iter().cloned()),
|
|
2202
|
+
other => result.push(other),
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
} else {
|
|
2206
|
+
for (i, v) in arr_borrow.iter().enumerate() {
|
|
2207
|
+
let mapped = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
2208
|
+
match mapped {
|
|
2209
|
+
Value::Array(inner) => result.extend(inner.borrow().iter().cloned()),
|
|
2210
|
+
other => result.push(other),
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return Ok(Value::Array(Rc::new(RefCell::new(result))));
|
|
2215
|
+
}
|
|
1448
2216
|
"filter" => {
|
|
1449
2217
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1450
|
-
let arr_borrow = arr.borrow();
|
|
2218
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2219
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1451
2220
|
let mut result = Vec::new();
|
|
1452
2221
|
// Try simple callback fast path
|
|
1453
2222
|
let use_simple = arr_borrow.first().map(|v| {
|
|
@@ -1463,14 +2232,14 @@ impl Evaluator {
|
|
|
1463
2232
|
}
|
|
1464
2233
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1465
2234
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1466
|
-
let keep = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2235
|
+
let keep = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1467
2236
|
if keep.is_truthy() {
|
|
1468
2237
|
result.push(v.clone());
|
|
1469
2238
|
}
|
|
1470
2239
|
}
|
|
1471
2240
|
} else {
|
|
1472
2241
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1473
|
-
let keep = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2242
|
+
let keep = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1474
2243
|
if keep.is_truthy() {
|
|
1475
2244
|
result.push(v.clone());
|
|
1476
2245
|
}
|
|
@@ -1480,7 +2249,8 @@ impl Evaluator {
|
|
|
1480
2249
|
}
|
|
1481
2250
|
"reduce" => {
|
|
1482
2251
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1483
|
-
let arr_borrow = arr.borrow();
|
|
2252
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2253
|
+
let arr_value = Value::Array(arr.clone()); // JS 4th callback arg
|
|
1484
2254
|
let (mut acc, start_idx) = if arg_vals.len() > 1 {
|
|
1485
2255
|
(arg_vals[1].clone(), 0)
|
|
1486
2256
|
} else if !arr_borrow.is_empty() {
|
|
@@ -1490,18 +2260,75 @@ impl Evaluator {
|
|
|
1490
2260
|
};
|
|
1491
2261
|
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1492
2262
|
for (i, v) in arr_borrow.iter().enumerate().skip(start_idx) {
|
|
1493
|
-
acc = self.call_with_scope(&scope, ¶ms, &body, &[acc, v.clone(), Value::Number(i as f64)])?;
|
|
2263
|
+
acc = self.call_with_scope(&scope, ¶ms, &body, &[acc, v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1494
2264
|
}
|
|
1495
2265
|
} else {
|
|
1496
2266
|
for (i, v) in arr_borrow.iter().enumerate().skip(start_idx) {
|
|
1497
|
-
acc = self.call_func(&callback, &[acc, v.clone(), Value::Number(i as f64)])?;
|
|
2267
|
+
acc = self.call_func(&callback, &[acc, v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1498
2268
|
}
|
|
1499
2269
|
}
|
|
1500
2270
|
return Ok(acc);
|
|
1501
2271
|
}
|
|
2272
|
+
"reduceRight" => {
|
|
2273
|
+
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
2274
|
+
let arr_borrow = arr.borrow().clone();
|
|
2275
|
+
let arr_value = Value::Array(arr.clone());
|
|
2276
|
+
let len = arr_borrow.len();
|
|
2277
|
+
// With an initial value, fold indices len-1..0; without, seed from the
|
|
2278
|
+
// last element and fold len-2..0. Empty + no init → TypeError.
|
|
2279
|
+
let (mut acc, start) = if arg_vals.len() > 1 {
|
|
2280
|
+
(arg_vals[1].clone(), len)
|
|
2281
|
+
} else if len > 0 {
|
|
2282
|
+
(arr_borrow[len - 1].clone(), len - 1)
|
|
2283
|
+
} else {
|
|
2284
|
+
return Err(EvalError::Error("Reduce of empty array with no initial value".to_string()));
|
|
2285
|
+
};
|
|
2286
|
+
let mut i = start;
|
|
2287
|
+
while i > 0 {
|
|
2288
|
+
i -= 1;
|
|
2289
|
+
acc = self.call_func(&callback, &[acc, arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
2290
|
+
}
|
|
2291
|
+
return Ok(acc);
|
|
2292
|
+
}
|
|
2293
|
+
"keys" => {
|
|
2294
|
+
let len = arr.borrow().len();
|
|
2295
|
+
let it = tishlang_builtins::iterator::array_iterator(
|
|
2296
|
+
(0..len).map(|i| tishlang_core::Value::Number(i as f64)).collect(),
|
|
2297
|
+
);
|
|
2298
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2299
|
+
}
|
|
2300
|
+
"values" => {
|
|
2301
|
+
let items: Vec<tishlang_core::Value> = arr
|
|
2302
|
+
.borrow()
|
|
2303
|
+
.iter()
|
|
2304
|
+
.map(|v| {
|
|
2305
|
+
crate::value_convert::eval_to_core(v)
|
|
2306
|
+
.unwrap_or(tishlang_core::Value::Null)
|
|
2307
|
+
})
|
|
2308
|
+
.collect();
|
|
2309
|
+
let it = tishlang_builtins::iterator::array_iterator(items);
|
|
2310
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2311
|
+
}
|
|
2312
|
+
"entries" => {
|
|
2313
|
+
let items: Vec<tishlang_core::Value> = arr
|
|
2314
|
+
.borrow()
|
|
2315
|
+
.iter()
|
|
2316
|
+
.enumerate()
|
|
2317
|
+
.map(|(i, v)| {
|
|
2318
|
+
tishlang_core::Value::Array(tishlang_core::VmRef::new(vec![
|
|
2319
|
+
tishlang_core::Value::Number(i as f64),
|
|
2320
|
+
crate::value_convert::eval_to_core(v)
|
|
2321
|
+
.unwrap_or(tishlang_core::Value::Null),
|
|
2322
|
+
]))
|
|
2323
|
+
})
|
|
2324
|
+
.collect();
|
|
2325
|
+
let it = tishlang_builtins::iterator::array_iterator(items);
|
|
2326
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2327
|
+
}
|
|
1502
2328
|
"find" => {
|
|
1503
2329
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1504
|
-
let arr_borrow = arr.borrow();
|
|
2330
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2331
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1505
2332
|
// Try simple callback fast path
|
|
1506
2333
|
let use_simple = arr_borrow.first().map(|v| {
|
|
1507
2334
|
self.eval_simple_callback(&callback, &[v.clone()]).is_some()
|
|
@@ -1516,14 +2343,14 @@ impl Evaluator {
|
|
|
1516
2343
|
}
|
|
1517
2344
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1518
2345
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1519
|
-
let found = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2346
|
+
let found = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1520
2347
|
if found.is_truthy() {
|
|
1521
2348
|
return Ok(v.clone());
|
|
1522
2349
|
}
|
|
1523
2350
|
}
|
|
1524
2351
|
} else {
|
|
1525
2352
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1526
|
-
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2353
|
+
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1527
2354
|
if found.is_truthy() {
|
|
1528
2355
|
return Ok(v.clone());
|
|
1529
2356
|
}
|
|
@@ -1533,17 +2360,18 @@ impl Evaluator {
|
|
|
1533
2360
|
}
|
|
1534
2361
|
"findIndex" => {
|
|
1535
2362
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1536
|
-
let arr_borrow = arr.borrow();
|
|
2363
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2364
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1537
2365
|
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1538
2366
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1539
|
-
let found = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2367
|
+
let found = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1540
2368
|
if found.is_truthy() {
|
|
1541
2369
|
return Ok(Value::Number(i as f64));
|
|
1542
2370
|
}
|
|
1543
2371
|
}
|
|
1544
2372
|
} else {
|
|
1545
2373
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1546
|
-
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2374
|
+
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1547
2375
|
if found.is_truthy() {
|
|
1548
2376
|
return Ok(Value::Number(i as f64));
|
|
1549
2377
|
}
|
|
@@ -1552,13 +2380,14 @@ impl Evaluator {
|
|
|
1552
2380
|
return Ok(Value::Number(-1.0));
|
|
1553
2381
|
}
|
|
1554
2382
|
"findLast" => {
|
|
1555
|
-
// Like find, from the end (#247). Callback gets (value, original index).
|
|
2383
|
+
// Like find, from the end (#247). Callback gets (value, original index, array).
|
|
1556
2384
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1557
|
-
let arr_borrow = arr.borrow();
|
|
2385
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2386
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1558
2387
|
let scoped = self.create_callback_scope(&callback);
|
|
1559
2388
|
for i in (0..arr_borrow.len()).rev() {
|
|
1560
2389
|
let v = arr_borrow[i].clone();
|
|
1561
|
-
let args = [v.clone(), Value::Number(i as f64)];
|
|
2390
|
+
let args = [v.clone(), Value::Number(i as f64), arr_value.clone()];
|
|
1562
2391
|
let found = match &scoped {
|
|
1563
2392
|
Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
|
|
1564
2393
|
None => self.call_func(&callback, &args)?,
|
|
@@ -1571,10 +2400,11 @@ impl Evaluator {
|
|
|
1571
2400
|
}
|
|
1572
2401
|
"findLastIndex" => {
|
|
1573
2402
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1574
|
-
let arr_borrow = arr.borrow();
|
|
2403
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2404
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1575
2405
|
let scoped = self.create_callback_scope(&callback);
|
|
1576
2406
|
for i in (0..arr_borrow.len()).rev() {
|
|
1577
|
-
let args = [arr_borrow[i].clone(), Value::Number(i as f64)];
|
|
2407
|
+
let args = [arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()];
|
|
1578
2408
|
let found = match &scoped {
|
|
1579
2409
|
Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
|
|
1580
2410
|
None => self.call_func(&callback, &args)?,
|
|
@@ -1601,21 +2431,23 @@ impl Evaluator {
|
|
|
1601
2431
|
}
|
|
1602
2432
|
"forEach" => {
|
|
1603
2433
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1604
|
-
let arr_borrow = arr.borrow();
|
|
2434
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2435
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1605
2436
|
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1606
2437
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1607
|
-
self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2438
|
+
self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1608
2439
|
}
|
|
1609
2440
|
} else {
|
|
1610
2441
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1611
|
-
self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2442
|
+
self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1612
2443
|
}
|
|
1613
2444
|
}
|
|
1614
2445
|
return Ok(Value::Null);
|
|
1615
2446
|
}
|
|
1616
2447
|
"some" => {
|
|
1617
2448
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1618
|
-
let arr_borrow = arr.borrow();
|
|
2449
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2450
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1619
2451
|
// Try simple callback fast path
|
|
1620
2452
|
let use_simple = arr_borrow.first().map(|v| {
|
|
1621
2453
|
self.eval_simple_callback(&callback, &[v.clone()]).is_some()
|
|
@@ -1630,14 +2462,14 @@ impl Evaluator {
|
|
|
1630
2462
|
}
|
|
1631
2463
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1632
2464
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1633
|
-
let result = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2465
|
+
let result = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1634
2466
|
if result.is_truthy() {
|
|
1635
2467
|
return Ok(Value::Bool(true));
|
|
1636
2468
|
}
|
|
1637
2469
|
}
|
|
1638
2470
|
} else {
|
|
1639
2471
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1640
|
-
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2472
|
+
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1641
2473
|
if result.is_truthy() {
|
|
1642
2474
|
return Ok(Value::Bool(true));
|
|
1643
2475
|
}
|
|
@@ -1647,7 +2479,8 @@ impl Evaluator {
|
|
|
1647
2479
|
}
|
|
1648
2480
|
"every" => {
|
|
1649
2481
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1650
|
-
let arr_borrow = arr.borrow();
|
|
2482
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2483
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1651
2484
|
// Try simple callback fast path
|
|
1652
2485
|
let use_simple = arr_borrow.first().map(|v| {
|
|
1653
2486
|
self.eval_simple_callback(&callback, &[v.clone()]).is_some()
|
|
@@ -1662,14 +2495,14 @@ impl Evaluator {
|
|
|
1662
2495
|
}
|
|
1663
2496
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1664
2497
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1665
|
-
let result = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2498
|
+
let result = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1666
2499
|
if !result.is_truthy() {
|
|
1667
2500
|
return Ok(Value::Bool(false));
|
|
1668
2501
|
}
|
|
1669
2502
|
}
|
|
1670
2503
|
} else {
|
|
1671
2504
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1672
|
-
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2505
|
+
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1673
2506
|
if !result.is_truthy() {
|
|
1674
2507
|
return Ok(Value::Bool(false));
|
|
1675
2508
|
}
|
|
@@ -1807,6 +2640,32 @@ impl Evaluator {
|
|
|
1807
2640
|
"trim" => {
|
|
1808
2641
|
return Ok(Value::String(s.trim().into()));
|
|
1809
2642
|
}
|
|
2643
|
+
"trimStart" => {
|
|
2644
|
+
return Ok(Value::String(s.trim_start().into()));
|
|
2645
|
+
}
|
|
2646
|
+
"trimEnd" => {
|
|
2647
|
+
return Ok(Value::String(s.trim_end().into()));
|
|
2648
|
+
}
|
|
2649
|
+
"normalize" => {
|
|
2650
|
+
let form = match arg_vals.first() {
|
|
2651
|
+
None | Some(Value::Null) => "NFC".to_string(),
|
|
2652
|
+
Some(Value::String(f)) => f.to_string(),
|
|
2653
|
+
Some(v) => v.to_string(),
|
|
2654
|
+
};
|
|
2655
|
+
match tishlang_builtins::string::normalize_form(s, &form) {
|
|
2656
|
+
Some(out) => return Ok(Value::String(out.into())),
|
|
2657
|
+
None => {
|
|
2658
|
+
let err = crate::natives::range_error_construct(&[
|
|
2659
|
+
Value::String(
|
|
2660
|
+
"The normalization form should be one of NFC, NFD, NFKC, NFKD."
|
|
2661
|
+
.into(),
|
|
2662
|
+
),
|
|
2663
|
+
])
|
|
2664
|
+
.unwrap_or(Value::Null);
|
|
2665
|
+
return Err(EvalError::Throw(err));
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
1810
2669
|
"toUpperCase" => {
|
|
1811
2670
|
return Ok(Value::String(s.to_uppercase().into()));
|
|
1812
2671
|
}
|
|
@@ -1818,14 +2677,28 @@ impl Evaluator {
|
|
|
1818
2677
|
Some(Value::String(ss)) => ss.as_ref(),
|
|
1819
2678
|
_ => return Ok(Value::Bool(false)),
|
|
1820
2679
|
};
|
|
1821
|
-
|
|
2680
|
+
// position: test as if the string began at char `position`.
|
|
2681
|
+
let pos = match arg_vals.get(1) {
|
|
2682
|
+
Some(Value::Number(n)) if *n > 0.0 => *n as usize,
|
|
2683
|
+
_ => 0,
|
|
2684
|
+
};
|
|
2685
|
+
let byte_start = s.char_indices().nth(pos).map(|(b, _)| b).unwrap_or(s.len());
|
|
2686
|
+
return Ok(Value::Bool(s[byte_start..].starts_with(search)));
|
|
1822
2687
|
}
|
|
1823
2688
|
"endsWith" => {
|
|
1824
2689
|
let search = match arg_vals.first() {
|
|
1825
2690
|
Some(Value::String(ss)) => ss.as_ref(),
|
|
1826
2691
|
_ => return Ok(Value::Bool(false)),
|
|
1827
2692
|
};
|
|
1828
|
-
|
|
2693
|
+
// endPosition: test as if the string ended at char `endPosition`.
|
|
2694
|
+
let char_count = s.chars().count();
|
|
2695
|
+
let end = match arg_vals.get(1) {
|
|
2696
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(char_count),
|
|
2697
|
+
Some(Value::Number(_)) => 0,
|
|
2698
|
+
_ => char_count,
|
|
2699
|
+
};
|
|
2700
|
+
let byte_end = s.char_indices().nth(end).map(|(b, _)| b).unwrap_or(s.len());
|
|
2701
|
+
return Ok(Value::Bool(s[..byte_end].ends_with(search)));
|
|
1829
2702
|
}
|
|
1830
2703
|
"replace" => {
|
|
1831
2704
|
#[cfg(feature = "regex")]
|
|
@@ -1865,6 +2738,17 @@ impl Evaluator {
|
|
|
1865
2738
|
Some(Value::String(ss)) => ss.to_string(),
|
|
1866
2739
|
_ => String::new(),
|
|
1867
2740
|
};
|
|
2741
|
+
if replacement.contains('$') && !search.is_empty() {
|
|
2742
|
+
let sref = s.as_ref();
|
|
2743
|
+
if let Some(m) = sref.find(&search) {
|
|
2744
|
+
let mut out = String::with_capacity(sref.len());
|
|
2745
|
+
out.push_str(&sref[..m]);
|
|
2746
|
+
out.push_str(&tishlang_builtins::string::expand_replacement(
|
|
2747
|
+
&replacement, &search, &sref[..m], &sref[m + search.len()..]));
|
|
2748
|
+
out.push_str(&sref[m + search.len()..]);
|
|
2749
|
+
return Ok(Value::String(out.into()));
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
1868
2752
|
return Ok(Value::String(s.replacen(&search, &replacement, 1).into()));
|
|
1869
2753
|
}
|
|
1870
2754
|
#[cfg(feature = "regex")]
|
|
@@ -1879,15 +2763,30 @@ impl Evaluator {
|
|
|
1879
2763
|
Some(Value::String(ss)) => ss.to_string(),
|
|
1880
2764
|
_ => String::new(),
|
|
1881
2765
|
};
|
|
2766
|
+
if replacement.contains('$') && !search.is_empty() {
|
|
2767
|
+
let sref = s.as_ref();
|
|
2768
|
+
let mut out = String::with_capacity(sref.len());
|
|
2769
|
+
let (mut last, mut start) = (0usize, 0usize);
|
|
2770
|
+
while let Some(pos) = sref[start..].find(&search) {
|
|
2771
|
+
let m = start + pos;
|
|
2772
|
+
out.push_str(&sref[last..m]);
|
|
2773
|
+
out.push_str(&tishlang_builtins::string::expand_replacement(
|
|
2774
|
+
&replacement, &search, &sref[..m], &sref[m + search.len()..]));
|
|
2775
|
+
last = m + search.len();
|
|
2776
|
+
start = last;
|
|
2777
|
+
}
|
|
2778
|
+
out.push_str(&sref[last..]);
|
|
2779
|
+
return Ok(Value::String(out.into()));
|
|
2780
|
+
}
|
|
1882
2781
|
return Ok(Value::String(s.replace(&search, &replacement).into()));
|
|
1883
2782
|
}
|
|
1884
2783
|
"charAt" => {
|
|
2784
|
+
// Cursor cache instead of collecting a fresh Vec<char> each call (#203).
|
|
1885
2785
|
let idx = match arg_vals.first() {
|
|
1886
2786
|
Some(Value::Number(n)) => *n as usize,
|
|
1887
2787
|
_ => 0,
|
|
1888
2788
|
};
|
|
1889
|
-
|
|
1890
|
-
return Ok(chars.get(idx)
|
|
2789
|
+
return Ok(nth_char_cached(s, idx)
|
|
1891
2790
|
.map(|c| Value::String(c.to_string().into()))
|
|
1892
2791
|
.unwrap_or(Value::String("".into())));
|
|
1893
2792
|
}
|
|
@@ -1897,11 +2796,11 @@ impl Evaluator {
|
|
|
1897
2796
|
Some(Value::Number(n)) => *n as i64,
|
|
1898
2797
|
_ => 0,
|
|
1899
2798
|
};
|
|
1900
|
-
let
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2799
|
+
let idx = if i < 0 { s.chars().count() as i64 + i } else { i };
|
|
2800
|
+
if idx >= 0 {
|
|
2801
|
+
if let Some(c) = nth_char_cached(s, idx as usize) {
|
|
2802
|
+
return Ok(Value::String(c.to_string().into()));
|
|
2803
|
+
}
|
|
1905
2804
|
}
|
|
1906
2805
|
return Ok(Value::Null);
|
|
1907
2806
|
}
|
|
@@ -1910,11 +2809,39 @@ impl Evaluator {
|
|
|
1910
2809
|
Some(Value::Number(n)) => *n as usize,
|
|
1911
2810
|
_ => 0,
|
|
1912
2811
|
};
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
.map(|c| Value::Number(*c as u32 as f64))
|
|
2812
|
+
return Ok(nth_char_cached(s, idx)
|
|
2813
|
+
.map(|c| Value::Number(c as u32 as f64))
|
|
1916
2814
|
.unwrap_or(Value::Number(f64::NAN)));
|
|
1917
2815
|
}
|
|
2816
|
+
"codePointAt" => {
|
|
2817
|
+
let idx = match arg_vals.first() {
|
|
2818
|
+
Some(Value::Number(n)) if *n >= 0.0 => *n as usize,
|
|
2819
|
+
_ => return Ok(Value::Null),
|
|
2820
|
+
};
|
|
2821
|
+
return Ok(nth_char_cached(s, idx)
|
|
2822
|
+
.map(|c| Value::Number(c as u32 as f64))
|
|
2823
|
+
.unwrap_or(Value::Null));
|
|
2824
|
+
}
|
|
2825
|
+
"substr" => {
|
|
2826
|
+
// (interp lacked substr; vm/native had it — this closes the gap.)
|
|
2827
|
+
let chars: Vec<char> = s.chars().collect();
|
|
2828
|
+
let len = chars.len() as i64;
|
|
2829
|
+
let mut start = match arg_vals.first() {
|
|
2830
|
+
Some(Value::Number(n)) => *n as i64,
|
|
2831
|
+
_ => 0,
|
|
2832
|
+
};
|
|
2833
|
+
if start < 0 {
|
|
2834
|
+
start = (len + start).max(0);
|
|
2835
|
+
}
|
|
2836
|
+
let start = (start as usize).min(chars.len());
|
|
2837
|
+
let count = match arg_vals.get(1) {
|
|
2838
|
+
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
2839
|
+
_ => chars.len() - start,
|
|
2840
|
+
};
|
|
2841
|
+
let out: String =
|
|
2842
|
+
chars[start..(start + count).min(chars.len())].iter().collect();
|
|
2843
|
+
return Ok(Value::String(out.into()));
|
|
2844
|
+
}
|
|
1918
2845
|
"repeat" => {
|
|
1919
2846
|
let count = match arg_vals.first() {
|
|
1920
2847
|
Some(Value::Number(n)) if *n >= 0.0 => *n as usize,
|
|
@@ -1964,6 +2891,35 @@ impl Evaluator {
|
|
|
1964
2891
|
return Ok(Value::Null);
|
|
1965
2892
|
}
|
|
1966
2893
|
#[cfg(feature = "regex")]
|
|
2894
|
+
"matchAll" => {
|
|
2895
|
+
let regexp = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
2896
|
+
match crate::regex::string_match_all(s, ®exp) {
|
|
2897
|
+
Ok(matches) => {
|
|
2898
|
+
let core_matches: Vec<tishlang_core::Value> = matches
|
|
2899
|
+
.iter()
|
|
2900
|
+
.map(|m| {
|
|
2901
|
+
crate::value_convert::eval_to_core(m)
|
|
2902
|
+
.unwrap_or(tishlang_core::Value::Null)
|
|
2903
|
+
})
|
|
2904
|
+
.collect();
|
|
2905
|
+
let it = tishlang_builtins::iterator::array_iterator(
|
|
2906
|
+
core_matches,
|
|
2907
|
+
);
|
|
2908
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2909
|
+
}
|
|
2910
|
+
Err(()) => {
|
|
2911
|
+
let err = crate::natives::type_error_construct(&[
|
|
2912
|
+
Value::String(
|
|
2913
|
+
"String.prototype.matchAll called with a non-global RegExp argument"
|
|
2914
|
+
.into(),
|
|
2915
|
+
),
|
|
2916
|
+
])
|
|
2917
|
+
.unwrap_or(Value::Null);
|
|
2918
|
+
return Err(EvalError::Throw(err));
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
#[cfg(feature = "regex")]
|
|
1967
2923
|
"search" => {
|
|
1968
2924
|
if let Some(regexp) = arg_vals.first() {
|
|
1969
2925
|
return Ok(crate::regex::string_search(s, regexp));
|
|
@@ -1989,6 +2945,40 @@ impl Evaluator {
|
|
|
1989
2945
|
let formatted = tishlang_builtins::number::to_fixed_str(*n, digits as usize);
|
|
1990
2946
|
return Ok(Value::String(formatted.into()));
|
|
1991
2947
|
}
|
|
2948
|
+
if method_name.as_ref() == "toExponential" {
|
|
2949
|
+
let digits = arg_vals.first().and_then(|v| match v {
|
|
2950
|
+
Value::Number(d) => Some((*d as i32).clamp(0, 100) as usize),
|
|
2951
|
+
_ => None,
|
|
2952
|
+
});
|
|
2953
|
+
return Ok(Value::String(
|
|
2954
|
+
tishlang_builtins::number::to_exponential_str(*n, digits).into(),
|
|
2955
|
+
));
|
|
2956
|
+
}
|
|
2957
|
+
if method_name.as_ref() == "toPrecision" {
|
|
2958
|
+
match arg_vals.first() {
|
|
2959
|
+
Some(Value::Number(p)) => {
|
|
2960
|
+
let pi = *p as i32;
|
|
2961
|
+
if !(1..=100).contains(&pi) && n.is_finite() {
|
|
2962
|
+
let err = crate::natives::range_error_construct(&[
|
|
2963
|
+
Value::String(
|
|
2964
|
+
"toPrecision() argument must be between 1 and 100"
|
|
2965
|
+
.into(),
|
|
2966
|
+
),
|
|
2967
|
+
])
|
|
2968
|
+
.unwrap_or(Value::Null);
|
|
2969
|
+
return Err(EvalError::Throw(err));
|
|
2970
|
+
}
|
|
2971
|
+
return Ok(Value::String(
|
|
2972
|
+
tishlang_builtins::number::to_precision_str(*n, pi).into(),
|
|
2973
|
+
));
|
|
2974
|
+
}
|
|
2975
|
+
_ => {
|
|
2976
|
+
return Ok(Value::String(
|
|
2977
|
+
tishlang_core::js_number_to_string(*n).into(),
|
|
2978
|
+
))
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
1992
2982
|
if method_name.as_ref() == "toString" {
|
|
1993
2983
|
// Shares the VM/native formatting via the backend-agnostic helper
|
|
1994
2984
|
// (issue #59). Radix defaults to 10; 2–36 supported, else RangeError.
|
|
@@ -2158,6 +3148,9 @@ impl Evaluator {
|
|
|
2158
3148
|
}
|
|
2159
3149
|
Expr::Assign { name, value, .. } => {
|
|
2160
3150
|
let v = self.eval_expr(value)?;
|
|
3151
|
+
// A plain assignment overwrites the variable, so drop any builder buffering it (the
|
|
3152
|
+
// buffered value is about to be replaced). `value` may itself have read+flushed it.
|
|
3153
|
+
self.discard_pending_for(name.as_ref());
|
|
2161
3154
|
match self.scope.borrow_mut().assign(name.as_ref(), v.clone()) {
|
|
2162
3155
|
Ok(true) => Ok(v),
|
|
2163
3156
|
Ok(false) => Err(EvalError::Error(format!("Undefined variable: {}", name))),
|
|
@@ -2306,6 +3299,10 @@ impl Evaluator {
|
|
|
2306
3299
|
}
|
|
2307
3300
|
}
|
|
2308
3301
|
Expr::CompoundAssign { name, op, value, .. } => {
|
|
3302
|
+
// Expression-position `+=` (result is used): flush any builder so `current` is the
|
|
3303
|
+
// full string, then take the generic path. The O(1) builder fast path is reserved
|
|
3304
|
+
// for statement position (see `eval_expr_discard`).
|
|
3305
|
+
self.flush_pending_for(name.as_ref());
|
|
2309
3306
|
let current = self.scope.borrow().get(name.as_ref())
|
|
2310
3307
|
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
2311
3308
|
let rhs = self.eval_expr(value)?;
|
|
@@ -2324,6 +3321,7 @@ impl Evaluator {
|
|
|
2324
3321
|
}
|
|
2325
3322
|
}
|
|
2326
3323
|
Expr::LogicalAssign { name, op, value, .. } => {
|
|
3324
|
+
self.flush_pending_for(name.as_ref());
|
|
2327
3325
|
let current = self.scope.borrow().get(name.as_ref())
|
|
2328
3326
|
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
2329
3327
|
let result = match op {
|
|
@@ -2394,6 +3392,12 @@ impl Evaluator {
|
|
|
2394
3392
|
Value::Array(arr) => {
|
|
2395
3393
|
let idx = match &idx_val {
|
|
2396
3394
|
Value::Number(n) => *n as usize,
|
|
3395
|
+
// Canonical integer string key → index (#432); a non-index string key
|
|
3396
|
+
// can't live in a Vec-backed array, so the write is a no-op.
|
|
3397
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
3398
|
+
Some(i) => i,
|
|
3399
|
+
None => return Ok(val),
|
|
3400
|
+
},
|
|
2397
3401
|
_ => return Err(EvalError::Error(format!(
|
|
2398
3402
|
"Array index must be a number, got {:?}",
|
|
2399
3403
|
idx_val
|
|
@@ -2750,6 +3754,9 @@ impl Evaluator {
|
|
|
2750
3754
|
module_cache: Rc::clone(&self.module_cache),
|
|
2751
3755
|
current_dir: RefCell::new(self.current_dir.borrow().clone()),
|
|
2752
3756
|
virtual_builtins: Rc::clone(&self.virtual_builtins),
|
|
3757
|
+
string_builder: Rc::clone(&self.string_builder),
|
|
3758
|
+
call_depth: Rc::clone(&self.call_depth),
|
|
3759
|
+
max_call_depth: self.max_call_depth,
|
|
2753
3760
|
};
|
|
2754
3761
|
match eval.eval_statement(body) {
|
|
2755
3762
|
Ok(v) => Ok(v),
|
|
@@ -2901,7 +3908,31 @@ impl Evaluator {
|
|
|
2901
3908
|
}
|
|
2902
3909
|
Err(EvalError::Error("Not a function".to_string()))
|
|
2903
3910
|
}
|
|
2904
|
-
Value::Native(native_fn) =>
|
|
3911
|
+
Value::Native(native_fn) => {
|
|
3912
|
+
// #122: Node fs callback form — `readFile(path, (err, data) => …)`. The self-less
|
|
3913
|
+
// `Value::Native` bridge can't invoke an eval callback (a core `Callable` can't
|
|
3914
|
+
// re-enter the interpreter), so handle it here where `&self` is available: run the
|
|
3915
|
+
// op's core on the non-callback args, split its Result into `(err, data)`, and call
|
|
3916
|
+
// the callback. Matches the VM/native backends (fs_ext runs the callback in-core).
|
|
3917
|
+
#[cfg(feature = "fs")]
|
|
3918
|
+
if let Some(cb @ Value::Function { .. }) = args.last() {
|
|
3919
|
+
if let Some(core_op) = natives::fsx::callback_core(*native_fn) {
|
|
3920
|
+
let cb = cb.clone();
|
|
3921
|
+
let core_args = args[..args.len() - 1]
|
|
3922
|
+
.iter()
|
|
3923
|
+
.map(crate::value_convert::eval_to_core)
|
|
3924
|
+
.collect::<Result<Vec<_>, String>>()
|
|
3925
|
+
.map_err(EvalError::Error)?;
|
|
3926
|
+
let (err, data) = match core_op(&core_args) {
|
|
3927
|
+
Ok(v) => (Value::Null, crate::value_convert::core_to_eval(v)),
|
|
3928
|
+
Err(e) => (crate::value_convert::core_to_eval(e), Value::Null),
|
|
3929
|
+
};
|
|
3930
|
+
self.call_func(&cb, &[err, data])?;
|
|
3931
|
+
return Ok(Value::Null);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
native_fn(args).map_err(EvalError::Error)
|
|
3935
|
+
}
|
|
2905
3936
|
#[cfg(feature = "http")]
|
|
2906
3937
|
Value::PromiseResolver(r) => {
|
|
2907
3938
|
let value = args.first().cloned().unwrap_or(Value::Null);
|
|
@@ -3018,6 +4049,9 @@ impl Evaluator {
|
|
|
3018
4049
|
module_cache: Rc::clone(&self.module_cache),
|
|
3019
4050
|
current_dir: RefCell::new(self.current_dir.borrow().clone()),
|
|
3020
4051
|
virtual_builtins: Rc::clone(&self.virtual_builtins),
|
|
4052
|
+
string_builder: Rc::clone(&self.string_builder),
|
|
4053
|
+
call_depth: Rc::clone(&self.call_depth),
|
|
4054
|
+
max_call_depth: self.max_call_depth,
|
|
3021
4055
|
};
|
|
3022
4056
|
{
|
|
3023
4057
|
let mut s = scope.borrow_mut();
|
|
@@ -3074,6 +4108,20 @@ impl Evaluator {
|
|
|
3074
4108
|
// VM's single `run_chunk` re-entry. 128 KiB is smaller than one level's chain, so the
|
|
3075
4109
|
// stack overflows BETWEEN checks; 1 MiB comfortably covers a level (verified to depth
|
|
3076
4110
|
// 20000 in both debug and release). 16 MiB segments keep grow frequency low.
|
|
4111
|
+
// #381: bound recursion with a catchable `RangeError` instead of letting `stacker`
|
|
4112
|
+
// grow the stack toward OOM/abort. The counter is shared (`Rc`) across every call
|
|
4113
|
+
// frame, so it measures true nesting depth; it is decremented on the way out (both
|
|
4114
|
+
// the Ok and Err paths) via the explicit `set` below so a caught throw doesn't leak
|
|
4115
|
+
// depth.
|
|
4116
|
+
let depth = eval.call_depth.get() + 1;
|
|
4117
|
+
if depth > eval.max_call_depth {
|
|
4118
|
+
let err = crate::natives::range_error_construct(&[Value::String(
|
|
4119
|
+
"Maximum call stack size exceeded".into(),
|
|
4120
|
+
)])
|
|
4121
|
+
.unwrap_or(Value::Null);
|
|
4122
|
+
return Err(EvalError::Throw(err));
|
|
4123
|
+
}
|
|
4124
|
+
eval.call_depth.set(depth);
|
|
3077
4125
|
let body_result = {
|
|
3078
4126
|
#[cfg(not(target_arch = "wasm32"))]
|
|
3079
4127
|
{
|
|
@@ -3086,6 +4134,7 @@ impl Evaluator {
|
|
|
3086
4134
|
eval.eval_statement(body)
|
|
3087
4135
|
}
|
|
3088
4136
|
};
|
|
4137
|
+
eval.call_depth.set(depth - 1);
|
|
3089
4138
|
match body_result {
|
|
3090
4139
|
Ok(v) => Ok(v),
|
|
3091
4140
|
Err(EvalError::Return(v)) => Ok(v),
|
|
@@ -3511,6 +4560,15 @@ impl Evaluator {
|
|
|
3511
4560
|
/// `.entries()` — into a `Vec` by calling `next()` until `done`. Returns `None` when `v`
|
|
3512
4561
|
/// is not such an object. Shared by `for…of` and spread so both treat iterators like JS.
|
|
3513
4562
|
fn drain_eval_iterator(&self, v: &Value) -> Option<Vec<Value>> {
|
|
4563
|
+
// A string spreads as its characters (`[...s]`, `f(...s)`); for-of over a string is handled
|
|
4564
|
+
// separately at the loop site, but spread routes through here. (#440)
|
|
4565
|
+
if let Value::String(s) = v {
|
|
4566
|
+
return Some(
|
|
4567
|
+
s.chars()
|
|
4568
|
+
.map(|c| Value::String(std::sync::Arc::from(c.to_string())))
|
|
4569
|
+
.collect(),
|
|
4570
|
+
);
|
|
4571
|
+
}
|
|
3514
4572
|
if !matches!(v, Value::Object(_)) {
|
|
3515
4573
|
return None;
|
|
3516
4574
|
}
|
|
@@ -3566,7 +4624,7 @@ impl Evaluator {
|
|
|
3566
4624
|
}
|
|
3567
4625
|
Value::String(s) => {
|
|
3568
4626
|
if key == "length" {
|
|
3569
|
-
Ok(Value::Number(s
|
|
4627
|
+
Ok(Value::Number(char_count_cached(s) as f64))
|
|
3570
4628
|
} else {
|
|
3571
4629
|
Ok(Value::Null)
|
|
3572
4630
|
}
|
|
@@ -3623,6 +4681,11 @@ impl Evaluator {
|
|
|
3623
4681
|
_ => Ok(Value::Null),
|
|
3624
4682
|
}
|
|
3625
4683
|
}
|
|
4684
|
+
// Reading a property of the nullish value throws a catchable `TypeError`, matching the
|
|
4685
|
+
// bytecode VM (`get_member`'s `_` arm), cranelift/wasi, and node — not a silent `null`.
|
|
4686
|
+
// The tree-walker used to fall through to `_ => Ok(Value::Null)`, so `null.length` read
|
|
4687
|
+
// back as `null` on interp while every other backend threw (a pure interp≠vm bug).
|
|
4688
|
+
Value::Null => Err(format!("Cannot read property '{}' of null", key)),
|
|
3626
4689
|
_ => Ok(Value::Null),
|
|
3627
4690
|
}
|
|
3628
4691
|
}
|
|
@@ -3632,6 +4695,12 @@ impl Evaluator {
|
|
|
3632
4695
|
Value::Array(arr) => {
|
|
3633
4696
|
let idx = match index {
|
|
3634
4697
|
Value::Number(n) => *n as usize,
|
|
4698
|
+
// A canonical integer string key ("0","1",…) indexes the array (#432), so
|
|
4699
|
+
// `arr["0"] === arr[0]`; any other key reads Null.
|
|
4700
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
4701
|
+
Some(i) => i,
|
|
4702
|
+
None => return Ok(Value::Null),
|
|
4703
|
+
},
|
|
3635
4704
|
_ => return Ok(Value::Null),
|
|
3636
4705
|
};
|
|
3637
4706
|
Ok(arr.borrow().get(idx).cloned().unwrap_or(Value::Null))
|
|
@@ -3644,9 +4713,7 @@ impl Evaluator {
|
|
|
3644
4713
|
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => *n as usize,
|
|
3645
4714
|
_ => return Ok(Value::Null),
|
|
3646
4715
|
};
|
|
3647
|
-
Ok(s
|
|
3648
|
-
.chars()
|
|
3649
|
-
.nth(idx)
|
|
4716
|
+
Ok(nth_char_cached(s, idx)
|
|
3650
4717
|
.map(|c| Value::String(c.to_string().into()))
|
|
3651
4718
|
.unwrap_or(Value::Null))
|
|
3652
4719
|
}
|
|
@@ -3659,6 +4726,9 @@ impl Evaluator {
|
|
|
3659
4726
|
};
|
|
3660
4727
|
self.get_prop(obj, key)
|
|
3661
4728
|
}
|
|
4729
|
+
// Indexing the nullish value throws a catchable `TypeError` (like `get_prop` above and
|
|
4730
|
+
// the VM/cranelift/wasi/node) rather than silently reading back `null`.
|
|
4731
|
+
Value::Null => Err(format!("Cannot read property '{}' of null", index)),
|
|
3662
4732
|
_ => Ok(Value::Null),
|
|
3663
4733
|
}
|
|
3664
4734
|
}
|
|
@@ -3677,8 +4747,13 @@ impl Evaluator {
|
|
|
3677
4747
|
}
|
|
3678
4748
|
}
|
|
3679
4749
|
|
|
3680
|
-
|
|
3681
|
-
|
|
4750
|
+
/// #381 — ancestor-guarded like `tishlang_core::json_stringify_into_guarded`: a back-edge to an
|
|
4751
|
+
/// ancestor (`a.self = a`) is a cycle and returns `Err(())` instead of recursing forever (this
|
|
4752
|
+
/// was an unguarded native-stack overflow → uncatchable abort; core's guard from #389 never
|
|
4753
|
+
/// covered the interpreter's own stringifier). Ancestor-path only, not all-visited: a node
|
|
4754
|
+
/// repeated across sibling branches is a legal DAG and must serialize twice.
|
|
4755
|
+
fn json_stringify_value(v: &Value, ancestors: &mut Vec<*const ()>) -> Result<String, ()> {
|
|
4756
|
+
Ok(match v {
|
|
3682
4757
|
Value::Null => "null".to_string(),
|
|
3683
4758
|
Value::Bool(b) => b.to_string(),
|
|
3684
4759
|
Value::Number(n) => {
|
|
@@ -3689,37 +4764,50 @@ impl Evaluator {
|
|
|
3689
4764
|
tishlang_core::write_json_number(&mut s, *n);
|
|
3690
4765
|
s
|
|
3691
4766
|
}
|
|
3692
|
-
Value::String(s) =>
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
4767
|
+
Value::String(s) => {
|
|
4768
|
+
// Use the shared core escaper so control chars (`\b`, `\f`, `\u00xx`) are escaped
|
|
4769
|
+
// exactly like the vm/native path — the old hand-rolled `.replace()` chain missed
|
|
4770
|
+
// them and produced invalid JSON (raw control bytes). #437
|
|
4771
|
+
let mut out = String::with_capacity(s.len() + 2);
|
|
4772
|
+
out.push('"');
|
|
4773
|
+
tishlang_core::escape_json_string_into(&mut out, s);
|
|
4774
|
+
out.push('"');
|
|
4775
|
+
out
|
|
4776
|
+
}
|
|
3700
4777
|
Value::Array(arr) => {
|
|
3701
|
-
let
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
4778
|
+
let ptr = Rc::as_ptr(arr) as *const ();
|
|
4779
|
+
if ancestors.contains(&ptr) {
|
|
4780
|
+
return Err(());
|
|
4781
|
+
}
|
|
4782
|
+
ancestors.push(ptr);
|
|
4783
|
+
let borrowed = arr.borrow();
|
|
4784
|
+
let mut inner: Vec<String> = Vec::with_capacity(borrowed.len());
|
|
4785
|
+
for item in borrowed.iter() {
|
|
4786
|
+
inner.push(Self::json_stringify_value(item, ancestors)?);
|
|
4787
|
+
}
|
|
4788
|
+
drop(borrowed);
|
|
4789
|
+
ancestors.pop();
|
|
3706
4790
|
format!("[{}]", inner.join(","))
|
|
3707
4791
|
}
|
|
3708
4792
|
Value::Object(map) => {
|
|
4793
|
+
let ptr = Rc::as_ptr(map) as *const ();
|
|
4794
|
+
if ancestors.contains(&ptr) {
|
|
4795
|
+
return Err(());
|
|
4796
|
+
}
|
|
4797
|
+
ancestors.push(ptr);
|
|
3709
4798
|
// Insertion order (PropMap is an IndexMap) — matches JS/Node and the
|
|
3710
4799
|
// VM/rust backends. No key sort.
|
|
3711
|
-
let
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
.
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
.collect();
|
|
4800
|
+
let borrowed = map.borrow();
|
|
4801
|
+
let mut entries: Vec<String> = Vec::with_capacity(borrowed.strings.len());
|
|
4802
|
+
for (k, v) in borrowed.strings.iter() {
|
|
4803
|
+
entries.push(format!(
|
|
4804
|
+
"\"{}\":{}",
|
|
4805
|
+
k.replace('\\', "\\\\").replace('"', "\\\""),
|
|
4806
|
+
Self::json_stringify_value(v, ancestors)?
|
|
4807
|
+
));
|
|
4808
|
+
}
|
|
4809
|
+
drop(borrowed);
|
|
4810
|
+
ancestors.pop();
|
|
3723
4811
|
format!("{{{}}}", entries.join(","))
|
|
3724
4812
|
}
|
|
3725
4813
|
Value::Symbol(_) => "null".to_string(),
|
|
@@ -3738,7 +4826,7 @@ impl Evaluator {
|
|
|
3738
4826
|
#[cfg(feature = "regex")]
|
|
3739
4827
|
Value::RegExp(_) => "null".to_string(),
|
|
3740
4828
|
Value::Opaque(_) | Value::OpaqueMethod(_, _) => "null".to_string(),
|
|
3741
|
-
}
|
|
4829
|
+
})
|
|
3742
4830
|
}
|
|
3743
4831
|
|
|
3744
4832
|
// Static native wrapper functions (these need to be fn pointers, not closures with &self)
|
|
@@ -3749,20 +4837,36 @@ impl Evaluator {
|
|
|
3749
4837
|
|
|
3750
4838
|
fn json_stringify_native(args: &[Value]) -> Result<Value, String> {
|
|
3751
4839
|
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
3752
|
-
|
|
4840
|
+
let mut ancestors: Vec<*const ()> = Vec::new();
|
|
4841
|
+
match Self::json_stringify_value(&v, &mut ancestors) {
|
|
4842
|
+
Ok(s) => Ok(Value::String(s.into())),
|
|
4843
|
+
// Surfaced by `call_func` as `EvalError::Error`, which the Try handler boxes as
|
|
4844
|
+
// `{ name: "TypeError", message }` — node-identical for the circular case (#381).
|
|
4845
|
+
Err(()) => Err("Converting circular structure to JSON".to_string()),
|
|
4846
|
+
}
|
|
3753
4847
|
}
|
|
3754
4848
|
|
|
3755
4849
|
fn object_keys(args: &[Value]) -> Result<Value, String> {
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
4850
|
+
match args.first() {
|
|
4851
|
+
Some(Value::Object(obj)) => {
|
|
4852
|
+
let keys: Vec<Value> = obj
|
|
4853
|
+
.borrow()
|
|
4854
|
+
.strings
|
|
4855
|
+
.keys()
|
|
4856
|
+
.map(|k| Value::String(Arc::clone(k)))
|
|
4857
|
+
.collect();
|
|
4858
|
+
Ok(Value::Array(Rc::new(RefCell::new(keys))))
|
|
4859
|
+
}
|
|
4860
|
+
// `Object.keys(array)` → index strings "0".."len-1" (own enumerable keys are the indices),
|
|
4861
|
+
// matching node and the shared builtin.
|
|
4862
|
+
Some(Value::Array(arr)) => {
|
|
4863
|
+
let len = arr.borrow().len();
|
|
4864
|
+
let keys: Vec<Value> = (0..len)
|
|
4865
|
+
.map(|i| Value::String(Arc::from(i.to_string())))
|
|
4866
|
+
.collect();
|
|
4867
|
+
Ok(Value::Array(Rc::new(RefCell::new(keys))))
|
|
4868
|
+
}
|
|
4869
|
+
_ => Ok(Value::Array(Rc::new(RefCell::new(Vec::new())))),
|
|
3766
4870
|
}
|
|
3767
4871
|
}
|
|
3768
4872
|
|
|
@@ -4214,3 +5318,126 @@ impl std::fmt::Display for EvalError {
|
|
|
4214
5318
|
}
|
|
4215
5319
|
|
|
4216
5320
|
impl std::error::Error for EvalError {}
|
|
5321
|
+
|
|
5322
|
+
#[cfg(test)]
|
|
5323
|
+
mod recursion_limit_tests_381 {
|
|
5324
|
+
use super::Evaluator;
|
|
5325
|
+
use tishlang_parser::parse;
|
|
5326
|
+
|
|
5327
|
+
fn run_with_depth(src: &str, max_depth: usize) -> String {
|
|
5328
|
+
let program = parse(src).unwrap();
|
|
5329
|
+
let mut eval = Evaluator::new();
|
|
5330
|
+
eval.max_call_depth = max_depth;
|
|
5331
|
+
eval.eval_program(&program).unwrap().to_string()
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
#[test]
|
|
5335
|
+
fn deep_recursion_throws_catchable_range_error() {
|
|
5336
|
+
// Infinite (non-tail) recursion past the limit must throw a CATCHABLE RangeError, not abort:
|
|
5337
|
+
// try/catch recovers and the program keeps running.
|
|
5338
|
+
let src = "fn rec(n) { return 1 + rec(n + 1) }\n\
|
|
5339
|
+
let name = 'none'\n\
|
|
5340
|
+
try { rec(0) } catch (e) { name = e.name }\n\
|
|
5341
|
+
name";
|
|
5342
|
+
assert_eq!(run_with_depth(src, 200), "RangeError");
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
#[test]
|
|
5346
|
+
fn normal_recursion_is_unaffected() {
|
|
5347
|
+
let src = "fn fib(n) { if (n < 2) { return n } return fib(n - 1) + fib(n - 2) }\nfib(12)";
|
|
5348
|
+
assert_eq!(run_with_depth(src, 20000), "144");
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
|
|
5352
|
+
#[cfg(test)]
|
|
5353
|
+
mod null_member_access_tests {
|
|
5354
|
+
// Reading a property or index of the nullish value throws a catchable `TypeError`, matching the
|
|
5355
|
+
// bytecode VM / cranelift / wasi / node. The tree-walker used to fall through to `Ok(Null)`, so
|
|
5356
|
+
// `null.length` read back as `null` on interp while every other backend threw (a pure interp≠vm
|
|
5357
|
+
// divergence). These lock the interpreter to the throwing behavior.
|
|
5358
|
+
use super::Evaluator;
|
|
5359
|
+
use tishlang_parser::parse;
|
|
5360
|
+
|
|
5361
|
+
fn run(src: &str) -> String {
|
|
5362
|
+
let program = parse(src).unwrap();
|
|
5363
|
+
let mut eval = Evaluator::new();
|
|
5364
|
+
eval.eval_program(&program).unwrap().to_string()
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
#[test]
|
|
5368
|
+
fn null_property_read_throws_type_error() {
|
|
5369
|
+
let src = "let name = 'none'\n\
|
|
5370
|
+
try { let z = null; z.length } catch (e) { name = e.name }\n\
|
|
5371
|
+
name";
|
|
5372
|
+
assert_eq!(run(src), "TypeError");
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5375
|
+
#[test]
|
|
5376
|
+
fn null_index_read_throws_type_error() {
|
|
5377
|
+
let src = "let name = 'none'\n\
|
|
5378
|
+
try { let z = null; z[0] } catch (e) { name = e.name }\n\
|
|
5379
|
+
name";
|
|
5380
|
+
assert_eq!(run(src), "TypeError");
|
|
5381
|
+
}
|
|
5382
|
+
|
|
5383
|
+
#[test]
|
|
5384
|
+
fn object_missing_property_still_reads_null() {
|
|
5385
|
+
// Guard against over-reach: a MISSING property of a real object is `null`, not a throw.
|
|
5386
|
+
let src = "let o = { a: 1 }\nString(o.b === null)";
|
|
5387
|
+
assert_eq!(run(src), "true");
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5390
|
+
#[test]
|
|
5391
|
+
fn array_oob_index_still_reads_null() {
|
|
5392
|
+
// Out-of-bounds array index stays nullish (only a null/undefined *receiver* throws).
|
|
5393
|
+
let src = "let a = [1, 2, 3]\nString(a[9] === null)";
|
|
5394
|
+
assert_eq!(run(src), "true");
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5398
|
+
#[cfg(test)]
|
|
5399
|
+
mod array_hof_callback_arg_tests {
|
|
5400
|
+
// Array HOFs pass the source array as the trailing callback arg (JS `(element, index, array)`,
|
|
5401
|
+
// reduce `(acc, element, index, array)`). The inline interp loops snapshot the backing store and
|
|
5402
|
+
// drop the borrow before any callback, so a callback that MUTATES the array via the `array` arg
|
|
5403
|
+
// (or a capture) can't RefCell-panic — it iterates the pre-call snapshot, matching JS + the shared
|
|
5404
|
+
// #382 builtins path.
|
|
5405
|
+
use super::Evaluator;
|
|
5406
|
+
use tishlang_parser::parse;
|
|
5407
|
+
|
|
5408
|
+
fn run(src: &str) -> String {
|
|
5409
|
+
let program = parse(src).unwrap();
|
|
5410
|
+
let mut eval = Evaluator::new();
|
|
5411
|
+
eval.eval_program(&program).unwrap().to_string()
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
#[test]
|
|
5415
|
+
fn map_receives_array_as_third_arg() {
|
|
5416
|
+
assert_eq!(run("[5, 3, 8].map((x, i, arr) => arr.length).join(',')"), "3,3,3");
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
#[test]
|
|
5420
|
+
fn reduce_receives_array_as_fourth_arg() {
|
|
5421
|
+
assert_eq!(run("[1, 2, 3].reduce((a, x, i, arr) => a + arr.length, 0)"), "9");
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5424
|
+
#[test]
|
|
5425
|
+
fn callback_may_index_the_array_arg() {
|
|
5426
|
+
assert_eq!(run("[10, 20, 30].map((x, i, arr) => arr[i] * 2).join(',')"), "20,40,60");
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
#[test]
|
|
5430
|
+
fn callback_mutating_the_array_arg_does_not_panic() {
|
|
5431
|
+
// Pre-snapshot this RefCell-panicked (map held the borrow across the loop). Now iteration is
|
|
5432
|
+
// over the snapshot (3 elements); the 3 pushes land on the live array → final length 6, matching
|
|
5433
|
+
// JS forEach semantics (does not visit elements appended during iteration).
|
|
5434
|
+
let src = "let a = [1, 2, 3]\na.forEach((x, i, arr) => { arr.push(x) })\na.length";
|
|
5435
|
+
assert_eq!(run(src), "6");
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
#[test]
|
|
5439
|
+
fn one_and_two_arg_callbacks_unchanged() {
|
|
5440
|
+
assert_eq!(run("[5, 3, 8].map(x => x * 2).join(',')"), "10,6,16");
|
|
5441
|
+
assert_eq!(run("[5, 3, 8].map((x, i) => x + i).join(',')"), "5,4,10");
|
|
5442
|
+
}
|
|
5443
|
+
}
|