jz 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.4.0",
4
- "description": "Modern functional JS compiling to WASM",
3
+ "version": "0.5.0",
4
+ "description": "Functional JS subset compiling to WASM",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
- "./wasi": "./wasi.js"
9
+ "./wasi": "./wasi.js",
10
+ "./interop": "./interop.js"
10
11
  },
11
12
  "files": [
12
13
  "index.js",
13
14
  "cli.js",
14
15
  "wasi.js",
16
+ "interop.js",
15
17
  "src",
16
18
  "module",
17
19
  "README.md"
@@ -26,8 +28,9 @@
26
28
  "bench": "node bench/bench.mjs",
27
29
  "bench:size": "node scripts/bench-size.mjs",
28
30
  "bench:compile": "node scripts/bench-compile.mjs",
29
- "test:bench-pin": "node test/bench-pin.js",
30
- "test:all": "npm test && npm run test:bench-pin",
31
+ "build:examples": "for dir in examples/*; do node \"$dir/build.mjs\"; done",
32
+ "test:bench": "node test/bench.js",
33
+ "test:all": "npm test && npm run test:bench",
31
34
  "prepublishOnly": "npm run test:all"
32
35
  },
33
36
  "repository": {
@@ -41,8 +44,8 @@
41
44
  "author": "Dmitry Iv",
42
45
  "license": "MIT",
43
46
  "dependencies": {
44
- "subscript": "^10.4.8",
45
- "watr": "^4.6.5"
47
+ "subscript": "^10.4.15",
48
+ "watr": "^4.6.9"
46
49
  },
47
50
  "keywords": [
48
51
  "javascript",
@@ -0,0 +1,89 @@
1
+ /**
2
+ * src/abi/array — ARRAY backing-store carriers.
3
+ *
4
+ * An ARRAY value is an `__alloc_hdr` block — `[-8:len(i32)][-4:cap(i32)]` then
5
+ * N element cells. The carrier owns where element `i` lives and how wide each
6
+ * cell is. Every array-element access routed through `ctx.abi.array.ops` reads
7
+ * layout from one place: the `[…]` literal store, the elem-schema fast-path
8
+ * read, the `ir.js` element helpers (`slotAddr`/`elemLoad`/`elemStore`, and so
9
+ * `arrayLoop`), the boxed-handle inner-array load.
10
+ *
11
+ * Ops are cycle-safe (no `src/*` imports) and return raw wasm IR arrays;
12
+ * callers wrap in `typed()`. `base` is an i32 IR node holding the array's data
13
+ * offset (post-header — a `['local.get', …]`, a `['local.tee', …]`, or a
14
+ * `['call', '$__ptr_offset', …]` that follows the grow-forwarding chain). `idx`
15
+ * is either a JS integer (constant slot — folds the multiply) or an i32 IR node
16
+ * (runtime index).
17
+ *
18
+ * taggedLinear — today's layout: every element one 8-byte f64 cell. An
19
+ * element holds a raw f64 (Array<NUMBER>) or a NaN-boxed
20
+ * pointer (Array<OBJECT|STRING>); both are 8 bytes wide, so
21
+ * one carrier serves every plain `[]`.
22
+ *
23
+ * structInline — SRoA layout for an `Array<{uniform K-field schema}>` whose
24
+ * element pointers never escape as object identities: the K
25
+ * f64 schema fields are inlined into the data region (stride
26
+ * K), so `rows[i].x` is one direct `f64.load` with no per-row
27
+ * object allocation and no pointer indirection. `len`/`cap`
28
+ * still count *physical* f64 cells, so every stride-8 helper
29
+ * (`__alloc_hdr`, `__arr_grow`, `__len`, `__set_len`) is
30
+ * reused untouched — `.push` of a struct writes K cells and
31
+ * adds K to `len`; `.length` divides the physical len by K.
32
+ * Picked per-schema by `analyzeStructInline` (src/analyze.js),
33
+ * whole-program, default-disqualify.
34
+ *
35
+ * Typed arrays (`Float64Array`/`Int32Array`/…) are a distinct value type
36
+ * (`VAL.TYPED`, ctor-determined stride) handled by `module/typedarray.js`, not
37
+ * a carrier here — they are not `VAL.ARRAY`.
38
+ *
39
+ * @module src/abi/array
40
+ */
41
+
42
+ // Byte address of element `idx` off an i32 base. A JS-integer idx folds the
43
+ // `*8` (and idx=0 returns base untouched — matches `slotAddr`); an IR-node idx
44
+ // emits the runtime `i32.shl … 3`.
45
+ const addr = (base, idx) =>
46
+ typeof idx === 'number'
47
+ ? (idx === 0 ? base : ['i32.add', base, ['i32.const', idx * 8]])
48
+ : ['i32.add', base, ['i32.shl', idx, ['i32.const', 3]]]
49
+
50
+ export const taggedLinear = {
51
+ // Element operations the compiler routes array access through.
52
+ ops: {
53
+ // Byte address of element `idx` — for callers that need the address
54
+ // itself (`slotAddr`) rather than a load/store.
55
+ addr,
56
+
57
+ // Element `idx` read as the canonical f64 value.
58
+ load: (base, idx) => ['f64.load', addr(base, idx)],
59
+
60
+ // Element `idx` read as raw i64 bits — moves a slot without reinterpreting
61
+ // through f64.
62
+ loadBits: (base, idx) => ['i64.load', addr(base, idx)],
63
+
64
+ // Element `idx` write of an f64-typed value IR node.
65
+ store: (base, idx, val) => ['f64.store', addr(base, idx), val],
66
+ },
67
+ }
68
+
69
+ // structInline(K) — SRoA carrier factory. Logical element `idx` occupies K
70
+ // consecutive 8-byte cells; the carrier hands back the byte address of the
71
+ // element's first cell, and field `f` is then a plain `+f*8` composed by the
72
+ // schema slot machinery (`ctx.abi.object.ops`). Built on demand per schema
73
+ // (`K = ctx.schema.list[sid].length`) — not a `ctx.abi` default.
74
+ export const structInline = (K) => ({
75
+ K,
76
+ ops: {
77
+ // Byte address of logical element `idx`'s first cell off i32 `base`.
78
+ // A JS-integer idx folds to a constant; an IR-node idx emits `idx*K`
79
+ // then the `<<3` (via `addr`).
80
+ elemAddr: (base, idx) =>
81
+ typeof idx === 'number'
82
+ ? addr(base, idx * K)
83
+ : addr(base, K === 1 ? idx : ['i32.mul', idx, ['i32.const', K]]),
84
+ },
85
+ })
86
+
87
+ // Default carrier — picked when the narrower has no stronger evidence.
88
+ // Reached via `ctx.abi.array`.
89
+ export default taggedLinear
@@ -0,0 +1,35 @@
1
+ /**
2
+ * src/abi — internal codegen carriers.
3
+ *
4
+ * The `abi/` directory hosts compiler-internal codegen modules — one file per
5
+ * value type, each exporting every carrier (slot strategy) the compiler may
6
+ * pick for that type. **No user surface.** `opts.host` is the only knob users
7
+ * see; internal representation is analysis-driven and per-site.
8
+ *
9
+ * Today the narrower has not yet been wired to pick carriers per site, so
10
+ * each type module's `default` export is used as the carrier for every site
11
+ * of that type. `ctx.abi.<type>` resolves to that default; codegen reads
12
+ * `ctx.abi.string.ops.byteLen(...)` etc. Per-site dispatch arrives by
13
+ * exposing all carriers (`ctx.abi.string.sso`, `.jsstring`) and letting
14
+ * `narrow.js` tag each binding with a carrier choice.
15
+ *
16
+ * No presets, no preset-name discriminant, no public ABI knob — carrier
17
+ * choice is analysis-driven, not user-pickable. See `.work/todo.md`
18
+ * "Boundary protocol and internal representation" for the policy.
19
+ *
20
+ * @module src/abi
21
+ */
22
+
23
+ import nanboxF64 from './number.js'
24
+ import sso, { jsstring } from './string.js'
25
+ import tagged from './object.js'
26
+ import taggedLinear, { structInline } from './array.js'
27
+
28
+ /** The default carrier bundle — what `ctx.abi` resolves to. Identity-stable
29
+ * reference so codegen can compare without string keys. */
30
+ export const DEFAULTS = Object.freeze({ number: nanboxF64, string: sso, object: tagged, array: taggedLinear })
31
+
32
+ // Carrier re-exports — for tests and tools that want to reach a specific
33
+ // carrier directly. Per-site narrowing will use these via `ctx.abi.<type>`
34
+ // once the narrower exposes the full carrier dictionary.
35
+ export { nanboxF64, sso, jsstring, tagged, taggedLinear, structInline }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * src/abi/number — number carriers.
3
+ *
4
+ * One file holds every strategy the compiler may pick for a number-typed
5
+ * binding. Today `nanboxF64` is the only carrier; flat-i32 / flat-f64
6
+ * specialization is achieved through direct type narrowing in `narrow.js`
7
+ * (Phase E i32 results, Phase E3 pointer results, locals/params lifted by
8
+ * the fixpoint) rather than via a carrier-bundle dispatch. The single-carrier
9
+ * `ctx.abi` infrastructure is scaffold for the architectural cleanup that
10
+ * would migrate those narrowed cases into named carriers here. See
11
+ * `.work/todo.md` "Boundary protocol and internal representation".
12
+ *
13
+ * Carriers:
14
+ * - `nanboxF64` default. f64 carrier with NaN-boxed pointers. Owns the
15
+ * NaN-box-layout peephole folds — pure-WASM equivalences
16
+ * (`wrap(extend x)→x`, `trunc(convert x)→x`, …) stay in
17
+ * `src/optimize.js`; the folds here assume the low 32 bits
18
+ * of a NaN-boxed f64 are the pointer offset.
19
+ *
20
+ * No `name`/`type` discriminant field — carriers are referenced by object
21
+ * identity from the default-bundle in `src/abi/index.js`.
22
+ *
23
+ * @module src/abi/number
24
+ */
25
+
26
+ // ── nanboxF64 ─────────────────────────────────────────────────────────────
27
+
28
+ export const nanboxF64 = {
29
+ slotTypes: ['f64'],
30
+
31
+ /**
32
+ * Carrier-specific peephole rules — folds that depend on the NaN-box layout.
33
+ * Called by the optimizer's fused-rewrite walk after generic folds; returns
34
+ * a replacement node or `null` to leave the node unchanged.
35
+ *
36
+ * The folds:
37
+ * 1. `i64.reinterpret_f64 (f64.reinterpret_i64 x)` → `x` — rebox-undo.
38
+ * Re-introduced by watr's inliner at boundaries (caller's `boxPtrIR(g)`
39
+ * meets callee's `i32.wrap_i64 (i64.reinterpret_f64 __env)`), so the
40
+ * post-watr reopt pass needs this to keep nanbox call boundaries clean.
41
+ * 2. `f64.reinterpret_i64 (i64.reinterpret_f64 x)` → `x` — unbox-undo.
42
+ * 3. `i32.wrap_i64 (i64.reinterpret_f64 (f64.load A ?off))` → `i32.load A ?off`.
43
+ * Wasm is little-endian; the low 32 bits of a NaN-boxed f64 at A are
44
+ * exactly i32.load(A). Saves two ops on every pointer extraction from
45
+ * an array slot or struct field.
46
+ * 4. `i32.wrap_i64 (i64.reinterpret_f64 (call $__mkptr* … offset))` → offset.
47
+ * A NaN-boxed pointer keeps type/aux in the high bits and the i32
48
+ * offset in the low 32, so the f64 round-trip is pure overhead when the
49
+ * consumer only wants the offset. Covers generic `$__mkptr` and
50
+ * specialized `$__mkptr_T_A_d` trampolines (offset is the last arg).
51
+ * 5. As (4) but reaching through `(block (result f64) … (call $__mkptr …))`
52
+ * — `new TypedArray(n)` lowers to this shape — by retyping the block
53
+ * to i32 and dropping the box on its tail.
54
+ * 6. `i32.wrap_i64 (i64.or HIGH_ONLY (i64.extend_i32_* X))` → X.
55
+ * The NaN-tag-or-extend pattern: high bits hold the NaN prefix + tag,
56
+ * low 32 bits carry the i32 offset unchanged.
57
+ */
58
+ peephole(node) {
59
+ const op = node[0]
60
+
61
+ if (op === 'i64.reinterpret_f64' && node.length === 2) {
62
+ const a = node[1]
63
+ if (Array.isArray(a) && a[0] === 'f64.reinterpret_i64' && a.length === 2) return a[1]
64
+ return null
65
+ }
66
+
67
+ if (op === 'f64.reinterpret_i64' && node.length === 2) {
68
+ const a = node[1]
69
+ if (Array.isArray(a) && a[0] === 'i64.reinterpret_f64' && a.length === 2) return a[1]
70
+ return null
71
+ }
72
+
73
+ if (op === 'i32.wrap_i64' && node.length === 2) {
74
+ const a = node[1]
75
+
76
+ if (Array.isArray(a) && a[0] === 'i64.reinterpret_f64' && a.length === 2) {
77
+ const inner = a[1]
78
+ // (3) wrap(reinterpret(f64.load ADDR ?offset)) → (i32.load ADDR ?offset)
79
+ if (Array.isArray(inner) && inner[0] === 'f64.load') {
80
+ const out = ['i32.load']
81
+ for (let i = 1; i < inner.length; i++) out.push(inner[i])
82
+ return out
83
+ }
84
+ // (4) wrap(reinterpret(call $__mkptr* … offset)) → offset
85
+ if (isMkptr(inner)) return inner[inner.length - 1]
86
+ // (5) reach through (block (result f64) … (call $__mkptr*))
87
+ if (Array.isArray(inner) && inner[0] === 'block' && isMkptr(inner[inner.length - 1])) {
88
+ let ri = -1
89
+ for (let i = 1; i <= 2 && i < inner.length; i++) {
90
+ if (Array.isArray(inner[i]) && inner[i][0] === 'result') { ri = i; break }
91
+ }
92
+ if (ri >= 0 && inner[ri][1] === 'f64') {
93
+ const tail = inner[inner.length - 1]
94
+ const nb = inner.slice()
95
+ nb[ri] = ['result', 'i32']
96
+ nb[nb.length - 1] = tail[tail.length - 1]
97
+ return nb
98
+ }
99
+ }
100
+ return null
101
+ }
102
+
103
+ // (6) wrap(or HIGH_ONLY (extend X)) → X
104
+ if (Array.isArray(a) && a[0] === 'i64.or' && a.length === 3) {
105
+ const l = a[1], r = a[2]
106
+ if (isHighOnly(l) && isExtend(r)) return r[1]
107
+ if (isHighOnly(r) && isExtend(l)) return l[1]
108
+ }
109
+ }
110
+
111
+ return null
112
+ },
113
+ }
114
+
115
+ // ── helpers (carrier-local; cycle-safe — no src/* import) ─────────────────
116
+
117
+ const isMkptr = (n) => Array.isArray(n) && n[0] === 'call' && typeof n[1] === 'string'
118
+ && (n[1] === '$__mkptr' || n[1].startsWith('$__mkptr_'))
119
+
120
+ const isExtend = (n) => Array.isArray(n) &&
121
+ (n[0] === 'i64.extend_i32_u' || n[0] === 'i64.extend_i32_s') && n.length === 2
122
+
123
+ const isHighOnly = (n) => {
124
+ if (!Array.isArray(n) || n[0] !== 'i64.const') return false
125
+ const v = n[1]
126
+ let bi
127
+ if (typeof v === 'number') bi = BigInt(v)
128
+ else if (typeof v === 'string') {
129
+ try { bi = v.startsWith('-') ? -BigInt(v.slice(1)) : BigInt(v) } catch { return false }
130
+ } else return false
131
+ return (bi & 0xFFFFFFFFn) === 0n
132
+ }
133
+
134
+ // Default carrier — picked when narrower has no stronger evidence. Reached
135
+ // via `ctx.abi.number` (which the default-bundle in `src/abi/index.js` binds
136
+ // to this export).
137
+ export default nanboxF64
@@ -0,0 +1,57 @@
1
+ /**
2
+ * src/abi/object — OBJECT layout carriers.
3
+ *
4
+ * An OBJECT value is `__alloc_hdr` + N field cells; the carrier owns where
5
+ * field `i` lives and how wide each cell is. Every object-field access in the
6
+ * compiler — literal stores, `.prop` / `obj['k']` reads & writes, the bulk
7
+ * copies in `Object.values`/`entries`/`assign`/`create` and object spread,
8
+ * JSON-const objects, boxed-handle slot 0, `toPrimitive` method slots —
9
+ * routes through `ctx.abi.object.ops` so layout lives in one place.
10
+ *
11
+ * Ops are cycle-safe (no `src/*` imports) and return raw wasm IR arrays;
12
+ * callers wrap in `typed()`. `base` is an i32 IR node holding the object's
13
+ * data offset (a `['local.get', …]` or a `ptrOffsetIR(…)` expression).
14
+ *
15
+ * tagged — today's layout: schema-tagged, every field one 8-byte f64 cell.
16
+ *
17
+ * A future `packed` carrier (narrowing per-field width from
18
+ * `ctx.schema.slotIntCertain` / `slotTypes`) slots in here without touching a
19
+ * single call site — that is the seam this file exists to provide.
20
+ *
21
+ * `flat` (SRoA) is the third object strategy but lives outside this seam: a
22
+ * non-escaping `let/const o = {staticLiteral}` binding has no heap presence at
23
+ * all — its fields are dissolved into plain WASM locals (`o#0`, `o#1`, …) and
24
+ * `o.prop` compiles to `local.get`. It carries no memory base, so it cannot be
25
+ * expressed as `ops.load(base, i)`; it is a binding-dissolution transform
26
+ * driven by `scanFlatObjects` (src/analyze.js) and the codegen flat hooks
27
+ * (emitDecl, the `.`/`[]` read & write emitters), not a layout carrier here.
28
+ *
29
+ * @module src/abi/object
30
+ */
31
+
32
+ // Byte address of field `i` off an i32 base. idx=0 returns the base node
33
+ // untouched — matches `slotAddr` so routed sites stay byte-identical.
34
+ const addr = (base, i) => i === 0 ? base : ['i32.add', base, ['i32.const', i * 8]]
35
+
36
+ export const tagged = {
37
+ // Field operations the compiler routes object access through.
38
+ ops: {
39
+ // Heap cells `__alloc_hdr` must reserve for an N-field object. Floored at
40
+ // 1 so a zero-field object still owns a header-addressable cell.
41
+ allocSlots: (n) => Math.max(1, n),
42
+
43
+ // Field `i` read as the canonical f64 value.
44
+ load: (base, i) => ['f64.load', addr(base, i)],
45
+
46
+ // Field `i` read as raw i64 bits — dyn-shadow writes and cross-schema
47
+ // copies move slots without reinterpreting through f64.
48
+ loadBits: (base, i) => ['i64.load', addr(base, i)],
49
+
50
+ // Field `i` write of an f64-typed value IR node.
51
+ store: (base, i, val) => ['f64.store', addr(base, i), val],
52
+ },
53
+ }
54
+
55
+ // Default carrier — picked when the narrower has no stronger evidence.
56
+ // Reached via `ctx.abi.object`.
57
+ export default tagged