@scriptc/runtime 0.0.22 → 0.0.23

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/src/scr_json.c CHANGED
@@ -1637,8 +1637,17 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
1637
1637
  case SCR_DYN_OBJ:
1638
1638
  return scr_str_new("[object Object]", 15);
1639
1639
  case SCR_DYN_HANDLE:
1640
- /* IncomingMessage/ServerResponse/Socket inherit
1641
- * Object.prototype.toString Node's String() answer exactly. */
1640
+ if (d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL &&
1641
+ d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) {
1642
+ ScrJsonBuf b;
1643
+ scr_jb_init(&b);
1644
+ scr_jb_puts(&b, "[object ");
1645
+ scr_jb_puts(&b, scr_dyn_handle_cls(d));
1646
+ scr_jb_putc(&b, ']');
1647
+ return scr_jb_finish(&b);
1648
+ }
1649
+ /* IncomingMessage/ServerResponse/Socket and the other Node handles
1650
+ * inherit Object.prototype.toString without a @@toStringTag. */
1642
1651
  return scr_str_new("[object Object]", 15);
1643
1652
  case SCR_DYN_PROMISE:
1644
1653
  /* Object.prototype.toString with the Promise @@toStringTag. */
@@ -1789,6 +1798,81 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
1789
1798
  return scr_dyn_string_coerce(d);
1790
1799
  }
1791
1800
 
1801
+ /* JS ToNumber over a checked-dynamic value, including OrdinaryToPrimitive's
1802
+ * NUMBER hint for object snapshots. This is the numeric twin of
1803
+ * scr_dyn_string_coerce_js above: valueOf precedes toString, inherited
1804
+ * Object.prototype.valueOf returns the receiver (so conversion continues),
1805
+ * and the inherited toString fallback supplies "[object Object]". Borrows d;
1806
+ * false means an object hook threw or no primitive value could be produced. */
1807
+ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) {
1808
+ if (d->kind == SCR_DYN_TYPED_REF) {
1809
+ ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
1810
+ bool ok = scr_dyn_number_coerce_js(materialized, out);
1811
+ scr_dyn_release(materialized);
1812
+ return ok;
1813
+ }
1814
+ switch (d->kind) {
1815
+ case SCR_DYN_NULL:
1816
+ *out = 0.0;
1817
+ return true;
1818
+ case SCR_DYN_BOOL:
1819
+ *out = d->v.b ? 1.0 : 0.0;
1820
+ return true;
1821
+ case SCR_DYN_NUM:
1822
+ *out = d->v.num;
1823
+ return true;
1824
+ case SCR_DYN_STR:
1825
+ *out = scr_string_to_number(d->v.str);
1826
+ return true;
1827
+ case SCR_DYN_UNDEF:
1828
+ *out = NAN;
1829
+ return true;
1830
+ case SCR_DYN_OBJ: {
1831
+ static const char *const hint[2] = { "valueOf", "toString" };
1832
+ for (int i = 0; i < 2; i++) {
1833
+ ScrDyn *m = scr_dyn_obj_get(d, hint[i], strlen(hint[i])); /* borrowed */
1834
+ if (!m) {
1835
+ if (!d->null_proto && i == 0) {
1836
+ /* Inherited Object.prototype.valueOf returns the object, so the
1837
+ * number-hint protocol advances to toString. */
1838
+ continue;
1839
+ }
1840
+ if (!d->null_proto && i == 1) {
1841
+ *out = NAN; /* Number("[object Object]") */
1842
+ return true;
1843
+ }
1844
+ continue;
1845
+ }
1846
+ if (m->kind != SCR_DYN_FUNC) continue;
1847
+ scr_dyn_this_push_dyn(d);
1848
+ ScrDyn *r = scr_dyn_call(m, NULL, 0, hint[i]);
1849
+ scr_dyn_this_pop();
1850
+ if (!r) return false;
1851
+ if (scr_dyn_to_primitive_result_is_object(r)) {
1852
+ scr_dyn_release(r);
1853
+ continue;
1854
+ }
1855
+ bool ok = scr_dyn_number_coerce_js(r, out);
1856
+ scr_dyn_release(r);
1857
+ return ok;
1858
+ }
1859
+ static const char msg[] = "Cannot convert object to primitive value";
1860
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
1861
+ return false;
1862
+ }
1863
+ default: {
1864
+ /* Arrays, byte views, functions, promises, and native handles inherit a
1865
+ * valueOf that returns the receiver, then use their existing JS-exact
1866
+ * string rendering for the numeric conversion. */
1867
+ ScrStr *text = scr_dyn_string_coerce(d);
1868
+ if (!text) return false;
1869
+ *out = scr_string_to_number(text);
1870
+ scr_str_release(text);
1871
+ return true;
1872
+ }
1873
+ }
1874
+ }
1875
+
1792
1876
  /* The checked-dynamic keyed WRITE (`h.k = v` on a dyn receiver): OBJ sets
1793
1877
  * the member (later writes win, insertion order — JS); undefined/null
1794
1878
  * throws Node's "Cannot set properties of ..."; every other kind throws
package/src/scr_lib.c CHANGED
@@ -1646,12 +1646,77 @@ double scr_fs_open(ScrStr *path, ScrStr *flags) {
1646
1646
 
1647
1647
  /* fs.closeSync(fd) — close(2); failure throws Node's path-less fs error
1648
1648
  * shape ("EBADF: bad file descriptor, close"). */
1649
- /* fs.readSync(fd, buffer, offset, length) — the 4-argument buffer form.
1649
+
1650
+ /* Offset-preserving read for fs.readSync's numeric-position form. POSIX
1651
+ * supplies pread(2). Windows follows libuv's synchronous-handle recipe:
1652
+ * ReadFile with an OVERLAPPED byte offset, then restore the handle position
1653
+ * because synchronous ReadFile updates it even when OVERLAPPED is present. */
1654
+ static ssize_t scr_fs_pread(int fd, void *data, size_t length, double position) {
1655
+ #ifdef _WIN32
1656
+ HANDLE handle = (HANDLE)_get_osfhandle(fd);
1657
+ if (handle == INVALID_HANDLE_VALUE) {
1658
+ errno = EBADF;
1659
+ return -1;
1660
+ }
1661
+
1662
+ OVERLAPPED overlapped;
1663
+ memset(&overlapped, 0, sizeof overlapped);
1664
+ LARGE_INTEGER at;
1665
+ at.QuadPart = (LONGLONG)position;
1666
+ overlapped.Offset = at.LowPart;
1667
+ overlapped.OffsetHigh = at.HighPart;
1668
+
1669
+ LARGE_INTEGER zero;
1670
+ LARGE_INTEGER original;
1671
+ zero.QuadPart = 0;
1672
+ BOOL restore = SetFilePointerEx(handle, zero, &original, FILE_CURRENT);
1673
+ DWORD got = 0;
1674
+ DWORD want = length > (size_t)UINT32_MAX ? UINT32_MAX : (DWORD)length;
1675
+ BOOL ok = ReadFile(handle, data, want, &got, &overlapped);
1676
+ DWORD error = ok ? ERROR_SUCCESS : GetLastError();
1677
+ if (restore) (void)SetFilePointerEx(handle, original, NULL, FILE_BEGIN);
1678
+ /* ReadFile may report a terminal status after still filling part of the
1679
+ * caller's buffer (notably ERROR_MORE_DATA for a message-mode pipe).
1680
+ * libuv/Node return those bytes and surface the status on the next read. */
1681
+ if (ok || got > 0 || error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE) {
1682
+ return (ssize_t)got;
1683
+ }
1684
+
1685
+ /* The read-specific subset of libuv's Win32-to-errno translation. */
1686
+ switch (error) {
1687
+ case ERROR_INVALID_HANDLE:
1688
+ case ERROR_ACCESS_DENIED:
1689
+ errno = EBADF;
1690
+ break;
1691
+ case ERROR_INVALID_FUNCTION:
1692
+ case ERROR_INVALID_PARAMETER:
1693
+ errno = EINVAL;
1694
+ break;
1695
+ case ERROR_NOT_ENOUGH_MEMORY:
1696
+ case ERROR_OUTOFMEMORY:
1697
+ errno = ENOMEM;
1698
+ break;
1699
+ case ERROR_OPERATION_ABORTED:
1700
+ errno = EINTR;
1701
+ break;
1702
+ default:
1703
+ errno = EIO;
1704
+ break;
1705
+ }
1706
+ return -1;
1707
+ #else
1708
+ return pread(fd, data, length, (off_t)position);
1709
+ #endif
1710
+ }
1711
+
1712
+ /* fs.readSync(fd, buffer, offset, length[, position]) — the buffer form.
1650
1713
  * Node validates offset/length against the buffer before reading and
1651
1714
  * throws ERR_OUT_OF_RANGE; here the checks clamp to the same contract and
1652
- * throw the RangeError shape. Returns the byte count read(2) reports;
1653
- * errors carry the errno name like the other fd operations. */
1654
- double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length) {
1715
+ * throw the RangeError shape. Position -1 means the fd's current offset;
1716
+ * nonnegative positions do not advance it. Returns the byte count the OS
1717
+ * reports; errors carry the errno name like the other fd operations. */
1718
+ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length,
1719
+ double position) {
1655
1720
  size_t bytelen = buf->len; /* u8 buffers: elem count == byte count */
1656
1721
  char msg[160];
1657
1722
  int mlen;
@@ -1659,7 +1724,28 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length)
1659
1724
  * form, length against the remaining window (validateOffset vs the
1660
1725
  * buffer-bounds check in fs.readSync). */
1661
1726
  char numbuf[40];
1662
- if (offset < 0 || offset > (double)bytelen) {
1727
+ if (!(isfinite(offset) && trunc(offset) == offset)) {
1728
+ char recv[48];
1729
+ scr_num_received(offset, recv);
1730
+ mlen = snprintf(msg, sizeof msg,
1731
+ "The value of \"offset\" is out of range. It must be an integer. Received %s",
1732
+ recv);
1733
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
1734
+ return 0;
1735
+ }
1736
+ if (offset < 0 || offset > 9007199254740991.0) {
1737
+ numbuf[scr_f64_to_str(offset, numbuf)] = 0;
1738
+ mlen = snprintf(msg, sizeof msg,
1739
+ "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
1740
+ numbuf);
1741
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
1742
+ return 0;
1743
+ }
1744
+ /* Node returns after offset's intrinsic validation when the requested
1745
+ * length normalizes to zero: buffer-window bounds, position, and fd are
1746
+ * not consulted. Preserve the existing size_t coercion's [0, 1) case. */
1747
+ if (length >= 0 && length < 1) return 0;
1748
+ if (offset > (double)bytelen) {
1663
1749
  numbuf[scr_f64_to_str(offset, numbuf)] = 0;
1664
1750
  mlen = snprintf(msg, sizeof msg,
1665
1751
  "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
@@ -1677,7 +1763,27 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length)
1677
1763
  return 0;
1678
1764
  }
1679
1765
  size_t want = (size_t)length;
1680
- ssize_t n = read((int)fd, buf->data + off, want);
1766
+ if (!(isfinite(position) && trunc(position) == position)) {
1767
+ char recv[48];
1768
+ scr_num_received(position, recv);
1769
+ mlen = snprintf(msg, sizeof msg,
1770
+ "The value of \"position\" is out of range. It must be an integer. Received %s",
1771
+ recv);
1772
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
1773
+ return 0;
1774
+ }
1775
+ if (position < -1 || position > 9007199254740991.0) {
1776
+ char recv[48];
1777
+ scr_num_received(position, recv);
1778
+ mlen = snprintf(msg, sizeof msg,
1779
+ "The value of \"position\" is out of range. It must be >= -1 && <= 9007199254740991. Received %s",
1780
+ recv);
1781
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
1782
+ return 0;
1783
+ }
1784
+ ssize_t n = position == -1
1785
+ ? read((int)fd, buf->data + off, want)
1786
+ : scr_fs_pread((int)fd, buf->data + off, want, position);
1681
1787
  if (n < 0) {
1682
1788
  int e = errno;
1683
1789
  char namebuf[16];
@@ -3214,9 +3320,10 @@ double scr_str_last_index_of(ScrStr *s, ScrStr *needle) {
3214
3320
  return -1.0;
3215
3321
  }
3216
3322
 
3217
- /* ── Date, the composed slice ──────────────────────────────────────────
3218
- * Date values have no representation the compiled surface is exactly
3219
- * Date.now() and the composed new Date(ms?).toISOString(). */
3323
+ /* ── Date, the read-only value slice ───────────────────────────────────
3324
+ * Values are TimeClip'd epoch-millisecond scalars. Identity/mutation are
3325
+ * frontend-fenced; construction, storage, getters, and ISO formatting
3326
+ * observe exactly this payload. */
3220
3327
 
3221
3328
  double scr_date_now(void) {
3222
3329
  struct timespec ts;
@@ -3225,6 +3332,16 @@ double scr_date_now(void) {
3225
3332
  return floor((double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6);
3226
3333
  }
3227
3334
 
3335
+ /* Date's TimeClip: non-finite/out-of-range values become Invalid Date,
3336
+ * finite values truncate toward zero, and -0 normalizes to +0. */
3337
+ double scr_date_new_ms(double ms) {
3338
+ if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return NAN;
3339
+ double clipped = trunc(ms);
3340
+ return clipped == 0 ? 0 : clipped;
3341
+ }
3342
+
3343
+ double scr_date_get_time(double ms) { return ms; }
3344
+
3228
3345
  /* Node's Date.prototype.toISOString over a millisecond time value:
3229
3346
  * TimeClip's ToInteger truncation, proleptic Gregorian civil-from-days
3230
3347
  * (Howard Hinnant's algorithm), YYYY-MM-DDTHH:mm:ss.sssZ with expanded
@@ -3291,7 +3408,7 @@ static double scr_days_from_civil(long long y, int m, int d) {
3291
3408
  return (double)(era * 146097 + (long long)doe - 719468);
3292
3409
  }
3293
3410
 
3294
- static double scr_date_ms_of(long long y, int mo, int d, int hh, int mi, int ss, int ms) {
3411
+ static double scr_date_make_ms(long long y, int mo, int d, int hh, int mi, int ss, int ms) {
3295
3412
  /* V8 accepts days 1..31 in every month and ROLLS OVER past the month's
3296
3413
  * end (Feb 30 → Mar 2) — days_from_civil extrapolates linearly, so the
3297
3414
  * rollover falls out; day 0 and 32+ are NaN, like V8. */
@@ -3299,10 +3416,13 @@ static double scr_date_ms_of(long long y, int mo, int d, int hh, int mi, int ss,
3299
3416
  if (hh > 24 || mi > 59 || ss > 59 || (hh == 24 && (mi || ss || ms))) return NAN;
3300
3417
  double t = scr_days_from_civil(y, mo, d) * 86400000.0 +
3301
3418
  hh * 3600000.0 + mi * 60000.0 + ss * 1000.0 + ms;
3302
- if (fabs(t) > 8640000000000000.0) return NAN;
3303
3419
  return t;
3304
3420
  }
3305
3421
 
3422
+ static double scr_date_ms_of(long long y, int mo, int d, int hh, int mi, int ss, int ms) {
3423
+ return scr_date_new_ms(scr_date_make_ms(y, mo, d, hh, mi, ss, ms));
3424
+ }
3425
+
3306
3426
  static bool scr_date_digits(const char **p, const char *end, int n, int *out) {
3307
3427
  int v = 0;
3308
3428
  if (end - *p < n) return false;
@@ -3358,6 +3478,7 @@ double scr_date_parse_get_time(ScrStr *s) {
3358
3478
  bool neg = *p == '-';
3359
3479
  p++;
3360
3480
  if (!scr_date_digits(&p, end, 6, &y6)) return NAN;
3481
+ if (neg && y6 == 0) return NAN; /* ECMA forbids expanded -000000 */
3361
3482
  yy = neg ? -(long long)y6 : y6;
3362
3483
  } else {
3363
3484
  if (!scr_date_digits(&p, end, 4, &y)) return NAN;
@@ -3393,14 +3514,18 @@ double scr_date_parse_get_time(ScrStr *s) {
3393
3514
  p++;
3394
3515
  if (!scr_date_digits(&p, end, 2, &oh) || p >= end || *p++ != ':') return NAN;
3395
3516
  if (!scr_date_digits(&p, end, 2, &om)) return NAN;
3517
+ if (oh > 23 || om > 59) return NAN;
3396
3518
  off = (oh * 60 + om) * 60000.0;
3397
3519
  if (neg) off = -off;
3398
3520
  } else {
3399
3521
  return NAN;
3400
3522
  }
3401
3523
  if (p != end) return NAN;
3402
- double t = scr_date_ms_of(yy, mo, d, hh, mi, ss, ms);
3403
- return isnan(t) ? t : t - off;
3524
+ /* MakeDate can lie just beyond the TimeClip boundary while the
3525
+ * explicit offset brings the final UTC instant back into range. The
3526
+ * spec clips only after that offset has been applied. */
3527
+ double t = scr_date_make_ms(yy, mo, d, hh, mi, ss, ms);
3528
+ return scr_date_new_ms(t - off);
3404
3529
  }
3405
3530
  }
3406
3531
 
@@ -3412,10 +3537,10 @@ double scr_date_parse_get_time(ScrStr *s) {
3412
3537
  * 1900+year (the spec's MakeFullYear), out-of-range months ROLL into the
3413
3538
  * year (Date.UTC(2017, 13) is Feb 2018) and any integer date offsets from
3414
3539
  * day 1 of that month — days_from_civil extrapolates linearly, so both
3415
- * rollovers fall out. V8 bounds MakeDay's year to ±1e6 (kMaxYear/kMinYear,
3416
- * date.h) before TimeClip can see the result, and Node answers NaN past
3417
- * it matched here, and it keeps days_from_civil's long long exact.
3418
- * Never throws. */
3540
+ * rollovers fall out. V8 bounds MakeDay's input year to ±1e6 and input
3541
+ * month to ±1e7 before normalizing the month (kMaxYear/kMinYear and
3542
+ * kMaxMonth/kMinMonth, date.h); Node answers NaN past either bound even
3543
+ * when the two inputs would normalize back into range. Never throws. */
3419
3544
  double scr_date_utc(double y, double mo, double d,
3420
3545
  double h, double mi, double s, double ms) {
3421
3546
  if (!isfinite(y) || !isfinite(mo) || !isfinite(d) || !isfinite(h) ||
@@ -3429,16 +3554,150 @@ double scr_date_utc(double y, double mo, double d,
3429
3554
  mi = trunc(mi);
3430
3555
  s = trunc(s);
3431
3556
  ms = trunc(ms);
3557
+ if (fabs(y) > 1000000.0 || fabs(mo) > 10000000.0) return NAN;
3432
3558
  if (y >= 0 && y <= 99) y += 1900;
3433
3559
  double ym = y + floor(mo / 12.0);
3434
3560
  int mn = (int)(mo - floor(mo / 12.0) * 12.0); /* 0..11 */
3435
- if (fabs(ym) > 1000000.0) return NAN; /* V8's MakeDay year bound */
3436
3561
  double days = scr_days_from_civil((long long)ym, mn + 1, 1) + (d - 1.0);
3437
3562
  double t = days * 86400000.0 + h * 3600000.0 + mi * 60000.0 + s * 1000.0 + ms;
3438
3563
  if (fabs(t) > 8640000000000000.0) return NAN; /* TimeClip */
3439
3564
  return t == 0 ? 0 : t; /* normalize -0 (TimeClip's +0) */
3440
3565
  }
3441
3566
 
3567
+ /* ── Date calendar getters ────────────────────────────────────────────
3568
+ * UTC fields use the same proleptic-Gregorian walk as toISOString, so the
3569
+ * whole Date range is portable. Local fields use the host timezone via
3570
+ * localtime, exactly the environment the sibling Node process observes;
3571
+ * a libc that cannot represent an extreme instant answers NaN rather than
3572
+ * inventing a zone. */
3573
+
3574
+ typedef struct ScrDateParts {
3575
+ long long year;
3576
+ int month, date, day, hours, minutes, seconds, milliseconds;
3577
+ double timezone_offset;
3578
+ } ScrDateParts;
3579
+
3580
+ /* Calendar decomposition itself also serves LocalTime(t), which can lie
3581
+ * just outside TimeClip's UTC interval after applying a zone offset at an
3582
+ * endpoint. The checked wrapper is the public UTC-getter gate; the inner
3583
+ * walk accepts those bounded local-time values too. */
3584
+ static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
3585
+ double dayd = floor(t / 86400000.0);
3586
+ long long msday = (long long)(t - dayd * 86400000.0);
3587
+ long long z = (long long)dayd + 719468;
3588
+ long long era = (z >= 0 ? z : z - 146096) / 146097;
3589
+ unsigned long long doe = (unsigned long long)(z - era * 146097);
3590
+ unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
3591
+ long long y = (long long)yoe + era * 400;
3592
+ unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
3593
+ unsigned long long mp = (5 * doy + 2) / 153;
3594
+ unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
3595
+ unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
3596
+ if (m <= 2) y++;
3597
+ long long wday = ((long long)dayd + 4) % 7;
3598
+ if (wday < 0) wday += 7;
3599
+ out->year = y;
3600
+ out->month = (int)m - 1;
3601
+ out->date = (int)d;
3602
+ out->day = (int)wday;
3603
+ out->hours = (int)(msday / 3600000);
3604
+ out->minutes = (int)(msday / 60000 % 60);
3605
+ out->seconds = (int)(msday / 1000 % 60);
3606
+ out->milliseconds = (int)(msday % 1000);
3607
+ out->timezone_offset = 0;
3608
+ }
3609
+
3610
+ static bool scr_date_utc_parts(double ms, ScrDateParts *out) {
3611
+ if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return false;
3612
+ scr_date_utc_parts_unchecked(trunc(ms), out);
3613
+ return true;
3614
+ }
3615
+
3616
+ static bool scr_date_localtime(double secd, struct tm *out) {
3617
+ time_t sec = (time_t)secd;
3618
+ if ((double)sec != secd) return false;
3619
+ #ifdef _WIN32
3620
+ return localtime_s(out, &sec) == 0;
3621
+ #else
3622
+ return localtime_r(&sec, out) != NULL;
3623
+ #endif
3624
+ }
3625
+
3626
+ static bool scr_date_local_parts(double ms, ScrDateParts *out) {
3627
+ if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return false;
3628
+ double clipped = trunc(ms);
3629
+ double secd = floor(clipped / 1000.0);
3630
+ struct tm tmv;
3631
+ double basis_secd = secd;
3632
+ if (!scr_date_localtime(basis_secd, &tmv)) {
3633
+ /* Windows' _localtime64_s rejects pre-epoch instants and years after
3634
+ * 3001 even though both are valid ECMAScript Dates. Query the host's
3635
+ * zone rule at a calendar-equivalent surrogate year in 2000..2399:
3636
+ * Gregorian weekdays/leap years repeat every 400 years. This keeps
3637
+ * every valid Date finite; the OS-vs-Node historical-rule difference
3638
+ * remains the documented timezone-data divergence. */
3639
+ ScrDateParts utc;
3640
+ scr_date_utc_parts_unchecked(clipped, &utc);
3641
+ long long cycle_year = (utc.year - 2000) % 400;
3642
+ if (cycle_year < 0) cycle_year += 400;
3643
+ long long surrogate_year = 2000 + cycle_year;
3644
+ basis_secd =
3645
+ scr_days_from_civil(surrogate_year, utc.month + 1, utc.date) * 86400.0 +
3646
+ utc.hours * 3600.0 + utc.minutes * 60.0 + utc.seconds;
3647
+ if (!scr_date_localtime(basis_secd, &tmv)) return false;
3648
+ }
3649
+ /* Treat the local broken-down fields as UTC. Its distance from the real
3650
+ * (or surrogate) epoch second is the zone offset. Apply that offset to
3651
+ * the original instant and use the portable Gregorian walk for its
3652
+ * fields; Date#getTimezoneOffset uses the
3653
+ * opposite sign (UTC - local), in whole minutes. Historical local-mean
3654
+ * offsets can contain seconds, which JavaScript truncates toward zero. */
3655
+ double local_as_utc =
3656
+ scr_days_from_civil((long long)tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday) * 86400.0 +
3657
+ tmv.tm_hour * 3600.0 + tmv.tm_min * 60.0 + tmv.tm_sec;
3658
+ double local_offset = local_as_utc - basis_secd;
3659
+ scr_date_utc_parts_unchecked(clipped + local_offset * 1000.0, out);
3660
+ double timezone_offset = trunc(-local_offset / 60.0);
3661
+ out->timezone_offset = timezone_offset == 0 ? 0 : timezone_offset;
3662
+ return true;
3663
+ }
3664
+
3665
+ static bool scr_date_parts(double ms, bool utc, ScrDateParts *out) {
3666
+ return utc ? scr_date_utc_parts(ms, out) : scr_date_local_parts(ms, out);
3667
+ }
3668
+
3669
+ #define SCR_DATE_PART_GETTER(name, field) \
3670
+ double scr_date_get_##name(double ms, bool utc) { \
3671
+ ScrDateParts p; \
3672
+ return scr_date_parts(ms, utc, &p) ? (double)p.field : NAN; \
3673
+ } \
3674
+ double scr_date_get_##name##_local(double ms) { \
3675
+ return scr_date_get_##name(ms, false); \
3676
+ } \
3677
+ double scr_date_get_##name##_utc(double ms) { \
3678
+ return scr_date_get_##name(ms, true); \
3679
+ }
3680
+
3681
+ SCR_DATE_PART_GETTER(full_year, year)
3682
+ SCR_DATE_PART_GETTER(month, month)
3683
+ SCR_DATE_PART_GETTER(date, date)
3684
+ SCR_DATE_PART_GETTER(day, day)
3685
+ SCR_DATE_PART_GETTER(hours, hours)
3686
+ SCR_DATE_PART_GETTER(minutes, minutes)
3687
+ SCR_DATE_PART_GETTER(seconds, seconds)
3688
+
3689
+ #undef SCR_DATE_PART_GETTER
3690
+
3691
+ double scr_date_get_milliseconds(double ms) {
3692
+ ScrDateParts p;
3693
+ return scr_date_utc_parts(ms, &p) ? (double)p.milliseconds : NAN;
3694
+ }
3695
+
3696
+ double scr_date_get_timezone_offset(double ms) {
3697
+ ScrDateParts p;
3698
+ return scr_date_local_parts(ms, &p) ? p.timezone_offset : NAN;
3699
+ }
3700
+
3442
3701
  /* ── Number statics ────────────────────────────────────────────────────
3443
3702
  * JS-exact: the ES2015 Number statics never coerce (unlike the global
3444
3703
  * isNaN/isFinite), and the compiler routes only number-typed arguments
package/src/scr_runtime.h CHANGED
@@ -197,9 +197,15 @@ void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len);
197
197
  * collection walks the buffer (markGray: trial-decrement internal edges;
198
198
  * scan: restore externally-referenced subgraphs; collectWhite: free the
199
199
  * dead cycle members, releasing only edges that LEAVE the white set).
200
- * Collection points: program exit (before the RC audit), event-loop
201
- * quiescence, and a root-buffer threshold (SCR_CYCLE_THRESHOLD env var,
202
- * default 256). There is no concurrent or incremental collection.
200
+ * It is GENERATIONAL: each header carries a generation, a pass names the
201
+ * oldest one it will walk, and objects that survive a pass are promoted out
202
+ * of the nursery so later nursery passes never re-walk them. That is what
203
+ * keeps a pass proportional to recent allocation rather than to the whole
204
+ * live heap — see the generation note in scr_cycle.c for the soundness
205
+ * argument and the schedule. Collection points: program exit (before the RC
206
+ * audit), event-loop quiescence, and the per-generation triggers.
207
+ * SCR_CYCLE_THRESHOLD pins the nursery trigger to a fixed candidate count.
208
+ * There is no concurrent or incremental collection.
203
209
  *
204
210
  * Contract for trace/teardown pairs (the compiler emits them for shapes,
205
211
  * the runtime owns its own): trace(obj) visits exactly the strong
@@ -214,16 +220,44 @@ typedef void (*ScrTraceVisit)(void *child, void *ctx);
214
220
  typedef void (*ScrTraceFn)(void *obj, ScrTraceVisit visit, void *ctx);
215
221
  typedef void (*ScrCycFreeFn)(void *obj);
216
222
 
217
- enum { SCR_CYC_BLACK = 0, SCR_CYC_PURPLE = 1, SCR_CYC_GRAY = 2, SCR_CYC_WHITE = 3 };
223
+ /* DOOMED is WHITE that collectWhite has already gathered: it stops the
224
+ * gather recursing twice, and distinguishes "about to be freed" from a
225
+ * survivor for the re-buffering step (see scr_cycle.c). */
226
+ enum {
227
+ SCR_CYC_BLACK = 0, SCR_CYC_PURPLE = 1, SCR_CYC_GRAY = 2, SCR_CYC_WHITE = 3,
228
+ SCR_CYC_DOOMED = 4
229
+ };
230
+
231
+ /* Generations. A candidate sits in the buffer named by its own `gen`, and a
232
+ * pass walks only objects at or below the generation it collects. */
233
+ enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_NGENS = 2 };
218
234
 
219
235
  typedef struct ScrCycHdr {
220
236
  ScrTraceFn trace;
221
237
  ScrCycFreeFn free_fn;
222
238
  uint32_t color; /* SCR_CYC_* */
223
- uint32_t buffered; /* 1 = sitting in the candidate-root buffer */
239
+ uint16_t buffered; /* 1 = sitting in its generation's candidate buffer */
240
+ uint16_t gen; /* SCR_CYC_NURSERY..SCR_CYC_MATURE (the walk filter) */
224
241
  size_t buf_index; /* position there (O(1) removal when rc hits 0) */
225
242
  } ScrCycHdr;
226
243
 
244
+ /* This layout is an ABI, not an implementation detail: the LLVM backend
245
+ * inlines scr_cyc_mark_live as a raw `store i32 0` at obj-16 (it is a
246
+ * static inline here, so there is no symbol to call) and reaches the header
247
+ * at obj-32. Three sites emit it — llvm/shapes.ts, llvm/classes.ts,
248
+ * llvm/emitter.ts. Nothing but `color` may share those four bytes: a field
249
+ * placed in them is silently zeroed by every retain, which is invisible to
250
+ * the type system and to the C compiler. Hence the assertions. */
251
+ _Static_assert(sizeof(ScrCycHdr) == 32, "LLVM backend reads the header at obj-32");
252
+ _Static_assert(offsetof(ScrCycHdr, color) == 16,
253
+ "LLVM backend's inlined mark-live stores i32 0 at obj-16");
254
+ _Static_assert(sizeof(((ScrCycHdr *)0)->color) == 4,
255
+ "mark-live is an i32 store: color must own all four bytes");
256
+ _Static_assert(SCR_CYC_BLACK == 0,
257
+ "the emitted mark-live stores the LITERAL 0, not the enumerator "
258
+ "— reordering the colors would make every compiled retain write "
259
+ "the wrong one");
260
+
227
261
  static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; }
228
262
 
229
263
  /* Zeroed allocation with a cycle header in front; returns the OBJECT
@@ -243,9 +277,18 @@ static inline void scr_cyc_mark_live(void *obj) {
243
277
  scr_cyc_hdr(obj)->color = SCR_CYC_BLACK;
244
278
  }
245
279
 
246
- /* Run one full trial-deletion pass over the buffered candidates now. */
280
+ /* Full sweep: trial-deletion passes over EVERY generation, to a fixpoint.
281
+ * This is the exit / session-reset entry point (the RC audit runs straight
282
+ * after and wants nothing reclaimable left), and it costs a walk of the
283
+ * live heap — do not put it on a per-turn path. */
247
284
  void scr_collect_cycles(void);
248
285
 
286
+ /* One pass on the normal generational schedule, for callers that reach a
287
+ * natural collection point rather than a threshold (the event loop between
288
+ * turns). Cheap: usually a nursery pass; a waiting mature backlog ages into
289
+ * a bounded full pass so sparse roots cannot float forever. */
290
+ void scr_cyc_collect_scheduled(void);
291
+
249
292
  /* ── class hierarchies (single inheritance) ───────────────────────────
250
293
  * Classes in an `extends` hierarchy share a two-word object prefix: the
251
294
  * usual `size_t rc`, then a pointer to the class's static vtable (emitted
@@ -2416,7 +2459,10 @@ ScrStr *scr_path_win32_to_namespaced_path(ScrStr *path);
2416
2459
  * (failure throws the path-less "EBADF: bad file descriptor, close").
2417
2460
  * The pair behind spawn's fd-stdio form. */
2418
2461
  double scr_fs_open(ScrStr *path, ScrStr *flags);
2419
- double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length);
2462
+ /* position == -1 reads from and advances the descriptor's current offset;
2463
+ * nonnegative positions leave that offset unchanged (pread/ReadFile seam). */
2464
+ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length,
2465
+ double position);
2420
2466
  void scr_fs_close(double fd);
2421
2467
 
2422
2468
  /* ── WHATWG URL (scr_url.c) ──────────────────────────────────────────
@@ -2714,6 +2760,7 @@ typedef enum {
2714
2760
  SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */
2715
2761
  SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */
2716
2762
  SCR_DYNH_EVENT, /* native static-fetch abort Event */
2763
+ SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */
2717
2764
  SCR_DYNH_COUNT,
2718
2765
  } ScrDynHandleTag;
2719
2766
 
@@ -2980,6 +3027,7 @@ ScrStr *scr_dyn_string_coerce(const ScrDyn *d);
2980
3027
  * called, their throws propagating) — the WHATWG USVString conversions.
2981
3028
  * Borrows; +1 or NULL with the exception pending. */
2982
3029
  ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d);
3030
+ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out);
2983
3031
 
2984
3032
  /* `d instanceof TypeError` (and the other builtin error classes) on a
2985
3033
  * checked-dynamic value: the from_error cache resolves the dyn encoding
@@ -3907,9 +3955,11 @@ long scr_promise_live_count(void);
3907
3955
  * emitted main calls scr_fetch_install in either form. */
3908
3956
  void scr_fetch_install(void);
3909
3957
  ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise<Response handle> */
3958
+ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */
3910
3959
  ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
3911
3960
  ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
3912
3961
  ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise<dyn> */
3962
+ ScrDyn *scr_fetch_abort_controller_new(void); /* +1 AbortController handle */
3913
3963
  /* Borrowed number; +1 AbortSignal handle or NULL pending. */
3914
3964
  ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay);
3915
3965
  ScrDyn *scr_fetch_abort_now(ScrDyn *reason); /* borrowed reason; +1 handle */
@@ -4398,10 +4448,13 @@ bool scr_num_same_value(double a, double b);
4398
4448
  * en-US is the one embedded locale. Result +1; never throws. */
4399
4449
  ScrStr *scr_intl_num_format_en_us(double x);
4400
4450
 
4401
- /* ── Date, the composed slice (scr_lib.c) ─────────────────────────────
4402
- * Date values have no representation; the runtime surface is exactly
4403
- * Date.now() and toISOString over a millisecond time value. */
4451
+ /* ── Date, the read-only value slice (scr_lib.c) ──────────────────────
4452
+ * A Date value is its TimeClip'd epoch-millisecond scalar. Identity and
4453
+ * mutation are frontend-fenced; construction, storage, getters, and ISO
4454
+ * formatting are exact over this representation. */
4404
4455
  double scr_date_now(void); /* integer ms since epoch, like Node */
4456
+ double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */
4457
+ double scr_date_get_time(double ms);
4405
4458
  /* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside
4406
4459
  * 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or
4407
4460
  * |ms| > 8.64e15 and returns NULL; +1 otherwise. */
@@ -4415,6 +4468,30 @@ double scr_date_parse_get_time(ScrStr *s);
4415
4468
  * and years past V8's ±1e6 MakeDay bound. Never throws. */
4416
4469
  double scr_date_utc(double y, double mo, double d,
4417
4470
  double h, double mi, double s, double ms);
4471
+ double scr_date_get_full_year(double ms, bool utc);
4472
+ double scr_date_get_month(double ms, bool utc);
4473
+ double scr_date_get_date(double ms, bool utc);
4474
+ double scr_date_get_day(double ms, bool utc);
4475
+ double scr_date_get_hours(double ms, bool utc);
4476
+ double scr_date_get_minutes(double ms, bool utc);
4477
+ double scr_date_get_seconds(double ms, bool utc);
4478
+ double scr_date_get_milliseconds(double ms);
4479
+ double scr_date_get_timezone_offset(double ms);
4480
+ /* One-argument ABI wrappers used by the LLVM lib-call table. */
4481
+ double scr_date_get_full_year_local(double ms);
4482
+ double scr_date_get_full_year_utc(double ms);
4483
+ double scr_date_get_month_local(double ms);
4484
+ double scr_date_get_month_utc(double ms);
4485
+ double scr_date_get_date_local(double ms);
4486
+ double scr_date_get_date_utc(double ms);
4487
+ double scr_date_get_day_local(double ms);
4488
+ double scr_date_get_day_utc(double ms);
4489
+ double scr_date_get_hours_local(double ms);
4490
+ double scr_date_get_hours_utc(double ms);
4491
+ double scr_date_get_minutes_local(double ms);
4492
+ double scr_date_get_minutes_utc(double ms);
4493
+ double scr_date_get_seconds_local(double ms);
4494
+ double scr_date_get_seconds_utc(double ms);
4418
4495
 
4419
4496
  /* ── bitwise operators (scr_lib.c) ────────────────────────────────────
4420
4497
  * JS-exact ToInt32/ToUint32 semantics: NaN/±Infinity → 0, truncation