@tishlang/tish-format 1.0.13 → 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 +2 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +10 -2
- 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/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +61 -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 +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
- package/README.md +0 -138
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! The global `Promise` value is an **object** with a `__call` entry so the VM can
|
|
4
4
|
//! invoke `Promise(executor)` like `new Promise(executor)` in JS. Static methods live
|
|
5
|
-
//! on the same object (`resolve`, `reject`, `all`, `race`).
|
|
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.
|
|
6
27
|
|
|
7
28
|
use std::sync::mpsc;
|
|
8
29
|
use std::sync::{Arc, Mutex};
|
|
@@ -32,6 +53,12 @@ impl TishPromise for ImmediateSettledPromise {
|
|
|
32
53
|
"Promise already settled or consumed".into(),
|
|
33
54
|
)))
|
|
34
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
|
+
}
|
|
35
62
|
}
|
|
36
63
|
|
|
37
64
|
fn fulfilled(v: Value) -> Value {
|
|
@@ -66,6 +93,30 @@ impl TishPromise for DeferredChannelPromise {
|
|
|
66
93
|
)),
|
|
67
94
|
}
|
|
68
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
|
+
}
|
|
69
120
|
}
|
|
70
121
|
|
|
71
122
|
/// `.then` / `.catch` chain: when awaited, settle the predecessor then optionally invoke a handler.
|
|
@@ -80,14 +131,14 @@ impl TishPromise for ThenPromise {
|
|
|
80
131
|
match self.pred.block_until_settled() {
|
|
81
132
|
Ok(v) => {
|
|
82
133
|
if let Some(Value::Function(f)) = &self.on_fulfilled {
|
|
83
|
-
flatten_chain_out(f(&[v]))
|
|
134
|
+
flatten_chain_out(f.call(&[v]))
|
|
84
135
|
} else {
|
|
85
136
|
Ok(v)
|
|
86
137
|
}
|
|
87
138
|
}
|
|
88
139
|
Err(e) => {
|
|
89
140
|
if let Some(Value::Function(f)) = &self.on_rejected {
|
|
90
|
-
flatten_chain_out(f(&[e]))
|
|
141
|
+
flatten_chain_out(f.call(&[e]))
|
|
91
142
|
} else {
|
|
92
143
|
Err(e)
|
|
93
144
|
}
|
|
@@ -137,24 +188,252 @@ pub fn promise_all(args: &[Value]) -> Value {
|
|
|
137
188
|
}
|
|
138
189
|
}
|
|
139
190
|
|
|
140
|
-
|
|
141
|
-
|
|
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>> {
|
|
142
201
|
match args.first() {
|
|
143
|
-
Some(Value::Array(arr)) =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|
|
151
317
|
}
|
|
152
|
-
|
|
318
|
+
Err(_) => break,
|
|
153
319
|
}
|
|
154
|
-
Value::Null
|
|
155
320
|
}
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
}
|
|
158
437
|
}
|
|
159
438
|
}
|
|
160
439
|
|
|
@@ -188,7 +467,7 @@ pub fn promise_object() -> Value {
|
|
|
188
467
|
Value::Null
|
|
189
468
|
}
|
|
190
469
|
});
|
|
191
|
-
let _ = f(&[resolve, reject]);
|
|
470
|
+
let _ = f.call(&[resolve, reject]);
|
|
192
471
|
Value::Promise(Arc::new(DeferredChannelPromise {
|
|
193
472
|
rx: Mutex::new(Some(rx)),
|
|
194
473
|
}))
|
|
@@ -213,6 +492,18 @@ pub fn promise_object() -> Value {
|
|
|
213
492
|
Arc::from("race"),
|
|
214
493
|
Value::native(|args: &[Value]| promise_race(args)),
|
|
215
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
|
+
);
|
|
216
507
|
Value::object(map)
|
|
217
508
|
}
|
|
218
509
|
|
|
@@ -246,3 +537,22 @@ pub fn await_promise(v: Value) -> Value {
|
|
|
246
537
|
v
|
|
247
538
|
}
|
|
248
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
|
+
}
|
|
@@ -56,7 +56,7 @@ fn run_due_timers() {
|
|
|
56
56
|
}
|
|
57
57
|
for (id, callback, args, interval_ms) in due {
|
|
58
58
|
if let Value::Function(f) = &callback {
|
|
59
|
-
let _ = f(&args);
|
|
59
|
+
let _ = f.call(&args);
|
|
60
60
|
}
|
|
61
61
|
if interval_ms > 0 {
|
|
62
62
|
re_register_interval(id, callback, args, interval_ms);
|
|
@@ -69,15 +69,21 @@ fn take_due_timers() -> Vec<(u64, Value, Vec<Value>, u64)> {
|
|
|
69
69
|
let now = Instant::now();
|
|
70
70
|
REGISTRY.with(|r| {
|
|
71
71
|
let mut reg = r.borrow_mut();
|
|
72
|
-
let due: Vec<_> = reg
|
|
72
|
+
let mut due: Vec<_> = reg
|
|
73
73
|
.iter()
|
|
74
74
|
.filter(|(_, e)| e.due <= now)
|
|
75
|
-
.map(|(id, e)| (*id, e.callback.clone(), e.args.clone(), e.interval_ms))
|
|
75
|
+
.map(|(id, e)| (e.due, *id, e.callback.clone(), e.args.clone(), e.interval_ms))
|
|
76
76
|
.collect();
|
|
77
|
-
|
|
77
|
+
// Deterministic JS timer order: earliest `due` first, ties broken by registration order
|
|
78
|
+
// (the monotonic id). REGISTRY is a HashMap whose iteration order is otherwise arbitrary,
|
|
79
|
+
// which scrambled same-delay timers (e.g. three `setTimeout(_, 0)` firing out of order).
|
|
80
|
+
due.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
|
|
81
|
+
for (_, id, _, _, _) in &due {
|
|
78
82
|
reg.remove(id);
|
|
79
83
|
}
|
|
80
|
-
due
|
|
84
|
+
due.into_iter()
|
|
85
|
+
.map(|(_, id, cb, args, iv)| (id, cb, args, iv))
|
|
86
|
+
.collect()
|
|
81
87
|
})
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -100,7 +106,7 @@ fn re_register_interval(id: u64, callback: Value, args: Vec<Value>, interval_ms:
|
|
|
100
106
|
/// Callbacks run when run_due_timers() is invoked (e.g. from ws.receiveTimeout poll loop).
|
|
101
107
|
pub fn set_timeout(args: &[Value]) -> Value {
|
|
102
108
|
let callback = args.first().cloned().unwrap_or(Value::Null);
|
|
103
|
-
let delay_ms = extract_num(args.get(1)).min(
|
|
109
|
+
let delay_ms = extract_num(args.get(1)).min(3_600_000);
|
|
104
110
|
let extra_args: Vec<Value> = args.iter().skip(2).cloned().collect();
|
|
105
111
|
if matches!(callback, Value::Null) {
|
|
106
112
|
return Value::Number(next_id() as f64);
|
|
@@ -124,7 +130,7 @@ pub fn set_timeout(args: &[Value]) -> Value {
|
|
|
124
130
|
/// setInterval(callback, intervalMs, ...args) — first run after `intervalMs`, then repeats.
|
|
125
131
|
pub fn set_interval(args: &[Value]) -> Value {
|
|
126
132
|
let callback = args.first().cloned().unwrap_or(Value::Null);
|
|
127
|
-
let interval_ms = extract_num(args.get(1)).min(
|
|
133
|
+
let interval_ms = extract_num(args.get(1)).min(3_600_000);
|
|
128
134
|
let extra_args: Vec<Value> = args.iter().skip(2).cloned().collect();
|
|
129
135
|
if matches!(callback, Value::Null) {
|
|
130
136
|
return Value::Number(next_id() as f64);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//! Interactive terminal I/O for Tish (issue #101), behind the `tty` feature.
|
|
2
|
+
//!
|
|
3
|
+
//! Exposes raw mode, the alternate screen, terminal size, and key/resize **events** via
|
|
4
|
+
//! `crossterm`, so Tish programs can build interactive TUIs (menus, forms, live keyboard
|
|
5
|
+
//! navigation). Imported as `import { … } from 'tish:tty'`.
|
|
6
|
+
//!
|
|
7
|
+
//! The Value-agnostic core (`size`, `is_tty`, `set_raw_mode`, `read_event`, …) returns plain
|
|
8
|
+
//! Rust data so every backend — the bytecode VM (via the `tty_*` wrappers here) and the
|
|
9
|
+
//! tree-walk interpreter (whose `Value` is a distinct type) — can build its own `Value` from
|
|
10
|
+
//! the same logic. Errors surface as `null`/`false` rather than panicking.
|
|
11
|
+
|
|
12
|
+
use std::io::{IsTerminal, Write};
|
|
13
|
+
use std::sync::Arc;
|
|
14
|
+
use std::time::Duration;
|
|
15
|
+
|
|
16
|
+
use tishlang_core::{ObjectMap, Value};
|
|
17
|
+
|
|
18
|
+
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
|
|
19
|
+
use crossterm::terminal;
|
|
20
|
+
|
|
21
|
+
/// A terminal event delivered by [`read_event`]. Plain data so each backend maps it to its
|
|
22
|
+
/// own `Value` object.
|
|
23
|
+
pub enum TtyEvent {
|
|
24
|
+
Key {
|
|
25
|
+
key: String,
|
|
26
|
+
ctrl: bool,
|
|
27
|
+
alt: bool,
|
|
28
|
+
shift: bool,
|
|
29
|
+
},
|
|
30
|
+
Resize {
|
|
31
|
+
cols: u16,
|
|
32
|
+
rows: u16,
|
|
33
|
+
},
|
|
34
|
+
/// Mouse / focus / paste — reported generically so an input loop can ignore it.
|
|
35
|
+
Other,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Value-agnostic core (shared by every backend) ───────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/// Terminal `(cols, rows)`, or `None` if stdout is not connected to a terminal.
|
|
41
|
+
///
|
|
42
|
+
/// Gated on `stdout().is_terminal()` to match Node (`process.stdout.columns` is
|
|
43
|
+
/// `undefined` when stdout is not a TTY). Without the gate, `crossterm::terminal::size()`
|
|
44
|
+
/// can still succeed via the controlling terminal even when stdout is redirected — so a
|
|
45
|
+
/// piped run (e.g. CI, `tish run x.tish | cat`) would report a bogus size instead of null.
|
|
46
|
+
pub fn size() -> Option<(u16, u16)> {
|
|
47
|
+
if std::io::stdout().is_terminal() {
|
|
48
|
+
terminal::size().ok()
|
|
49
|
+
} else {
|
|
50
|
+
None
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Whether stdin **and** stdout are connected to a terminal.
|
|
55
|
+
pub fn is_tty() -> bool {
|
|
56
|
+
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Enter (cbreak) or leave raw mode. Returns `true` on success.
|
|
60
|
+
pub fn set_raw_mode(enabled: bool) -> bool {
|
|
61
|
+
if enabled {
|
|
62
|
+
terminal::enable_raw_mode().is_ok()
|
|
63
|
+
} else {
|
|
64
|
+
terminal::disable_raw_mode().is_ok()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Switch to / from the alternate screen buffer (full-screen apps).
|
|
69
|
+
pub fn enter_alt_screen() -> bool {
|
|
70
|
+
let mut out = std::io::stdout();
|
|
71
|
+
let ok = crossterm::execute!(out, terminal::EnterAlternateScreen).is_ok();
|
|
72
|
+
let _ = out.flush();
|
|
73
|
+
ok
|
|
74
|
+
}
|
|
75
|
+
pub fn leave_alt_screen() -> bool {
|
|
76
|
+
let mut out = std::io::stdout();
|
|
77
|
+
let ok = crossterm::execute!(out, terminal::LeaveAlternateScreen).is_ok();
|
|
78
|
+
let _ = out.flush();
|
|
79
|
+
ok
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Read the next terminal event. `timeout_ms = None` blocks; `Some(ms)` polls for `ms`
|
|
83
|
+
/// milliseconds (0 = non-blocking) and returns `None` on timeout. Key-release events
|
|
84
|
+
/// (Windows) are skipped, yielding `None`.
|
|
85
|
+
pub fn read_event(timeout_ms: Option<u64>) -> Option<TtyEvent> {
|
|
86
|
+
if let Some(ms) = timeout_ms {
|
|
87
|
+
match event::poll(Duration::from_millis(ms)) {
|
|
88
|
+
Ok(true) => {}
|
|
89
|
+
_ => return None,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
match event::read().ok()? {
|
|
93
|
+
Event::Key(k) => {
|
|
94
|
+
if k.kind == KeyEventKind::Release {
|
|
95
|
+
return None;
|
|
96
|
+
}
|
|
97
|
+
Some(TtyEvent::Key {
|
|
98
|
+
key: key_code_name(k.code),
|
|
99
|
+
ctrl: k.modifiers.contains(KeyModifiers::CONTROL),
|
|
100
|
+
alt: k.modifiers.contains(KeyModifiers::ALT),
|
|
101
|
+
shift: k.modifiers.contains(KeyModifiers::SHIFT),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
Event::Resize(cols, rows) => Some(TtyEvent::Resize { cols, rows }),
|
|
105
|
+
_ => Some(TtyEvent::Other),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Read one line of **cooked** input from stdin (line mode), with the trailing newline
|
|
110
|
+
/// stripped. Returns `None` at EOF. For raw key-by-key input use [`read_event`].
|
|
111
|
+
pub fn read_line() -> Option<String> {
|
|
112
|
+
use std::io::BufRead;
|
|
113
|
+
let mut s = String::new();
|
|
114
|
+
match std::io::stdin().lock().read_line(&mut s) {
|
|
115
|
+
Ok(0) => None,
|
|
116
|
+
Ok(_) => {
|
|
117
|
+
while s.ends_with('\n') || s.ends_with('\r') {
|
|
118
|
+
s.pop();
|
|
119
|
+
}
|
|
120
|
+
Some(s)
|
|
121
|
+
}
|
|
122
|
+
Err(_) => None,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Normalize a crossterm key code to a stable JS-friendly name (`"a"`, `"Enter"`, `"Up"`,
|
|
127
|
+
/// `"Esc"`, `"F1"`, …).
|
|
128
|
+
fn key_code_name(code: KeyCode) -> String {
|
|
129
|
+
match code {
|
|
130
|
+
KeyCode::Char(c) => c.to_string(),
|
|
131
|
+
KeyCode::Enter => "Enter".into(),
|
|
132
|
+
KeyCode::Esc => "Esc".into(),
|
|
133
|
+
KeyCode::Backspace => "Backspace".into(),
|
|
134
|
+
KeyCode::Tab => "Tab".into(),
|
|
135
|
+
KeyCode::BackTab => "BackTab".into(),
|
|
136
|
+
KeyCode::Delete => "Delete".into(),
|
|
137
|
+
KeyCode::Insert => "Insert".into(),
|
|
138
|
+
KeyCode::Home => "Home".into(),
|
|
139
|
+
KeyCode::End => "End".into(),
|
|
140
|
+
KeyCode::PageUp => "PageUp".into(),
|
|
141
|
+
KeyCode::PageDown => "PageDown".into(),
|
|
142
|
+
KeyCode::Up => "Up".into(),
|
|
143
|
+
KeyCode::Down => "Down".into(),
|
|
144
|
+
KeyCode::Left => "Left".into(),
|
|
145
|
+
KeyCode::Right => "Right".into(),
|
|
146
|
+
KeyCode::F(n) => format!("F{n}"),
|
|
147
|
+
KeyCode::Null => "Null".into(),
|
|
148
|
+
other => format!("{other:?}"),
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── core::Value wrappers for the bytecode VM / native runtime ────────────────────────────
|
|
153
|
+
|
|
154
|
+
fn obj(pairs: Vec<(&str, Value)>) -> Value {
|
|
155
|
+
let mut m = ObjectMap::default();
|
|
156
|
+
for (k, v) in pairs {
|
|
157
|
+
m.insert(Arc::from(k), v);
|
|
158
|
+
}
|
|
159
|
+
Value::object(m)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// `size()` → `{ cols, rows }` or `null`.
|
|
163
|
+
pub fn tty_size(_args: &[Value]) -> Value {
|
|
164
|
+
match size() {
|
|
165
|
+
Some((cols, rows)) => obj(vec![
|
|
166
|
+
("cols", Value::Number(cols as f64)),
|
|
167
|
+
("rows", Value::Number(rows as f64)),
|
|
168
|
+
]),
|
|
169
|
+
None => Value::Null,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// `isTTY()` → bool.
|
|
174
|
+
pub fn tty_is_tty(_args: &[Value]) -> Value {
|
|
175
|
+
Value::Bool(is_tty())
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// `setRawMode(enabled)` → bool (success).
|
|
179
|
+
pub fn tty_set_raw_mode(args: &[Value]) -> Value {
|
|
180
|
+
Value::Bool(set_raw_mode(args.first().map(|v| v.is_truthy()).unwrap_or(false)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// `enterAltScreen()` / `leaveAltScreen()` → bool.
|
|
184
|
+
pub fn tty_enter_alt_screen(_args: &[Value]) -> Value {
|
|
185
|
+
Value::Bool(enter_alt_screen())
|
|
186
|
+
}
|
|
187
|
+
pub fn tty_leave_alt_screen(_args: &[Value]) -> Value {
|
|
188
|
+
Value::Bool(leave_alt_screen())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// `readLine()` → one line of cooked stdin (no trailing newline), or `null` at EOF.
|
|
192
|
+
pub fn tty_read_line(_args: &[Value]) -> Value {
|
|
193
|
+
match read_line() {
|
|
194
|
+
Some(s) => Value::String(s.into()),
|
|
195
|
+
None => Value::Null,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// `read(timeoutMs?)` → an event object (`{ type, … }`) or `null`.
|
|
200
|
+
pub fn tty_read(args: &[Value]) -> Value {
|
|
201
|
+
let timeout = match args.first() {
|
|
202
|
+
Some(Value::Number(ms)) => Some(ms.max(0.0) as u64),
|
|
203
|
+
_ => None,
|
|
204
|
+
};
|
|
205
|
+
match read_event(timeout) {
|
|
206
|
+
Some(TtyEvent::Key {
|
|
207
|
+
key,
|
|
208
|
+
ctrl,
|
|
209
|
+
alt,
|
|
210
|
+
shift,
|
|
211
|
+
}) => obj(vec![
|
|
212
|
+
("type", Value::String("key".into())),
|
|
213
|
+
("key", Value::String(key.into())),
|
|
214
|
+
("ctrl", Value::Bool(ctrl)),
|
|
215
|
+
("alt", Value::Bool(alt)),
|
|
216
|
+
("shift", Value::Bool(shift)),
|
|
217
|
+
]),
|
|
218
|
+
Some(TtyEvent::Resize { cols, rows }) => obj(vec![
|
|
219
|
+
("type", Value::String("resize".into())),
|
|
220
|
+
("cols", Value::Number(cols as f64)),
|
|
221
|
+
("rows", Value::Number(rows as f64)),
|
|
222
|
+
]),
|
|
223
|
+
Some(TtyEvent::Other) => obj(vec![("type", Value::String("other".into()))]),
|
|
224
|
+
None => Value::Null,
|
|
225
|
+
}
|
|
226
|
+
}
|