@scriptc/runtime 0.0.8 → 0.0.10

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_lib.c CHANGED
@@ -1347,7 +1347,27 @@ static double scr_net_autosel_timeout_ms = 250;
1347
1347
 
1348
1348
  double scr_net_get_autosel_timeout(void) { return scr_net_autosel_timeout_ms; }
1349
1349
 
1350
- void scr_net_set_autosel_timeout(double ms) { scr_net_autosel_timeout_ms = ms; }
1350
+ /* Node's setDefaultAutoSelectFamilyAttemptTimeout: validateInt32(value,
1351
+ * 'value', 1), then the sub-10ms floor (Node clamps small budgets to
1352
+ * 10ms). Throws ERR_OUT_OF_RANGE catchably. */
1353
+ void scr_net_set_autosel_timeout(double ms) {
1354
+ char recv[48], msg[160];
1355
+ if (!(isfinite(ms) && trunc(ms) == ms)) {
1356
+ scr_num_received(ms, recv);
1357
+ int len = snprintf(msg, sizeof msg,
1358
+ "The value of \"value\" is out of range. It must be an integer. Received %s", recv);
1359
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1360
+ return;
1361
+ }
1362
+ if (ms < 1 || ms > 2147483647.0) {
1363
+ scr_num_received(ms, recv);
1364
+ int len = snprintf(msg, sizeof msg,
1365
+ "The value of \"value\" is out of range. It must be >= 1 && <= 2147483647. Received %s", recv);
1366
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1367
+ return;
1368
+ }
1369
+ scr_net_autosel_timeout_ms = ms < 10 ? 10 : ms;
1370
+ }
1351
1371
 
1352
1372
  /* ── fs error formatting ─────────────────────────────────────────────
1353
1373
  * Node's fs errors read "<ERRNO>: <text>, <syscall> '<path>'"
@@ -1647,7 +1667,7 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length)
1647
1667
  mlen = snprintf(msg, sizeof msg,
1648
1668
  "The value of \"offset\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s",
1649
1669
  numbuf);
1650
- scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)mlen);
1670
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
1651
1671
  return 0;
1652
1672
  }
1653
1673
  size_t off = (size_t)offset;
@@ -1656,7 +1676,7 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length)
1656
1676
  mlen = snprintf(msg, sizeof msg,
1657
1677
  "The value of \"length\" is out of range. It must be <= %zu. Received %s",
1658
1678
  bytelen - off, numbuf);
1659
- scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)mlen);
1679
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
1660
1680
  return 0;
1661
1681
  }
1662
1682
  size_t want = (size_t)length;
@@ -2528,7 +2548,7 @@ ScrStr *scr_crypto_random_string(double n, ScrStr *enc) {
2528
2548
  int mlen = snprintf(msg, sizeof msg,
2529
2549
  "The value of \"size\" is out of range. It must be >= 0 && <= 2147483647. Received %.*s",
2530
2550
  (int)numlen, num);
2531
- scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)mlen);
2551
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
2532
2552
  return NULL;
2533
2553
  }
2534
2554
  size_t size = (size_t)n;
@@ -3486,6 +3506,104 @@ ScrStr *scr_num_to_fixed0(double x) {
3486
3506
  return r;
3487
3507
  }
3488
3508
 
3509
+ /* Increment a decimal digit string in place. Returns true on overflow —
3510
+ * the value becomes 1 followed by len zeros (the caller folds the zeros
3511
+ * into its scale); an EMPTY string increments to "1" the same way (the
3512
+ * round-up-from-nothing case: 0.0005 at 3 fraction digits). */
3513
+ static bool scr_dec_inc(char *d, int len) {
3514
+ for (int i = len - 1; i >= 0; i--) {
3515
+ if (d[i] != '9') {
3516
+ d[i]++;
3517
+ return false;
3518
+ }
3519
+ d[i] = '0';
3520
+ }
3521
+ d[0] = '1';
3522
+ return true;
3523
+ }
3524
+
3525
+ /* Intl.NumberFormat("en-US").format(x) / x.toLocaleString("en-US") with
3526
+ * DEFAULT options: decimal notation, minimum 0 / maximum 3 fraction
3527
+ * digits, "," grouping every three integer digits, "∞"/"NaN" texts, and
3528
+ * "-0" whenever the input is negative or negative zero even after
3529
+ * rounding to zero. Rounding is half-up ON THE SHORTEST ROUND-TRIPPING
3530
+ * DECIMAL — ICU's rounding input, probed against Node: format(1.0005) is
3531
+ * "1.001" although the double is 1.000499... and toFixed(3) answers
3532
+ * "1.000"; format(1e23) prints the shortest form's trailing zeros, not
3533
+ * the double's exact expansion. The en-US/latn symbols (",", ".", "∞",
3534
+ * "NaN", group size 3) are the whole embedded locale surface. Verified
3535
+ * differentially against Node. Result +1; never throws. */
3536
+ ScrStr *scr_intl_num_format_en_us(double x) {
3537
+ if (isnan(x)) return scr_str_new("NaN", 3);
3538
+ if (isinf(x)) {
3539
+ return x < 0 ? scr_str_new("-\xE2\x88\x9E", 4) : scr_str_new("\xE2\x88\x9E", 3);
3540
+ }
3541
+ bool neg = signbit(x) != 0;
3542
+ if (x == 0) return neg ? scr_str_new("-0", 2) : scr_str_new("0", 1);
3543
+ double a = neg ? -x : x;
3544
+
3545
+ /* Shortest digits: value = 0.d × 10^n (no trailing zeros, k ≤ 17). */
3546
+ char d[18];
3547
+ int n;
3548
+ int k = scr_f64_digits(a, d, &n);
3549
+
3550
+ /* Round at 3 fraction digits: fraction position p is digit index
3551
+ * n+p-1, so index n+3 is the first DROPPED digit. Half-up on the
3552
+ * decimal digits — the shortest string ends right after them, so
3553
+ * "first dropped digit ≥ 5" IS the whole decision. */
3554
+ int keep = n + 3;
3555
+ if (keep < k) {
3556
+ bool up = keep >= 0 && d[keep] >= '5';
3557
+ k = keep < 0 ? 0 : keep;
3558
+ if (up && scr_dec_inc(d, k)) {
3559
+ /* Carried out (all nines, or the round-up-from-nothing 0.0005
3560
+ * case): one leading 1, the dropped nines fold into the scale. */
3561
+ k = 1;
3562
+ n += 1;
3563
+ } else if (k == 0) {
3564
+ /* Everything rounded away: ±0 with the sign preserved. */
3565
+ return neg ? scr_str_new("-0", 2) : scr_str_new("0", 1);
3566
+ }
3567
+ }
3568
+
3569
+ /* Assemble: integer digits (indices [0, n)), zero-padded past k, then
3570
+ * the ≤ 3 fraction digits (indices n..n+2, '0' outside [0, k)) with
3571
+ * trailing zeros trimmed, then commas every three integer digits. */
3572
+ char frac[3];
3573
+ int flen = 0;
3574
+ for (int p = 1; p <= 3; p++) {
3575
+ int idx = n + p - 1;
3576
+ frac[flen++] = (idx >= 0 && idx < k) ? d[idx] : '0';
3577
+ }
3578
+ while (flen > 0 && frac[flen - 1] == '0') flen--;
3579
+
3580
+ char out[512];
3581
+ int o = 0;
3582
+ if (neg) out[o++] = '-';
3583
+ if (n <= 0) {
3584
+ out[o++] = '0';
3585
+ } else {
3586
+ for (int i = 0; i < n; i++) {
3587
+ if (i > 0 && (n - i) % 3 == 0) out[o++] = ',';
3588
+ out[o++] = (i < k) ? d[i] : '0';
3589
+ }
3590
+ }
3591
+ if (flen > 0) {
3592
+ out[o++] = '.';
3593
+ memcpy(out + o, frac, (size_t)flen);
3594
+ o += flen;
3595
+ }
3596
+ return scr_str_new(out, (size_t)o);
3597
+ }
3598
+
3599
+ /* Object.is over two numbers — the spec's SameValue on doubles: NaN
3600
+ * equals NaN, +0 differs from -0, everything else is ==. */
3601
+ bool scr_num_same_value(double a, double b) {
3602
+ if (a != a) return b != b;
3603
+ if (a == 0 && b == 0) return signbit(a) == signbit(b);
3604
+ return a == b;
3605
+ }
3606
+
3489
3607
  bool scr_num_is_nan(double x) { return isnan(x) != 0; }
3490
3608
 
3491
3609
  bool scr_num_is_integer(double x) { return isfinite(x) && trunc(x) == x; }
package/src/scr_library.c CHANGED
@@ -40,7 +40,19 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
40
40
  * conforming survival pattern), then deliver exactly once, then abort:
41
41
  * before registration, or if the sink returns (the ruled host-contract
42
42
  * violation). The address is the funnel frame's return address — the trap
43
- * site — 0 where the toolchain cannot supply one. */
43
+ * site — 0 where the toolchain cannot supply one.
44
+ *
45
+ * Delivery shape (the ratified structured trap-teaching encoding): a
46
+ * message that already begins with the 0x01 marker — a facade-authored
47
+ * structured throw riding the verbatim rule, or the wrapper's compile-
48
+ * time-assembled SC4012 contract trap — is delivered byte-for-byte. Every
49
+ * OTHER message is a trap the runtime DETECTED, and the funnel assembles
50
+ * it here into 0x01 text 0x1F code 0x1F symbol [0x1F remediation]: the
51
+ * baseline human line becomes field 0 unchanged (so plain-text hosts read
52
+ * exactly what they always read), the code classifies the trap kind, the
53
+ * symbol is the entry the trapping call came through (recorded by the
54
+ * entry prologue below), and the remediation is the profile's for that
55
+ * code when the program TU's overlay table declares one. */
44
56
 
45
57
  #if defined(__GNUC__) || defined(__clang__)
46
58
  #define SCR_TRAP_ADDR() ((uint64_t)(uintptr_t)__builtin_return_address(0))
@@ -48,8 +60,95 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
48
60
  #define SCR_TRAP_ADDR() ((uint64_t)0)
49
61
  #endif
50
62
 
63
+ /* The current-entry slot: every generated entry's prologue records its
64
+ * external symbol before dispatching into core code. A single static slot
65
+ * is sound — exactly one core is live per process (the one-live-core rule),
66
+ * entries never nest, and a trap can only fire while an entry is on the
67
+ * stack. NULL (never entered) renders as the empty symbol field. */
68
+ static const char *scr_library_entry_symbol = NULL;
69
+
70
+ /* Detected-trap classification: the runtime's trap sites self-classify
71
+ * through their message conventions (the exact bytes the executable lane
72
+ * prints), so the kind → code mapping keys on those prefixes. The codes are
73
+ * the compiler registry's runtime family (diagnostics/diagnostic.ts —
74
+ * documented beside SC4012); SC4019 is the family's residual for detected
75
+ * traps outside the named kinds (environment failures, unsupported
76
+ * operations, the RC audit). */
77
+ static const struct {
78
+ const char *prefix;
79
+ const char *code;
80
+ } scr_library_trap_kinds[] = {
81
+ {"Uncaught ", "SC4013"}, /* escaped exception at an entry */
82
+ {"scriptc: RangeError: ", "SC4014"}, /* range trap */
83
+ {"scriptc: TypeError: ", "SC4015"}, /* type trap */
84
+ {"scriptc: SyntaxError: ", "SC4016"}, /* syntax trap (regex compile) */
85
+ {"scriptc: out of memory", "SC4017"}, /* allocation failure */
86
+ {"scriptc: internal error: ", "SC4018"}, /* internal invariant failure */
87
+ };
88
+
89
+ static const char *scr_library_trap_code(const char *msg, size_t len) {
90
+ for (size_t i = 0; i < sizeof scr_library_trap_kinds / sizeof scr_library_trap_kinds[0]; i++) {
91
+ size_t plen = strlen(scr_library_trap_kinds[i].prefix);
92
+ if (len >= plen && memcmp(msg, scr_library_trap_kinds[i].prefix, plen) == 0) {
93
+ return scr_library_trap_kinds[i].code;
94
+ }
95
+ }
96
+ return "SC4019"; /* other detected trap */
97
+ }
98
+
51
99
  static _Noreturn void scr_library_trap_deliver(const char *msg, size_t len, uint64_t addr) {
52
100
  scr_library_poisoned = true;
101
+ if (!(len > 0 && (uint8_t)msg[0] == 0x01)) {
102
+ /* A detected trap: assemble the structured message. Static buffer —
103
+ * no malloc on the failure path; text truncates before structure ever
104
+ * would (codes and symbols are short; an oversized remediation drops
105
+ * whole, never split). */
106
+ static char buf[2048];
107
+ const char *code = scr_library_trap_code(msg, len);
108
+ const char *text = msg;
109
+ size_t text_len = len;
110
+ const char *rem = NULL;
111
+ for (size_t i = 0; i < scr_library_trap_overlays_len; i++) {
112
+ const char *const *t = &scr_library_trap_overlays[3 * i];
113
+ if (strcmp(t[0], code) == 0) {
114
+ if (t[1] != NULL) {
115
+ text = t[1];
116
+ text_len = strlen(t[1]);
117
+ }
118
+ rem = t[2];
119
+ break;
120
+ }
121
+ }
122
+ const char *sym = scr_library_entry_symbol != NULL ? scr_library_entry_symbol : "";
123
+ size_t tail = 2 + strlen(code) + strlen(sym);
124
+ if (rem != NULL) {
125
+ if (tail + 1 + strlen(rem) > sizeof buf - 1) rem = NULL; /* drop whole, keep structure */
126
+ else tail += 1 + strlen(rem);
127
+ }
128
+ size_t n = 0;
129
+ buf[n++] = '\x01';
130
+ size_t cap = sizeof buf - 1 - tail;
131
+ if (text_len > cap) text_len = cap;
132
+ for (size_t i = 0; i < text_len; i++) {
133
+ /* The encoding reserves 0x01/0x1F; runtime messages never contain
134
+ * them, but an escaped exception's rendered text embeds user bytes. */
135
+ char c = text[i];
136
+ buf[n++] = (c == '\x01' || c == '\x1f') ? ' ' : c;
137
+ }
138
+ buf[n++] = '\x1f';
139
+ memcpy(buf + n, code, strlen(code));
140
+ n += strlen(code);
141
+ buf[n++] = '\x1f';
142
+ memcpy(buf + n, sym, strlen(sym));
143
+ n += strlen(sym);
144
+ if (rem != NULL) {
145
+ buf[n++] = '\x1f';
146
+ memcpy(buf + n, rem, strlen(rem));
147
+ n += strlen(rem);
148
+ }
149
+ msg = buf;
150
+ len = n;
151
+ }
53
152
  if (scr_library_sink != NULL) {
54
153
  scr_library_sink(scr_library_sink_ctx, (const uint8_t *)msg, len, addr);
55
154
  }
@@ -80,7 +179,11 @@ __attribute__((noinline)) _Noreturn void scr_trap_fmt(const char *fmt, ...) {
80
179
 
81
180
  /* ── entry prologues ──────────────────────────────────────────────────── */
82
181
 
83
- void scr_library_entry(bool reset_arena) {
182
+ void scr_library_entry(bool reset_arena, const char *entry_symbol) {
183
+ /* Record the entry symbol FIRST so even a poisoned-abort's core dump
184
+ * names the entry; a trap anywhere below (the arena reset's OOM
185
+ * included) then reports the right symbol. */
186
+ scr_library_entry_symbol = entry_symbol;
84
187
  /* A poisoned library's entries abort deterministically — never through the
85
188
  * sink again (it received its exactly-once message when the trap fired),
86
189
  * never into a heap whose invariants already failed. */
package/src/scr_net.c CHANGED
@@ -92,6 +92,7 @@
92
92
  #include "scr_runtime.h"
93
93
 
94
94
  #include <errno.h>
95
+ #include <math.h>
95
96
  #include <fcntl.h>
96
97
  #include <stdio.h>
97
98
  #include <stdlib.h>
@@ -1601,6 +1602,117 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/,
1601
1602
  return s;
1602
1603
  }
1603
1604
 
1605
+ /* ── the connect option-bag validation ladders (checked-dynamic lane) ──
1606
+ * Node-order validation over DOM option values with Node's exact typed
1607
+ * errors; the honest tail (connect for the validated forms, the
1608
+ * compiler-rendered fence for bags with unmodeled keys) runs only after
1609
+ * every validation passes. */
1610
+
1611
+ static bool scr_net_attempt_timeout_chk(const ScrDyn *t, const char *name) {
1612
+ if (t->kind != SCR_DYN_NUM) {
1613
+ scr_dyn_prop_type_fail(name, "of type number", t);
1614
+ return false;
1615
+ }
1616
+ char recv[48], msg[192];
1617
+ if (!(isfinite(t->v.num) && trunc(t->v.num) == t->v.num)) {
1618
+ scr_num_received(t->v.num, recv);
1619
+ int len = snprintf(msg, sizeof msg,
1620
+ "The value of \"%s\" is out of range. It must be an integer. Received %s",
1621
+ name, recv);
1622
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1623
+ return false;
1624
+ }
1625
+ if (t->v.num < 1 || t->v.num > 2147483647.0) {
1626
+ scr_num_received(t->v.num, recv);
1627
+ int len = snprintf(msg, sizeof msg,
1628
+ "The value of \"%s\" is out of range. It must be >= 1 && <= 2147483647. Received %s",
1629
+ name, recv);
1630
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
1631
+ return false;
1632
+ }
1633
+ return true;
1634
+ }
1635
+
1636
+ /* connect({ ..., autoSelectFamilyAttemptTimeout }): the budget validates
1637
+ * (validateInt32 from 1, Node's exact texts) and is then inert — this
1638
+ * slice's single dial has nothing to time, the same simplification the
1639
+ * autoSelectFamily flag already takes. NULL with the throw pending. */
1640
+ ScrNetSocket *scr_net_connect_attempt(double port, ScrStr *host, const ScrDyn *t) {
1641
+ if (!scr_net_attempt_timeout_chk(t, "options.autoSelectFamilyAttemptTimeout")) return NULL;
1642
+ return scr_net_connect(port, host, NULL);
1643
+ }
1644
+
1645
+ static bool scr_net_dyn_truthy(const ScrDyn *v) {
1646
+ switch (v->kind) {
1647
+ case SCR_DYN_UNDEF:
1648
+ case SCR_DYN_NULL: return false;
1649
+ case SCR_DYN_BOOL: return v->v.b;
1650
+ case SCR_DYN_NUM: return v->v.num == v->v.num && v->v.num != 0;
1651
+ case SCR_DYN_STR: return v->v.str->len > 0;
1652
+ default: return true;
1653
+ }
1654
+ }
1655
+
1656
+ /* net.connect/createConnection over a RUNTIME option bag (computed keys
1657
+ * — the invalid-input probes): Node's Socket-constructor order — the
1658
+ * objectMode trio throws ERR_INVALID_ARG_VALUE first, then the port
1659
+ * (validatePort), the host's string contract, autoSelectFamily's boolean
1660
+ * contract, and the attempt budget. A bag that survives everything meets
1661
+ * the compiler-rendered fence: an unmodeled key must refuse loudly, never
1662
+ * silently drop. Always leaves an exception pending. */
1663
+ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) {
1664
+ if (opts == NULL || opts->kind != SCR_DYN_OBJ) {
1665
+ scr_dyn_arg_type_fail("options", "of type object",
1666
+ opts ? opts : scr_dyn_undefined());
1667
+ return;
1668
+ }
1669
+ static const char *const om[] = { "objectMode", "readableObjectMode", "writableObjectMode" };
1670
+ for (size_t i = 0; i < 3; i++) {
1671
+ const ScrDyn *v = scr_dyn_obj_get(opts, om[i], strlen(om[i]));
1672
+ if (v != NULL && scr_net_dyn_truthy(v)) {
1673
+ char name[48];
1674
+ snprintf(name, sizeof name, "options.%s", om[i]);
1675
+ scr_dyn_arg_value_fail(name, "is not supported", v);
1676
+ return;
1677
+ }
1678
+ }
1679
+ const ScrDyn *port = scr_dyn_obj_get(opts, "port", 4);
1680
+ if (port != NULL && port->kind != SCR_DYN_UNDEF) {
1681
+ bool ok = port->kind == SCR_DYN_NUM && trunc(port->v.num) == port->v.num &&
1682
+ port->v.num >= 0 && port->v.num < 65536;
1683
+ if (!ok && port->kind == SCR_DYN_STR) {
1684
+ ScrStr *ps = scr_str_retain(port->v.str);
1685
+ double n = scr_string_to_number(ps);
1686
+ scr_str_release(ps);
1687
+ ok = n == n && trunc(n) == n && n >= 0 && n < 65536 && port->v.str->len > 0;
1688
+ }
1689
+ if (!ok) {
1690
+ char detail[64], msg[160];
1691
+ const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
1692
+ int len = snprintf(msg, sizeof msg,
1693
+ "options.port should be >= 0 and < 65536. Received %s", d);
1694
+ scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
1695
+ return;
1696
+ }
1697
+ }
1698
+ const ScrDyn *host = scr_dyn_obj_get(opts, "host", 4);
1699
+ if (host != NULL && host->kind != SCR_DYN_UNDEF && host->kind != SCR_DYN_STR) {
1700
+ scr_dyn_prop_type_fail("options.host", "of type string", host);
1701
+ return;
1702
+ }
1703
+ const ScrDyn *asf = scr_dyn_obj_get(opts, "autoSelectFamily", 16);
1704
+ if (asf != NULL && asf->kind != SCR_DYN_UNDEF && asf->kind != SCR_DYN_BOOL) {
1705
+ scr_dyn_prop_type_fail("options.autoSelectFamily", "of type boolean", asf);
1706
+ return;
1707
+ }
1708
+ const ScrDyn *att = scr_dyn_obj_get(opts, "autoSelectFamilyAttemptTimeout", 30);
1709
+ if (att != NULL && att->kind != SCR_DYN_UNDEF &&
1710
+ !scr_net_attempt_timeout_chk(att, "options.autoSelectFamilyAttemptTimeout")) {
1711
+ return;
1712
+ }
1713
+ scr_throw_lowering_fence(fence);
1714
+ }
1715
+
1604
1716
  /* ── the caller-lookup dial (net.connect with a lookup option) ─────────
1605
1717
  *
1606
1718
  * portless's createLoopbackConnection: connect({ host, port,
package/src/scr_number.c CHANGED
@@ -24,27 +24,19 @@
24
24
  * unchanged. Provides d2d(), d2d_small_int(), decimalLength17(), div10(). */
25
25
  #include "../vendor/ryu/d2s.c"
26
26
 
27
- size_t scr_f64_to_str(double x, char *buf) {
28
- if (isnan(x)) return (size_t)(stpcpy(buf, "NaN") - buf);
29
- if (x == 0) return (size_t)(stpcpy(buf, "0") - buf); /* covers -0 */
30
- if (isinf(x)) {
31
- return (size_t)(stpcpy(buf, x < 0 ? "-Infinity" : "Infinity") - buf);
32
- }
33
-
34
- char *out = buf;
35
- if (x < 0) {
36
- *out++ = '-';
37
- x = -x;
38
- }
39
-
40
- /* Shortest round-tripping digits via Ryū. Mirrors d2s_buffered_n's
41
- * dispatch: the exact small-integer fast path first (trailing decimal
42
- * zeros folded into the exponent), the full algorithm otherwise. */
27
+ /* The Ryū digit core, shared by the ECMA placement below and the Intl
28
+ * en-US number formatter (scr_lib.c): the shortest round-tripping digit
29
+ * string for a positive finite double value = 0.digits × 10^n with no
30
+ * trailing zeros. Returns k (the digit count, ≤ 17); digits is
31
+ * NUL-terminated. Mirrors d2s_buffered_n's dispatch: the exact
32
+ * small-integer fast path first (trailing decimal zeros folded into the
33
+ * exponent), the full algorithm otherwise. */
34
+ int scr_f64_digits(double x, char digits[18], int *n_out) {
43
35
  uint64_t bits;
44
36
  memcpy(&bits, &x, sizeof bits);
45
37
  const uint64_t ieeeMantissa = bits & ((1ull << DOUBLE_MANTISSA_BITS) - 1);
46
38
  const uint32_t ieeeExponent =
47
- (uint32_t)(bits >> DOUBLE_MANTISSA_BITS); /* sign already stripped */
39
+ (uint32_t)(bits >> DOUBLE_MANTISSA_BITS); /* sign must be stripped */
48
40
  floating_decimal_64 v;
49
41
  if (d2d_small_int(ieeeMantissa, ieeeExponent, &v)) {
50
42
  for (;;) {
@@ -60,9 +52,8 @@ size_t scr_f64_to_str(double x, char *buf) {
60
52
 
61
53
  /* Ryū's (mantissa, exponent) → ECMA's (digits, k, n): the k mantissa
62
54
  * digits have no trailing zeros, and value = 0.digits * 10^n. */
63
- char digits[18];
64
55
  int k = (int)decimalLength17(v.mantissa);
65
- int n = v.exponent + k;
56
+ *n_out = v.exponent + k;
66
57
  digits[k] = '\0';
67
58
  uint64_t m = v.mantissa;
68
59
  for (int i = k - 1; i >= 0; i--) {
@@ -70,6 +61,25 @@ size_t scr_f64_to_str(double x, char *buf) {
70
61
  digits[i] = (char)('0' + (uint32_t)m - 10 * (uint32_t)q);
71
62
  m = q;
72
63
  }
64
+ return k;
65
+ }
66
+
67
+ size_t scr_f64_to_str(double x, char *buf) {
68
+ if (isnan(x)) return (size_t)(stpcpy(buf, "NaN") - buf);
69
+ if (x == 0) return (size_t)(stpcpy(buf, "0") - buf); /* covers -0 */
70
+ if (isinf(x)) {
71
+ return (size_t)(stpcpy(buf, x < 0 ? "-Infinity" : "Infinity") - buf);
72
+ }
73
+
74
+ char *out = buf;
75
+ if (x < 0) {
76
+ *out++ = '-';
77
+ x = -x;
78
+ }
79
+
80
+ char digits[18];
81
+ int n;
82
+ int k = scr_f64_digits(x, digits, &n);
73
83
 
74
84
  if (k <= n && n <= 21) {
75
85
  /* Integer: digits followed by n-k zeros. */