@scriptc/runtime 0.0.26 → 0.0.27

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.26",
3
+ "version": "0.0.27",
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
@@ -8,7 +8,7 @@
8
8
  /* Live heap-array count for the RC audit lane (-DSCR_RC_AUDIT); same
9
9
  * contract as scr_str_live_count in scr_string.c. */
10
10
  #ifdef SCR_RC_AUDIT
11
- static long scr_live_arrays = 0;
11
+ static SCR_TL long scr_live_arrays = 0;
12
12
  long scr_arr_live_count(void) { return scr_live_arrays; }
13
13
  #endif
14
14
 
package/src/scr_assert.c CHANGED
@@ -158,9 +158,9 @@ void scr_assert_ok(bool pass, ScrStr *message) {
158
158
  * PAIR already being compared answers equal (the coinductive step —
159
159
  * Node's memo behavior exactly). The stack is global (comparisons never
160
160
  * interleave; the emitted walks cannot throw mid-compare). */
161
- static struct { const void *a, *b; } *g_deq_stack;
162
- static size_t g_deq_len;
163
- static size_t g_deq_cap;
161
+ static SCR_TL struct { const void *a, *b; } *g_deq_stack;
162
+ static SCR_TL size_t g_deq_len;
163
+ static SCR_TL size_t g_deq_cap;
164
164
 
165
165
  bool scr_assert_deq_enter(const void *a, const void *b) {
166
166
  for (size_t i = 0; i < g_deq_len; i++) {
@@ -1228,7 +1228,7 @@ typedef struct {
1228
1228
  ScrStr *val; /* owned: the string value, or the regex's /source/flags */
1229
1229
  } ScrShapeSlot;
1230
1230
 
1231
- static struct {
1231
+ static SCR_TL struct {
1232
1232
  ScrError *err; /* borrowed — alive across the straight-line sequence */
1233
1233
  ScrShapeSlot slots[3];
1234
1234
  } scr_shape;
package/src/scr_bytes.c CHANGED
@@ -17,7 +17,7 @@
17
17
  #include <string.h>
18
18
 
19
19
  #ifdef SCR_RC_AUDIT
20
- static long scr_live_bytes = 0;
20
+ static SCR_TL long scr_live_bytes = 0;
21
21
  long scr_bytes_live_count(void) { return scr_live_bytes; }
22
22
  #endif
23
23
 
package/src/scr_closure.c CHANGED
@@ -5,8 +5,8 @@
5
5
  #include <string.h>
6
6
 
7
7
  #ifdef SCR_RC_AUDIT
8
- static long scr_live_boxes = 0;
9
- static long scr_live_closures = 0;
8
+ static SCR_TL long scr_live_boxes = 0;
9
+ static SCR_TL long scr_live_closures = 0;
10
10
  long scr_box_live_count(void) { return scr_live_boxes; }
11
11
  long scr_closure_live_count(void) { return scr_live_closures; }
12
12
  #endif
package/src/scr_console.c CHANGED
@@ -11,7 +11,7 @@
11
11
  /* Set by scr_async.c at loop exhaustion; lives here (unconditionally) so
12
12
  * binaries that link the console without the async runtime still link, and
13
13
  * plain builds satisfy scr_async's reference. */
14
- static long scr_abandoned_fibers = 0;
14
+ static SCR_TL long scr_abandoned_fibers = 0;
15
15
  void scr_note_abandoned_fibers(long n) { scr_abandoned_fibers = n; }
16
16
 
17
17
  #ifndef SCR_LIB
package/src/scr_cycle.c CHANGED
@@ -95,7 +95,7 @@ static void scr_cyc_oom(void) {
95
95
  }
96
96
 
97
97
  /* Live cycle-headered objects — what the full-pass trigger watches. */
98
- static size_t scr_cyc_live = 0;
98
+ static SCR_TL size_t scr_cyc_live = 0;
99
99
 
100
100
  void *scr_cyc_alloc(size_t size, ScrTraceFn trace, ScrCycFreeFn free_fn) {
101
101
  ScrCycHdr *h = calloc(1, sizeof(ScrCycHdr) + size);
@@ -137,25 +137,25 @@ static void scr_cyc_push(ScrVec *vec, void *obj) {
137
137
 
138
138
  /* ── the candidate-root buffers (one per generation) ──────────────────── */
139
139
 
140
- static ScrVec scr_roots[SCR_CYC_NGENS];
141
- static bool scr_collecting = false;
140
+ static SCR_TL ScrVec scr_roots[SCR_CYC_NGENS];
141
+ static SCR_TL bool scr_collecting = false;
142
142
 
143
143
  /* The candidates of the pass in flight, gathered across the generations it
144
144
  * collects so the phases can walk them without re-filtering the buffers. */
145
- static ScrVec scr_cands;
145
+ static SCR_TL ScrVec scr_cands;
146
146
 
147
147
  /* Nursery survivors awaiting promotion (see scr_scan_black). */
148
- static ScrVec scr_promote;
148
+ static SCR_TL ScrVec scr_promote;
149
149
 
150
150
  /* The gathered white set (freed after the walk completes). */
151
- static ScrVec scr_white;
151
+ static SCR_TL ScrVec scr_white;
152
152
 
153
153
  /* Cross-generation targets pinned by the white set (see scr_xg_pin_visit),
154
154
  * one entry per skipped edge. Drained after the teardowns. */
155
- static ScrVec scr_xgen;
155
+ static SCR_TL ScrVec scr_xgen;
156
156
 
157
157
  /* Objects above this generation are invisible to the pass in flight. */
158
- static unsigned scr_gen_limit = SCR_CYC_MATURE;
158
+ static SCR_TL unsigned scr_gen_limit = SCR_CYC_MATURE;
159
159
 
160
160
  static size_t scr_cyc_pass(unsigned gen_limit);
161
161
 
@@ -166,10 +166,10 @@ static size_t scr_cyc_pass(unsigned gen_limit);
166
166
  #define SCR_CYC_FULL_FLOOR 4096 /* ...but not below this many objects */
167
167
 
168
168
  /* Live count as of the end of the last full pass. */
169
- static size_t scr_cyc_live_after_full = 0;
169
+ static SCR_TL size_t scr_cyc_live_after_full = 0;
170
170
 
171
171
  static size_t scr_cyc_nursery_threshold(void) {
172
- static size_t cached = 0;
172
+ static SCR_TL size_t cached = 0;
173
173
  if (cached == 0) {
174
174
  const char *env = getenv("SCR_CYCLE_THRESHOLD");
175
175
  long v = env ? strtol(env, NULL, 10) : 0;
@@ -186,12 +186,12 @@ static size_t scr_cyc_nursery_threshold(void) {
186
186
  * hysteresis a large mature buffer — which a nursery pass cannot drain —
187
187
  * would re-arm on every single release. Both start at zero, so the first
188
188
  * release runs one trivial pass that arms them properly. */
189
- static size_t scr_cyc_nbuffered = 0;
190
- static size_t scr_cyc_trigger = 0;
189
+ static SCR_TL size_t scr_cyc_nbuffered = 0;
190
+ static SCR_TL size_t scr_cyc_trigger = 0;
191
191
 
192
192
  /* Scheduled nursery passes for which at least one mature root was waiting.
193
193
  * A full pass resets the age; with no mature backlog there is nothing to age. */
194
- static size_t scr_cyc_scheduled_mature_age = 0;
194
+ static SCR_TL size_t scr_cyc_scheduled_mature_age = 0;
195
195
 
196
196
  static size_t scr_cyc_buffered(void) {
197
197
  return scr_roots[SCR_CYC_NURSERY].n + scr_roots[SCR_CYC_MATURE].n;
@@ -404,7 +404,7 @@ static void scr_xg_pin_visit(void *child, void *ctx) {
404
404
  }
405
405
 
406
406
  /* Freed by the cross-generation drain, for the caller's fixpoint. */
407
- static size_t scr_xg_freed = 0;
407
+ static SCR_TL size_t scr_xg_freed = 0;
408
408
 
409
409
  static void scr_xg_release(void *obj);
410
410
  static void scr_xg_child_visit(void *child, void *ctx) {
package/src/scr_error.c CHANGED
@@ -17,7 +17,7 @@ static void scr_error_oom(void) {
17
17
  scr_trap("scriptc: out of memory\n");
18
18
  }
19
19
 
20
- static bool scr_error_traced = false;
20
+ static SCR_TL bool scr_error_traced = false;
21
21
 
22
22
  void scr_error_set_traced(void) { scr_error_traced = true; }
23
23
 
@@ -49,7 +49,7 @@ static void scr_error_gcfree(void *obj) {
49
49
  * must stay linkable WITHOUT the checked-dynamic tree (the runtime C-unit tests link
50
50
  * subsets), so the drop goes through a hook scr_json.c installs before
51
51
  * any cause can exist (scr_domex_new is the only producer). */
52
- static void (*scr_domex_cause_drop)(void *obj) = NULL;
52
+ static SCR_TL void (*scr_domex_cause_drop)(void *obj) = NULL;
53
53
 
54
54
  void scr_domex_install_cause_drop(void (*fn)(void *obj)) {
55
55
  scr_domex_cause_drop = fn;
@@ -90,7 +90,7 @@ static void scr_domex_reld(void *obj) {
90
90
 
91
91
  /* Defaults cover only the instants before main() stamps the program's real
92
92
  * preorder intervals; release is permanent. */
93
- ScrVt scr_error_vts[5] = {
93
+ SCR_TL ScrVt scr_error_vts[5] = {
94
94
  {0, 4, &scr_error_reld}, /* Error */
95
95
  {1, 1, &scr_error_reld}, /* TypeError */
96
96
  {2, 2, &scr_error_reld}, /* RangeError */
@@ -97,11 +97,11 @@ struct ScrEeReg {
97
97
  * stamps pre/post from the program's preorder numbering (the
98
98
  * scr_error_vts precedent). release is the direct teardown below. */
99
99
  static void scr_emitter_release_direct(void *obj);
100
- ScrVt scr_emitter_vt = {0, 0, &scr_emitter_release_direct};
100
+ SCR_TL ScrVt scr_emitter_vt = {0, 0, &scr_emitter_release_direct};
101
101
 
102
- double scr_emitter_default_max = 10; /* EventEmitter.defaultMaxListeners */
102
+ SCR_TL double scr_emitter_default_max = 10; /* EventEmitter.defaultMaxListeners */
103
103
 
104
- static bool scr_ee_trace_hint_shown = false;
104
+ static SCR_TL bool scr_ee_trace_hint_shown = false;
105
105
 
106
106
  /* Post-registration hook (scr_runtime.h): the stream unit's flow kick.
107
107
  * NULL in stream-free builds — behavior byte-identical. */
@@ -35,28 +35,38 @@ _Noreturn void scr_trap_fmt(const char *fmt, ...) {
35
35
  }
36
36
  #endif /* !SCR_LIB */
37
37
 
38
- static ScrExcCell scr_main_exc; /* fiber zero (the main stack) */
38
+ static SCR_TL ScrExcCell scr_main_exc; /* fiber zero (the main stack) */
39
+ #if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
40
+ /* A thread-local pointer cannot be initialized with &scr_main_exc — the
41
+ * address of a thread-local is not a constant expression — so NULL stands
42
+ * for the main cell and every read resolves it. Fibers never exist in
43
+ * library artifacts, so the cell only ever IS the main cell here. */
44
+ static SCR_TL ScrExcCell *scr_cur;
45
+ #define SCR_EXC_CUR() (scr_cur ? scr_cur : &scr_main_exc)
46
+ #else
39
47
  static ScrExcCell *scr_cur = &scr_main_exc;
48
+ #define SCR_EXC_CUR() (scr_cur)
49
+ #endif
40
50
 
41
51
  /* Fiber switching (scr_async.c) points the exception machinery at the
42
52
  * incoming fiber's cell; returns the previous cell. */
43
53
  ScrExcCell *scr_exc_swap_cell(ScrExcCell *cell) {
44
- ScrExcCell *prev = scr_cur;
54
+ ScrExcCell *prev = SCR_EXC_CUR();
45
55
  scr_cur = cell ? cell : &scr_main_exc;
46
56
  return prev;
47
57
  }
48
58
 
49
59
  /* The ACTIVE cell, for runtime code that moves a pending payload out of it
50
60
  * (a new-Promise executor throw rejecting the promise). */
51
- ScrExcCell *scr_exc_current_cell(void) { return scr_cur; }
61
+ ScrExcCell *scr_exc_current_cell(void) { return SCR_EXC_CUR(); }
52
62
 
53
- #define scr_exc_kind (scr_cur->kind)
54
- #define scr_exc_f64 (scr_cur->f64)
55
- #define scr_exc_bool (scr_cur->b)
56
- #define scr_exc_payload (scr_cur->payload)
57
- #define scr_exc_retain_fn (scr_cur->retain_fn)
58
- #define scr_exc_release_fn (scr_cur->release_fn)
59
- #define scr_exc_trace_fn (scr_cur->trace_fn)
63
+ #define scr_exc_kind (SCR_EXC_CUR()->kind)
64
+ #define scr_exc_f64 (SCR_EXC_CUR()->f64)
65
+ #define scr_exc_bool (SCR_EXC_CUR()->b)
66
+ #define scr_exc_payload (SCR_EXC_CUR()->payload)
67
+ #define scr_exc_retain_fn (SCR_EXC_CUR()->retain_fn)
68
+ #define scr_exc_release_fn (SCR_EXC_CUR()->release_fn)
69
+ #define scr_exc_trace_fn (SCR_EXC_CUR()->trace_fn)
60
70
 
61
71
  bool scr_exc_pending(void) { return scr_exc_kind != SCR_EXC_NONE; }
62
72
 
@@ -187,7 +197,7 @@ ScrStr *scr_caught_to_string(const ScrCaught *c) {
187
197
  * delivery; only the 0x01-led verbatim path below bypasses assembly. */
188
198
  void scr_library_check_exc(void) {
189
199
  if (!scr_exc_pending()) return;
190
- static char buf[1024]; /* the message is copied out before the payload dies */
200
+ static SCR_TL char buf[1024]; /* the message is copied out before the payload dies */
191
201
  /* The ratified verbatim rule: a thrown message that ALREADY begins with
192
202
  * the structured trap-teaching marker (0x01) is delivered exactly as
193
203
  * authored — no "Uncaught " prefix, no added newline — which is how a
@@ -301,7 +311,7 @@ void scr_throw_obj(void *v, void *(*retain)(void *), void (*release)(void *),
301
311
  * events unit's atexit, when linked) must see that code, like Node's.
302
312
  * Lives HERE so the unit is self-contained (the reporters below and in
303
313
  * scr_async.c both note it; scr_events.c reads it). */
304
- static int scr_exit_code_hint = 0;
314
+ static SCR_TL int scr_exit_code_hint = 0;
305
315
  void scr_exit_code_note(int code) { scr_exit_code_hint = code; }
306
316
  int scr_exit_code_hint_get(void) { return scr_exit_code_hint; }
307
317
 
package/src/scr_inspect.c CHANGED
@@ -237,11 +237,11 @@ typedef struct {
237
237
  bool all_num; /* every entry so far flagged numeric (grouping order) */
238
238
  } InspFrame;
239
239
 
240
- static InspFrame *g_frames;
241
- static size_t g_nframes;
242
- static size_t g_frames_cap;
243
- static size_t g_indent; /* ctx.indentationLvl */
244
- static double g_cur_depth; /* ctx.currentDepth (last-entered composite) */
240
+ static SCR_TL InspFrame *g_frames;
241
+ static SCR_TL size_t g_nframes;
242
+ static SCR_TL size_t g_frames_cap;
243
+ static SCR_TL size_t g_indent; /* ctx.indentationLvl */
244
+ static SCR_TL double g_cur_depth; /* ctx.currentDepth (last-entered composite) */
245
245
 
246
246
  static InspFrame *insp_top(void) { return &g_frames[g_nframes - 1]; }
247
247
 
@@ -297,12 +297,12 @@ typedef struct {
297
297
  int id; /* circular id (0 while only on the stack) */
298
298
  } InspSeenEnt;
299
299
 
300
- static InspSeenEnt *g_seen;
301
- static size_t g_nseen;
302
- static size_t g_seen_cap;
300
+ static SCR_TL InspSeenEnt *g_seen;
301
+ static SCR_TL size_t g_nseen;
302
+ static SCR_TL size_t g_seen_cap;
303
303
  /* Detection-numbered circular targets (persist across the call). */
304
- static const void *g_circ[64];
305
- static int g_ncirc;
304
+ static SCR_TL const void *g_circ[64];
305
+ static SCR_TL int g_ncirc;
306
306
 
307
307
  static void insp_circ_reset(void) {
308
308
  g_nseen = 0;
package/src/scr_json.c CHANGED
@@ -31,7 +31,7 @@
31
31
  /* Live dyn-node count for the RC audit lane (-DSCR_RC_AUDIT); same contract
32
32
  * as scr_str_live_count in scr_string.c. */
33
33
  #ifdef SCR_RC_AUDIT
34
- static long scr_live_dyns = 0;
34
+ static SCR_TL long scr_live_dyns = 0;
35
35
  long scr_dyn_live_count(void) { return scr_live_dyns; }
36
36
  #endif
37
37
 
@@ -49,7 +49,7 @@ static void scr_json_oom(void) {
49
49
  /* The ScrStr block behind a non-empty buffer. */
50
50
  #define SCR_JB_STR(b) ((ScrStr *)((char *)(b)->data - offsetof(ScrStr, data)))
51
51
 
52
- static size_t scr_jb_hint = 64;
52
+ static SCR_TL size_t scr_jb_hint = 64;
53
53
 
54
54
  void scr_jb_init(ScrJsonBuf *b) {
55
55
  b->data = NULL;
@@ -280,9 +280,9 @@ void scr_jb_edge_idx(ScrJsonBuf *b, size_t i) {
280
280
  * (SEMANTICS.md documents the divergence). The emitted converters over
281
281
  * cycle-capable containers bracket their walks with enter/leave; the
282
282
  * stack is global (conversions never interleave). */
283
- static const void **g_td_seen;
284
- static size_t g_td_nseen;
285
- static size_t g_td_cap;
283
+ static SCR_TL const void **g_td_seen;
284
+ static SCR_TL size_t g_td_nseen;
285
+ static SCR_TL size_t g_td_cap;
286
286
 
287
287
  void scr_dyn_from_enter(const void *v) {
288
288
  for (size_t i = 0; i < g_td_nseen; i++) {
@@ -315,8 +315,8 @@ void scr_dyn_from_leave(void) {
315
315
  * strict alloc/free balance.
316
316
  */
317
317
  #ifndef SCR_RC_AUDIT
318
- static ScrDyn *scr_dyn_free_arr, *scr_dyn_free_obj, *scr_dyn_free_misc;
319
- static size_t scr_dyn_free_count;
318
+ static SCR_TL ScrDyn *scr_dyn_free_arr, *scr_dyn_free_obj, *scr_dyn_free_misc;
319
+ static SCR_TL size_t scr_dyn_free_count;
320
320
  #define SCR_DYN_FREE_MAX 8192
321
321
  #endif
322
322
 
@@ -818,7 +818,7 @@ ScrBytes *scr_dyn_bytes_copy_out(const ScrDyn *d) {
818
818
  * Per-chunk utf8 decode: a multibyte character split across chunks does
819
819
  * not re-join (Node's StringDecoder holds the partial byte); ASCII-clean
820
820
  * bodies — the overwhelming test shape — are exact. */
821
- static bool scr_dyn_chunk_utf8;
821
+ static SCR_TL bool scr_dyn_chunk_utf8;
822
822
  void scr_dyn_chunk_enc(bool utf8) { scr_dyn_chunk_utf8 = utf8; }
823
823
 
824
824
  /* One 'data' payload as the dyn value the current window dictates:
@@ -1122,7 +1122,7 @@ ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) {
1122
1122
  * this allocator view and installs the engine-routing ops — the
1123
1123
  * scr_dyn_alloc_promise story: a dynamic-free link never references
1124
1124
  * engine symbols, and JSVAL nodes exist only after an install. */
1125
- static const ScrDynJsvalOps *scr_dynjs_ops = NULL;
1125
+ static SCR_TL const ScrDynJsvalOps *scr_dynjs_ops = NULL;
1126
1126
 
1127
1127
  ScrDyn *scr_dyn_alloc_jsval(ScrJsval *cell, const ScrDynJsvalOps *ops) {
1128
1128
  scr_dynjs_ops = ops;
@@ -1256,8 +1256,8 @@ typedef struct {
1256
1256
  ScrDyn *dv; /* dyn entry (+1); NULL for handle/undefined entries */
1257
1257
  } ScrDynThisEnt;
1258
1258
 
1259
- static ScrDynThisEnt *scr_dyn_this_stack;
1260
- static size_t scr_dyn_this_n, scr_dyn_this_cap;
1259
+ static SCR_TL ScrDynThisEnt *scr_dyn_this_stack;
1260
+ static SCR_TL size_t scr_dyn_this_n, scr_dyn_this_cap;
1261
1261
 
1262
1262
  static ScrDynThisEnt *scr_dyn_this_grow(void) {
1263
1263
  if (scr_dyn_this_n == scr_dyn_this_cap) {
@@ -1526,9 +1526,9 @@ typedef struct {
1526
1526
  ScrDyn *dyn; /* retained */
1527
1527
  } ScrErrDynEnt;
1528
1528
 
1529
- static ScrErrDynEnt *scr_errdyn_cache = NULL;
1530
- static size_t scr_errdyn_n = 0, scr_errdyn_cap = 0;
1531
- static bool scr_errdyn_teardown_registered = false;
1529
+ static SCR_TL ScrErrDynEnt *scr_errdyn_cache = NULL;
1530
+ static SCR_TL size_t scr_errdyn_n = 0, scr_errdyn_cap = 0;
1531
+ static SCR_TL bool scr_errdyn_teardown_registered = false;
1532
1532
 
1533
1533
  static void scr_errdyn_teardown(void) {
1534
1534
  for (size_t i = 0; i < scr_errdyn_n; i++) {
package/src/scr_lib.c CHANGED
@@ -104,14 +104,14 @@ extern char **environ; /* env snapshot (scr_env_pairs) */
104
104
 
105
105
  /* ── process ─────────────────────────────────────────────────────────── */
106
106
 
107
- static int scr_lib_argc = 0;
108
- static char **scr_lib_argv = NULL;
109
- static ScrArr *scr_argv_arr = NULL; /* interned process.argv */
110
- static ScrStr *scr_platform_str = NULL; /* interned process.platform */
111
- static ScrStr *scr_exec_path_str = NULL; /* interned process.execPath */
112
- static ScrStr *scr_arch_str = NULL; /* interned process.arch */
113
- static ScrStr *scr_versions_node_str = NULL; /* interned process.versions.node */
114
- static ScrStr *scr_versions_openssl_str = NULL; /* interned process.versions.openssl */
107
+ static SCR_TL int scr_lib_argc = 0;
108
+ static SCR_TL char **scr_lib_argv = NULL;
109
+ static SCR_TL ScrArr *scr_argv_arr = NULL; /* interned process.argv */
110
+ static SCR_TL ScrStr *scr_platform_str = NULL; /* interned process.platform */
111
+ static SCR_TL ScrStr *scr_exec_path_str = NULL; /* interned process.execPath */
112
+ static SCR_TL ScrStr *scr_arch_str = NULL; /* interned process.arch */
113
+ static SCR_TL ScrStr *scr_versions_node_str = NULL; /* interned process.versions.node */
114
+ static SCR_TL ScrStr *scr_versions_openssl_str = NULL; /* interned process.versions.openssl */
115
115
 
116
116
  static void scr_lib_cleanup(void) {
117
117
  scr_arr_release(scr_argv_arr);
@@ -1103,8 +1103,27 @@ void scr_process_exit(double code) {
1103
1103
  * attribute monotonic stamp — the binary's own start, which is what
1104
1104
  * "the current Node.js process" means for a compiled program). */
1105
1105
  #ifdef _WIN32
1106
+ #if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
1107
+ /* The uptime anchor belongs to the host PROCESS, not to a library
1108
+ * instance. InitOnce keeps the lazy Windows spelling race-free when
1109
+ * several thread instances ask for the clock concurrently. */
1110
+ static INIT_ONCE scr_uptime_once = INIT_ONCE_STATIC_INIT;
1106
1111
  static double scr_uptime_t0_ms;
1112
+ static BOOL CALLBACK scr_uptime_anchor_once(PINIT_ONCE once, PVOID param,
1113
+ PVOID *ctx) {
1114
+ (void)once;
1115
+ (void)param;
1116
+ (void)ctx;
1117
+ scr_uptime_t0_ms = (double)GetTickCount64();
1118
+ return TRUE;
1119
+ }
1120
+ static void scr_uptime_anchor_init(void) {
1121
+ InitOnceExecuteOnce(&scr_uptime_once, scr_uptime_anchor_once, NULL, NULL);
1122
+ }
1123
+ #else
1124
+ static SCR_TL double scr_uptime_t0_ms;
1107
1125
  static void scr_uptime_anchor_init(void) { scr_uptime_t0_ms = (double)GetTickCount64(); }
1126
+ #endif
1108
1127
  static double scr_uptime_now_ms(void) { return (double)GetTickCount64(); }
1109
1128
  #else
1110
1129
  #include <sys/resource.h>
@@ -1112,6 +1131,10 @@ static double scr_uptime_now_ms(void) { return (double)GetTickCount64(); }
1112
1131
  #ifdef __APPLE__
1113
1132
  #include <mach/mach.h>
1114
1133
  #endif
1134
+ /* A load-time, process-wide anchor: the constructor runs on only the
1135
+ * initial thread, while thread-instanced archives are entered from worker
1136
+ * threads whose TLS slots would otherwise stay zero. It is immutable once
1137
+ * main starts, so sharing it adds no cross-instance mutable state. */
1115
1138
  static double scr_uptime_t0_ms;
1116
1139
  static double scr_uptime_now_ms(void) {
1117
1140
  struct timespec ts;
@@ -1124,7 +1147,9 @@ __attribute__((constructor)) static void scr_uptime_anchor_init(void) {
1124
1147
  #endif
1125
1148
 
1126
1149
  double scr_process_uptime(void) {
1127
- #ifdef _WIN32
1150
+ #if defined(_WIN32) && defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
1151
+ scr_uptime_anchor_init();
1152
+ #elif defined(_WIN32)
1128
1153
  if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
1129
1154
  #endif
1130
1155
  return (scr_uptime_now_ms() - scr_uptime_t0_ms) / 1000.0;
@@ -1134,7 +1159,9 @@ double scr_process_uptime(void) {
1134
1159
  * start (Node's timeOrigin anchor), fractional — the same monotonic
1135
1160
  * clock and anchor as uptime, in Node's performance.now units. */
1136
1161
  double scr_perf_now(void) {
1137
- #ifdef _WIN32
1162
+ #if defined(_WIN32) && defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
1163
+ scr_uptime_anchor_init();
1164
+ #elif defined(_WIN32)
1138
1165
  if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
1139
1166
  #endif
1140
1167
  return scr_uptime_now_ms() - scr_uptime_t0_ms;
@@ -1353,7 +1380,7 @@ double scr_available_memory(void) {
1353
1380
  /* process._exiting — true once the exit sequence began (set above and by
1354
1381
  * scr_run_exit_listeners in scr_events.c; the flag lives HERE so reading
1355
1382
  * it never forces the events unit into the link). */
1356
- bool scr_process_in_exit = false;
1383
+ SCR_TL bool scr_process_in_exit = false;
1357
1384
 
1358
1385
  bool scr_process_exiting(void) { return scr_process_in_exit; }
1359
1386
 
@@ -1372,7 +1399,7 @@ double scr_process_umask(double mask) {
1372
1399
  }
1373
1400
  return (double)prev;
1374
1401
  #elif defined(__wasi__)
1375
- static mode_t current = 022;
1402
+ static SCR_TL mode_t current = 022;
1376
1403
  mode_t prev = current;
1377
1404
  if (mask >= 0) current = (mode_t)mask;
1378
1405
  return (double)prev;
@@ -1399,7 +1426,7 @@ void scr_process_chdir(ScrStr *dir) {
1399
1426
  /* net's process-wide happy-eyeballs attempt budget (Node's
1400
1427
  * getDefaultAutoSelectFamilyAttemptTimeout pair, default 250ms). Lives in
1401
1428
  * the core unit so the knob never forces scr_net.c into the link. */
1402
- static double scr_net_autosel_timeout_ms = 250;
1429
+ static SCR_TL double scr_net_autosel_timeout_ms = 250;
1403
1430
 
1404
1431
  double scr_net_get_autosel_timeout(void) { return scr_net_autosel_timeout_ms; }
1405
1432
 
@@ -2745,8 +2772,8 @@ double scr_process_columns(double fd) {
2745
2772
  * NON-TTY stdin: Node's process.stdin is a Socket with no setRawMode
2746
2773
  * member at all, so the call throws Node's exact catchable TypeError. */
2747
2774
  #ifdef _WIN32
2748
- static DWORD scr_stdin_cooked;
2749
- static bool scr_stdin_cooked_saved = false;
2775
+ static SCR_TL DWORD scr_stdin_cooked;
2776
+ static SCR_TL bool scr_stdin_cooked_saved = false;
2750
2777
 
2751
2778
  void scr_process_stdin_set_raw_mode(bool raw) {
2752
2779
  if (!isatty(0)) {
@@ -2778,8 +2805,8 @@ void scr_process_stdin_set_raw_mode(bool raw) {
2778
2805
  scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
2779
2806
  }
2780
2807
  #else
2781
- static struct termios scr_stdin_cooked;
2782
- static bool scr_stdin_cooked_saved = false;
2808
+ static SCR_TL struct termios scr_stdin_cooked;
2809
+ static SCR_TL bool scr_stdin_cooked_saved = false;
2783
2810
 
2784
2811
  void scr_process_stdin_set_raw_mode(bool raw) {
2785
2812
  if (!isatty(0)) {
package/src/scr_library.c CHANGED
@@ -10,6 +10,24 @@
10
10
  * profile-agnostic and the program TU carries every profile-named symbol —
11
11
  * one place for the conformance symbol audit to look.
12
12
  *
13
+ * Multi-instance processes (the profile's abi.localize_runtime): the
14
+ * archive build demotes this file's externals — and every other runtime
15
+ * internal — to LOCAL symbols, so each linked archive carries its own
16
+ * private copy of all the state here: its own sink registration, poison
17
+ * flag, arena, and entry-symbol slot. Sinks therefore register per
18
+ * instance, and a trap poisons only the instance it fired in. Nothing in
19
+ * this file is thread-aware; the embedder contract is one thread per
20
+ * instance (an instance is never entered from two threads).
21
+ *
22
+ * Thread-instanced archives (the profile's abi.instance_per_thread) take
23
+ * the same story one level down: SCR_TL moves this file's state — and
24
+ * every other mutable static in the archive — into thread-local storage,
25
+ * so ONE linked archive serves one independent instance per embedder
26
+ * thread. A thread registers its own sink and calls the init entry before
27
+ * serving; its trap poisons only its own instance while sibling threads'
28
+ * instances keep answering. The one-thread-per-instance contract is
29
+ * unchanged — the calling thread IS the instance selector.
30
+ *
13
31
  * State after a sink call: none is legal. The trap fired mid-operation with
14
32
  * no unwinding — heap, arena, and collector state are unspecified; the library
15
33
  * is POISONED. Every runtime-touching entry prologue aborts on the flag
@@ -23,9 +41,9 @@
23
41
 
24
42
  /* ── sink registration + poison ───────────────────────────────────────── */
25
43
 
26
- static ScrLibSinkFn scr_library_sink = NULL;
27
- static void *scr_library_sink_ctx = NULL;
28
- static bool scr_library_poisoned = false;
44
+ static SCR_TL ScrLibSinkFn scr_library_sink = NULL;
45
+ static SCR_TL void *scr_library_sink_ctx = NULL;
46
+ static SCR_TL bool scr_library_poisoned = false;
29
47
 
30
48
  void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
31
49
  /* Latest registration wins; re-registration is permitted before a trap.
@@ -62,10 +80,12 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
62
80
 
63
81
  /* The current-entry slot: every generated entry's prologue records its
64
82
  * 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;
83
+ * is sound — exactly one core is live per copy of this state (the sole
84
+ * core in a classic process; each archive's own instance under
85
+ * abi.localize_runtime, where this slot is a per-instance local), entries
86
+ * never nest, and a trap can only fire while an entry is on the stack.
87
+ * NULL (never entered) renders as the empty symbol field. */
88
+ static SCR_TL const char *scr_library_entry_symbol = NULL;
69
89
 
70
90
  /* Detected-trap classification: the runtime's trap sites self-classify
71
91
  * through their message conventions (the exact bytes the executable lane
@@ -103,7 +123,7 @@ static _Noreturn void scr_library_trap_deliver(const char *msg, size_t len, uint
103
123
  * no malloc on the failure path; text truncates before structure ever
104
124
  * would (codes and symbols are short; an oversized remediation drops
105
125
  * whole, never split). */
106
- static char buf[2048];
126
+ static SCR_TL char buf[2048];
107
127
  const char *code = scr_library_trap_code(msg, len);
108
128
  const char *text = msg;
109
129
  size_t text_len = len;
@@ -168,7 +188,7 @@ __attribute__((noinline)) _Noreturn void scr_trap_len(const char *msg, size_t le
168
188
  }
169
189
 
170
190
  __attribute__((noinline)) _Noreturn void scr_trap_fmt(const char *fmt, ...) {
171
- static char buf[512]; /* no malloc on the invariant-failure path */
191
+ static SCR_TL char buf[512]; /* no malloc on the invariant-failure path */
172
192
  va_list ap;
173
193
  va_start(ap, fmt);
174
194
  int n = vsnprintf(buf, sizeof buf, fmt, ap);
@@ -202,8 +222,8 @@ typedef struct {
202
222
  bool is_str; /* ScrStr vs ScrBytes — picks the release */
203
223
  } ScrCoreArenaEnt;
204
224
 
205
- static ScrCoreArenaEnt *scr_library_arena = NULL;
206
- static size_t scr_library_arena_n = 0, scr_library_arena_cap = 0;
225
+ static SCR_TL ScrCoreArenaEnt *scr_library_arena = NULL;
226
+ static SCR_TL size_t scr_library_arena_n = 0, scr_library_arena_cap = 0;
207
227
 
208
228
  static void scr_library_arena_keep(void *v, bool is_str) {
209
229
  if (scr_library_arena_n == scr_library_arena_cap) {
@@ -285,8 +305,8 @@ void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len) {
285
305
  * satisfied because the entry persists. */
286
306
 
287
307
  #define SCR_LIB_MAX_RESETS 32
288
- static void (*scr_library_resets[SCR_LIB_MAX_RESETS])(void);
289
- static size_t scr_library_nresets = 0;
308
+ static SCR_TL void (*scr_library_resets[SCR_LIB_MAX_RESETS])(void);
309
+ static SCR_TL size_t scr_library_nresets = 0;
290
310
 
291
311
  void scr_library_register_reset(void (*fn)(void)) {
292
312
  for (size_t i = 0; i < scr_library_nresets; i++) {
package/src/scr_map.c CHANGED
@@ -24,7 +24,7 @@
24
24
  /* Live heap-map count for the RC audit lane (-DSCR_RC_AUDIT); same contract
25
25
  * as scr_str_live_count in scr_string.c. */
26
26
  #ifdef SCR_RC_AUDIT
27
- static long scr_live_maps = 0;
27
+ static SCR_TL long scr_live_maps = 0;
28
28
  long scr_map_live_count(void) { return scr_live_maps; }
29
29
  #endif
30
30
 
package/src/scr_object.c CHANGED
@@ -33,7 +33,7 @@ void scr_record_key_miss(ScrStr *k) {
33
33
  }
34
34
 
35
35
  #ifdef SCR_RC_AUDIT
36
- static long scr_live_objects = 0;
36
+ static SCR_TL long scr_live_objects = 0;
37
37
  long scr_obj_live_count(void) { return scr_live_objects; }
38
38
  void scr_obj_alloc_note(void) { scr_live_objects++; }
39
39
  void scr_obj_free_note(void) { scr_live_objects--; }
package/src/scr_regex.c CHANGED
@@ -82,8 +82,8 @@ void *lre_realloc(void *opaque, void *ptr, size_t size) {
82
82
  * allocation audit sees the bytecode gone).
83
83
  */
84
84
 
85
- static ScrRegex **scr_compiled = NULL;
86
- static size_t scr_compiled_len = 0, scr_compiled_cap = 0;
85
+ static SCR_TL ScrRegex **scr_compiled = NULL;
86
+ static SCR_TL size_t scr_compiled_len = 0, scr_compiled_cap = 0;
87
87
 
88
88
  static void scr_regex_free_bytecodes(void) {
89
89
  for (size_t i = 0; i < scr_compiled_len; i++) {
package/src/scr_runtime.h CHANGED
@@ -29,6 +29,24 @@ char *strcasestr(const char *hay, const char *needle);
29
29
  void arc4random_buf(void *buf, size_t n);
30
30
  #endif
31
31
 
32
+ /* ── thread-instanced library state ─────────────────────────────────────
33
+ * Archives built under the profile's abi.instance_per_thread compile every
34
+ * TU with -DSCR_THREAD_INSTANCES, and SCR_TL moves each unit's mutable
35
+ * state — and the emitted program's globals — into thread-local storage.
36
+ * A thread that calls the profile's init entry then owns a complete,
37
+ * independent instance: its own collector, result arena, panic-sink
38
+ * registration, poison flag, and program state. The instance's lifetime is
39
+ * the thread's; the one-thread-per-instance contract (an instance is never
40
+ * entered from two threads) is unchanged — this mode adds instances, not
41
+ * thread awareness. Expands to nothing everywhere else, so executable
42
+ * builds and classic library builds carry the exact bytes they always
43
+ * carried. Truly immutable tables (static const data) stay shared. */
44
+ #if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
45
+ #define SCR_TL _Thread_local
46
+ #else
47
+ #define SCR_TL
48
+ #endif
49
+
32
50
  /* ── process ──────────────────────────────────────────────────────────── */
33
51
 
34
52
  /* Called once at the top of main: private stdout formatter buffer,
@@ -445,7 +463,7 @@ enum {
445
463
  SCR_ERR_DOMEX = 4, /* DOMException — ScrDomException, the wider layout */
446
464
  };
447
465
 
448
- extern ScrVt scr_error_vts[5]; /* indexed by SCR_ERR_*; main() stamps pre/post */
466
+ extern SCR_TL ScrVt scr_error_vts[5]; /* indexed by SCR_ERR_*; main() stamps pre/post */
449
467
 
450
468
  struct ScrDyn; /* full declaration below (the checked-dynamic tree section) */
451
469
 
@@ -1373,8 +1391,8 @@ typedef struct ScrEmitter {
1373
1391
  const char *cls; /* display name for the leak warning ("EventEmitter") */
1374
1392
  } ScrEmitter;
1375
1393
 
1376
- extern ScrVt scr_emitter_vt; /* main() stamps pre/post */
1377
- extern double scr_emitter_default_max;
1394
+ extern SCR_TL ScrVt scr_emitter_vt; /* main() stamps pre/post */
1395
+ extern SCR_TL double scr_emitter_default_max;
1378
1396
 
1379
1397
  ScrEmitter *scr_emitter_new(void); /* +1, collector-headered */
1380
1398
  void scr_emitter_init(void *obj); /* super() into the prefix (no-op today) */
@@ -1976,7 +1994,7 @@ bool scr_process_kill_named(double pid, const ScrStr *signal);
1976
1994
  void scr_process_exit(double code);
1977
1995
  /* process._exiting: true once the exit sequence began (process.exit or
1978
1996
  * the exit-listener runner set the flag). Never throws. */
1979
- extern bool scr_process_in_exit;
1997
+ extern SCR_TL bool scr_process_in_exit;
1980
1998
  bool scr_process_exiting(void);
1981
1999
  /* umask(2): mask < 0 reads without setting; otherwise sets and answers
1982
2000
  * the previous mask. Never throws. */
package/src/scr_string.c CHANGED
@@ -10,7 +10,7 @@
10
10
  * leaks nothing and frees nothing twice (double-free shows up as ASan
11
11
  * use-after-free on the rc field or a negative count here). */
12
12
  #ifdef SCR_RC_AUDIT
13
- static long scr_live_strings = 0;
13
+ static SCR_TL long scr_live_strings = 0;
14
14
  long scr_str_live_count(void) { return scr_live_strings; }
15
15
  #endif
16
16
 
@@ -41,8 +41,8 @@ typedef struct {
41
41
  size_t u16len; /* SCR_U16_UNKNOWN until computed */
42
42
  size_t cu, cb; /* cursor: byte offset cb starts the char at unit cu */
43
43
  } ScrSidx;
44
- static ScrSidx scr_sidx_tab[SCR_SIDX_N];
45
- static unsigned scr_sidx_clock;
44
+ static SCR_TL ScrSidx scr_sidx_tab[SCR_SIDX_N];
45
+ static SCR_TL unsigned scr_sidx_clock;
46
46
 
47
47
  static void scr_sidx_purge(const ScrStr *s) {
48
48
  for (int i = 0; i < SCR_SIDX_N; i++) {
@@ -90,7 +90,7 @@ ScrStr *scr_str_new(const char *bytes, size_t len) {
90
90
  * blocks instead of paging in fresh zero-filled memory 40k times. Disabled
91
91
  * in the audit lane so ASan sees every logical free as a real free. */
92
92
  #ifndef SCR_RC_AUDIT
93
- static ScrStr *scr_str_spare;
93
+ static SCR_TL ScrStr *scr_str_spare;
94
94
  #endif
95
95
 
96
96
  /* A spare-block reuse must not waste grossly (cap <= 4x the need) and only
package/src/scr_symbol.c CHANGED
@@ -55,7 +55,7 @@ ScrSym *scr_sym_new(ScrStr *desc) {
55
55
 
56
56
  /* ── the Symbol.for global registry ──────────────────────────────────── */
57
57
 
58
- static ScrSym *g_sym_registry = NULL;
58
+ static SCR_TL ScrSym *g_sym_registry = NULL;
59
59
 
60
60
  static void scr_sym_registry_cleanup(void) {
61
61
  ScrSym *s = g_sym_registry;
package/src/scr_union.c CHANGED
@@ -15,7 +15,7 @@
15
15
  /* Live union count for the RC audit lane (-DSCR_RC_AUDIT); same contract
16
16
  * as scr_str_live_count in scr_string.c. */
17
17
  #ifdef SCR_RC_AUDIT
18
- static long scr_live_unions = 0;
18
+ static SCR_TL long scr_live_unions = 0;
19
19
  long scr_union_live_count(void) { return scr_live_unions; }
20
20
  #endif
21
21