jz 0.9.0 → 0.9.2
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/README.md +19 -7
- package/bench/bench.svg +18 -18
- package/dist/interop.js +1 -1
- package/dist/jz.js +2026 -1429
- package/interop.js +73 -6
- package/jzify/index.js +7 -1
- package/jzify/transform.js +21 -2
- package/jzify/webrt.js +145 -0
- package/layout.js +13 -3
- package/module/array.js +49 -42
- package/module/collection.js +222 -111
- package/module/console.js +4 -0
- package/module/core.js +86 -7
- package/module/crypto.js +124 -0
- package/module/index.js +3 -1
- package/module/math.js +5 -0
- package/module/navigator.js +26 -0
- package/module/schema.js +11 -3
- package/module/string.js +433 -28
- package/module/timer.js +42 -1
- package/module/typedarray.js +370 -60
- package/package.json +3 -3
- package/src/abi/array.js +24 -16
- package/src/abi/index.js +2 -2
- package/src/abi/object.js +17 -0
- package/src/ast.js +53 -0
- package/src/autoload.js +19 -3
- package/src/compile/analyze.js +190 -21
- package/src/compile/emit-assign.js +111 -7
- package/src/compile/emit.js +84 -20
- package/src/compile/index.js +37 -4
- package/src/compile/inplace-store.js +10 -9
- package/src/compile/narrow.js +71 -1
- package/src/compile/plan/index.js +5 -0
- package/src/compile/plan/literals.js +11 -2
- package/src/ctx.js +14 -0
- package/src/ir.js +26 -1
- package/src/optimize/index.js +1 -0
- package/src/optimize/vectorize.js +80 -6
- package/src/prepare/index.js +6 -0
- package/src/static.js +31 -0
- package/src/type.js +356 -47
package/src/ctx.js
CHANGED
|
@@ -385,6 +385,20 @@ export function reset(proto, globals, bridge) {
|
|
|
385
385
|
// Populated whole-program by `analyzeStructInline`
|
|
386
386
|
// (default-disqualify); read by the array
|
|
387
387
|
// push/index/length codegen.
|
|
388
|
+
inlineCellI32: new Set(), // inlineArray subset — every slot strict-int32
|
|
389
|
+
// (slotI32Certain) and K ≥ 2, so the element
|
|
390
|
+
// packs K raw i32 cells (⌈K/2⌉ physical 8-byte
|
|
391
|
+
// cells, C's exact record layout): raw i32
|
|
392
|
+
// loads/stores, no trunc_sat/convert per field.
|
|
393
|
+
// Standalone `{S}` objects of the same sid keep
|
|
394
|
+
// f64 slots — the packed decision rides the
|
|
395
|
+
// CURSOR (inlineCellCursors → node.cellI32),
|
|
396
|
+
// never the bare sid.
|
|
397
|
+
inlineCellCursors: new Map(), // sig → Set<name>: `const p = a[i]` cursor
|
|
398
|
+
// locals of packed (inlineCellI32) arrays in
|
|
399
|
+
// that function — readVar tags their reads
|
|
400
|
+
// with `.cellI32` so slot access picks the
|
|
401
|
+
// packed i32 ops instead of f64 cells.
|
|
388
402
|
}
|
|
389
403
|
|
|
390
404
|
ctx.closure = {
|
package/src/ir.js
CHANGED
|
@@ -835,7 +835,7 @@ export const cloneIR = (n) => Array.isArray(n) ? n.map(cloneIR) : n
|
|
|
835
835
|
* anything else → itself. `valIR` must be side-effect-free (a local read) — it is duplicated,
|
|
836
836
|
* so each occurrence gets a fresh clone. Used for bindings flagged in ctx.func.maybeNullish;
|
|
837
837
|
* a real number isn't either sentinel, so it falls through the `else` unchanged. */
|
|
838
|
-
const coerceNullishToNum = (valIR) => typed(
|
|
838
|
+
export const coerceNullishToNum = (valIR) => typed(
|
|
839
839
|
['if', ['result', 'f64'],
|
|
840
840
|
['i64.eq', ['i64.reinterpret_f64', cloneIR(valIR)], ['i64.const', NULL_NAN]],
|
|
841
841
|
['then', ['f64.const', 0]],
|
|
@@ -857,6 +857,26 @@ export function toNumF64(node, v) {
|
|
|
857
857
|
// not a number — skipping coercion would reinterpret pointer bits as an f64.
|
|
858
858
|
// Only a plain i32 (loop counter, `x|0`) is genuinely already-numeric.
|
|
859
859
|
if ((v.type === 'i32' && v.ptrKind == null) || isLit(v)) return asF64(v)
|
|
860
|
+
// Checked typed-array read (`.typed:[]` tags checkedNumRead): number|undefined
|
|
861
|
+
// with the undefined confined to a CONSTANT miss arm. ToNumber of that arm
|
|
862
|
+
// folds statically (undefined → canonical NaN) — the hit arm is already a
|
|
863
|
+
// plain-number load. Without the fold the UNDEF sentinel enters f64 arithmetic
|
|
864
|
+
// as a "number" (valTypeOf claims NUMBER from the ELEMENT type, blind to the
|
|
865
|
+
// OOB path — checked BEFORE the vt fast-outs below for exactly that reason),
|
|
866
|
+
// and hardware NaN propagation carries its PAYLOAD to the escape, where the
|
|
867
|
+
// boundary decodes it back as `undefined` (JS: NaN).
|
|
868
|
+
if (v.checkedNumRead && Array.isArray(v)) {
|
|
869
|
+
const foldArm = (n) => Array.isArray(n) && n[0] === 'f64.const' && n[1] === `nan:${UNDEF_NAN}`
|
|
870
|
+
? ['f64.const', 'nan'] : n
|
|
871
|
+
if (v[0] === 'if') // (if (result f64) cond (then load) (else UNDEF))
|
|
872
|
+
return typed(v.map(c => Array.isArray(c) && c[0] === 'else' && c.length === 2
|
|
873
|
+
? ['else', foldArm(c[1])] : c), 'f64')
|
|
874
|
+
if (v[0] === 'block') { // (block (result f64) …sets (select load UNDEF in))
|
|
875
|
+
const tail = v[v.length - 1]
|
|
876
|
+
if (Array.isArray(tail) && tail[0] === 'select')
|
|
877
|
+
return typed([...v.slice(0, -1), ['select', tail[1], foldArm(tail[2]), tail[3]]], 'f64')
|
|
878
|
+
}
|
|
879
|
+
}
|
|
860
880
|
// A binding assigned a nullish literal may hold null/undefined here — coerce per ToNumber
|
|
861
881
|
// (null→+0, undefined→NaN); a real number falls through unchanged. Only flagged bindings pay
|
|
862
882
|
// this, so the numeric kernels jz optimizes for (which never assign null) stay untouched.
|
|
@@ -1240,6 +1260,11 @@ export function readVar(name) {
|
|
|
1240
1260
|
node.ptrKind = rep.ptrKind
|
|
1241
1261
|
const aux = rep.ptrAux ?? ctx.schema.idOf?.(name)
|
|
1242
1262
|
if (aux != null) node.ptrAux = aux
|
|
1263
|
+
// structInline cursor into a PACKED (i32-cell) array: slot access must
|
|
1264
|
+
// pick the packedI32 ops, not the f64 slot layout — the flag rides the
|
|
1265
|
+
// node because a standalone object of the same sid keeps f64 slots.
|
|
1266
|
+
if (rep.ptrKind === VAL.OBJECT && ctx.schema.inlineCellCursors?.get(ctx.func.current)?.has(name))
|
|
1267
|
+
node.cellI32 = true
|
|
1243
1268
|
}
|
|
1244
1269
|
return node
|
|
1245
1270
|
}
|
package/src/optimize/index.js
CHANGED
|
@@ -166,6 +166,7 @@ const LEVEL_PRESETS = Object.freeze({
|
|
|
166
166
|
unrollRecurrence: false, // ×2 body duplication is a size regression — speed-only
|
|
167
167
|
clampPeel: false, // edge-clamp peel triples a stencil loop (clamp-free interior + 2 edges) to vectorize — speed-only
|
|
168
168
|
versionTypedBounds: false,// typed-bounds loop versioning duplicates every proven nest (guarded fast arm + checked twin, ×1.5-3 on small kernels) — the branchless checked reads alone are the size-tier lowering; speed-only trade
|
|
169
|
+
leanCheckedIdx: true, // unproven typed reads emit the if-form (guard → direct load, else undefined) — ~6 ops/site smaller than the select-clamp form, which exists only so SPEED-tier kernel bodies stay branch-free for the SIMD lift (off here)
|
|
169
170
|
|
|
170
171
|
boolConvertToSelect: false, // adds a const + op per site — speed-only latency trade
|
|
171
172
|
devirtIndirect: false, // guards + duplicated args grow bytes — speed-only trade
|
|
@@ -978,6 +978,32 @@ function matchLaneOffset(off, ind, offsetTees, allowAos, aosPix, idxTees) {
|
|
|
978
978
|
return null
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Mirror lane address: `base + ((INV − iv) << K)` — the DESCENDING twin of the
|
|
983
|
+
* canonical lane address (symmetric fills: `inp[N−k] = lm`). INV is an i32
|
|
984
|
+
* const or a bare local; the CALLER must verify a named INV is body-invariant
|
|
985
|
+
* (not in the body writes set). f64 store sites only (2 lanes): the vector for
|
|
986
|
+
* (iv, iv+1) mirrors to (INV−iv, INV−iv−1) — contiguous descending, stored as
|
|
987
|
+
* one v128 at INV−iv−1 with the lanes swapped.
|
|
988
|
+
*/
|
|
989
|
+
function matchMirrorAddr(addr, ind) {
|
|
990
|
+
if (!isArr(addr) || addr[0] !== 'i32.add' || addr.length !== 3) return null
|
|
991
|
+
for (const k of [1, 2]) {
|
|
992
|
+
const off = addr[k]
|
|
993
|
+
if (!isArr(off) || off[0] !== 'i32.shl' || off.length !== 3) continue
|
|
994
|
+
const K = constNum(off[2])
|
|
995
|
+
if (K == null || K < 0 || K > 3) continue
|
|
996
|
+
const sub = off[1]
|
|
997
|
+
if (!isArr(sub) || sub[0] !== 'i32.sub' || sub.length !== 3) continue
|
|
998
|
+
if (!isLocalGet(sub[2], ind)) continue
|
|
999
|
+
const inv = sub[1]
|
|
1000
|
+
const invName = isArr(inv) && inv[0] === 'local.get' && typeof inv[1] === 'string' ? inv[1] : null
|
|
1001
|
+
if (!invName && constNum(inv) == null) continue
|
|
1002
|
+
return { base: addr[k === 1 ? 2 : 1], invExpr: inv, invName, strideLog2: K }
|
|
1003
|
+
}
|
|
1004
|
+
return null
|
|
1005
|
+
}
|
|
1006
|
+
|
|
981
1007
|
/**
|
|
982
1008
|
* Match an address expression `(i32.add base OFFSET)`, with optional outer
|
|
983
1009
|
* `(local.tee $A ...)`. OFFSET is matched by matchLaneOffset (which also
|
|
@@ -1267,6 +1293,7 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
|
|
|
1267
1293
|
const allowAos = true
|
|
1268
1294
|
const aosPix = new Map()
|
|
1269
1295
|
let aosPixelStride = 1
|
|
1296
|
+
const mirrorSites = [] // mirror stores a[INV−iv] — invariance of INV checked post-scan
|
|
1270
1297
|
// Pixel-INDEX locals: `$J = P*i` (the `const j = 3*i` of an AoS loop, kept as its own local
|
|
1271
1298
|
// pre-watr). A channel address is then `base + ((local.get $J) << K)`; idxTees lets
|
|
1272
1299
|
// matchLaneOffset resolve $J → pixel-stride P. Value -1 marks an inconsistent local (bail).
|
|
@@ -1357,8 +1384,20 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
|
|
|
1357
1384
|
if (laneType != null && sty !== laneType && !narrowing) return false
|
|
1358
1385
|
if (laneType == null) { laneType = sty; stride = LANE_INFO[laneType].stride }
|
|
1359
1386
|
const memarg = typeof node[1] === 'string' && node[1].startsWith('offset=')
|
|
1360
|
-
|
|
1361
|
-
if (!m)
|
|
1387
|
+
let m = matchLaneAddr(memAddr(node), incVar, addrLocals, offsetTees, allowAos, aosPix, idxTees)
|
|
1388
|
+
if (!m) {
|
|
1389
|
+
// mirror store `a[INV − iv] = lane` (f64, full-width, no memarg): the
|
|
1390
|
+
// descending twin — accepted as its own site class; INV invariance is
|
|
1391
|
+
// verified post-scan against the body writes set.
|
|
1392
|
+
const mm = !memarg && sty === 'f64' && laneType === 'f64' && matchMirrorAddr(memAddr(node), incVar)
|
|
1393
|
+
if (mm && (1 << mm.strideLog2) === stride) {
|
|
1394
|
+
mirrorSites.push(mm)
|
|
1395
|
+
siteStrides.push(1)
|
|
1396
|
+
loadStoreSites.push({ parent, idx: pi, kind: 'store' })
|
|
1397
|
+
return scanForLoadsStores(node[2], node, 2)
|
|
1398
|
+
}
|
|
1399
|
+
return false
|
|
1400
|
+
}
|
|
1362
1401
|
if ((1 << m.strideLog2) !== (narrowing ? LANE_INFO[sty].stride : stride)) return false
|
|
1363
1402
|
if (!recordAos(m)) return false
|
|
1364
1403
|
if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, pixelStride: m.pixelStride, base: m.base })
|
|
@@ -1410,6 +1449,10 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
|
|
|
1410
1449
|
const writes = new Set()
|
|
1411
1450
|
for (const s of body) collectWrites(s, writes)
|
|
1412
1451
|
if (boundLocal && writes.has(boundLocal)) return null // bound varies in body → bail
|
|
1452
|
+
// a mirror INV written in the body is not invariant — the descending window would drift
|
|
1453
|
+
for (const mm of mirrorSites) if (mm.invName && writes.has(mm.invName)) return null
|
|
1454
|
+
// AoS gather/scatter and mirror windows don't compose (distinct per-step deltas)
|
|
1455
|
+
if (mirrorSites.length && aosPixelStride > 1) return null
|
|
1413
1456
|
|
|
1414
1457
|
const localKind = new Map() // name → 'lane' | 'invariant' | 'addr'
|
|
1415
1458
|
// Walk to collect ALL referenced names
|
|
@@ -1542,9 +1585,14 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
|
|
|
1542
1585
|
]
|
|
1543
1586
|
]
|
|
1544
1587
|
|
|
1545
|
-
// Bound setup:
|
|
1588
|
+
// Bound setup: align the SPAN, not the bound — simdBound = iv + ((bound − iv)
|
|
1589
|
+
// & ~(lanes−1)). `bound & mask` assumed a 0 entry: a loop entering at k=1
|
|
1590
|
+
// (symmetric fills start past the DC bin) would run its last vector step at
|
|
1591
|
+
// k = bound−1 and overrun one lane past the bound. iv holds the entry value
|
|
1592
|
+
// here (the setup precedes the SIMD block).
|
|
1546
1593
|
const boundSetup = ['local.set', simdBoundName,
|
|
1547
|
-
['i32.
|
|
1594
|
+
['i32.add', ['local.get', incVar],
|
|
1595
|
+
['i32.and', ['i32.sub', boundExpr, ['local.get', incVar]], ['i32.const', mask]]]]
|
|
1548
1596
|
|
|
1549
1597
|
// Synthetic outer wrapper — has no result, no label, just sequences.
|
|
1550
1598
|
// A clone of any LICM-hoisted preamble runs first (so the SIMD block sees the
|
|
@@ -2359,7 +2407,10 @@ function tryMapReduceVectorize(bl, fnLocals, freshIdRef) {
|
|
|
2359
2407
|
...lifted,
|
|
2360
2408
|
['local.set', incVar, ['i32.add', ['local.get', incVar], ['i32.const', 2]]],
|
|
2361
2409
|
['br', simdLoop]]]
|
|
2362
|
-
|
|
2410
|
+
// span-aligned (same entry≠0 hazard as tryVectorize's bound — see there)
|
|
2411
|
+
const boundSetup = ['local.set', simdBoundName,
|
|
2412
|
+
['i32.add', ['local.get', incVar],
|
|
2413
|
+
['i32.and', ['i32.sub', boundExpr, ['local.get', incVar]], ['i32.const', -2]]]]
|
|
2363
2414
|
const wrapper = ['block', boundSetup, simdBlock, bl.blockNode]
|
|
2364
2415
|
return { wrapper, newLocalDecls: [['local', simdBoundName, 'i32'], ...newLocalDecls] }
|
|
2365
2416
|
}
|
|
@@ -2807,6 +2858,26 @@ function liftStmt(stmt, ctx) {
|
|
|
2807
2858
|
// Handle memarg if present (last positional after addr/val): unlikely in
|
|
2808
2859
|
// pre-watr IR for this shape; bail if more than 3 children.
|
|
2809
2860
|
if (stmt.length !== 3) return liftFail(ctx, `${op} with memarg`)
|
|
2861
|
+
// Mirror store `a[INV − iv] = lane` (f64, 2 lanes): the vector's lanes
|
|
2862
|
+
// (iv, iv+1) mirror to (INV−iv, INV−iv−1) — one v128 store at INV−iv−1
|
|
2863
|
+
// with the f64 lanes SWAPPED. The scalar remainder keeps the plain form.
|
|
2864
|
+
if (ctx.laneType === 'f64' && sty === 'f64') {
|
|
2865
|
+
const mm = matchMirrorAddr(addr, ctx.incVar)
|
|
2866
|
+
if (mm) {
|
|
2867
|
+
const v = liftExprV(stmt[2], ctx)
|
|
2868
|
+
if (ctx.fail) return null
|
|
2869
|
+
const vt = `$__mirv${ctx.freshIdRef.next++}`
|
|
2870
|
+
ctx.extraLocals.push(['local', vt, 'v128'])
|
|
2871
|
+
const mAddr = ['i32.add', mm.base,
|
|
2872
|
+
['i32.shl', ['i32.sub', ['i32.sub', mm.invExpr, ['local.get', ctx.incVar]], ['i32.const', 1]],
|
|
2873
|
+
['i32.const', mm.strideLog2]]]
|
|
2874
|
+
return ['__seq__',
|
|
2875
|
+
['local.set', vt, v],
|
|
2876
|
+
['v128.store', mAddr,
|
|
2877
|
+
['i8x16.shuffle', '8', '9', '10', '11', '12', '13', '14', '15', '0', '1', '2', '3', '4', '5', '6', '7',
|
|
2878
|
+
['local.get', vt], ['local.get', vt]]]]
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2810
2881
|
// Narrowing store: a narrower element written from a wider float lane (`o[i] =
|
|
2811
2882
|
// narrow(f(x))` — codec encode / downsample). The scalar store value carries a
|
|
2812
2883
|
// conversion (f32.demote_f64, or the float→int ToInt32 idiom); peel it, lift the
|
|
@@ -3992,7 +4063,10 @@ function tryRampMap(blockNode, fnLocals, freshIdRef) {
|
|
|
3992
4063
|
...lifted,
|
|
3993
4064
|
...scaledIncs,
|
|
3994
4065
|
['br', simdLoopLabel]]]
|
|
3995
|
-
|
|
4066
|
+
// span-aligned (same entry≠0 hazard as tryVectorize's bound — see there)
|
|
4067
|
+
const boundSetup = ['local.set', simdBoundName,
|
|
4068
|
+
['i32.add', ['local.get', ivName],
|
|
4069
|
+
['i32.and', ['i32.sub', boundExpr, ['local.get', ivName]], ['i32.const', -LANES]]]]
|
|
3996
4070
|
const wrapper = ['block', boundSetup, simdBlock, blockNode]
|
|
3997
4071
|
const newLocalDecls = [
|
|
3998
4072
|
['local', simdBoundName, 'i32'],
|
package/src/prepare/index.js
CHANGED
|
@@ -1051,6 +1051,12 @@ export const GLOBALS = Object.assign(Object.create(null), {
|
|
|
1051
1051
|
parseFloat: 'number',
|
|
1052
1052
|
encodeURIComponent: 'encodeURIComponent',
|
|
1053
1053
|
decodeURIComponent: 'decodeURIComponent',
|
|
1054
|
+
encodeURI: 'encodeURI',
|
|
1055
|
+
decodeURI: 'decodeURI',
|
|
1056
|
+
atob: 'atob',
|
|
1057
|
+
btoa: 'btoa',
|
|
1058
|
+
crypto: 'crypto',
|
|
1059
|
+
navigator: 'navigator',
|
|
1054
1060
|
Error: 'Error',
|
|
1055
1061
|
// Error subclasses: distinct names in JS, but jz doesn't carry typed error
|
|
1056
1062
|
// info — `throw` accepts any value and stringification goes through the
|
package/src/static.js
CHANGED
|
@@ -163,6 +163,37 @@ export function objLiteralSchemaId(expr) {
|
|
|
163
163
|
return parsed ? ctx.schema.register(parsed.names) : null
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** Canonical content key for an inplace/structInline replace-store site —
|
|
167
|
+
* the ','-wrapper around literal props is normalized away between plan and
|
|
168
|
+
* emit, so flatten before serializing. Shared by scanInplaceStores (plan),
|
|
169
|
+
* analyzeStructInline (eligibility), and the emit arms; lives here so the
|
|
170
|
+
* three importers stay acyclic (the self-host resolver rejects cycles). */
|
|
171
|
+
export const inplaceKey = (arrName, lit) => {
|
|
172
|
+
const props = lit.slice(1)
|
|
173
|
+
const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ',' ? props[0].slice(1) : props
|
|
174
|
+
return `${arrName}|${JSON.stringify(flat)}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** K schema-ordered field-value AST nodes of an object literal `{S}` — the
|
|
178
|
+
* cell-store order for a structInline `.push({S})` / `a[i] = {S}` — or null if
|
|
179
|
+
* `lit` is not a plain static-key `{}` literal carrying exactly schema `sid`'s
|
|
180
|
+
* fields. Mapped by name into schema order so sites with differing key order
|
|
181
|
+
* flatten to the same cell run. */
|
|
182
|
+
export function structLiteralFields(lit, sid) {
|
|
183
|
+
if (!Array.isArray(lit) || lit[0] !== '{}') return null
|
|
184
|
+
const parsed = staticObjectProps(lit.slice(1))
|
|
185
|
+
const schema = ctx.schema.list[sid]
|
|
186
|
+
if (!parsed || parsed.names.length !== schema.length) return null
|
|
187
|
+
const byName = new Map()
|
|
188
|
+
for (let i = 0; i < parsed.names.length; i++) byName.set(parsed.names[i], parsed.values[i])
|
|
189
|
+
const out = []
|
|
190
|
+
for (const name of schema) {
|
|
191
|
+
if (!byName.has(name)) return null
|
|
192
|
+
out.push(byName.get(name))
|
|
193
|
+
}
|
|
194
|
+
return out
|
|
195
|
+
}
|
|
196
|
+
|
|
166
197
|
/** Resolve schemaId of an expression, given a per-function schemaId map for locals.
|
|
167
198
|
* Used for both intra-function arr elem-schema observation and func.arrayElemSchema
|
|
168
199
|
* return inference. Recognizes: object literals, var names with bound schemaId,
|