@tishlang/tish-format 3.10.4 → 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.
Files changed (44) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/tish/src/cargo_native_registry.rs +4 -0
  3. package/crates/tish/src/main.rs +65 -25
  4. package/crates/tish/tests/integration_test.rs +3 -0
  5. package/crates/tish_builtins/src/globals.rs +3 -3
  6. package/crates/tish_builtins/src/symbol.rs +86 -4
  7. package/crates/tish_bytecode/src/compiler.rs +65 -25
  8. package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
  9. package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
  10. package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
  11. package/crates/tish_core/src/shape.rs +55 -2
  12. package/crates/tish_core/src/value.rs +229 -13
  13. package/crates/tish_eval/src/timers.rs +66 -2
  14. package/crates/tish_ffi/src/lib.rs +74 -12
  15. package/crates/tish_ffi/tests/loader.rs +35 -0
  16. package/crates/tish_fmt/Cargo.toml +1 -1
  17. package/crates/tish_lsp/src/main.rs +91 -2
  18. package/crates/tish_pg/src/lib.rs +59 -8
  19. package/crates/tish_pg/src/statement_registry.rs +250 -0
  20. package/crates/tish_runtime/src/http.rs +19 -0
  21. package/crates/tish_runtime/src/http_fetch.rs +159 -65
  22. package/crates/tish_runtime/src/http_hyper.rs +18 -2
  23. package/crates/tish_runtime/src/lib.rs +4 -0
  24. package/crates/tish_runtime/src/net.rs +125 -97
  25. package/crates/tish_runtime/src/process_spawn.rs +326 -113
  26. package/crates/tish_runtime/src/promise.rs +521 -90
  27. package/crates/tish_runtime/src/promise_io.rs +29 -6
  28. package/crates/tish_runtime/src/pty.rs +161 -107
  29. package/crates/tish_runtime/src/stream_buf.rs +419 -0
  30. package/crates/tish_runtime/src/ws.rs +431 -52
  31. package/crates/tish_vm/src/jit.rs +473 -67
  32. package/crates/tish_vm/src/lib.rs +6 -0
  33. package/crates/tish_vm/src/vm.rs +8 -5
  34. package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
  35. package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
  36. package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -0
  37. package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
  38. package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -0
  39. package/package.json +1 -1
  40. package/platform/darwin-arm64/tish-fmt +0 -0
  41. package/platform/darwin-x64/tish-fmt +0 -0
  42. package/platform/linux-arm64/tish-fmt +0 -0
  43. package/platform/linux-x64/tish-fmt +0 -0
  44. 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
- //! `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:
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
- /// Fulfilled or rejected before anyone awaits — `block_until_settled` consumes the result once.
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
- slot: Mutex<Option<Result<Value, Value>>>,
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
- slot: Mutex::new(Some(result)),
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.slot
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.slot.lock().unwrap().take().unwrap_or(
59
- Err(Value::String("Promise already consumed".into())),
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)` — executor runs synchronously; `resolve` / `reject` unblock `recv`.
80
- struct DeferredChannelPromise {
81
- rx: Mutex<Option<mpsc::Receiver<Result<Value, Value>>>>,
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 DeferredChannelPromise {
192
+ impl TishPromise for DeferredCellPromise {
85
193
  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
- }
194
+ self.cell.wait()
95
195
  }
96
196
 
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.
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
- 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
- }
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 TishPromise for ThenPromise {
130
- fn block_until_settled(&self) -> std::result::Result<Value, Value> {
131
- match self.pred.block_until_settled() {
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. 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.
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 any threads are spawned. This gives deterministic
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`) go to background threads,
214
- /// which is where concurrency matters.
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 settle (ImmediateSettledPromise, ThenPromise
229
- // over immediate, etc.). These never need a thread; handle in order.
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 — spawn a thread.
234
- let p = Arc::clone(p);
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
- std::thread::spawn(move || {
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 (tx, rx) = mpsc::channel::<std::result::Result<Value, Value>>();
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
- let _ = tx.send(match result {
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(DeferredChannelPromise {
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 (tx, rx) = mpsc::channel();
447
- let tx_cell = Arc::new(Mutex::new(Some(tx)));
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 tx_cell = Arc::clone(&tx_cell);
576
+ let guard = Arc::clone(&guard);
450
577
  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
- }
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 tx_cell = Arc::clone(&tx_cell);
585
+ let guard = Arc::clone(&guard);
461
586
  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
- }
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(DeferredChannelPromise {
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
+ }