@tishlang/tish 1.13.2 → 2.0.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 +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//! Regression: concurrent HTTP handlers that mutate shared module-level state must not deadlock.
|
|
2
|
+
//!
|
|
3
|
+
//! ## What this guards
|
|
4
|
+
//!
|
|
5
|
+
//! Under `send-values` (forced on by the `http` feature), `serve(port, handler)` runs the handler
|
|
6
|
+
//! closure — a `NativeFn` (`Arc<dyn Callable>`, `Send + Sync`) — **directly on each accept thread**
|
|
7
|
+
//! (`tish_runtime::http::worker_loop_direct`). So N concurrent requests execute the SAME handler in
|
|
8
|
+
//! parallel, all sharing the captured module scope through `Arc<Mutex>` (`VmRef`). A handler that
|
|
9
|
+
//! mutates a module-level `let` (a request counter / cache / rate-limiter) therefore has many threads
|
|
10
|
+
//! reading and writing the same scope cell at once.
|
|
11
|
+
//!
|
|
12
|
+
//! This test drives that exact path without a network: it pulls the handler `Value::Function` out of
|
|
13
|
+
//! a freshly-run program and invokes it from many OS threads while they all read-modify-write a shared
|
|
14
|
+
//! module-level `let`. It exists to catch a regression where the VM's variable-write path holds a
|
|
15
|
+
//! scope guard across a re-acquisition of the same lock (which would deadlock concurrent writers and
|
|
16
|
+
//! hang `serve`). The watchdog turns such a hang into a fast, explicit test failure instead of a
|
|
17
|
+
//! stuck CI job.
|
|
18
|
+
//!
|
|
19
|
+
//! Note on coverage: macOS `SO_REUSEPORT` funnels HTTP accepts to a single worker thread, so a
|
|
20
|
+
//! network-level test can't actually run two handlers concurrently on macOS. Calling the handler
|
|
21
|
+
//! closure directly does — and OS thread scheduling + mutex semantics are platform-independent, so
|
|
22
|
+
//! this reproduces the Linux multi-worker dispatch contention the bug was reported against.
|
|
23
|
+
#![cfg(feature = "send-values")]
|
|
24
|
+
|
|
25
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
26
|
+
use std::sync::Arc;
|
|
27
|
+
use std::time::{Duration, Instant};
|
|
28
|
+
use tishlang_bytecode::compile;
|
|
29
|
+
use tishlang_core::{NativeFn, Value};
|
|
30
|
+
use tishlang_vm::Vm;
|
|
31
|
+
|
|
32
|
+
// `Value::Function` holds a `NativeFn` (= `Arc<dyn Callable>`, `Callable: Send + Sync` under
|
|
33
|
+
// send-values); invoke a handler via the trait-object method `.call(args)`.
|
|
34
|
+
type Handler = NativeFn;
|
|
35
|
+
|
|
36
|
+
/// Compile + run `src`, then pull a function it stored in a global back out.
|
|
37
|
+
fn export(vm: &Vm, name: &str) -> Handler {
|
|
38
|
+
match vm.get_global(name).unwrap_or_else(|| panic!("global `{name}` not found")) {
|
|
39
|
+
Value::Function(f) => f,
|
|
40
|
+
other => panic!("global `{name}` is not a function: {other:?}"),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn read_num(obj: &Value, field: &str) -> f64 {
|
|
45
|
+
match obj {
|
|
46
|
+
Value::Object(o) => match o.borrow().strings.get(field) {
|
|
47
|
+
Some(Value::Number(n)) => *n,
|
|
48
|
+
other => panic!("stats.{field} is not a number: {other:?}"),
|
|
49
|
+
},
|
|
50
|
+
other => panic!("stats is not an object: {other:?}"),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[test]
|
|
55
|
+
fn concurrent_handlers_mutating_shared_module_state_do_not_deadlock() {
|
|
56
|
+
// `handler`/`stats` are bare top-level assignments (undeclared names) -> stored in globals, so
|
|
57
|
+
// the test can pull them out. Both functions close over the same module-level `let`s, exactly as
|
|
58
|
+
// a real `serve` handler closes over module state. `served` is monotonic (only incremented), so
|
|
59
|
+
// it gives a deterministic plausibility bound even though the read-modify-write is racy.
|
|
60
|
+
let src = r#"
|
|
61
|
+
let active = 0
|
|
62
|
+
let maxActive = 0
|
|
63
|
+
let served = 0
|
|
64
|
+
fn handleRequest(req) {
|
|
65
|
+
active = active + 1
|
|
66
|
+
served = served + 1
|
|
67
|
+
if (active > maxActive) { maxActive = active }
|
|
68
|
+
let i = 0
|
|
69
|
+
while (i < 2000) { i = i + 1 } // brief CPU hold so handlers overlap
|
|
70
|
+
active = active - 1
|
|
71
|
+
return { status: 200, body: "ok" }
|
|
72
|
+
}
|
|
73
|
+
fn getStats() {
|
|
74
|
+
return { active: active, maxActive: maxActive, served: served }
|
|
75
|
+
}
|
|
76
|
+
handler = handleRequest
|
|
77
|
+
stats = getStats
|
|
78
|
+
"#;
|
|
79
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
80
|
+
let chunk = compile(&program).expect("compile");
|
|
81
|
+
let mut vm = Vm::new();
|
|
82
|
+
vm.run(&chunk).expect("run top-level");
|
|
83
|
+
let handler = export(&vm, "handler");
|
|
84
|
+
let stats = export(&vm, "stats");
|
|
85
|
+
|
|
86
|
+
const THREADS: usize = 12;
|
|
87
|
+
const ITERS: usize = 100;
|
|
88
|
+
let total = THREADS * ITERS;
|
|
89
|
+
|
|
90
|
+
let done = Arc::new(AtomicUsize::new(0));
|
|
91
|
+
let start = Instant::now();
|
|
92
|
+
let mut handles = Vec::with_capacity(THREADS);
|
|
93
|
+
for t in 0..THREADS {
|
|
94
|
+
let h = handler.clone();
|
|
95
|
+
let done = Arc::clone(&done);
|
|
96
|
+
handles.push(std::thread::spawn(move || {
|
|
97
|
+
for i in 0..ITERS {
|
|
98
|
+
let resp = h.call(&[Value::Number((t * ITERS + i) as f64)]);
|
|
99
|
+
assert!(matches!(resp, Value::Object(_)), "handler must return a response object");
|
|
100
|
+
done.fetch_add(1, Ordering::Relaxed);
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Watchdog: if concurrent writers deadlocked on the scope mutex, `done` stops advancing.
|
|
106
|
+
// Fail fast (and loudly) rather than hang the test runner.
|
|
107
|
+
let mut last = 0usize;
|
|
108
|
+
let mut last_change = Instant::now();
|
|
109
|
+
while done.load(Ordering::Relaxed) < total {
|
|
110
|
+
let cur = done.load(Ordering::Relaxed);
|
|
111
|
+
if cur != last {
|
|
112
|
+
last = cur;
|
|
113
|
+
last_change = Instant::now();
|
|
114
|
+
}
|
|
115
|
+
assert!(
|
|
116
|
+
last_change.elapsed() < Duration::from_secs(15),
|
|
117
|
+
"DEADLOCK regression: concurrent handlers stalled at {cur}/{total} (no progress for 15s)"
|
|
118
|
+
);
|
|
119
|
+
std::thread::sleep(Duration::from_millis(20));
|
|
120
|
+
}
|
|
121
|
+
for h in handles {
|
|
122
|
+
h.join().expect("a handler thread panicked");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// All calls returned without hanging. Read the shared counters back.
|
|
126
|
+
let s = stats.call(&[]);
|
|
127
|
+
let served = read_num(&s, "served");
|
|
128
|
+
let max_active = read_num(&s, "maxActive");
|
|
129
|
+
let active = read_num(&s, "active");
|
|
130
|
+
eprintln!(
|
|
131
|
+
"completed {total} concurrent calls / {THREADS} threads in {:?}; served={served}, maxActive={max_active}, active(final)={active}",
|
|
132
|
+
start.elapsed()
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// `served` is monotonic, so it is deterministically in (0, total] regardless of lost updates.
|
|
136
|
+
assert!(served > 0.0 && served <= total as f64, "served={served} out of plausible range (0, {total}]");
|
|
137
|
+
// `maxActive` >= 2 proves at least two handlers were genuinely in-flight simultaneously, i.e. we
|
|
138
|
+
// actually exercised concurrent shared-state mutation (not an accidentally-serialized run).
|
|
139
|
+
assert!(max_active >= 2.0, "handlers never overlapped (maxActive={max_active}); test did not exercise concurrency");
|
|
140
|
+
}
|
|
@@ -379,10 +379,14 @@ fn main() {
|
|
|
379
379
|
message: format!("Cannot write main.rs: {}", e),
|
|
380
380
|
})?;
|
|
381
381
|
|
|
382
|
-
// Build
|
|
382
|
+
// Build into a SHARED target dir (one per host), not per-program. The wasi runtime + embedded
|
|
383
|
+
// VM then compile ONCE and are reused by every wasi build; only each program's tiny main is
|
|
384
|
+
// rebuilt. Without this each program left its own multi-GB `target/` and a full-suite sweep
|
|
385
|
+
// would fill the disk (same issue fixed for cranelift; see full-backend-parity-plan.md A3).
|
|
386
|
+
// cargo's target lock serializes concurrent builds safely.
|
|
383
387
|
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
|
|
384
388
|
let bin_name = format!("tish_wasi_{}", stem);
|
|
385
|
-
let target_dir =
|
|
389
|
+
let target_dir = std::env::temp_dir().join("tishlang_wasi_target");
|
|
386
390
|
let build_status = Command::new(&cargo)
|
|
387
391
|
.current_dir(&build_dir)
|
|
388
392
|
.env("CARGO_TARGET_DIR", &target_dir)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//! command API is synchronous, so this covers it)
|
|
11
11
|
//! - `js_new(ctorNameOrHandle, argsArray)`
|
|
12
12
|
//! - `js_typeof(handle)` — debugging
|
|
13
|
-
//! - `f32a(arr)` / `u16a(arr)` / `u8a(arr)` — tish `number[]` → real typed array
|
|
13
|
+
//! - `f32a(arr)` / `u16a(arr)` / `u8a(arr)` / `u32a(arr)` — tish `number[]` → real typed array
|
|
14
14
|
//! - `request_animation_frame(cb)` — drive a render loop
|
|
15
15
|
//!
|
|
16
16
|
//! GPU/JS objects (device, queue, context, buffers, pipelines, textures,
|
|
@@ -284,6 +284,21 @@ fn ffi_u8a() -> Value {
|
|
|
284
284
|
})
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
fn ffi_u32a() -> Value {
|
|
288
|
+
Value::native(|args: &[Value]| {
|
|
289
|
+
let arr = match args.first() {
|
|
290
|
+
Some(Value::Array(a)) => a.clone(),
|
|
291
|
+
_ => return Value::Null,
|
|
292
|
+
};
|
|
293
|
+
let b = arr.borrow();
|
|
294
|
+
let ta = js_sys::Uint32Array::new_with_length(b.len() as u32);
|
|
295
|
+
for (i, v) in b.iter().enumerate() {
|
|
296
|
+
ta.set_index(i as u32, v.as_number().unwrap_or(0.0) as u32);
|
|
297
|
+
}
|
|
298
|
+
wrap(ta.into())
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
287
302
|
// ---------------------------------------------------------------------------
|
|
288
303
|
// requestAnimationFrame render loop
|
|
289
304
|
// ---------------------------------------------------------------------------
|
|
@@ -362,6 +377,7 @@ fn install_ffi(vm: &mut Vm) {
|
|
|
362
377
|
vm.set_global("f32a".into(), ffi_f32a());
|
|
363
378
|
vm.set_global("u16a".into(), ffi_u16a());
|
|
364
379
|
vm.set_global("u8a".into(), ffi_u8a());
|
|
380
|
+
vm.set_global("u32a".into(), ffi_u32a());
|
|
365
381
|
vm.set_global("request_animation_frame".into(), ffi_request_animation_frame());
|
|
366
382
|
}
|
|
367
383
|
|
|
@@ -39,9 +39,7 @@ fn classify_tish_abi(item: &ItemFn) -> Option<SignatureClass> {
|
|
|
39
39
|
if value_args != 1 || sig.inputs.len() != 1 {
|
|
40
40
|
return None;
|
|
41
41
|
}
|
|
42
|
-
let
|
|
43
|
-
return None;
|
|
44
|
-
};
|
|
42
|
+
let ret_ty = return_type_inner(&sig.output)?;
|
|
45
43
|
if !is_value_type(ret_ty) {
|
|
46
44
|
return None;
|
|
47
45
|
}
|
|
@@ -121,7 +121,7 @@ fn generate_from_resolved(
|
|
|
121
121
|
impl BindgenConfig {
|
|
122
122
|
/// Write using [`generate_from_registry_dependency`].
|
|
123
123
|
pub fn write_files(&self) -> io::Result<()> {
|
|
124
|
-
generate_from_registry_dependency(self).map_err(
|
|
124
|
+
generate_from_registry_dependency(self).map_err(io::Error::other)
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -199,7 +199,7 @@ fn render_generated_lib(
|
|
|
199
199
|
name = rust_fn
|
|
200
200
|
),
|
|
201
201
|
SignatureClass::SerializeRefToResultString => format!(
|
|
202
|
-
"pub fn {name}(args: &[Value]) -> Value {{\n let Some(v) = args.first() else {{ return Value::Null }};\n match _tish_upstream::{name}(&tish_to_json(v)) {{\n Ok(s) => Value::String(
|
|
202
|
+
"pub fn {name}(args: &[Value]) -> Value {{\n let Some(v) = args.first() else {{ return Value::Null }};\n match _tish_upstream::{name}(&tish_to_json(v)) {{\n Ok(s) => Value::String(arcstr::ArcStr::from(s)),\n Err(_) => Value::Null,\n }}\n}}\n\n",
|
|
203
203
|
name = rust_fn
|
|
204
204
|
),
|
|
205
205
|
SignatureClass::DeserializeStrToResult => format!(
|
package/justfile
CHANGED
|
@@ -258,6 +258,14 @@ perf-suite *ARGS:
|
|
|
258
258
|
perf-suite-gen:
|
|
259
259
|
./scripts/generate_perf_ci_main.sh
|
|
260
260
|
|
|
261
|
+
# HTTP throughput: tish vs Node, single vs multi-worker, plaintext + json (needs oha + jq)
|
|
262
|
+
perf-http *ARGS:
|
|
263
|
+
./scripts/run_http_perf.sh {{ARGS}}
|
|
264
|
+
|
|
265
|
+
# Perf gauntlet: compute benchmarks vs Node, incl. known-fail targets to evolve past (needs node)
|
|
266
|
+
perf-gauntlet *ARGS:
|
|
267
|
+
./scripts/run_perf_gauntlet.sh {{ARGS}}
|
|
268
|
+
|
|
261
269
|
# Show binary sizes for different builds
|
|
262
270
|
sizes:
|
|
263
271
|
@echo "Building secure binary..."
|
package/package.json
CHANGED
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|