Repository navigation
Expand file tree
/
Copy pathinterop.js
More file actions
2229 lines (2157 loc) · 121 KB
/
Copy pathinterop.js
File metadata and controls
2229 lines (2157 loc) · 121 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* jz/interop — host-side boundary codec.
*
* Importable as `jz/interop` without pulling the compiler, parser, or watr —
* use this to run prebuilt jz wasm from a host that doesn't need to compile.
* Dependencies contain only host linking, layout, errors and text decoding.
*
* Marshals NaN-boxed `f64` values across the boundary: bump-allocated heap
* blobs (strings, arrays, typed arrays, objects), schema transport for
* fixed-shape objects, host-object externrefs.
*
* Exports:
* UNDEF_NAN, NULL_NAN, coerce — null/undefined sentinels
* i64ToF64, f64ToI64 — bit-cast across the i64 boundary
* ptr / offset / type / aux — NaN-boxed pointer codec
* memory(src) — enhance a WebAssembly.Memory with read/write/String/Array/…
* wrap(memSrc, inst?) — adapt raw wasm exports to JS calling convention
* instantiate(wasm, opts?) — instantiate prebuilt wasm bytes + wrap
*
* One boundary codec per binary: a jz wasm picks its host shape at compile
* time (`opts.host`). There is no runtime "driver sniff" — the host loading
* the binary knows which variant it asked for.
*
* @module jz/interop
*/
import { wasi, attachTimers } from './wasi.js'
import { HEAP, PTR, FIELD, encodePtrHi, decodePtrType, decodePtrAux, ATOM, ATOM_HI, LAYOUT, SYMBOL_MIN, TOMB_NAN, DATA_VIEW_FLAG, DATA_VIEW_AUX, TYPED_ELEM_VIEW_FLAG, ctorFromElemAux, HIDDEN_PROPERTY_SEQ } from './layout.js'
import { ERROR_CODE_HI, ERR_INFO, ERR_CLASS_NAMES } from './err-codes.js'
import { decodeUtf8 } from './utf8.js'
// UTF-8 codecs for Wasm metadata. String values use lossless UTF-16 marshalling.
const TEXT_DEC = { decode: decodeUtf8 }
// ── WASI linking ────────────────────────────────────────────────────────────
const linkWasi = (mod, opts) => {
const needsWasi = WebAssembly.Module.imports(mod).some(i => i.module === 'wasi_snapshot_preview1')
return { needsWasi, wasiImports: needsWasi ? wasi(opts) : null }
}
const envFuncNames = (mod) =>
new Set(WebAssembly.Module.imports(mod)
.filter(i => i.module === 'env' && i.kind === 'function').map(i => i.name))
// ── Allocator wiring ────────────────────────────────────────────────────────
// Heap pointer: the exported `$__heap` global when the module has one (non-shared
// memory), else memory[1020] (shared memory — globals are per-instance, so
// threads must share a pointer cell in linear memory). 8-byte aligned bump on
// the JS side; wasm `_alloc` takes over if exported.
// Every i32 heap address that crosses the wasm boundary — a `$__heap` Global's `.value`,
// or a DataView 32-bit read — comes back through JS as a SIGNED int32 (WebAssembly JS API
// spec: i32 is observable as ToInt32, range -2^31..2^31-1), regardless of what the address
// actually represents (offsets are conceptually unsigned, 0..4GiB). `>>> 0` reinterprets
// the bit pattern back to unsigned. Skipping this is harmless below 2 GiB (same value
// either way) and silently wrong past it — a "negative" address then poisons every
// downstream `+`/comparison, and a DataView write at a negative offset throws RangeError.
const makeJsAllocator = (mem, heapGlobal) => {
const dv = () => new DataView(mem.buffer)
const getPtr = heapGlobal ? () => heapGlobal.value >>> 0 : () => dv().getUint32(HEAP.PTR_ADDR, true)
const setPtr = heapGlobal ? v => { heapGlobal.value = v } : v => dv().setInt32(HEAP.PTR_ADDR, v, true)
// Rewind target: the global's post-static-init value, else the fixed start.
let base = heapGlobal ? (heapGlobal.value >>> 0) : HEAP.START
const alloc = (bytes) => {
// Align up to 8 without `& ~7` — a JS bitwise op ToInt32-truncates its RESULT too,
// so `(x + 7) & ~7` would re-introduce the same sign flip past 2 GiB even with a
// correctly-unsigned `getPtr()`. Plain arithmetic has no such ceiling.
const ptr = getPtr()
const aligned = Math.ceil(ptr / 8) * 8
const next = aligned + bytes
if (!Number.isSafeInteger(bytes) || bytes < 0 || next >= 2 ** 32)
throw new RangeError('allocation exceeds the wasm32 heap or has an invalid size')
if (next > mem.buffer.byteLength)
mem.grow(Math.ceil((next - mem.buffer.byteLength) / 65536))
setPtr(next)
return aligned
}
const reset = () => setPtr(base)
// The global is initialized by wasm at module load; only the memory cell needs
// a JS-side nudge in case it underflows the heap start.
const initHeapPtr = () => {
if (heapGlobal) return
const d = dv()
if (d.getUint32(HEAP.PTR_ADDR, true) < HEAP.START) d.setInt32(HEAP.PTR_ADDR, HEAP.START, true)
}
// The heap top and the post-init base: `memory.used` is their distance, and a
// call that may release its argument copies rewinds the top to a mark.
return { alloc, reset, initHeapPtr, top: getPtr, setTop: setPtr,
used: () => getPtr() - base,
// Owned modules may initialize after the host adapter is ready. Cell-backed
// memories keep their fixed reset target (HEAP.START).
markBase: () => { if (heapGlobal) base = getPtr() },
}
}
// ── Custom-section reading ──────────────────────────────────────────────────
const customSection = (mod, name) => {
const secs = WebAssembly.Module.customSections(mod, name)
return secs.length ? new Uint8Array(secs[0]) : null
}
const sectionReader = (bytes) => {
const td = TEXT_DEC
let i = 0
return {
pos: () => i,
seek: (p) => { i = p },
eof: () => i >= bytes.length,
u8: () => bytes[i++],
varint: () => {
let r = 0, s = 0
// eslint-disable-next-line no-constant-condition
while (true) {
const x = bytes[i++]
r |= (x & 0x7F) << s
if (!(x & 0x80)) return r
s += 7
}
},
str: (n) => { const s = td.decode(bytes.subarray(i, i + n)); i += n; return s },
bytes: (n) => { const r = bytes.subarray(i, i + n); i += n; return r },
}
}
// ── NaN-box codec ───────────────────────────────────────────────────────────
// NaN-box codec — integer / BigInt based. A box NEVER becomes a JS number: JSC (Safari)
// canonicalizes a NaN payload the instant it materializes as f64 (boundary return,
// Float64Array read, getFloat64), so a box is carried in JS-land as a BigInt (the i64
// bits) and decoded with integer ops. Only genuine (non-NaN) numbers ever touch f64.
const MASK32 = 0xffffffffn
// Reinterpret for GENUINE numbers (and freshly-built boxes leaving JS): `_f64` only ever
// holds a real number here, never a live NaN-box, so there is nothing for JSC to purify.
// The bits cross through one 8-byte cell (a BigInt stored takes its value mod 2^64):
// shifting and masking a BigInt allocates at every step, several per argument.
const _buf = new ArrayBuffer(8), _u32 = new Uint32Array(_buf), _f64 = new Float64Array(_buf), _u64 = new BigUint64Array(_buf)
export const f64ToI64 = (n) => { _f64[0] = n; return _u64[0] }
export const i64ToF64 = (b) => { _u64[0] = b; return _f64[0] }
const hi32 = (b) => { _u64[0] = b; return _u32[1] }
const lo32 = (b) => { _u64[0] = b; return _u32[0] }
// A NaN-box is a sign-0 quiet NaN — high u32 carries jz's 0x7FF8 prefix. The
// mask MUST include the sign bit (0xFFF80000, not 0x7FF80000): a plain host
// BigInt's 64-bit two's-complement sign-extension sets hi32's top 12 prefix
// bits (0x7FF8's own span, bits 30-19) to 1 for any negative value whose
// magnitude keeps them saturated (every -1n..-2^51n verified live) — with
// the sign bit (bit 31, 0x80000000) left out of the mask, isBox(-5n) read
// true, so i64Arg's `!isBox(x)` gate and wrapVal's `isBox(v)` gate both
// treated a raw negative host BigInt as an ALREADY-BUILT box and skipped
// mem.BigInt's allocation entirely. The unboxed bits then reached
// __to_bigint's (module/number.js) tag dispatch inside wasm, whose
// $__ptr_type read a garbage tag off the non-box pattern (not PTR.BIGINT)
// and fell to the "not a box, not a string" 0n default — negative host
// BigInt ingress silently computed from magnitude 0 instead of throwing or
// computing correctly (`f(-5n)` where `f=(v)=>parse(v)+1n` read back `1n`,
// not `-4n`). module/core.js's $__typeof already gates the identical
// distinction correctly (its own `0xFFF0000000000000` mask: "negative-NaN
// bit patterns (sign bit set) don't match NAN_PREFIX so are uniquely
// numeric") — this brings isBox into agreement with that reference. A
// GENUINE box always has sign=0 (ptrBits/encodePtrHi never set bit 63), so
// this tightening never rejects a real box — it only excludes negative bit
// patterns no legitimate box can ever produce.
const BOX_HI = LAYOUT.NAN_PREFIX << 16, BOX_HI_MASK = BOX_HI | 0x80000000
const isBox = (b) => (hi32(b) & BOX_HI_MASK) === BOX_HI
// i64 bits for a wrapVal result (BigInt box, or number → its f64 bits): memory staging + i64 params.
const bits = (v) => typeof v === 'bigint' ? v : f64ToI64(v)
// Reserved atoms (type=ATOM, offset=0): aux 1/2/4/5 → null/undefined/false/true. BigInt boxes.
export const NULL_NAN = BigInt(ATOM_HI[ATOM.NULL]) << 32n
export const UNDEF_NAN = BigInt(ATOM_HI[ATOM.UNDEF]) << 32n
export const FALSE_NAN = BigInt(ATOM_HI[ATOM.FALSE]) << 32n
export const TRUE_NAN = BigInt(ATOM_HI[ATOM.TRUE]) << 32n
// Absent array cells have no own index; present undefined keeps UNDEF_NAN.
const TOMB_BITS = BigInt(TOMB_NAN)
// Semantic JS values enter the tagged lane here. Numbers cannot carry handles;
// canonicalize NaNs before their payload can be mistaken for an atom or pointer.
export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v !== v ? NaN : v
// SSO-encode a string ≤6 ASCII chars to a NaN-box BigInt (no heap needed).
// Mirrors mem.String's SSO branch. Used when marshaling a string into an i64-carrier
// param of a memoryless module (no linear memory, so only self-contained bit encodings
// like SSO can survive the boundary). Non-SSO strings throw clearly rather than silently
// becoming NaN.
const encodeSSO = (s) => {
let p = 0n
for (let i = 0; i < s.length; i++) p |= BigInt(s.charCodeAt(i)) << BigInt(i * 7)
p |= BigInt(s.length) << 42n
return ptr(4, Number(p >> 32n) | LAYOUT.SSO_BIT, Number(p & 0xFFFFFFFFn))
}
// Accept either the i64 carrier (BigInt, canonical) or a legacy f64 NaN-box (intact on V8 —
// e.g. an adaptI64 result, or user code holding a pre-i64 pointer) — normalize before decode.
const asBits = (p) => typeof p === 'bigint' ? p : f64ToI64(p)
export const ptr = (type, aux, offset) => { _u32[1] = encodePtrHi(type, aux); _u32[0] = offset; return _u64[0] }
export const offset = (p) => lo32(asBits(p))
export const type = (p) => decodePtrType(hi32(asBits(p)))
export const aux = (p) => decodePtrAux(hi32(asBits(p)))
// SSO string decode from i64 bits: 7-bit ASCII, char i at payload bit i*7, len at bits 42-44.
const decodeSSO = (b) => {
const a = decodePtrAux(hi32(b)), len = (a >>> 10) & 7
const payload = (BigInt(a) << 32n) | BigInt(Number(b & MASK32))
let s = ''
for (let i = 0; i < len; i++) s += String.fromCharCode(Number((payload >> BigInt(i * 7)) & 0x7fn))
return s
}
// A Symbol belongs to one compiled instance. Host inputs and compiled factories
// draw from the same counter; interned ids resolve through the host registry.
// Keep this table across arena resets: Symbols have identity but no heap storage.
const symbolCodecs = new WeakMap()
const symbolsOf = (owner, mod, raw) => {
let codec = symbolCodecs.get(owner)
if (codec) return codec
const values = new Map(), ids = new Map(), counter = raw?.__symbol_id
let next = ptr(PTR.ATOM, SYMBOL_MIN, 0)
const remember = (id, value) => { values.set(id, value); ids.set(value, id); return value }
const section = mod && customSection(mod, 'jz:symbols')
if (section) for (const [key, id] of JSON.parse(TEXT_DEC.decode(section)))
remember(ptr(PTR.ATOM, id, 0), Symbol.for(key))
codec = {
read(id) { return values.has(id) ? values.get(id) : remember(id, Symbol()) },
write(value) {
if (ids.has(value)) return ids.get(value)
let id = (counter ? counter.value : next) + 1n
if (!(id & MASK32)) id++
if (id >= TOMB_BITS) throw new RangeError('Symbol identity space exhausted')
if (counter) counter.value = id
else next = id
remember(id, value)
return id
},
}
symbolCodecs.set(owner, codec)
return codec
}
// Memory-free decode of an i64-bits boundary value: numbers pass through, a box becomes
// its atom / SSO string. Exactly the forms a *memoryless* module can carry (no linear
// memory → no heap string/array/object). Heap-carrying modules route through `mem.read`.
// `fnOf` reads a closure as a JS function (wrap's reader): a module with no
// heap holds closures too, one that captures nothing needs none.
const decode = (v, fnOf = null, symbols = null) => {
if (Array.isArray(v)) return v.map(x => decode(x, fnOf, symbols)) // multi-value tuple, each lane an i64-carrier (memoryless)
if (typeof v === 'number') { if (v === v) return v; v = f64ToI64(v) } // f64 NaN-box (intact on V8) → bits
else if (typeof v !== 'bigint') return v // already-decoded JS value
if (!isBox(v)) return i64ToF64(v) // non-NaN bits → number
if (type(v) === 4 && (aux(v) & LAYOUT.SSO_BIT)) return decodeSSO(v)
if (type(v) === 10 && fnOf) return fnOf(v)
if (symbols && type(v) === PTR.ATOM && aux(v) >= SYMBOL_MIN && v !== TOMB_BITS) return symbols.read(v)
if (offset(v) === 0) {
if (v === NULL_NAN) return null
if (v === UNDEF_NAN) return undefined
if (v === FALSE_NAN) return false
if (v === TRUE_NAN) return true
}
return i64ToF64(v) // canonical NaN-number / unknown
}
// Decode a boundary value arriving as i64 bits (BigInt). Heap modules go through mem.read.
const readArgBits = (state, big) => state.mem ? state.mem.read(big) : decode(big)
// Typed element metadata: [elemId, byteStride, DataView getter, DataView setter]
const ELEMS = {
Int8Array: [0, 1, 'getInt8', 'setInt8'],
Uint8Array: [1, 1, 'getUint8', 'setUint8'],
Int16Array: [2, 2, 'getInt16', 'setInt16'],
Uint16Array: [3, 2, 'getUint16', 'setUint16'],
Int32Array: [4, 4, 'getInt32', 'setInt32'],
Uint32Array: [5, 4, 'getUint32', 'setUint32'],
Float32Array: [6, 4, 'getFloat32', 'setFloat32'],
Float64Array: [7, 8, 'getFloat64', 'setFloat64'],
// flag-carrying kinds: elemId = base code | flag (16 = BigInt,
// 32 = f16, 64 = clamped). BigInt64/BigUint64 share the compiler's raw
// two's-complement storage tag; decoding follows JZ's documented signed-i64
// BigInt contract.
BigInt64Array: [23, 8, 'getBigInt64', 'setBigInt64'],
BigUint64Array: [23, 8, 'getBigUint64', 'setBigUint64'],
Float16Array: [35, 2, 'getFloat16', 'setFloat16'],
Uint8ClampedArray: [65, 1, 'getUint8', 'setUint8'],
}
// Pre-built lookup by element ID (avoids Object.values on each access)
const ELEM_BY_ID = Object.values(ELEMS)
const _enhanced = new WeakSet()
/**
* The tables a module declares for the memory it links to, read from its
* custom sections (the jz:schema writer in compile/index.js): the Error class
* and the user-class brand by schema id, the field contracts and the property
* lists by position (entry index === compile-time schema id). A schema entry
* is { type, payload }: type 0 a null (computed or missing key), type 1 a
* nested [null, name] (synthetic shape), type 3 a JSON-escaped property name
* without its quotes, type 2 legacy text.
*/
const NO_TABLES = { errorClasses: new Map(), brands: new Map(), regexes: new Map(), fields: [], schemas: [], views: new Set() }
const moduleTables = (mod) => {
const errorClasses = new Map(), brands = new Map(), regexes = new Map(), fields = [], schemas = [], views = new Set()
// the schemas with an object literal's accessor: read through the module's data copy (`__view_data`)
const viewBytes = customSection(mod, 'jz:views')
if (viewBytes) {
const r = sectionReader(viewBytes), n = r.varint()
for (let j = 0; j < n; j++) views.add(r.varint())
}
const errClsBytes = customSection(mod, 'jz:errcls')
if (errClsBytes) {
const r = sectionReader(errClsBytes), n = r.varint()
for (let j = 0; j < n; j++) { const sid = r.varint(); errorClasses.set(sid, r.str(r.varint())) }
}
const brandBytes = customSection(mod, 'jz:brand')
if (brandBytes) {
const r = sectionReader(brandBytes), n = r.varint()
for (let j = 0; j < n; j++) { const sid = r.varint(); brands.set(sid, r.str(r.varint())) }
}
const regexBytes = customSection(mod, 'jz:regexp')
if (regexBytes) {
const r = sectionReader(regexBytes), n = r.varint()
for (let j = 0; j < n; j++) { const sid = r.varint(); regexes.set(sid, [JSON.parse(r.str(r.varint())), r.str(r.varint())]) }
}
const fieldBytes = customSection(mod, 'jz:fields')
if (fieldBytes) {
const r = sectionReader(fieldBytes), count = r.varint()
for (let sid = 0; sid < count; sid++) {
const row = [], n = r.varint()
for (let i = 0; i < n; i++) {
const header = r.varint()
row.push([header >>> 3, header & 1 ? r.varint() : -1, header & 2 ? r.varint() : 0, header & 4 ? Number(r.str(r.varint())) : null])
}
fields.push(row)
}
}
const schemaBytes = customSection(mod, 'jz:schema')
if (schemaBytes) {
const r = sectionReader(schemaBytes)
const dec = () => {
const t = r.u8()
if (t === 0) return null
if (t === 1) return [null, dec()]
const name = r.str(r.varint())
return t === 3 ? JSON.parse('"' + name + '"') : name
}
const n = r.varint()
for (let j = 0; j < n; j++) { const k = r.varint(), props = []; for (let p = 0; p < k; p++) props.push(dec()); schemas.push(props) }
}
return { errorClasses, brands, regexes, fields, schemas, views }
}
/**
* A module's tables merged into a memory's, on copies: the memory's own tables
* do not change, so a rejected module leaves no trace, and `instantiate` runs
* the merge before a module links to the memory it would share, where its
* start function and data would already have written. The enhancer commits
* the result. The first module's numbering is authoritative: a sid already
* known keeps its class and brand.
*
* A pointer carries the schema id its module compiled with, so every schema
* must bind at that id here: a module whose schema would bind at another id
* is rejected. Modules sharing a memory agree on their ids (one compilation,
* or the same module again) or take memories of their own. The dedup key
* mirrors ctx.schema.register's compile-time key (module/schema.js): the
* property list as JSON (a separator could not tell `a\u0001b, c` from
* `a, b\u0001c`) salted with the module's own class name for the schema (all
* seven built-in Error classes share the props ['message', 'name'] and are
* distinct only by their salt; two user classes of one field list likewise).
* `_schemaKeyToId` remembers the salted key per id across merges: `schemas`
* itself stays the plain list of property names that `read` indexes by sid,
* from which a salted key cannot be recovered.
*/
const mergeTables = (mem, t) => {
const errorSidToClass = new Map(mem.errorSidToClass), brandOfSid = new Map(mem.brandOfSid), regexOfSid = new Map(mem.regexOfSid)
const schemas = [...(mem.schemas || [])], _schemaKeyToId = new Map(mem._schemaKeyToId), fieldContracts = [...(mem.fieldContracts || [])]
const views = new Set([...(mem.views || []), ...t.views])
for (const [sid, name] of t.errorClasses) if (!errorSidToClass.has(sid)) errorSidToClass.set(sid, name)
const keys = t.schemas.map((s, j) => { const salt = t.errorClasses.get(j) ?? t.brands.get(j) ?? (t.regexes.has(j) ? 'RegExp:' + JSON.stringify(t.regexes.get(j)) : null); return JSON.stringify(s) + (salt ? '\x02' + salt : '') })
keys.forEach((key, j) => {
let sid = _schemaKeyToId.get(key)
if (sid === undefined) { _schemaKeyToId.set(key, sid = schemas.length); schemas.push(t.schemas[j]) }
if (sid !== j) throw new TypeError(`jz: schema ${j} {${t.schemas[j].join(', ')}} of this module binds as schema ${sid} in the memory it shares; modules sharing a memory must bind their schemas at the same ids (compile them together, or give each its own memory)`)
if (t.brands.has(j)) brandOfSid.set(j, t.brands.get(j))
if (t.regexes.has(j)) regexOfSid.set(j, t.regexes.get(j))
const row = t.fields[j]
if (!row?.length) return
if (fieldContracts[j] && JSON.stringify(fieldContracts[j]) !== JSON.stringify(row))
throw new TypeError('jz: incompatible field contracts for a schema already bound to this memory')
fieldContracts[j] = row
})
return { schemas, _schemaKeyToId, errorSidToClass, brandOfSid, regexOfSid, fieldContracts, views }
}
/**
* Enhance WebAssembly.Memory with jz read/write methods (monkey-patch).
* - memory() → create new Memory, patch, return
* - memory({ initial: N }) → create with options, patch, return
* - memory(wasmMemory) → patch existing, return same object
* - memory(instanceResult) → bind to instance (patch its memory, bind alloc/schemas/extMap)
*/
export const memory = (src) => {
// Already enhanced — return as-is (idempotent)
if (src instanceof WebAssembly.Memory && _enhanced.has(src)) return src
// Create new Memory from nothing or options
if (!src || (typeof src === 'object' && !(src instanceof WebAssembly.Memory) && !src.instance && !src.exports && !src.memory)) {
const mem = new WebAssembly.Memory({ initial: src?.initial || 1, ...(src?.maximum ? { maximum: src.maximum } : {}), ...(src?.shared ? { shared: src.shared } : {}) })
return memory(mem)
}
// Resolve the WebAssembly.Memory object
let mem, wasmExports, extMap, mod, symbols
if (src instanceof WebAssembly.Memory) {
mem = src
wasmExports = null
extMap = null
mod = null
symbols = symbolsOf(mem, null, null)
} else {
// Instance result: { module, instance, exports, extMap }
const raw = src?.instance?.exports || src?.exports || src
mem = src?.exports?.memory || raw.memory
// Memoryless module (SSO strings / atoms / numbers only — no linear memory):
// hand back a minimal reader instead of null so callers can still decode its
// boundary values from bits. `read`/`wrapVal` cover the value forms that exist
// without memory; `scalar` flags the fast path that skips heap marshaling.
symbols = symbolsOf(src.instance || src, src.module, raw)
if (!mem) return {
read: (v, fnOf) => decode(v, fnOf, symbols),
wrapVal: v => typeof v === 'symbol' ? symbols.write(v) : coerce(v),
scalar: true,
}
wasmExports = { ...raw, memory: mem }
extMap = src.extMap || null
mod = src.module || null
}
const dv = () => new DataView(mem.buffer)
// Allocator scaffold: bumps the exported `$__heap` global (or memory[1020] for
// shared memory). Wasm `_alloc` takes over when exported; `_clear`/jsReset rewinds.
const { alloc: jsAlloc, reset: jsReset, initHeapPtr, top, setTop, used, markBase } = makeJsAllocator(mem, wasmExports?.__heap)
// `_alloc`'s i32 result crosses the wasm→JS boundary SIGNED (same ToInt32 rule as any
// other i32 — see makeJsAllocator's comment); `>>> 0` restores the true unsigned address
// once the heap grows past 2 GiB, matching jsAlloc's own already-unsigned return.
const wasmAlloc = wasmExports?._alloc && (bytes => wasmExports._alloc(bytes) >>> 0)
let alloc = wasmAlloc || jsAlloc
const reset = wasmExports?._clear || jsReset
initHeapPtr()
// Write 16-byte header matching WASM `__alloc_hdr`:
// [propsPtr@+0(i64=0), len@+8, cap@+12], return data offset (raw+16).
// Read paths (ARRAY at off-8/-4, BUFFER at off-8) and the propsPtr slot at
// off-16 then work uniformly on JS- and WASM-allocated values.
const hdr = (len, cap, bytes) => {
const raw = alloc(16 + bytes)
const m = dv()
m.setBigInt64(raw, 0n, true)
m.setInt32(raw + 8, len, true)
m.setInt32(raw + 12, cap, true)
return raw + 16
}
// The module's tables joined to the memory's (rejected before anything
// changes when the module compiled with other ids), committed as a whole
Object.assign(mem, mergeTables(mem, mod ? moduleTables(mod) : NO_TABLES))
if (wasmExports?.__view_data) mem.viewData = wasmExports.__view_data
if (wasmExports?.__obj_props) mem.objProps = wasmExports.__obj_props
if (wasmExports?.__obj_deleted) mem.objDeleted = wasmExports.__obj_deleted
mem._symbols = symbols
// If already enhanced, just update bindings (new module compiled into same memory)
if (_enhanced.has(mem)) {
if (wasmAlloc) { alloc = wasmAlloc; mem.alloc = alloc }
mem.reset = () => { mem._views = new WeakMap(); reset() }
if (extMap) mem._extMap = extMap
return mem
}
// Patch methods onto the Memory instance
mem._extMap = extMap
// Bytes the heap holds above the mark `memory.reset()` returns to: what calls
// allocated and kept, and what the host allocated. A host that sees it climb
// call after call has a leak to fix; a reset returns it to 0.
Object.defineProperty(mem, 'used', { get: used, configurable: true })
mem._markBase = markBase
// What a decoded value names above this address holds the memory of the call that returned it (mem.read).
mem._above = Infinity
mem._held = false
mem._top = top
mem._setTop = setTop
mem.Array = (data) => {
const n = data.length, off = hdr(n, n, n * 8), source = Object(data)
// Stage as i64 bits, not as JS Numbers: V8 may transition a JS Array holding
// NaN-payload doubles to HOLEY_DOUBLE_ELEMENTS, which canonicalizes the NaN
// payload to 0x7FF8000000000000 — destroying the type/offset bits.
const wrapped = new BigInt64Array(n)
for (let i = 0; i < n; i++) wrapped[i] = i in source ? bits(mem.wrapVal(source[i])) : TOMB_BITS
const dst = new BigInt64Array(mem.buffer, off, n)
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
return ptr(1, 0, off)
}
mem.String = (str) => {
if (str.length <= 6 && /^[\x00-\x7f]*$/.test(str)) {
// 7-bit ASCII SSO: char i at payload bit i*7, len at bits 42-44 (see module/string.js codec).
let p = 0n
for (let i = 0; i < str.length; i++) p |= BigInt(str.charCodeAt(i)) << BigInt(i * 7)
p |= BigInt(str.length) << 42n
return ptr(4, Number(p >> 32n) | LAYOUT.SSO_BIT, Number(p & 0xFFFFFFFFn)) // STRING + SSO_BIT
}
const n = str.length, raw = alloc(4 + n * 2), m = dv()
m.setInt32(raw, n, true)
const off = raw + 4
for (let i = 0; i < n; i++) m.setUint16(off + i * 2, str.charCodeAt(i), true)
return ptr(4, 0, off)
}
mem.BigInt = (value) => {
const off = alloc(8)
dv().setBigInt64(off, BigInt.asIntN(64, value), true)
return ptr(5, 0, off)
}
mem.Buffer = (data) => {
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data)
: ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
: new Uint8Array(data)
const n = bytes.length, off = hdr(n, n, n), m = new Uint8Array(mem.buffer)
m.set(bytes, off)
return ptr(2, 0, off)
}
mem.wrapVal = function(v) {
if (v === null || v === undefined) return coerce(v)
// A view the module left on a host object (`__ext_set`) is the module's own
// storage, read back as itself.
if (typeof v === 'object') { const own = mem._views?.get(v); if (own !== undefined) return own }
if (typeof v === 'number') return coerce(v)
if (typeof v === 'boolean') return v ? TRUE_NAN : FALSE_NAN
if (typeof v === 'string') return mem.String(v)
if (typeof v === 'symbol') return mem._symbols.write(v)
// A BigInt that is a NaN-box (jz's i64 carrier — e.g. a value pre-built via memory.String/
// ptr/BigInt) passes straight through. A plain bigint VALUE has no per-slot host-ABI
// evidence to consult here — wrapVal is the GENERAL memory.*/mem.Array/mem.Hash/host-import-
// return marshalling entry, not the export-argument path (i64Arg, below, consults
// jz:hostabi per param; the rest-element path consults its `rest` flag per element) — so
// this refuses loudly instead of the phase-c-C4b-killed silent-wrong fallback
// (`mem.String(v.toString())`, a decimal string a wasm numeric parser happens to accept —
// every consumer expecting a real BigInt got a corrupted value instead with no error, the
// worst class).
if (typeof v === 'bigint') {
if (isBox(v)) return v
throw new TypeError(`jz: plain BigInt ${v}n passed to memory marshaling (memory.Array/memory.Object/memory.Hash/a host-import return/…) with no BigInt evidence — box it explicitly: mem.BigInt(${v}n)`)
}
if (Array.isArray(v)) return mem.Array(v)
if (v instanceof ArrayBuffer) return mem.Buffer(v)
if (v instanceof DataView) {
const parent = offset(mem.Buffer(v.buffer)), off = alloc(16), m = dv()
m.setInt32(off, v.byteLength, true)
m.setInt32(off + 4, parent + v.byteOffset, true)
m.setInt32(off + 8, parent, true)
return ptr(3, DATA_VIEW_AUX, off)
}
const typedName = v?.constructor?.name
// An erased host slot has no source-level proof that downstream code uses
// the BigInt element domain. Keep ordinary numeric TypedArrays zero-copy-
// compatible, but reject evidence-free BigInt typed ingress rather than
// forcing every numeric hot loop onto the tagged/boxing path. Programs
// that construct BigInt typed storage internally retain full support.
if (typedName === 'BigInt64Array' || typedName === 'BigUint64Array' ||
typedName === 'Float16Array' || typedName === 'Uint8ClampedArray')
throw new TypeError(`jz: host ${typedName} at an untyped boundary is not supported — construct it inside the compiled source so its element storage policy is provable`)
if (typedName && ELEMS[typedName]) return mem[typedName](v)
if (typeof v === 'object' || typeof v === 'function') return mem.External(v)
return UNDEF_NAN
}
mem.External = function(obj) {
if (obj === null || obj === undefined) return coerce(obj)
const map = mem._extMap
if (!map) return UNDEF_NAN
let id = map.indexOf(obj)
if (id === -1) { id = map.length; map.push(obj) }
return ptr(11, 0, id)
}
// First-class jz HASH from a plain JS object — the schema-less marshal. Builds the
// kernel's exact open-addressed table ([seq<<32|hash:i64][key:f64][val:f64] × cap,
// home slot = hash & (cap-1), linear probe, len/cap header at -8/-4) so every
// wasm-side dyn op — reads, writes, NEW props, growth, delete, iteration — runs
// natively with stable identity. The External reflection path decodes/re-marshals
// per access, so nested container mutation (`params.P[i][j] = …`) lands on
// marshaling copies and silently vanishes — a params-bag must be a real hash.
// Hash twins of module/collection.js (clampHash / ssoMix / unitFnv) — MUST agree
// with __str_hash or wasm probes start at the wrong home slot and miss.
const clampHash = (h) => ((h >>> 0) <= 1 ? (h + 2) | 0 : h)
const jzStrHash = (box) => {
const b = bits(box)
if (type(b) === PTR.ATOM) return (Math.imul(aux(b) ^ offset(b), 0x9E3779B9) | 2) >>> 0
if ((b >> 32n) & BigInt(LAYOUT.SSO_BIT)) { // SSO: fixed-cost mix over payload
const lo = Number(b & 0xFFFFFFFFn) | 0
const hi = Number((b >> 32n) & 0x1FFFn) | 0
let h = Math.imul(hi ^ 0x9E3779B9, 0x85EBCA6B)
h = Math.imul(lo ^ h, 0xC2B2AE35)
h = (h ^ (h >>> 15)) | 0
return clampHash(h) >>> 0
}
const off = Number(b & 0xFFFFFFFFn), m = dv()
const len = m.getInt32(off - 4, true)
let h = 0x811c9dc5 | 0
for (let i = 0; i < len; i++) h = Math.imul(h ^ m.getUint16(off + i * 2, true), 0x01000193) | 0
return clampHash(h) >>> 0
}
mem.Hash = function(obj) {
if (obj == null) throw new TypeError('Cannot convert undefined or null to object')
const entries = [], source = Object(obj)
for (const key of Reflect.ownKeys(source))
if (Object.prototype.propertyIsEnumerable.call(source, key)) entries.push([key, source[key]])
let cap = 8
while (entries.length * 4 >= cap * 3) cap <<= 1 // stay under the 75% grow trigger
// cap × (24-B entry + 4-B probe hash lane) — collection.js's exact layout;
// the lane (after the entries) is what wasm probes walk
const off = hdr(entries.length, cap, cap * 28)
// Stage every slot as i64 bits (empty = 0) — same NaN-canonicalization dodge as mem.Array.
const staged = new BigInt64Array(cap * 3)
const lane = new Int32Array(cap)
entries.forEach(([k, v], seq) => {
const keyBox = typeof k === 'symbol' ? mem._symbols.write(k) : mem.String(k)
const h = jzStrHash(keyBox)
let idx = h & (cap - 1)
while (staged[idx * 3] !== 0n) idx = (idx + 1) & (cap - 1)
staged[idx * 3] = (BigInt(seq) << 32n) | BigInt(h >>> 0)
staged[idx * 3 + 1] = bits(keyBox)
staged[idx * 3 + 2] = bits(mem.wrapVal(v))
lane[idx] = h | 0
})
const dst = new BigInt64Array(mem.buffer, off, cap * 3)
dst.set(staged)
new Int32Array(mem.buffer, off + cap * 24, cap).set(lane)
return ptr(7, 0, off)
}
// Plain compiler-emitted data governs every structured ingress and update.
// Check the marshalled representation too: same JS constructor is not enough
// when a view descriptor or a different nested schema changes the layout.
const wrapField = (sid, i, value) => {
const rule = mem.fieldContracts[sid]?.[i]
if (rule?.[2] === 8) throw new TypeError('jz: field ' + mem.schemas[sid][i] + ' has ambiguous raw BigInt storage; use distinct object shapes')
let wrapped = rule && (rule[0] & (1 << PTR.OBJECT)) && value?.constructor === Object
? mem.Object(value)
: rule && (rule[0] & (1 << PTR.TYPED)) && ArrayBuffer.isView(value) && ELEMS[value.constructor.name]
? mem[value.constructor.name](value) : mem.wrapVal(value)
if (!rule) return wrapped
const [mask, detail, integer, constant] = rule
if (detail >= 0 && (mask & (1 << PTR.TYPED)) && typeof wrapped === 'bigint' && type(wrapped) === PTR.TYPED &&
(aux(wrapped) & ~TYPED_ELEM_VIEW_FLAG) === (detail & ~TYPED_ELEM_VIEW_FLAG) && aux(wrapped) !== detail) {
if (detail & TYPED_ELEM_VIEW_FLAG) {
const data = offset(wrapped), length = dv().getUint32(data - 8, true), descriptor = alloc(16), view = dv()
view.setUint32(descriptor, length, true)
view.setUint32(descriptor + 4, data, true)
view.setUint32(descriptor + 8, data, true)
wrapped = ptr(PTR.TYPED, detail, descriptor)
} else wrapped = mem.wrapVal(mem.read(wrapped))
}
let family = typeof value === 'boolean' ? FIELD.BOOL : FIELD.NUMBER
if (typeof wrapped === 'bigint' && isBox(wrapped)) {
const t = type(wrapped), a = aux(wrapped)
family = t === PTR.ATOM ? (a === ATOM.NULL || a === ATOM.UNDEF ? FIELD.NULLISH : a === ATOM.FALSE || a === ATOM.TRUE ? FIELD.BOOL : a >= SYMBOL_MIN ? 1 << PTR.ATOM : FIELD.NUMBER) : 1 << t
}
const nested = family === (1 << PTR.OBJECT), typed = family === (1 << PTR.TYPED)
const badDetail = (nested || typed) && detail >= 0 && aux(wrapped) !== detail
const badNumber = family === FIELD.NUMBER && ((integer && (!Number.isInteger(value) || Object.is(value, -0))) ||
(integer === 2 && (value < -2147483648 || value > 2147483647)) || (constant !== null && !Object.is(value, constant)))
if (!(mask & family) || badDetail || badNumber)
throw new TypeError(`jz: field ${mem.schemas[sid][i]} violates its compiled representation contract${constant !== null ? ': expected discriminant ' + constant : (mask & (1 << PTR.TYPED)) && detail >= 0 ? ': expected ' + (ctorFromElemAux(detail) || 'DataView') : integer === 2 ? ': expected an int32 without negative zero' : ''}`)
return integer === 4 ? dv().getBigInt64(offset(wrapped), true) : wrapped
}
mem.Object = function(obj) {
const objKeys = Object.keys(obj)
const key = objKeys.join(',')
const schemas = mem.schemas
// The shapes with these keys in this order, else in any order; among
// several, a plain object is the plain shape's, not a class's (two
// classes of one field list are told apart by brand, which a plain
// object does not carry).
const pick = (matches) => {
if (matches.length <= 1) return matches[0] ?? -1
const plain = matches.filter(i => !mem.brandOfSid.has(i))
return plain.length === 1 ? plain[0] : -2
}
let matches = schemas.reduce((a, s, i) => (!mem.regexOfSid.has(i) && s.join(',') === key ? a.concat(i) : a), [])
if (!matches.length) matches = schemas.reduce((a, s, i) =>
(!mem.regexOfSid.has(i) && s.length === objKeys.length && objKeys.every(k => s.includes(k)) ? a.concat(i) : a), [])
const sid = pick(matches)
if (sid === -2) throw Error(`Ambiguous schema for {${key}} — ${matches.length} compiled shapes match this key set; pass keys in one of these orders: ${matches.map(i => schemas[i].join(',')).join(' | ')}`)
if (sid === -1) return mem.Hash(obj) // no compiled schema: first-class hash (External loses nested-mutation identity)
const schema = schemas[sid], n = schema.length, raw = alloc(n * 8)
// Stage as i64 bits so V8 can't canonicalize NaN-payload pointers across
// recursive allocations. See mem.Array for the same pattern — and route
// every property value through mem.wrapVal the same way mem.Array/
// mem.Hash do: this loop used to hand-roll a partial null/string/array
// dispatch and fall through to bare `bits(v)` for everything else
// (numbers were incidentally fine; a plain BIGINT property value was
// not — silently stored as raw unmarked bits, no BigInt tag, instead of
// wrapVal's post-C4b typed throw. Nested plain objects/typed arrays/
// buffers/functions were equally unhandled). One dispatch, no duplicate
// logic to drift out of sync with wrapVal's.
const wrapped = new BigInt64Array(n)
for (let i = 0; i < n; i++) wrapped[i] = bits(wrapField(sid, i, obj[schema[i]]))
const dst = new BigInt64Array(mem.buffer, raw, n)
for (let i = 0; i < n; i++) dst[i] = wrapped[i]
return ptr(6, sid, raw)
}
// The live entries of a HASH (7), SET (8) or MAP (9) at `off` in insertion
// order, as __coll_order walks them: a durable-heap tombstone is no key.
const liveSlots = (off, t) => {
const m = dv(), cap = m.getInt32(off - 4, true), stride = t === 8 ? 16 : 24, slots = []
for (let i = 0; i < cap; i++) {
const slot = off + i * stride, hash = m.getBigUint64(slot, true)
if (hash && (t !== 7 || Number(hash >> 32n) !== HIDDEN_PROPERTY_SEQ) && m.getBigUint64(slot + 8, true) !== 0x7FF87FFFFFFFFFFFn)
slots.push([Number(hash >> 32n), slot])
}
return slots.sort((a, b) => a[0] - b[0]).map(([, slot]) => slot)
}
// A dictionary's [key, value] pairs, the box followed past a grow.
const hashEntries = (bits, fnOf = null) => {
const m = dv()
let off = offset(bits)
while (m.getInt32(off - 4, true) === -1) off = m.getUint32(off - 8, true)
return liveSlots(off, 7).map(slot => [mem.read(m.getBigInt64(slot + 8, true), fnOf), mem.read(m.getBigInt64(slot + 16, true), fnOf)])
}
// `fnOf` reads a closure as a JS function that calls it (wrap's per-instance
// reader: a closure's table index names a function of the module that made it).
mem.read = function(p, fnOf = null) {
if (Array.isArray(p)) return p.map(v => mem.read(v, fnOf)) // multi-value tuple
if (typeof p === 'number') {
if (p === p) return p // genuine number passthrough (NaN fails ===)
p = f64ToI64(p) // f64 NaN-box (intact on V8) → bits; decode below
} else if (typeof p !== 'bigint') {
return p // already a decoded JS value (string/object/…) — passthrough
}
// p is now i64 bits (BigInt). Decode with integer ops — never materialize as f64.
if (!isBox(p)) return i64ToF64(p) // non-NaN bits → genuine number
const m = dv(), t = type(p), a = aux(p)
let off = offset(p)
if (t === PTR.ATOM && a >= SYMBOL_MIN && p !== TOMB_BITS) return mem._symbols.read(p)
// Arrays and collections retain their identity when storage grows.
if (t === 1 || t >= 7 && t <= 9)
while (m.getInt32(off - 4, true) === -1) off = m.getUint32(off - 8, true)
if (t === 0 && off === 0) {
if (a === 0) return NaN
if (a === 1) return null
if (a === 2) return undefined
if (a === 4) return false
if (a === 5) return true
}
if (t === 11 && mem._extMap) return mem._extMap[off]
if (t === 10 && fnOf) {
if (off >= mem._above) mem._held = true
return fnOf(p)
}
if (t === 1) { // ARRAY
const len = m.getInt32(off - 8, true), out = new Array(len)
for (let i = 0; i < len; i++) {
const value = m.getBigInt64(off + i * 8, true)
if (value !== TOMB_BITS) out[i] = mem.read(value, fnOf)
}
return out
}
if (t === 3) { // TYPED
// A view of the module's memory: the bytes it shows stay while the host
// holds it (`held`, for the call that returned it, interop `settled`).
const held = (at) => { if (at >= mem._above) mem._held = true; return at }
if (a & DATA_VIEW_FLAG) return new DataView(mem.buffer, held(m.getInt32(off + 4, true)), m.getInt32(off, true))
const elem = a & 7
const [, stride] = ELEM_BY_ID[elem]
const Ctor = (a & 16) ? BigInt64Array
: (a & 32)
? (globalThis.Float16Array ?? (() => { throw new Error('decoding a Float16Array result needs a host with Float16Array (Node ≥ 24 / modern browsers)') })())
: (a & 64) ? Uint8ClampedArray
: [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array][elem]
if (a & 8) {
const byteLen = m.getInt32(off, true), dataOff = m.getInt32(off + 4, true)
return new Ctor(mem.buffer, held(dataOff), byteLen / stride)
}
const byteLen = m.getInt32(off - 8, true)
return new Ctor(mem.buffer, held(off), byteLen / stride)
}
if (t === 2) { // BUFFER
const byteLen = m.getInt32(off - 8, true)
const out = new ArrayBuffer(byteLen)
new Uint8Array(out).set(new Uint8Array(mem.buffer, off, byteLen))
return out
}
if (t === 4) { // STRING (aux SSO_BIT = inline, else heap)
if (a & LAYOUT.SSO_BIT) return decodeSSO(p)
const len = a & LAYOUT.SLICE_BIT ? a & LAYOUT.SLICE_LEN_MASK : m.getUint32(off - 4, true)
const chunks = []
for (let i = 0; i < len; i += 4096) {
const n = Math.min(4096, len - i), units = new Array(n)
for (let j = 0; j < n; j++) units[j] = m.getUint16(off + (i + j) * 2, true)
chunks.push(String.fromCharCode(...units))
}
return chunks.join('')
}
// A boxed BigInt's 8-byte payload is the raw two's-complement i64.
if (t === 5) return m.getBigInt64(off, true) // BIGINT
if (t === 6) { // OBJECT
// An object literal's accessor reads through its getter: the module
// copies such an object's data into a dictionary, decoded below.
// Error transport reads its stored message before constructing the host Error.
if (mem.views?.has(a) && mem.viewData && !mem.errorSidToClass?.has(a) && !mem.regexOfSid?.has(a)) return mem.read(mem.viewData(p), fnOf)
const keys = mem.schemas[a]
if (!keys) { if (off >= mem._above) mem._held = true; return p }
const re = mem.regexOfSid?.get(a), obj = re ? new RegExp(re[0], re[1]) : {}
// A deleted slot keeps undefined and a bit of the mask (layout.js
// deletedSlotWat): from slot 31 on one sticky bit covers the undefined ones.
const mask = mem.objDeleted ? mem.objDeleted(p) : 0
for (let i = 0; i < keys.length; i++) {
const rule = mem.fieldContracts[a]?.[i], raw = m.getBigInt64(off + i * 8, true)
if (mask && (i < 31 ? (mask >>> i) & 1 : (mask >>> 31) & 1 && raw === UNDEF_NAN)) continue
if (rule?.[2] === 8) throw new TypeError('jz: field ' + keys[i] + ' has ambiguous raw BigInt storage; use distinct object shapes')
let value = rule?.[2] === 4 ? raw : mem.read(raw, fnOf)
if (value != null && rule && (rule[0] & ~FIELD.NULLISH) === FIELD.BOOL) value = !!value
obj[keys[i]] = value
}
// A property stored outside the layout (through an alias, a destructuring
// target, a helper's parameter) is in the object's dictionaries: its
// header's, then the one a durable object's offset keys (module
// __obj_props). A later one's value replaces an earlier one's in place.
if (mem.objProps) for (const which of [0, 1]) {
const d = mem.objProps(p, which)
if (d) for (const [key, value] of hashEntries(d, fnOf)) obj[key] = value
}
fnOf?.owners.set(obj, p)
return obj
}
if (t >= 7 && t <= 9) { // HASH / SET / MAP share the insertion sequence.
if (t === 8) return new Set(liveSlots(off, t).map(slot => mem.read(m.getBigInt64(slot + 8, true), fnOf)))
const entries = liveSlots(off, t).map(slot => [mem.read(m.getBigInt64(slot + 8, true), fnOf), mem.read(m.getBigInt64(slot + 16, true), fnOf)])
if (t !== 7) return new Map(entries)
const out = Object.fromEntries(entries)
fnOf?.owners.set(out, p)
return out
}
// a handle on the module's memory, held as the view of a typed array is
if (t !== 0 && off >= mem._above) mem._held = true
return i64ToF64(p) // canonical NaN-number, a CLOSURE without a reader, unknown: reinterpret to f64
}
mem.write = function(p, data) {
const t = type(p)
let off = offset(p), m = dv()
if (t === 1) {
while (m.getInt32(off - 4, true) === -1) off = m.getUint32(off - 8, true)
const cap = m.getInt32(off - 4, true), length = data.length
if (!Number.isSafeInteger(length) || length < 0) throw new RangeError('mem.write: invalid array length')
if (length > cap) throw Error(`mem.write: ${data.length} elements exceeds this array's capacity of ${cap} — allocate it with a larger capacity, or write ${cap} or fewer elements`)
// Recursive marshalling can grow memory. Commit only after all values
// marshal, then reacquire the destination view. Failed staging leaves
// destination contents/length intact; allocations are reclaimed by reset.
const staged = new BigInt64Array(length), source = Object(data)
for (let i = 0; i < length; i++) staged[i] = i in source ? bits(mem.wrapVal(source[i])) : TOMB_BITS
m = dv()
for (let i = 0; i < staged.length; i++) m.setBigInt64(off + i * 8, staged[i], true)
m.setInt32(off - 8, staged.length, true)
} else if (t === 3) {
const a2 = aux(p), elem = a2 & 7
const [, stride, , setter] = (a2 & 16) ? ELEMS.BigInt64Array : ELEM_BY_ID[elem]
const byteLen = data.length * stride
if (a2 & 8) {
const viewByteLen = m.getInt32(off, true), dataOff = m.getInt32(off + 4, true)
if (byteLen > viewByteLen) throw Error(`mem.write: ${byteLen} bytes exceeds this typed array view's size of ${viewByteLen} bytes — allocate a larger view, or write fewer elements`)
for (let i = 0; i < data.length; i++) m[setter](dataOff + i * stride, data[i], true)
} else {
const byteCap = m.getInt32(off - 4, true)
if (byteLen > byteCap) throw Error(`mem.write: ${byteLen} bytes exceeds this typed array's capacity of ${byteCap} bytes — allocate it with a larger capacity, or write fewer elements`)
m.setInt32(off - 8, byteLen, true)
for (let i = 0; i < data.length; i++) m[setter](off + i * stride, data[i], true)
}
} else if (t === 6) {
const schema = mem.schemas[aux(p)]
if (!schema) throw Error(`mem.write: this pointer's schema id (${aux(p)}) has no compiled OBJECT schema — write to a pointer returned by mem.Object() for a schema this program compiled`)
const staged = []
for (const k of Object.keys(data)) {
const i = schema.indexOf(k)
if (i >= 0) staged.push([i, bits(wrapField(aux(p), i, data[k]))])
}
m = dv()
for (const [i, value] of staged) m.setBigInt64(off + i * 8, value, true)
} else {
throw Error(`mem.write only supports ARRAY, TYPED array, and OBJECT pointers — this pointer is a different kind (type tag ${t})`)
}
}
mem.alloc = alloc
// The compiled reset owns the post-init mark, cache invalidation and durable
// state healing. A JS-only memory has no runtime state and just rewinds. The
// views the module left on host objects (`__ext_set`) name storage a reset frees.
mem._views = new WeakMap()
mem.reset = () => { mem._views = new WeakMap(); reset() }
// TypedArray constructors: memory.Float64Array(data), etc.
// Bulk-copy path: when input is a TypedArray whose element type matches
// the target (same stride), use .set() for a fast memcpy instead of
// per-element DataView writes. Falls back to DataView for mismatched types.
const TA = [Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array]
TA[65] = Uint8ClampedArray
if (globalThis.Float16Array) TA[35] = globalThis.Float16Array
for (const [name, [elemId, stride, , setter]] of Object.entries(ELEMS)) {
mem[name] = (data) => {
// Coerce before allocating, and snapshot views over our growable buffer.
if (TA[elemId] && (!(data instanceof TA[elemId]) || data.buffer === mem.buffer)) data = new TA[elemId](data)
const n = data.length, bytes = n * stride, off = hdr(bytes, bytes, bytes)
// Same-type source → native memcpy via `.set` (incl. stride-1 Uint8Array:
// a multi-MB file copied byte-by-byte through DataView dominates decode).
if (TA[elemId] && data instanceof TA[elemId]) {
new TA[elemId](mem.buffer, off, n).set(data)
} else {
const m = dv()
for (let i = 0; i < n; i++) m[setter](off + i * stride, data[i], true)
}
return ptr(3, elemId, off)
}
}
// Zero-copy input: reserve a typed-array region in wasm memory and return BOTH
// a live `view` over it and the NaN-box `box` pointer to pass as an argument.
// The caller fills `view` directly (one I/O-side copy, no second JS→wasm copy)
// and hands `box` to the export, which reads the bytes in place. Decoded typed
// arrays already come back as views (mem.read), so a decode can be copy-free
// end-to-end. LIFETIME: `view` is detached by any mem.grow() (alloc past the
// current buffer) and clobbered by mem.reset()/the next decode — re-derive a
// fresh view with mem.read(box) after growth, or copy out what must persist.
// Back the module with a shared memory (WebAssembly.Memory{shared:true}) to
// keep views valid across grow and to hand them to a worker/AudioWorklet.
mem.allocTyped = (Ctor, n) => {
const meta = ELEMS[Ctor?.name]
if (!meta) throw Error(`mem.allocTyped: ${Ctor?.name ?? Ctor} is not a supported typed array constructor — pass one of ${Object.keys(ELEMS).join(', ')}`)
const [elemId, stride] = meta
const bytes = n * stride, off = hdr(bytes, bytes, bytes)
return { view: new Ctor(mem.buffer, off, n), box: ptr(3, elemId, off) }
}
_enhanced.add(mem)
return mem
}
/**
* Wrap raw WASM exports with JS calling convention adaptation.
* Handles: undefined → sentinel NaN for defaults, rest-param array packing.
*/
export const wrap = (memSrc, inst, state) => {
const restFuncs = new Map()
const mod = inst ? memSrc : memSrc.module || memSrc
const realInst = inst || memSrc.instance || memSrc
const td = TEXT_DEC
const restBytes = customSection(mod, 'jz:rest')
if (restBytes) {
try {
for (const entry of JSON.parse(td.decode(restBytes)))
restFuncs.set(typeof entry === 'string' ? entry : entry.name, typeof entry === 'string' ? 0 : entry.fixed)
} catch (e) { /* ignore */ }
}
// externref-param exports: positions where the wasm side takes an externref
// (jsstring carrier — js-host only). JS values at these positions pass through
// unchanged — no `mem.wrapVal` (would NaN-box into f64, defeating the point).
// `def` (optional) maps idx → default-string for jsstring params whose
// default substitution happens JS-side (the wasm side never sees null).
const extExp = new Map()
const extBytes = customSection(mod, 'jz:extparam')
if (extBytes) {
try {
for (const e of JSON.parse(td.decode(extBytes))) {
const idx = new Set(e.p)
// Hang the defaults off the Set as a property so call-sites that only
// check membership stay unchanged; the slow path reads `extInfo.def`.
if (e.d) idx.def = new Map(Object.entries(e.d).map(([k, v]) => [Number(k), v]))
extExp.set(e.name, idx)
}
} catch { /* ignore */ }
}
// i64-carrier map: per export, which params ride i64 and whether a result
// takes generic tagged decode. A proven raw BigInt result has no result flag.
const i64Exp = new Map()
const i64Bytes = customSection(mod, 'jz:i64exp')
if (i64Bytes) {
try { for (const e of JSON.parse(td.decode(i64Bytes))) i64Exp.set(e.name, { p: new Set(e.p || []), r: !!e.r, t: e.t || null, k: new Set(e.k || []), v: e.v || null }) }
catch { /* ignore */ }
}
// jz:hostabi — the ONE authority for per-slot host-BigInt ingress policy
// (phase-c C4b, audit P0 #1/#2). Per export: `raw` = i64 param indices the
// plan proved ALWAYS bigint (plain bigint crosses with no box —
// architecturally unreachable at the export boundary today, see
// src/compile/index.js's jz:hostabi doc for the reachability proof; the