@mikrojs/quickjs 0.17.0 → 0.17.1-pr-300.20260731003316

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/apply-patches.js CHANGED
@@ -14,7 +14,9 @@
14
14
  * @mikrojs/native (which calls the patched module-management symbols).
15
15
  */
16
16
  import {execFileSync} from 'node:child_process'
17
- import {existsSync, readdirSync} from 'node:fs'
17
+ import {createHash} from 'node:crypto'
18
+ import {existsSync, readdirSync, readFileSync, writeFileSync} from 'node:fs'
19
+ import {tmpdir} from 'node:os'
18
20
  import {dirname, join} from 'node:path'
19
21
  import {fileURLToPath} from 'node:url'
20
22
 
@@ -22,31 +24,116 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
22
24
  const qjsDir = join(__dirname, 'deps', 'quickjs')
23
25
  const patchesDir = join(__dirname, 'patches')
24
26
 
27
+ function seriesHash(files) {
28
+ const hash = createHash('sha256')
29
+ for (const f of files) hash.update(readFileSync(join(patchesDir, f)))
30
+ return hash.digest('hex')
31
+ }
32
+
33
+ function applyAll(files) {
34
+ for (const f of files) {
35
+ try {
36
+ execFileSync('git', ['apply', join(patchesDir, f)], {cwd: qjsDir, stdio: 'inherit'})
37
+ console.log(`@mikrojs/quickjs: applied patch ${f}`)
38
+ } catch (err) {
39
+ console.error(`@mikrojs/quickjs: failed to apply ${f}`, err.message)
40
+ process.exit(1)
41
+ }
42
+ }
43
+ }
44
+
25
45
  export function applyPatches() {
46
+ // Published tarballs ship pre-patched sources and exclude patches/ from
47
+ // the package files, so consumers return here and never need git.
26
48
  if (!existsSync(patchesDir)) return
27
49
  const files = readdirSync(patchesDir)
28
50
  .filter((f) => f.endsWith('.patch'))
29
51
  .sort()
30
- for (const f of files) {
31
- const patch = join(patchesDir, f)
32
- // Probe: is the patch already applied?
52
+ if (files.length === 0) return
53
+
54
+ // Patches may stack (a later patch can modify code an earlier one added),
55
+ // so they are only meaningful as a whole series applied in order to a
56
+ // pristine tree. A stamp records which series the tree currently carries.
57
+ const stampFile = join(qjsDir, '.patches-stamp')
58
+ const hash = seriesHash(files)
59
+ if (existsSync(stampFile) && readFileSync(stampFile, 'utf8').trim() === hash) {
60
+ // The stamp can outlive the tree it describes (e.g. `git submodule
61
+ // update` resets sources but leaves untracked files); trust it only if
62
+ // the series bounds are actually present. First and last together catch
63
+ // both a reset tree and a partially reverted one.
64
+ try {
65
+ for (const probe of [files[0], files[files.length - 1]]) {
66
+ execFileSync('git', ['apply', '--reverse', '--check', join(patchesDir, probe)], {
67
+ cwd: qjsDir,
68
+ stdio: 'pipe',
69
+ })
70
+ }
71
+ return // series already applied
72
+ } catch {
73
+ // stale stamp — fall through and re-apply
74
+ }
75
+ }
76
+
77
+ let inRepo = true
78
+ try {
79
+ execFileSync('git', ['-C', qjsDir, 'rev-parse', 'HEAD'], {stdio: 'pipe'})
80
+ } catch {
81
+ inRepo = false
82
+ }
83
+
84
+ if (!inRepo) {
85
+ // Sources without git metadata (should not happen: tarballs exclude
86
+ // patches/ and return above). Refuse rather than guess.
87
+ console.error('@mikrojs/quickjs: patches/ present but deps/quickjs is not a git checkout')
88
+ process.exit(1)
89
+ }
90
+
91
+ // In-repo: reset to the pinned commit, then apply the whole series.
92
+ // Only reset when the tree state is accounted for (a previous series
93
+ // stamp, a clean tree, or a tree matching the pre-stamp-era series);
94
+ // otherwise refuse rather than discard unknown local edits. Untracked
95
+ // files (editor droppings, agent scratch dirs) are ignored: they cannot
96
+ // conflict with `git apply` of tracked-file patches, and `checkout -- .`
97
+ // could not remove them anyway.
98
+ const dirty =
99
+ execFileSync('git', ['-C', qjsDir, 'status', '--porcelain', '--untracked-files=no'], {
100
+ encoding: 'utf8',
101
+ })
102
+ .split('\n')
103
+ .filter((l) => l.trim()).length > 0
104
+ if (dirty && !existsSync(stampFile)) {
105
+ // Pre-stamp checkouts have some prefix of the series applied; the
106
+ // first patch reverse-checking is strong evidence of that state.
33
107
  try {
34
- execFileSync('git', ['apply', '--reverse', '--check', patch], {
108
+ execFileSync('git', ['apply', '--reverse', '--check', join(patchesDir, files[0])], {
35
109
  cwd: qjsDir,
36
110
  stdio: 'pipe',
37
111
  })
38
- continue // already applied
39
112
  } catch {
40
- // not applied — fall through
41
- }
42
- try {
43
- execFileSync('git', ['apply', patch], {cwd: qjsDir, stdio: 'inherit'})
44
- console.log(`@mikrojs/quickjs: applied patch ${f}`)
45
- } catch (err) {
46
- console.error(`@mikrojs/quickjs: failed to apply ${f}`, err.message)
113
+ console.error(
114
+ '@mikrojs/quickjs: submodule tree has local changes that do not match the patch series.\n' +
115
+ `Commit them as a patch or discard them: git -C ${qjsDir} checkout -- .`,
116
+ )
47
117
  process.exit(1)
48
118
  }
49
119
  }
120
+ if (dirty) {
121
+ // The tree is about to be discarded and re-derived from the patch
122
+ // series. This also runs from CMake configure, i.e. mid-development
123
+ // when someone is iterating on a patch — keep a backup of whatever
124
+ // tracked changes are thrown away so an unregenerated edit is
125
+ // recoverable instead of silently gone.
126
+ const diff = execFileSync('git', ['-C', qjsDir, 'diff'], {encoding: 'utf8'})
127
+ if (diff.length > 0) {
128
+ const backup = join(tmpdir(), `quickjs-discarded-${Date.now()}.patch`)
129
+ writeFileSync(backup, diff)
130
+ console.log(`@mikrojs/quickjs: re-deriving submodule tree from the patch series`)
131
+ console.log(`@mikrojs/quickjs: previous tracked changes saved to ${backup}`)
132
+ }
133
+ execFileSync('git', ['-C', qjsDir, 'checkout', '--', '.'], {stdio: 'inherit'})
134
+ }
135
+ applyAll(files)
136
+ writeFileSync(stampFile, hash + '\n')
50
137
  }
51
138
 
52
139
  // Run directly: sync the submodule, then apply patches.
@@ -331,6 +331,65 @@ static const char main_c_template2[] =
331
331
 
332
332
  #define PROG_NAME "qjsc"
333
333
 
334
+ /* mikrojs patch: load a frozen atom table (see JS_PreloadFrozenAtoms) from
335
+ a little-endian binary file: u32 count, then per atom u32 len + bytes. */
336
+ static int load_frozen_atoms(JSRuntime *rt, const char *filename)
337
+ {
338
+ FILE *f;
339
+ uint8_t hdr[4];
340
+ uint32_t count = 0, i, len;
341
+ char **names = NULL;
342
+ uint32_t *lens = NULL;
343
+ int ret = -1;
344
+
345
+ f = fopen(filename, "rb");
346
+ if (!f) {
347
+ fprintf(stderr, "qjsc: could not open atom table '%s'\n", filename);
348
+ return -1;
349
+ }
350
+ if (fread(hdr, 1, 4, f) != 4)
351
+ goto done;
352
+ count = hdr[0] | (hdr[1] << 8) | (hdr[2] << 16) | ((uint32_t)hdr[3] << 24);
353
+ names = calloc(count, sizeof(*names));
354
+ lens = calloc(count, sizeof(*lens));
355
+ if (!names || !lens)
356
+ goto done;
357
+ for (i = 0; i < count; i++) {
358
+ if (fread(hdr, 1, 4, f) != 4)
359
+ goto done;
360
+ len = hdr[0] | (hdr[1] << 8) | (hdr[2] << 16) | ((uint32_t)hdr[3] << 24);
361
+ if (len > (1u << 20))
362
+ goto done; /* corrupted table; also guards the len + 1 overflow */
363
+ names[i] = malloc(len + 1);
364
+ if (!names[i])
365
+ goto done;
366
+ if (len != 0 && fread(names[i], 1, len, f) != len)
367
+ goto done;
368
+ names[i][len] = '\0';
369
+ lens[i] = len;
370
+ }
371
+ if (JS_PreloadFrozenAtoms(rt, (const char *const *)names, lens, count)) {
372
+ fprintf(stderr, "qjsc: failed to preload frozen atom table\n");
373
+ ret = -2; /* already reported */
374
+ goto done;
375
+ }
376
+ /* The runtime keeps pointers into names/lens for lazy atom
377
+ materialization; leak them intentionally (process-lifetime tool). */
378
+ fclose(f);
379
+ return 0;
380
+ done:
381
+ if (ret == -1)
382
+ fprintf(stderr, "qjsc: bad atom table '%s'\n", filename);
383
+ if (names) {
384
+ for (i = 0; i < count; i++)
385
+ free(names[i]);
386
+ free(names);
387
+ }
388
+ free(lens);
389
+ fclose(f);
390
+ return ret;
391
+ }
392
+
334
393
  void help(void)
335
394
  {
336
395
  printf("QuickJS-ng Compiler version %s\n"
@@ -349,6 +408,7 @@ void help(void)
349
408
  "-p prefix set the prefix of the generated C names\n"
350
409
  "-P do not add default system modules\n"
351
410
  "-s strip the source code, specify twice to also strip debug info\n"
411
+ "-A file preload a frozen atom table (binary: u32le count, then u32le len + bytes per atom)\n"
352
412
  "-S n set the maximum stack size to 'n' bytes (default=%d)\n",
353
413
  JS_GetVersion(),
354
414
  JS_DEFAULT_STACK_SIZE);
@@ -407,6 +467,8 @@ int main(int argc, char **argv)
407
467
  namelist_t dynamic_module_list;
408
468
  bool load_system_modules = true;
409
469
 
470
+ const char *frozen_atoms_file = NULL;
471
+
410
472
  out_filename = NULL;
411
473
  script_name = NULL;
412
474
  output_type = OUTPUT_C;
@@ -462,6 +524,14 @@ int main(int argc, char **argv)
462
524
  output_type = OUTPUT_C_MAIN;
463
525
  continue;
464
526
  }
527
+ if (opt == 'A') {
528
+ if (!optarg) {
529
+ check_hasarg(optind, argc, opt);
530
+ optarg = argv[optind++];
531
+ }
532
+ frozen_atoms_file = optarg;
533
+ continue;
534
+ }
465
535
  if (opt == 'n') {
466
536
  if (!optarg) {
467
537
  check_hasarg(optind, argc, opt);
@@ -571,6 +641,8 @@ int main(int argc, char **argv)
571
641
  outfile = fo;
572
642
 
573
643
  rt = JS_NewRuntime();
644
+ if (frozen_atoms_file && load_frozen_atoms(rt, frozen_atoms_file))
645
+ exit(1);
574
646
  ctx = JS_NewContext(rt);
575
647
 
576
648
  /* loader for ES6 modules */
@@ -279,6 +279,18 @@ struct JSRuntime {
279
279
  uint32_t *atom_hash;
280
280
  JSAtomStruct **atom_array;
281
281
  int atom_free_index; /* 0 = none */
282
+ /* mikrojs patch: frozen atom table (JS_PreloadFrozenAtoms). Atoms below
283
+ frozen_atom_end serialize as final ids; hash is nonzero once loaded.
284
+ Ids in [frozen_first, frozen_first + frozen_count) are reserved slots
285
+ whose JSString bodies materialize lazily from the (flash-resident)
286
+ frozen_names table on first string use. */
287
+ uint32_t frozen_atom_end;
288
+ uint32_t frozen_atom_hash;
289
+ const char *const *frozen_names;
290
+ const uint32_t *frozen_lens;
291
+ uint32_t *frozen_sorted; /* rt-owned indexes into frozen_names, sorted */
292
+ uint32_t frozen_count;
293
+ JSAtom frozen_first;
282
294
 
283
295
  JSClassID js_class_id_alloc; /* counter for user defined classes */
284
296
  int class_count; /* size of class_array */
@@ -790,7 +802,13 @@ typedef struct JSFunctionBytecode {
790
802
  uint8_t super_allowed : 1;
791
803
  uint8_t arguments_allowed : 1;
792
804
  uint8_t backtrace_barrier : 1; /* stop backtrace on this function */
793
- /* XXX: 5 bits available */
805
+ /* mikrojs patch: byte_code_buf points into the (ROM) input buffer of
806
+ JS_ReadObject and must not be freed or written */
807
+ uint8_t bc_in_rom : 1;
808
+ /* mikrojs patch: byte_code_buf is a separate heap allocation instead of
809
+ the tail of the function block (in-place read fallback) */
810
+ uint8_t bc_separate : 1;
811
+ /* XXX: 3 bits available */
794
812
  uint8_t *byte_code_buf; /* (self pointer) */
795
813
  int byte_code_len;
796
814
  JSAtom func_name;
@@ -1598,6 +1616,15 @@ static void js_trigger_gc(JSRuntime *rt, size_t size)
1598
1616
  JS_RunGC(rt);
1599
1617
  rt->malloc_gc_threshold = rt->malloc_state.malloc_size +
1600
1618
  (rt->malloc_state.malloc_size >> 1);
1619
+ /* mikrojs patch: never re-arm the threshold above the memory limit,
1620
+ else a single long job whose live size exceeds ~2/3 of the limit
1621
+ accumulates collectible garbage straight into OOM. */
1622
+ if (rt->malloc_state.malloc_limit &&
1623
+ rt->malloc_gc_threshold >
1624
+ rt->malloc_state.malloc_limit - (rt->malloc_state.malloc_limit >> 3)) {
1625
+ rt->malloc_gc_threshold =
1626
+ rt->malloc_state.malloc_limit - (rt->malloc_state.malloc_limit >> 3);
1627
+ }
1601
1628
  }
1602
1629
  }
1603
1630
 
@@ -1991,6 +2018,7 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque)
1991
2018
 
1992
2019
  if (JS_InitAtoms(rt))
1993
2020
  goto fail;
2021
+ rt->frozen_atom_end = JS_ATOM_END;
1994
2022
 
1995
2023
  /* create the object, array and function classes */
1996
2024
  if (init_class_range(rt, js_std_class_def, JS_CLASS_OBJECT,
@@ -2216,6 +2244,33 @@ static inline JSAtomStruct *atom_set_free(uint32_t v)
2216
2244
  return (JSAtomStruct *)(((uintptr_t)v << 1) | 1);
2217
2245
  }
2218
2246
 
2247
+ /* mikrojs patch: shared placeholder occupying reserved frozen-atom slots
2248
+ until their string materializes. Deliberately a valid, readable JSString
2249
+ (empty, narrow, type STRING) so walkers that only inspect atom_type/kind
2250
+ need no guard; only string-producing paths and the free/teardown loops
2251
+ check for it. Never hashed, never refcounted, never freed. */
2252
+ static JSAtomStruct js_lazy_atom_placeholder = {
2253
+ .header = {.ref_count = 1},
2254
+ .len = 0,
2255
+ .is_wide_char = 0,
2256
+ .hash = 0,
2257
+ .kind = JS_STRING_KIND_NORMAL,
2258
+ .atom_type = JS_ATOM_TYPE_STRING,
2259
+ .hash_next = 0,
2260
+ };
2261
+
2262
+ static inline bool js_atom_is_lazy(const JSAtomStruct *p)
2263
+ {
2264
+ return p == &js_lazy_atom_placeholder;
2265
+ }
2266
+
2267
+ /* mikrojs patch: frozen atoms are refcount-exempt like predefined atoms;
2268
+ the runtime pins them for its whole lifetime. */
2269
+ static inline bool js_atom_is_frozen_id(const JSRuntime *rt, JSAtom v)
2270
+ {
2271
+ return v - rt->frozen_first < rt->frozen_count;
2272
+ }
2273
+
2219
2274
  /* Note: the string contents are uninitialized */
2220
2275
  static JSString *js_alloc_string_rt(JSRuntime *rt, int max_len, int is_wide_char)
2221
2276
  {
@@ -2363,7 +2418,7 @@ void JS_FreeRuntime(JSRuntime *rt)
2363
2418
 
2364
2419
  for(i = 0; i < rt->atom_size; i++) {
2365
2420
  JSAtomStruct *p = rt->atom_array[i];
2366
- if (!atom_is_free(p) /* && p->str*/) {
2421
+ if (!atom_is_free(p) && !js_atom_is_lazy(p) /* && p->str*/) {
2367
2422
  if (i >= JS_ATOM_END || p->header.ref_count != 1) {
2368
2423
  if (!header_done) {
2369
2424
  header_done = true;
@@ -2418,7 +2473,7 @@ void JS_FreeRuntime(JSRuntime *rt)
2418
2473
  /* free the atoms */
2419
2474
  for(i = 0; i < rt->atom_size; i++) {
2420
2475
  JSAtomStruct *p = rt->atom_array[i];
2421
- if (!atom_is_free(p)) {
2476
+ if (!atom_is_free(p) && !js_atom_is_lazy(p)) {
2422
2477
  #ifdef ENABLE_DUMPS // JS_DUMP_LEAKS
2423
2478
  list_del(&p->link);
2424
2479
  #endif
@@ -2427,6 +2482,7 @@ void JS_FreeRuntime(JSRuntime *rt)
2427
2482
  }
2428
2483
  js_free_rt(rt, rt->atom_array);
2429
2484
  js_free_rt(rt, rt->atom_hash);
2485
+ js_free_rt(rt, rt->frozen_sorted);
2430
2486
  js_free_rt(rt, rt->shape_hash);
2431
2487
  #ifdef ENABLE_DUMPS // JS_DUMP_LEAKS
2432
2488
  if (check_dump_flag(rt, JS_DUMP_LEAKS) && !list_empty(&rt->string_list)) {
@@ -2952,7 +3008,7 @@ static __maybe_unused void JS_DumpAtoms(JSRuntime *rt)
2952
3008
  printf("JSAtom table: {\n");
2953
3009
  for(i = 0; i < rt->atom_size; i++) {
2954
3010
  p = rt->atom_array[i];
2955
- if (!atom_is_free(p)) {
3011
+ if (!atom_is_free(p) && !js_atom_is_lazy(p)) {
2956
3012
  printf(" %d: { %d %08x ", i, p->atom_type, p->hash);
2957
3013
  if (!(p->len == 0 && p->is_wide_char != 0))
2958
3014
  JS_DumpString(rt, p);
@@ -3025,7 +3081,7 @@ JSAtom JS_DupAtomRT(JSRuntime *rt, JSAtom v)
3025
3081
  {
3026
3082
  JSAtomStruct *p;
3027
3083
 
3028
- if (!__JS_AtomIsConst(v)) {
3084
+ if (!__JS_AtomIsConst(v) && !js_atom_is_frozen_id(rt, v)) {
3029
3085
  p = rt->atom_array[v];
3030
3086
  p->header.ref_count++;
3031
3087
  }
@@ -3037,8 +3093,8 @@ JSAtom JS_DupAtom(JSContext *ctx, JSAtom v)
3037
3093
  JSRuntime *rt;
3038
3094
  JSAtomStruct *p;
3039
3095
 
3040
- if (!__JS_AtomIsConst(v)) {
3041
- rt = ctx->rt;
3096
+ rt = ctx->rt;
3097
+ if (!__JS_AtomIsConst(v) && !js_atom_is_frozen_id(rt, v)) {
3042
3098
  p = rt->atom_array[v];
3043
3099
  p->header.ref_count++;
3044
3100
  }
@@ -3091,6 +3147,133 @@ static JSAtom js_get_atom_index(JSRuntime *rt, JSAtomStruct *p)
3091
3147
  return i;
3092
3148
  }
3093
3149
 
3150
+ /* mikrojs patch: compare a JSString against a frozen table entry with a
3151
+ total order (length first, then charwise numeric) matching the order
3152
+ frozen_sorted was built with. */
3153
+ static int js_frozen_cmp_str(JSString *str, const uint8_t *name,
3154
+ uint32_t nlen)
3155
+ {
3156
+ uint32_t i, len = str->len;
3157
+ if (len != nlen)
3158
+ return len < nlen ? -1 : 1;
3159
+ for (i = 0; i < len; i++) {
3160
+ uint32_t c = str->is_wide_char ? str16(str)[i] : str8(str)[i];
3161
+ if (c != name[i])
3162
+ return c < name[i] ? -1 : 1;
3163
+ }
3164
+ return 0;
3165
+ }
3166
+
3167
+ /* mikrojs patch: find a frozen atom id by content, or JS_ATOM_NULL. */
3168
+ static JSAtom js_frozen_lookup(JSRuntime *rt, JSString *str)
3169
+ {
3170
+ uint32_t lo = 0, hi = rt->frozen_count;
3171
+ while (lo < hi) {
3172
+ uint32_t mid = (lo + hi) / 2;
3173
+ uint32_t idx = rt->frozen_sorted[mid];
3174
+ int cmp = js_frozen_cmp_str(str, (const uint8_t *)rt->frozen_names[idx],
3175
+ rt->frozen_lens[idx]);
3176
+ if (cmp == 0)
3177
+ return rt->frozen_first + idx;
3178
+ if (cmp < 0)
3179
+ hi = mid;
3180
+ else
3181
+ lo = mid + 1;
3182
+ }
3183
+ return JS_ATOM_NULL;
3184
+ }
3185
+
3186
+ /* mikrojs patch: materialize a reserved frozen atom's JSString from the
3187
+ flash name table and insert it into the hash. Returns NULL on OOM. */
3188
+ static JSAtomStruct *js_frozen_materialize(JSRuntime *rt, JSAtom v)
3189
+ {
3190
+ uint32_t idx = v - rt->frozen_first;
3191
+ const char *name = rt->frozen_names[idx];
3192
+ uint32_t len = rt->frozen_lens[idx];
3193
+ uint32_t h, h1;
3194
+ JSString *p;
3195
+
3196
+ p = js_alloc_string_rt(rt, len, 0);
3197
+ if (!p)
3198
+ return NULL;
3199
+ memcpy(str8(p), name, len);
3200
+ str8(p)[len] = '\0';
3201
+ h = hash_string8((const uint8_t *)name, len, JS_ATOM_TYPE_STRING);
3202
+ h &= JS_ATOM_HASH_MASK;
3203
+ p->hash = h;
3204
+ p->atom_type = JS_ATOM_TYPE_STRING;
3205
+ p->first_weak_ref = NULL;
3206
+ rt->atom_array[v] = p;
3207
+ h1 = h & (rt->atom_hash_size - 1);
3208
+ p->hash_next = rt->atom_hash[h1];
3209
+ rt->atom_hash[h1] = v;
3210
+ rt->atom_count++;
3211
+ if (unlikely(rt->atom_count >= rt->atom_count_resize))
3212
+ JS_ResizeAtomHash(rt, rt->atom_hash_size * 2);
3213
+ return p;
3214
+ }
3215
+
3216
+ /* mikrojs patch: atom_array access that materializes reserved frozen slots
3217
+ on demand; NULL only on OOM. */
3218
+ static JSAtomStruct *js_atom_ensure_str(JSRuntime *rt, JSAtom v)
3219
+ {
3220
+ JSAtomStruct *p = rt->atom_array[v];
3221
+ if (unlikely(js_atom_is_lazy(p)))
3222
+ p = js_frozen_materialize(rt, v);
3223
+ return p;
3224
+ }
3225
+
3226
+ /* mikrojs patch: grow the atom array so at least one free slot exists.
3227
+ Extracted from __JS_NewAtom so frozen-slot reservation can share it. */
3228
+ static int js_atom_grow_free(JSRuntime *rt)
3229
+ {
3230
+ uint32_t new_size, start, i;
3231
+ JSAtomStruct **new_array;
3232
+ JSAtomStruct *p;
3233
+
3234
+ /* alloc new with size progression 3/2:
3235
+ 4 6 9 13 19 28 42 63 94 141 211 316 474 711 1066 1599 2398 3597 5395 8092
3236
+ preallocating space for predefined atoms (at least 504).
3237
+ */
3238
+ new_size = max_int(711, rt->atom_size * 3 / 2);
3239
+ if (new_size > JS_ATOM_MAX)
3240
+ return -1;
3241
+ /* XXX: should use realloc2 to use slack space */
3242
+ new_array = js_realloc_rt(rt, rt->atom_array, sizeof(*new_array) * new_size);
3243
+ if (!new_array)
3244
+ return -1;
3245
+ /* Note: the atom 0 is not used */
3246
+ start = rt->atom_size;
3247
+ if (start == 0) {
3248
+ /* JS_ATOM_NULL entry */
3249
+ p = js_mallocz_rt(rt, sizeof(JSAtomStruct));
3250
+ if (!p) {
3251
+ js_free_rt(rt, new_array);
3252
+ return -1;
3253
+ }
3254
+ p->header.ref_count = 1; /* not refcounted */
3255
+ p->atom_type = JS_ATOM_TYPE_SYMBOL;
3256
+ #ifdef ENABLE_DUMPS // JS_DUMP_LEAKS
3257
+ list_add_tail(&p->link, &rt->string_list);
3258
+ #endif
3259
+ new_array[0] = p;
3260
+ rt->atom_count++;
3261
+ start = 1;
3262
+ }
3263
+ rt->atom_size = new_size;
3264
+ rt->atom_array = new_array;
3265
+ rt->atom_free_index = start;
3266
+ for(i = start; i < new_size; i++) {
3267
+ uint32_t next;
3268
+ if (i == (new_size - 1))
3269
+ next = 0;
3270
+ else
3271
+ next = i + 1;
3272
+ rt->atom_array[i] = atom_set_free(next);
3273
+ }
3274
+ return 0;
3275
+ }
3276
+
3094
3277
  /* string case (internal). Return JS_ATOM_NULL if error. 'str' is
3095
3278
  freed. */
3096
3279
  static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type)
@@ -3105,7 +3288,7 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type)
3105
3288
  /* str is the atom, return its index */
3106
3289
  i = js_get_atom_index(rt, str);
3107
3290
  /* reduce string refcount and increase atom's unless constant */
3108
- if (__JS_AtomIsConst(i))
3291
+ if (__JS_AtomIsConst(i) || js_atom_is_frozen_id(rt, i))
3109
3292
  str->header.ref_count--;
3110
3293
  return i;
3111
3294
  }
@@ -3121,12 +3304,23 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type)
3121
3304
  p->atom_type == atom_type &&
3122
3305
  p->len == len &&
3123
3306
  js_string_memcmp(p, str, len) == 0) {
3124
- if (!__JS_AtomIsConst(i))
3307
+ if (!__JS_AtomIsConst(i) && !js_atom_is_frozen_id(rt, i))
3125
3308
  p->header.ref_count++;
3126
3309
  goto done;
3127
3310
  }
3128
3311
  i = p->hash_next;
3129
3312
  }
3313
+ /* mikrojs patch: an equal string may exist as a reserved frozen
3314
+ atom that is not in the hash yet; materialize it instead of
3315
+ creating a second atom with a different id */
3316
+ if (atom_type == JS_ATOM_TYPE_STRING && rt->frozen_count != 0) {
3317
+ i = js_frozen_lookup(rt, str);
3318
+ if (i != JS_ATOM_NULL && js_atom_is_lazy(rt->atom_array[i])) {
3319
+ if (!js_frozen_materialize(rt, i))
3320
+ goto fail;
3321
+ goto done; /* frozen: refcount-exempt, str freed at done */
3322
+ }
3323
+ }
3130
3324
  } else {
3131
3325
  h1 = 0; /* avoid warning */
3132
3326
  if (atom_type == JS_ATOM_TYPE_SYMBOL) {
@@ -3139,49 +3333,8 @@ static JSAtom __JS_NewAtom(JSRuntime *rt, JSString *str, int atom_type)
3139
3333
 
3140
3334
  if (rt->atom_free_index == 0) {
3141
3335
  /* allow new atom entries */
3142
- uint32_t new_size, start;
3143
- JSAtomStruct **new_array;
3144
-
3145
- /* alloc new with size progression 3/2:
3146
- 4 6 9 13 19 28 42 63 94 141 211 316 474 711 1066 1599 2398 3597 5395 8092
3147
- preallocating space for predefined atoms (at least 504).
3148
- */
3149
- new_size = max_int(711, rt->atom_size * 3 / 2);
3150
- if (new_size > JS_ATOM_MAX)
3336
+ if (js_atom_grow_free(rt))
3151
3337
  goto fail;
3152
- /* XXX: should use realloc2 to use slack space */
3153
- new_array = js_realloc_rt(rt, rt->atom_array, sizeof(*new_array) * new_size);
3154
- if (!new_array)
3155
- goto fail;
3156
- /* Note: the atom 0 is not used */
3157
- start = rt->atom_size;
3158
- if (start == 0) {
3159
- /* JS_ATOM_NULL entry */
3160
- p = js_mallocz_rt(rt, sizeof(JSAtomStruct));
3161
- if (!p) {
3162
- js_free_rt(rt, new_array);
3163
- goto fail;
3164
- }
3165
- p->header.ref_count = 1; /* not refcounted */
3166
- p->atom_type = JS_ATOM_TYPE_SYMBOL;
3167
- #ifdef ENABLE_DUMPS // JS_DUMP_LEAKS
3168
- list_add_tail(&p->link, &rt->string_list);
3169
- #endif
3170
- new_array[0] = p;
3171
- rt->atom_count++;
3172
- start = 1;
3173
- }
3174
- rt->atom_size = new_size;
3175
- rt->atom_array = new_array;
3176
- rt->atom_free_index = start;
3177
- for(i = start; i < new_size; i++) {
3178
- uint32_t next;
3179
- if (i == (new_size - 1))
3180
- next = 0;
3181
- else
3182
- next = i + 1;
3183
- rt->atom_array[i] = atom_set_free(next);
3184
- }
3185
3338
  }
3186
3339
 
3187
3340
  if (str) {
@@ -3262,6 +3415,132 @@ static JSAtom __JS_NewAtomInit(JSRuntime *rt, const char *str, int len,
3262
3415
  return __JS_NewAtom(rt, p, atom_type);
3263
3416
  }
3264
3417
 
3418
+ typedef struct JSFrozenSortCtx {
3419
+ const char *const *names;
3420
+ const uint32_t *lens;
3421
+ } JSFrozenSortCtx;
3422
+
3423
+ static int js_frozen_sort_cmp(const void *a, const void *b, void *opaque)
3424
+ {
3425
+ JSFrozenSortCtx *c = opaque;
3426
+ uint32_t ia = *(const uint32_t *)a, ib = *(const uint32_t *)b;
3427
+ uint32_t la = c->lens[ia], lb = c->lens[ib];
3428
+ int cmp;
3429
+ if (la != lb)
3430
+ return la < lb ? -1 : 1;
3431
+ cmp = memcmp(c->names[ia], c->names[ib], la);
3432
+ return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
3433
+ }
3434
+
3435
+ /* mikrojs patch: reserve a contiguous id range for a fixed atom table so
3436
+ * blobs compiled against the same list (in a runtime that ran the same
3437
+ * preload) reference them by final id with no relocation at load time.
3438
+ * The JSString bodies are NOT allocated here: reserved slots hold a shared
3439
+ * placeholder and materialize lazily from `names` on first string use, so
3440
+ * the boot-time heap cost is one atom_array slot per name. `names`/`lens`
3441
+ * must outlive the runtime (e.g. rodata). The writer/reader mapping is
3442
+ * validated via a hash over the pre-preload dynamic atoms and the table,
3443
+ * so any divergence rejects blobs instead of silently resolving atoms to
3444
+ * the wrong strings. Names must be raw 8-bit, unique, non-numeric strings.
3445
+ * Must run before any dynamic atom is freed. Returns 0 on success. */
3446
+ int JS_PreloadFrozenAtoms(JSRuntime *rt, const char *const *names,
3447
+ const uint32_t *lens, uint32_t count)
3448
+ {
3449
+ uint32_t i, h, slot, reserved;
3450
+ uint32_t *sorted;
3451
+ JSAtom first = 0;
3452
+ JSFrozenSortCtx sctx = {names, lens};
3453
+
3454
+ if (rt->frozen_atom_hash != 0 || rt->frozen_count != 0)
3455
+ return -1; /* already preloaded */
3456
+ if (count == 0)
3457
+ return 0;
3458
+
3459
+ sorted = js_malloc_rt(rt, sizeof(*sorted) * count);
3460
+ if (!sorted)
3461
+ return -1;
3462
+ for (i = 0; i < count; i++)
3463
+ sorted[i] = i;
3464
+ rqsort(sorted, count, sizeof(*sorted), js_frozen_sort_cmp, &sctx);
3465
+ for (i = 1; i < count; i++) {
3466
+ if (js_frozen_sort_cmp(&sorted[i - 1], &sorted[i], &sctx) == 0)
3467
+ goto fail_sorted; /* duplicate names break the id mapping */
3468
+ }
3469
+
3470
+ /* reserve contiguous ids; the free list is sequential on a fresh
3471
+ runtime, so pops come out ascending */
3472
+ reserved = 0;
3473
+ for (i = 0; i < count; i++) {
3474
+ if (rt->atom_free_index == 0 && js_atom_grow_free(rt))
3475
+ goto fail_slots;
3476
+ slot = rt->atom_free_index;
3477
+ rt->atom_free_index = atom_get_free(rt->atom_array[slot]);
3478
+ rt->atom_array[slot] = &js_lazy_atom_placeholder;
3479
+ if (i == 0) {
3480
+ first = slot;
3481
+ } else if (slot != first + i) {
3482
+ /* atoms were freed before preload; put this stray slot back
3483
+ first, then unwind the sequential prefix */
3484
+ rt->atom_array[slot] = atom_set_free(rt->atom_free_index);
3485
+ rt->atom_free_index = slot;
3486
+ goto fail_slots;
3487
+ }
3488
+ reserved++;
3489
+ }
3490
+
3491
+ /* hash the pre-preload dynamic atoms (identical on writer and reader
3492
+ by construction) plus the frozen names: any divergence changes the
3493
+ mapping, so it must change the hash */
3494
+ h = 5381;
3495
+ for (i = JS_ATOM_END; i < (uint32_t)first; i++) {
3496
+ JSAtomStruct *p = rt->atom_array[i];
3497
+ if (atom_is_free(p)) {
3498
+ h = h * 33 ^ 0xfe;
3499
+ continue;
3500
+ }
3501
+ h = h * 33 ^ p->atom_type;
3502
+ if (p->is_wide_char) {
3503
+ uint32_t j;
3504
+ for (j = 0; j < p->len; j++) {
3505
+ h = h * 33 ^ (str16(p)[j] & 0xff);
3506
+ h = h * 33 ^ (str16(p)[j] >> 8);
3507
+ }
3508
+ } else {
3509
+ uint32_t j;
3510
+ for (j = 0; j < p->len; j++)
3511
+ h = h * 33 ^ str8(p)[j];
3512
+ }
3513
+ h = h * 33 ^ 0xff;
3514
+ }
3515
+ for (i = 0; i < count; i++) {
3516
+ const uint8_t *nb = (const uint8_t *)names[i];
3517
+ uint32_t j;
3518
+ for (j = 0; j < lens[i]; j++)
3519
+ h = h * 33 ^ nb[j];
3520
+ h = h * 33 ^ 0xff;
3521
+ }
3522
+ h = h * 33 ^ first;
3523
+
3524
+ rt->frozen_names = names;
3525
+ rt->frozen_lens = lens;
3526
+ rt->frozen_sorted = sorted;
3527
+ rt->frozen_count = count;
3528
+ rt->frozen_first = first;
3529
+ rt->frozen_atom_end = first + count;
3530
+ rt->frozen_atom_hash = h | 1; /* nonzero marks "loaded" */
3531
+ return 0;
3532
+
3533
+ fail_slots:
3534
+ /* return reserved slots to the free list */
3535
+ for (i = 0; i < reserved; i++) {
3536
+ rt->atom_array[first + i] = atom_set_free(rt->atom_free_index);
3537
+ rt->atom_free_index = first + i;
3538
+ }
3539
+ fail_sorted:
3540
+ js_free_rt(rt, sorted);
3541
+ return -1;
3542
+ }
3543
+
3265
3544
  // XXX: `str` must be raw 8-bit contents. No UTF-8 encoded strings
3266
3545
  static JSAtom __JS_FindAtom(JSRuntime *rt, const char *str, size_t len,
3267
3546
  int atom_type)
@@ -3280,12 +3559,42 @@ static JSAtom __JS_FindAtom(JSRuntime *rt, const char *str, size_t len,
3280
3559
  p->len == len &&
3281
3560
  p->is_wide_char == 0 &&
3282
3561
  memcmp(str8(p), str, len) == 0) {
3283
- if (!__JS_AtomIsConst(i))
3562
+ if (!__JS_AtomIsConst(i) && !js_atom_is_frozen_id(rt, i))
3284
3563
  p->header.ref_count++;
3285
3564
  return i;
3286
3565
  }
3287
3566
  i = p->hash_next;
3288
3567
  }
3568
+ /* mikrojs patch: a reserved frozen atom exists even before it is in
3569
+ the hash; report it (materialized) rather than "not found" */
3570
+ if (atom_type == JS_ATOM_TYPE_STRING && rt->frozen_count != 0) {
3571
+ uint32_t lo = 0, hi = rt->frozen_count;
3572
+ while (lo < hi) {
3573
+ uint32_t mid = (lo + hi) / 2;
3574
+ uint32_t idx = rt->frozen_sorted[mid];
3575
+ uint32_t nlen = rt->frozen_lens[idx];
3576
+ const uint8_t *name = (const uint8_t *)rt->frozen_names[idx];
3577
+ int cmp;
3578
+ if (len != nlen) {
3579
+ cmp = len < nlen ? -1 : 1;
3580
+ } else {
3581
+ cmp = memcmp(str, name, len);
3582
+ if (cmp != 0)
3583
+ cmp = cmp < 0 ? -1 : 1;
3584
+ }
3585
+ if (cmp == 0) {
3586
+ i = rt->frozen_first + idx;
3587
+ if (js_atom_is_lazy(rt->atom_array[i]) &&
3588
+ !js_frozen_materialize(rt, i))
3589
+ return JS_ATOM_NULL;
3590
+ return i;
3591
+ }
3592
+ if (cmp < 0)
3593
+ hi = mid;
3594
+ else
3595
+ lo = mid + 1;
3596
+ }
3597
+ }
3289
3598
  return JS_ATOM_NULL;
3290
3599
  }
3291
3600
 
@@ -3428,7 +3737,13 @@ static JSValue JS_NewSymbolFromAtom(JSContext *ctx, JSAtom descr,
3428
3737
 
3429
3738
  assert(!__JS_AtomIsTaggedInt(descr));
3430
3739
  assert(descr < rt->atom_size);
3431
- p = rt->atom_array[descr];
3740
+ /* mikrojs patch: the symbol takes ownership of the description string,
3741
+ so a reserved frozen atom must materialize first — duping the shared
3742
+ lazy placeholder would mutate and eventually free a static. Reachable
3743
+ via OP_private_symbol when a #private field name is a frozen atom. */
3744
+ p = js_atom_ensure_str(rt, descr);
3745
+ if (unlikely(!p))
3746
+ return JS_ThrowOutOfMemory(ctx);
3432
3747
  js_dup(JS_MKPTR(JS_TAG_STRING, p));
3433
3748
  return JS_NewSymbolInternal(ctx, p, atom_type);
3434
3749
  }
@@ -3470,6 +3785,15 @@ static const char *JS_AtomGetStrRT(JSRuntime *rt, char *buf, int buf_size,
3470
3785
  } else {
3471
3786
  JSAtomStruct *p = rt->atom_array[atom];
3472
3787
  *buf = '\0';
3788
+ if (unlikely(js_atom_is_lazy(p))) {
3789
+ /* mikrojs patch: read straight from the frozen table; this
3790
+ path must not allocate (used while reporting errors) */
3791
+ uint32_t idx = atom - rt->frozen_first;
3792
+ utf8_encode_buf8(buf, buf_size,
3793
+ (const uint8_t *)rt->frozen_names[idx],
3794
+ rt->frozen_lens[idx]);
3795
+ return buf;
3796
+ }
3473
3797
  if (atom_is_free(p)) {
3474
3798
  snprintf(buf, buf_size, "<free %x>", atom);
3475
3799
  } else if (p != NULL) {
@@ -3501,7 +3825,9 @@ static JSValue __JS_AtomToValue(JSContext *ctx, JSAtom atom, bool force_string)
3501
3825
  JSRuntime *rt = ctx->rt;
3502
3826
  JSAtomStruct *p;
3503
3827
  assert(atom < rt->atom_size);
3504
- p = rt->atom_array[atom];
3828
+ p = js_atom_ensure_str(rt, atom);
3829
+ if (unlikely(!p))
3830
+ return JS_ThrowOutOfMemory(ctx);
3505
3831
  if (p->atom_type == JS_ATOM_TYPE_STRING) {
3506
3832
  goto ret_string;
3507
3833
  } else if (force_string) {
@@ -3540,7 +3866,12 @@ static bool JS_AtomIsArrayIndex(JSContext *ctx, uint32_t *pval, JSAtom atom)
3540
3866
  uint32_t val;
3541
3867
 
3542
3868
  assert(atom < rt->atom_size);
3543
- p = rt->atom_array[atom];
3869
+ p = js_atom_ensure_str(rt, atom);
3870
+ if (unlikely(!p)) {
3871
+ /* OOM while materializing; a frozen name is not an index */
3872
+ *pval = 0;
3873
+ return false;
3874
+ }
3544
3875
  if (p->atom_type == JS_ATOM_TYPE_STRING &&
3545
3876
  is_num_string(&val, p) && val != -1) {
3546
3877
  *pval = val;
@@ -3566,7 +3897,9 @@ static JSValue JS_AtomIsNumericIndex1(JSContext *ctx, JSAtom atom)
3566
3897
  if (__JS_AtomIsTaggedInt(atom))
3567
3898
  return js_int32(__JS_AtomToUInt32(atom));
3568
3899
  assert(atom < rt->atom_size);
3569
- p1 = rt->atom_array[atom];
3900
+ p1 = js_atom_ensure_str(rt, atom);
3901
+ if (unlikely(!p1))
3902
+ return JS_ThrowOutOfMemory(ctx);
3570
3903
  if (p1->atom_type != JS_ATOM_TYPE_STRING)
3571
3904
  return JS_UNDEFINED;
3572
3905
  p = p1;
@@ -3649,13 +3982,13 @@ static int JS_AtomIsNumericIndex(JSContext *ctx, JSAtom atom)
3649
3982
 
3650
3983
  void JS_FreeAtom(JSContext *ctx, JSAtom v)
3651
3984
  {
3652
- if (!__JS_AtomIsConst(v))
3985
+ if (!__JS_AtomIsConst(v) && !js_atom_is_frozen_id(ctx->rt, v))
3653
3986
  __JS_FreeAtom(ctx->rt, v);
3654
3987
  }
3655
3988
 
3656
3989
  void JS_FreeAtomRT(JSRuntime *rt, JSAtom v)
3657
3990
  {
3658
- if (!__JS_AtomIsConst(v))
3991
+ if (!__JS_AtomIsConst(v) && !js_atom_is_frozen_id(rt, v))
3659
3992
  __JS_FreeAtom(rt, v);
3660
3993
  }
3661
3994
 
@@ -7144,7 +7477,7 @@ static void compute_bytecode_size(JSFunctionBytecode *b, JSMemoryUsage_helper *h
7144
7477
  if (b->closure_var) {
7145
7478
  js_func_size += b->closure_var_count * sizeof(*b->closure_var);
7146
7479
  }
7147
- if (b->byte_code_buf) {
7480
+ if (b->byte_code_buf && !b->bc_in_rom) {
7148
7481
  hp->js_func_code_size += b->byte_code_len;
7149
7482
  }
7150
7483
  memory_used_count++;
@@ -7441,7 +7774,7 @@ void JS_ComputeMemoryUsage(JSRuntime *rt, JSMemoryUsage *s)
7441
7774
  sizeof(rt->atom_hash[0]) * rt->atom_hash_size;
7442
7775
  for(i = 0; i < rt->atom_size; i++) {
7443
7776
  JSAtomStruct *p = rt->atom_array[i];
7444
- if (!atom_is_free(p)) {
7777
+ if (!atom_is_free(p) && !js_atom_is_lazy(p)) {
7445
7778
  s->atom_size += (sizeof(*p) + (p->len << p->is_wide_char) +
7446
7779
  1 - p->is_wide_char);
7447
7780
  }
@@ -31238,9 +31571,19 @@ done:
31238
31571
  goto done;
31239
31572
  }
31240
31573
 
31574
+ /* mikrojs patch: once a module reaches EVALUATED its init function can
31575
+ never run again; drop the bytecode so top-level-only code is reclaimed.
31576
+ Live closures and export binding var_refs hold their own references. */
31577
+ static void js_module_release_func(JSContext *ctx, JSModuleDef *m)
31578
+ {
31579
+ JS_FreeValue(ctx, m->func_obj);
31580
+ m->func_obj = JS_UNDEFINED;
31581
+ }
31582
+
31241
31583
  static void js_set_module_evaluated(JSContext *ctx, JSModuleDef *m)
31242
31584
  {
31243
31585
  m->status = JS_MODULE_STATUS_EVALUATED;
31586
+ js_module_release_func(ctx, m);
31244
31587
  if (!JS_IsUndefined(m->promise)) {
31245
31588
  JSValue ret_val;
31246
31589
  assert(m->cycle_root == m);
@@ -31335,6 +31678,7 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t
31335
31678
  module->eval_has_exception = true;
31336
31679
  module->eval_exception = js_dup(error);
31337
31680
  module->status = JS_MODULE_STATUS_EVALUATED;
31681
+ js_module_release_func(ctx, module);
31338
31682
 
31339
31683
  for(i = 0; i < module->async_parent_modules_count; i++) {
31340
31684
  JSModuleDef *m = module->async_parent_modules[i];
@@ -31564,6 +31908,7 @@ static int js_inner_module_evaluation(JSContext *ctx, JSModuleDef *m,
31564
31908
  *pstack_top = m1->stack_prev;
31565
31909
  if (!m1->async_evaluation) {
31566
31910
  m1->status = JS_MODULE_STATUS_EVALUATED;
31911
+ js_module_release_func(ctx, m1);
31567
31912
  } else {
31568
31913
  m1->status = JS_MODULE_STATUS_EVALUATING_ASYNC;
31569
31914
  }
@@ -31604,6 +31949,7 @@ static JSValue js_evaluate_module(JSContext *ctx, JSModuleDef *m)
31604
31949
  m1 = stack_top;
31605
31950
  assert(m1->status == JS_MODULE_STATUS_EVALUATING);
31606
31951
  m1->status = JS_MODULE_STATUS_EVALUATED;
31952
+ js_module_release_func(ctx, m1);
31607
31953
  m1->eval_has_exception = true;
31608
31954
  m1->eval_exception = js_dup(result);
31609
31955
  m1->cycle_root = m; /* spec bug: should be present */
@@ -36360,6 +36706,8 @@ static void free_function_bytecode(JSRuntime *rt, JSFunctionBytecode *b)
36360
36706
 
36361
36707
  if (b->byte_code_buf)
36362
36708
  free_bytecode_atoms(rt, b->byte_code_buf, b->byte_code_len, true);
36709
+ if (b->bc_separate)
36710
+ js_free_rt(rt, b->byte_code_buf);
36363
36711
 
36364
36712
  if (b->vardefs) {
36365
36713
  for(i = 0; i < b->arg_count + b->var_count; i++) {
@@ -37696,6 +38044,11 @@ typedef enum BCTagEnum {
37696
38044
  } BCTagEnum;
37697
38045
 
37698
38046
  #define BC_VERSION 26
38047
+ /* mikrojs patch: variant of BC_VERSION for blobs whose atom references
38048
+ below the frozen boundary are final runtime atom ids (see
38049
+ JS_PreloadFrozenAtoms). The header carries the boundary and the frozen
38050
+ table hash so a mismatched runtime rejects the blob at load. */
38051
+ #define BC_VERSION_FROZEN (BC_VERSION | 0x40)
37699
38052
 
37700
38053
  typedef struct BCWriterState {
37701
38054
  JSContext *ctx;
@@ -38432,8 +38785,16 @@ static int JS_WriteObjectAtoms(BCWriterState *s)
38432
38785
 
38433
38786
  dbuf1 = s->dbuf;
38434
38787
  js_dbuf_init(s->ctx, &s->dbuf);
38435
- bc_put_u8(s, BC_VERSION);
38436
- bc_put_u32(s, 0); // checksum, filled in after serialization
38788
+ if (s->allow_bytecode && s->first_atom > JS_ATOM_END) {
38789
+ /* mikrojs patch: frozen-atom blob; record boundary + table hash */
38790
+ bc_put_u8(s, BC_VERSION_FROZEN);
38791
+ bc_put_u32(s, 0); // checksum, filled in after serialization
38792
+ bc_put_leb128(s, s->first_atom);
38793
+ bc_put_u32(s, rt->frozen_atom_hash);
38794
+ } else {
38795
+ bc_put_u8(s, BC_VERSION);
38796
+ bc_put_u32(s, 0); // checksum, filled in after serialization
38797
+ }
38437
38798
 
38438
38799
  bc_put_leb128(s, s->idx_to_atom_count);
38439
38800
  for(i = 0; i < s->idx_to_atom_count; i++) {
@@ -38444,8 +38805,11 @@ static int JS_WriteObjectAtoms(BCWriterState *s)
38444
38805
  more efficient. */
38445
38806
  bc_put_u32(s, atom);
38446
38807
  } else {
38447
- JSAtomStruct *p = rt->atom_array[atom];
38448
- uint8_t type = p->atom_type;
38808
+ JSAtomStruct *p = js_atom_ensure_str(rt, atom);
38809
+ uint8_t type;
38810
+ if (!p)
38811
+ goto fail;
38812
+ type = p->atom_type;
38449
38813
  assert(type != JS_ATOM_TYPE_PRIVATE);
38450
38814
  bc_put_u8(s, type);
38451
38815
  JS_WriteString(s, p);
@@ -38486,7 +38850,9 @@ uint8_t *JS_WriteObject2(JSContext *ctx, size_t *psize, JSValueConst obj,
38486
38850
  s->allow_debug = ((flags & JS_WRITE_OBJ_STRIP_DEBUG) == 0);
38487
38851
  /* XXX: could use a different version when bytecode is included */
38488
38852
  if (s->allow_bytecode)
38489
- s->first_atom = JS_ATOM_END;
38853
+ /* mikrojs patch: frozen atoms (< frozen_atom_end) serialize as
38854
+ final ids; JS_ATOM_END when no frozen table is loaded */
38855
+ s->first_atom = ctx->rt->frozen_atom_end;
38490
38856
  else
38491
38857
  s->first_atom = 1;
38492
38858
  js_dbuf_init(ctx, &s->dbuf);
@@ -38540,6 +38906,9 @@ typedef struct BCReaderState {
38540
38906
  bool allow_sab;
38541
38907
  bool allow_bytecode;
38542
38908
  bool allow_reference;
38909
+ /* mikrojs patch: keep qualifying bytecode in the input buffer (which
38910
+ must then outlive the runtime, e.g. flash rodata) */
38911
+ bool inplace_bytecode;
38543
38912
  /* object references */
38544
38913
  JSObject **objects;
38545
38914
  int objects_count;
@@ -38780,6 +39149,37 @@ static uint32_t bc_get_flags(uint32_t flags, int *pidx, int n)
38780
39149
  return val;
38781
39150
  }
38782
39151
 
39152
+ /* mikrojs patch: true if every atom operand is already a final atom id
39153
+ (tagged int or below first_atom), i.e. loading needs no relocation
39154
+ writes and the instruction stream can stay in the (ROM) input buffer */
39155
+ static bool bc_bytecode_is_final(BCReaderState *s, const uint8_t *bc_buf,
39156
+ uint32_t bc_len)
39157
+ {
39158
+ uint32_t pos = 0;
39159
+ int op, len;
39160
+ uint32_t idx;
39161
+
39162
+ while (pos < bc_len) {
39163
+ op = bc_buf[pos];
39164
+ len = short_opcode_info(op).size;
39165
+ switch(short_opcode_info(op).fmt) {
39166
+ case OP_FMT_atom:
39167
+ case OP_FMT_atom_u8:
39168
+ case OP_FMT_atom_u16:
39169
+ case OP_FMT_atom_label_u8:
39170
+ case OP_FMT_atom_label_u16:
39171
+ idx = get_u32(bc_buf + pos + 1);
39172
+ if (!__JS_AtomIsTaggedInt(idx) && idx >= s->first_atom)
39173
+ return false;
39174
+ break;
39175
+ default:
39176
+ break;
39177
+ }
39178
+ pos += len;
39179
+ }
39180
+ return true;
39181
+ }
39182
+
38783
39183
  static int JS_ReadFunctionBytecode(BCReaderState *s, JSFunctionBytecode *b,
38784
39184
  int byte_code_offset, uint32_t bc_len)
38785
39185
  {
@@ -38788,7 +39188,50 @@ static int JS_ReadFunctionBytecode(BCReaderState *s, JSFunctionBytecode *b,
38788
39188
  JSAtom atom;
38789
39189
  uint32_t idx;
38790
39190
 
38791
- bc_buf = (uint8_t*)b + byte_code_offset;
39191
+ if (s->inplace_bytecode) {
39192
+ /* mikrojs patch: zero-copy when possible, else a separate heap
39193
+ copy (the function block was allocated without a bytecode tail) */
39194
+ if (bc_len != 0 && (uint32_t)(s->buf_end - s->ptr) >= bc_len &&
39195
+ bc_bytecode_is_final(s, s->ptr, bc_len)) {
39196
+ const uint8_t *rom = s->ptr;
39197
+ /* take a reference on each atom operand so the free path
39198
+ (free_bytecode_atoms) stays symmetric with the copy path */
39199
+ pos = 0;
39200
+ while (pos < (int)bc_len) {
39201
+ op = rom[pos];
39202
+ len = short_opcode_info(op).size;
39203
+ switch(short_opcode_info(op).fmt) {
39204
+ case OP_FMT_atom:
39205
+ case OP_FMT_atom_u8:
39206
+ case OP_FMT_atom_u16:
39207
+ case OP_FMT_atom_label_u8:
39208
+ case OP_FMT_atom_label_u16:
39209
+ idx = get_u32(rom + pos + 1);
39210
+ if (!__JS_AtomIsTaggedInt(idx))
39211
+ JS_DupAtom(s->ctx, idx);
39212
+ break;
39213
+ default:
39214
+ break;
39215
+ }
39216
+ pos += len;
39217
+ }
39218
+ b->byte_code_buf = (uint8_t *)rom;
39219
+ b->bc_in_rom = 1;
39220
+ s->ptr += bc_len;
39221
+ return 0;
39222
+ }
39223
+ if (bc_len == 0) {
39224
+ b->byte_code_buf = NULL;
39225
+ return 0;
39226
+ }
39227
+ bc_buf = js_mallocz(s->ctx, bc_len);
39228
+ if (!bc_buf)
39229
+ return s->error_state = -1;
39230
+ b->bc_separate = 1;
39231
+ b->byte_code_buf = bc_buf; /* owned now; freed on error paths too */
39232
+ } else {
39233
+ bc_buf = (uint8_t*)b + byte_code_offset;
39234
+ }
38792
39235
  if (bc_get_buf(s, bc_buf, bc_len))
38793
39236
  return -1;
38794
39237
  b->byte_code_buf = bc_buf;
@@ -38958,7 +39401,10 @@ static JSValue JS_ReadFunctionTag(BCReaderState *s)
38958
39401
  closure_var_offset = function_size;
38959
39402
  function_size += bc.closure_var_count * sizeof(*bc.closure_var);
38960
39403
  byte_code_offset = function_size;
38961
- function_size += bc.byte_code_len;
39404
+ /* mikrojs patch: in-place reads keep the bytecode out of the function
39405
+ block (ROM pointer, or a separate heap copy on fallback) */
39406
+ if (!s->inplace_bytecode)
39407
+ function_size += bc.byte_code_len;
38962
39408
 
38963
39409
  b = js_mallocz(ctx, function_size);
38964
39410
  if (!b)
@@ -39698,7 +40144,8 @@ static int JS_ReadObjectAtoms(BCReaderState *s)
39698
40144
 
39699
40145
  if (bc_get_u8(s, &v8))
39700
40146
  return -1;
39701
- if (v8 != BC_VERSION) {
40147
+ if (v8 != BC_VERSION &&
40148
+ !(v8 == BC_VERSION_FROZEN && s->allow_bytecode)) {
39702
40149
  JS_ThrowSyntaxError(s->ctx, "invalid version (%d expected=%d)",
39703
40150
  v8, BC_VERSION);
39704
40151
  return -1;
@@ -39711,6 +40158,22 @@ static int JS_ReadObjectAtoms(BCReaderState *s)
39711
40158
  JS_ThrowSyntaxError(s->ctx, "checksum error");
39712
40159
  return -1;
39713
40160
  }
40161
+ if (v8 == BC_VERSION_FROZEN) {
40162
+ /* mikrojs patch: atom ids below the boundary are final; require the
40163
+ exact same frozen table the blob was compiled against */
40164
+ uint32_t boundary, fhash;
40165
+ JSRuntime *rt = s->ctx->rt;
40166
+ if (bc_get_leb128(s, &boundary))
40167
+ return -1;
40168
+ if (bc_get_u32(s, &fhash))
40169
+ return -1;
40170
+ if (boundary != rt->frozen_atom_end ||
40171
+ fhash != rt->frozen_atom_hash) {
40172
+ JS_ThrowSyntaxError(s->ctx, "frozen atom table mismatch");
40173
+ return -1;
40174
+ }
40175
+ s->first_atom = boundary;
40176
+ }
39714
40177
  if (bc_get_leb128(s, &s->idx_to_atom_count))
39715
40178
  return -1;
39716
40179
  if (s->idx_to_atom_count > 1000*1000) {
@@ -39785,6 +40248,8 @@ JSValue JS_ReadObject2(JSContext *ctx, const uint8_t *buf, size_t buf_len,
39785
40248
  s->allow_bytecode = ((flags & JS_READ_OBJ_BYTECODE) != 0);
39786
40249
  s->allow_sab = ((flags & JS_READ_OBJ_SAB) != 0);
39787
40250
  s->allow_reference = ((flags & JS_READ_OBJ_REFERENCE) != 0);
40251
+ s->inplace_bytecode = s->allow_bytecode &&
40252
+ ((flags & JS_READ_OBJ_INPLACE) != 0);
39788
40253
  if (s->allow_bytecode)
39789
40254
  s->first_atom = JS_ATOM_END;
39790
40255
  else
@@ -1251,6 +1251,20 @@ JS_EXTERN uint8_t *JS_WriteObject2(JSContext *ctx, size_t *psize, JSValueConst o
1251
1251
  writer in the same process (e.g. another Worker on the same runtime). */
1252
1252
  #define JS_READ_OBJ_SAB (1 << 2) /* allow SharedArrayBuffer */
1253
1253
  #define JS_READ_OBJ_REFERENCE (1 << 3) /* allow object references */
1254
+ /* mikrojs patch: keep bytecode whose atom operands are all final ids
1255
+ (predefined or frozen, see JS_PreloadFrozenAtoms) in the input buffer
1256
+ instead of copying it to the heap. The buffer must then outlive every
1257
+ function read from it (e.g. flash rodata / static const data). */
1258
+ #define JS_READ_OBJ_INPLACE (1 << 4)
1259
+
1260
+ /* mikrojs patch: reserve ids for `count` atoms right after JS_NewRuntime so
1261
+ blobs compiled by a writer runtime with the same preload reference them
1262
+ as final ids (enables JS_READ_OBJ_INPLACE zero-copy). Atom strings
1263
+ materialize lazily from `names` on first string use, so `names` and
1264
+ `lens` must outlive the runtime (e.g. rodata). Must be called before any
1265
+ dynamic atom is freed; returns 0 on success. */
1266
+ JS_EXTERN int JS_PreloadFrozenAtoms(JSRuntime *rt, const char *const *names,
1267
+ const uint32_t *lens, uint32_t count);
1254
1268
  JS_EXTERN JSValue JS_ReadObject(JSContext *ctx, const uint8_t *buf, size_t buf_len, int flags);
1255
1269
  JS_EXTERN JSValue JS_ReadObject2(JSContext *ctx, const uint8_t *buf, size_t buf_len,
1256
1270
  int flags, JSSABTab *psab_tab);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/quickjs",
3
- "version": "0.17.0",
3
+ "version": "0.17.1-pr-300.20260731003316",
4
4
  "description": "QuickJS-NG engine source, shared CMake module, and qjsc bytecode compiler",
5
5
  "keywords": [
6
6
  "bytecode",
package/postinstall.js CHANGED
@@ -8,7 +8,8 @@
8
8
  * Skips gracefully if cc or the submodule is not available.
9
9
  */
10
10
  import {execFileSync} from 'node:child_process'
11
- import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs'
11
+ import {createHash} from 'node:crypto'
12
+ import {existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from 'node:fs'
12
13
  import {dirname, join} from 'node:path'
13
14
  import {fileURLToPath} from 'node:url'
14
15
 
@@ -51,6 +52,21 @@ try {
51
52
  // Not a git repo (e.g. published package), skip commit tracking
52
53
  }
53
54
 
55
+ // Patches change qjsc behavior without moving the submodule commit, so the
56
+ // rebuild stamp must cover them too or bin/qjsc goes stale silently.
57
+ if (currentCommit) {
58
+ const patchesDir = join(__dirname, 'patches')
59
+ if (existsSync(patchesDir)) {
60
+ const hash = createHash('sha256')
61
+ for (const f of readdirSync(patchesDir)
62
+ .filter((f) => f.endsWith('.patch'))
63
+ .sort()) {
64
+ hash.update(readFileSync(join(patchesDir, f)))
65
+ }
66
+ currentCommit += `+${hash.digest('hex').slice(0, 16)}`
67
+ }
68
+ }
69
+
54
70
  // On Windows the C compiler appends .exe automatically.
55
71
  const exeSuffix = process.platform === 'win32' ? '.exe' : ''
56
72
  const qjscPath = join(binDir, `qjsc${exeSuffix}`)
package/quickjs.cmake CHANGED
@@ -28,6 +28,35 @@ endif()
28
28
  get_filename_component(_QUICKJS_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
29
29
  set(QUICKJS_INCLUDE_DIR "${_QUICKJS_CMAKE_DIR}/deps/quickjs")
30
30
 
31
+ # Sync the patched QuickJS tree AND the qjsc compiler before anything
32
+ # compiles or generates bytecode. postinstall covers `pnpm install`, but a
33
+ # patch file added or edited after install was previously a silent no-op:
34
+ # the tree built unpatched and the full test suite ran against code the
35
+ # patch never touched. Running postinstall (not just apply-patches) matters
36
+ # because qjsc is compiled FROM the patched sources: a stale qjsc paired
37
+ # with a fresh runtime emits bytecode the runtime rejects at import time
38
+ # (frozen atom table mismatch) — a runtime failure born from a green build.
39
+ # Both steps are stamp-guarded, so the no-change case is cheap; the GLOB's
40
+ # CONFIGURE_DEPENDS reruns configure when the patch set changes, and
41
+ # CMAKE_CONFIGURE_DEPENDS reruns it when patch contents change.
42
+ #
43
+ # Guarded for CMake script mode: ESP-IDF's early component-requirements
44
+ # pass includes component CMakeLists (and through them this file) via
45
+ # script-mode CMake, where CONFIGURE_DEPENDS globs and directory
46
+ # properties are hard errors. The real project-mode configure that
47
+ # follows runs the sync; the requirements pass only needs the variables.
48
+ if(NOT CMAKE_SCRIPT_MODE_FILE)
49
+ file(GLOB _QUICKJS_PATCHES CONFIGURE_DEPENDS "${_QUICKJS_CMAKE_DIR}/patches/*.patch")
50
+ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_QUICKJS_PATCHES})
51
+ execute_process(
52
+ COMMAND node "${_QUICKJS_CMAKE_DIR}/postinstall.js"
53
+ RESULT_VARIABLE _QUICKJS_PATCH_RC
54
+ )
55
+ if(NOT _QUICKJS_PATCH_RC EQUAL 0)
56
+ message(FATAL_ERROR "quickjs.cmake: syncing QuickJS patches/qjsc failed (see output above)")
57
+ endif()
58
+ endif()
59
+
31
60
  # Source files
32
61
  set(QUICKJS_SOURCES
33
62
  "${QUICKJS_INCLUDE_DIR}/quickjs.c"