@tishlang/tish-format 3.12.2 → 3.15.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 +1 -0
- package/bin/tish-format +0 -0
- package/crates/tish/Cargo.toml +3 -1
- package/crates/tish/src/cli_help.rs +2 -1
- package/crates/tish/src/main.rs +21 -2
- package/crates/tish_bytecode/src/compiler.rs +6 -8
- package/crates/tish_compile/src/codegen.rs +93 -6
- package/crates/tish_compile/src/lib.rs +4 -0
- package/crates/tish_compile/src/resolve.rs +1 -0
- package/crates/tish_compile/tests/regr_ffi_module_emit.rs +120 -0
- package/crates/tish_core/src/lib.rs +43 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +2 -0
- package/crates/tish_ffi/src/lib.rs +48 -25
- package/crates/tish_ffi/src/v2.rs +349 -0
- package/crates/tish_ffi/tests/fixtures/testmod_v2/Cargo.lock +282 -0
- package/crates/tish_ffi/tests/fixtures/testmod_v2/Cargo.toml +16 -0
- package/crates/tish_ffi/tests/fixtures/testmod_v2/src/lib.rs +111 -0
- package/crates/tish_ffi/tests/loader_v2.rs +159 -0
- package/crates/tish_ffi_guest/Cargo.toml +11 -0
- package/crates/tish_ffi_guest/src/lib.rs +333 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +54 -15
- package/crates/tish_native/src/config.rs +10 -0
- package/crates/tish_native/src/lib.rs +7 -2
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/lib.rs +21 -0
- package/crates/tish_vm/Cargo.toml +2 -0
- package/crates/tish_vm/src/vm.rs +61 -6
- package/crates/tish_vm/tests/ffi_load_module.rs +32 -0
- package/crates/tish_vm/tests/jit_off_leaves_jit_state.rs +24 -0
- package/crates/tish_vm/tests/jsx_lexical_resolution.rs +42 -0
- package/crates/tish_vm/tests/multi_vm_isolation.rs +123 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -22,6 +22,7 @@ const RUNTIME_CARGO_FEATURES: &[&str] = &[
|
|
|
22
22
|
"encoding",
|
|
23
23
|
"crypto",
|
|
24
24
|
"zip",
|
|
25
|
+
"ffi",
|
|
25
26
|
];
|
|
26
27
|
|
|
27
28
|
/// Map CLI/compile features to flags passed to `tishlang_runtime` in the temp crate's Cargo.toml.
|
|
@@ -224,11 +225,26 @@ pub fn build_via_cargo_with_config(
|
|
|
224
225
|
};
|
|
225
226
|
|
|
226
227
|
// mimalloc as the program's global allocator — binary output only (a staticlib does not own the
|
|
227
|
-
// allocator), native only (
|
|
228
|
+
// allocator), native or another macOS slice only (the host clang builds mimalloc's C for both
|
|
229
|
+
// Mac architectures; other cross targets are skipped). Adds one cached dep + a global_alloc
|
|
228
230
|
// statement; semantically transparent. `TISH_NATIVE_FAST_ALLOC=0` opts out.
|
|
229
231
|
let use_fast_alloc = fast_alloc_enabled()
|
|
230
232
|
&& build_config.artifact != NativeArtifact::StaticLib
|
|
231
|
-
&& build_config.
|
|
233
|
+
&& build_config.artifact != NativeArtifact::FfiModule
|
|
234
|
+
&& build_config
|
|
235
|
+
.cargo_target
|
|
236
|
+
.as_deref()
|
|
237
|
+
.map_or(true, |t| cfg!(target_os = "macos") && t.ends_with("-apple-darwin"));
|
|
238
|
+
if build_config.artifact == NativeArtifact::FfiModule {
|
|
239
|
+
let guest_path = Path::new(&runtime_path)
|
|
240
|
+
.parent()
|
|
241
|
+
.map(|crates| crates.join("tish_ffi_guest"))
|
|
242
|
+
.ok_or_else(|| format!("Cannot locate tish_ffi_guest next to {}", runtime_path))?;
|
|
243
|
+
more_deps.push_str(&format!(
|
|
244
|
+
"\ntishlang_ffi_guest = {{ path = {:?} }}\n",
|
|
245
|
+
guest_path.display().to_string().replace('\\', "/")
|
|
246
|
+
));
|
|
247
|
+
}
|
|
232
248
|
if use_fast_alloc {
|
|
233
249
|
more_deps.push_str("\nmimalloc = \"0.1\"\n");
|
|
234
250
|
}
|
|
@@ -241,20 +257,25 @@ pub fn build_via_cargo_with_config(
|
|
|
241
257
|
let ui_dep = ui_dep_toml(&runtime_path, rust_code);
|
|
242
258
|
|
|
243
259
|
let profile = nested_release_profile_toml();
|
|
244
|
-
let
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
};
|
|
249
|
-
let crate_section = if
|
|
260
|
+
let is_lib_artifact = matches!(
|
|
261
|
+
build_config.artifact,
|
|
262
|
+
NativeArtifact::StaticLib | NativeArtifact::FfiModule
|
|
263
|
+
);
|
|
264
|
+
let src_file = if is_lib_artifact { "lib.rs" } else { "main.rs" };
|
|
265
|
+
let crate_section = if is_lib_artifact {
|
|
266
|
+
let crate_type = if build_config.artifact == NativeArtifact::FfiModule {
|
|
267
|
+
"cdylib"
|
|
268
|
+
} else {
|
|
269
|
+
"staticlib"
|
|
270
|
+
};
|
|
250
271
|
format!(
|
|
251
272
|
r#"[lib]
|
|
252
273
|
name = "{}"
|
|
253
|
-
crate-type = ["
|
|
274
|
+
crate-type = ["{}"]
|
|
254
275
|
path = "src/lib.rs"
|
|
255
276
|
|
|
256
277
|
"#,
|
|
257
|
-
cargo_name
|
|
278
|
+
cargo_name, crate_type
|
|
258
279
|
)
|
|
259
280
|
} else {
|
|
260
281
|
format!(
|
|
@@ -317,12 +338,16 @@ edition = "2021"
|
|
|
317
338
|
|
|
318
339
|
tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref(), cross)?;
|
|
319
340
|
|
|
320
|
-
let artifact =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
341
|
+
let artifact = match build_config.artifact {
|
|
342
|
+
NativeArtifact::StaticLib => {
|
|
343
|
+
tishlang_build_utils::find_release_staticlib(&binary_dir, &cargo_name)?
|
|
344
|
+
}
|
|
345
|
+
NativeArtifact::FfiModule => find_release_cdylib(&binary_dir, &cargo_name)?,
|
|
346
|
+
_ => tishlang_build_utils::find_release_binary(&binary_dir, &cargo_name)?,
|
|
324
347
|
};
|
|
325
|
-
let target = if build_config.artifact == NativeArtifact::
|
|
348
|
+
let target = if build_config.artifact == NativeArtifact::FfiModule {
|
|
349
|
+
output_path.to_path_buf()
|
|
350
|
+
} else if build_config.artifact == NativeArtifact::StaticLib {
|
|
326
351
|
if output_path.extension().is_some_and(|e| e == "a") {
|
|
327
352
|
output_path.to_path_buf()
|
|
328
353
|
} else if output_path.to_string_lossy().ends_with('/') || output_path.is_dir() {
|
|
@@ -339,6 +364,20 @@ edition = "2021"
|
|
|
339
364
|
Ok(())
|
|
340
365
|
}
|
|
341
366
|
|
|
367
|
+
/// The built `cdylib` for `cargo_name` under `dir` (`lib<name>.dylib` / `lib<name>.so` / `<name>.dll`).
|
|
368
|
+
fn find_release_cdylib(dir: &Path, cargo_name: &str) -> Result<PathBuf, String> {
|
|
369
|
+
let candidates = [
|
|
370
|
+
format!("lib{cargo_name}.dylib"),
|
|
371
|
+
format!("lib{cargo_name}.so"),
|
|
372
|
+
format!("{cargo_name}.dll"),
|
|
373
|
+
];
|
|
374
|
+
candidates
|
|
375
|
+
.iter()
|
|
376
|
+
.map(|c| dir.join(c))
|
|
377
|
+
.find(|p| p.is_file())
|
|
378
|
+
.ok_or_else(|| format!("Built cdylib not found in {} (looked for {:?})", dir.display(), candidates))
|
|
379
|
+
}
|
|
380
|
+
|
|
342
381
|
/// Read the agb version the facade pins, from `<facade>/Cargo.toml`. This keeps the generated ROM
|
|
343
382
|
/// crate's agb in lockstep with the facade's (the single source of truth) — see [`build_gba_rom`].
|
|
344
383
|
/// Matches an `agb = "X"` or `agb = { version = "X", … }` line; `None` if absent (caller falls back).
|
|
@@ -15,6 +15,8 @@ pub enum NativeArtifact {
|
|
|
15
15
|
/// module's `export fn`s. Unlike every other artifact, cargo is **not** run — the emitted
|
|
16
16
|
/// crate directory is itself the output, for the consumer to add as a path/registry dependency.
|
|
17
17
|
RustLib,
|
|
18
|
+
/// A dynamically loadable tish native module (`crate-type = ["cdylib"]`, FFI ABI v2).
|
|
19
|
+
FfiModule,
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/// Options passed from the CLI into nested `cargo build`.
|
|
@@ -59,6 +61,14 @@ impl NativeBuildConfig {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
pub fn ffi_module() -> Self {
|
|
65
|
+
Self {
|
|
66
|
+
artifact: NativeArtifact::FfiModule,
|
|
67
|
+
cargo_target: None,
|
|
68
|
+
emit_mode: NativeEmitMode::FfiModule,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
pub fn is_cross_compile(&self) -> bool {
|
|
63
73
|
self.cargo_target.is_some()
|
|
64
74
|
}
|
|
@@ -107,7 +107,12 @@ pub fn compile_to_native_with_config(
|
|
|
107
107
|
ios_runtime_features(f)
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
|
-
|
|
110
|
+
// A desktop triple (one slice of a universal macOS build) is not a restricted target.
|
|
111
|
+
let capped = build_config
|
|
112
|
+
.cargo_target
|
|
113
|
+
.as_deref()
|
|
114
|
+
.filter(|t| is_gba || !t.ends_with("-apple-darwin"));
|
|
115
|
+
let cross_cap = capped.map(|_| {
|
|
111
116
|
cap(features)
|
|
112
117
|
.into_iter()
|
|
113
118
|
.collect::<std::collections::HashSet<_>>()
|
|
@@ -130,7 +135,7 @@ pub fn compile_to_native_with_config(
|
|
|
130
135
|
message: e.to_string(),
|
|
131
136
|
})?;
|
|
132
137
|
|
|
133
|
-
let features_for_cargo = if
|
|
138
|
+
let features_for_cargo = if capped.is_some() {
|
|
134
139
|
cap(&effective_features)
|
|
135
140
|
} else {
|
|
136
141
|
effective_features
|
|
@@ -65,6 +65,9 @@ encoding = ["dep:base64"]
|
|
|
65
65
|
crypto = ["dep:sha2", "dep:rand"]
|
|
66
66
|
# Zip extraction (tish:zip) — VSIX / archive extract with `..`-traversal guard + unix-mode preserved.
|
|
67
67
|
zip = ["dep:zip"]
|
|
68
|
+
# Runtime loading of tish FFI cdylibs (tish:ffi `loadModule`), e.g. plugins built with
|
|
69
|
+
# `tish build --crate-type cdylib`. Hosts need no linker flags: ABI v2 passes the accessor table.
|
|
70
|
+
ffi = ["dep:tishlang_ffi"]
|
|
68
71
|
# Interactive terminal I/O (issue #101): raw mode, key/resize events, size, alt screen.
|
|
69
72
|
tty = ["dep:crossterm"]
|
|
70
73
|
# Pseudoterminal hosting (issue #493): spawn a shell/program on a real PTY, read/write/resize/kill —
|
|
@@ -97,6 +100,7 @@ futures = { version = "0.3.32", optional = true }
|
|
|
97
100
|
base64 = { version = "0.22", optional = true }
|
|
98
101
|
sha2 = { version = "0.10", optional = true }
|
|
99
102
|
zip = { version = "2", default-features = false, features = ["deflate"], optional = true }
|
|
103
|
+
tishlang_ffi = { path = "../tish_ffi", version = ">=0.1", optional = true }
|
|
100
104
|
bytes = { version = "1", optional = true }
|
|
101
105
|
tiny_http = { version = "0.12.0", optional = true }
|
|
102
106
|
# fs feature only: random suffix for `mkdtemp`.
|
|
@@ -1828,6 +1828,27 @@ pub mod zip;
|
|
|
1828
1828
|
#[cfg(feature = "zip")]
|
|
1829
1829
|
pub use zip::{zip_entries, zip_extract};
|
|
1830
1830
|
|
|
1831
|
+
/// `tish:ffi` `loadModule(path)`: load a tish FFI cdylib (ABI v2, falling back to v1) and return
|
|
1832
|
+
/// its exports as an object. Loading the same path again returns the cached module. Throws a
|
|
1833
|
+
/// `TypeError` if the path is not a string or the library cannot be loaded.
|
|
1834
|
+
#[cfg(all(feature = "ffi", not(target_arch = "wasm32")))]
|
|
1835
|
+
pub fn ffi_load_module(args: &[Value]) -> Value {
|
|
1836
|
+
let path = match args.first() {
|
|
1837
|
+
Some(Value::String(s)) => s.to_string(),
|
|
1838
|
+
_ => {
|
|
1839
|
+
set_pending_throw(tishlang_core::type_error("loadModule: expected a library path string"));
|
|
1840
|
+
return Value::Null;
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
match tishlang_ffi::load_module(&path) {
|
|
1844
|
+
Ok(exports) => Value::object(exports),
|
|
1845
|
+
Err(e) => {
|
|
1846
|
+
set_pending_throw(tishlang_core::type_error(format!("loadModule: {e}")));
|
|
1847
|
+
Value::Null
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1831
1852
|
#[cfg(feature = "ws")]
|
|
1832
1853
|
pub use ws::{
|
|
1833
1854
|
web_socket_client, web_socket_server_accept, web_socket_server_construct,
|
|
@@ -32,6 +32,8 @@ http = ["dep:tishlang_runtime", "tishlang_runtime/http", "send-values", "timers"
|
|
|
32
32
|
# Promise + `tish:http.await` / `Promise` export without the full HTTP client stack (e.g. wasm32-wasip1).
|
|
33
33
|
promise = ["dep:tishlang_runtime", "tishlang_runtime/promise", "send-values"]
|
|
34
34
|
ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
|
|
35
|
+
# `tish:ffi` loadModule: runtime loading of tish FFI cdylibs.
|
|
36
|
+
ffi = ["dep:tishlang_runtime", "tishlang_runtime/ffi"]
|
|
35
37
|
|
|
36
38
|
[dependencies]
|
|
37
39
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -134,7 +134,9 @@ fn value_object_from_map(m: ObjectMap) -> Value {
|
|
|
134
134
|
feature = "promise",
|
|
135
135
|
feature = "timers",
|
|
136
136
|
feature = "process",
|
|
137
|
-
feature = "ws"
|
|
137
|
+
feature = "ws",
|
|
138
|
+
feature = "tty",
|
|
139
|
+
feature = "ffi"
|
|
138
140
|
))]
|
|
139
141
|
#[inline]
|
|
140
142
|
fn cap_allows(enabled: &HashSet<String>, name: &str) -> bool {
|
|
@@ -161,6 +163,8 @@ pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
|
161
163
|
s.insert("ws".to_string());
|
|
162
164
|
#[cfg(feature = "tty")]
|
|
163
165
|
s.insert("tty".to_string());
|
|
166
|
+
#[cfg(feature = "ffi")]
|
|
167
|
+
s.insert("ffi".to_string());
|
|
164
168
|
s
|
|
165
169
|
}
|
|
166
170
|
|
|
@@ -173,7 +177,8 @@ pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
|
173
177
|
feature = "timers",
|
|
174
178
|
feature = "process",
|
|
175
179
|
feature = "ws",
|
|
176
|
-
feature = "tty"
|
|
180
|
+
feature = "tty",
|
|
181
|
+
feature = "ffi"
|
|
177
182
|
)),
|
|
178
183
|
allow(unused_variables)
|
|
179
184
|
)]
|
|
@@ -455,6 +460,15 @@ fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str)
|
|
|
455
460
|
_ => None,
|
|
456
461
|
};
|
|
457
462
|
}
|
|
463
|
+
#[cfg(feature = "ffi")]
|
|
464
|
+
if spec == "tish:ffi" && cap_allows(enabled, "ffi") {
|
|
465
|
+
return match export_name {
|
|
466
|
+
"loadModule" => Some(Value::native(|args: &[Value]| {
|
|
467
|
+
tishlang_runtime::ffi_load_module(args)
|
|
468
|
+
})),
|
|
469
|
+
_ => None,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
458
472
|
None
|
|
459
473
|
}
|
|
460
474
|
|
|
@@ -1169,6 +1183,13 @@ pub struct Vm {
|
|
|
1169
1183
|
/// [`register_native_module`]). Phase-2 item 11: unblocks `cargo:`
|
|
1170
1184
|
/// imports on the cranelift and llvm backends which run this VM.
|
|
1171
1185
|
native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
|
|
1186
|
+
/// Numeric JIT + OSR for code this VM runs (default on). The JIT's code caches and callee
|
|
1187
|
+
/// registry are process-global and the registry is keyed by bare global name, so two VMs in one
|
|
1188
|
+
/// process that both JIT can resolve each other's same-named functions. An embedder hosting
|
|
1189
|
+
/// mutually untrusted programs turns this off ([`Vm::set_jit_enabled`]): such a VM never
|
|
1190
|
+
/// compiles, never OSRs (so every loop polls the execution deadline), and never touches the
|
|
1191
|
+
/// shared registry. Inherited by every closure the VM creates.
|
|
1192
|
+
jit: bool,
|
|
1172
1193
|
}
|
|
1173
1194
|
|
|
1174
1195
|
/// A bytecode-VM closure: a compiled chunk plus its captured lexical chain and shared VM state.
|
|
@@ -1584,6 +1605,8 @@ struct VmClosure {
|
|
|
1584
1605
|
globals: VmRef<ObjectMap>,
|
|
1585
1606
|
capabilities: Arc<HashSet<String>>,
|
|
1586
1607
|
native_modules: VmRef<HashMap<String, VmRef<ObjectMap>>>,
|
|
1608
|
+
/// The creating VM's [`Vm::jit`] setting, carried into every `Vm` built to run this closure.
|
|
1609
|
+
jit: bool,
|
|
1587
1610
|
}
|
|
1588
1611
|
|
|
1589
1612
|
impl tishlang_core::Callable for VmClosure {
|
|
@@ -1681,6 +1704,11 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1681
1704
|
// than overflowing the native stack (this recursive re-entry is the DEFAULT `tish run` path).
|
|
1682
1705
|
// The counter is thread-local because each closure call builds a fresh `Vm`; the parked throw
|
|
1683
1706
|
// is picked up by the caller's `take_pending_throw()` check (Call/SelfCall post-call).
|
|
1707
|
+
// Loop-free recursion (e.g. naive `fib`) never reaches a back-edge, so calls poll too.
|
|
1708
|
+
if tishlang_core::execution_deadline_exceeded() {
|
|
1709
|
+
set_pending_throw(construct_builtin::error_object("Error", tishlang_core::DEADLINE_ERROR));
|
|
1710
|
+
return Value::Null;
|
|
1711
|
+
}
|
|
1684
1712
|
let depth = tishlang_core::inc_call_depth();
|
|
1685
1713
|
if depth > max_call_depth() {
|
|
1686
1714
|
tishlang_core::dec_call_depth();
|
|
@@ -1694,6 +1722,7 @@ impl tishlang_core::Callable for VmClosure {
|
|
|
1694
1722
|
globals: self.globals.clone(),
|
|
1695
1723
|
capabilities: Arc::clone(&self.capabilities),
|
|
1696
1724
|
native_modules: self.native_modules.clone(),
|
|
1725
|
+
jit: self.jit,
|
|
1697
1726
|
};
|
|
1698
1727
|
#[cfg(not(target_arch = "wasm32"))]
|
|
1699
1728
|
let run = stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
@@ -1742,9 +1771,21 @@ impl Vm {
|
|
|
1742
1771
|
globals: VmRef::new(init_globals(capabilities.as_ref())),
|
|
1743
1772
|
capabilities,
|
|
1744
1773
|
native_modules: VmRef::new(HashMap::new()),
|
|
1774
|
+
jit: true,
|
|
1745
1775
|
}
|
|
1746
1776
|
}
|
|
1747
1777
|
|
|
1778
|
+
/// Turn the numeric JIT and OSR on or off for this VM and every closure it creates from now on
|
|
1779
|
+
/// (see [`Vm::jit`]). Set before [`Vm::run`]. Off isolates a VM from other VMs' JIT state in the
|
|
1780
|
+
/// same process and guarantees every loop polls the execution deadline; it never changes results.
|
|
1781
|
+
pub fn set_jit_enabled(&mut self, on: bool) {
|
|
1782
|
+
self.jit = on;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
pub fn jit_enabled(&self) -> bool {
|
|
1786
|
+
self.jit
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1748
1789
|
/// Register an externally-supplied native module under a `cargo:`-style
|
|
1749
1790
|
/// spec (e.g. `"cargo:tish_pg"`). The `exports` map is what
|
|
1750
1791
|
/// `LoadNativeExport` will index into when user code imports from this
|
|
@@ -1811,8 +1852,11 @@ impl Vm {
|
|
|
1811
1852
|
// here (the outermost entry — closures run via `run_chunk`, not this) so a long-lived process
|
|
1812
1853
|
// (REPL / embedder) running a NEW program never resolves a stale callee registered by a prior
|
|
1813
1854
|
// one. A cross-caller and its callee always compile within the same run, so this is sufficient.
|
|
1855
|
+
// A JIT-off VM registers no callees, so it must not clear other VMs' registrations either.
|
|
1814
1856
|
#[cfg(not(target_arch = "wasm32"))]
|
|
1815
|
-
|
|
1857
|
+
if self.jit {
|
|
1858
|
+
crate::jit::reset_callees();
|
|
1859
|
+
}
|
|
1816
1860
|
let result = self.run_chunk(chunk, &chunk.nested, &[], repl_mode);
|
|
1817
1861
|
// A throw that escaped every `catch` reaches here as the pending-throw sentinel; turn the
|
|
1818
1862
|
// parked value into the conventional uncaught-error message (issue #60).
|
|
@@ -2188,7 +2232,7 @@ impl Vm {
|
|
|
2188
2232
|
// `region_end - dist`. Once a loop is hot, try to run its remaining iterations in
|
|
2189
2233
|
// native code (numeric slot loops only; anything else falls straight through).
|
|
2190
2234
|
#[cfg(not(target_arch = "wasm32"))]
|
|
2191
|
-
if Self::osr_enabled() {
|
|
2235
|
+
if self.jit && Self::osr_enabled() {
|
|
2192
2236
|
let region_end = ip;
|
|
2193
2237
|
let header_ip = ip.saturating_sub(dist);
|
|
2194
2238
|
let key = (Arc::as_ptr(&cur) as usize, header_ip);
|
|
@@ -2237,6 +2281,9 @@ impl Vm {
|
|
|
2237
2281
|
set_pending_throw(stack_overflow_error());
|
|
2238
2282
|
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
2239
2283
|
}
|
|
2284
|
+
if tishlang_core::execution_deadline_exceeded() {
|
|
2285
|
+
return Some(Err(tishlang_core::DEADLINE_ERROR.to_string()));
|
|
2286
|
+
}
|
|
2240
2287
|
frames.push((cur.clone(), ip, slot_base, stack_base, enclosing.clone()));
|
|
2241
2288
|
let new_base = slots.len();
|
|
2242
2289
|
slots.resize(new_base + cur.num_slots as usize, Value::Null);
|
|
@@ -2276,6 +2323,9 @@ impl Vm {
|
|
|
2276
2323
|
set_pending_throw(stack_overflow_error());
|
|
2277
2324
|
return Some(Err(PENDING_THROW_SENTINEL.to_string()));
|
|
2278
2325
|
}
|
|
2326
|
+
if tishlang_core::execution_deadline_exceeded() {
|
|
2327
|
+
return Some(Err(tishlang_core::DEADLINE_ERROR.to_string()));
|
|
2328
|
+
}
|
|
2279
2329
|
frames.push((cur, ip, slot_base, stack_base, enclosing));
|
|
2280
2330
|
cur = next_chunk;
|
|
2281
2331
|
enclosing = next_enc;
|
|
@@ -2617,7 +2667,7 @@ impl Vm {
|
|
|
2617
2667
|
// and call native code when all args are numbers; else fall back to
|
|
2618
2668
|
// the interpreter below. Purely additive — can't change behaviour.
|
|
2619
2669
|
#[cfg(not(target_arch = "wasm32"))]
|
|
2620
|
-
let jit_fn = crate::jit::try_compile_numeric(inner);
|
|
2670
|
+
let jit_fn = if self.jit { crate::jit::try_compile_numeric(inner) } else { None };
|
|
2621
2671
|
let inner_clone = inner.clone();
|
|
2622
2672
|
let globals = self.globals.clone();
|
|
2623
2673
|
// The closure captures its defining frame's scope PLUS that frame's own
|
|
@@ -2674,6 +2724,7 @@ impl Vm {
|
|
|
2674
2724
|
globals,
|
|
2675
2725
|
capabilities,
|
|
2676
2726
|
native_modules,
|
|
2727
|
+
jit: self.jit,
|
|
2677
2728
|
};
|
|
2678
2729
|
#[cfg(feature = "send-values")]
|
|
2679
2730
|
{
|
|
@@ -2957,6 +3008,9 @@ impl Vm {
|
|
|
2957
3008
|
// #381: SelfCall is a second native recursive re-entry (a self-recursive function
|
|
2958
3009
|
// re-enters run_chunk directly). Bound it with the same shared counter; `raise!` is
|
|
2959
3010
|
// in scope here, so throw the catchable RangeError directly.
|
|
3011
|
+
if tishlang_core::execution_deadline_exceeded() {
|
|
3012
|
+
return Err(tishlang_core::DEADLINE_ERROR.to_string());
|
|
3013
|
+
}
|
|
2960
3014
|
let depth = tishlang_core::inc_call_depth();
|
|
2961
3015
|
if depth > max_call_depth() {
|
|
2962
3016
|
tishlang_core::dec_call_depth();
|
|
@@ -2969,6 +3023,7 @@ impl Vm {
|
|
|
2969
3023
|
globals: self.globals.clone(),
|
|
2970
3024
|
capabilities: Arc::clone(&self.capabilities),
|
|
2971
3025
|
native_modules: self.native_modules.clone(),
|
|
3026
|
+
jit: self.jit,
|
|
2972
3027
|
};
|
|
2973
3028
|
#[cfg(not(target_arch = "wasm32"))]
|
|
2974
3029
|
let result = stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
|
|
@@ -3109,7 +3164,7 @@ impl Vm {
|
|
|
3109
3164
|
// only (`slot_locals` is the live frame); non-slot / non-numeric loops fail the
|
|
3110
3165
|
// whitelist or the live-in check and fall straight through to the interpreter.
|
|
3111
3166
|
#[cfg(not(target_arch = "wasm32"))]
|
|
3112
|
-
if chunk.slot_based && Self::osr_enabled() {
|
|
3167
|
+
if self.jit && chunk.slot_based && Self::osr_enabled() {
|
|
3113
3168
|
let region_end = ip;
|
|
3114
3169
|
let header_ip = ip.saturating_sub(dist);
|
|
3115
3170
|
// Count on the RAW header via the single-entry fast slot — byte-identical to the
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//! `tish:ffi` `loadModule` on the bytecode VM. Loading real modules is covered by
|
|
2
|
+
//! `crates/tish_ffi/tests/loader_v2.rs`; this pins the builtin wiring and its error contract.
|
|
3
|
+
#![cfg(feature = "ffi")]
|
|
4
|
+
|
|
5
|
+
use tishlang_bytecode::compile;
|
|
6
|
+
use tishlang_vm::run;
|
|
7
|
+
|
|
8
|
+
fn run_src(src: &str) {
|
|
9
|
+
let cwd = std::env::current_dir().expect("cwd");
|
|
10
|
+
let modules = tishlang_compile::resolve_project_from_stdin(src, &cwd).expect("resolve");
|
|
11
|
+
let program = tishlang_compile::merge_modules(modules).expect("merge").program;
|
|
12
|
+
let chunk = compile(&program).expect("compile");
|
|
13
|
+
run(&chunk).expect("run");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[test]
|
|
17
|
+
fn load_failures_throw_catchable_type_errors() {
|
|
18
|
+
run_src(
|
|
19
|
+
r#"
|
|
20
|
+
import { loadModule } from "tish:ffi"
|
|
21
|
+
if (typeof loadModule !== "function") { throw "loadModule is not wired" }
|
|
22
|
+
|
|
23
|
+
let missing = ""
|
|
24
|
+
try { loadModule("/nonexistent/nimble-test.lib") } catch (e) { missing = e.name + " " + e.message }
|
|
25
|
+
if (missing.indexOf("TypeError loadModule:") !== 0) { throw "missing library: " + missing }
|
|
26
|
+
|
|
27
|
+
let badArg = ""
|
|
28
|
+
try { loadModule(42) } catch (e) { badArg = e.name }
|
|
29
|
+
if (badArg !== "TypeError") { throw "non-string path: " + badArg }
|
|
30
|
+
"#,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//! A JIT-off VM must not touch the process-global JIT state. Its own test binary (one test, one
|
|
2
|
+
//! process) because other tests JIT in parallel and would move the cache counts.
|
|
3
|
+
|
|
4
|
+
use tishlang_bytecode::compile;
|
|
5
|
+
use tishlang_vm::{jit_cache_lens, Vm};
|
|
6
|
+
|
|
7
|
+
#[test]
|
|
8
|
+
fn jit_off_vm_never_compiles() {
|
|
9
|
+
let src = "fn sq(x) { return x * x }\n\
|
|
10
|
+
fn sum(n) { let s = 0\nfor (let i = 0; i < n; i = i + 1) { s = s + sq(i) }\nreturn s }\n\
|
|
11
|
+
fn make() { fn calc(x) { return sq(x) + 1 }\nreturn calc }\n\
|
|
12
|
+
let total = sum(20000) + make()(3)";
|
|
13
|
+
let chunk = compile(&tishlang_parser::parse(src).expect("parse")).expect("compile");
|
|
14
|
+
|
|
15
|
+
let before = jit_cache_lens();
|
|
16
|
+
let mut off = Vm::new();
|
|
17
|
+
off.set_jit_enabled(false);
|
|
18
|
+
off.run(&chunk).expect("jit off runs");
|
|
19
|
+
assert_eq!(jit_cache_lens(), before, "a JIT-off VM compiled something");
|
|
20
|
+
|
|
21
|
+
let mut on = Vm::new();
|
|
22
|
+
on.run(&chunk).expect("jit on runs");
|
|
23
|
+
assert_ne!(jit_cache_lens(), before, "control: the same program JIT-compiles with the JIT on");
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//! Bytecode JSX resolves `h`, `Fragment` and component tags lexically, falling back to globals.
|
|
2
|
+
|
|
3
|
+
use tishlang_bytecode::compile;
|
|
4
|
+
use tishlang_vm::run;
|
|
5
|
+
|
|
6
|
+
fn run_src(src: &str) {
|
|
7
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
8
|
+
let chunk = compile(&program).expect("compile");
|
|
9
|
+
run(&chunk).expect("run");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn component_tag_resolves_script_level_fn() {
|
|
14
|
+
run_src(
|
|
15
|
+
r#"
|
|
16
|
+
fn h(tag, props, children) {
|
|
17
|
+
if (typeof tag === "function") { return tag(props) }
|
|
18
|
+
return { tag: tag, props: props, children: children }
|
|
19
|
+
}
|
|
20
|
+
fn Row(props) { return <item title={props.title} /> }
|
|
21
|
+
let tree = <list><Row title="a" /></list>
|
|
22
|
+
let row = tree.children[0]
|
|
23
|
+
if (row.tag !== "item" || row.props.title !== "a") { throw "component tag did not resolve" }
|
|
24
|
+
"#,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[test]
|
|
29
|
+
fn h_and_fragment_resolve_from_enclosing_scope() {
|
|
30
|
+
run_src(
|
|
31
|
+
r#"
|
|
32
|
+
fn render() {
|
|
33
|
+
let Fragment = "frag"
|
|
34
|
+
let h = (tag, props, children) => typeof tag === "function" ? tag(props) : { tag: tag, children: children }
|
|
35
|
+
fn Inner(p) { return <leaf /> }
|
|
36
|
+
return <><Inner /></>
|
|
37
|
+
}
|
|
38
|
+
let t = render()
|
|
39
|
+
if (t.tag !== "frag" || t.children[0].tag !== "leaf") { throw "local h/Fragment/component not used" }
|
|
40
|
+
"#,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//! Several mutually untrusted programs in one process, each in its own `Vm` (an embedder hosting
|
|
2
|
+
//! plugins). Two process-wide mechanisms leak between such VMs, and these tests pin the controls:
|
|
3
|
+
//!
|
|
4
|
+
//! - The numeric JIT's caches and callee registry are process-global, and every JIT-on `run` resets
|
|
5
|
+
//! the registry, which races with VMs running on other threads. `Vm::set_jit_enabled(false)` keeps
|
|
6
|
+
//! a VM out of the JIT entirely (`jit_off_leaves_jit_state.rs` checks it never touches the caches).
|
|
7
|
+
//! - `set_execution_deadline` is process-wide. `set_thread_execution_deadline` budgets only the
|
|
8
|
+
//! calling thread. The interpreter polls on loop back-edges and on calls; JIT-compiled loops do
|
|
9
|
+
//! not poll, so a budgeted VM runs JIT-off.
|
|
10
|
+
//!
|
|
11
|
+
//! Programs hand values out through bare top-level assignments (undeclared names are globals).
|
|
12
|
+
#![cfg(feature = "send-values")]
|
|
13
|
+
|
|
14
|
+
use std::sync::mpsc;
|
|
15
|
+
use std::time::{Duration, Instant};
|
|
16
|
+
|
|
17
|
+
use tishlang_bytecode::compile;
|
|
18
|
+
use tishlang_core::{set_thread_execution_deadline, Value, DEADLINE_ERROR};
|
|
19
|
+
use tishlang_vm::Vm;
|
|
20
|
+
|
|
21
|
+
fn run(src: &str, jit: bool) -> (Vm, Result<Value, String>) {
|
|
22
|
+
let chunk = compile(&tishlang_parser::parse(src).expect("parse")).expect("compile");
|
|
23
|
+
let mut vm = Vm::new();
|
|
24
|
+
vm.set_jit_enabled(jit);
|
|
25
|
+
let r = vm.run(&chunk);
|
|
26
|
+
(vm, r)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn global_num(vm: &Vm, name: &str) -> f64 {
|
|
30
|
+
match vm.get_global(name) {
|
|
31
|
+
Some(Value::Number(n)) => n,
|
|
32
|
+
other => panic!("global `{name}` is not a number: {other:?}"),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn global_fn(vm: &Vm, name: &str) -> tishlang_core::NativeFn {
|
|
37
|
+
match vm.get_global(name) {
|
|
38
|
+
Some(Value::Function(f)) => f,
|
|
39
|
+
other => panic!("global `{name}` is not a function: {other:?}"),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Run `f` on a fresh thread and fail (instead of hanging the suite) if it takes longer than `limit`.
|
|
44
|
+
fn within<T: Send + 'static>(limit: Duration, f: impl FnOnce() -> T + Send + 'static) -> T {
|
|
45
|
+
let (tx, rx) = mpsc::channel();
|
|
46
|
+
std::thread::spawn(move || {
|
|
47
|
+
let _ = tx.send(f());
|
|
48
|
+
});
|
|
49
|
+
rx.recv_timeout(limit).expect("did not finish in time")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SPIN: &str = "fn spin(n) { let s = 0\nlet i = 0\nwhile (i < n) { s = s + i\ni = i + 1 }\nreturn s }\n";
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn jit_off_gives_the_same_results() {
|
|
56
|
+
let src = "fn fib(n) { if (n < 2) { return n }\nreturn fib(n - 1) + fib(n - 2) }\n\
|
|
57
|
+
fn sum(n) { let s = 0\nfor (let i = 0; i < n; i = i + 1) { s = s + i * 0.5 }\nreturn s }\n\
|
|
58
|
+
out = fib(20) + sum(50000)";
|
|
59
|
+
let (on, r) = run(src, true);
|
|
60
|
+
r.expect("jit on");
|
|
61
|
+
let (off, r) = run(src, false);
|
|
62
|
+
r.expect("jit off");
|
|
63
|
+
assert_eq!(global_num(&on, "out"), 6765.0 + 624987500.0);
|
|
64
|
+
assert_eq!(global_num(&on, "out"), global_num(&off, "out"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// A hot numeric loop inside a function: with the JIT on it would be compiled and never poll.
|
|
68
|
+
#[test]
|
|
69
|
+
fn thread_deadline_stops_a_jit_off_hot_loop() {
|
|
70
|
+
let (r, elapsed) = within(Duration::from_secs(10), || {
|
|
71
|
+
let t0 = Instant::now();
|
|
72
|
+
set_thread_execution_deadline(Some(100));
|
|
73
|
+
let r = run(&format!("{SPIN}out = spin(1000000000000)"), false).1;
|
|
74
|
+
set_thread_execution_deadline(None);
|
|
75
|
+
(r, t0.elapsed())
|
|
76
|
+
});
|
|
77
|
+
let err = r.expect_err("the deadline must abort the loop");
|
|
78
|
+
assert!(err.contains(DEADLINE_ERROR), "{err}");
|
|
79
|
+
assert!(elapsed < Duration::from_secs(2), "took {elapsed:?}");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// No loop at all: naive recursion stays far below the call-depth limit but never hits a back-edge.
|
|
83
|
+
#[test]
|
|
84
|
+
fn thread_deadline_stops_loop_free_recursion() {
|
|
85
|
+
let (r, elapsed) = within(Duration::from_secs(10), || {
|
|
86
|
+
let t0 = Instant::now();
|
|
87
|
+
set_thread_execution_deadline(Some(100));
|
|
88
|
+
let r = run("fn fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2) }\nout = fib(45)", false).1;
|
|
89
|
+
set_thread_execution_deadline(None);
|
|
90
|
+
(r, t0.elapsed())
|
|
91
|
+
});
|
|
92
|
+
let err = r.expect_err("the deadline must abort the recursion");
|
|
93
|
+
assert!(err.contains(DEADLINE_ERROR), "{err}");
|
|
94
|
+
assert!(elapsed < Duration::from_secs(2), "took {elapsed:?}");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn thread_deadline_is_per_thread() {
|
|
99
|
+
set_thread_execution_deadline(Some(1));
|
|
100
|
+
std::thread::sleep(Duration::from_millis(5));
|
|
101
|
+
let n = within(Duration::from_secs(20), || {
|
|
102
|
+
let (vm, r) = run("let s = 0\nfor (let i = 0; i < 3000000; i = i + 1) { s = s + 1 }\nout = s", false);
|
|
103
|
+
r.expect("another thread's expired deadline must not apply");
|
|
104
|
+
global_num(&vm, "out")
|
|
105
|
+
});
|
|
106
|
+
set_thread_execution_deadline(None);
|
|
107
|
+
assert_eq!(n, 3000000.0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn jit_setting_is_inherited_by_closures_called_from_the_host() {
|
|
112
|
+
let (vm, r) = run(&format!("{SPIN}spinFn = spin"), false);
|
|
113
|
+
r.expect("runs");
|
|
114
|
+
let spin = global_fn(&vm, "spinFn");
|
|
115
|
+
let thrown = within(Duration::from_secs(10), move || {
|
|
116
|
+
set_thread_execution_deadline(Some(100));
|
|
117
|
+
let _ = spin.call(&[Value::Number(1e12)]);
|
|
118
|
+
set_thread_execution_deadline(None);
|
|
119
|
+
tishlang_core::take_pending_throw().map(|v| v.to_display_string())
|
|
120
|
+
});
|
|
121
|
+
let thrown = thrown.expect("a host call into a JIT-off closure must hit the deadline and throw");
|
|
122
|
+
assert!(thrown.contains(DEADLINE_ERROR), "{thrown}");
|
|
123
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|