@scriptc/runtime 0.0.15 → 0.0.17

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.15",
3
+ "version": "0.0.17",
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",
@@ -154,13 +154,12 @@ ScrBytes *scr_crypto_random_bytes(double n) {
154
154
  /* ── process.stdout/stderr.write(buf) ──────────────────────────────────── */
155
155
 
156
156
  bool scr_process_stdout_write_bytes(const ScrBytes *b) {
157
- fwrite(b->data, 1, b->len * scr_bytes_elem_size(b->elem), stdout);
157
+ scr_stdio_write(1, b->data, b->len * scr_bytes_elem_size(b->elem));
158
158
  return true;
159
159
  }
160
160
 
161
161
  bool scr_process_stderr_write_bytes(const ScrBytes *b) {
162
- fflush(stdout); /* merged 2>&1 output keeps source order (scr_lib.c) */
163
- fwrite(b->data, 1, b->len * scr_bytes_elem_size(b->elem), stderr);
162
+ scr_stdio_write(2, b->data, b->len * scr_bytes_elem_size(b->elem));
164
163
  return true;
165
164
  }
166
165
 
package/src/scr_console.c CHANGED
@@ -83,8 +83,11 @@ void scr_init(void) {
83
83
  _setmode(_fileno(stderr), _O_BINARY);
84
84
  _setmode(_fileno(stdin), _O_BINARY);
85
85
  #endif
86
- /* Full buffering matches Node writing to a pipe (the differential harness
87
- * never runs either side on a TTY) and makes output-heavy programs fast. */
86
+ /* A private formatting buffer coalesces the several stdio calls needed to
87
+ * render one console line. Every JavaScript-visible stdout write flushes
88
+ * before returning, so this buffer is never observable as delayed output:
89
+ * Node hands each console.log/process.stdout.write chunk to its backing
90
+ * stream immediately. */
88
91
  static char outbuf[1 << 16];
89
92
  setvbuf(stdout, outbuf, _IOFBF, sizeof outbuf);
90
93
  /* atexit is LIFO; registration order makes exit run: library cleanup
@@ -131,18 +134,36 @@ static void scr_console_write(FILE *out, size_t n, const ScrLogArg *args) {
131
134
  }
132
135
  }
133
136
  fputc('\n', out);
137
+ /* Console methods submit one formatted chunk to their backing stream.
138
+ * Keep the C buffer only as a formatter coalescing detail: a caller
139
+ * observing a live child must see this line before the next JS turn. */
140
+ fflush(out);
134
141
  }
135
142
 
136
143
  void scr_console_log(size_t n, const ScrLogArg *args) {
137
144
  scr_console_write(stdout, n, args);
138
145
  }
139
146
 
140
- /* console.error and console.warn. stderr is UNBUFFERED (C's default; Node
141
- * writes stderr synchronously too), and stdout FLUSHES first so a merged
142
- * redirection (2>&1) sees writes in source order despite stdout's full
143
- * buffering Node's piped stdout/stderr preserve write order the same
144
- * way. */
147
+ /* console.error and console.warn. Flush stdout first so runtime-internal
148
+ * output that is still being assembled cannot cross this stderr line under
149
+ * a merged redirection (2>&1); JavaScript-visible stdout writes have already
150
+ * flushed themselves. */
145
151
  void scr_console_error(size_t n, const ScrLogArg *args) {
146
152
  fflush(stdout);
147
153
  scr_console_write(stderr, n, args);
148
154
  }
155
+
156
+ /* One JavaScript-visible raw write. Node's global console and process stream
157
+ * methods hand each chunk to the backing stream when called; they do not
158
+ * retain stdout until a later stderr write, a 64 KiB threshold, or process
159
+ * exit. The runtime remains synchronous internally, so its backpressure
160
+ * surface is still constantly true, but the bytes are observable promptly.
161
+ *
162
+ * stderr flushes any runtime-internal stdout fragment first to preserve the
163
+ * existing merged-fd ordering convention. */
164
+ void scr_stdio_write(int fd, const void *data, size_t len) {
165
+ FILE *out = fd == 2 ? stderr : stdout;
166
+ if (fd == 2) fflush(stdout);
167
+ if (len > 0) fwrite(data, 1, len, out);
168
+ fflush(out);
169
+ }
@@ -307,8 +307,8 @@ int scr_exit_code_hint_get(void) { return scr_exit_code_hint; }
307
307
 
308
308
  void scr_exc_print_uncaught(void) {
309
309
  scr_exit_code_note(1);
310
- /* Pre-throw stdout must land before the stderr line when both share a
311
- * terminal; stdout is fully buffered (scr_init). */
310
+ /* Settle any runtime-internal stdout fragment before the stderr line when
311
+ * both share an fd. JavaScript-visible writes already flush themselves. */
312
312
  fflush(stdout);
313
313
  fputs("Uncaught ", stderr);
314
314
  switch (scr_exc_kind) {
package/src/scr_island.c CHANGED
@@ -2682,7 +2682,7 @@ static JSValue isl_host_write(JSContext *ctx, JSValueConst this_val, int argc,
2682
2682
  size_t len;
2683
2683
  const char *s = JS_ToCStringLen(ctx, &len, argv[1]);
2684
2684
  if (!s) return JS_EXCEPTION;
2685
- fwrite(s, 1, len, fd == 2 ? stderr : stdout);
2685
+ scr_stdio_write(fd, s, len);
2686
2686
  JS_FreeCString(ctx, s);
2687
2687
  return JS_TRUE;
2688
2688
  }
package/src/scr_lib.c CHANGED
@@ -584,21 +584,18 @@ ScrStr *scr_process_cwd(void) {
584
584
  return scr_str_new(buf, strlen(buf));
585
585
  }
586
586
 
587
- /* The raw byte writes. stdout goes through the SAME stdio stream and
588
- * buffer console.log uses (scr_init's setvbuf), so interleaved writes
589
- * keep source order; stderr is unbuffered, like Node's, and FLUSHES
590
- * stdout first (the console.error convention) so merged 2>&1 output
591
- * keeps source order despite stdout's full buffering. The boolean is
592
- * Node's backpressure signal — a synchronous fwrite has no backpressure,
593
- * so it is constantly true. */
587
+ /* The raw byte writes use the SAME stdio stream as console, and each call
588
+ * flushes before returning so live consumers see Node's source order without
589
+ * an observable C buffering delay. The boolean is Node's backpressure signal
590
+ * this synchronous runtime has no queued backpressure, so it is constantly
591
+ * true. */
594
592
  bool scr_process_stdout_write(const ScrStr *data) {
595
- fwrite(data->data, 1, data->len, stdout);
593
+ scr_stdio_write(1, data->data, data->len);
596
594
  return true;
597
595
  }
598
596
 
599
597
  bool scr_process_stderr_write(const ScrStr *data) {
600
- fflush(stdout);
601
- fwrite(data->data, 1, data->len, stderr);
598
+ scr_stdio_write(2, data->data, data->len);
602
599
  return true;
603
600
  }
604
601
 
@@ -606,7 +603,7 @@ bool scr_process_stderr_write(const ScrStr *data) {
606
603
  * WritableStream-typed value — the prefixStream idiom): the value is the
607
604
  * stream's fd (1 or 2, minted by the process.stdout/stderr reads), and
608
605
  * the write dispatches onto the exact stdout/stderr paths above so
609
- * buffering and ordering stay identical. */
606
+ * prompt submission and ordering stay identical. */
610
607
  bool scr_proc_stream_write(double fd, const ScrStr *data) {
611
608
  return (int)fd == 2 ? scr_process_stderr_write(data) : scr_process_stdout_write(data);
612
609
  }
@@ -3506,6 +3503,170 @@ ScrStr *scr_num_to_fixed0(double x) {
3506
3503
  return r;
3507
3504
  }
3508
3505
 
3506
+ /* The explicit-fraction-digits toFixed needs the exact binary value, not
3507
+ * the shortest decimal that round-trips to it: (1.005).toFixed(2) is
3508
+ * "1.00", for example. Represent abs(x) * 10^f as
3509
+ *
3510
+ * mantissa * 5^f * 2^(binary_exponent + f)
3511
+ *
3512
+ * in a tiny base-2^32 integer, then shift right with the spec's
3513
+ * round-half-up rule. The largest value handled here is below 1e21 with
3514
+ * f=100, so the rounded integer is below 1e121 (402 bits); sixteen limbs
3515
+ * leave comfortable headroom without heap allocation. */
3516
+ #define SCR_FIXED_LIMBS 16
3517
+ typedef struct {
3518
+ uint32_t limb[SCR_FIXED_LIMBS]; /* little-endian */
3519
+ int len;
3520
+ } ScrFixedInt;
3521
+
3522
+ static void scr_fixed_normalize(ScrFixedInt *v) {
3523
+ while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--;
3524
+ }
3525
+
3526
+ static void scr_fixed_mul5(ScrFixedInt *v) {
3527
+ uint64_t carry = 0;
3528
+ for (int i = 0; i < v->len; i++) {
3529
+ uint64_t p = (uint64_t)v->limb[i] * 5 + carry;
3530
+ v->limb[i] = (uint32_t)p;
3531
+ carry = p >> 32;
3532
+ }
3533
+ if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
3534
+ }
3535
+
3536
+ static bool scr_fixed_bit(const ScrFixedInt *v, int bit) {
3537
+ int word = bit / 32;
3538
+ return word < v->len && ((v->limb[word] >> (bit % 32)) & 1u) != 0;
3539
+ }
3540
+
3541
+ static void scr_fixed_shr(ScrFixedInt *v, int bits) {
3542
+ int words = bits / 32;
3543
+ int rem = bits % 32;
3544
+ if (words >= v->len) {
3545
+ v->limb[0] = 0;
3546
+ v->len = 1;
3547
+ return;
3548
+ }
3549
+ int n = v->len - words;
3550
+ for (int i = 0; i < n; i++) {
3551
+ uint32_t lo = v->limb[i + words] >> rem;
3552
+ uint32_t hi =
3553
+ rem != 0 && i + words + 1 < v->len
3554
+ ? v->limb[i + words + 1] << (32 - rem)
3555
+ : 0;
3556
+ v->limb[i] = lo | hi;
3557
+ }
3558
+ v->len = n;
3559
+ scr_fixed_normalize(v);
3560
+ }
3561
+
3562
+ static void scr_fixed_shl(ScrFixedInt *v, int bits) {
3563
+ uint32_t out[SCR_FIXED_LIMBS] = {0};
3564
+ int words = bits / 32;
3565
+ int rem = bits % 32;
3566
+ for (int i = 0; i < v->len; i++) {
3567
+ int at = i + words;
3568
+ out[at] |= v->limb[i] << rem;
3569
+ if (rem != 0) out[at + 1] |= v->limb[i] >> (32 - rem);
3570
+ }
3571
+ int n = v->len + words + (rem != 0 ? 1 : 0);
3572
+ memcpy(v->limb, out, sizeof out);
3573
+ v->len = n;
3574
+ scr_fixed_normalize(v);
3575
+ }
3576
+
3577
+ static void scr_fixed_inc(ScrFixedInt *v) {
3578
+ uint64_t carry = 1;
3579
+ for (int i = 0; i < v->len && carry != 0; i++) {
3580
+ uint64_t s = (uint64_t)v->limb[i] + carry;
3581
+ v->limb[i] = (uint32_t)s;
3582
+ carry = s >> 32;
3583
+ }
3584
+ if (carry != 0) v->limb[v->len++] = (uint32_t)carry;
3585
+ }
3586
+
3587
+ /* Divide in place by 1e9; each quotient limb still fits uint32_t because
3588
+ * the carried remainder is below the divisor. Returns the remainder. */
3589
+ static uint32_t scr_fixed_div1e9(ScrFixedInt *v) {
3590
+ uint64_t rem = 0;
3591
+ for (int i = v->len - 1; i >= 0; i--) {
3592
+ uint64_t cur = (rem << 32) | v->limb[i];
3593
+ v->limb[i] = (uint32_t)(cur / 1000000000u);
3594
+ rem = cur % 1000000000u;
3595
+ }
3596
+ scr_fixed_normalize(v);
3597
+ return (uint32_t)rem;
3598
+ }
3599
+
3600
+ /* Number.prototype.toFixed(fractionDigits), with the argument already
3601
+ * number-typed by the frontend. ToIntegerOrInfinity validation precedes
3602
+ * the receiver's non-finite arm, as ECMA-262 requires. Invalid precision
3603
+ * raises V8's catchable RangeError text; otherwise the result is +1. */
3604
+ ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
3605
+ double fd = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
3606
+ if (!(fd >= 0 && fd <= 100)) {
3607
+ static const char msg[] =
3608
+ "toFixed() digits argument must be between 0 and 100";
3609
+ scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
3610
+ return NULL;
3611
+ }
3612
+ int f = (int)fd;
3613
+ if (!isfinite(x) || fabs(x) >= 1e21) return scr_f64_to_scrstr(x);
3614
+
3615
+ bool neg = x < 0; /* false for -0, exactly like the spec's sign arm */
3616
+ double a = neg ? -x : x;
3617
+ uint64_t bits;
3618
+ memcpy(&bits, &a, sizeof bits);
3619
+ uint64_t mantissa = bits & ((1ull << 52) - 1);
3620
+ int ieee_exp = (int)((bits >> 52) & 0x7ffu);
3621
+ int binary_exp;
3622
+ if (ieee_exp == 0) {
3623
+ binary_exp = -1074;
3624
+ } else {
3625
+ mantissa |= 1ull << 52;
3626
+ binary_exp = ieee_exp - 1023 - 52;
3627
+ }
3628
+
3629
+ ScrFixedInt n = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2};
3630
+ scr_fixed_normalize(&n);
3631
+ for (int i = 0; i < f; i++) scr_fixed_mul5(&n);
3632
+ int shift = binary_exp + f;
3633
+ if (shift >= 0) {
3634
+ scr_fixed_shl(&n, shift);
3635
+ } else {
3636
+ int right = -shift;
3637
+ bool round_up = scr_fixed_bit(&n, right - 1);
3638
+ scr_fixed_shr(&n, right);
3639
+ if (round_up) scr_fixed_inc(&n);
3640
+ }
3641
+
3642
+ /* Render the exact rounded integer through base-1e9 chunks, then place
3643
+ * the decimal point f digits from the right (padding through zero). */
3644
+ uint32_t chunks[SCR_FIXED_LIMBS];
3645
+ int chunk_count = 0;
3646
+ do {
3647
+ chunks[chunk_count++] = scr_fixed_div1e9(&n);
3648
+ } while (!(n.len == 1 && n.limb[0] == 0));
3649
+
3650
+ char digits[128];
3651
+ int dlen = snprintf(digits, sizeof digits, "%u", chunks[chunk_count - 1]);
3652
+ for (int i = chunk_count - 2; i >= 0; i--) {
3653
+ dlen += snprintf(digits + dlen, sizeof digits - (size_t)dlen,
3654
+ "%09u", chunks[i]);
3655
+ }
3656
+
3657
+ char out[128];
3658
+ int o = 0;
3659
+ if (neg) out[o++] = '-';
3660
+ int padded = dlen > f + 1 ? dlen : f + 1;
3661
+ int integer_digits = padded - f;
3662
+ int leading_zeros = padded - dlen;
3663
+ for (int i = 0; i < padded; i++) {
3664
+ if (f != 0 && i == integer_digits) out[o++] = '.';
3665
+ out[o++] = i < leading_zeros ? '0' : digits[i - leading_zeros];
3666
+ }
3667
+ return scr_str_new(out, (size_t)o);
3668
+ }
3669
+
3509
3670
  /* Increment a decimal digit string in place. Returns true on overflow —
3510
3671
  * the value becomes 1 followed by len zeros (the caller folds the zeros
3511
3672
  * into its scale); an EMPTY string increments to "1" the same way (the
@@ -235,9 +235,10 @@ void scr_rl_question(double id, const ScrStr *query, ScrClosure *cb /*moves*/,
235
235
  scr_throw_error_msg(SCR_ERR_ERROR, "readline was closed", 19);
236
236
  return;
237
237
  }
238
- /* The prompt writes to stdout under pipes too (Node's question does);
239
- * same stream and buffer as console.log, so ordering holds. */
240
- fwrite(query->data, 1, query->len, stdout);
238
+ /* The prompt writes to stdout under pipes too (Node's question does), and
239
+ * must be visible before input arrives even though it has no trailing
240
+ * newline. */
241
+ scr_stdio_write(1, query->data, query->len);
241
242
  if (rl->dead) {
242
243
  /* Created after stdin ended: pinned against Node — the prompt still
243
244
  * writes, but the DESTROYED stream delivers nothing and 'close'
package/src/scr_runtime.h CHANGED
@@ -35,8 +35,9 @@ char *strcasestr(const char *hay, const char *needle);
35
35
 
36
36
  /* ── process ──────────────────────────────────────────────────────────── */
37
37
 
38
- /* Called once at the top of main: stdout buffering to match piped Node,
39
- * flush-at-exit, RC audit registration (when built with -DSCR_RC_AUDIT). */
38
+ /* Called once at the top of main: private stdout formatter buffer,
39
+ * flush-at-exit, RC audit registration (when built with -DSCR_RC_AUDIT).
40
+ * JavaScript-visible writes flush before returning. */
40
41
  void scr_init(void);
41
42
 
42
43
  /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
@@ -1840,10 +1841,13 @@ int scr_lib_arg_count(void);
1840
1841
  const char *scr_lib_arg(int i);
1841
1842
  ScrStr *scr_process_platform(void); /* +1 interned ("darwin", "linux", ...) */
1842
1843
  ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */
1843
- /* process.stdout/.stderr .write the raw byte writes (no newline, no
1844
- * formatting; data borrowed). stdout shares console.log's stream+buffer
1845
- * (ordering preserved); stderr is unbuffered like Node's. Constantly true
1846
- * (Node's backpressure boolean never fires on synchronous writes). */
1844
+ /* Submit one raw chunk to fd 1/2 and flush it before returning. Used by all
1845
+ * JavaScript-visible console/process/readline/island output paths so the
1846
+ * internal stdio formatting buffer never delays live output. */
1847
+ void scr_stdio_write(int fd, const void *data, size_t len);
1848
+ /* process.stdout/.stderr .write — raw bytes (no newline or formatting; data
1849
+ * borrowed), promptly visible and ordered with console output. Constantly
1850
+ * true (the synchronous runtime never queues backpressure). */
1847
1851
  bool scr_process_stdout_write(const ScrStr *data);
1848
1852
  bool scr_process_stderr_write(const ScrStr *data);
1849
1853
  /* The first-class WritableStream write: fd is the stream value itself
@@ -4239,12 +4243,14 @@ bool scr_num_is_finite(double x);
4239
4243
  bool scr_num_is_nan(double x);
4240
4244
  bool scr_num_is_integer(double x);
4241
4245
  bool scr_num_is_safe_integer(double x);
4242
- /* The fraction-digits-free Number.prototype formatters: toExponential()
4243
- * (shortest correctly-rounded mantissa, "7e+0" exponent shape) and
4244
- * toFixed() (closest integer, ties toward +∞ on the magnitude, "-0" for
4245
- * negative fractions, ToString at |x| 1e21). +1 results; never throw. */
4246
+ /* Number.prototype formatters: toExponential() is the fraction-digits-free
4247
+ * shortest correctly-rounded mantissa; toFixed0 is the non-throwing omitted
4248
+ * argument form. toFixed implements an explicit 0..100 fractionDigits with
4249
+ * exact binary-value rounding and THROWS V8's catchable RangeError outside
4250
+ * that range. All successful results are +1. */
4246
4251
  ScrStr *scr_num_to_exponential(double x);
4247
4252
  ScrStr *scr_num_to_fixed0(double x);
4253
+ ScrStr *scr_num_to_fixed(double x, double fraction_digits);
4248
4254
 
4249
4255
  /* Object.is over two numbers — the spec's SameValue on doubles: NaN
4250
4256
  * equals NaN, +0 differs from -0, everything else is ==. Never throws. */
@@ -5835,11 +5841,11 @@ typedef struct {
5835
5841
  } v;
5836
5842
  } ScrLogArg;
5837
5843
 
5838
- /* Space-joined args + '\n', single buffered write. Borrows string args. */
5844
+ /* Space-joined args + '\n', submitted before return. Borrows string args. */
5839
5845
  void scr_console_log(size_t n, const ScrLogArg *args);
5840
5846
  /* The stderr twin (console.error AND console.warn — one stream in Node):
5841
- * identical formatting, unbuffered stderr, stdout flushed first so merged
5842
- * (2>&1) output keeps source order. */
5847
+ * identical formatting, with stdout settled first so merged (2>&1) output
5848
+ * keeps source order. */
5843
5849
  void scr_console_error(size_t n, const ScrLogArg *args);
5844
5850
 
5845
5851
  #endif /* SCR_RUNTIME_H */
package/src/scr_web.c CHANGED
@@ -1739,7 +1739,7 @@ static JSValue web_host_write(JSContext *ctx, JSValueConst this_val, int argc,
1739
1739
  size_t len;
1740
1740
  const char *s = JS_ToCStringLen(ctx, &len, argv[1]);
1741
1741
  if (!s) return JS_EXCEPTION;
1742
- fwrite(s, 1, len, fd == 2 ? stderr : stdout);
1742
+ scr_stdio_write(fd, s, len);
1743
1743
  JS_FreeCString(ctx, s);
1744
1744
  return JS_UNDEFINED;
1745
1745
  }