@tishlang/tish 2.12.0 → 2.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -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 {
|
|
@@ -193,6 +311,7 @@ impl Evaluator {
|
|
|
193
311
|
process_obj.insert("exit".into(), Value::Native(natives::process_exit));
|
|
194
312
|
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
195
313
|
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
314
|
+
process_obj.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
196
315
|
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
197
316
|
.into_iter()
|
|
198
317
|
.map(|s| Value::String(s.into()))
|
|
@@ -367,6 +486,9 @@ impl Evaluator {
|
|
|
367
486
|
module_cache: Rc::new(RefCell::new(HashMap::new())),
|
|
368
487
|
current_dir: RefCell::new(None),
|
|
369
488
|
virtual_builtins: Rc::new(RefCell::new(HashMap::new())),
|
|
489
|
+
string_builder: Rc::new(StringBuilderState::default()),
|
|
490
|
+
call_depth: Rc::new(std::cell::Cell::new(0)),
|
|
491
|
+
max_call_depth: env_max_call_depth(),
|
|
370
492
|
}
|
|
371
493
|
}
|
|
372
494
|
|
|
@@ -401,6 +523,9 @@ impl Evaluator {
|
|
|
401
523
|
for stmt in &program.statements {
|
|
402
524
|
last = self.eval_statement(stmt).map_err(|e| e.to_string())?;
|
|
403
525
|
}
|
|
526
|
+
// Flush any still-buffered string accumulator so the variable's slot is correct for any
|
|
527
|
+
// post-run observation (timers, REPL, embedders reading scope state).
|
|
528
|
+
self.flush_pending();
|
|
404
529
|
Ok(last)
|
|
405
530
|
}
|
|
406
531
|
|
|
@@ -451,7 +576,11 @@ impl Evaluator {
|
|
|
451
576
|
self.bind_destruct_pattern(pattern, &value, *mutable)?;
|
|
452
577
|
Ok(Value::Null)
|
|
453
578
|
}
|
|
454
|
-
Statement::ExprStmt { expr, .. } =>
|
|
579
|
+
Statement::ExprStmt { expr, .. } => {
|
|
580
|
+
// Statement position: route through the path that keeps statement-position
|
|
581
|
+
// `acc += x` O(1) (no result materialization) while preserving values otherwise.
|
|
582
|
+
self.eval_expr_discard(expr)
|
|
583
|
+
}
|
|
455
584
|
Statement::If {
|
|
456
585
|
cond,
|
|
457
586
|
then_branch,
|
|
@@ -815,6 +944,33 @@ impl Evaluator {
|
|
|
815
944
|
let v = self.eval_expr(e)?;
|
|
816
945
|
self.scope.borrow_mut().set(Arc::from("default"), v, false);
|
|
817
946
|
}
|
|
947
|
+
// #305: re-export in the running program — load the dep and bind the names locally.
|
|
948
|
+
ExportDeclaration::ReExport {
|
|
949
|
+
specifiers,
|
|
950
|
+
all,
|
|
951
|
+
from,
|
|
952
|
+
..
|
|
953
|
+
} => {
|
|
954
|
+
let dep_val = self.load_module(from.as_ref())?;
|
|
955
|
+
let dep = match &dep_val {
|
|
956
|
+
Value::Object(m) => m.borrow().clone(),
|
|
957
|
+
_ => return Err(EvalError::Error("Module exports must be object".to_string())),
|
|
958
|
+
};
|
|
959
|
+
if *all {
|
|
960
|
+
for (k, v) in dep.strings.iter() {
|
|
961
|
+
self.scope.borrow_mut().set(Arc::from(k.as_ref()), v.clone(), false);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for spec in specifiers {
|
|
965
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
966
|
+
let v = dep.strings.get(name.as_ref()).ok_or_else(|| {
|
|
967
|
+
EvalError::Error(format!("Module does not export '{}'", name))
|
|
968
|
+
})?;
|
|
969
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
970
|
+
self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
818
974
|
}
|
|
819
975
|
Ok(Value::Null)
|
|
820
976
|
}
|
|
@@ -880,6 +1036,36 @@ impl Evaluator {
|
|
|
880
1036
|
self.scope.borrow_mut().set(Arc::from("default"), v, false);
|
|
881
1037
|
export_names.push("default".to_string());
|
|
882
1038
|
}
|
|
1039
|
+
// #305: re-export — load the dep, pull the requested (or all) exports into this
|
|
1040
|
+
// module's scope, and re-expose them in `export_names`.
|
|
1041
|
+
ExportDeclaration::ReExport {
|
|
1042
|
+
specifiers,
|
|
1043
|
+
all,
|
|
1044
|
+
from,
|
|
1045
|
+
..
|
|
1046
|
+
} => {
|
|
1047
|
+
let dep_val = self.load_module(from.as_ref())?;
|
|
1048
|
+
let dep = match &dep_val {
|
|
1049
|
+
Value::Object(m) => m.borrow().clone(),
|
|
1050
|
+
_ => return Err(EvalError::Error("Module exports must be object".to_string())),
|
|
1051
|
+
};
|
|
1052
|
+
if *all {
|
|
1053
|
+
for (k, v) in dep.strings.iter() {
|
|
1054
|
+
self.scope.borrow_mut().set(Arc::from(k.as_ref()), v.clone(), false);
|
|
1055
|
+
export_names.push(k.to_string());
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
for spec in specifiers {
|
|
1059
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
1060
|
+
let v = dep.strings.get(name.as_ref()).ok_or_else(|| {
|
|
1061
|
+
EvalError::Error(format!("Module does not export '{}'", name))
|
|
1062
|
+
})?;
|
|
1063
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1064
|
+
self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
|
|
1065
|
+
export_names.push(bind.to_string());
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
883
1069
|
}
|
|
884
1070
|
} else {
|
|
885
1071
|
let _ = self.eval_statement(stmt);
|
|
@@ -1057,6 +1243,7 @@ impl Evaluator {
|
|
|
1057
1243
|
exports.insert("exit".into(), Value::Native(natives::process_exit));
|
|
1058
1244
|
exports.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1059
1245
|
exports.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1246
|
+
exports.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
1060
1247
|
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
1061
1248
|
.into_iter()
|
|
1062
1249
|
.map(|s| Value::String(s.into()))
|
|
@@ -1076,6 +1263,7 @@ impl Evaluator {
|
|
|
1076
1263
|
process_obj.insert("exit".into(), Value::Native(natives::process_exit));
|
|
1077
1264
|
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1078
1265
|
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1266
|
+
process_obj.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
1079
1267
|
process_obj.insert("argv".into(), Value::Array(Rc::new(RefCell::new(argv))));
|
|
1080
1268
|
process_obj.insert("env".into(), Value::object(env_obj));
|
|
1081
1269
|
exports.insert(
|
|
@@ -1115,6 +1303,177 @@ impl Evaluator {
|
|
|
1115
1303
|
})
|
|
1116
1304
|
}
|
|
1117
1305
|
|
|
1306
|
+
// --- string-builder helpers (#186 / string_build): amortized O(1) `acc += x` ---
|
|
1307
|
+
|
|
1308
|
+
/// Append a value to a builder buffer using the exact JS coercion of `eval_binop`'s `+` (string
|
|
1309
|
+
/// operands push their raw chars; everything else goes through `to_js_string`).
|
|
1310
|
+
fn append_value_to_buf(buf: &mut String, v: &Value) {
|
|
1311
|
+
match v {
|
|
1312
|
+
Value::String(s) => buf.push_str(s),
|
|
1313
|
+
other => buf.push_str(&other.to_js_string()),
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/// Walk the scope chain from the current scope and return the exact scope that owns `name`.
|
|
1318
|
+
fn find_var_scope(&self, name: &str) -> Option<Rc<std::cell::RefCell<Scope>>> {
|
|
1319
|
+
let mut cur = Rc::clone(&self.scope);
|
|
1320
|
+
loop {
|
|
1321
|
+
if cur.borrow().vars.contains_key(name) {
|
|
1322
|
+
return Some(cur);
|
|
1323
|
+
}
|
|
1324
|
+
let parent = cur.borrow().parent.clone();
|
|
1325
|
+
match parent {
|
|
1326
|
+
Some(p) => cur = p,
|
|
1327
|
+
None => return None,
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/// Flush the active builder (if any) back into its owning scope slot, restoring the invariant
|
|
1333
|
+
/// that the slot holds the variable's true value. No-op when no builder is active.
|
|
1334
|
+
fn flush_pending(&self) {
|
|
1335
|
+
if !self.string_builder.active.get() {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if let Some(p) = self.string_builder.pending.borrow_mut().take() {
|
|
1339
|
+
p.scope
|
|
1340
|
+
.borrow_mut()
|
|
1341
|
+
.vars
|
|
1342
|
+
.insert(Arc::clone(&p.name), Value::String(p.buf.into()));
|
|
1343
|
+
}
|
|
1344
|
+
self.string_builder.active.set(false);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/// Flush the builder iff it is buffering `name` (which is about to be read). The `active` guard
|
|
1348
|
+
/// keeps this ~free on the hot identifier-read path when no builder exists.
|
|
1349
|
+
#[inline]
|
|
1350
|
+
fn flush_pending_for(&self, name: &str) {
|
|
1351
|
+
if !self.string_builder.active.get() {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
let hit = self
|
|
1355
|
+
.string_builder
|
|
1356
|
+
.pending
|
|
1357
|
+
.borrow()
|
|
1358
|
+
.as_ref()
|
|
1359
|
+
.is_some_and(|p| p.name.as_ref() == name);
|
|
1360
|
+
if hit {
|
|
1361
|
+
self.flush_pending();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/// Discard the builder iff it is buffering `name` (which is about to be overwritten by a plain
|
|
1366
|
+
/// assignment) — avoids an unnecessary O(n) flush of a value that is about to be replaced.
|
|
1367
|
+
#[inline]
|
|
1368
|
+
fn discard_pending_for(&self, name: &str) {
|
|
1369
|
+
if !self.string_builder.active.get() {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
let hit = self
|
|
1373
|
+
.string_builder
|
|
1374
|
+
.pending
|
|
1375
|
+
.borrow()
|
|
1376
|
+
.as_ref()
|
|
1377
|
+
.is_some_and(|p| p.name.as_ref() == name);
|
|
1378
|
+
if hit {
|
|
1379
|
+
*self.string_builder.pending.borrow_mut() = None;
|
|
1380
|
+
self.string_builder.active.set(false);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/// Try to handle `name += rhs` as an amortized-O(1) string append. Returns `true` if it was a
|
|
1385
|
+
/// string append (buffer updated); `false` if `name` is not a (mutable) string accumulator, so
|
|
1386
|
+
/// the caller falls back to the generic `+=`. Any builder for a DIFFERENT slot is flushed first;
|
|
1387
|
+
/// keying by the exact owning scope means a shadowing inner `name` never appends onto an outer
|
|
1388
|
+
/// `name`'s buffer.
|
|
1389
|
+
fn try_string_append(&self, name: &Arc<str>, rhs: &Value) -> bool {
|
|
1390
|
+
let owner = match self.find_var_scope(name) {
|
|
1391
|
+
Some(s) => s,
|
|
1392
|
+
None => return false, // undefined → let the generic path raise the error
|
|
1393
|
+
};
|
|
1394
|
+
// Continue an existing builder for this exact slot.
|
|
1395
|
+
if self.string_builder.active.get() {
|
|
1396
|
+
let same = self
|
|
1397
|
+
.string_builder
|
|
1398
|
+
.pending
|
|
1399
|
+
.borrow()
|
|
1400
|
+
.as_ref()
|
|
1401
|
+
.is_some_and(|p| p.name == *name && Rc::ptr_eq(&p.scope, &owner));
|
|
1402
|
+
if same {
|
|
1403
|
+
if let Some(p) = self.string_builder.pending.borrow_mut().as_mut() {
|
|
1404
|
+
Self::append_value_to_buf(&mut p.buf, rhs);
|
|
1405
|
+
}
|
|
1406
|
+
return true;
|
|
1407
|
+
}
|
|
1408
|
+
// A different slot is buffered — flush it before starting a new one.
|
|
1409
|
+
self.flush_pending();
|
|
1410
|
+
}
|
|
1411
|
+
// Start a new builder only if the accumulator currently holds a mutable string.
|
|
1412
|
+
let start = {
|
|
1413
|
+
let owner_ref = owner.borrow();
|
|
1414
|
+
if owner_ref.consts.contains(name.as_ref()) {
|
|
1415
|
+
return false; // `const acc += x` must raise the same error as the generic path
|
|
1416
|
+
}
|
|
1417
|
+
match owner_ref.vars.get(name.as_ref()) {
|
|
1418
|
+
Some(Value::String(a)) => Some(a.clone()),
|
|
1419
|
+
_ => None, // non-string accumulator → numeric/other `+=`
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
match start {
|
|
1423
|
+
Some(a) => {
|
|
1424
|
+
let mut buf = String::with_capacity(a.len() + 16);
|
|
1425
|
+
buf.push_str(&a);
|
|
1426
|
+
Self::append_value_to_buf(&mut buf, rhs);
|
|
1427
|
+
*self.string_builder.pending.borrow_mut() = Some(PendingAppend {
|
|
1428
|
+
scope: owner,
|
|
1429
|
+
name: Arc::clone(name),
|
|
1430
|
+
buf,
|
|
1431
|
+
});
|
|
1432
|
+
self.string_builder.active.set(true);
|
|
1433
|
+
true
|
|
1434
|
+
}
|
|
1435
|
+
None => false,
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/// Evaluate `expr` in statement position. Special-cases `acc += rhs` so the string-builder never
|
|
1440
|
+
/// has to materialize the assignment's result value — the key to keeping the append O(1). In that
|
|
1441
|
+
/// one case the (discarded) statement value is `null`; every other expression returns its real
|
|
1442
|
+
/// value, preserving block/program last-value semantics.
|
|
1443
|
+
fn eval_expr_discard(&self, expr: &Expr) -> Result<Value, EvalError> {
|
|
1444
|
+
if let Expr::CompoundAssign {
|
|
1445
|
+
name,
|
|
1446
|
+
op: tishlang_ast::CompoundOp::Add,
|
|
1447
|
+
value,
|
|
1448
|
+
..
|
|
1449
|
+
} = expr
|
|
1450
|
+
{
|
|
1451
|
+
// Evaluate rhs FIRST — it may read `name`, which flushes any active builder.
|
|
1452
|
+
let rhs = self.eval_expr(value)?;
|
|
1453
|
+
if self.try_string_append(name, &rhs) {
|
|
1454
|
+
// Statement-position result is discarded; returning null avoids the O(n) flatten.
|
|
1455
|
+
return Ok(Value::Null);
|
|
1456
|
+
}
|
|
1457
|
+
// Not a string accumulator: generic `+=` (numeric/other) — return its real value.
|
|
1458
|
+
self.flush_pending_for(name);
|
|
1459
|
+
let current = self
|
|
1460
|
+
.scope
|
|
1461
|
+
.borrow()
|
|
1462
|
+
.get(name.as_ref())
|
|
1463
|
+
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
1464
|
+
let result = self
|
|
1465
|
+
.eval_binop(¤t, BinOp::Add, &rhs)
|
|
1466
|
+
.map_err(EvalError::Error)?;
|
|
1467
|
+
match self.scope.borrow_mut().assign(name.as_ref(), result.clone()) {
|
|
1468
|
+
Ok(true) => Ok(result),
|
|
1469
|
+
Ok(false) => Err(EvalError::Error(format!("Undefined variable: {}", name))),
|
|
1470
|
+
Err(e) => Err(EvalError::Error(e)),
|
|
1471
|
+
}
|
|
1472
|
+
} else {
|
|
1473
|
+
self.eval_expr(expr)
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1118
1477
|
fn eval_expr(&self, expr: &Expr) -> Result<Value, EvalError> {
|
|
1119
1478
|
match expr {
|
|
1120
1479
|
Expr::Literal { value, .. } => Ok(match value {
|
|
@@ -1123,11 +1482,14 @@ impl Evaluator {
|
|
|
1123
1482
|
Literal::Bool(b) => Value::Bool(*b),
|
|
1124
1483
|
Literal::Null => Value::Null,
|
|
1125
1484
|
}),
|
|
1126
|
-
Expr::Ident { name, .. } =>
|
|
1127
|
-
.
|
|
1128
|
-
.
|
|
1129
|
-
.
|
|
1130
|
-
|
|
1485
|
+
Expr::Ident { name, .. } => {
|
|
1486
|
+
// Flush any string-builder buffering this variable so the read sees the full string.
|
|
1487
|
+
self.flush_pending_for(name.as_ref());
|
|
1488
|
+
self.scope
|
|
1489
|
+
.borrow()
|
|
1490
|
+
.get(name.as_ref())
|
|
1491
|
+
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))
|
|
1492
|
+
}
|
|
1131
1493
|
Expr::Binary {
|
|
1132
1494
|
left,
|
|
1133
1495
|
op,
|
|
@@ -1313,27 +1675,45 @@ impl Evaluator {
|
|
|
1313
1675
|
let len = arr_mut.len();
|
|
1314
1676
|
let mut indices: Vec<usize> = (0..len).collect();
|
|
1315
1677
|
let arr_values: Vec<Value> = std::mem::take(&mut *arr_mut);
|
|
1678
|
+
// A `throw` from the comparator must propagate (be catchable) and
|
|
1679
|
+
// must NOT corrupt the array. Previously the `_ => Equal` arm
|
|
1680
|
+
// swallowed the `Err` and wrote a bogus reordering back; now we
|
|
1681
|
+
// capture the throw, stop comparing, and restore the original order
|
|
1682
|
+
// — matching the vm/native backends and node.
|
|
1683
|
+
let mut pending: Option<EvalError> = None;
|
|
1316
1684
|
|
|
1317
1685
|
if let Some((scope, params, body)) = self.create_callback_scope(&cmp_fn) {
|
|
1318
1686
|
indices.sort_by(|&i, &j| {
|
|
1319
|
-
|
|
1320
|
-
|
|
1687
|
+
if pending.is_some() {
|
|
1688
|
+
return std::cmp::Ordering::Equal;
|
|
1689
|
+
}
|
|
1690
|
+
match self.call_with_scope(&scope, ¶ms, &body, &[arr_values[i].clone(), arr_values[j].clone()]) {
|
|
1321
1691
|
Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
|
|
1322
1692
|
Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
|
|
1323
|
-
_ => std::cmp::Ordering::Equal,
|
|
1693
|
+
Ok(_) => std::cmp::Ordering::Equal,
|
|
1694
|
+
Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
|
|
1324
1695
|
}
|
|
1325
1696
|
});
|
|
1326
1697
|
} else {
|
|
1327
1698
|
indices.sort_by(|&i, &j| {
|
|
1328
|
-
|
|
1329
|
-
|
|
1699
|
+
if pending.is_some() {
|
|
1700
|
+
return std::cmp::Ordering::Equal;
|
|
1701
|
+
}
|
|
1702
|
+
match self.call_func(&cmp_fn, &[arr_values[i].clone(), arr_values[j].clone()]) {
|
|
1330
1703
|
Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
|
|
1331
1704
|
Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
|
|
1332
|
-
_ => std::cmp::Ordering::Equal,
|
|
1705
|
+
Ok(_) => std::cmp::Ordering::Equal,
|
|
1706
|
+
Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
|
|
1333
1707
|
}
|
|
1334
1708
|
});
|
|
1335
1709
|
}
|
|
1336
1710
|
|
|
1711
|
+
if let Some(e) = pending {
|
|
1712
|
+
// Comparator threw: leave the array untouched and re-raise.
|
|
1713
|
+
*arr_mut = arr_values;
|
|
1714
|
+
drop(arr_mut);
|
|
1715
|
+
return Err(e);
|
|
1716
|
+
}
|
|
1337
1717
|
*arr_mut = indices.into_iter().map(|i| arr_values[i].clone()).collect();
|
|
1338
1718
|
}
|
|
1339
1719
|
} else {
|
|
@@ -1416,7 +1796,11 @@ impl Evaluator {
|
|
|
1416
1796
|
}
|
|
1417
1797
|
"map" => {
|
|
1418
1798
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1419
|
-
|
|
1799
|
+
// #382-style snapshot: own the elements and DROP the array borrow before
|
|
1800
|
+
// any callback runs, so a callback that mutates the array (via the `array`
|
|
1801
|
+
// arg or a capture) can't RefCell-panic. `arr_value` is the JS 3rd arg.
|
|
1802
|
+
let arr_borrow = arr.borrow().clone();
|
|
1803
|
+
let arr_value = Value::Array(arr.clone());
|
|
1420
1804
|
let mut result = Vec::with_capacity(arr_borrow.len());
|
|
1421
1805
|
// Try fastest path: simple single-expression callbacks
|
|
1422
1806
|
let first_result = self.eval_simple_callback(&callback, &[arr_borrow.first().cloned().unwrap_or(Value::Null)]);
|
|
@@ -1433,21 +1817,49 @@ impl Evaluator {
|
|
|
1433
1817
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1434
1818
|
// Reusable scope path
|
|
1435
1819
|
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)])?;
|
|
1820
|
+
let mapped = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1437
1821
|
result.push(mapped);
|
|
1438
1822
|
}
|
|
1439
1823
|
} else {
|
|
1440
1824
|
// Full call_func path
|
|
1441
1825
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1442
|
-
let mapped = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
1826
|
+
let mapped = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1443
1827
|
result.push(mapped);
|
|
1444
1828
|
}
|
|
1445
1829
|
}
|
|
1446
1830
|
return Ok(Value::Array(Rc::new(RefCell::new(result))));
|
|
1447
1831
|
}
|
|
1832
|
+
"flatMap" => {
|
|
1833
|
+
// map + flatten one level (Array.prototype.flatMap). A callback `throw`
|
|
1834
|
+
// propagates via `?` (catchable), matching vm/native/node — previously
|
|
1835
|
+
// interp lacked flatMap entirely ("Not a function").
|
|
1836
|
+
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1837
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1838
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1839
|
+
let mut result: Vec<Value> = Vec::with_capacity(arr_borrow.len());
|
|
1840
|
+
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1841
|
+
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1842
|
+
let mapped = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1843
|
+
match mapped {
|
|
1844
|
+
Value::Array(inner) => result.extend(inner.borrow().iter().cloned()),
|
|
1845
|
+
other => result.push(other),
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
} else {
|
|
1849
|
+
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1850
|
+
let mapped = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1851
|
+
match mapped {
|
|
1852
|
+
Value::Array(inner) => result.extend(inner.borrow().iter().cloned()),
|
|
1853
|
+
other => result.push(other),
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return Ok(Value::Array(Rc::new(RefCell::new(result))));
|
|
1858
|
+
}
|
|
1448
1859
|
"filter" => {
|
|
1449
1860
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1450
|
-
let arr_borrow = arr.borrow();
|
|
1861
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1862
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1451
1863
|
let mut result = Vec::new();
|
|
1452
1864
|
// Try simple callback fast path
|
|
1453
1865
|
let use_simple = arr_borrow.first().map(|v| {
|
|
@@ -1463,14 +1875,14 @@ impl Evaluator {
|
|
|
1463
1875
|
}
|
|
1464
1876
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1465
1877
|
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)])?;
|
|
1878
|
+
let keep = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1467
1879
|
if keep.is_truthy() {
|
|
1468
1880
|
result.push(v.clone());
|
|
1469
1881
|
}
|
|
1470
1882
|
}
|
|
1471
1883
|
} else {
|
|
1472
1884
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1473
|
-
let keep = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
1885
|
+
let keep = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1474
1886
|
if keep.is_truthy() {
|
|
1475
1887
|
result.push(v.clone());
|
|
1476
1888
|
}
|
|
@@ -1480,7 +1892,8 @@ impl Evaluator {
|
|
|
1480
1892
|
}
|
|
1481
1893
|
"reduce" => {
|
|
1482
1894
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1483
|
-
let arr_borrow = arr.borrow();
|
|
1895
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1896
|
+
let arr_value = Value::Array(arr.clone()); // JS 4th callback arg
|
|
1484
1897
|
let (mut acc, start_idx) = if arg_vals.len() > 1 {
|
|
1485
1898
|
(arg_vals[1].clone(), 0)
|
|
1486
1899
|
} else if !arr_borrow.is_empty() {
|
|
@@ -1490,18 +1903,19 @@ impl Evaluator {
|
|
|
1490
1903
|
};
|
|
1491
1904
|
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1492
1905
|
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)])?;
|
|
1906
|
+
acc = self.call_with_scope(&scope, ¶ms, &body, &[acc, v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1494
1907
|
}
|
|
1495
1908
|
} else {
|
|
1496
1909
|
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)])?;
|
|
1910
|
+
acc = self.call_func(&callback, &[acc, v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1498
1911
|
}
|
|
1499
1912
|
}
|
|
1500
1913
|
return Ok(acc);
|
|
1501
1914
|
}
|
|
1502
1915
|
"find" => {
|
|
1503
1916
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1504
|
-
let arr_borrow = arr.borrow();
|
|
1917
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1918
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1505
1919
|
// Try simple callback fast path
|
|
1506
1920
|
let use_simple = arr_borrow.first().map(|v| {
|
|
1507
1921
|
self.eval_simple_callback(&callback, &[v.clone()]).is_some()
|
|
@@ -1516,14 +1930,14 @@ impl Evaluator {
|
|
|
1516
1930
|
}
|
|
1517
1931
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1518
1932
|
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)])?;
|
|
1933
|
+
let found = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1520
1934
|
if found.is_truthy() {
|
|
1521
1935
|
return Ok(v.clone());
|
|
1522
1936
|
}
|
|
1523
1937
|
}
|
|
1524
1938
|
} else {
|
|
1525
1939
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1526
|
-
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
1940
|
+
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1527
1941
|
if found.is_truthy() {
|
|
1528
1942
|
return Ok(v.clone());
|
|
1529
1943
|
}
|
|
@@ -1533,17 +1947,18 @@ impl Evaluator {
|
|
|
1533
1947
|
}
|
|
1534
1948
|
"findIndex" => {
|
|
1535
1949
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1536
|
-
let arr_borrow = arr.borrow();
|
|
1950
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1951
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1537
1952
|
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1538
1953
|
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)])?;
|
|
1954
|
+
let found = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1540
1955
|
if found.is_truthy() {
|
|
1541
1956
|
return Ok(Value::Number(i as f64));
|
|
1542
1957
|
}
|
|
1543
1958
|
}
|
|
1544
1959
|
} else {
|
|
1545
1960
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1546
|
-
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
1961
|
+
let found = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1547
1962
|
if found.is_truthy() {
|
|
1548
1963
|
return Ok(Value::Number(i as f64));
|
|
1549
1964
|
}
|
|
@@ -1552,13 +1967,14 @@ impl Evaluator {
|
|
|
1552
1967
|
return Ok(Value::Number(-1.0));
|
|
1553
1968
|
}
|
|
1554
1969
|
"findLast" => {
|
|
1555
|
-
// Like find, from the end (#247). Callback gets (value, original index).
|
|
1970
|
+
// Like find, from the end (#247). Callback gets (value, original index, array).
|
|
1556
1971
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1557
|
-
let arr_borrow = arr.borrow();
|
|
1972
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1973
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1558
1974
|
let scoped = self.create_callback_scope(&callback);
|
|
1559
1975
|
for i in (0..arr_borrow.len()).rev() {
|
|
1560
1976
|
let v = arr_borrow[i].clone();
|
|
1561
|
-
let args = [v.clone(), Value::Number(i as f64)];
|
|
1977
|
+
let args = [v.clone(), Value::Number(i as f64), arr_value.clone()];
|
|
1562
1978
|
let found = match &scoped {
|
|
1563
1979
|
Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
|
|
1564
1980
|
None => self.call_func(&callback, &args)?,
|
|
@@ -1571,10 +1987,11 @@ impl Evaluator {
|
|
|
1571
1987
|
}
|
|
1572
1988
|
"findLastIndex" => {
|
|
1573
1989
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1574
|
-
let arr_borrow = arr.borrow();
|
|
1990
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
1991
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1575
1992
|
let scoped = self.create_callback_scope(&callback);
|
|
1576
1993
|
for i in (0..arr_borrow.len()).rev() {
|
|
1577
|
-
let args = [arr_borrow[i].clone(), Value::Number(i as f64)];
|
|
1994
|
+
let args = [arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()];
|
|
1578
1995
|
let found = match &scoped {
|
|
1579
1996
|
Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
|
|
1580
1997
|
None => self.call_func(&callback, &args)?,
|
|
@@ -1601,21 +2018,23 @@ impl Evaluator {
|
|
|
1601
2018
|
}
|
|
1602
2019
|
"forEach" => {
|
|
1603
2020
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1604
|
-
let arr_borrow = arr.borrow();
|
|
2021
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2022
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1605
2023
|
if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1606
2024
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1607
|
-
self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64)])?;
|
|
2025
|
+
self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1608
2026
|
}
|
|
1609
2027
|
} else {
|
|
1610
2028
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1611
|
-
self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2029
|
+
self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1612
2030
|
}
|
|
1613
2031
|
}
|
|
1614
2032
|
return Ok(Value::Null);
|
|
1615
2033
|
}
|
|
1616
2034
|
"some" => {
|
|
1617
2035
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1618
|
-
let arr_borrow = arr.borrow();
|
|
2036
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2037
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1619
2038
|
// Try simple callback fast path
|
|
1620
2039
|
let use_simple = arr_borrow.first().map(|v| {
|
|
1621
2040
|
self.eval_simple_callback(&callback, &[v.clone()]).is_some()
|
|
@@ -1630,14 +2049,14 @@ impl Evaluator {
|
|
|
1630
2049
|
}
|
|
1631
2050
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1632
2051
|
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)])?;
|
|
2052
|
+
let result = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1634
2053
|
if result.is_truthy() {
|
|
1635
2054
|
return Ok(Value::Bool(true));
|
|
1636
2055
|
}
|
|
1637
2056
|
}
|
|
1638
2057
|
} else {
|
|
1639
2058
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1640
|
-
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2059
|
+
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1641
2060
|
if result.is_truthy() {
|
|
1642
2061
|
return Ok(Value::Bool(true));
|
|
1643
2062
|
}
|
|
@@ -1647,7 +2066,8 @@ impl Evaluator {
|
|
|
1647
2066
|
}
|
|
1648
2067
|
"every" => {
|
|
1649
2068
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1650
|
-
let arr_borrow = arr.borrow();
|
|
2069
|
+
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
2070
|
+
let arr_value = Value::Array(arr.clone()); // JS 3rd callback arg
|
|
1651
2071
|
// Try simple callback fast path
|
|
1652
2072
|
let use_simple = arr_borrow.first().map(|v| {
|
|
1653
2073
|
self.eval_simple_callback(&callback, &[v.clone()]).is_some()
|
|
@@ -1662,14 +2082,14 @@ impl Evaluator {
|
|
|
1662
2082
|
}
|
|
1663
2083
|
} else if let Some((scope, params, body)) = self.create_callback_scope(&callback) {
|
|
1664
2084
|
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)])?;
|
|
2085
|
+
let result = self.call_with_scope(&scope, ¶ms, &body, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1666
2086
|
if !result.is_truthy() {
|
|
1667
2087
|
return Ok(Value::Bool(false));
|
|
1668
2088
|
}
|
|
1669
2089
|
}
|
|
1670
2090
|
} else {
|
|
1671
2091
|
for (i, v) in arr_borrow.iter().enumerate() {
|
|
1672
|
-
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64)])?;
|
|
2092
|
+
let result = self.call_func(&callback, &[v.clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
1673
2093
|
if !result.is_truthy() {
|
|
1674
2094
|
return Ok(Value::Bool(false));
|
|
1675
2095
|
}
|
|
@@ -1882,12 +2302,12 @@ impl Evaluator {
|
|
|
1882
2302
|
return Ok(Value::String(s.replace(&search, &replacement).into()));
|
|
1883
2303
|
}
|
|
1884
2304
|
"charAt" => {
|
|
2305
|
+
// Cursor cache instead of collecting a fresh Vec<char> each call (#203).
|
|
1885
2306
|
let idx = match arg_vals.first() {
|
|
1886
2307
|
Some(Value::Number(n)) => *n as usize,
|
|
1887
2308
|
_ => 0,
|
|
1888
2309
|
};
|
|
1889
|
-
|
|
1890
|
-
return Ok(chars.get(idx)
|
|
2310
|
+
return Ok(nth_char_cached(s, idx)
|
|
1891
2311
|
.map(|c| Value::String(c.to_string().into()))
|
|
1892
2312
|
.unwrap_or(Value::String("".into())));
|
|
1893
2313
|
}
|
|
@@ -1897,11 +2317,11 @@ impl Evaluator {
|
|
|
1897
2317
|
Some(Value::Number(n)) => *n as i64,
|
|
1898
2318
|
_ => 0,
|
|
1899
2319
|
};
|
|
1900
|
-
let
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2320
|
+
let idx = if i < 0 { s.chars().count() as i64 + i } else { i };
|
|
2321
|
+
if idx >= 0 {
|
|
2322
|
+
if let Some(c) = nth_char_cached(s, idx as usize) {
|
|
2323
|
+
return Ok(Value::String(c.to_string().into()));
|
|
2324
|
+
}
|
|
1905
2325
|
}
|
|
1906
2326
|
return Ok(Value::Null);
|
|
1907
2327
|
}
|
|
@@ -1910,9 +2330,8 @@ impl Evaluator {
|
|
|
1910
2330
|
Some(Value::Number(n)) => *n as usize,
|
|
1911
2331
|
_ => 0,
|
|
1912
2332
|
};
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
.map(|c| Value::Number(*c as u32 as f64))
|
|
2333
|
+
return Ok(nth_char_cached(s, idx)
|
|
2334
|
+
.map(|c| Value::Number(c as u32 as f64))
|
|
1916
2335
|
.unwrap_or(Value::Number(f64::NAN)));
|
|
1917
2336
|
}
|
|
1918
2337
|
"repeat" => {
|
|
@@ -2158,6 +2577,9 @@ impl Evaluator {
|
|
|
2158
2577
|
}
|
|
2159
2578
|
Expr::Assign { name, value, .. } => {
|
|
2160
2579
|
let v = self.eval_expr(value)?;
|
|
2580
|
+
// A plain assignment overwrites the variable, so drop any builder buffering it (the
|
|
2581
|
+
// buffered value is about to be replaced). `value` may itself have read+flushed it.
|
|
2582
|
+
self.discard_pending_for(name.as_ref());
|
|
2161
2583
|
match self.scope.borrow_mut().assign(name.as_ref(), v.clone()) {
|
|
2162
2584
|
Ok(true) => Ok(v),
|
|
2163
2585
|
Ok(false) => Err(EvalError::Error(format!("Undefined variable: {}", name))),
|
|
@@ -2306,6 +2728,10 @@ impl Evaluator {
|
|
|
2306
2728
|
}
|
|
2307
2729
|
}
|
|
2308
2730
|
Expr::CompoundAssign { name, op, value, .. } => {
|
|
2731
|
+
// Expression-position `+=` (result is used): flush any builder so `current` is the
|
|
2732
|
+
// full string, then take the generic path. The O(1) builder fast path is reserved
|
|
2733
|
+
// for statement position (see `eval_expr_discard`).
|
|
2734
|
+
self.flush_pending_for(name.as_ref());
|
|
2309
2735
|
let current = self.scope.borrow().get(name.as_ref())
|
|
2310
2736
|
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
2311
2737
|
let rhs = self.eval_expr(value)?;
|
|
@@ -2324,6 +2750,7 @@ impl Evaluator {
|
|
|
2324
2750
|
}
|
|
2325
2751
|
}
|
|
2326
2752
|
Expr::LogicalAssign { name, op, value, .. } => {
|
|
2753
|
+
self.flush_pending_for(name.as_ref());
|
|
2327
2754
|
let current = self.scope.borrow().get(name.as_ref())
|
|
2328
2755
|
.ok_or_else(|| EvalError::Error(format!("Undefined variable: {}", name)))?;
|
|
2329
2756
|
let result = match op {
|
|
@@ -2750,6 +3177,9 @@ impl Evaluator {
|
|
|
2750
3177
|
module_cache: Rc::clone(&self.module_cache),
|
|
2751
3178
|
current_dir: RefCell::new(self.current_dir.borrow().clone()),
|
|
2752
3179
|
virtual_builtins: Rc::clone(&self.virtual_builtins),
|
|
3180
|
+
string_builder: Rc::clone(&self.string_builder),
|
|
3181
|
+
call_depth: Rc::clone(&self.call_depth),
|
|
3182
|
+
max_call_depth: self.max_call_depth,
|
|
2753
3183
|
};
|
|
2754
3184
|
match eval.eval_statement(body) {
|
|
2755
3185
|
Ok(v) => Ok(v),
|
|
@@ -3018,6 +3448,9 @@ impl Evaluator {
|
|
|
3018
3448
|
module_cache: Rc::clone(&self.module_cache),
|
|
3019
3449
|
current_dir: RefCell::new(self.current_dir.borrow().clone()),
|
|
3020
3450
|
virtual_builtins: Rc::clone(&self.virtual_builtins),
|
|
3451
|
+
string_builder: Rc::clone(&self.string_builder),
|
|
3452
|
+
call_depth: Rc::clone(&self.call_depth),
|
|
3453
|
+
max_call_depth: self.max_call_depth,
|
|
3021
3454
|
};
|
|
3022
3455
|
{
|
|
3023
3456
|
let mut s = scope.borrow_mut();
|
|
@@ -3074,6 +3507,20 @@ impl Evaluator {
|
|
|
3074
3507
|
// VM's single `run_chunk` re-entry. 128 KiB is smaller than one level's chain, so the
|
|
3075
3508
|
// stack overflows BETWEEN checks; 1 MiB comfortably covers a level (verified to depth
|
|
3076
3509
|
// 20000 in both debug and release). 16 MiB segments keep grow frequency low.
|
|
3510
|
+
// #381: bound recursion with a catchable `RangeError` instead of letting `stacker`
|
|
3511
|
+
// grow the stack toward OOM/abort. The counter is shared (`Rc`) across every call
|
|
3512
|
+
// frame, so it measures true nesting depth; it is decremented on the way out (both
|
|
3513
|
+
// the Ok and Err paths) via the explicit `set` below so a caught throw doesn't leak
|
|
3514
|
+
// depth.
|
|
3515
|
+
let depth = eval.call_depth.get() + 1;
|
|
3516
|
+
if depth > eval.max_call_depth {
|
|
3517
|
+
let err = crate::natives::range_error_construct(&[Value::String(
|
|
3518
|
+
"Maximum call stack size exceeded".into(),
|
|
3519
|
+
)])
|
|
3520
|
+
.unwrap_or(Value::Null);
|
|
3521
|
+
return Err(EvalError::Throw(err));
|
|
3522
|
+
}
|
|
3523
|
+
eval.call_depth.set(depth);
|
|
3077
3524
|
let body_result = {
|
|
3078
3525
|
#[cfg(not(target_arch = "wasm32"))]
|
|
3079
3526
|
{
|
|
@@ -3086,6 +3533,7 @@ impl Evaluator {
|
|
|
3086
3533
|
eval.eval_statement(body)
|
|
3087
3534
|
}
|
|
3088
3535
|
};
|
|
3536
|
+
eval.call_depth.set(depth - 1);
|
|
3089
3537
|
match body_result {
|
|
3090
3538
|
Ok(v) => Ok(v),
|
|
3091
3539
|
Err(EvalError::Return(v)) => Ok(v),
|
|
@@ -3566,7 +4014,7 @@ impl Evaluator {
|
|
|
3566
4014
|
}
|
|
3567
4015
|
Value::String(s) => {
|
|
3568
4016
|
if key == "length" {
|
|
3569
|
-
Ok(Value::Number(s
|
|
4017
|
+
Ok(Value::Number(char_count_cached(s) as f64))
|
|
3570
4018
|
} else {
|
|
3571
4019
|
Ok(Value::Null)
|
|
3572
4020
|
}
|
|
@@ -3623,6 +4071,11 @@ impl Evaluator {
|
|
|
3623
4071
|
_ => Ok(Value::Null),
|
|
3624
4072
|
}
|
|
3625
4073
|
}
|
|
4074
|
+
// Reading a property of the nullish value throws a catchable `TypeError`, matching the
|
|
4075
|
+
// bytecode VM (`get_member`'s `_` arm), cranelift/wasi, and node — not a silent `null`.
|
|
4076
|
+
// The tree-walker used to fall through to `_ => Ok(Value::Null)`, so `null.length` read
|
|
4077
|
+
// back as `null` on interp while every other backend threw (a pure interp≠vm bug).
|
|
4078
|
+
Value::Null => Err(format!("Cannot read property '{}' of null", key)),
|
|
3626
4079
|
_ => Ok(Value::Null),
|
|
3627
4080
|
}
|
|
3628
4081
|
}
|
|
@@ -3644,9 +4097,7 @@ impl Evaluator {
|
|
|
3644
4097
|
Value::Number(n) if *n >= 0.0 && n.fract() == 0.0 => *n as usize,
|
|
3645
4098
|
_ => return Ok(Value::Null),
|
|
3646
4099
|
};
|
|
3647
|
-
Ok(s
|
|
3648
|
-
.chars()
|
|
3649
|
-
.nth(idx)
|
|
4100
|
+
Ok(nth_char_cached(s, idx)
|
|
3650
4101
|
.map(|c| Value::String(c.to_string().into()))
|
|
3651
4102
|
.unwrap_or(Value::Null))
|
|
3652
4103
|
}
|
|
@@ -3659,6 +4110,9 @@ impl Evaluator {
|
|
|
3659
4110
|
};
|
|
3660
4111
|
self.get_prop(obj, key)
|
|
3661
4112
|
}
|
|
4113
|
+
// Indexing the nullish value throws a catchable `TypeError` (like `get_prop` above and
|
|
4114
|
+
// the VM/cranelift/wasi/node) rather than silently reading back `null`.
|
|
4115
|
+
Value::Null => Err(format!("Cannot read property '{}' of null", index)),
|
|
3662
4116
|
_ => Ok(Value::Null),
|
|
3663
4117
|
}
|
|
3664
4118
|
}
|
|
@@ -3677,8 +4131,13 @@ impl Evaluator {
|
|
|
3677
4131
|
}
|
|
3678
4132
|
}
|
|
3679
4133
|
|
|
3680
|
-
|
|
3681
|
-
|
|
4134
|
+
/// #381 — ancestor-guarded like `tishlang_core::json_stringify_into_guarded`: a back-edge to an
|
|
4135
|
+
/// ancestor (`a.self = a`) is a cycle and returns `Err(())` instead of recursing forever (this
|
|
4136
|
+
/// was an unguarded native-stack overflow → uncatchable abort; core's guard from #389 never
|
|
4137
|
+
/// covered the interpreter's own stringifier). Ancestor-path only, not all-visited: a node
|
|
4138
|
+
/// repeated across sibling branches is a legal DAG and must serialize twice.
|
|
4139
|
+
fn json_stringify_value(v: &Value, ancestors: &mut Vec<*const ()>) -> Result<String, ()> {
|
|
4140
|
+
Ok(match v {
|
|
3682
4141
|
Value::Null => "null".to_string(),
|
|
3683
4142
|
Value::Bool(b) => b.to_string(),
|
|
3684
4143
|
Value::Number(n) => {
|
|
@@ -3698,28 +4157,39 @@ impl Evaluator {
|
|
|
3698
4157
|
.replace('\t', "\\t")
|
|
3699
4158
|
),
|
|
3700
4159
|
Value::Array(arr) => {
|
|
3701
|
-
let
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
4160
|
+
let ptr = Rc::as_ptr(arr) as *const ();
|
|
4161
|
+
if ancestors.contains(&ptr) {
|
|
4162
|
+
return Err(());
|
|
4163
|
+
}
|
|
4164
|
+
ancestors.push(ptr);
|
|
4165
|
+
let borrowed = arr.borrow();
|
|
4166
|
+
let mut inner: Vec<String> = Vec::with_capacity(borrowed.len());
|
|
4167
|
+
for item in borrowed.iter() {
|
|
4168
|
+
inner.push(Self::json_stringify_value(item, ancestors)?);
|
|
4169
|
+
}
|
|
4170
|
+
drop(borrowed);
|
|
4171
|
+
ancestors.pop();
|
|
3706
4172
|
format!("[{}]", inner.join(","))
|
|
3707
4173
|
}
|
|
3708
4174
|
Value::Object(map) => {
|
|
4175
|
+
let ptr = Rc::as_ptr(map) as *const ();
|
|
4176
|
+
if ancestors.contains(&ptr) {
|
|
4177
|
+
return Err(());
|
|
4178
|
+
}
|
|
4179
|
+
ancestors.push(ptr);
|
|
3709
4180
|
// Insertion order (PropMap is an IndexMap) — matches JS/Node and the
|
|
3710
4181
|
// VM/rust backends. No key sort.
|
|
3711
|
-
let
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
.
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
.collect();
|
|
4182
|
+
let borrowed = map.borrow();
|
|
4183
|
+
let mut entries: Vec<String> = Vec::with_capacity(borrowed.strings.len());
|
|
4184
|
+
for (k, v) in borrowed.strings.iter() {
|
|
4185
|
+
entries.push(format!(
|
|
4186
|
+
"\"{}\":{}",
|
|
4187
|
+
k.replace('\\', "\\\\").replace('"', "\\\""),
|
|
4188
|
+
Self::json_stringify_value(v, ancestors)?
|
|
4189
|
+
));
|
|
4190
|
+
}
|
|
4191
|
+
drop(borrowed);
|
|
4192
|
+
ancestors.pop();
|
|
3723
4193
|
format!("{{{}}}", entries.join(","))
|
|
3724
4194
|
}
|
|
3725
4195
|
Value::Symbol(_) => "null".to_string(),
|
|
@@ -3738,7 +4208,7 @@ impl Evaluator {
|
|
|
3738
4208
|
#[cfg(feature = "regex")]
|
|
3739
4209
|
Value::RegExp(_) => "null".to_string(),
|
|
3740
4210
|
Value::Opaque(_) | Value::OpaqueMethod(_, _) => "null".to_string(),
|
|
3741
|
-
}
|
|
4211
|
+
})
|
|
3742
4212
|
}
|
|
3743
4213
|
|
|
3744
4214
|
// Static native wrapper functions (these need to be fn pointers, not closures with &self)
|
|
@@ -3749,7 +4219,13 @@ impl Evaluator {
|
|
|
3749
4219
|
|
|
3750
4220
|
fn json_stringify_native(args: &[Value]) -> Result<Value, String> {
|
|
3751
4221
|
let v = args.first().cloned().unwrap_or(Value::Null);
|
|
3752
|
-
|
|
4222
|
+
let mut ancestors: Vec<*const ()> = Vec::new();
|
|
4223
|
+
match Self::json_stringify_value(&v, &mut ancestors) {
|
|
4224
|
+
Ok(s) => Ok(Value::String(s.into())),
|
|
4225
|
+
// Surfaced by `call_func` as `EvalError::Error`, which the Try handler boxes as
|
|
4226
|
+
// `{ name: "TypeError", message }` — node-identical for the circular case (#381).
|
|
4227
|
+
Err(()) => Err("Converting circular structure to JSON".to_string()),
|
|
4228
|
+
}
|
|
3753
4229
|
}
|
|
3754
4230
|
|
|
3755
4231
|
fn object_keys(args: &[Value]) -> Result<Value, String> {
|
|
@@ -4214,3 +4690,126 @@ impl std::fmt::Display for EvalError {
|
|
|
4214
4690
|
}
|
|
4215
4691
|
|
|
4216
4692
|
impl std::error::Error for EvalError {}
|
|
4693
|
+
|
|
4694
|
+
#[cfg(test)]
|
|
4695
|
+
mod recursion_limit_tests_381 {
|
|
4696
|
+
use super::Evaluator;
|
|
4697
|
+
use tishlang_parser::parse;
|
|
4698
|
+
|
|
4699
|
+
fn run_with_depth(src: &str, max_depth: usize) -> String {
|
|
4700
|
+
let program = parse(src).unwrap();
|
|
4701
|
+
let mut eval = Evaluator::new();
|
|
4702
|
+
eval.max_call_depth = max_depth;
|
|
4703
|
+
eval.eval_program(&program).unwrap().to_string()
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
#[test]
|
|
4707
|
+
fn deep_recursion_throws_catchable_range_error() {
|
|
4708
|
+
// Infinite (non-tail) recursion past the limit must throw a CATCHABLE RangeError, not abort:
|
|
4709
|
+
// try/catch recovers and the program keeps running.
|
|
4710
|
+
let src = "fn rec(n) { return 1 + rec(n + 1) }\n\
|
|
4711
|
+
let name = 'none'\n\
|
|
4712
|
+
try { rec(0) } catch (e) { name = e.name }\n\
|
|
4713
|
+
name";
|
|
4714
|
+
assert_eq!(run_with_depth(src, 200), "RangeError");
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
#[test]
|
|
4718
|
+
fn normal_recursion_is_unaffected() {
|
|
4719
|
+
let src = "fn fib(n) { if (n < 2) { return n } return fib(n - 1) + fib(n - 2) }\nfib(12)";
|
|
4720
|
+
assert_eq!(run_with_depth(src, 20000), "144");
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
#[cfg(test)]
|
|
4725
|
+
mod null_member_access_tests {
|
|
4726
|
+
// Reading a property or index of the nullish value throws a catchable `TypeError`, matching the
|
|
4727
|
+
// bytecode VM / cranelift / wasi / node. The tree-walker used to fall through to `Ok(Null)`, so
|
|
4728
|
+
// `null.length` read back as `null` on interp while every other backend threw (a pure interp≠vm
|
|
4729
|
+
// divergence). These lock the interpreter to the throwing behavior.
|
|
4730
|
+
use super::Evaluator;
|
|
4731
|
+
use tishlang_parser::parse;
|
|
4732
|
+
|
|
4733
|
+
fn run(src: &str) -> String {
|
|
4734
|
+
let program = parse(src).unwrap();
|
|
4735
|
+
let mut eval = Evaluator::new();
|
|
4736
|
+
eval.eval_program(&program).unwrap().to_string()
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
#[test]
|
|
4740
|
+
fn null_property_read_throws_type_error() {
|
|
4741
|
+
let src = "let name = 'none'\n\
|
|
4742
|
+
try { let z = null; z.length } catch (e) { name = e.name }\n\
|
|
4743
|
+
name";
|
|
4744
|
+
assert_eq!(run(src), "TypeError");
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
#[test]
|
|
4748
|
+
fn null_index_read_throws_type_error() {
|
|
4749
|
+
let src = "let name = 'none'\n\
|
|
4750
|
+
try { let z = null; z[0] } catch (e) { name = e.name }\n\
|
|
4751
|
+
name";
|
|
4752
|
+
assert_eq!(run(src), "TypeError");
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
#[test]
|
|
4756
|
+
fn object_missing_property_still_reads_null() {
|
|
4757
|
+
// Guard against over-reach: a MISSING property of a real object is `null`, not a throw.
|
|
4758
|
+
let src = "let o = { a: 1 }\nString(o.b === null)";
|
|
4759
|
+
assert_eq!(run(src), "true");
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
#[test]
|
|
4763
|
+
fn array_oob_index_still_reads_null() {
|
|
4764
|
+
// Out-of-bounds array index stays nullish (only a null/undefined *receiver* throws).
|
|
4765
|
+
let src = "let a = [1, 2, 3]\nString(a[9] === null)";
|
|
4766
|
+
assert_eq!(run(src), "true");
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
#[cfg(test)]
|
|
4771
|
+
mod array_hof_callback_arg_tests {
|
|
4772
|
+
// Array HOFs pass the source array as the trailing callback arg (JS `(element, index, array)`,
|
|
4773
|
+
// reduce `(acc, element, index, array)`). The inline interp loops snapshot the backing store and
|
|
4774
|
+
// drop the borrow before any callback, so a callback that MUTATES the array via the `array` arg
|
|
4775
|
+
// (or a capture) can't RefCell-panic — it iterates the pre-call snapshot, matching JS + the shared
|
|
4776
|
+
// #382 builtins path.
|
|
4777
|
+
use super::Evaluator;
|
|
4778
|
+
use tishlang_parser::parse;
|
|
4779
|
+
|
|
4780
|
+
fn run(src: &str) -> String {
|
|
4781
|
+
let program = parse(src).unwrap();
|
|
4782
|
+
let mut eval = Evaluator::new();
|
|
4783
|
+
eval.eval_program(&program).unwrap().to_string()
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
#[test]
|
|
4787
|
+
fn map_receives_array_as_third_arg() {
|
|
4788
|
+
assert_eq!(run("[5, 3, 8].map((x, i, arr) => arr.length).join(',')"), "3,3,3");
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
#[test]
|
|
4792
|
+
fn reduce_receives_array_as_fourth_arg() {
|
|
4793
|
+
assert_eq!(run("[1, 2, 3].reduce((a, x, i, arr) => a + arr.length, 0)"), "9");
|
|
4794
|
+
}
|
|
4795
|
+
|
|
4796
|
+
#[test]
|
|
4797
|
+
fn callback_may_index_the_array_arg() {
|
|
4798
|
+
assert_eq!(run("[10, 20, 30].map((x, i, arr) => arr[i] * 2).join(',')"), "20,40,60");
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
#[test]
|
|
4802
|
+
fn callback_mutating_the_array_arg_does_not_panic() {
|
|
4803
|
+
// Pre-snapshot this RefCell-panicked (map held the borrow across the loop). Now iteration is
|
|
4804
|
+
// over the snapshot (3 elements); the 3 pushes land on the live array → final length 6, matching
|
|
4805
|
+
// JS forEach semantics (does not visit elements appended during iteration).
|
|
4806
|
+
let src = "let a = [1, 2, 3]\na.forEach((x, i, arr) => { arr.push(x) })\na.length";
|
|
4807
|
+
assert_eq!(run(src), "6");
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
#[test]
|
|
4811
|
+
fn one_and_two_arg_callbacks_unchanged() {
|
|
4812
|
+
assert_eq!(run("[5, 3, 8].map(x => x * 2).join(',')"), "10,6,16");
|
|
4813
|
+
assert_eq!(run("[5, 3, 8].map((x, i) => x + i).join(',')"), "5,4,10");
|
|
4814
|
+
}
|
|
4815
|
+
}
|