@scriptc/runtime 0.0.22 → 0.0.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scriptc/runtime",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "scriptc native runtime — C sources, compiled into every scriptc binary",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scriptc.dev",
package/src/scr_array.c CHANGED
@@ -68,6 +68,16 @@ static void scr_elem_release(const ScrArr *a, uint64_t slot) {
68
68
  else if (a->elem == SCR_ELEM_REF) a->elem_release(p);
69
69
  }
70
70
 
71
+ static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) {
72
+ if (!scr_elem_is_ref(a->elem)) return slot;
73
+ void *p = scr_slot_to_ptr(slot);
74
+ if (a->elem == SCR_ELEM_STR) p = scr_str_retain((ScrStr *)p);
75
+ else if (a->elem == SCR_ELEM_ARR) p = scr_arr_retain((ScrArr *)p);
76
+ else if (a->elem == SCR_ELEM_BYTES) p = scr_bytes_retain((ScrBytes *)p);
77
+ else p = a->elem_retain(p);
78
+ return scr_slot_from_ptr(p);
79
+ }
80
+
71
81
  /* ── lifecycle ─────────────────────────────────────────────────────────── */
72
82
 
73
83
  static void scr_arr_grow(ScrArr *a, size_t need) {
@@ -259,6 +269,61 @@ double scr_arr_push_ref(ScrArr *a, void *v) {
259
269
  return scr_arr_push_slot(a, scr_slot_from_ptr(v));
260
270
  }
261
271
 
272
+ /* ── unshift / reverse ──────────────────────────────────────────────────
273
+ * Single-element unshift takes ownership, matching push. The emitter calls
274
+ * it from right to left after evaluating every variadic argument, preserving
275
+ * JS argument order without a temporary array. The spread form borrows and
276
+ * retains its source, snapshots the count, and handles self-spread after the
277
+ * tail move by reading the relocated original block. */
278
+ static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) {
279
+ scr_arr_grow(a, a->len + 1);
280
+ memmove(a->data + 1, a->data, a->len * sizeof(uint64_t));
281
+ a->data[0] = slot;
282
+ a->len++;
283
+ return (double)a->len;
284
+ }
285
+
286
+ double scr_arr_unshift_f64(ScrArr *a, double v) {
287
+ return scr_arr_unshift_slot(a, scr_slot_from_f64(v));
288
+ }
289
+
290
+ double scr_arr_unshift_bool(ScrArr *a, bool v) {
291
+ return scr_arr_unshift_slot(a, (uint64_t)(v ? 1 : 0));
292
+ }
293
+
294
+ double scr_arr_unshift_ref(ScrArr *a, void *v) {
295
+ return scr_arr_unshift_slot(a, scr_slot_from_ptr(v));
296
+ }
297
+
298
+ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) {
299
+ size_t old_len = a->len;
300
+ size_t add = src->len;
301
+ if (add == 0) return (double)old_len;
302
+ if (add > SIZE_MAX - old_len) scr_arr_oom();
303
+ scr_arr_grow(a, old_len + add);
304
+ memmove(a->data + add, a->data, old_len * sizeof(uint64_t));
305
+ if (src == a) {
306
+ for (size_t i = 0; i < add; i++) {
307
+ a->data[i] = scr_elem_retain_slot(a, a->data[add + i]);
308
+ }
309
+ } else {
310
+ for (size_t i = 0; i < add; i++) {
311
+ a->data[i] = scr_elem_retain_slot(src, src->data[i]);
312
+ }
313
+ }
314
+ a->len = old_len + add;
315
+ return (double)a->len;
316
+ }
317
+
318
+ ScrArr *scr_arr_reverse(ScrArr *a) {
319
+ for (size_t i = 0; i < a->len / 2; i++) {
320
+ uint64_t tmp = a->data[i];
321
+ a->data[i] = a->data[a->len - 1 - i];
322
+ a->data[a->len - 1 - i] = tmp;
323
+ }
324
+ return scr_arr_retain(a);
325
+ }
326
+
262
327
  static uint64_t scr_arr_pop_slot(ScrArr *a) {
263
328
  if (a->len == 0) {
264
329
  scr_trap("scriptc: RangeError: pop() on an empty array\n");
package/src/scr_async.c CHANGED
@@ -39,6 +39,7 @@
39
39
  #include <windows.h>
40
40
  #else
41
41
  #include <poll.h>
42
+ #include <pthread.h>
42
43
  #include <signal.h>
43
44
  #include <ucontext.h>
44
45
  #include <unistd.h>
@@ -627,8 +628,9 @@ ScrArr *scr_active_resources(void) {
627
628
  * left queued on the uncaught paths) never run — the teardown releases
628
629
  * them, like Node dropping the queue at exit. */
629
630
  typedef struct ScrNtick {
630
- ScrClosure *cb; /* owned; NULL for a raw C-hook entry */
631
- void (*raw)(void); /* the hook when cb == NULL (stream tick markers) */
631
+ ScrClosure *cb; /* owned; NULL for a raw C-hook entry */
632
+ ScrFsRenameFn error_cb; /* process-write success adapter, or NULL */
633
+ void (*raw)(void); /* hook when cb == NULL (stream markers) */
632
634
  struct ScrNtick *next;
633
635
  } ScrNtick;
634
636
 
@@ -649,6 +651,20 @@ void scr_next_tick(ScrClosure *cb /*moves*/) {
649
651
  scr_nt_tail = t;
650
652
  }
651
653
 
654
+ /* stdout/stderr write completions are Node nextTicks too, but their
655
+ * callback receives the success `null` argument. Reuse the error-first
656
+ * adapter ABI also used by fs.rename: emitted adapters construct the
657
+ * program's Error | null union, and NULL selects its null arm. */
658
+ void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) {
659
+ ScrNtick *t = calloc(1, sizeof *t);
660
+ if (!t) scr_oom();
661
+ t->cb = cb;
662
+ t->error_cb = fn;
663
+ if (scr_nt_tail) scr_nt_tail->next = t;
664
+ else scr_nt_head = t;
665
+ scr_nt_tail = t;
666
+ }
667
+
652
668
  /* A RAW C-hook tick: the stream unit enqueues one marker per deferred
653
669
  * stream emission so those emissions interleave with user nextTicks in
654
670
  * true FIFO order — in Node they ARE nextTicks (resume_, emitReadable_,
@@ -1441,6 +1457,11 @@ ScrPromise *scr_fsp_write_file(ScrStr *path, ScrStr *data) {
1441
1457
  return scr_promise_settled_void();
1442
1458
  }
1443
1459
 
1460
+ ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
1461
+ scr_fs_write_file_mode(path, data, mode);
1462
+ return scr_promise_settled_void();
1463
+ }
1464
+
1444
1465
  ScrPromise *scr_fsp_mkdir(ScrStr *path) {
1445
1466
  scr_fs_mkdir(path);
1446
1467
  return scr_promise_settled_void();
@@ -1486,6 +1507,248 @@ ScrPromise *scr_fsp_stat(ScrStr *path) {
1486
1507
  return scr_promise_settled_ref(st, &scr_stats_retain_v, &scr_stats_release_v, NULL);
1487
1508
  }
1488
1509
 
1510
+ ScrPromise *scr_fsp_rename(ScrStr *oldpath, ScrStr *newpath) {
1511
+ scr_fs_rename(oldpath, newpath);
1512
+ return scr_promise_settled_void();
1513
+ }
1514
+
1515
+ /* fs.rename(old, new, cb): the static callback surface. The OS operation
1516
+ * starts immediately on a native worker, matching libuv's key contract:
1517
+ * while JS/main is synchronously occupied, the filesystem request can still
1518
+ * make progress. Only immutable path bytes cross onto the worker. Error
1519
+ * construction, callback invocation, and every RC mutation stay on the main
1520
+ * runtime thread when a later loop turn observes completion.
1521
+ *
1522
+ * A compact operation queue is also the liveness handle: an outstanding
1523
+ * callback-style request keeps the loop alive. Four persistent workers mirror
1524
+ * libuv's default filesystem concurrency without creating one OS thread per
1525
+ * request. A platform mutex publishes each result and the syscall's filesystem
1526
+ * effects before the loop removes it from the completion queue. */
1527
+ #ifndef SCR_LIB
1528
+ typedef struct ScrFsRenameOp {
1529
+ ScrStr *oldpath;
1530
+ ScrStr *newpath;
1531
+ ScrClosure *cb;
1532
+ ScrFsRenameFn fn;
1533
+ int error;
1534
+ struct ScrFsRenameOp *next;
1535
+ } ScrFsRenameOp;
1536
+
1537
+ static ScrFsRenameOp *scr_fs_rename_work = NULL;
1538
+ static ScrFsRenameOp **scr_fs_rename_work_tail = &scr_fs_rename_work;
1539
+ static ScrFsRenameOp *scr_fs_rename_done = NULL;
1540
+ static ScrFsRenameOp **scr_fs_rename_done_tail = &scr_fs_rename_done;
1541
+ static size_t scr_fs_rename_pending_count = 0;
1542
+ static size_t scr_fs_rename_worker_count = 0;
1543
+ static bool scr_fs_rename_stopping = false;
1544
+ static bool scr_fs_rename_shutdown_registered = false;
1545
+
1546
+ #ifdef _WIN32
1547
+ static HANDLE scr_fs_rename_workers[4];
1548
+ static SRWLOCK scr_fs_rename_lock = SRWLOCK_INIT;
1549
+ static CONDITION_VARIABLE scr_fs_rename_cond = CONDITION_VARIABLE_INIT;
1550
+ static void scr_fs_rename_lock_enter(void) { AcquireSRWLockExclusive(&scr_fs_rename_lock); }
1551
+ static void scr_fs_rename_lock_leave(void) { ReleaseSRWLockExclusive(&scr_fs_rename_lock); }
1552
+ static void scr_fs_rename_wait(void) {
1553
+ (void)SleepConditionVariableSRW(&scr_fs_rename_cond, &scr_fs_rename_lock, INFINITE, 0);
1554
+ }
1555
+ static void scr_fs_rename_signal(void) { WakeConditionVariable(&scr_fs_rename_cond); }
1556
+ static void scr_fs_rename_broadcast(void) { WakeAllConditionVariable(&scr_fs_rename_cond); }
1557
+ #else
1558
+ static pthread_t scr_fs_rename_workers[4];
1559
+ static pthread_mutex_t scr_fs_rename_lock = PTHREAD_MUTEX_INITIALIZER;
1560
+ static pthread_cond_t scr_fs_rename_cond = PTHREAD_COND_INITIALIZER;
1561
+ static void scr_fs_rename_lock_enter(void) { (void)pthread_mutex_lock(&scr_fs_rename_lock); }
1562
+ static void scr_fs_rename_lock_leave(void) { (void)pthread_mutex_unlock(&scr_fs_rename_lock); }
1563
+ static void scr_fs_rename_wait(void) {
1564
+ (void)pthread_cond_wait(&scr_fs_rename_cond, &scr_fs_rename_lock);
1565
+ }
1566
+ static void scr_fs_rename_signal(void) { (void)pthread_cond_signal(&scr_fs_rename_cond); }
1567
+ static void scr_fs_rename_broadcast(void) { (void)pthread_cond_broadcast(&scr_fs_rename_cond); }
1568
+ #endif
1569
+
1570
+ static void scr_fs_rename_op_release(ScrFsRenameOp *op) {
1571
+ scr_str_release(op->oldpath);
1572
+ scr_str_release(op->newpath);
1573
+ scr_closure_release(op->cb);
1574
+ free(op);
1575
+ }
1576
+
1577
+ static void scr_fs_rename_worker_loop(void) {
1578
+ for (;;) {
1579
+ scr_fs_rename_lock_enter();
1580
+ while (scr_fs_rename_work == NULL && !scr_fs_rename_stopping) scr_fs_rename_wait();
1581
+ if (scr_fs_rename_stopping) {
1582
+ scr_fs_rename_lock_leave();
1583
+ return;
1584
+ }
1585
+ ScrFsRenameOp *op = scr_fs_rename_work;
1586
+ scr_fs_rename_work = op->next;
1587
+ if (scr_fs_rename_work == NULL) scr_fs_rename_work_tail = &scr_fs_rename_work;
1588
+ scr_fs_rename_lock_leave();
1589
+
1590
+ op->error = scr_fs_rename_raw(op->oldpath, op->newpath);
1591
+
1592
+ scr_fs_rename_lock_enter();
1593
+ op->next = NULL;
1594
+ *scr_fs_rename_done_tail = op;
1595
+ scr_fs_rename_done_tail = &op->next;
1596
+ scr_fs_rename_lock_leave();
1597
+ }
1598
+ }
1599
+
1600
+ #ifdef _WIN32
1601
+ static DWORD WINAPI scr_fs_rename_worker(LPVOID payload) {
1602
+ (void)payload;
1603
+ scr_fs_rename_worker_loop();
1604
+ return 0;
1605
+ }
1606
+ #else
1607
+ static void *scr_fs_rename_worker(void *payload) {
1608
+ (void)payload;
1609
+ scr_fs_rename_worker_loop();
1610
+ return NULL;
1611
+ }
1612
+ #endif
1613
+
1614
+ /* Runs before the ordinary runtime atexit cleanup/audit (it is registered
1615
+ * lazily by the first rename request, after scr_init/scr_lib_init). Fatal
1616
+ * top-level or callback exceptions can stop the loop with requests still in
1617
+ * flight; join the workers before releasing their retained path/callback
1618
+ * payloads so sanitized builds keep the program's real exit verdict. */
1619
+ static void scr_fs_renames_shutdown(void) {
1620
+ scr_fs_rename_lock_enter();
1621
+ scr_fs_rename_stopping = true;
1622
+ scr_fs_rename_broadcast();
1623
+ scr_fs_rename_lock_leave();
1624
+
1625
+ for (size_t i = 0; i < scr_fs_rename_worker_count; i++) {
1626
+ #ifdef _WIN32
1627
+ (void)WaitForSingleObject(scr_fs_rename_workers[i], INFINITE);
1628
+ CloseHandle(scr_fs_rename_workers[i]);
1629
+ #else
1630
+ (void)pthread_join(scr_fs_rename_workers[i], NULL);
1631
+ #endif
1632
+ }
1633
+
1634
+ scr_fs_rename_lock_enter();
1635
+ ScrFsRenameOp *work = scr_fs_rename_work;
1636
+ ScrFsRenameOp *done = scr_fs_rename_done;
1637
+ scr_fs_rename_work = NULL;
1638
+ scr_fs_rename_work_tail = &scr_fs_rename_work;
1639
+ scr_fs_rename_done = NULL;
1640
+ scr_fs_rename_done_tail = &scr_fs_rename_done;
1641
+ scr_fs_rename_pending_count = 0;
1642
+ scr_fs_rename_lock_leave();
1643
+ while (work != NULL) {
1644
+ ScrFsRenameOp *next = work->next;
1645
+ scr_fs_rename_op_release(work);
1646
+ work = next;
1647
+ }
1648
+ while (done != NULL) {
1649
+ ScrFsRenameOp *next = done->next;
1650
+ scr_fs_rename_op_release(done);
1651
+ done = next;
1652
+ }
1653
+ }
1654
+
1655
+ static bool scr_fs_renames_pending(void) { return scr_fs_rename_pending_count != 0; }
1656
+
1657
+ static bool scr_fs_renames_dispatch(void) {
1658
+ scr_fs_rename_lock_enter();
1659
+ ScrFsRenameOp *op = scr_fs_rename_done;
1660
+ if (op != NULL) {
1661
+ scr_fs_rename_done = op->next;
1662
+ if (scr_fs_rename_done == NULL) scr_fs_rename_done_tail = &scr_fs_rename_done;
1663
+ }
1664
+ scr_fs_rename_lock_leave();
1665
+ if (op == NULL) return false;
1666
+ scr_fs_rename_pending_count--;
1667
+ ScrCaught *caught = NULL;
1668
+ ScrError *err = NULL;
1669
+ if (op->error != 0) {
1670
+ scr_fs_rename_error(op->error, op->oldpath, op->newpath);
1671
+ caught = scr_exc_take();
1672
+ if (caught && caught->kind == SCR_EXC_OBJ && scr_error_is(caught->payload)) {
1673
+ err = (ScrError *)caught->payload;
1674
+ }
1675
+ }
1676
+ op->fn(op->cb, err);
1677
+ scr_caught_release(caught);
1678
+ scr_fs_rename_op_release(op);
1679
+ return true;
1680
+ }
1681
+ #else
1682
+ static bool scr_fs_renames_pending(void) { return false; }
1683
+ static bool scr_fs_renames_dispatch(void) { return false; }
1684
+ #endif
1685
+
1686
+ void scr_fs_rename_async(ScrStr *oldpath, ScrStr *newpath,
1687
+ ScrClosure *cb, ScrFsRenameFn fn) {
1688
+ #ifdef SCR_LIB
1689
+ (void)oldpath; (void)newpath; (void)fn;
1690
+ scr_closure_release(cb);
1691
+ scr_trap("scriptc: internal error: fs.rename reached in a library build — please report this");
1692
+ #else
1693
+ if (!scr_fs_rename_shutdown_registered) {
1694
+ if (atexit(scr_fs_renames_shutdown) != 0) {
1695
+ scr_trap("scriptc: could not register fs.rename worker cleanup\n");
1696
+ }
1697
+ scr_fs_rename_shutdown_registered = true;
1698
+ }
1699
+ ScrFsRenameOp *op = calloc(1, sizeof *op);
1700
+ if (!op) scr_trap("scriptc: out of memory\n");
1701
+ op->oldpath = scr_str_retain(oldpath);
1702
+ op->newpath = scr_str_retain(newpath);
1703
+ op->cb = cb;
1704
+ op->fn = fn;
1705
+ scr_fs_rename_pending_count++;
1706
+ scr_fs_rename_lock_enter();
1707
+ *scr_fs_rename_work_tail = op;
1708
+ scr_fs_rename_work_tail = &op->next;
1709
+ int create_error = 0;
1710
+ if (scr_fs_rename_worker_count < 4) {
1711
+ #ifdef _WIN32
1712
+ HANDLE worker = CreateThread(NULL, 0, scr_fs_rename_worker, NULL, 0, NULL);
1713
+ if (worker != NULL) {
1714
+ scr_fs_rename_workers[scr_fs_rename_worker_count] = worker;
1715
+ scr_fs_rename_worker_count++;
1716
+ } else {
1717
+ create_error = EAGAIN;
1718
+ }
1719
+ #else
1720
+ pthread_t worker;
1721
+ create_error = pthread_create(&worker, NULL, scr_fs_rename_worker, NULL);
1722
+ if (create_error == 0) {
1723
+ scr_fs_rename_workers[scr_fs_rename_worker_count] = worker;
1724
+ scr_fs_rename_worker_count++;
1725
+ }
1726
+ #endif
1727
+ }
1728
+ if (create_error != 0 && scr_fs_rename_worker_count == 0) {
1729
+ /* Submission failure is still callback-asynchronous. No worker exists,
1730
+ * so publish every queued request as the same resource error. */
1731
+ while (scr_fs_rename_work != NULL) {
1732
+ ScrFsRenameOp *failed = scr_fs_rename_work;
1733
+ scr_fs_rename_work = failed->next;
1734
+ failed->error = create_error;
1735
+ failed->next = NULL;
1736
+ *scr_fs_rename_done_tail = failed;
1737
+ scr_fs_rename_done_tail = &failed->next;
1738
+ }
1739
+ scr_fs_rename_work_tail = &scr_fs_rename_work;
1740
+ } else {
1741
+ scr_fs_rename_signal();
1742
+ }
1743
+ scr_fs_rename_lock_leave();
1744
+ #endif
1745
+ }
1746
+
1747
+ void scr_fs_rename_thunk0(ScrClosure *cb, ScrError *err) {
1748
+ (void)err;
1749
+ ((void (*)(ScrClosure *))cb->fn)(cb);
1750
+ }
1751
+
1489
1752
  /* ── node:timers/promises ────────────────────────────────────────────
1490
1753
  * The promisified pair: a PENDING void promise a one-shot heap timer
1491
1754
  * (setTimeout) or the immediate queue (setImmediate) fulfills — the
@@ -1763,10 +2026,12 @@ bool scr_loop_run(ScrPromise *top_level) {
1763
2026
  scr_nt_head = t->next;
1764
2027
  if (scr_nt_head == NULL) scr_nt_tail = NULL;
1765
2028
  ScrClosure *cb = t->cb;
2029
+ ScrFsRenameFn error_cb = t->error_cb;
1766
2030
  void (*raw)(void) = t->raw;
1767
2031
  free(t);
1768
2032
  if (cb) {
1769
- ((void (*)(ScrClosure *))cb->fn)(cb);
2033
+ if (error_cb) error_cb(cb, NULL);
2034
+ else ((void (*)(ScrClosure *))cb->fn)(cb);
1770
2035
  scr_closure_release(cb);
1771
2036
  } else {
1772
2037
  raw(); /* one stream tick, FIFO with the user ticks around it */
@@ -1802,8 +2067,21 @@ bool scr_loop_run(ScrPromise *top_level) {
1802
2067
  }
1803
2068
  if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) continue;
1804
2069
  /* Quiescent between turns (microtasks drained, nothing running):
1805
- * collect any cycles the turn left behind. No-op on an empty buffer. */
1806
- scr_collect_cycles();
2070
+ * collect any cycles the turn left behind. One SCHEDULED pass almost
2071
+ * always a nursery one, sized to the turn's own garbage. A full sweep
2072
+ * here would walk the whole live heap every turn, which is precisely
2073
+ * what the generations exist to avoid. No-op on an empty buffer. */
2074
+ scr_cyc_collect_scheduled();
2075
+ /* Completed callback-style filesystem work is delivered on the main
2076
+ * runtime thread. A worker may have finished while synchronous user code
2077
+ * occupied that thread. Deliver exactly one completion per checkpoint:
2078
+ * Node drains nextTicks and microtasks after each native callback before
2079
+ * invoking the next ready callback. */
2080
+ if (scr_fs_renames_pending()) {
2081
+ bool dispatched = scr_fs_renames_dispatch();
2082
+ if (scr_exc_pending()) return false;
2083
+ if (dispatched) continue;
2084
+ }
1807
2085
  /* Stream tick dispatch (scr_stream.c, when linked): the deferred
1808
2086
  * next-tick emissions ('data' flow kicks, 'readable'/'end'/'finish'/
1809
2087
  * 'drain'/'error'/'close') fire now, FIRST — the nextTick station.
@@ -1871,7 +2149,8 @@ bool scr_loop_run(ScrPromise *top_level) {
1871
2149
  (scr_events_pending_fn != NULL && scr_events_pending_fn()) ||
1872
2150
  (scr_net_pending_fn != NULL && scr_net_pending_fn()) ||
1873
2151
  (scr_dgram_pending_fn != NULL && scr_dgram_pending_fn()) ||
1874
- (scr_watch_pending_fn != NULL && scr_watch_pending_fn());
2152
+ (scr_watch_pending_fn != NULL && scr_watch_pending_fn()) ||
2153
+ scr_fs_renames_pending();
1875
2154
  if (held) {
1876
2155
  scr_children_poll();
1877
2156
  if (scr_exc_pending()) return false; /* uncaught throw in a listener */
@@ -1890,13 +2169,14 @@ bool scr_loop_run(ScrPromise *top_level) {
1890
2169
  bool net = scr_net_pending_fn != NULL && scr_net_pending_fn();
1891
2170
  bool dgram = scr_dgram_pending_fn != NULL && scr_dgram_pending_fn();
1892
2171
  bool watch = scr_watch_pending_fn != NULL && scr_watch_pending_fn();
2172
+ bool renames = scr_fs_renames_pending();
1893
2173
  /* Timer liveness counts only REF'd timers: an unref'd timer stays in
1894
2174
  * the heap (and fires if the loop runs on for other reasons) but does
1895
2175
  * not by itself keep the process alive — Node's unref semantics.
1896
2176
  * Children follow the same rule: an unref'd child is still REAPED
1897
2177
  * while the loop runs (kids drives the sweeps and sleeps above) but
1898
2178
  * only reffed ones keep the process alive. */
1899
- if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !io && !events && !net && !dgram && !watch) break;
2179
+ if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !io && !events && !net && !dgram && !watch && !renames) break;
1900
2180
  /* Sleep to the earliest deadline, then run every due timer (each may
1901
2181
  * enqueue microtasks, which the next iteration drains first). Who
1902
2182
  * sleeps depends on what is pending:
@@ -1917,6 +2197,10 @@ bool scr_loop_run(ScrPromise *top_level) {
1917
2197
  * - timers only: plain nanosleep to the deadline. */
1918
2198
  double now = scr_now_ms();
1919
2199
  double due = scr_ntimers > 0 ? scr_timers[0].deadline_ms : now + SCR_IO_POLL_MS;
2200
+ /* The rename worker has no platform poll handle yet. Bound the idle
2201
+ * wait exactly like the portable child fallback so completion is noticed
2202
+ * promptly even when another poller owns the sleep. */
2203
+ if (renames && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
1920
2204
  /* An armed island timer (AbortSignal.timeout) caps the sleep: it must
1921
2205
  * fire on time even while the poller waits on socket readiness. */
1922
2206
  if (scr_island_deadline_fn != NULL) {