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
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
import { ctx, err, inc, warnDeopt, PTR, LAYOUT } from '../ctx.js'
|
|
14
14
|
import { T } from '../ast.js'
|
|
15
|
-
import { staticPropertyKey, staticIndexKey, staticObjectProps } from '../static.js'
|
|
15
|
+
import { staticPropertyKey, staticIndexKey, staticObjectProps, inlineArraySid, structLiteralFields, inplaceKey } from '../static.js'
|
|
16
|
+
import { packedI32, structInline } from '../abi/index.js'
|
|
16
17
|
import { i64Hex, encodePtrHi } from '../../layout.js'
|
|
17
|
-
import { inplaceKey } from './inplace-store.js'
|
|
18
18
|
import { recordDynFnTableWrite } from './dyn-closure-tables.js'
|
|
19
19
|
import { valTypeOf, shapeOf } from '../kind.js'
|
|
20
20
|
import { VAL, lookupValType, repOf } from '../reps.js'
|
|
21
21
|
import {
|
|
22
22
|
typed, asF64, asI32, asI64, temp, tempI32, withTemp, block64,
|
|
23
23
|
ptrOffsetIR, ptrTypeEq, boxedAddr, writeVar, isGlobal, isBoundName, isLiteralStr,
|
|
24
|
-
usesDynProps, needsDynShadow, boolBoxIR, carrierF64, mkPtrIR, isNumericIR,
|
|
24
|
+
usesDynProps, needsDynShadow, boolBoxIR, carrierF64, mkPtrIR, isNumericIR, undefExpr,
|
|
25
25
|
} from '../ir.js'
|
|
26
26
|
import { emit } from '../bridge.js'
|
|
27
27
|
|
|
@@ -324,7 +324,7 @@ function tryInplaceReplaceStore(arr, idx, val) {
|
|
|
324
324
|
const reuse = aliasOk && aliasType === 'f64' ? ['local.get', `$${entry.alias}`] : null
|
|
325
325
|
inc('__alloc_hdr')
|
|
326
326
|
if (!reuse) inc('__arr_idx')
|
|
327
|
-
const kT = tempI32('ipk'), eT = temp('ipe'), oT = tempI32('ipo'), hT = tempI32('iph')
|
|
327
|
+
const kT = tempI32('ipk'), eT = temp('ipe'), oT = tempI32('ipo'), hT = tempI32('iph'), aTb = temp('ipa')
|
|
328
328
|
const bitsE = () => ['i64.reinterpret_f64', ['local.get', `$${eT}`]]
|
|
329
329
|
const fast = ['block', ['result', 'f64'],
|
|
330
330
|
['local.set', `$${oT}`, ['i32.wrap_i64', bitsE()]],
|
|
@@ -333,12 +333,17 @@ function tryInplaceReplaceStore(arr, idx, val) {
|
|
|
333
333
|
const slow = ['block', ['result', 'f64'],
|
|
334
334
|
['local.set', `$${hT}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ops.allocSlots(schema.length)]]],
|
|
335
335
|
...slots.map((slot, i) => ops.store(['local.get', `$${hT}`], slot, ['local.get', `$${vTs[i]}`])),
|
|
336
|
-
storeArrayPayload(
|
|
336
|
+
storeArrayPayload(typed(['local.get', `$${aTb}`], 'f64'), ['f64.convert_i32_s', ['local.get', `$${kT}`]],
|
|
337
337
|
mkPtrIR(PTR.OBJECT, sid, ['local.get', `$${hT}`]), persistBinding(arr))]
|
|
338
|
+
// JS member-store order: GetValue(base), then the property key, then the RHS
|
|
339
|
+
// values — `a[i++] = {x: i}`'s x must see the incremented i while the store
|
|
340
|
+
// still lands at the old index (caught by a differential probe; the values-
|
|
341
|
+
// first order shipped that divergence at every optimize level).
|
|
338
342
|
return typed(['block', ['result', 'f64'],
|
|
339
|
-
|
|
343
|
+
['local.set', `$${aTb}`, asF64(emit(arr))],
|
|
340
344
|
['local.set', `$${kT}`, asI32(emit(idx))],
|
|
341
|
-
['local.set', `$${
|
|
345
|
+
...parsed.values.map((v, i) => ['local.set', `$${vTs[i]}`, storedValue(v)]),
|
|
346
|
+
['local.set', `$${eT}`, reuse ?? ['call', '$__arr_idx', ['i64.reinterpret_f64', ['local.get', `$${aTb}`]], ['local.get', `$${kT}`]]],
|
|
342
347
|
['if', ['result', 'f64'],
|
|
343
348
|
['i64.eq',
|
|
344
349
|
['i64.and', bitsE(), ['i64.const', '0xFFFFFFFF00000000']],
|
|
@@ -347,6 +352,88 @@ function tryInplaceReplaceStore(arr, idx, val) {
|
|
|
347
352
|
['else', slow]]], 'f64')
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
/** structInline Array<S> wholesale element replace `a[i] = {S-literal}` — K
|
|
356
|
+
* f64 cell stores into the element's inline cells: no allocation, no box
|
|
357
|
+
* read, no identity guard (cells cannot hold aliens — the layout is fixed by
|
|
358
|
+
* analyzeStructInline's whole-program proof, which accepted this store via
|
|
359
|
+
* the inplace sweep's alias-liveness + reuse verdicts).
|
|
360
|
+
*
|
|
361
|
+
* Evaluation order (JS: member target before RHS): the index and — on the
|
|
362
|
+
* non-cursor path — the receiver box spill FIRST, then the slot values (they
|
|
363
|
+
* may read the old element's fields: `a[i] = {x: p.y, y: p.x}` swaps).
|
|
364
|
+
*
|
|
365
|
+
* Bounds: one `cellIdx < physLen` u-compare. In-bounds → K stores; anything
|
|
366
|
+
* else — i ≥ length (JS: array-extend) or a negative int-certain index
|
|
367
|
+
* (JS: sidecar property) — DROPS the write, the same contract as the
|
|
368
|
+
* checked-by-default typed store (OOB writes ignored). By then JS itself
|
|
369
|
+
* would have thrown at the cursor's `p.x` projection (undefined.x), which
|
|
370
|
+
* the carrier's unchecked cursor read already deviates on; the analyzer only
|
|
371
|
+
* accepts stores preceded by a same-index cursor read (reuse verdict), so
|
|
372
|
+
* append-idiom builders (`out[out.length] = {…}`) stay on the plain layout.
|
|
373
|
+
* No grow call exists in the arm, so loop-invariant base hoists stay sound.
|
|
374
|
+
*
|
|
375
|
+
* Address: with the sweep's target-binding reuse, the cursor IS the cell
|
|
376
|
+
* address — base derives as `cursor − cellIdx*8` (pure arith); otherwise via
|
|
377
|
+
* __ptr_offset on the spilled box. */
|
|
378
|
+
function tryStructInlineReplaceStore(arr, idx, val) {
|
|
379
|
+
if (typeof arr !== 'string') return null
|
|
380
|
+
const sid = inlineArraySid(arr)
|
|
381
|
+
if (sid == null) return null
|
|
382
|
+
const schema = ctx.schema.list[sid]
|
|
383
|
+
const fields = structLiteralFields(val, sid)
|
|
384
|
+
// analyzeStructInline accepted every store site for this sid — a shape this
|
|
385
|
+
// arm cannot lower here means the phases disagree; never fall through to a
|
|
386
|
+
// boxed store on cell memory.
|
|
387
|
+
if (!fields) err(`structInline replace-store expects { ${schema.join(', ')} } literal`)
|
|
388
|
+
const void_ = ctx.func._expect === 'void'
|
|
389
|
+
const K = schema.length
|
|
390
|
+
const packed = ctx.schema.inlineCellI32?.has(sid)
|
|
391
|
+
const cpe = structInline(K, packed).cpe // physical 8-byte cells per element
|
|
392
|
+
const ops = packed ? packedI32.ops : ctx.abi.object.ops
|
|
393
|
+
const entry = ctx.schema.inplaceStores?.get(inplaceKey(arr, val))
|
|
394
|
+
// Cursor reuse under the same conditions as tryInplaceReplaceStore's
|
|
395
|
+
// strongest form: the tracked alias is an unboxed i32 OBJECT pointer of this
|
|
396
|
+
// schema reading the same index — for the inline carrier that IS the cell
|
|
397
|
+
// address (array.js '[]' structInline arm).
|
|
398
|
+
const alias = entry?.alias && typeof idx === 'string' && idx === entry.idx &&
|
|
399
|
+
!ctx.func.boxed?.has(entry.alias) && ctx.func.locals.get(entry.alias) === 'i32' &&
|
|
400
|
+
repOf(entry.alias)?.ptrKind === VAL.OBJECT &&
|
|
401
|
+
(ctx.schema.vars.get(entry.alias) ?? repOf(entry.alias)?.schemaId) === sid
|
|
402
|
+
? entry.alias : null
|
|
403
|
+
const kT = tempI32('sik'), cT = tempI32('sic'), bT = tempI32('sib'), aT = tempI32('sia')
|
|
404
|
+
const vTs = fields.map(() => packed ? tempI32('siv') : temp('siv'))
|
|
405
|
+
const boxT = alias ? null : temp('sit')
|
|
406
|
+
if (!alias) inc('__ptr_offset')
|
|
407
|
+
const cellIdx = cpe === 1 ? ['local.get', `$${kT}`] : ['i32.mul', ['local.get', `$${kT}`], ['i32.const', cpe]]
|
|
408
|
+
const body = [
|
|
409
|
+
['local.set', `$${kT}`, asI32(emit(idx))],
|
|
410
|
+
...(boxT ? [['local.set', `$${boxT}`, asF64(emit(arr))]] : []),
|
|
411
|
+
// packed values are int32-exact by the slotI32Certain census
|
|
412
|
+
...fields.map((v, i) => ['local.set', `$${vTs[i]}`, packed ? asI32(emit(v)) : storedValue(v)]),
|
|
413
|
+
['local.set', `$${cT}`, cellIdx],
|
|
414
|
+
['local.set', `$${bT}`, alias
|
|
415
|
+
? ['i32.sub', ['local.get', `$${alias}`], ['i32.shl', ['local.get', `$${cT}`], ['i32.const', 3]]]
|
|
416
|
+
: ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${boxT}`]]]],
|
|
417
|
+
]
|
|
418
|
+
const inBounds = ['i32.lt_u', ['local.get', `$${cT}`],
|
|
419
|
+
['i32.load', ['i32.sub', ['local.get', `$${bT}`], ['i32.const', 8]]]]
|
|
420
|
+
const stores = [
|
|
421
|
+
['local.set', `$${aT}`, ['i32.add', ['local.get', `$${bT}`], ['i32.shl', ['local.get', `$${cT}`], ['i32.const', 3]]]],
|
|
422
|
+
...fields.map((v, i) => ops.store(['local.get', `$${aT}`], i, ['local.get', `$${vTs[i]}`])),
|
|
423
|
+
]
|
|
424
|
+
if (void_) return typed(['block', ...body, ['if', inBounds, ['then', ...stores]]], 'void')
|
|
425
|
+
// Value position is analyzer-poisoned (sweep candidates are statement-only);
|
|
426
|
+
// belt for exotic statement shapes: yield the element as a boxed pointer —
|
|
427
|
+
// under the inline carrier the cell address IS the object identity. A
|
|
428
|
+
// PACKED cell address cannot be boxed (slot reads through a box assume f64
|
|
429
|
+
// cells), so the phases disagreeing there is a compile error, not bytes.
|
|
430
|
+
if (packed) err('structInline packed replace-store in value position — analyzeStructInline must poison this shape')
|
|
431
|
+
return typed(['block', ['result', 'f64'], ...body,
|
|
432
|
+
['if', ['result', 'f64'], inBounds,
|
|
433
|
+
['then', ...stores, mkPtrIR(PTR.OBJECT, sid, ['local.get', `$${aT}`])],
|
|
434
|
+
['else', undefExpr()]]], 'f64')
|
|
435
|
+
}
|
|
436
|
+
|
|
350
437
|
export function emitElementAssign(arr, idx, val) {
|
|
351
438
|
// 0. `obj.prop[idx] = val` where `obj`'s type is fully unknown (so `obj`
|
|
352
439
|
// could be a host EXTERNAL object at runtime) — `__ext_prop` (interop.js)
|
|
@@ -389,6 +476,11 @@ export function emitElementAssign(arr, idx, val) {
|
|
|
389
476
|
['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]]]],
|
|
390
477
|
['local.get', `$${resultTmp}`])
|
|
391
478
|
}
|
|
479
|
+
// structInline receivers first: once analyzeStructInline committed the sid
|
|
480
|
+
// to the inline-cell layout, a boxed store (generic/inplace paths below)
|
|
481
|
+
// would corrupt cell memory — this arm is the only sound lowering.
|
|
482
|
+
const sIn = tryStructInlineReplaceStore(arr, idx, val)
|
|
483
|
+
if (sIn) return sIn
|
|
392
484
|
const rmw = ctx.transform.optimize ? tryHashRmwFusion(arr, idx, val) : null
|
|
393
485
|
if (rmw) return rmw
|
|
394
486
|
const inplace = ctx.transform.optimize ? tryInplaceReplaceStore(arr, idx, val) : null
|
|
@@ -646,6 +738,18 @@ export function emitPropertyAssign(obj, prop, val) {
|
|
|
646
738
|
if (si >= 0) {
|
|
647
739
|
const shadow = needsDynShadow(typeof obj === 'string' ? obj : null)
|
|
648
740
|
if (shadow) inc('__dyn_set')
|
|
741
|
+
// Packed i32 cells (structInline cursor, `.cellI32` node tag): the
|
|
742
|
+
// field is a raw i32 at +si*4 — i32.store, no f64 boxing. The store
|
|
743
|
+
// value is int32-exact by the slotI32Certain census (packing
|
|
744
|
+
// precondition); the expression result converts back at one op.
|
|
745
|
+
if (vaProbe.cellI32) {
|
|
746
|
+
const t = tempI32('pcw')
|
|
747
|
+
return block64(
|
|
748
|
+
['local.set', `$${t}`, asI32(emit(val))],
|
|
749
|
+
packedI32.ops.store(ptrOffsetIR(vaProbe, VAL.OBJECT), si, ['local.get', `$${t}`]),
|
|
750
|
+
...(shadow ? [['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', asF64(emit(obj))], asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['f64.convert_i32_s', ['local.get', `$${t}`]]]]]] : []),
|
|
751
|
+
['f64.convert_i32_s', ['local.get', `$${t}`]])
|
|
752
|
+
}
|
|
649
753
|
return withTemp(storedValue(val), t => [
|
|
650
754
|
ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(obj)), VAL.OBJECT), si, ['local.get', `$${t}`]),
|
|
651
755
|
...(shadow ? [['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', asF64(emit(obj))], asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]] : []),
|
package/src/compile/emit.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import {
|
|
25
25
|
commaList, T, isBlockBody, isReassigned, mutatesArrayLength, isConstLiteral, constLiteralHoistable,
|
|
26
26
|
hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
|
|
27
|
-
ASSIGN_OPS,
|
|
27
|
+
ASSIGN_OPS, firstRefKind,
|
|
28
28
|
} from '../ast.js'
|
|
29
29
|
import { ctx, err, inc, warnDeopt, PTR, ssoBitI64Hex, LAYOUT } from '../ctx.js'
|
|
30
30
|
import { i64Hex, encodePtrHi, STR_HCACHE_BIT, typedElemAux } from '../../layout.js'
|
|
@@ -111,6 +111,13 @@ const tryI32Arith = (wasmOp, astOp, a, b, va, vb) => {
|
|
|
111
111
|
// own emit (math.js / unary `-`), so they reach canonNum already canonical.
|
|
112
112
|
const NAN_MINTING = new Set(['f64.div', 'f64.add', 'f64.sub', 'f64.mul'])
|
|
113
113
|
|
|
114
|
+
// Sign+exponent mask isolating "negative NaN or -Infinity" — used only after an
|
|
115
|
+
// f64.eq(v,v) self-check has already failed (so -Infinity is excluded, leaving
|
|
116
|
+
// only negative NaN). Pointers/atoms are always emitted sign-clear (nanPrefixMaskHex,
|
|
117
|
+
// layout.js), so a sign-bit-set NaN can only be a genuine float NaN. Mirrors
|
|
118
|
+
// $__typeof's dynamic dispatch (module/core.js) bit-for-bit.
|
|
119
|
+
const NEG_NAN_MASK = 0xFFF0000000000000n
|
|
120
|
+
|
|
114
121
|
const canonNum = (node) => {
|
|
115
122
|
// Fold a possibly-non-canonical NaN to the canonical number-NaN before it reaches a
|
|
116
123
|
// bit-comparing consumer (__is_truthy / untyped === / typeof), which match the canonical
|
|
@@ -329,7 +336,19 @@ export function emitTypeofCmp(a, b, cmpOp) {
|
|
|
329
336
|
if (code === TYPEOF.number) {
|
|
330
337
|
// typeof "number": v===v rejects NaN-box pointers; BOOL carrier is 0/1 → still typeof "boolean".
|
|
331
338
|
if (resolveValType(typeofExpr, valTypeOf, lookupValType) === VAL.BOOL) return typed(['i32.const', eq ? 0 : 1], 'i32')
|
|
332
|
-
|
|
339
|
+
// v===v alone is WRONG for the one payload that legitimately means "the number
|
|
340
|
+
// NaN": the canonical box prefix (tag=ATOM aux=0) that $__typeof (module/core.js)
|
|
341
|
+
// also carves out, plus any sign-bit-set NaN (pointers are always emitted
|
|
342
|
+
// sign-clear, so a negative NaN — e.g. x86's uncanonicalized 0/0 — can only be a
|
|
343
|
+
// real float NaN). Must mirror $__typeof's dynamic dispatch exactly, or
|
|
344
|
+
// `typeof NaN === 'number'` folds to false here while the general path says true.
|
|
345
|
+
const again = ['local.get', `$${t}`]
|
|
346
|
+
const notNan = ['f64.eq', ['local.tee', `$${t}`, va], again]
|
|
347
|
+
const bits = ['i64.reinterpret_f64', again]
|
|
348
|
+
const numberNan = ['i32.or',
|
|
349
|
+
['i64.eq', bits, ['i64.const', i64Hex(LAYOUT.NAN_PREFIX_BITS)]],
|
|
350
|
+
['i64.eq', ['i64.and', bits, ['i64.const', i64Hex(NEG_NAN_MASK)]], ['i64.const', i64Hex(NEG_NAN_MASK)]]]
|
|
351
|
+
return wrap(['i32.or', notNan, numberNan])
|
|
333
352
|
}
|
|
334
353
|
if (code === TYPEOF.string) return isPtrKind(PTR.STRING)
|
|
335
354
|
if (code === TYPEOF.undefined) return wrap(isNullish(va))
|
|
@@ -1185,6 +1204,16 @@ export function emitDecl(...inits) {
|
|
|
1185
1204
|
const i = inits[ii]
|
|
1186
1205
|
if (typeof i === 'string') {
|
|
1187
1206
|
const undef = undefExpr()
|
|
1207
|
+
// An uninitialized `let x` holds `undefined` until its first assignment —
|
|
1208
|
+
// a read may see the sentinel, so arithmetic on it must coerce (same flag
|
|
1209
|
+
// as explicit nullish inits below) UNLESS the first reference in
|
|
1210
|
+
// evaluation order is an UNCONDITIONAL write (`let ixSq; while ((ixSq =
|
|
1211
|
+
// …) …)` — the fractal-kernel shape): definitely-assigned-before-read
|
|
1212
|
+
// needs no coercion, and the per-read canon would break the SIMD
|
|
1213
|
+
// recognizers' body shapes. i32-narrowed locals are exempt either way:
|
|
1214
|
+
// the narrowing proof is assigned-before-read, and they zero-init.
|
|
1215
|
+
if (ctx.func.locals.get(i) !== 'i32' && firstRefKind(ctx.func.body, i) !== 'write')
|
|
1216
|
+
ctx.func.maybeNullish?.add(i)
|
|
1188
1217
|
if (ctx.func.boxed.has(i)) {
|
|
1189
1218
|
const cell = ctx.func.boxed.get(i)
|
|
1190
1219
|
ctx.func.locals.set(cell, 'i32')
|
|
@@ -3076,7 +3105,13 @@ function compoundAssign(name, val, f64op, i32op) {
|
|
|
3076
3105
|
}
|
|
3077
3106
|
if (i32op && va.type === 'i32' && vbi.type === 'i32')
|
|
3078
3107
|
return writeVar(name, i32op(va, vbi), void_)
|
|
3079
|
-
|
|
3108
|
+
// A checked typed read coerces like a '+' operand: toNumF64's checkedNumRead
|
|
3109
|
+
// seam folds the UNDEF miss arm to canonical NaN. A bare asF64 carries the
|
|
3110
|
+
// sentinel payload through f64 arithmetic to the boundary (decoded back as
|
|
3111
|
+
// `undefined`; JS: NaN) — `s += a[i]` is the accumulator shape the binary
|
|
3112
|
+
// '+' emitter never sees.
|
|
3113
|
+
const vbn = vb.checkedNumRead ? toNumF64(val, vb) : vb
|
|
3114
|
+
return writeVar(name, f64op(asF64(va), asF64(vbn)), void_)
|
|
3080
3115
|
}
|
|
3081
3116
|
|
|
3082
3117
|
// Ring 0.3 (re-landed after the dispatch rework dropped the uncommitted original):
|
|
@@ -3255,7 +3290,13 @@ export const emitter = {
|
|
|
3255
3290
|
// to a local first" — pinned in test/parser-bugs.js rather than chased further into
|
|
3256
3291
|
// the kernel's own call/branch codegen. See .work/todo.md (groundtruth archive).
|
|
3257
3292
|
const emitted = emit(expr)
|
|
3258
|
-
|
|
3293
|
+
// Closure-convention bodies return into a boxed-value position (the ftN f64
|
|
3294
|
+
// slot): a BOOL value must cross as its true/false atom — the result-side
|
|
3295
|
+
// mirror of closure.call's carrierF64 args. Raw funcs keep the plain 0/1
|
|
3296
|
+
// (their direct callers read valResult; boundary/trampoline rebox).
|
|
3297
|
+
const ir = pk != null ? asPtrOffset(emitted, pk)
|
|
3298
|
+
: (ctx.func.boxedResult && rt === 'f64') ? carrierF64(expr, emitted)
|
|
3299
|
+
: asParamType(emitted, rt)
|
|
3259
3300
|
const ty = pk != null ? 'i32' : rt
|
|
3260
3301
|
const tcoed = tcoTailRewrite(ir, ty)
|
|
3261
3302
|
if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
|
|
@@ -4179,6 +4220,12 @@ export const emitter = {
|
|
|
4179
4220
|
}
|
|
4180
4221
|
continue
|
|
4181
4222
|
}
|
|
4223
|
+
// maxIv = the TRUE max iv value at PRE-increment access sites:
|
|
4224
|
+
// bound−1 (strict) / bound (inclusive). A body-advanced iv (bump>0)
|
|
4225
|
+
// exceeds this only AFTER its write — those accesses carry cand.post
|
|
4226
|
+
// and their group widens by a·bump in the extent constants below.
|
|
4227
|
+
// (The old unconditional widening made `maxIv < len` fail exactly
|
|
4228
|
+
// when len == bound — every symmetric half-spectrum loop.)
|
|
4182
4229
|
const maxIv = tempI64('tvq')
|
|
4183
4230
|
if (vs.bKind === 'f64') {
|
|
4184
4231
|
const bF = temp('tvf')
|
|
@@ -4186,10 +4233,10 @@ export const emitter = {
|
|
|
4186
4233
|
conjs.push(['f64.le', ['f64.abs', ['local.get', `$${bF}`]], ['f64.const', 2147483648]])
|
|
4187
4234
|
result.push(['local.set', `$${maxIv}`,
|
|
4188
4235
|
['i64.trunc_sat_f64_s', [vs.incl ? 'f64.floor' : 'f64.ceil', ['local.get', `$${bF}`]]]])
|
|
4189
|
-
if (vs.
|
|
4190
|
-
['i64.add', ['local.get', `$${maxIv}`], i64c(
|
|
4236
|
+
if (!vs.incl) result.push(['local.set', `$${maxIv}`,
|
|
4237
|
+
['i64.add', ['local.get', `$${maxIv}`], i64c(-1)]])
|
|
4191
4238
|
} else {
|
|
4192
|
-
const adj = vs.
|
|
4239
|
+
const adj = vs.incl ? 0 : -1
|
|
4193
4240
|
result.push(['local.set', `$${maxIv}`,
|
|
4194
4241
|
adj ? ['i64.add', ext(asI32(emit(vs.bound))), i64c(adj)] : ext(asI32(emit(vs.bound)))])
|
|
4195
4242
|
}
|
|
@@ -4221,17 +4268,27 @@ export const emitter = {
|
|
|
4221
4268
|
}
|
|
4222
4269
|
const gk = c.recv + '\x00' + c.a + '\x00' + c.slots.map(t => t.k + '*' + slotKey(t.e)).join('+')
|
|
4223
4270
|
const g = groups.get(gk)
|
|
4224
|
-
if (!g) groups.set(gk, { recv: c.recv, a: c.a, slots: c.slots, maxC: c.bConst, minC: c.bConst })
|
|
4225
|
-
else { g.maxC = Math.max(g.maxC, c.bConst); g.minC = Math.min(g.minC, c.bConst) }
|
|
4271
|
+
if (!g) groups.set(gk, { recv: c.recv, a: c.a, slots: c.slots, maxC: c.bConst, minC: c.bConst, anyPost: !!c.post })
|
|
4272
|
+
else { g.maxC = Math.max(g.maxC, c.bConst); g.minC = Math.min(g.minC, c.bConst); if (c.post) g.anyPost = true }
|
|
4226
4273
|
}
|
|
4227
4274
|
for (const g of groups.values()) {
|
|
4228
|
-
|
|
4229
|
-
|
|
4275
|
+
// extremes follow the SIGN of a: a·iv is maximal at maxIv for a ≥ 0
|
|
4276
|
+
// but at ENTRY for a < 0 (mirror index `N−k` of symmetric fills),
|
|
4277
|
+
// and minimal at the other end. post-increment groups see iv up to
|
|
4278
|
+
// maxIv+bump — widen through the extent CONSTANT (a·bump).
|
|
4279
|
+
const postW = g.anyPost ? g.a * vs.bump : 0
|
|
4280
|
+
const hiC = g.maxC + (g.a >= 0 ? postW : 0)
|
|
4281
|
+
const loC = g.minC + (g.a < 0 ? postW : 0)
|
|
4282
|
+
const entryIR = () => vs.startC != null ? i64c(vs.startC) : slotI64(vs.iv, vs.ivKind)
|
|
4283
|
+
let hi = slotSum(['i64.mul', i64c(g.a), g.a >= 0 ? ['local.get', `$${maxIv}`] : entryIR()], g.slots)
|
|
4284
|
+
if (hiC) hi = ['i64.add', hi, i64c(hiC)]
|
|
4230
4285
|
conjs.push(['i64.lt_s', hi, len64Of(g.recv)])
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4286
|
+
// a ≥ 0 with a STATIC start: lo = a·startC+minC was validated
|
|
4287
|
+
// non-negative at candidate time (slotless), nothing to emit.
|
|
4288
|
+
if (g.a >= 0 && vs.startC != null && !g.slots.length) continue
|
|
4289
|
+
let lo = slotSum(g.a >= 0 && vs.startC != null ? i64c(g.a * vs.startC)
|
|
4290
|
+
: ['i64.mul', i64c(g.a), g.a >= 0 ? slotI64(vs.iv, vs.ivKind) : ['local.get', `$${maxIv}`]], g.slots, true)
|
|
4291
|
+
if (loC) lo = ['i64.add', lo, i64c(loC)]
|
|
4235
4292
|
conjs.push(['i64.ge_s', lo, i64c(0)])
|
|
4236
4293
|
}
|
|
4237
4294
|
// induction cursors (`k += step` in a comma step): value at iteration t is
|
|
@@ -4630,13 +4687,20 @@ export function emit(node, expect) {
|
|
|
4630
4687
|
// number, silently losing the pointer (a Map came back as e.g. 480360.0,
|
|
4631
4688
|
// so a caller's `for…of`/`.size` saw a number and read nothing).
|
|
4632
4689
|
const ptrResult = func?.sig.ptrKind != null
|
|
4690
|
+
// A BOOL-result func carries 0/1 in its raw ABI; the closure ABI is a
|
|
4691
|
+
// boxed-value position, so rebox to the true/false ATOM — the exact
|
|
4692
|
+
// mirror of the boundary wrapper (index.js resultBool). Without it a
|
|
4693
|
+
// field-held function's `=== true` / typeof observed a plain number.
|
|
4694
|
+
const boolResult = !ptrResult && func?.valResult === VAL.BOOL
|
|
4633
4695
|
const wrapped = ptrResult
|
|
4634
4696
|
? `(call $__mkptr (i32.const ${valKindToPtr(func.sig.ptrKind)}) (i32.const ${func.sig.ptrAux ?? 0}) ${callExpr})`
|
|
4635
|
-
:
|
|
4636
|
-
? (
|
|
4637
|
-
: resType === '
|
|
4638
|
-
? `(f64.
|
|
4639
|
-
:
|
|
4697
|
+
: boolResult
|
|
4698
|
+
? `(select (f64.const nan:${TRUE_NAN}) (f64.const nan:${FALSE_NAN}) ${resType === 'i32' ? `(i32.ne ${callExpr} (i32.const 0))` : `(f64.ne ${callExpr} (f64.const 0))`})`
|
|
4699
|
+
: resType === 'i32'
|
|
4700
|
+
? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
|
|
4701
|
+
: resType === 'i64'
|
|
4702
|
+
? `(f64.reinterpret_i64 ${callExpr})`
|
|
4703
|
+
: callExpr
|
|
4640
4704
|
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${restLocals}${restPrelude}${wrapped})`
|
|
4641
4705
|
inc(trampolineName, ...(ptrResult ? ['__mkptr'] : []), ...(restIdx >= 0 ? ['__alloc_hdr', '__mkptr'] : []))
|
|
4642
4706
|
}
|
package/src/compile/index.js
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
valKindToPtr, findBodyStart, tcoTailRewrite,
|
|
68
68
|
boolBoxIR,
|
|
69
69
|
I32_MIN, I32_MAX, dollar,
|
|
70
|
+
carrierF64,
|
|
70
71
|
} from '../ir.js'
|
|
71
72
|
import plan from './plan/index.js'
|
|
72
73
|
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
@@ -395,11 +396,13 @@ function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
|
395
396
|
ctx.func.uniq = uniq
|
|
396
397
|
ctx.func.current = sig
|
|
397
398
|
ctx.func.body = body
|
|
399
|
+
ctx.func.boxedResult = false // closure-convention bodies set true after enterFunc (ftN boxed result)
|
|
398
400
|
ctx.func.directClosures = directClosures
|
|
399
401
|
ctx.func.localProps = null
|
|
400
402
|
ctx.func.charDecomp = null
|
|
401
403
|
ctx.func.charDecompGlobals = false // only emitFunc's named path drains — it re-arms
|
|
402
404
|
ctx.func.probeHoist = null
|
|
405
|
+
ctx.func.lenHoist = null
|
|
403
406
|
if (ctx.transform.optimize) {
|
|
404
407
|
scanAndTagNonEscapingClosures(body)
|
|
405
408
|
}
|
|
@@ -484,6 +487,13 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
484
487
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
485
488
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
486
489
|
updateRep(pname, { val: VAL.TYPED })
|
|
490
|
+
// Unanimous static length from the call sites (validateTypedLenParams:
|
|
491
|
+
// module-local callee, never-written param, settled ctor) — the body's
|
|
492
|
+
// reads gain the static-length proof family, `.length` folds literal.
|
|
493
|
+
if (r.typedLen != null) {
|
|
494
|
+
if (!ctx.types.typedLen) ctx.types.typedLen = new Map()
|
|
495
|
+
if (!ctx.types.typedLen.has(pname)) ctx.types.typedLen.set(pname, r.typedLen)
|
|
496
|
+
}
|
|
487
497
|
}
|
|
488
498
|
if (r.val && !reassigned && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
489
499
|
if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
|
|
@@ -1142,6 +1152,10 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1142
1152
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
1143
1153
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
1144
1154
|
if (!ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
|
|
1155
|
+
if (r.typedLen != null) {
|
|
1156
|
+
if (!ctx.types.typedLen) ctx.types.typedLen = new Map()
|
|
1157
|
+
if (!ctx.types.typedLen.has(pname)) ctx.types.typedLen.set(pname, r.typedLen)
|
|
1158
|
+
}
|
|
1145
1159
|
}
|
|
1146
1160
|
if (r.schemaId != null && !reassigned && !exported && !ctx.schema.vars.has(pname)) {
|
|
1147
1161
|
ctx.schema.vars.set(pname, r.schemaId)
|
|
@@ -1242,6 +1256,10 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1242
1256
|
// answer is loop-invariant for a stable-global receiver — resolve it once.
|
|
1243
1257
|
// Mirrors sidecarOverride's arm: primitives (real numbers, strings) can
|
|
1244
1258
|
// never carry an own override, so only NaN-boxed non-STRING receivers probe.
|
|
1259
|
+
// Function-invariant typed lens (module/typedarray.js leanLen): a stable
|
|
1260
|
+
// PARAM receiver's element count, shared by every checked read/write guard.
|
|
1261
|
+
if (ctx.func.lenHoist) for (const h of ctx.func.lenHoist.values())
|
|
1262
|
+
inits.push(['local.set', `$${h.t}`, h.init])
|
|
1245
1263
|
if (ctx.func.probeHoist) for (const ph of ctx.func.probeHoist.values()) {
|
|
1246
1264
|
const g = () => ['i64.reinterpret_f64', ph.recvIR()]
|
|
1247
1265
|
inits.push(
|
|
@@ -1514,6 +1532,9 @@ function emitClosureBody(cb) {
|
|
|
1514
1532
|
uniq: Math.max(ctx.func.uniq, 100),
|
|
1515
1533
|
directClosures: cb.directClosures ? new Map(cb.directClosures) : null,
|
|
1516
1534
|
})
|
|
1535
|
+
// The ftN f64 result is a boxed-value position: `return <bool>` must cross
|
|
1536
|
+
// as the true/false atom (emit.js 'return' consults this; enterFunc clears it).
|
|
1537
|
+
ctx.func.boxedResult = true
|
|
1517
1538
|
|
|
1518
1539
|
const fn = ['func', `$${cb.name}`]
|
|
1519
1540
|
fn.push(['param', '$__env', 'f64'])
|
|
@@ -1597,7 +1618,7 @@ function emitClosureBody(cb) {
|
|
|
1597
1618
|
}
|
|
1598
1619
|
bodyIR = emitBlockBody(cb.body)
|
|
1599
1620
|
} else {
|
|
1600
|
-
bodyIR = [
|
|
1621
|
+
bodyIR = [carrierF64(cb.body, emit(cb.body))]
|
|
1601
1622
|
}
|
|
1602
1623
|
|
|
1603
1624
|
// Pre-allocate cache locals for env unpacking
|
|
@@ -2225,9 +2246,21 @@ export default function compile(ast, profiler) {
|
|
|
2225
2246
|
['global.set', '$__init_done', ['i32.const', 1]])
|
|
2226
2247
|
sFn.splice(2, 0, ['export', '"_initialize"'])
|
|
2227
2248
|
if (sDirIdx !== -1) sec.start.splice(sDirIdx, 1)
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2249
|
+
// REACTOR self-arming: EVERY exported function (commands and plain
|
|
2250
|
+
// reactor exports alike) ensures init ran. A raw `new WebAssembly.
|
|
2251
|
+
// Instance` embedder that never calls `_initialize` (the reactor
|
|
2252
|
+
// contract wasmtime honors automatically) otherwise runs against
|
|
2253
|
+
// unseeded state — `_clear()` resets the heap from a zero
|
|
2254
|
+
// `__heap_reset` and the next alloc writes out of bounds (the
|
|
2255
|
+
// color-space batch harness). One predicted global check per
|
|
2256
|
+
// boundary call; `__init_done` keeps init exactly-once either way.
|
|
2257
|
+
for (const f of sec.funcs) {
|
|
2258
|
+
if (!Array.isArray(f) || f[0] !== 'func') continue
|
|
2259
|
+
if (!f.some(x => Array.isArray(x) && x[0] === 'export')) continue
|
|
2260
|
+
let at = 2
|
|
2261
|
+
while (at < f.length && Array.isArray(f[at]) &&
|
|
2262
|
+
(f[at][0] === 'export' || f[at][0] === 'param' || f[at][0] === 'result' || f[at][0] === 'local')) at++
|
|
2263
|
+
f.splice(at, 0, ['if', ['i32.eqz', ['global.get', '$__init_done']], ['then', ['call', '$__start']]])
|
|
2231
2264
|
}
|
|
2232
2265
|
}
|
|
2233
2266
|
}
|
|
@@ -36,17 +36,12 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { ctx } from '../ctx.js'
|
|
38
38
|
import { analyzeBody } from './analyze.js'
|
|
39
|
-
import { staticObjectProps } from '../static.js'
|
|
39
|
+
import { staticObjectProps, inplaceKey } from '../static.js'
|
|
40
40
|
import { VAL } from '../reps.js'
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export const inplaceKey = (arrName, lit) => {
|
|
46
|
-
const props = lit.slice(1)
|
|
47
|
-
const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ',' ? props[0].slice(1) : props
|
|
48
|
-
return `${arrName}|${JSON.stringify(flat)}`
|
|
49
|
-
}
|
|
42
|
+
// Content key for a store site: `inplaceKey` (src/static.js) — shared with
|
|
43
|
+
// analyzeStructInline and the emit arms; hosted there to keep the import
|
|
44
|
+
// graph acyclic (the self-host resolver rejects cycles).
|
|
50
45
|
|
|
51
46
|
// env-gated debug — dist/jz.js runs in browsers where `process` doesn't exist
|
|
52
47
|
const DBG = typeof process !== 'undefined' && process.env.JZ_DBG_INPLACE
|
|
@@ -291,6 +286,12 @@ export function scanInplaceStores(programFacts) {
|
|
|
291
286
|
return false
|
|
292
287
|
}
|
|
293
288
|
|
|
289
|
+
// Verdicts are CONTENT-keyed (see inplaceKey): body transforms between this
|
|
290
|
+
// sweep and its consumers (analyzeFuncForEmit's loop rewrites, emit-time
|
|
291
|
+
// inlining) rebuild trees, so node identity does not survive. Sound for the
|
|
292
|
+
// structInline consumer too: a value-position `x = (a[i] = {…})` is never a
|
|
293
|
+
// candidate, but its `[]` target IS walked as a value read (walkVal) and
|
|
294
|
+
// poisons the sid — so every same-content verdict of that sid is false.
|
|
294
295
|
const verdict = new Map() // key → false | { alias, idx } | { alias: null }
|
|
295
296
|
for (const c of candidates) {
|
|
296
297
|
const key = inplaceKey(c.arrName, c.lit)
|
package/src/compile/narrow.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
analyzeBody, findMutations, invalidateLocalsCache, mayBeNullish,
|
|
15
15
|
} from './analyze.js'
|
|
16
16
|
import { staticObjectProps } from '../static.js'
|
|
17
|
-
import { scanBoundedLoops, exprType, typedElemCtor } from '../type.js'
|
|
17
|
+
import { scanBoundedLoops, exprType, typedElemCtor, typedStaticLen } from '../type.js'
|
|
18
18
|
import { typedElemAux, ctorFromElemAux } from '../../layout.js'
|
|
19
19
|
import { observeProgramSlots } from './program-facts.js'
|
|
20
20
|
import { valTypeOf } from '../kind.js'
|
|
@@ -117,6 +117,33 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// typedLen rides the same safety rails as intConst: only module-local direct
|
|
121
|
+
// callees (not exported / value-used / raw), no rest/default positions, and a
|
|
122
|
+
// body that never writes the param. Additionally requires the SETTLED typedCtor
|
|
123
|
+
// — length evidence for a receiver that never proved typed is dead weight the
|
|
124
|
+
// `.length` fold must not trust.
|
|
125
|
+
function validateTypedLenParams(paramReps, valueUsed) {
|
|
126
|
+
for (const func of ctx.func.list) {
|
|
127
|
+
const hostReachable = func.exported || func.raw || valueUsed.has(func.name)
|
|
128
|
+
const reps = paramReps.get(func.name)
|
|
129
|
+
if (!reps) continue
|
|
130
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
131
|
+
let candidates = null
|
|
132
|
+
for (const [k, r] of reps) {
|
|
133
|
+
if (r.typedLen == null) continue
|
|
134
|
+
if (hostReachable || !func.body || k === restIdx || k >= func.sig.params.length ||
|
|
135
|
+
r.typedCtor == null) { r.typedLen = null; continue }
|
|
136
|
+
const pname = func.sig.params[k].name
|
|
137
|
+
if (func.defaults?.[pname] != null) { r.typedLen = null; continue }
|
|
138
|
+
;(candidates ||= new Map()).set(pname, r)
|
|
139
|
+
}
|
|
140
|
+
if (!candidates) continue
|
|
141
|
+
const mutated = new Set()
|
|
142
|
+
findMutations(func.body, new Set(candidates.keys()), mutated)
|
|
143
|
+
for (const name of mutated) candidates.get(name).typedLen = null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
function validateIntConstParams(paramReps, valueUsed) {
|
|
121
148
|
for (const func of ctx.func.list) {
|
|
122
149
|
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
@@ -239,6 +266,28 @@ function buildCallerTypedCtx() {
|
|
|
239
266
|
return callerTypedCtx
|
|
240
267
|
}
|
|
241
268
|
|
|
269
|
+
// Static LENGTHS visible per caller — analyzeBody's typedLens (stable
|
|
270
|
+
// single-def `new T(<n>)` bindings; the tracker poisons on redef) shadowing
|
|
271
|
+
// module globals, same shadowing rule as callerTypedElemsFor.
|
|
272
|
+
function buildCallerTypedLenCtx() {
|
|
273
|
+
const out = new Map()
|
|
274
|
+
const globalTL = ctx.scope.globalTypedLen || new Map()
|
|
275
|
+
out.set(null, globalTL)
|
|
276
|
+
for (const func of ctx.func.list) {
|
|
277
|
+
if (!func.body || func.raw) continue
|
|
278
|
+
const facts = analyzeBody(func.body)
|
|
279
|
+
const local = facts.typedLens || new Map()
|
|
280
|
+
if (!globalTL.size) { out.set(func, local); continue }
|
|
281
|
+
const shadowed = new Set(facts.locals.keys())
|
|
282
|
+
for (const p of func.sig?.params || []) shadowed.add(p.name)
|
|
283
|
+
const merged = new Map()
|
|
284
|
+
for (const [k, v] of globalTL) if (!shadowed.has(k)) merged.set(k, v)
|
|
285
|
+
for (const [k, v] of local) merged.set(k, v)
|
|
286
|
+
out.set(func, merged)
|
|
287
|
+
}
|
|
288
|
+
return out
|
|
289
|
+
}
|
|
290
|
+
|
|
242
291
|
function applyTypedPointerParamAbi(paramReps, valueUsed) {
|
|
243
292
|
for (const func of ctx.func.list) {
|
|
244
293
|
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
@@ -1197,6 +1246,27 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
1197
1246
|
runTypedFixpoint()
|
|
1198
1247
|
runTypedFixpoint()
|
|
1199
1248
|
|
|
1249
|
+
// STATIC LENGTH down call chains: when every call site passes a typed array
|
|
1250
|
+
// of ONE known static length (`new Float64Array(8192)` — directly, via a
|
|
1251
|
+
// stable caller binding, or via the caller's own already-settled param), the
|
|
1252
|
+
// param carries it. Unlocks the whole static-length proof family inside
|
|
1253
|
+
// callees — typedIdxProven's literal/masked/interval classes and the
|
|
1254
|
+
// `.length` literal fold — where the length was born one frame up (heapsort
|
|
1255
|
+
// reading `a` sized in main; a codec writing `out` sized at the call site).
|
|
1256
|
+
// Exact-agreement only (mergeParamFact poisons on mismatch): the fact also
|
|
1257
|
+
// feeds `.length` folds, so an under-approximating min would miscompile.
|
|
1258
|
+
// Transitive via the same soft-fixpoint + hard-validate driver as typedCtor.
|
|
1259
|
+
const callerTypedLenCtx = buildCallerTypedLenCtx()
|
|
1260
|
+
const inferTypedLen = (arg, callerLens, paramFacts) => {
|
|
1261
|
+
if (typeof arg === 'string') return callerLens?.get(arg) ?? paramFacts?.get(arg) ?? null
|
|
1262
|
+
return typedStaticLen(arg)
|
|
1263
|
+
}
|
|
1264
|
+
runArrElemFixpoint('typedLen', inferTypedLen, callerTypedLenCtx)
|
|
1265
|
+
// A length without a settled ctor is unusable evidence (the receiver never
|
|
1266
|
+
// takes the typed read path) and a length on a host-reachable or rebound
|
|
1267
|
+
// param is unsound — same exclusion discipline as intConst.
|
|
1268
|
+
validateTypedLenParams(paramReps, valueUsed)
|
|
1269
|
+
|
|
1200
1270
|
// G: TYPED pointer-ABI narrowing — once .typedCtor agrees on a single
|
|
1201
1271
|
// ctor across all call sites, narrow the param from NaN-boxed f64 to raw
|
|
1202
1272
|
// i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
|
|
@@ -123,6 +123,11 @@ export default function plan(ast, profiler) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
t('narrowSignatures', () => narrowSignatures(programFacts, ast))
|
|
126
|
+
// Boolean/bigint result kinds for funcs the call-site census can't reach —
|
|
127
|
+
// value-used-only functions have no direct sites, but their results still
|
|
128
|
+
// cross boxed positions (closure trampolines, boundary wrappers). Guarded:
|
|
129
|
+
// only ever SETS an unset valResult (see narrowBoolResults doc).
|
|
130
|
+
t('narrowBoolResults', () => narrowBoolResults())
|
|
126
131
|
// Pass 2: narrowSignatures has now settled `programFacts.paramReps`, so a
|
|
127
132
|
// global written from a bare parameter alias (`cur = s`, subscript's parse-
|
|
128
133
|
// state shape) resolves — pass 1 saw only an untyped param and poisoned it.
|
|
@@ -246,7 +246,11 @@ const rewriteScalarTypedArrayUses = (node, arrays) => {
|
|
|
246
246
|
const slot = slotFor(node[1][2], entry)
|
|
247
247
|
if (!slot) return node
|
|
248
248
|
const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
|
|
249
|
-
|
|
249
|
+
// f64 slots (coerce '') still apply the store's ToNumber via unary plus —
|
|
250
|
+
// free for a provably numeric RHS, and it folds a checked read's undefined
|
|
251
|
+
// arm to NaN (raw sentinel bits in the slot would read back as undefined)
|
|
252
|
+
return op === '=' ? ['=', slot, entry.coerce ? coerceAST(entry.coerce, rhs[0]) : ['u+', rhs[0]]]
|
|
253
|
+
: [op, slot, ...rhs]
|
|
250
254
|
}
|
|
251
255
|
return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
|
|
252
256
|
}
|
|
@@ -359,7 +363,12 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
|
359
363
|
}
|
|
360
364
|
if (unsafe.length) {
|
|
361
365
|
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
|
|
362
|
-
|
|
366
|
+
// Only the synced (unsafe-here) arrays go through memory in this
|
|
367
|
+
// statement — every OTHER scalarized array's uses still rewrite to
|
|
368
|
+
// slots. Pushing the statement raw left e.g. `b[0] = a[i]` (unsafe for
|
|
369
|
+
// mirrored `a`, safe for candidate `b`) referencing the dissolved `b`.
|
|
370
|
+
const rest = new Map([...arrays].filter(([nm]) => !unsafe.some(([un]) => un === nm)))
|
|
371
|
+
out.push(rest.size ? rewriteScalarTypedArrayUses(stmts[i], rest) : stmts[i])
|
|
363
372
|
for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
|
|
364
373
|
changed = true
|
|
365
374
|
} else {
|