@tishlang/tish-format 1.0.12 → 2.0.1
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 +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -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 +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- 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/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- 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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//! Native Promise entrypoints for fetch / fetchAll.
|
|
2
|
+
|
|
3
|
+
use tishlang_core::Value;
|
|
4
|
+
|
|
5
|
+
pub fn fetch_promise(args: Vec<Value>) -> Value {
|
|
6
|
+
crate::http_fetch::fetch_promise_from_args(args)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
pub fn fetch_all_promise(args: Vec<Value>) -> Value {
|
|
10
|
+
crate::http_fetch::fetch_all_promise_from_args(args)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn fetch_async_promise(args: Vec<Value>) -> Value {
|
|
14
|
+
fetch_promise(args)
|
|
15
|
+
}
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
//! Promise helpers for the bytecode VM and native codegen (`Promise.resolve`, etc.).
|
|
2
|
+
//!
|
|
3
|
+
//! The global `Promise` value is an **object** with a `__call` entry so the VM can
|
|
4
|
+
//! invoke `Promise(executor)` like `new Promise(executor)` in JS. Static methods live
|
|
5
|
+
//! on the same object (`resolve`, `reject`, `all`, `race`, `any`, `allSettled`, `spawn`).
|
|
6
|
+
//!
|
|
7
|
+
//! ## Concurrency model for race / any / allSettled / spawn
|
|
8
|
+
//!
|
|
9
|
+
//! `TishPromise::block_until_settled` is a *blocking* call. To wait on "whichever of N
|
|
10
|
+
//! settles first" without serializing them, we spawn one OS thread per promise — each
|
|
11
|
+
//! calls `block_until_settled` and forwards the result (with its index) to a shared
|
|
12
|
+
//! `mpsc::channel`. The main thread reads from that channel:
|
|
13
|
+
//! - `race` → first message wins (fulfilled or rejected).
|
|
14
|
+
//! - `any` → first *fulfilled* message wins; collect rejections; if all reject →
|
|
15
|
+
//! `AggregateError` (array of reasons).
|
|
16
|
+
//! - `allSettled` → drain all N messages, sort by index, build `{status,value|reason}`.
|
|
17
|
+
//!
|
|
18
|
+
//! This requires `Value: Send`, which holds under the `send-values` feature (all handles
|
|
19
|
+
//! become `Arc<Mutex<…>>`). The `send-values` feature is enabled in every build that has
|
|
20
|
+
//! `http` (i.e. the shipped `full` binary). Without it (wasm / wasi) we fall back to a
|
|
21
|
+
//! sequential path — correct but not concurrent.
|
|
22
|
+
//!
|
|
23
|
+
//! `Promise.spawn(fn)` runs `fn()` on a fresh OS thread and returns a Promise. This is
|
|
24
|
+
//! the primitive for CPU-bound and GPU-bound work (e.g. `Promise.spawn(() => matmul(…))`
|
|
25
|
+
//! from `tish:mlx` or `tish:metal`). The thread is an ordinary OS thread, not a tokio
|
|
26
|
+
//! task, so it does not contend with the I/O runtime.
|
|
27
|
+
|
|
28
|
+
use std::sync::mpsc;
|
|
29
|
+
use std::sync::{Arc, Mutex};
|
|
30
|
+
|
|
31
|
+
use tishlang_core::{ObjectMap, TishPromise, Value, VmRef};
|
|
32
|
+
|
|
33
|
+
/// Fulfilled or rejected before anyone awaits — `block_until_settled` consumes the result once.
|
|
34
|
+
pub struct ImmediateSettledPromise {
|
|
35
|
+
slot: Mutex<Option<Result<Value, Value>>>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl ImmediateSettledPromise {
|
|
39
|
+
fn new(result: Result<Value, Value>) -> Self {
|
|
40
|
+
Self {
|
|
41
|
+
slot: Mutex::new(Some(result)),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl TishPromise for ImmediateSettledPromise {
|
|
47
|
+
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
48
|
+
self.slot
|
|
49
|
+
.lock()
|
|
50
|
+
.unwrap()
|
|
51
|
+
.take()
|
|
52
|
+
.unwrap_or(Err(Value::String(
|
|
53
|
+
"Promise already settled or consumed".into(),
|
|
54
|
+
)))
|
|
55
|
+
}
|
|
56
|
+
/// Always already settled — return the result immediately without blocking.
|
|
57
|
+
fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
|
|
58
|
+
Some(self.slot.lock().unwrap().take().unwrap_or(
|
|
59
|
+
Err(Value::String("Promise already consumed".into())),
|
|
60
|
+
))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn fulfilled(v: Value) -> Value {
|
|
65
|
+
Value::Promise(Arc::new(ImmediateSettledPromise::new(Ok(v))))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn rejected(v: Value) -> Value {
|
|
69
|
+
Value::Promise(Arc::new(ImmediateSettledPromise::new(Err(v))))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn flatten_chain_out(v: Value) -> std::result::Result<Value, Value> {
|
|
73
|
+
match v {
|
|
74
|
+
Value::Promise(p) => p.block_until_settled(),
|
|
75
|
+
other => Ok(other),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// `Promise(executor)` — executor runs synchronously; `resolve` / `reject` unblock `recv`.
|
|
80
|
+
struct DeferredChannelPromise {
|
|
81
|
+
rx: Mutex<Option<mpsc::Receiver<Result<Value, Value>>>>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl TishPromise for DeferredChannelPromise {
|
|
85
|
+
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
86
|
+
let rx = self.rx.lock().unwrap().take();
|
|
87
|
+
match rx {
|
|
88
|
+
Some(r) => r.recv().unwrap_or(Err(Value::String(
|
|
89
|
+
"Promise executor did not call resolve or reject".into(),
|
|
90
|
+
))),
|
|
91
|
+
None => Err(Value::String(
|
|
92
|
+
"Promise already consumed or settled".into(),
|
|
93
|
+
)),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Non-blocking settle: if the executor has already called resolve/reject (the channel
|
|
98
|
+
/// has a message waiting), return it immediately. Returns `None` if the work is still
|
|
99
|
+
/// pending (channel empty). This lets `race`/`any`/`allSettled` handle already-settled
|
|
100
|
+
/// `new Promise(executor)` promises in input-order without spawning threads.
|
|
101
|
+
fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
|
|
102
|
+
let mut lock = self.rx.lock().unwrap();
|
|
103
|
+
match lock.as_ref() {
|
|
104
|
+
None => Some(Err(Value::String("Promise already consumed".into()))),
|
|
105
|
+
Some(rx) => match rx.try_recv() {
|
|
106
|
+
Ok(r) => {
|
|
107
|
+
*lock = None; // consumed — block_until_settled would error now (correct)
|
|
108
|
+
Some(r)
|
|
109
|
+
}
|
|
110
|
+
Err(mpsc::TryRecvError::Disconnected) => {
|
|
111
|
+
*lock = None;
|
|
112
|
+
Some(Err(Value::String(
|
|
113
|
+
"Promise executor did not call resolve or reject".into(),
|
|
114
|
+
)))
|
|
115
|
+
}
|
|
116
|
+
Err(mpsc::TryRecvError::Empty) => None, // still pending
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// `.then` / `.catch` chain: when awaited, settle the predecessor then optionally invoke a handler.
|
|
123
|
+
pub struct ThenPromise {
|
|
124
|
+
pred: Arc<dyn TishPromise>,
|
|
125
|
+
on_fulfilled: Option<Value>,
|
|
126
|
+
on_rejected: Option<Value>,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl TishPromise for ThenPromise {
|
|
130
|
+
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
131
|
+
match self.pred.block_until_settled() {
|
|
132
|
+
Ok(v) => {
|
|
133
|
+
if let Some(Value::Function(f)) = &self.on_fulfilled {
|
|
134
|
+
flatten_chain_out(f.call(&[v]))
|
|
135
|
+
} else {
|
|
136
|
+
Ok(v)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
Err(e) => {
|
|
140
|
+
if let Some(Value::Function(f)) = &self.on_rejected {
|
|
141
|
+
flatten_chain_out(f.call(&[e]))
|
|
142
|
+
} else {
|
|
143
|
+
Err(e)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// `Promise.resolve(value)` — adopt promises, otherwise wrap in a fulfilled promise.
|
|
151
|
+
pub fn promise_resolve(args: &[Value]) -> Value {
|
|
152
|
+
match args.first() {
|
|
153
|
+
Some(Value::Promise(p)) => Value::Promise(Arc::clone(p)),
|
|
154
|
+
Some(v) => fulfilled(v.clone()),
|
|
155
|
+
None => fulfilled(Value::Null),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// `Promise.reject(reason)` — always a rejected promise.
|
|
160
|
+
pub fn promise_reject(args: &[Value]) -> Value {
|
|
161
|
+
rejected(
|
|
162
|
+
args.first()
|
|
163
|
+
.cloned()
|
|
164
|
+
.unwrap_or(Value::Null),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// `Promise.all(iterable)` — block on each promise in order; non-promises pass through.
|
|
169
|
+
pub fn promise_all(args: &[Value]) -> Value {
|
|
170
|
+
match args.first() {
|
|
171
|
+
Some(Value::Array(arr)) => {
|
|
172
|
+
let mut out: Vec<Value> = Vec::new();
|
|
173
|
+
for v in arr.borrow().iter() {
|
|
174
|
+
let item = if let Value::Promise(p) = v {
|
|
175
|
+
match p.block_until_settled() {
|
|
176
|
+
Ok(x) => x,
|
|
177
|
+
Err(rej) => return rejected(rej),
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
v.clone()
|
|
181
|
+
};
|
|
182
|
+
out.push(item);
|
|
183
|
+
}
|
|
184
|
+
fulfilled(Value::Array(VmRef::new(out)))
|
|
185
|
+
}
|
|
186
|
+
Some(v) => fulfilled(v.clone()),
|
|
187
|
+
None => fulfilled(Value::Null),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Concurrent combinators (race / any / allSettled) + Promise.spawn
|
|
193
|
+
//
|
|
194
|
+
// All three combinators need to wait on multiple promises concurrently. We
|
|
195
|
+
// spawn one OS thread per promise; each thread calls block_until_settled and
|
|
196
|
+
// sends (index, Result) to a shared mpsc channel on the calling thread.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/// Extract the array of items from `Promise.all/race/any/allSettled(array)`.
|
|
200
|
+
fn combinator_items(args: &[Value]) -> Option<Vec<Value>> {
|
|
201
|
+
match args.first() {
|
|
202
|
+
Some(Value::Array(arr)) => Some(arr.borrow().clone()),
|
|
203
|
+
_ => None,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Concurrent settlement channel for `race`/`any`/`allSettled`.
|
|
208
|
+
///
|
|
209
|
+
/// **Two-phase:** already-settled promises (`try_settle` returns `Some`) are handled
|
|
210
|
+
/// inline in input-order before any threads are spawned. This gives deterministic
|
|
211
|
+
/// JS-compatible ordering for already-settled inputs (e.g. `Promise.any([rej, ok, ok])`
|
|
212
|
+
/// reliably returns the first fulfilled, not a random thread-schedule winner). Only
|
|
213
|
+
/// genuinely-pending promises (e.g. from `Promise.spawn`) go to background threads,
|
|
214
|
+
/// which is where concurrency matters.
|
|
215
|
+
///
|
|
216
|
+
/// Returns the receiving end of the channel plus the count of items it will send.
|
|
217
|
+
#[cfg(feature = "send-values")]
|
|
218
|
+
#[allow(clippy::type_complexity)]
|
|
219
|
+
fn race_channel(
|
|
220
|
+
items: Vec<Value>,
|
|
221
|
+
) -> (mpsc::Receiver<(usize, std::result::Result<Value, Value>)>, usize) {
|
|
222
|
+
let (tx, rx) = mpsc::channel::<(usize, std::result::Result<Value, Value>)>();
|
|
223
|
+
let mut count = 0usize;
|
|
224
|
+
for (i, v) in items.into_iter().enumerate() {
|
|
225
|
+
count += 1;
|
|
226
|
+
match v {
|
|
227
|
+
Value::Promise(ref p) => {
|
|
228
|
+
// Phase 1: try non-blocking settle (ImmediateSettledPromise, ThenPromise
|
|
229
|
+
// over immediate, etc.). These never need a thread; handle in order.
|
|
230
|
+
if let Some(r) = p.try_settle() {
|
|
231
|
+
let _ = tx.send((i, r));
|
|
232
|
+
} else {
|
|
233
|
+
// Phase 2: genuinely pending — spawn a thread.
|
|
234
|
+
let p = Arc::clone(p);
|
|
235
|
+
let tx = tx.clone();
|
|
236
|
+
std::thread::spawn(move || {
|
|
237
|
+
let r = p.block_until_settled();
|
|
238
|
+
let _ = tx.send((i, r));
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
other => {
|
|
243
|
+
let _ = tx.send((i, Ok(other)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
drop(tx); // closes the channel when all senders finish
|
|
248
|
+
(rx, count)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// `Promise.race(iterable)` — first to settle (fulfilled or rejected) wins.
|
|
252
|
+
/// Fixed: genuinely concurrent — the old impl only ever blocked on element 0.
|
|
253
|
+
pub fn promise_race(args: &[Value]) -> Value {
|
|
254
|
+
let items = match combinator_items(args) {
|
|
255
|
+
Some(v) => v,
|
|
256
|
+
None => return fulfilled(args.first().cloned().unwrap_or(Value::Null)),
|
|
257
|
+
};
|
|
258
|
+
if items.is_empty() {
|
|
259
|
+
return rejected(Value::String("Promise.race: empty iterable".into()));
|
|
260
|
+
}
|
|
261
|
+
#[cfg(feature = "send-values")]
|
|
262
|
+
{
|
|
263
|
+
let (rx, _) = race_channel(items);
|
|
264
|
+
match rx.recv() {
|
|
265
|
+
Ok((_, Ok(v))) => fulfilled(v),
|
|
266
|
+
Ok((_, Err(e))) => rejected(e),
|
|
267
|
+
Err(_) => rejected(Value::String("Promise.race: all promises dropped".into())),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
#[cfg(not(feature = "send-values"))]
|
|
271
|
+
{
|
|
272
|
+
// Sequential fallback (no threads): first item wins, whether promise or value.
|
|
273
|
+
for item in items {
|
|
274
|
+
return match item {
|
|
275
|
+
Value::Promise(p) => match p.block_until_settled() {
|
|
276
|
+
Ok(v) => fulfilled(v),
|
|
277
|
+
Err(e) => rejected(e),
|
|
278
|
+
},
|
|
279
|
+
other => fulfilled(other),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
rejected(Value::String("Promise.race: empty iterable".into()))
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// `Promise.any(iterable)` — resolves with the **first fulfilled** value.
|
|
287
|
+
/// Rejects with an array of all rejection reasons only if every promise rejects
|
|
288
|
+
/// (matching the JS `AggregateError.errors` convention — we return the array
|
|
289
|
+
/// directly, not wrapped, to keep things simple without a full AggregateError class).
|
|
290
|
+
pub fn promise_any(args: &[Value]) -> Value {
|
|
291
|
+
let items = match combinator_items(args) {
|
|
292
|
+
Some(v) => v,
|
|
293
|
+
None => return fulfilled(args.first().cloned().unwrap_or(Value::Null)),
|
|
294
|
+
};
|
|
295
|
+
if items.is_empty() {
|
|
296
|
+
return rejected(Value::Array(VmRef::new(vec![])));
|
|
297
|
+
}
|
|
298
|
+
let n = items.len();
|
|
299
|
+
#[cfg(feature = "send-values")]
|
|
300
|
+
{
|
|
301
|
+
let (rx, sent) = race_channel(items);
|
|
302
|
+
let mut errors = vec![Value::Null; n];
|
|
303
|
+
let mut reject_count = 0usize;
|
|
304
|
+
// Drain the channel: the first fulfilled result wins immediately; collect
|
|
305
|
+
// all rejections in case every promise rejects.
|
|
306
|
+
let mut received = 0usize;
|
|
307
|
+
while received < sent {
|
|
308
|
+
match rx.recv() {
|
|
309
|
+
Ok((_, Ok(v))) => return fulfilled(v), // first fulfillment wins
|
|
310
|
+
Ok((i, Err(e))) => {
|
|
311
|
+
errors[i] = e;
|
|
312
|
+
reject_count += 1;
|
|
313
|
+
received += 1;
|
|
314
|
+
if reject_count == sent {
|
|
315
|
+
return rejected(Value::Array(VmRef::new(errors)));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
Err(_) => break,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
rejected(Value::Array(VmRef::new(errors)))
|
|
322
|
+
}
|
|
323
|
+
#[cfg(not(feature = "send-values"))]
|
|
324
|
+
{
|
|
325
|
+
// Sequential: return first fulfilled, or array of all rejections.
|
|
326
|
+
let mut errors = Vec::with_capacity(n);
|
|
327
|
+
for item in items {
|
|
328
|
+
match item {
|
|
329
|
+
Value::Promise(p) => match p.block_until_settled() {
|
|
330
|
+
Ok(v) => return fulfilled(v),
|
|
331
|
+
Err(e) => errors.push(e),
|
|
332
|
+
},
|
|
333
|
+
other => return fulfilled(other),
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
rejected(Value::Array(VmRef::new(errors)))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// `Promise.allSettled(iterable)` — always fulfills with an array of outcome objects.
|
|
341
|
+
/// Each entry is `{status:"fulfilled",value:v}` or `{status:"rejected",reason:e}`.
|
|
342
|
+
pub fn promise_all_settled(args: &[Value]) -> Value {
|
|
343
|
+
let items = match combinator_items(args) {
|
|
344
|
+
Some(v) => v,
|
|
345
|
+
None => return fulfilled(Value::Array(VmRef::new(vec![]))),
|
|
346
|
+
};
|
|
347
|
+
let n = items.len();
|
|
348
|
+
if n == 0 {
|
|
349
|
+
return fulfilled(Value::Array(VmRef::new(vec![])));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn make_settled(r: std::result::Result<Value, Value>) -> Value {
|
|
353
|
+
let mut obj = ObjectMap::default();
|
|
354
|
+
match r {
|
|
355
|
+
Ok(v) => {
|
|
356
|
+
obj.insert(Arc::from("status"), Value::String("fulfilled".into()));
|
|
357
|
+
obj.insert(Arc::from("value"), v);
|
|
358
|
+
}
|
|
359
|
+
Err(e) => {
|
|
360
|
+
obj.insert(Arc::from("status"), Value::String("rejected".into()));
|
|
361
|
+
obj.insert(Arc::from("reason"), e);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
Value::object(obj)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[cfg(feature = "send-values")]
|
|
368
|
+
{
|
|
369
|
+
let (rx, _) = race_channel(items);
|
|
370
|
+
let mut results = vec![None::<std::result::Result<Value, Value>>; n];
|
|
371
|
+
while let Ok((i, r)) = rx.recv() {
|
|
372
|
+
results[i] = Some(r);
|
|
373
|
+
}
|
|
374
|
+
let out: Vec<Value> = results
|
|
375
|
+
.into_iter()
|
|
376
|
+
.map(|r| make_settled(r.unwrap_or(Err(Value::String("Promise dropped".into())))))
|
|
377
|
+
.collect();
|
|
378
|
+
fulfilled(Value::Array(VmRef::new(out)))
|
|
379
|
+
}
|
|
380
|
+
#[cfg(not(feature = "send-values"))]
|
|
381
|
+
{
|
|
382
|
+
let out: Vec<Value> = items.into_iter().map(|item| {
|
|
383
|
+
let r = match item {
|
|
384
|
+
Value::Promise(p) => p.block_until_settled(),
|
|
385
|
+
other => Ok(other),
|
|
386
|
+
};
|
|
387
|
+
make_settled(r)
|
|
388
|
+
}).collect();
|
|
389
|
+
fulfilled(Value::Array(VmRef::new(out)))
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// `Promise.spawn(fn)` — run `fn()` on a background OS thread and return a Promise
|
|
394
|
+
/// that resolves with the function's return value. This is the key primitive for
|
|
395
|
+
/// CPU-bound and GPU-bound work:
|
|
396
|
+
///
|
|
397
|
+
/// ```tish
|
|
398
|
+
/// import { matmul } from 'tish:mlx'
|
|
399
|
+
/// let result = await Promise.any([
|
|
400
|
+
/// Promise.spawn(() => matmul(a, b, N)), // MLX GPU path
|
|
401
|
+
/// Promise.spawn(() => fallback(a, b, N)), // CPU fallback
|
|
402
|
+
/// ])
|
|
403
|
+
/// ```
|
|
404
|
+
///
|
|
405
|
+
/// Under `send-values` (the shipped `full` build), the function runs on a real OS
|
|
406
|
+
/// thread; other threads can proceed concurrently. Without `send-values` (wasm/wasi),
|
|
407
|
+
/// the function runs synchronously and the result is wrapped in an immediate promise.
|
|
408
|
+
pub fn promise_spawn(args: &[Value]) -> Value {
|
|
409
|
+
let f = match args.first() {
|
|
410
|
+
Some(Value::Function(f)) => Arc::clone(f),
|
|
411
|
+
_ => return rejected(Value::String("Promise.spawn: expected a function argument".into())),
|
|
412
|
+
};
|
|
413
|
+
#[cfg(feature = "send-values")]
|
|
414
|
+
{
|
|
415
|
+
let (tx, rx) = mpsc::channel::<std::result::Result<Value, Value>>();
|
|
416
|
+
std::thread::spawn(move || {
|
|
417
|
+
// Wrap in catch_unwind so a panicking GPU/CPU kernel rejects the promise
|
|
418
|
+
// rather than aborting the whole process.
|
|
419
|
+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f.call(&[])));
|
|
420
|
+
let _ = tx.send(match result {
|
|
421
|
+
Ok(v) => Ok(v),
|
|
422
|
+
Err(_) => Err(Value::String("Promise.spawn: task panicked".into())),
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
Value::Promise(Arc::new(DeferredChannelPromise {
|
|
426
|
+
rx: Mutex::new(Some(rx)),
|
|
427
|
+
}))
|
|
428
|
+
}
|
|
429
|
+
#[cfg(not(feature = "send-values"))]
|
|
430
|
+
{
|
|
431
|
+
// No threads available (wasm/wasi): run synchronously, wrap result.
|
|
432
|
+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f.call(&[])));
|
|
433
|
+
match result {
|
|
434
|
+
Ok(v) => fulfilled(v),
|
|
435
|
+
Err(_) => rejected(Value::String("Promise.spawn: task panicked".into())),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// Build the global `Promise` object: `__call` (constructor) + static methods.
|
|
441
|
+
pub fn promise_object() -> Value {
|
|
442
|
+
let mut map: ObjectMap = ObjectMap::default();
|
|
443
|
+
|
|
444
|
+
let ctor = Value::native(|args: &[Value]| match args.first() {
|
|
445
|
+
Some(Value::Function(f)) => {
|
|
446
|
+
let (tx, rx) = mpsc::channel();
|
|
447
|
+
let tx_cell = Arc::new(Mutex::new(Some(tx)));
|
|
448
|
+
let resolve = Value::native({
|
|
449
|
+
let tx_cell = Arc::clone(&tx_cell);
|
|
450
|
+
move |a: &[Value]| {
|
|
451
|
+
if let Some(t) = tx_cell.lock().unwrap().take() {
|
|
452
|
+
let _ = t.send(Ok(
|
|
453
|
+
a.first().cloned().unwrap_or(Value::Null),
|
|
454
|
+
));
|
|
455
|
+
}
|
|
456
|
+
Value::Null
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
let reject = Value::native({
|
|
460
|
+
let tx_cell = Arc::clone(&tx_cell);
|
|
461
|
+
move |a: &[Value]| {
|
|
462
|
+
if let Some(t) = tx_cell.lock().unwrap().take() {
|
|
463
|
+
let _ = t.send(Err(
|
|
464
|
+
a.first().cloned().unwrap_or(Value::Null),
|
|
465
|
+
));
|
|
466
|
+
}
|
|
467
|
+
Value::Null
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
let _ = f.call(&[resolve, reject]);
|
|
471
|
+
Value::Promise(Arc::new(DeferredChannelPromise {
|
|
472
|
+
rx: Mutex::new(Some(rx)),
|
|
473
|
+
}))
|
|
474
|
+
}
|
|
475
|
+
_ => Value::Null,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
map.insert(Arc::from("__call"), ctor);
|
|
479
|
+
map.insert(
|
|
480
|
+
Arc::from("resolve"),
|
|
481
|
+
Value::native(|args: &[Value]| promise_resolve(args)),
|
|
482
|
+
);
|
|
483
|
+
map.insert(
|
|
484
|
+
Arc::from("reject"),
|
|
485
|
+
Value::native(|args: &[Value]| promise_reject(args)),
|
|
486
|
+
);
|
|
487
|
+
map.insert(
|
|
488
|
+
Arc::from("all"),
|
|
489
|
+
Value::native(|args: &[Value]| promise_all(args)),
|
|
490
|
+
);
|
|
491
|
+
map.insert(
|
|
492
|
+
Arc::from("race"),
|
|
493
|
+
Value::native(|args: &[Value]| promise_race(args)),
|
|
494
|
+
);
|
|
495
|
+
map.insert(
|
|
496
|
+
Arc::from("any"),
|
|
497
|
+
Value::native(|args: &[Value]| promise_any(args)),
|
|
498
|
+
);
|
|
499
|
+
map.insert(
|
|
500
|
+
Arc::from("allSettled"),
|
|
501
|
+
Value::native(|args: &[Value]| promise_all_settled(args)),
|
|
502
|
+
);
|
|
503
|
+
map.insert(
|
|
504
|
+
Arc::from("spawn"),
|
|
505
|
+
Value::native(|args: &[Value]| promise_spawn(args)),
|
|
506
|
+
);
|
|
507
|
+
Value::object(map)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/// `.then(onFulfilled, onRejected)` for a `Value::Promise` instance (VM `GetMember`).
|
|
511
|
+
pub fn promise_instance_then(p: &Arc<dyn TishPromise>, args: &[Value]) -> Value {
|
|
512
|
+
Value::Promise(Arc::new(ThenPromise {
|
|
513
|
+
pred: Arc::clone(p),
|
|
514
|
+
on_fulfilled: args.first().cloned(),
|
|
515
|
+
on_rejected: args.get(1).cloned(),
|
|
516
|
+
}))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// `.catch(onRejected)` for a `Value::Promise` instance.
|
|
520
|
+
pub fn promise_instance_catch(p: &Arc<dyn TishPromise>, args: &[Value]) -> Value {
|
|
521
|
+
Value::Promise(Arc::new(ThenPromise {
|
|
522
|
+
pred: Arc::clone(p),
|
|
523
|
+
on_fulfilled: None,
|
|
524
|
+
on_rejected: args.first().cloned(),
|
|
525
|
+
}))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/// Unwrap a settled [`Value::Promise`], or pass non-promise values through (VM `AwaitPromise` /
|
|
529
|
+
/// `tish:http.await`). Fetch promises still require the `http` feature.
|
|
530
|
+
pub fn await_promise(v: Value) -> Value {
|
|
531
|
+
if let Value::Promise(p) = v {
|
|
532
|
+
match p.block_until_settled() {
|
|
533
|
+
Ok(val) => val,
|
|
534
|
+
Err(rejection) => rejection,
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
v
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/// Like [`await_promise`], but a REJECTED promise surfaces as a catchable throw rather than
|
|
542
|
+
/// silently yielding the rejection value. `await Promise.reject(x)` must throw `x` (so a
|
|
543
|
+
/// surrounding `try/catch` fires) — matching interp/vm/cranelift/wasi. The codegen emits this
|
|
544
|
+
/// variant (with `?`) wherever an error channel exists (inside a `try` body, or top-level `run()`),
|
|
545
|
+
/// and falls back to [`await_promise`] only where there is no channel (a nested value-fn with no
|
|
546
|
+
/// enclosing try), mirroring how `throw` is lowered.
|
|
547
|
+
pub fn await_promise_throw(v: Value) -> Result<Value, Box<dyn std::error::Error>> {
|
|
548
|
+
if let Value::Promise(p) = v {
|
|
549
|
+
match p.block_until_settled() {
|
|
550
|
+
Ok(val) => Ok(val),
|
|
551
|
+
Err(rejection) => {
|
|
552
|
+
Err(Box::new(crate::TishError::Throw(rejection)) as Box<dyn std::error::Error>)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
Ok(v)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//! Promises carrying only Send payloads (string results for text(), etc.).
|
|
2
|
+
|
|
3
|
+
use std::sync::{Arc, Mutex};
|
|
4
|
+
use tishlang_core::{ObjectMap, TishPromise, Value};
|
|
5
|
+
use tokio::sync::oneshot;
|
|
6
|
+
|
|
7
|
+
fn error_value(msg: String) -> Value {
|
|
8
|
+
let mut obj: ObjectMap = ObjectMap::with_capacity(2);
|
|
9
|
+
obj.insert(Arc::from("error"), Value::String(msg.into()));
|
|
10
|
+
obj.insert(Arc::from("ok"), Value::Bool(false));
|
|
11
|
+
Value::object(obj)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub struct StringResultPromise {
|
|
15
|
+
pub(crate) rx: Mutex<Option<oneshot::Receiver<Result<String, String>>>>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl TishPromise for StringResultPromise {
|
|
19
|
+
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
20
|
+
let rx = self.rx.lock().unwrap().take();
|
|
21
|
+
if let Some(rx) = rx {
|
|
22
|
+
let result = crate::http::block_on_http(rx);
|
|
23
|
+
match result {
|
|
24
|
+
Ok(Ok(s)) => Ok(Value::String(s.into())),
|
|
25
|
+
Ok(Err(e)) => Err(error_value(e)),
|
|
26
|
+
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
Err(Value::String("Promise already consumed".into()))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn string_result_promise(rx: oneshot::Receiver<Result<String, String>>) -> Value {
|
|
35
|
+
Value::Promise(Arc::new(StringResultPromise {
|
|
36
|
+
rx: Mutex::new(Some(rx)),
|
|
37
|
+
}))
|
|
38
|
+
}
|