@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
|
@@ -98,6 +98,21 @@ fn resolve_import_to_key(
|
|
|
98
98
|
))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/// The module specifier a statement imports/re-exports from, if any — covers both
|
|
102
|
+
/// `import … from "x"` and `export … from "x"` (#305 re-export). Dependency discovery and cycle
|
|
103
|
+
/// detection use this so they follow re-export edges, not just plain imports (otherwise a module
|
|
104
|
+
/// reached only via a re-export would never be loaded).
|
|
105
|
+
fn stmt_module_source(stmt: &Statement) -> Option<&str> {
|
|
106
|
+
match stmt {
|
|
107
|
+
Statement::Import { from, .. } => Some(&**from),
|
|
108
|
+
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
109
|
+
ExportDeclaration::ReExport { from, .. } => Some(&**from),
|
|
110
|
+
_ => None,
|
|
111
|
+
},
|
|
112
|
+
_ => None,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
/// Resolve all modules starting from the entry file. Returns modules in dependency order.
|
|
102
117
|
pub fn resolve_virtual(
|
|
103
118
|
entry_path: &str,
|
|
@@ -163,7 +178,7 @@ fn load_module_recursive(
|
|
|
163
178
|
|
|
164
179
|
let from_dir = parent_dir(module_path);
|
|
165
180
|
for stmt in &program.statements {
|
|
166
|
-
if let
|
|
181
|
+
if let Some(from) = stmt_module_source(stmt) {
|
|
167
182
|
if is_native_import(from) {
|
|
168
183
|
continue;
|
|
169
184
|
}
|
|
@@ -217,7 +232,7 @@ fn has_cycle_from(
|
|
|
217
232
|
visiting: &mut HashSet<usize>,
|
|
218
233
|
) -> Result<bool, String> {
|
|
219
234
|
for stmt in &program.statements {
|
|
220
|
-
if let
|
|
235
|
+
if let Some(from) = stmt_module_source(stmt) {
|
|
221
236
|
if is_native_import(from) {
|
|
222
237
|
continue;
|
|
223
238
|
}
|
|
@@ -292,6 +307,40 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
292
307
|
let default_name = format!("__default_{}", idx);
|
|
293
308
|
module_exports[idx].insert("default".to_string(), default_name);
|
|
294
309
|
}
|
|
310
|
+
// #305: re-export — map each re-exported name to the DEP's binding (the dep is
|
|
311
|
+
// earlier in load order, so its export table is already built). `export *` copies
|
|
312
|
+
// every dep export without overriding an explicit one; `export { a as b }` maps
|
|
313
|
+
// b -> dep's binding for a. Mirrors `merge_modules` in tish_compile/src/resolve.rs.
|
|
314
|
+
ExportDeclaration::ReExport {
|
|
315
|
+
specifiers,
|
|
316
|
+
all,
|
|
317
|
+
from,
|
|
318
|
+
..
|
|
319
|
+
} => {
|
|
320
|
+
let dir = parent_dir(&module.path);
|
|
321
|
+
let dep = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)
|
|
322
|
+
.ok()
|
|
323
|
+
.and_then(|key| path_to_idx.get(&key).copied())
|
|
324
|
+
.map(|dep_idx| module_exports[dep_idx].clone());
|
|
325
|
+
if let Some(dep) = dep {
|
|
326
|
+
if *all {
|
|
327
|
+
for (k, v) in &dep {
|
|
328
|
+
module_exports[idx]
|
|
329
|
+
.entry(k.clone())
|
|
330
|
+
.or_insert_with(|| v.clone());
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for spec in specifiers {
|
|
334
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
335
|
+
if let Some(binding) = dep.get(name.as_ref()) {
|
|
336
|
+
let export_name =
|
|
337
|
+
alias.as_deref().unwrap_or(name.as_ref()).to_string();
|
|
338
|
+
module_exports[idx].insert(export_name, binding.clone());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
295
344
|
}
|
|
296
345
|
}
|
|
297
346
|
}
|
|
@@ -440,6 +489,10 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
|
|
|
440
489
|
span: espan,
|
|
441
490
|
});
|
|
442
491
|
}
|
|
492
|
+
// #305: re-export emits no code — `module_exports` already maps the re-exported
|
|
493
|
+
// names to the dep's bindings (in scope in the flattened bundle), so downstream
|
|
494
|
+
// imports resolve directly. Nothing to push here.
|
|
495
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
443
496
|
},
|
|
444
497
|
_ => statements.push(stmt.clone()),
|
|
445
498
|
}
|
|
@@ -471,4 +524,49 @@ mod tests {
|
|
|
471
524
|
let program = merge_modules_virtual(modules).unwrap();
|
|
472
525
|
assert!(!program.statements.is_empty());
|
|
473
526
|
}
|
|
527
|
+
|
|
528
|
+
#[test]
|
|
529
|
+
fn test_resolve_virtual_reexport_alias() {
|
|
530
|
+
// #305: `export { add as plus } from "./dep"` re-exports dep's `add` as `plus`; a downstream
|
|
531
|
+
// `import { plus }` must resolve to dep's binding. Regression guard: the merge previously did
|
|
532
|
+
// not handle `ExportDeclaration::ReExport` (E0004 build failure), and discovery did not follow
|
|
533
|
+
// the re-export edge (so a dep reached only via re-export was never loaded).
|
|
534
|
+
let mut files = HashMap::new();
|
|
535
|
+
files.insert(
|
|
536
|
+
"dep.tish".to_string(),
|
|
537
|
+
"export fn add(a, b) { return a + b }".to_string(),
|
|
538
|
+
);
|
|
539
|
+
files.insert(
|
|
540
|
+
"mid.tish".to_string(),
|
|
541
|
+
"export { add as plus } from \"./dep.tish\"".to_string(),
|
|
542
|
+
);
|
|
543
|
+
files.insert(
|
|
544
|
+
"main.tish".to_string(),
|
|
545
|
+
"import { plus } from \"./mid.tish\"\nconsole.log(plus(1, 2))".to_string(),
|
|
546
|
+
);
|
|
547
|
+
let modules = resolve_virtual("main.tish", &files).unwrap();
|
|
548
|
+
// dep.tish is reached ONLY via mid's re-export → discovery must have loaded it.
|
|
549
|
+
assert!(
|
|
550
|
+
modules.iter().any(|m| m.path == "dep.tish"),
|
|
551
|
+
"re-exported dep should be discovered and loaded"
|
|
552
|
+
);
|
|
553
|
+
detect_cycles_virtual(&modules).unwrap();
|
|
554
|
+
let program = merge_modules_virtual(modules).unwrap();
|
|
555
|
+
let has_add_fn = program
|
|
556
|
+
.statements
|
|
557
|
+
.iter()
|
|
558
|
+
.any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "add"));
|
|
559
|
+
let binds_plus_to_add = program.statements.iter().any(|s| {
|
|
560
|
+
matches!(
|
|
561
|
+
s,
|
|
562
|
+
Statement::VarDecl { name, init: Some(Expr::Ident { name: src, .. }), .. }
|
|
563
|
+
if name.as_ref() == "plus" && src.as_ref() == "add"
|
|
564
|
+
)
|
|
565
|
+
});
|
|
566
|
+
assert!(has_add_fn, "dep's `add` fn should be in the merged bundle");
|
|
567
|
+
assert!(
|
|
568
|
+
binds_plus_to_add,
|
|
569
|
+
"import of re-exported `plus` should bind to dep's `add`"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
474
572
|
}
|
|
@@ -25,6 +25,10 @@ smallvec = "1"
|
|
|
25
25
|
indexmap = "2"
|
|
26
26
|
# Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
|
|
27
27
|
itoa = "1"
|
|
28
|
+
# Fast shortest-float→decimal for the number→string general path (JSON/template/log). ryu writes the
|
|
29
|
+
# shortest round-trip digits straight to a buffer with none of `core::fmt`'s `{:e}` Formatter overhead
|
|
30
|
+
# (profiled as ~40% of JSON.stringify self-time on numeric payloads). Already in the workspace lock.
|
|
31
|
+
ryu = "1"
|
|
28
32
|
fancy-regex = { version = "0.17.0", optional = true }
|
|
29
33
|
# Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
|
|
30
34
|
# single atomic with no pthread syscall — profiled as a top cost on object/array access.
|
|
@@ -80,10 +80,31 @@ pub fn json_parse(json: &str) -> Result<Value, String> {
|
|
|
80
80
|
/// per-node `String` only to copy + drop it on the way back up. For a
|
|
81
81
|
/// 20-row TFB `/queries` response (~40 numbers, 2 keys × 20 = ~80 string
|
|
82
82
|
/// ops) that saves dozens of small allocations per request.
|
|
83
|
+
fn json_stringify_capacity_hint(value: &Value) -> usize {
|
|
84
|
+
match value {
|
|
85
|
+
Value::Array(arr) => {
|
|
86
|
+
let n = arr.borrow().len();
|
|
87
|
+
if n > 64 {
|
|
88
|
+
// json_roundtrip / large API payloads: ~80–100 B per row is typical.
|
|
89
|
+
n.saturating_mul(96).max(256)
|
|
90
|
+
} else {
|
|
91
|
+
256
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
Value::Object(obj) => {
|
|
95
|
+
let n = obj.borrow().strings.len();
|
|
96
|
+
if n > 32 {
|
|
97
|
+
n.saturating_mul(128).max(256)
|
|
98
|
+
} else {
|
|
99
|
+
256
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
_ => 256,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
pub fn json_stringify(value: &Value) -> String {
|
|
84
|
-
|
|
85
|
-
// `/queries=20` is ~700 B). Larger payloads reallocate normally.
|
|
86
|
-
let mut buf = String::with_capacity(256);
|
|
107
|
+
let mut buf = String::with_capacity(json_stringify_capacity_hint(value));
|
|
87
108
|
json_stringify_into(&mut buf, value);
|
|
88
109
|
buf
|
|
89
110
|
}
|
|
@@ -91,7 +112,39 @@ pub fn json_stringify(value: &Value) -> String {
|
|
|
91
112
|
/// Append a JSON-stringified `value` to `buf`. Used by JSON.stringify for
|
|
92
113
|
/// the recursive case so we don't pay for an intermediate `String` per
|
|
93
114
|
/// node.
|
|
115
|
+
///
|
|
116
|
+
/// Cyclic object graphs (`a.self = a`) are detected and — matching JS, which throws
|
|
117
|
+
/// `TypeError: Converting circular structure to JSON` — set a pending throw and emit `null` for the
|
|
118
|
+
/// back-reference instead of recursing forever (#381: this was an unrecoverable thread hang, directly
|
|
119
|
+
/// reachable from HTTP response serialization).
|
|
94
120
|
pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
121
|
+
// Track the CURRENT ancestor path only (not all-visited): a node repeated across sibling branches
|
|
122
|
+
// is a legal DAG and must serialize twice; only a back-edge to an ancestor is a cycle.
|
|
123
|
+
let mut ancestors: Vec<*const ()> = Vec::new();
|
|
124
|
+
json_stringify_into_guarded(buf, value, &mut ancestors);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Set the JS "circular structure" TypeError as the pending throw (once). The `{ name, message }`
|
|
128
|
+
/// shape matches node (`e.name === "TypeError"`, `e.message === "Converting circular structure to
|
|
129
|
+
/// JSON"`) and tish's own thrown-error convention (`construct_builtin::error_object`, the recursion
|
|
130
|
+
/// `RangeError` from [`crate::stack_overflow_error`]) — the previous `{ error: <msg> }` shape left
|
|
131
|
+
/// `e.name` null in a catch block, diverging from node.
|
|
132
|
+
fn signal_circular_json_throw() {
|
|
133
|
+
if !crate::has_pending_throw() {
|
|
134
|
+
crate::set_pending_throw(Value::object_from_pairs([
|
|
135
|
+
(
|
|
136
|
+
std::sync::Arc::from("name"),
|
|
137
|
+
Value::String("TypeError".into()),
|
|
138
|
+
),
|
|
139
|
+
(
|
|
140
|
+
std::sync::Arc::from("message"),
|
|
141
|
+
Value::String("Converting circular structure to JSON".into()),
|
|
142
|
+
),
|
|
143
|
+
]));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn json_stringify_into_guarded(buf: &mut String, value: &Value, ancestors: &mut Vec<*const ()>) {
|
|
95
148
|
match value {
|
|
96
149
|
Value::Null => buf.push_str("null"),
|
|
97
150
|
Value::Bool(true) => buf.push_str("true"),
|
|
@@ -103,15 +156,24 @@ pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
|
103
156
|
buf.push('"');
|
|
104
157
|
}
|
|
105
158
|
Value::Array(arr) => {
|
|
159
|
+
let ptr = arr.as_ptr();
|
|
160
|
+
if ancestors.contains(&ptr) {
|
|
161
|
+
signal_circular_json_throw();
|
|
162
|
+
buf.push_str("null");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
ancestors.push(ptr);
|
|
106
166
|
let borrowed = arr.borrow();
|
|
107
167
|
buf.push('[');
|
|
108
168
|
for (i, item) in borrowed.iter().enumerate() {
|
|
109
169
|
if i > 0 {
|
|
110
170
|
buf.push(',');
|
|
111
171
|
}
|
|
112
|
-
|
|
172
|
+
json_stringify_into_guarded(buf, item, ancestors);
|
|
113
173
|
}
|
|
114
174
|
buf.push(']');
|
|
175
|
+
drop(borrowed);
|
|
176
|
+
ancestors.pop();
|
|
115
177
|
}
|
|
116
178
|
Value::NumberArray(arr) => {
|
|
117
179
|
let borrowed = arr.borrow();
|
|
@@ -125,6 +187,13 @@ pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
|
125
187
|
buf.push(']');
|
|
126
188
|
}
|
|
127
189
|
Value::Object(obj) => {
|
|
190
|
+
let ptr = obj.as_ptr();
|
|
191
|
+
if ancestors.contains(&ptr) {
|
|
192
|
+
signal_circular_json_throw();
|
|
193
|
+
buf.push_str("null");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
ancestors.push(ptr);
|
|
128
197
|
let borrowed = obj.borrow();
|
|
129
198
|
// Iterate in insertion order (PropMap preserves it) — matches JS/Node
|
|
130
199
|
// and `Object.keys`. No intermediate key Vec, no sort: one fewer
|
|
@@ -137,9 +206,11 @@ pub fn json_stringify_into(buf: &mut String, value: &Value) {
|
|
|
137
206
|
buf.push('"');
|
|
138
207
|
escape_json_string_into(buf, key);
|
|
139
208
|
buf.push_str("\":");
|
|
140
|
-
|
|
209
|
+
json_stringify_into_guarded(buf, val, ancestors);
|
|
141
210
|
}
|
|
142
211
|
buf.push('}');
|
|
212
|
+
drop(borrowed);
|
|
213
|
+
ancestors.pop();
|
|
143
214
|
}
|
|
144
215
|
Value::Function(_) | Value::Promise(_) | Value::Opaque(_) | Value::Symbol(_) => {
|
|
145
216
|
buf.push_str("null");
|
|
@@ -477,6 +548,34 @@ fn parse_object<'a>(
|
|
|
477
548
|
mod tests {
|
|
478
549
|
use super::*;
|
|
479
550
|
|
|
551
|
+
/// #381: a self-referential array must NOT loop forever — `JSON.stringify` terminates, emits a
|
|
552
|
+
/// finite string, and (matching JS) leaves a pending TypeError. The test *completing* is itself
|
|
553
|
+
/// the assertion that the former infinite hang is gone.
|
|
554
|
+
#[test]
|
|
555
|
+
fn json_stringify_cyclic_array_terminates_and_throws() {
|
|
556
|
+
let _ = crate::take_pending_throw(); // isolate from any prior thread-local throw
|
|
557
|
+
let a = Value::Array(crate::VmRef::new(Vec::new()));
|
|
558
|
+
if let Value::Array(inner) = &a {
|
|
559
|
+
inner.borrow_mut().push(a.clone()); // a = [a]
|
|
560
|
+
}
|
|
561
|
+
let s = json_stringify(&a);
|
|
562
|
+
assert_eq!(s, "[null]", "back-reference serializes as null, finite output");
|
|
563
|
+
assert!(crate::has_pending_throw(), "cyclic stringify must set a pending TypeError");
|
|
564
|
+
let _ = crate::take_pending_throw(); // clean up thread-local for other tests
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/// A shared (non-cyclic) node repeated across sibling branches is a legal DAG and must serialize
|
|
568
|
+
/// twice — the ancestor-only tracking must NOT misflag it as a cycle.
|
|
569
|
+
#[test]
|
|
570
|
+
fn json_stringify_shared_dag_node_is_not_a_cycle() {
|
|
571
|
+
let _ = crate::take_pending_throw();
|
|
572
|
+
let shared = Value::Array(crate::VmRef::new(vec![Value::Number(1.0)]));
|
|
573
|
+
let root = Value::Array(crate::VmRef::new(vec![shared.clone(), shared.clone()]));
|
|
574
|
+
let s = json_stringify(&root);
|
|
575
|
+
assert_eq!(s, "[[1],[1]]", "a shared node is a DAG, not a cycle");
|
|
576
|
+
assert!(!crate::has_pending_throw(), "a DAG must NOT throw");
|
|
577
|
+
}
|
|
578
|
+
|
|
480
579
|
#[test]
|
|
481
580
|
fn test_parse_primitives() {
|
|
482
581
|
assert!(matches!(json_parse("null").unwrap(), Value::Null));
|
|
@@ -40,3 +40,177 @@ pub fn process_argv() -> Vec<String> {
|
|
|
40
40
|
None => std::env::args().collect(),
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/// #303 — a thrown JS value parked while it unwinds across a boundary that can't carry a `Result`.
|
|
45
|
+
///
|
|
46
|
+
/// The native value-fn ABI is `Fn(&[Value]) -> Value`, and `Callable::call` likewise returns a bare
|
|
47
|
+
/// `Value`, so a `throw` crossing such a boundary can't ride a `Result`. It is parked here and picked
|
|
48
|
+
/// up at the next checkpoint: native codegen checks it after each call; the VM checks it after each
|
|
49
|
+
/// `Callable::call`; and the shared array builtins (`forEach`/`map`/`sort`/…) check it between
|
|
50
|
+
/// elements so they stop iterating promptly instead of running the callback for the rest of the
|
|
51
|
+
/// array. The slot lives in `tishlang_core` (rather than `tishlang_runtime`) so `tishlang_builtins`
|
|
52
|
+
/// can poll it without a `builtins -> runtime` dependency cycle; `tishlang_runtime` and `tishlang_vm`
|
|
53
|
+
/// share this one slot. First-throw-wins; drained exactly once by [`take_pending_throw`] at the frame
|
|
54
|
+
/// that converts it back into a `Result`.
|
|
55
|
+
thread_local! {
|
|
56
|
+
static PENDING_THROW: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Park a thrown value to propagate across a non-`Result` boundary. First-throw-wins: if one is
|
|
60
|
+
/// already pending (an erroneous continuation reached a second throw before the slot was drained),
|
|
61
|
+
/// keep the first — that is the throw JS would have raised.
|
|
62
|
+
pub fn set_pending_throw(v: Value) {
|
|
63
|
+
PENDING_THROW.with(|c| {
|
|
64
|
+
let mut slot = c.borrow_mut();
|
|
65
|
+
if slot.is_none() {
|
|
66
|
+
*slot = Some(v);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Is a thrown value waiting to propagate?
|
|
72
|
+
pub fn has_pending_throw() -> bool {
|
|
73
|
+
PENDING_THROW.with(|c| c.borrow().is_some())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Take the parked thrown value, clearing the slot (drains it exactly once).
|
|
77
|
+
pub fn take_pending_throw() -> Option<Value> {
|
|
78
|
+
PENDING_THROW.with(|c| c.borrow_mut().take())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
thread_local! {
|
|
82
|
+
/// Current user-function call depth for the bytecode VM. The VM's recursive path builds a fresh
|
|
83
|
+
/// `Vm` per call (so no shared struct field can accumulate) and its `Callable::call` signature is
|
|
84
|
+
/// fixed — a thread-local is the VM's equivalent of the interpreter's shared `Rc<Cell>` counter.
|
|
85
|
+
/// Lives here (not `tish_vm`) beside `PENDING_THROW` for the same layering reason. #381
|
|
86
|
+
static CALL_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Enter one VM call frame; returns the new depth. Pair every call with [`dec_call_depth`].
|
|
90
|
+
#[inline]
|
|
91
|
+
pub fn inc_call_depth() -> usize {
|
|
92
|
+
CALL_DEPTH.with(|c| {
|
|
93
|
+
let d = c.get() + 1;
|
|
94
|
+
c.set(d);
|
|
95
|
+
d
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Leave one VM call frame.
|
|
100
|
+
#[inline]
|
|
101
|
+
pub fn dec_call_depth() {
|
|
102
|
+
CALL_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Default recursion ceiling shared by every backend that counts call depth (#381). Chosen with the
|
|
106
|
+
/// interpreter: comfortably deep for real programs, but reached long before counted recursion can
|
|
107
|
+
/// exhaust memory or the stack.
|
|
108
|
+
pub const DEFAULT_MAX_CALL_DEPTH: usize = 20_000;
|
|
109
|
+
|
|
110
|
+
thread_local! {
|
|
111
|
+
// The recursion ceiling, lazily initialized from `TISH_MAX_CALL_DEPTH` (0 = uninitialized). A
|
|
112
|
+
// thread-local Cell (not OnceLock) so tests can override it per-thread without racing the
|
|
113
|
+
// process-wide env. Shared by the VM's guard and the native backend's boxed-call guard. #381
|
|
114
|
+
static MAX_CALL_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// The recursion ceiling for depth-counting guards: user-fn calls deeper than this raise a catchable
|
|
118
|
+
/// `RangeError` instead of overflowing the native stack. `TISH_MAX_CALL_DEPTH` overrides (default
|
|
119
|
+
/// [`DEFAULT_MAX_CALL_DEPTH`]). #381
|
|
120
|
+
pub fn max_call_depth() -> usize {
|
|
121
|
+
MAX_CALL_DEPTH.with(|c| {
|
|
122
|
+
let v = c.get();
|
|
123
|
+
if v != 0 {
|
|
124
|
+
return v;
|
|
125
|
+
}
|
|
126
|
+
let init = std::env::var("TISH_MAX_CALL_DEPTH")
|
|
127
|
+
.ok()
|
|
128
|
+
.and_then(|s| s.parse::<usize>().ok())
|
|
129
|
+
.filter(|&n| n > 0)
|
|
130
|
+
.unwrap_or(DEFAULT_MAX_CALL_DEPTH);
|
|
131
|
+
c.set(init);
|
|
132
|
+
init
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Test hook: pin the ceiling for the current thread (bypasses the env read).
|
|
137
|
+
pub fn set_max_call_depth_for_test(n: usize) {
|
|
138
|
+
MAX_CALL_DEPTH.with(|c| c.set(n));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// The catchable `RangeError` raised when a recursion guard trips. Built by hand (`tishlang_core`
|
|
142
|
+
/// sits below `tishlang_builtins`) but shape-identical to `construct_builtin::error_object`:
|
|
143
|
+
/// a `{ name, message }` object — keep the two in lock-step.
|
|
144
|
+
pub fn stack_overflow_error() -> Value {
|
|
145
|
+
let mut e = ObjectMap::default();
|
|
146
|
+
e.insert(
|
|
147
|
+
std::sync::Arc::from("name"),
|
|
148
|
+
Value::String("RangeError".into()),
|
|
149
|
+
);
|
|
150
|
+
e.insert(
|
|
151
|
+
std::sync::Arc::from("message"),
|
|
152
|
+
Value::String("Maximum call stack size exceeded".into()),
|
|
153
|
+
);
|
|
154
|
+
Value::object(e)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// A catchable `TypeError` for calling a non-callable value — the `{ name, message }` shape matches
|
|
158
|
+
/// the VM/interpreter/node (`e.name === "TypeError"`). Used to PARK a pending throw (#381) instead of
|
|
159
|
+
/// aborting the process, so `value_call` on a non-function surfaces as a catchable error at the next
|
|
160
|
+
/// pending-throw checkpoint rather than an uncatchable native panic.
|
|
161
|
+
pub fn not_a_function_error(message: impl Into<String>) -> Value {
|
|
162
|
+
let mut e = ObjectMap::default();
|
|
163
|
+
e.insert(
|
|
164
|
+
std::sync::Arc::from("name"),
|
|
165
|
+
Value::String("TypeError".into()),
|
|
166
|
+
);
|
|
167
|
+
e.insert(
|
|
168
|
+
std::sync::Arc::from("message"),
|
|
169
|
+
Value::String(message.into().into()),
|
|
170
|
+
);
|
|
171
|
+
Value::object(e)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// A catchable `TypeError` for reading a property/index of the nullish value — the `{ name, message }`
|
|
175
|
+
/// shape matches the VM/interpreter/node (`e.name === "TypeError"`). Used to PARK a pending throw in
|
|
176
|
+
/// the native/runtime `get_prop`/`get_index` null arm (#425), so `null.length` / `null[0]` surface a
|
|
177
|
+
/// catchable error at the next pending-throw checkpoint instead of silently reading back `null`.
|
|
178
|
+
pub fn cannot_read_property_error(prop: &str) -> Value {
|
|
179
|
+
let mut e = ObjectMap::default();
|
|
180
|
+
e.insert(
|
|
181
|
+
std::sync::Arc::from("name"),
|
|
182
|
+
Value::String("TypeError".into()),
|
|
183
|
+
);
|
|
184
|
+
e.insert(
|
|
185
|
+
std::sync::Arc::from("message"),
|
|
186
|
+
Value::String(format!("Cannot read property '{}' of null", prop).into()),
|
|
187
|
+
);
|
|
188
|
+
Value::object(e)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// RAII frame for the depth-counting recursion guard: holding one means the depth was incremented;
|
|
192
|
+
/// dropping it decrements, so early returns in generated code can't leak a level. #381
|
|
193
|
+
pub struct CallDepthGuard(());
|
|
194
|
+
|
|
195
|
+
impl Drop for CallDepthGuard {
|
|
196
|
+
#[inline]
|
|
197
|
+
fn drop(&mut self) {
|
|
198
|
+
dec_call_depth();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Enter a counted user-fn call frame, or trip the recursion guard: past [`max_call_depth`] this
|
|
203
|
+
/// parks the catchable stack-overflow `RangeError` in the pending-throw slot and returns `None` —
|
|
204
|
+
/// the caller just returns its frame's dummy value (`Value::Null`) and the throw surfaces at the
|
|
205
|
+
/// next pending-throw checkpoint. The native backend emits this at the top of every boxed user-fn
|
|
206
|
+
/// closure. #381
|
|
207
|
+
#[inline]
|
|
208
|
+
pub fn enter_call_guarded() -> Option<CallDepthGuard> {
|
|
209
|
+
let depth = inc_call_depth();
|
|
210
|
+
if depth > max_call_depth() {
|
|
211
|
+
dec_call_depth();
|
|
212
|
+
set_pending_throw(stack_overflow_error());
|
|
213
|
+
return None;
|
|
214
|
+
}
|
|
215
|
+
Some(CallDepthGuard(()))
|
|
216
|
+
}
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
//! `PropMap` on a cache miss). Phase 1b will attach the ordered key list to each shape so objects can
|
|
13
13
|
//! drop per-object key storage entirely (the butterfly representation).
|
|
14
14
|
|
|
15
|
-
use std::collections::HashMap;
|
|
16
15
|
use std::sync::{Arc, OnceLock, RwLock};
|
|
17
16
|
|
|
18
17
|
/// Identity of an object's ordered key-set.
|
|
@@ -27,9 +26,12 @@ pub const EMPTY_SHAPE: ShapeId = 0;
|
|
|
27
26
|
pub const DICT_SHAPE: ShapeId = u32::MAX;
|
|
28
27
|
|
|
29
28
|
/// One node in the structure-transition tree: from this shape, adding a given key yields a child shape.
|
|
29
|
+
/// Edges are keyed by an `ahash` map, not the default SipHash one: `transition` is on the hot path of
|
|
30
|
+
/// every object construction (and `JSON.parse` — profiled as ~16% of parse, almost all SipHash), and
|
|
31
|
+
/// ahash is ~2–3× faster on short string keys. Random-seeded per process, so no HashDoS regression.
|
|
30
32
|
#[derive(Default)]
|
|
31
33
|
struct ShapeNode {
|
|
32
|
-
transitions:
|
|
34
|
+
transitions: ahash::AHashMap<Arc<str>, ShapeId>,
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
struct Registry {
|