@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.
Files changed (38) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/tish/Cargo.toml +3 -1
  4. package/crates/tish/src/cli_help.rs +2 -1
  5. package/crates/tish/src/main.rs +21 -2
  6. package/crates/tish_bytecode/src/compiler.rs +6 -8
  7. package/crates/tish_compile/src/codegen.rs +93 -6
  8. package/crates/tish_compile/src/lib.rs +4 -0
  9. package/crates/tish_compile/src/resolve.rs +1 -0
  10. package/crates/tish_compile/tests/regr_ffi_module_emit.rs +120 -0
  11. package/crates/tish_core/src/lib.rs +43 -4
  12. package/crates/tish_cranelift_runtime/Cargo.toml +2 -0
  13. package/crates/tish_ffi/src/lib.rs +48 -25
  14. package/crates/tish_ffi/src/v2.rs +349 -0
  15. package/crates/tish_ffi/tests/fixtures/testmod_v2/Cargo.lock +282 -0
  16. package/crates/tish_ffi/tests/fixtures/testmod_v2/Cargo.toml +16 -0
  17. package/crates/tish_ffi/tests/fixtures/testmod_v2/src/lib.rs +111 -0
  18. package/crates/tish_ffi/tests/loader_v2.rs +159 -0
  19. package/crates/tish_ffi_guest/Cargo.toml +11 -0
  20. package/crates/tish_ffi_guest/src/lib.rs +333 -0
  21. package/crates/tish_fmt/Cargo.toml +1 -1
  22. package/crates/tish_native/src/build.rs +54 -15
  23. package/crates/tish_native/src/config.rs +10 -0
  24. package/crates/tish_native/src/lib.rs +7 -2
  25. package/crates/tish_runtime/Cargo.toml +4 -0
  26. package/crates/tish_runtime/src/lib.rs +21 -0
  27. package/crates/tish_vm/Cargo.toml +2 -0
  28. package/crates/tish_vm/src/vm.rs +61 -6
  29. package/crates/tish_vm/tests/ffi_load_module.rs +32 -0
  30. package/crates/tish_vm/tests/jit_off_leaves_jit_state.rs +24 -0
  31. package/crates/tish_vm/tests/jsx_lexical_resolution.rs +42 -0
  32. package/crates/tish_vm/tests/multi_vm_isolation.rs +123 -0
  33. package/package.json +1 -1
  34. package/platform/darwin-arm64/tish-fmt +0 -0
  35. package/platform/darwin-x64/tish-fmt +0 -0
  36. package/platform/linux-arm64/tish-fmt +0 -0
  37. package/platform/linux-x64/tish-fmt +0 -0
  38. 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 (don't cross-compile mimalloc's C). Adds one cached dep + a global_alloc
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.cargo_target.is_none();
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 src_file = if build_config.artifact == NativeArtifact::StaticLib {
245
- "lib.rs"
246
- } else {
247
- "main.rs"
248
- };
249
- let crate_section = if build_config.artifact == NativeArtifact::StaticLib {
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 = ["staticlib"]
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 = if build_config.artifact == NativeArtifact::StaticLib {
321
- tishlang_build_utils::find_release_staticlib(&binary_dir, &cargo_name)?
322
- } else {
323
- tishlang_build_utils::find_release_binary(&binary_dir, &cargo_name)?
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::StaticLib {
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
- let cross_cap = build_config.cargo_target.as_ref().map(|_| {
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 build_config.cargo_target.is_some() {
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" }
@@ -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
- crate::jit::reset_callees();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-format",
3
- "version": "3.12.2",
3
+ "version": "3.15.0",
4
4
  "description": "Tish code formatter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file