@tishlang/tish-format 3.10.9 → 3.10.11
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-format +0 -0
- package/crates/tish/src/cargo_native_registry.rs +4 -0
- package/crates/tish/tests/integration_test.rs +3 -0
- package/crates/tish_bytecode/src/compiler.rs +43 -19
- package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
- package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
- package/crates/tish_core/src/value.rs +31 -4
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_pg/src/lib.rs +59 -8
- package/crates/tish_pg/src/statement_registry.rs +250 -0
- package/crates/tish_runtime/src/http.rs +19 -0
- package/crates/tish_runtime/src/http_fetch.rs +159 -65
- package/crates/tish_runtime/src/promise.rs +521 -90
- package/crates/tish_runtime/src/promise_io.rs +29 -6
- package/crates/tish_vm/src/vm.rs +5 -4
- package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
- package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
- package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
- package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -0
- package/package.json +1 -1
- 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
|
@@ -6,15 +6,23 @@
|
|
|
6
6
|
//!
|
|
7
7
|
//! ## Concurrency model for race / any / allSettled / spawn
|
|
8
8
|
//!
|
|
9
|
-
//!
|
|
10
|
-
//!
|
|
11
|
-
//!
|
|
12
|
-
//! `mpsc::channel`. The
|
|
9
|
+
//! To wait on "whichever of N settles first" without serializing them, each combinator
|
|
10
|
+
//! SUBSCRIBES to its pending inputs ([`TishPromise::subscribe`]): the input promise
|
|
11
|
+
//! stores a completion callback and invokes it from whichever thread settles it, with
|
|
12
|
+
//! the result forwarded (with its index) to a shared `mpsc::channel`. The calling
|
|
13
|
+
//! thread's `recv` on that channel is the only blocking wait — no OS thread is parked
|
|
14
|
+
//! per pending input (issue #702: thread-per-input leaked one uncancellable 2 MiB-stack
|
|
15
|
+
//! thread per never-settling loser until `pthread_create` failed). A loser's callback
|
|
16
|
+
//! fires into a disconnected channel whenever it eventually settles (a no-op), or is
|
|
17
|
+
//! dropped with the promise — no cancellation path is needed. The channel reader:
|
|
13
18
|
//! - `race` → first message wins (fulfilled or rejected).
|
|
14
19
|
//! - `any` → first *fulfilled* message wins; collect rejections; if all reject →
|
|
15
20
|
//! `AggregateError` (array of reasons).
|
|
16
21
|
//! - `allSettled` → drain all N messages, sort by index, build `{status,value|reason}`.
|
|
17
22
|
//!
|
|
23
|
+
//! Settlement itself is memoized in a [`SettleCell`] (peek, not take): awaiting or
|
|
24
|
+
//! racing a promise never consumes it, so an input that lost a race stays awaitable.
|
|
25
|
+
//!
|
|
18
26
|
//! This requires `Value: Send`, which holds under the `send-values` feature (all handles
|
|
19
27
|
//! become `Arc<Mutex<…>>`). The `send-values` feature is enabled in every build that has
|
|
20
28
|
//! `http` (i.e. the shipped `full` binary). Without it (wasm / wasi) we fall back to a
|
|
@@ -26,38 +34,136 @@
|
|
|
26
34
|
//! task, so it does not contend with the I/O runtime.
|
|
27
35
|
|
|
28
36
|
use std::sync::mpsc;
|
|
29
|
-
use std::sync::{Arc, Mutex};
|
|
37
|
+
use std::sync::{Arc, Condvar, Mutex};
|
|
30
38
|
|
|
31
39
|
use tishlang_core::{ObjectMap, TishPromise, Value, VmRef};
|
|
32
40
|
|
|
33
|
-
///
|
|
41
|
+
/// Completion callback registered on a pending promise; invoked exactly once on settlement.
|
|
42
|
+
type SettleCallback = Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>;
|
|
43
|
+
|
|
44
|
+
/// Shared, memoizing settlement state for deferred promises (`Promise(executor)`,
|
|
45
|
+
/// `Promise.spawn`). Replaces the one-shot `mpsc` channel that made awaiting CONSUME
|
|
46
|
+
/// the promise and forced `race`/`any` to park one blocking OS thread per pending
|
|
47
|
+
/// input (issue #702):
|
|
48
|
+
/// - the result is stored, not taken — `wait`/`peek` clone it, so a promise can be
|
|
49
|
+
/// awaited any number of times, including after losing a race;
|
|
50
|
+
/// - pending waiters either block on the condvar (plain `await`) or register a
|
|
51
|
+
/// [`SettleCallback`] (`race`/`any`/`allSettled` subscriptions) that the settling
|
|
52
|
+
/// thread invokes — zero threads parked while pending.
|
|
53
|
+
struct SettleCell {
|
|
54
|
+
state: Mutex<CellState>,
|
|
55
|
+
settled: Condvar,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
enum CellState {
|
|
59
|
+
Pending(Vec<SettleCallback>),
|
|
60
|
+
Settled(std::result::Result<Value, Value>),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl SettleCell {
|
|
64
|
+
fn new() -> Self {
|
|
65
|
+
Self {
|
|
66
|
+
state: Mutex::new(CellState::Pending(Vec::new())),
|
|
67
|
+
settled: Condvar::new(),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// First settle wins; later calls are no-ops (JS: resolve/reject fire once).
|
|
72
|
+
/// Registered callbacks run on the settling thread, after the lock is released.
|
|
73
|
+
fn settle(&self, r: std::result::Result<Value, Value>) {
|
|
74
|
+
let callbacks = {
|
|
75
|
+
let mut st = self.state.lock().unwrap();
|
|
76
|
+
match &mut *st {
|
|
77
|
+
CellState::Pending(subs) => {
|
|
78
|
+
let subs = std::mem::take(subs);
|
|
79
|
+
*st = CellState::Settled(r.clone());
|
|
80
|
+
self.settled.notify_all();
|
|
81
|
+
subs
|
|
82
|
+
}
|
|
83
|
+
CellState::Settled(_) => return,
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
for cb in callbacks {
|
|
87
|
+
cb(r.clone());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Block until settled. Memoized: peeks the stored result, never consumes it.
|
|
92
|
+
fn wait(&self) -> std::result::Result<Value, Value> {
|
|
93
|
+
let mut st = self.state.lock().unwrap();
|
|
94
|
+
loop {
|
|
95
|
+
match &*st {
|
|
96
|
+
CellState::Settled(r) => return r.clone(),
|
|
97
|
+
CellState::Pending(_) => st = self.settled.wait(st).unwrap(),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Non-blocking peek: `Some(result)` once settled, `None` while pending.
|
|
103
|
+
fn peek(&self) -> Option<std::result::Result<Value, Value>> {
|
|
104
|
+
match &*self.state.lock().unwrap() {
|
|
105
|
+
CellState::Settled(r) => Some(r.clone()),
|
|
106
|
+
CellState::Pending(_) => None,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Register `cb` to run on settlement; runs it inline if already settled.
|
|
111
|
+
fn subscribe(&self, cb: SettleCallback) {
|
|
112
|
+
let mut st = self.state.lock().unwrap();
|
|
113
|
+
match &mut *st {
|
|
114
|
+
CellState::Pending(subs) => subs.push(cb),
|
|
115
|
+
CellState::Settled(r) => {
|
|
116
|
+
let r = r.clone();
|
|
117
|
+
drop(st);
|
|
118
|
+
cb(r);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Held by the `resolve`/`reject` natives handed to a `Promise(executor)`. If the
|
|
125
|
+
/// executor drops both without calling either (and without stashing them anywhere),
|
|
126
|
+
/// the cell would stay pending forever and every `await` would hang; settling with
|
|
127
|
+
/// the canonical rejection on last-handle drop preserves the historical
|
|
128
|
+
/// "executor did not call resolve or reject" error instead. `settle` is a no-op if
|
|
129
|
+
/// resolve/reject already fired.
|
|
130
|
+
struct SettlerGuard {
|
|
131
|
+
cell: Arc<SettleCell>,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl Drop for SettlerGuard {
|
|
135
|
+
fn drop(&mut self) {
|
|
136
|
+
self.cell.settle(Err(Value::String(
|
|
137
|
+
"Promise executor did not call resolve or reject".into(),
|
|
138
|
+
)));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Fulfilled or rejected at construction — every read peeks the memoized result.
|
|
34
143
|
pub struct ImmediateSettledPromise {
|
|
35
|
-
|
|
144
|
+
result: Mutex<Result<Value, Value>>,
|
|
36
145
|
}
|
|
37
146
|
|
|
38
147
|
impl ImmediateSettledPromise {
|
|
39
148
|
fn new(result: Result<Value, Value>) -> Self {
|
|
40
149
|
Self {
|
|
41
|
-
|
|
150
|
+
result: Mutex::new(result),
|
|
42
151
|
}
|
|
43
152
|
}
|
|
44
153
|
}
|
|
45
154
|
|
|
46
155
|
impl TishPromise for ImmediateSettledPromise {
|
|
47
156
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
48
|
-
self.
|
|
49
|
-
.lock()
|
|
50
|
-
.unwrap()
|
|
51
|
-
.take()
|
|
52
|
-
.unwrap_or(Err(Value::String(
|
|
53
|
-
"Promise already settled or consumed".into(),
|
|
54
|
-
)))
|
|
157
|
+
self.result.lock().unwrap().clone()
|
|
55
158
|
}
|
|
56
159
|
/// Always already settled — return the result immediately without blocking.
|
|
57
160
|
fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
|
|
58
|
-
Some(self.
|
|
59
|
-
|
|
60
|
-
|
|
161
|
+
Some(self.result.lock().unwrap().clone())
|
|
162
|
+
}
|
|
163
|
+
#[cfg(feature = "send-values")]
|
|
164
|
+
fn subscribe(self: Arc<Self>, on_settled: SettleCallback) {
|
|
165
|
+
let r = self.result.lock().unwrap().clone();
|
|
166
|
+
on_settled(r);
|
|
61
167
|
}
|
|
62
168
|
}
|
|
63
169
|
|
|
@@ -76,46 +182,31 @@ fn flatten_chain_out(v: Value) -> std::result::Result<Value, Value> {
|
|
|
76
182
|
}
|
|
77
183
|
}
|
|
78
184
|
|
|
79
|
-
/// `Promise(executor)`
|
|
80
|
-
|
|
81
|
-
|
|
185
|
+
/// `Promise(executor)` / `Promise.spawn` — a pending promise settled exactly once
|
|
186
|
+
/// through its shared [`SettleCell`]. Never consumed: awaiting, racing, and peeking
|
|
187
|
+
/// all read the memoized settlement.
|
|
188
|
+
struct DeferredCellPromise {
|
|
189
|
+
cell: Arc<SettleCell>,
|
|
82
190
|
}
|
|
83
191
|
|
|
84
|
-
impl TishPromise for
|
|
192
|
+
impl TishPromise for DeferredCellPromise {
|
|
85
193
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
86
|
-
|
|
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
|
-
}
|
|
194
|
+
self.cell.wait()
|
|
95
195
|
}
|
|
96
196
|
|
|
97
|
-
/// Non-blocking
|
|
98
|
-
///
|
|
99
|
-
///
|
|
100
|
-
///
|
|
197
|
+
/// Non-blocking peek: `Some(result)` once resolve/reject fired (or the executor
|
|
198
|
+
/// provably never will), `None` while genuinely pending. Lets `race`/`any`/
|
|
199
|
+
/// `allSettled` handle already-settled `new Promise(executor)` promises in
|
|
200
|
+
/// input-order without subscriptions.
|
|
101
201
|
fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}
|
|
202
|
+
self.cell.peek()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// The no-thread path for `race`/`any`/`allSettled` (issue #702): the callback is
|
|
206
|
+
/// stored in the cell and invoked by whichever thread settles the promise.
|
|
207
|
+
#[cfg(feature = "send-values")]
|
|
208
|
+
fn subscribe(self: Arc<Self>, on_settled: SettleCallback) {
|
|
209
|
+
self.cell.subscribe(on_settled);
|
|
119
210
|
}
|
|
120
211
|
}
|
|
121
212
|
|
|
@@ -126,9 +217,14 @@ pub struct ThenPromise {
|
|
|
126
217
|
on_rejected: Option<Value>,
|
|
127
218
|
}
|
|
128
219
|
|
|
129
|
-
impl
|
|
130
|
-
|
|
131
|
-
|
|
220
|
+
impl ThenPromise {
|
|
221
|
+
/// Apply the stored handlers to the predecessor's settlement (shared by the
|
|
222
|
+
/// blocking await path and the subscription path).
|
|
223
|
+
fn apply(
|
|
224
|
+
&self,
|
|
225
|
+
pred_result: std::result::Result<Value, Value>,
|
|
226
|
+
) -> std::result::Result<Value, Value> {
|
|
227
|
+
match pred_result {
|
|
132
228
|
Ok(v) => {
|
|
133
229
|
if let Some(Value::Function(f)) = &self.on_fulfilled {
|
|
134
230
|
flatten_chain_out(f.call(&[v]))
|
|
@@ -147,6 +243,29 @@ impl TishPromise for ThenPromise {
|
|
|
147
243
|
}
|
|
148
244
|
}
|
|
149
245
|
|
|
246
|
+
impl TishPromise for ThenPromise {
|
|
247
|
+
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
248
|
+
let r = self.pred.block_until_settled();
|
|
249
|
+
self.apply(r)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// NOTE: deliberately no `try_settle` — handlers are user code that can block
|
|
253
|
+
// (a handler returning a promise is settled inline by `flatten_chain_out`), and
|
|
254
|
+
// `race_channel`'s phase-1 walk must never block on one input.
|
|
255
|
+
|
|
256
|
+
/// Subscribe to the predecessor instead of parking a waiter thread (issue #702).
|
|
257
|
+
/// The handlers run on a TRANSIENT thread spawned only once the predecessor has
|
|
258
|
+
/// settled: nothing is parked while pending, and the settling thread is never
|
|
259
|
+
/// blocked by user handler code.
|
|
260
|
+
#[cfg(feature = "send-values")]
|
|
261
|
+
fn subscribe(self: Arc<Self>, on_settled: SettleCallback) {
|
|
262
|
+
let this = Arc::clone(&self);
|
|
263
|
+
Arc::clone(&self.pred).subscribe(Box::new(move |r| {
|
|
264
|
+
std::thread::spawn(move || on_settled(this.apply(r)));
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
150
269
|
/// `Promise.resolve(value)` — adopt promises, otherwise wrap in a fulfilled promise.
|
|
151
270
|
pub fn promise_resolve(args: &[Value]) -> Value {
|
|
152
271
|
match args.first() {
|
|
@@ -191,9 +310,12 @@ pub fn promise_all(args: &[Value]) -> Value {
|
|
|
191
310
|
// ---------------------------------------------------------------------------
|
|
192
311
|
// Concurrent combinators (race / any / allSettled) + Promise.spawn
|
|
193
312
|
//
|
|
194
|
-
// All three combinators need to wait on multiple promises concurrently.
|
|
195
|
-
//
|
|
196
|
-
//
|
|
313
|
+
// All three combinators need to wait on multiple promises concurrently. Each
|
|
314
|
+
// pending input gets a SUBSCRIPTION (TishPromise::subscribe) that sends
|
|
315
|
+
// (index, Result) to a shared mpsc channel when the input settles; the calling
|
|
316
|
+
// thread reads from that channel. No OS thread is parked per pending input
|
|
317
|
+
// (issue #702) — losers' callbacks fire into a disconnected channel (no-op)
|
|
318
|
+
// or are dropped with their promise.
|
|
197
319
|
// ---------------------------------------------------------------------------
|
|
198
320
|
|
|
199
321
|
/// Extract the array of items from `Promise.all/race/any/allSettled(array)`.
|
|
@@ -207,11 +329,12 @@ fn combinator_items(args: &[Value]) -> Option<Vec<Value>> {
|
|
|
207
329
|
/// Concurrent settlement channel for `race`/`any`/`allSettled`.
|
|
208
330
|
///
|
|
209
331
|
/// **Two-phase:** already-settled promises (`try_settle` returns `Some`) are handled
|
|
210
|
-
/// inline in input-order before
|
|
332
|
+
/// inline in input-order before anything is subscribed. This gives deterministic
|
|
211
333
|
/// JS-compatible ordering for already-settled inputs (e.g. `Promise.any([rej, ok, ok])`
|
|
212
334
|
/// reliably returns the first fulfilled, not a random thread-schedule winner). Only
|
|
213
|
-
/// genuinely-pending promises (e.g. from `Promise.spawn`)
|
|
214
|
-
///
|
|
335
|
+
/// genuinely-pending promises (e.g. from `Promise.spawn`) get subscriptions — their
|
|
336
|
+
/// settling thread forwards the result; NO waiter thread is parked per input (#702).
|
|
337
|
+
/// Inputs are peeked, not consumed: they all remain awaitable after the combinator.
|
|
215
338
|
///
|
|
216
339
|
/// Returns the receiving end of the channel plus the count of items it will send.
|
|
217
340
|
#[cfg(feature = "send-values")]
|
|
@@ -225,18 +348,18 @@ fn race_channel(
|
|
|
225
348
|
count += 1;
|
|
226
349
|
match v {
|
|
227
350
|
Value::Promise(ref p) => {
|
|
228
|
-
// Phase 1: try non-blocking
|
|
229
|
-
//
|
|
351
|
+
// Phase 1: try non-blocking peek (ImmediateSettledPromise, settled
|
|
352
|
+
// DeferredCellPromise, etc.). These never need a subscription; handle in order.
|
|
230
353
|
if let Some(r) = p.try_settle() {
|
|
231
354
|
let _ = tx.send((i, r));
|
|
232
355
|
} else {
|
|
233
|
-
// Phase 2: genuinely pending —
|
|
234
|
-
|
|
356
|
+
// Phase 2: genuinely pending — subscribe. The settling thread sends;
|
|
357
|
+
// if this combinator has already returned, the send is a harmless
|
|
358
|
+
// no-op on a disconnected channel and the callback is freed.
|
|
235
359
|
let tx = tx.clone();
|
|
236
|
-
|
|
237
|
-
let r = p.block_until_settled();
|
|
360
|
+
Arc::clone(p).subscribe(Box::new(move |r| {
|
|
238
361
|
let _ = tx.send((i, r));
|
|
239
|
-
});
|
|
362
|
+
}));
|
|
240
363
|
}
|
|
241
364
|
}
|
|
242
365
|
other => {
|
|
@@ -412,19 +535,18 @@ pub fn promise_spawn(args: &[Value]) -> Value {
|
|
|
412
535
|
};
|
|
413
536
|
#[cfg(feature = "send-values")]
|
|
414
537
|
{
|
|
415
|
-
let
|
|
538
|
+
let cell = Arc::new(SettleCell::new());
|
|
539
|
+
let worker_cell = Arc::clone(&cell);
|
|
416
540
|
std::thread::spawn(move || {
|
|
417
541
|
// Wrap in catch_unwind so a panicking GPU/CPU kernel rejects the promise
|
|
418
542
|
// rather than aborting the whole process.
|
|
419
543
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f.call(&[])));
|
|
420
|
-
|
|
544
|
+
worker_cell.settle(match result {
|
|
421
545
|
Ok(v) => Ok(v),
|
|
422
546
|
Err(_) => Err(Value::String("Promise.spawn: task panicked".into())),
|
|
423
547
|
});
|
|
424
548
|
});
|
|
425
|
-
Value::Promise(Arc::new(
|
|
426
|
-
rx: Mutex::new(Some(rx)),
|
|
427
|
-
}))
|
|
549
|
+
Value::Promise(Arc::new(DeferredCellPromise { cell }))
|
|
428
550
|
}
|
|
429
551
|
#[cfg(not(feature = "send-values"))]
|
|
430
552
|
{
|
|
@@ -443,34 +565,36 @@ pub fn promise_object() -> Value {
|
|
|
443
565
|
|
|
444
566
|
let ctor = Value::native(|args: &[Value]| match args.first() {
|
|
445
567
|
Some(Value::Function(f)) => {
|
|
446
|
-
let
|
|
447
|
-
|
|
568
|
+
let cell = Arc::new(SettleCell::new());
|
|
569
|
+
// Both natives share one guard; when the executor drops the last handle
|
|
570
|
+
// without settling, the guard rejects with the canonical error (see
|
|
571
|
+
// `SettlerGuard`). First settle wins — later resolve/reject calls no-op.
|
|
572
|
+
let guard = Arc::new(SettlerGuard {
|
|
573
|
+
cell: Arc::clone(&cell),
|
|
574
|
+
});
|
|
448
575
|
let resolve = Value::native({
|
|
449
|
-
let
|
|
576
|
+
let guard = Arc::clone(&guard);
|
|
450
577
|
move |a: &[Value]| {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
));
|
|
455
|
-
}
|
|
578
|
+
guard
|
|
579
|
+
.cell
|
|
580
|
+
.settle(Ok(a.first().cloned().unwrap_or(Value::Null)));
|
|
456
581
|
Value::Null
|
|
457
582
|
}
|
|
458
583
|
});
|
|
459
584
|
let reject = Value::native({
|
|
460
|
-
let
|
|
585
|
+
let guard = Arc::clone(&guard);
|
|
461
586
|
move |a: &[Value]| {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
));
|
|
466
|
-
}
|
|
587
|
+
guard
|
|
588
|
+
.cell
|
|
589
|
+
.settle(Err(a.first().cloned().unwrap_or(Value::Null)));
|
|
467
590
|
Value::Null
|
|
468
591
|
}
|
|
469
592
|
});
|
|
593
|
+
// The ctor's own guard handle must die before the executor runs, so the
|
|
594
|
+
// resolve/reject natives hold the only references.
|
|
595
|
+
drop(guard);
|
|
470
596
|
let _ = f.call(&[resolve, reject]);
|
|
471
|
-
Value::Promise(Arc::new(
|
|
472
|
-
rx: Mutex::new(Some(rx)),
|
|
473
|
-
}))
|
|
597
|
+
Value::Promise(Arc::new(DeferredCellPromise { cell }))
|
|
474
598
|
}
|
|
475
599
|
_ => Value::Null,
|
|
476
600
|
});
|
|
@@ -556,3 +680,310 @@ pub fn await_promise_throw(v: Value) -> Result<Value, Box<dyn std::error::Error>
|
|
|
556
680
|
Ok(v)
|
|
557
681
|
}
|
|
558
682
|
}
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// Tests — issue #702: Promise.race / Promise.any must not park one OS thread
|
|
686
|
+
// per pending input, and combinator inputs must remain awaitable afterwards.
|
|
687
|
+
// Everything goes through the public surface (`promise_object`, `promise_race`,
|
|
688
|
+
// `promise_any`, `promise_all_settled`) so the tests are independent of the
|
|
689
|
+
// promise internals they guard.
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
#[cfg(test)]
|
|
692
|
+
mod tests {
|
|
693
|
+
use super::*;
|
|
694
|
+
use tishlang_core::object_get;
|
|
695
|
+
|
|
696
|
+
/// Build `new Promise(executor)` through the public `Promise.__call` ctor, with an
|
|
697
|
+
/// executor that stashes `resolve` for later (or never) — the genuinely-pending
|
|
698
|
+
/// shape from issue #702 (event-registry / never-settling loser).
|
|
699
|
+
/// Returns `(promise, resolve)`.
|
|
700
|
+
fn deferred() -> (Value, Value) {
|
|
701
|
+
let stash: Arc<Mutex<Vec<Value>>> = Arc::new(Mutex::new(Vec::new()));
|
|
702
|
+
let executor = Value::native({
|
|
703
|
+
let stash = Arc::clone(&stash);
|
|
704
|
+
move |args: &[Value]| {
|
|
705
|
+
stash
|
|
706
|
+
.lock()
|
|
707
|
+
.unwrap()
|
|
708
|
+
.push(args.first().cloned().unwrap_or(Value::Null));
|
|
709
|
+
Value::Null
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
let promise_global = promise_object();
|
|
713
|
+
let ctor = object_get(&promise_global, &Value::String("__call".into()))
|
|
714
|
+
.expect("Promise global has __call");
|
|
715
|
+
let p = match &ctor {
|
|
716
|
+
Value::Function(f) => f.call(std::slice::from_ref(&executor)),
|
|
717
|
+
_ => panic!("Promise.__call is not callable"),
|
|
718
|
+
};
|
|
719
|
+
let resolve = stash
|
|
720
|
+
.lock()
|
|
721
|
+
.unwrap()
|
|
722
|
+
.first()
|
|
723
|
+
.cloned()
|
|
724
|
+
.expect("executor ran synchronously");
|
|
725
|
+
(p, resolve)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
fn call_value(f: &Value, args: &[Value]) {
|
|
729
|
+
match f {
|
|
730
|
+
Value::Function(f) => {
|
|
731
|
+
f.call(args);
|
|
732
|
+
}
|
|
733
|
+
_ => panic!("expected a callable Value"),
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/// Await a `Value::Promise` (panics on non-promise).
|
|
738
|
+
fn settle(v: &Value) -> std::result::Result<Value, Value> {
|
|
739
|
+
match v {
|
|
740
|
+
Value::Promise(p) => p.block_until_settled(),
|
|
741
|
+
other => panic!("expected a promise, got {}", other.type_name()),
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
fn expect_num(r: std::result::Result<Value, Value>) -> f64 {
|
|
746
|
+
match r {
|
|
747
|
+
Ok(Value::Number(n)) => n,
|
|
748
|
+
other => panic!("expected fulfilled number, got {other:?}"),
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/// Live OS threads in this process (macOS via `ps -M`, Linux via /proc).
|
|
753
|
+
fn os_thread_count() -> usize {
|
|
754
|
+
#[cfg(target_os = "linux")]
|
|
755
|
+
{
|
|
756
|
+
std::fs::read_dir("/proc/self/task").unwrap().count()
|
|
757
|
+
}
|
|
758
|
+
#[cfg(target_os = "macos")]
|
|
759
|
+
{
|
|
760
|
+
let pid = std::process::id().to_string();
|
|
761
|
+
let out = std::process::Command::new("ps")
|
|
762
|
+
.args(["-M", &pid])
|
|
763
|
+
.output()
|
|
764
|
+
.expect("run ps -M");
|
|
765
|
+
String::from_utf8_lossy(&out.stdout)
|
|
766
|
+
.lines()
|
|
767
|
+
.count()
|
|
768
|
+
.saturating_sub(1) // header line
|
|
769
|
+
}
|
|
770
|
+
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
771
|
+
{
|
|
772
|
+
0
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/// Issue #702 regression: N races against never-settling losers must NOT grow the
|
|
777
|
+
/// process thread count by N. Before the subscription redesign each pending input
|
|
778
|
+
/// parked one uncancellable OS thread in `block_until_settled` forever.
|
|
779
|
+
#[test]
|
|
780
|
+
fn race_with_never_settling_loser_does_not_park_threads() {
|
|
781
|
+
if os_thread_count() == 0 {
|
|
782
|
+
return; // platform without a thread-count probe
|
|
783
|
+
}
|
|
784
|
+
let n = 64usize;
|
|
785
|
+
// Keep the losers (and their stashed resolvers) alive: the leak is conditional
|
|
786
|
+
// on the loser never settling, which requires its resolve to stay reachable.
|
|
787
|
+
let mut keep: Vec<(Value, Value)> = Vec::new();
|
|
788
|
+
|
|
789
|
+
// Warm up lazy machinery so it doesn't count against the delta.
|
|
790
|
+
let (wp, wr) = deferred();
|
|
791
|
+
let out = promise_race(&[Value::array(vec![
|
|
792
|
+
promise_resolve(&[Value::Number(-1.0)]),
|
|
793
|
+
wp.clone(),
|
|
794
|
+
])]);
|
|
795
|
+
assert_eq!(expect_num(settle(&out)), -1.0);
|
|
796
|
+
keep.push((wp, wr));
|
|
797
|
+
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
798
|
+
|
|
799
|
+
let baseline = os_thread_count();
|
|
800
|
+
for i in 0..n {
|
|
801
|
+
let (p, r) = deferred();
|
|
802
|
+
let out = promise_race(&[Value::array(vec![
|
|
803
|
+
promise_resolve(&[Value::Number(i as f64)]),
|
|
804
|
+
p.clone(),
|
|
805
|
+
])]);
|
|
806
|
+
assert_eq!(expect_num(settle(&out)), i as f64);
|
|
807
|
+
keep.push((p, r));
|
|
808
|
+
}
|
|
809
|
+
// Give any transient threads a moment to exit before measuring.
|
|
810
|
+
std::thread::sleep(std::time::Duration::from_millis(200));
|
|
811
|
+
let after = os_thread_count();
|
|
812
|
+
let grown = after.saturating_sub(baseline);
|
|
813
|
+
assert!(
|
|
814
|
+
grown < n / 4,
|
|
815
|
+
"Promise.race parked ~1 thread per never-settling loser: baseline {} -> {} (+{}) after {} races",
|
|
816
|
+
baseline,
|
|
817
|
+
after,
|
|
818
|
+
grown,
|
|
819
|
+
n
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/// Same regression for `Promise.any`.
|
|
824
|
+
#[test]
|
|
825
|
+
fn any_with_never_settling_loser_does_not_park_threads() {
|
|
826
|
+
if os_thread_count() == 0 {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
let n = 64usize;
|
|
830
|
+
let mut keep: Vec<(Value, Value)> = Vec::new();
|
|
831
|
+
|
|
832
|
+
let (wp, wr) = deferred();
|
|
833
|
+
let out = promise_any(&[Value::array(vec![
|
|
834
|
+
wp.clone(),
|
|
835
|
+
promise_resolve(&[Value::Number(-1.0)]),
|
|
836
|
+
])]);
|
|
837
|
+
assert_eq!(expect_num(settle(&out)), -1.0);
|
|
838
|
+
keep.push((wp, wr));
|
|
839
|
+
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
840
|
+
|
|
841
|
+
let baseline = os_thread_count();
|
|
842
|
+
for i in 0..n {
|
|
843
|
+
let (p, r) = deferred();
|
|
844
|
+
let out = promise_any(&[Value::array(vec![
|
|
845
|
+
p.clone(),
|
|
846
|
+
promise_resolve(&[Value::Number(i as f64)]),
|
|
847
|
+
])]);
|
|
848
|
+
assert_eq!(expect_num(settle(&out)), i as f64);
|
|
849
|
+
keep.push((p, r));
|
|
850
|
+
}
|
|
851
|
+
std::thread::sleep(std::time::Duration::from_millis(200));
|
|
852
|
+
let after = os_thread_count();
|
|
853
|
+
let grown = after.saturating_sub(baseline);
|
|
854
|
+
assert!(
|
|
855
|
+
grown < n / 4,
|
|
856
|
+
"Promise.any parked ~1 thread per never-settling loser: baseline {} -> {} (+{}) after {} calls",
|
|
857
|
+
baseline,
|
|
858
|
+
after,
|
|
859
|
+
grown,
|
|
860
|
+
n
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/// Semantics guard: race settles with the first settled input.
|
|
865
|
+
#[test]
|
|
866
|
+
fn race_first_settled_input_wins() {
|
|
867
|
+
let (pending, _r) = deferred();
|
|
868
|
+
let out = promise_race(&[Value::array(vec![
|
|
869
|
+
promise_resolve(&[Value::Number(1.0)]),
|
|
870
|
+
pending,
|
|
871
|
+
])]);
|
|
872
|
+
assert_eq!(expect_num(settle(&out)), 1.0);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/// Semantics guard: a genuinely-pending winner settled from another thread wins the race.
|
|
876
|
+
#[test]
|
|
877
|
+
fn race_pending_winner_settled_from_another_thread() {
|
|
878
|
+
let (winner, resolve) = deferred();
|
|
879
|
+
let (never, _keep) = deferred();
|
|
880
|
+
let t = std::thread::spawn(move || {
|
|
881
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
882
|
+
call_value(&resolve, &[Value::Number(7.0)]);
|
|
883
|
+
});
|
|
884
|
+
let out = promise_race(&[Value::array(vec![winner, never])]);
|
|
885
|
+
assert_eq!(expect_num(settle(&out)), 7.0);
|
|
886
|
+
t.join().unwrap();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/// Semantics guard: any returns the first FULFILLMENT even with a pending loser present.
|
|
890
|
+
#[test]
|
|
891
|
+
fn any_first_fulfillment_wins_with_pending_loser() {
|
|
892
|
+
let (never, _keep) = deferred();
|
|
893
|
+
let out = promise_any(&[Value::array(vec![
|
|
894
|
+
never,
|
|
895
|
+
promise_reject(&[Value::String("nope".into())]),
|
|
896
|
+
promise_resolve(&[Value::Number(3.0)]),
|
|
897
|
+
])]);
|
|
898
|
+
assert_eq!(expect_num(settle(&out)), 3.0);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/// Semantics guard: any rejects with all reasons, in input order, when every input rejects.
|
|
902
|
+
#[test]
|
|
903
|
+
fn any_all_rejected_yields_reasons_in_order() {
|
|
904
|
+
let out = promise_any(&[Value::array(vec![
|
|
905
|
+
promise_reject(&[Value::String("a".into())]),
|
|
906
|
+
promise_reject(&[Value::String("b".into())]),
|
|
907
|
+
])]);
|
|
908
|
+
match settle(&out) {
|
|
909
|
+
Err(Value::Array(arr)) => {
|
|
910
|
+
let arr = arr.borrow();
|
|
911
|
+
assert_eq!(arr.len(), 2);
|
|
912
|
+
assert!(matches!(&arr[0], Value::String(s) if s.as_str() == "a"));
|
|
913
|
+
assert!(matches!(&arr[1], Value::String(s) if s.as_str() == "b"));
|
|
914
|
+
}
|
|
915
|
+
other => panic!("expected rejection with reasons array, got {other:?}"),
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/// Semantics guard: allSettled drains pending inputs settled from another thread.
|
|
920
|
+
#[test]
|
|
921
|
+
fn all_settled_waits_for_pending_inputs() {
|
|
922
|
+
let (p1, r1) = deferred();
|
|
923
|
+
let (p2, r2) = deferred();
|
|
924
|
+
let t = std::thread::spawn(move || {
|
|
925
|
+
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
926
|
+
call_value(&r1, &[Value::Number(1.0)]);
|
|
927
|
+
std::thread::sleep(std::time::Duration::from_millis(30));
|
|
928
|
+
call_value(&r2, &[Value::Number(2.0)]);
|
|
929
|
+
});
|
|
930
|
+
let out = promise_all_settled(&[Value::array(vec![p1, p2])]);
|
|
931
|
+
let settled = settle(&out).expect("allSettled fulfills");
|
|
932
|
+
let Value::Array(arr) = settled else {
|
|
933
|
+
panic!("allSettled must fulfill with an array")
|
|
934
|
+
};
|
|
935
|
+
let arr = arr.borrow();
|
|
936
|
+
assert_eq!(arr.len(), 2);
|
|
937
|
+
for (i, expected) in [(0usize, 1.0f64), (1, 2.0)] {
|
|
938
|
+
let v = object_get(&arr[i], &Value::String("value".into())).expect("value key");
|
|
939
|
+
assert!(matches!(v, Value::Number(n) if n == expected), "slot {i}");
|
|
940
|
+
}
|
|
941
|
+
t.join().unwrap();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/// Issue #702 adjacent defect: `race_channel` must not CONSUME its inputs. A promise
|
|
945
|
+
/// that loses a race stays awaitable — settling it later and awaiting must yield the
|
|
946
|
+
/// value, not "Promise already settled or consumed".
|
|
947
|
+
#[test]
|
|
948
|
+
fn race_loser_remains_awaitable() {
|
|
949
|
+
let (loser, resolve) = deferred();
|
|
950
|
+
let out = promise_race(&[Value::array(vec![
|
|
951
|
+
promise_resolve(&[Value::Number(1.0)]),
|
|
952
|
+
loser.clone(),
|
|
953
|
+
])]);
|
|
954
|
+
assert_eq!(expect_num(settle(&out)), 1.0);
|
|
955
|
+
|
|
956
|
+
call_value(&resolve, &[Value::Number(9.0)]);
|
|
957
|
+
// Give the (pre-fix) parked waiter thread time to consume the settlement, so the
|
|
958
|
+
// pre-fix failure is deterministic rather than a race with the test body.
|
|
959
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
960
|
+
assert_eq!(
|
|
961
|
+
expect_num(settle(&loser)),
|
|
962
|
+
9.0,
|
|
963
|
+
"a promise that lost a race must remain awaitable"
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/// Issue #702 adjacent defect (peek-not-take): an already-settled promise can be
|
|
968
|
+
/// awaited more than once, like in JS — including after losing a race.
|
|
969
|
+
#[test]
|
|
970
|
+
fn settled_promise_is_reawaitable() {
|
|
971
|
+
let p = promise_resolve(&[Value::Number(5.0)]);
|
|
972
|
+
assert_eq!(expect_num(settle(&p)), 5.0);
|
|
973
|
+
assert_eq!(
|
|
974
|
+
expect_num(settle(&p)),
|
|
975
|
+
5.0,
|
|
976
|
+
"second await of a settled promise must yield the value again"
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// And after being consumed by a race in phase 1 (try_settle path):
|
|
980
|
+
let q = promise_resolve(&[Value::Number(6.0)]);
|
|
981
|
+
let out = promise_race(&[Value::array(vec![q.clone()])]);
|
|
982
|
+
assert_eq!(expect_num(settle(&out)), 6.0);
|
|
983
|
+
assert_eq!(
|
|
984
|
+
expect_num(settle(&q)),
|
|
985
|
+
6.0,
|
|
986
|
+
"a settled promise that entered a race must remain awaitable"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|