@tishlang/tish 1.9.2 → 1.12.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/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +8 -6
- package/crates/js_to_tish/src/transform/stmt.rs +12 -13
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cargo_native_registry.rs +4 -1
- package/crates/tish/src/cli_help.rs +9 -1
- package/crates/tish/src/main.rs +66 -11
- package/crates/tish/tests/integration_test.rs +145 -7
- package/crates/tish_ast/src/ast.rs +3 -9
- package/crates/tish_build_utils/src/lib.rs +74 -23
- package/crates/tish_builtins/src/array.rs +2 -3
- package/crates/tish_builtins/src/construct.rs +15 -28
- package/crates/tish_builtins/src/globals.rs +18 -16
- package/crates/tish_builtins/src/helpers.rs +1 -4
- package/crates/tish_builtins/src/lib.rs +1 -0
- package/crates/tish_builtins/src/math.rs +7 -0
- package/crates/tish_builtins/src/object.rs +10 -10
- package/crates/tish_builtins/src/string.rs +27 -3
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_compile/src/codegen.rs +324 -158
- package/crates/tish_compile/src/lib.rs +39 -7
- package/crates/tish_compile/src/resolve.rs +191 -6
- package/crates/tish_compile/src/types.rs +6 -6
- package/crates/tish_compile_js/src/codegen.rs +8 -5
- package/crates/tish_core/src/console_style.rs +9 -0
- package/crates/tish_core/src/json.rs +17 -7
- package/crates/tish_core/src/macros.rs +2 -2
- package/crates/tish_core/src/value.rs +213 -4
- package/crates/tish_cranelift/src/link.rs +1 -1
- package/crates/tish_cranelift_runtime/Cargo.toml +4 -0
- package/crates/tish_eval/src/eval.rs +135 -73
- package/crates/tish_eval/src/http.rs +18 -12
- package/crates/tish_eval/src/lib.rs +29 -0
- package/crates/tish_eval/src/regex.rs +1 -1
- package/crates/tish_eval/src/value.rs +89 -4
- package/crates/tish_eval/src/value_convert.rs +30 -8
- package/crates/tish_fmt/src/lib.rs +4 -1
- package/crates/tish_lexer/src/lib.rs +7 -2
- package/crates/tish_llvm/src/lib.rs +2 -2
- package/crates/tish_lsp/src/builtin_goto.rs +111 -10
- package/crates/tish_lsp/src/import_goto.rs +35 -22
- package/crates/tish_lsp/src/main.rs +118 -85
- package/crates/tish_native/src/build.rs +270 -24
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +139 -12
- package/crates/tish_parser/src/lib.rs +5 -2
- package/crates/tish_parser/src/parser.rs +45 -75
- package/crates/tish_pg/src/error.rs +1 -1
- package/crates/tish_pg/src/lib.rs +61 -73
- package/crates/tish_resolve/src/lib.rs +283 -158
- package/crates/tish_resolve/src/pos.rs +10 -2
- package/crates/tish_runtime/Cargo.toml +3 -0
- package/crates/tish_runtime/src/http.rs +39 -39
- package/crates/tish_runtime/src/http_fetch.rs +12 -12
- package/crates/tish_runtime/src/lib.rs +35 -44
- package/crates/tish_runtime/src/native_promise.rs +0 -11
- package/crates/tish_runtime/src/promise.rs +14 -1
- package/crates/tish_runtime/src/promise_io.rs +1 -4
- package/crates/tish_runtime/src/timers.rs +12 -7
- package/crates/tish_runtime/src/ws.rs +40 -27
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +10 -8
- package/crates/tish_ui/src/jsx.rs +6 -4
- package/crates/tish_ui/src/lib.rs +5 -4
- package/crates/tish_ui/src/runtime/hooks.rs +123 -37
- package/crates/tish_ui/src/runtime/mod.rs +21 -41
- package/crates/tish_vm/Cargo.toml +2 -0
- package/crates/tish_vm/src/vm.rs +258 -153
- package/crates/tish_wasm/src/lib.rs +60 -7
- package/crates/tish_wasm_runtime/Cargo.toml +10 -1
- package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
- package/crates/tish_wasm_runtime/src/lib.rs +7 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/discover.rs +10 -5
- package/crates/tishlang_cargo_bindgen/src/infer.rs +18 -8
- package/crates/tishlang_cargo_bindgen/src/lib.rs +25 -26
- package/crates/tishlang_cargo_bindgen/src/main.rs +41 -38
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +4 -1
- package/justfile +3 -3
- 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
|
@@ -30,6 +30,22 @@ impl std::fmt::Display for WasmError {
|
|
|
30
30
|
|
|
31
31
|
impl std::error::Error for WasmError {}
|
|
32
32
|
|
|
33
|
+
/// Map CLI / import capability names to `tishlang_wasm_runtime` Cargo features for wasm32-wasip1.
|
|
34
|
+
/// The full `http` stack (tokio/socket2/…) does not build on WASI here; `http` maps to `promise`
|
|
35
|
+
/// so `Promise` / `await` work. `ws` is skipped for the same reason.
|
|
36
|
+
fn insert_wasi_runtime_cap(out: &mut BTreeSet<String>, cap: &str) {
|
|
37
|
+
match cap {
|
|
38
|
+
"http" => {
|
|
39
|
+
out.insert("promise".to_string());
|
|
40
|
+
}
|
|
41
|
+
"ws" => {}
|
|
42
|
+
"fs" | "process" | "promise" | "timers" | "regex" => {
|
|
43
|
+
out.insert(cap.to_string());
|
|
44
|
+
}
|
|
45
|
+
_ => {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
/// Resolve project, merge modules, and compile to bytecode chunk.
|
|
34
50
|
/// Returns (Chunk, Program) so WASI can extract features for the runtime.
|
|
35
51
|
fn resolve_and_compile_to_chunk(
|
|
@@ -47,8 +63,8 @@ fn resolve_and_compile_to_chunk(
|
|
|
47
63
|
let prog = merge_modules(modules)
|
|
48
64
|
.map(|m| m.program)
|
|
49
65
|
.map_err(|e| WasmError {
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
message: e.to_string(),
|
|
67
|
+
})?;
|
|
52
68
|
if optimize {
|
|
53
69
|
tishlang_opt::optimize(&prog)
|
|
54
70
|
} else {
|
|
@@ -215,17 +231,49 @@ pub fn compile_to_wasm(
|
|
|
215
231
|
emit_wasm_from_chunk(&chunk, output_path)
|
|
216
232
|
}
|
|
217
233
|
|
|
234
|
+
/// Compile a Tish project to a raw serialized bytecode chunk.
|
|
235
|
+
///
|
|
236
|
+
/// Writes a single `{output}` file of the exact bytes that the wasm/WASI runtime entry points
|
|
237
|
+
/// (`start` / `run`) deserialize directly — the same chunk `--target wasm` embeds as base64 in
|
|
238
|
+
/// its generated HTML loader, but written raw with no VM binary, JS glue, or HTML wrapper. Lets a
|
|
239
|
+
/// host that already ships the VM runtime (e.g. a bundler) consume the bytecode without the
|
|
240
|
+
/// throwaway standalone build.
|
|
241
|
+
pub fn compile_to_bytecode(
|
|
242
|
+
entry_path: &Path,
|
|
243
|
+
project_root: Option<&Path>,
|
|
244
|
+
output_path: &Path,
|
|
245
|
+
optimize: bool,
|
|
246
|
+
) -> Result<(), WasmError> {
|
|
247
|
+
let (chunk, _) = resolve_and_compile_to_chunk(entry_path, project_root, optimize)?;
|
|
248
|
+
let bytes = serialize(&chunk);
|
|
249
|
+
if let Some(parent) = output_path.parent().filter(|p| !p.as_os_str().is_empty()) {
|
|
250
|
+
std::fs::create_dir_all(parent).map_err(|e| WasmError {
|
|
251
|
+
message: format!("Cannot create output directory: {}", e),
|
|
252
|
+
})?;
|
|
253
|
+
}
|
|
254
|
+
std::fs::write(output_path, &bytes).map_err(|e| WasmError {
|
|
255
|
+
message: format!("Cannot write {}: {}", output_path.display(), e),
|
|
256
|
+
})?;
|
|
257
|
+
println!("Built: {} ({} bytes)", output_path.display(), bytes.len());
|
|
258
|
+
Ok(())
|
|
259
|
+
}
|
|
260
|
+
|
|
218
261
|
/// Compile a Tish project for Wasmtime/WASI.
|
|
219
262
|
///
|
|
220
263
|
/// Produces a single `{output}.wasm` with embedded bytecode. Run with:
|
|
221
264
|
/// `wasmtime {output}.wasm`
|
|
222
265
|
///
|
|
223
266
|
/// Requires: `rustup target add wasm32-wasip1`
|
|
267
|
+
///
|
|
268
|
+
/// `capabilities` is the same capability list as `tish build --target native` (e.g. from
|
|
269
|
+
/// `native_build_features_from_cli`): merged with `import`-inferred features so globals like
|
|
270
|
+
/// `Promise` / `fetch` work without a top-level `import … from 'http'`.
|
|
224
271
|
pub fn compile_to_wasi(
|
|
225
272
|
entry_path: &Path,
|
|
226
273
|
project_root: Option<&Path>,
|
|
227
274
|
output_path: &Path,
|
|
228
275
|
optimize: bool,
|
|
276
|
+
capabilities: &[String],
|
|
229
277
|
) -> Result<(), WasmError> {
|
|
230
278
|
let (chunk, program) = resolve_and_compile_to_chunk(entry_path, project_root, optimize)?;
|
|
231
279
|
if has_external_native_imports(&program) {
|
|
@@ -233,7 +281,16 @@ pub fn compile_to_wasi(
|
|
|
233
281
|
message: "WASI backend does not support external native imports (tish:egui, @scope/pkg). Built-in tish:fs, tish:http, tish:process, tish:timers are supported.".to_string(),
|
|
234
282
|
});
|
|
235
283
|
}
|
|
236
|
-
let
|
|
284
|
+
let mut wasi_feature_set: BTreeSet<String> = BTreeSet::new();
|
|
285
|
+
for f in extract_native_import_features(&program) {
|
|
286
|
+
insert_wasi_runtime_cap(&mut wasi_feature_set, f.as_str());
|
|
287
|
+
}
|
|
288
|
+
for f in capabilities {
|
|
289
|
+
insert_wasi_runtime_cap(&mut wasi_feature_set, f.as_str());
|
|
290
|
+
}
|
|
291
|
+
// Many scripts use global setTimeout without `import` from timers.
|
|
292
|
+
wasi_feature_set.insert("timers".to_string());
|
|
293
|
+
|
|
237
294
|
let chunk_bytes = serialize(&chunk);
|
|
238
295
|
|
|
239
296
|
let stem = output_path
|
|
@@ -277,10 +334,6 @@ pub fn compile_to_wasi(
|
|
|
277
334
|
.to_string_lossy()
|
|
278
335
|
.replace('\\', "/");
|
|
279
336
|
|
|
280
|
-
// Bundled perf (`tests/main.tish`) and many scripts use global setTimeout without a top-level
|
|
281
|
-
// `import` (so `extract_native_import_features` is empty). Always link timers for WASI VM.
|
|
282
|
-
let mut wasi_feature_set: BTreeSet<String> = wasi_features.into_iter().collect();
|
|
283
|
-
wasi_feature_set.insert("timers".to_string());
|
|
284
337
|
let features_str = format!(
|
|
285
338
|
", features = [{}]",
|
|
286
339
|
wasi_feature_set
|
|
@@ -12,16 +12,25 @@ crate-type = ["cdylib", "rlib"]
|
|
|
12
12
|
[features]
|
|
13
13
|
# For wasm32-unknown-unknown (browser): wasm-bindgen, console output
|
|
14
14
|
browser = ["dep:wasm-bindgen", "tishlang_vm/wasm"]
|
|
15
|
-
#
|
|
15
|
+
# Browser WebGPU / JS-interop FFI + requestAnimationFrame render loop (the
|
|
16
|
+
# `start(chunk, env)` entry). Reflection-based bridge over js-sys; no web-sys
|
|
17
|
+
# WebGPU bindings needed since the WebGPU command API is synchronous.
|
|
18
|
+
gpu = ["browser", "dep:js-sys"]
|
|
19
|
+
# Built-in modules for WASI (wasm32-wasip1): align with `tishlang_cranelift_runtime` / CLI caps
|
|
16
20
|
fs = ["tishlang_vm/fs"]
|
|
17
21
|
process = ["tishlang_vm/process"]
|
|
18
22
|
http = ["tishlang_vm/http"]
|
|
23
|
+
promise = ["tishlang_vm/promise"]
|
|
19
24
|
timers = ["tishlang_vm/timers"]
|
|
25
|
+
regex = ["tishlang_vm/regex"]
|
|
26
|
+
ws = ["tishlang_vm/ws"]
|
|
20
27
|
|
|
21
28
|
[dependencies]
|
|
22
29
|
tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
|
|
23
30
|
tishlang_vm = { path = "../tish_vm", version = ">=0.1" }
|
|
31
|
+
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
24
32
|
wasm-bindgen = { version = "0.2", optional = true }
|
|
33
|
+
js-sys = { version = "0.3", optional = true }
|
|
25
34
|
|
|
26
35
|
# rand_core → getrandom 0.4 needs wasm_js on wasm32-unknown-unknown (browser VM build).
|
|
27
36
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
//! Browser WebGPU / JS-interop FFI for the tish bytecode VM.
|
|
2
|
+
//!
|
|
3
|
+
//! Lets a tish program compiled to wasm drive the browser's WebGPU (and any
|
|
4
|
+
//! Web API) without hand-binding each call. The bridge is a tiny set of
|
|
5
|
+
//! reflection-based primitives exposed as VM globals:
|
|
6
|
+
//!
|
|
7
|
+
//! - `js_global(name)` — read a JS global (e.g. `navigator`, `GPUBufferUsage`)
|
|
8
|
+
//! - `js_get(handle, key)` / `js_set(handle, key, val)`
|
|
9
|
+
//! - `js_call(handle, method, argsArray)` — call a method (the whole WebGPU
|
|
10
|
+
//! command API is synchronous, so this covers it)
|
|
11
|
+
//! - `js_new(ctorNameOrHandle, argsArray)`
|
|
12
|
+
//! - `js_typeof(handle)` — debugging
|
|
13
|
+
//! - `f32a(arr)` / `u16a(arr)` / `u8a(arr)` — tish `number[]` → real typed array
|
|
14
|
+
//! - `request_animation_frame(cb)` — drive a render loop
|
|
15
|
+
//!
|
|
16
|
+
//! GPU/JS objects (device, queue, context, buffers, pipelines, textures,
|
|
17
|
+
//! ImageBitmaps, the host env object …) round-trip through the VM as opaque
|
|
18
|
+
//! [`JsHandle`] values. Async startup (requestAdapter/requestDevice/fetch/
|
|
19
|
+
//! createImageBitmap) is done in JS glue; the ready handles are handed to the
|
|
20
|
+
//! VM via the `host` global by [`start`].
|
|
21
|
+
|
|
22
|
+
use std::any::Any;
|
|
23
|
+
use std::cell::{Cell, RefCell};
|
|
24
|
+
use std::sync::Arc;
|
|
25
|
+
|
|
26
|
+
use tishlang_bytecode::deserialize;
|
|
27
|
+
use tishlang_core::{value_call, NativeFn, TishOpaque, Value};
|
|
28
|
+
use tishlang_vm::Vm;
|
|
29
|
+
use wasm_bindgen::prelude::*;
|
|
30
|
+
use wasm_bindgen::JsCast;
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Opaque JS handle
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/// Opaque tish value wrapping a browser `JsValue`. `JsValue` is `!Send`, which
|
|
37
|
+
/// is why `TishOpaque`'s `Send + Sync` bound is gated off in the browser
|
|
38
|
+
/// (`!send-values`) build — see `tish_core/src/value.rs`.
|
|
39
|
+
struct JsHandle(JsValue);
|
|
40
|
+
|
|
41
|
+
impl TishOpaque for JsHandle {
|
|
42
|
+
fn type_name(&self) -> &'static str {
|
|
43
|
+
"JsHandle"
|
|
44
|
+
}
|
|
45
|
+
fn get_method(&self, _name: &str) -> Option<NativeFn> {
|
|
46
|
+
None
|
|
47
|
+
}
|
|
48
|
+
fn as_any(&self) -> &dyn Any {
|
|
49
|
+
self
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn wrap(v: JsValue) -> Value {
|
|
54
|
+
Value::Opaque(Arc::new(JsHandle(v)))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn unwrap_handle(v: &Value) -> Option<JsValue> {
|
|
58
|
+
match v {
|
|
59
|
+
Value::Opaque(o) => o.as_any().downcast_ref::<JsHandle>().map(|h| h.0.clone()),
|
|
60
|
+
_ => None,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Marshalling tish Value <-> JsValue
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/// Convert a tish value to a JS value. Objects/arrays recurse; an opaque
|
|
69
|
+
/// `JsHandle` is spliced in **by reference** (so descriptors can embed live GPU
|
|
70
|
+
/// handles, e.g. `beginRenderPass({ colorAttachments:[{ view:<handle> }] })`).
|
|
71
|
+
/// Functions/promises/symbols are not marshalled (return `null`).
|
|
72
|
+
fn value_to_js(v: &Value) -> JsValue {
|
|
73
|
+
match v {
|
|
74
|
+
Value::Number(n) => JsValue::from_f64(*n),
|
|
75
|
+
Value::String(s) => JsValue::from_str(s.as_ref()),
|
|
76
|
+
Value::Bool(b) => JsValue::from_bool(*b),
|
|
77
|
+
Value::Null => JsValue::NULL,
|
|
78
|
+
Value::Opaque(o) => match o.as_any().downcast_ref::<JsHandle>() {
|
|
79
|
+
Some(h) => h.0.clone(),
|
|
80
|
+
None => JsValue::NULL,
|
|
81
|
+
},
|
|
82
|
+
Value::Array(arr) => {
|
|
83
|
+
let out = js_sys::Array::new();
|
|
84
|
+
for item in arr.borrow().iter() {
|
|
85
|
+
out.push(&value_to_js(item));
|
|
86
|
+
}
|
|
87
|
+
out.into()
|
|
88
|
+
}
|
|
89
|
+
Value::Object(obj) => {
|
|
90
|
+
let out = js_sys::Object::new();
|
|
91
|
+
let b = obj.borrow();
|
|
92
|
+
for (k, val) in b.strings.iter() {
|
|
93
|
+
let _ = js_sys::Reflect::set(
|
|
94
|
+
&out,
|
|
95
|
+
&JsValue::from_str(k.as_ref()),
|
|
96
|
+
&value_to_js(val),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
out.into()
|
|
100
|
+
}
|
|
101
|
+
_ => JsValue::NULL,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Convert a JS value back to tish. Primitives map directly; everything else
|
|
106
|
+
/// (objects, functions, typed arrays, GPU objects) becomes an opaque handle —
|
|
107
|
+
/// we deliberately do **not** expand JS containers into tish arrays/objects
|
|
108
|
+
/// (that would re-introduce boxing and lose typed-array identity).
|
|
109
|
+
fn js_to_value(v: JsValue) -> Value {
|
|
110
|
+
if v.is_null() || v.is_undefined() {
|
|
111
|
+
Value::Null
|
|
112
|
+
} else if let Some(n) = v.as_f64() {
|
|
113
|
+
Value::Number(n)
|
|
114
|
+
} else if let Some(b) = v.as_bool() {
|
|
115
|
+
Value::Bool(b)
|
|
116
|
+
} else if let Some(s) = v.as_string() {
|
|
117
|
+
Value::String(s.into())
|
|
118
|
+
} else {
|
|
119
|
+
wrap(v)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// FFI builtins
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
fn ffi_js_global() -> Value {
|
|
128
|
+
Value::native(|args: &[Value]| {
|
|
129
|
+
let name = match args.first() {
|
|
130
|
+
Some(Value::String(s)) => s.clone(),
|
|
131
|
+
_ => return Value::Null,
|
|
132
|
+
};
|
|
133
|
+
match js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str(name.as_ref())) {
|
|
134
|
+
Ok(v) => js_to_value(v),
|
|
135
|
+
Err(_) => Value::Null,
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn ffi_js_get() -> Value {
|
|
141
|
+
Value::native(|args: &[Value]| {
|
|
142
|
+
let obj = match args.first().and_then(unwrap_handle) {
|
|
143
|
+
Some(o) => o,
|
|
144
|
+
None => return Value::Null,
|
|
145
|
+
};
|
|
146
|
+
let key = args.get(1).map(value_to_js).unwrap_or(JsValue::NULL);
|
|
147
|
+
match js_sys::Reflect::get(&obj, &key) {
|
|
148
|
+
Ok(v) => js_to_value(v),
|
|
149
|
+
Err(_) => Value::Null,
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn ffi_js_set() -> Value {
|
|
155
|
+
Value::native(|args: &[Value]| {
|
|
156
|
+
let obj = match args.first().and_then(unwrap_handle) {
|
|
157
|
+
Some(o) => o,
|
|
158
|
+
None => return Value::Null,
|
|
159
|
+
};
|
|
160
|
+
let key = args.get(1).map(value_to_js).unwrap_or(JsValue::NULL);
|
|
161
|
+
let val = args.get(2).map(value_to_js).unwrap_or(JsValue::NULL);
|
|
162
|
+
let _ = js_sys::Reflect::set(&obj, &key, &val);
|
|
163
|
+
Value::Null
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn ffi_js_call() -> Value {
|
|
168
|
+
Value::native(|args: &[Value]| {
|
|
169
|
+
let obj = match args.first().and_then(unwrap_handle) {
|
|
170
|
+
Some(o) => o,
|
|
171
|
+
None => return Value::Null,
|
|
172
|
+
};
|
|
173
|
+
let method = match args.get(1) {
|
|
174
|
+
Some(Value::String(s)) => s.clone(),
|
|
175
|
+
_ => return Value::Null,
|
|
176
|
+
};
|
|
177
|
+
let func = match js_sys::Reflect::get(&obj, &JsValue::from_str(method.as_ref())) {
|
|
178
|
+
Ok(f) => match f.dyn_into::<js_sys::Function>() {
|
|
179
|
+
Ok(f) => f,
|
|
180
|
+
Err(_) => return Value::Null,
|
|
181
|
+
},
|
|
182
|
+
Err(_) => return Value::Null,
|
|
183
|
+
};
|
|
184
|
+
let js_args = js_sys::Array::new();
|
|
185
|
+
if let Some(Value::Array(a)) = args.get(2) {
|
|
186
|
+
for item in a.borrow().iter() {
|
|
187
|
+
js_args.push(&value_to_js(item));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
match js_sys::Reflect::apply(&func, &obj, &js_args) {
|
|
191
|
+
Ok(v) => js_to_value(v),
|
|
192
|
+
Err(_) => Value::Null,
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn ffi_js_new() -> Value {
|
|
198
|
+
Value::native(|args: &[Value]| {
|
|
199
|
+
let ctor: JsValue = match args.first() {
|
|
200
|
+
Some(Value::String(s)) => {
|
|
201
|
+
match js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str(s.as_ref())) {
|
|
202
|
+
Ok(v) => v,
|
|
203
|
+
Err(_) => return Value::Null,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
Some(v @ Value::Opaque(_)) => match unwrap_handle(v) {
|
|
207
|
+
Some(h) => h,
|
|
208
|
+
None => return Value::Null,
|
|
209
|
+
},
|
|
210
|
+
_ => return Value::Null,
|
|
211
|
+
};
|
|
212
|
+
let ctor_fn = match ctor.dyn_into::<js_sys::Function>() {
|
|
213
|
+
Ok(f) => f,
|
|
214
|
+
Err(_) => return Value::Null,
|
|
215
|
+
};
|
|
216
|
+
let js_args = js_sys::Array::new();
|
|
217
|
+
if let Some(Value::Array(a)) = args.get(1) {
|
|
218
|
+
for item in a.borrow().iter() {
|
|
219
|
+
js_args.push(&value_to_js(item));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
match js_sys::Reflect::construct(&ctor_fn, &js_args) {
|
|
223
|
+
Ok(v) => js_to_value(v),
|
|
224
|
+
Err(_) => Value::Null,
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn ffi_js_typeof() -> Value {
|
|
230
|
+
Value::native(|args: &[Value]| {
|
|
231
|
+
let v = args.first().map(value_to_js).unwrap_or(JsValue::NULL);
|
|
232
|
+
match v.js_typeof().as_string() {
|
|
233
|
+
Some(s) => Value::String(s.into()),
|
|
234
|
+
None => Value::Null,
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// `f32a(numberArray)` -> opaque `Float32Array` handle (one-shot copy). Use for
|
|
240
|
+
/// per-frame uniform/transform staging. Large static buffers should instead be
|
|
241
|
+
/// materialised host-side and passed in opaque (never boxed into a tish array).
|
|
242
|
+
fn ffi_f32a() -> Value {
|
|
243
|
+
Value::native(|args: &[Value]| {
|
|
244
|
+
let arr = match args.first() {
|
|
245
|
+
Some(Value::Array(a)) => a.clone(),
|
|
246
|
+
_ => return Value::Null,
|
|
247
|
+
};
|
|
248
|
+
let b = arr.borrow();
|
|
249
|
+
let ta = js_sys::Float32Array::new_with_length(b.len() as u32);
|
|
250
|
+
for (i, v) in b.iter().enumerate() {
|
|
251
|
+
ta.set_index(i as u32, v.as_number().unwrap_or(0.0) as f32);
|
|
252
|
+
}
|
|
253
|
+
wrap(ta.into())
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn ffi_u16a() -> Value {
|
|
258
|
+
Value::native(|args: &[Value]| {
|
|
259
|
+
let arr = match args.first() {
|
|
260
|
+
Some(Value::Array(a)) => a.clone(),
|
|
261
|
+
_ => return Value::Null,
|
|
262
|
+
};
|
|
263
|
+
let b = arr.borrow();
|
|
264
|
+
let ta = js_sys::Uint16Array::new_with_length(b.len() as u32);
|
|
265
|
+
for (i, v) in b.iter().enumerate() {
|
|
266
|
+
ta.set_index(i as u32, v.as_number().unwrap_or(0.0) as u16);
|
|
267
|
+
}
|
|
268
|
+
wrap(ta.into())
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn ffi_u8a() -> Value {
|
|
273
|
+
Value::native(|args: &[Value]| {
|
|
274
|
+
let arr = match args.first() {
|
|
275
|
+
Some(Value::Array(a)) => a.clone(),
|
|
276
|
+
_ => return Value::Null,
|
|
277
|
+
};
|
|
278
|
+
let b = arr.borrow();
|
|
279
|
+
let ta = js_sys::Uint8Array::new_with_length(b.len() as u32);
|
|
280
|
+
for (i, v) in b.iter().enumerate() {
|
|
281
|
+
ta.set_index(i as u32, v.as_number().unwrap_or(0.0) as u8);
|
|
282
|
+
}
|
|
283
|
+
wrap(ta.into())
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// requestAnimationFrame render loop
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
thread_local! {
|
|
292
|
+
static RAF_CALLBACK: RefCell<Option<Value>> = const { RefCell::new(None) };
|
|
293
|
+
static RAF_CLOSURE: RefCell<Option<Closure<dyn FnMut(f64)>>> = const { RefCell::new(None) };
|
|
294
|
+
// True while a frame is pending, so repeated request_animation_frame calls
|
|
295
|
+
// within one frame don't compound into runaway scheduling.
|
|
296
|
+
static RAF_SCHEDULED: Cell<bool> = const { Cell::new(false) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Browser-driven per-frame entry: invoke the stored tish callback, then
|
|
300
|
+
/// re-arm for the next frame. We re-schedule from Rust (rather than requiring
|
|
301
|
+
/// the tish callback to call `request_animation_frame` again each frame) so the
|
|
302
|
+
/// loop runs continuously as long as a callback is registered. `cancel`-ing the
|
|
303
|
+
/// loop = clearing `RAF_CALLBACK`. The `value_call` runs the frame closure to
|
|
304
|
+
/// completion; all WebGPU recording happens synchronously inside.
|
|
305
|
+
fn tick(ts: f64) {
|
|
306
|
+
let cb = RAF_CALLBACK.with(|c| c.borrow().clone());
|
|
307
|
+
// This frame's pending schedule is now consumed.
|
|
308
|
+
RAF_SCHEDULED.with(|f| f.set(false));
|
|
309
|
+
if let Some(cb) = cb {
|
|
310
|
+
if matches!(cb, Value::Function(_)) {
|
|
311
|
+
value_call(&cb, &[Value::Number(ts)]);
|
|
312
|
+
}
|
|
313
|
+
// Re-arm only if the callback is still registered (allows a future
|
|
314
|
+
// cancel by clearing RAF_CALLBACK).
|
|
315
|
+
if RAF_CALLBACK.with(|c| c.borrow().is_some()) {
|
|
316
|
+
schedule_raf();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn schedule_raf() {
|
|
322
|
+
// Coalesce: at most one rAF in flight at a time.
|
|
323
|
+
if RAF_SCHEDULED.with(|f| f.get()) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
RAF_SCHEDULED.with(|f| f.set(true));
|
|
327
|
+
RAF_CLOSURE.with(|slot| {
|
|
328
|
+
let mut s = slot.borrow_mut();
|
|
329
|
+
if s.is_none() {
|
|
330
|
+
*s = Some(Closure::wrap(Box::new(|ts: f64| tick(ts)) as Box<dyn FnMut(f64)>));
|
|
331
|
+
}
|
|
332
|
+
let g = js_sys::global();
|
|
333
|
+
if let Ok(raf) = js_sys::Reflect::get(&g, &JsValue::from_str("requestAnimationFrame")) {
|
|
334
|
+
if let Ok(raf_fn) = raf.dyn_into::<js_sys::Function>() {
|
|
335
|
+
let _ = raf_fn.call1(&g, s.as_ref().unwrap().as_ref().unchecked_ref());
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn ffi_request_animation_frame() -> Value {
|
|
342
|
+
Value::native(|args: &[Value]| {
|
|
343
|
+
if let Some(cb) = args.first() {
|
|
344
|
+
RAF_CALLBACK.with(|c| *c.borrow_mut() = Some(cb.clone()));
|
|
345
|
+
}
|
|
346
|
+
schedule_raf();
|
|
347
|
+
Value::Null
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Install + entry point
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
fn install_ffi(vm: &mut Vm) {
|
|
356
|
+
vm.set_global("js_global".into(), ffi_js_global());
|
|
357
|
+
vm.set_global("js_get".into(), ffi_js_get());
|
|
358
|
+
vm.set_global("js_set".into(), ffi_js_set());
|
|
359
|
+
vm.set_global("js_call".into(), ffi_js_call());
|
|
360
|
+
vm.set_global("js_new".into(), ffi_js_new());
|
|
361
|
+
vm.set_global("js_typeof".into(), ffi_js_typeof());
|
|
362
|
+
vm.set_global("f32a".into(), ffi_f32a());
|
|
363
|
+
vm.set_global("u16a".into(), ffi_u16a());
|
|
364
|
+
vm.set_global("u8a".into(), ffi_u8a());
|
|
365
|
+
vm.set_global("request_animation_frame".into(), ffi_request_animation_frame());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#[wasm_bindgen]
|
|
369
|
+
extern "C" {
|
|
370
|
+
#[wasm_bindgen(js_namespace = console, js_name = error)]
|
|
371
|
+
fn console_error(s: &str);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn set_panic_hook() {
|
|
375
|
+
use std::sync::Once;
|
|
376
|
+
static HOOK: Once = Once::new();
|
|
377
|
+
HOOK.call_once(|| {
|
|
378
|
+
std::panic::set_hook(Box::new(|info| {
|
|
379
|
+
console_error(&format!("tish wasm panic: {}", info));
|
|
380
|
+
}));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Browser entry: run a tish bytecode `chunk` with the JS-interop FFI installed
|
|
385
|
+
/// and the host environment object (device/queue/context/format/canvas/assets,
|
|
386
|
+
/// built by the page's async startup glue) exposed as the `host` global.
|
|
387
|
+
///
|
|
388
|
+
/// Returns after top-level tish runs; the `requestAnimationFrame` loop keeps
|
|
389
|
+
/// the captured globals alive via the stored callback, so the VM state persists
|
|
390
|
+
/// across frames even though this call returns.
|
|
391
|
+
/// Invoke the registered frame callback exactly once, without re-scheduling.
|
|
392
|
+
/// For driving frames deterministically from JS when `requestAnimationFrame` is
|
|
393
|
+
/// throttled (e.g. a hidden/offscreen preview tab) — verification & debugging.
|
|
394
|
+
#[wasm_bindgen]
|
|
395
|
+
pub fn tick_once(ts: f64) {
|
|
396
|
+
let cb = RAF_CALLBACK.with(|c| c.borrow().clone());
|
|
397
|
+
if let Some(cb) = cb {
|
|
398
|
+
if matches!(cb, Value::Function(_)) {
|
|
399
|
+
value_call(&cb, &[Value::Number(ts)]);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#[wasm_bindgen]
|
|
405
|
+
pub fn start(chunk: Vec<u8>, env: JsValue) -> Result<(), JsValue> {
|
|
406
|
+
set_panic_hook();
|
|
407
|
+
let chunk = deserialize(&chunk).map_err(|e| JsValue::from_str(&e))?;
|
|
408
|
+
let mut vm = Vm::new();
|
|
409
|
+
install_ffi(&mut vm);
|
|
410
|
+
vm.set_global("host".into(), wrap(env));
|
|
411
|
+
vm.run(&chunk).map_err(|e| JsValue::from_str(&e))?;
|
|
412
|
+
Ok(())
|
|
413
|
+
}
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Two targets:
|
|
4
4
|
//! - **Browser** (wasm32-unknown-unknown): use `--features browser`, wasm-bindgen, console output
|
|
5
|
-
//! - **WASI/Wasmtime** (wasm32-wasip1): optional `timers` / `http` / … via Cargo features; `compile_to_wasi`
|
|
5
|
+
//! - **WASI/Wasmtime** (wasm32-wasip1): optional `timers` / `http` / … via Cargo features; `compile_to_wasi`
|
|
6
|
+
//! merges CLI capability flags with imports and always enables `timers` when globals use `setTimeout`.
|
|
6
7
|
|
|
7
8
|
use tishlang_bytecode::deserialize;
|
|
8
9
|
use tishlang_vm::Vm;
|
|
9
10
|
|
|
11
|
+
/// Browser WebGPU / JS-interop FFI + requestAnimationFrame render loop.
|
|
12
|
+
/// Adds the `start(chunk, env)` wasm-bindgen entry used by the engine.
|
|
13
|
+
#[cfg(feature = "gpu")]
|
|
14
|
+
pub mod gpu;
|
|
15
|
+
|
|
10
16
|
/// Run serialized Tish bytecode (WASI/Wasmtime or native).
|
|
11
17
|
///
|
|
12
18
|
/// `chunk` is the output of `tishlang_bytecode::serialize(chunk)`.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
//! Classify a `pub fn` from syn for glue emission (driven by signature shape, not crate name).
|
|
2
2
|
|
|
3
|
-
use syn::{
|
|
4
|
-
FnArg, GenericArgument, ItemFn, PathArguments, ReturnType, Type, TypeReference,
|
|
5
|
-
};
|
|
3
|
+
use syn::{FnArg, GenericArgument, ItemFn, PathArguments, ReturnType, Type, TypeReference};
|
|
6
4
|
|
|
7
5
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
8
6
|
pub enum SignatureClass {
|
|
@@ -26,8 +26,10 @@ pub fn discover_public_functions(crate_root: &Path) -> Result<HashMap<String, It
|
|
|
26
26
|
.filter(|e| e.path().extension().map(|x| x == "rs").unwrap_or(false))
|
|
27
27
|
{
|
|
28
28
|
let path = entry.path();
|
|
29
|
-
let text =
|
|
30
|
-
|
|
29
|
+
let text =
|
|
30
|
+
fs::read_to_string(path).map_err(|e| format!("read {}: {}", path.display(), e))?;
|
|
31
|
+
let file =
|
|
32
|
+
syn::parse_file(&text).map_err(|e| format!("parse {}: {}", path.display(), e))?;
|
|
31
33
|
|
|
32
34
|
for item in file.items {
|
|
33
35
|
if let Item::Fn(f) = item {
|
|
@@ -70,8 +72,10 @@ pub fn rust_public_fn_location(
|
|
|
70
72
|
.filter(|e| e.path().extension().map(|x| x == "rs").unwrap_or(false))
|
|
71
73
|
{
|
|
72
74
|
let path = entry.path();
|
|
73
|
-
let text =
|
|
74
|
-
|
|
75
|
+
let text =
|
|
76
|
+
fs::read_to_string(path).map_err(|e| format!("read {}: {}", path.display(), e))?;
|
|
77
|
+
let file =
|
|
78
|
+
syn::parse_file(&text).map_err(|e| format!("parse {}: {}", path.display(), e))?;
|
|
75
79
|
|
|
76
80
|
for item in file.items {
|
|
77
81
|
if let Item::Fn(f) = item {
|
|
@@ -85,7 +89,8 @@ pub fn rust_public_fn_location(
|
|
|
85
89
|
let line = u32::try_from(lc.line)
|
|
86
90
|
.map_err(|_| "span line out of range".to_string())?
|
|
87
91
|
.saturating_sub(1);
|
|
88
|
-
let col =
|
|
92
|
+
let col =
|
|
93
|
+
u32::try_from(lc.column).map_err(|_| "span column out of range".to_string())?;
|
|
89
94
|
return Ok((path.to_path_buf(), line, col));
|
|
90
95
|
}
|
|
91
96
|
}
|