jz 0.7.0 → 0.8.1
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 +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
package/src/prepare/index.js
CHANGED
|
@@ -169,7 +169,7 @@ const addHostImport = (mod, name, alias, spec) => {
|
|
|
169
169
|
// ABI — record it so references fold to an f64 literal (see prep's identifier resolution) instead
|
|
170
170
|
// of emitting a 0-arg func import that can't be read as a value ("'PI' is not in scope").
|
|
171
171
|
if (typeof spec === 'number') {
|
|
172
|
-
if (!ctx.scope.hostConsts) ctx.scope.hostConsts =
|
|
172
|
+
if (!ctx.scope.hostConsts) ctx.scope.hostConsts = Object.create(null) // name-keyed: prototype-less (see derive)
|
|
173
173
|
ctx.scope.hostConsts[alias] = spec
|
|
174
174
|
return
|
|
175
175
|
}
|
|
@@ -196,6 +196,19 @@ const stripBoolNot = c => {
|
|
|
196
196
|
while (Array.isArray(c) && c[0] === '!' && Array.isArray(c[1]) && c[1][0] === '!') c = c[1][1]
|
|
197
197
|
return c
|
|
198
198
|
}
|
|
199
|
+
// In a statement (value-discarded) position, postfix `x++`/`x--` is lowered to `(++x) − 1` /
|
|
200
|
+
// `(--x) + 1` to recover the old value — but nobody reads it, so drop the ∓1 and keep the bare
|
|
201
|
+
// increment. (`obj.p++` lowers via `obj.p = obj.p + 1`, also wrapped.) Cleaner AST for the loop/
|
|
202
|
+
// recurrence passes; codegen already discarded the ∓1, so this is purely canonicalization.
|
|
203
|
+
const isOne = n => Array.isArray(n) && n[0] == null && n[1] === 1
|
|
204
|
+
const dropDeadPostfix = s => {
|
|
205
|
+
if (Array.isArray(s) && s.length === 3 && isOne(s[2]) && Array.isArray(s[1])) {
|
|
206
|
+
const inner = s[1][0]
|
|
207
|
+
if ((s[0] === '-' && (inner === '++' || inner === '=')) ||
|
|
208
|
+
(s[0] === '+' && (inner === '--' || inner === '='))) return s[1]
|
|
209
|
+
}
|
|
210
|
+
return s
|
|
211
|
+
}
|
|
199
212
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
200
213
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
201
214
|
|
|
@@ -590,9 +603,13 @@ export default function prepare(node) {
|
|
|
590
603
|
|
|
591
604
|
// Named constants → numeric literals. The JZ_NULL/JZ_UNDEF atom sentinels live
|
|
592
605
|
// in ast.js — shared with emit without crossing the prepare↔compile boundary.
|
|
593
|
-
|
|
606
|
+
// Prototype-less (Object.create(null)): a plain `{}` inherits Object.prototype in V8, so
|
|
607
|
+
// `'valueOf' in CONSTANTS` / `CONSTANTS['toString']` would hit an inherited method and
|
|
608
|
+
// mis-resolve a user identifier named like an Object method (jz.js-only — kernel objects
|
|
609
|
+
// are already prototype-less). Same reason on F64_CONSTANTS / GLOBALS / REJECT_IDENTS.
|
|
610
|
+
const CONSTANTS = Object.assign(Object.create(null), { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_UNDEF })
|
|
594
611
|
// NaN/Infinity stay as special f64 values in emit()
|
|
595
|
-
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
612
|
+
const F64_CONSTANTS = Object.assign(Object.create(null), { 'NaN': NaN, 'Infinity': Infinity })
|
|
596
613
|
|
|
597
614
|
/** Resolve variable name through block scope chain (innermost rename wins). */
|
|
598
615
|
function resolveScope(name) {
|
|
@@ -848,7 +865,7 @@ function prep(node) {
|
|
|
848
865
|
// Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
|
|
849
866
|
// program (they do not live in any module). jzify auto-injecting imports would still
|
|
850
867
|
// need a list of these names to know what to emit, so the table lives here either way.
|
|
851
|
-
export const GLOBALS = {
|
|
868
|
+
export const GLOBALS = Object.assign(Object.create(null), {
|
|
852
869
|
Math: 'math',
|
|
853
870
|
Number: 'Number',
|
|
854
871
|
Array: 'Array',
|
|
@@ -878,7 +895,7 @@ export const GLOBALS = {
|
|
|
878
895
|
BigInt: 'BigInt',
|
|
879
896
|
TextEncoder: 'TextEncoder',
|
|
880
897
|
TextDecoder: 'TextDecoder',
|
|
881
|
-
}
|
|
898
|
+
})
|
|
882
899
|
|
|
883
900
|
const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
|
|
884
901
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
@@ -1583,7 +1600,7 @@ const handlers = {
|
|
|
1583
1600
|
if (Array.isArray(specifiers) && specifiers[0] === 'as' && specifiers[1] === '*') {
|
|
1584
1601
|
const alias = specifiers[2]
|
|
1585
1602
|
// Store namespace mapping so '.' handler can resolve X.prop → mangled name
|
|
1586
|
-
if (!ctx.module.namespaces) ctx.module.namespaces =
|
|
1603
|
+
if (!ctx.module.namespaces) ctx.module.namespaces = Object.create(null) // name-keyed: prototype-less (see derive)
|
|
1587
1604
|
ctx.module.namespaces[alias] = resolved.exports
|
|
1588
1605
|
return null
|
|
1589
1606
|
}
|
|
@@ -1632,20 +1649,20 @@ const handlers = {
|
|
|
1632
1649
|
'!=='(a, b) { return prepStrictEq('!==', a, b) },
|
|
1633
1650
|
|
|
1634
1651
|
// Statements
|
|
1635
|
-
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
|
|
1652
|
+
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null).map(dropDeadPostfix)],
|
|
1636
1653
|
'let': (...inits) => prepDecl('let', ...inits),
|
|
1637
1654
|
'const': (...inits) => prepDecl('const', ...inits),
|
|
1638
1655
|
|
|
1639
1656
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
1640
1657
|
'if': (cond, then, els) => {
|
|
1641
1658
|
const c = prep(stripBoolNot(cond))
|
|
1642
|
-
pushScope(); const t = prep(then); popScope()
|
|
1643
|
-
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
1659
|
+
pushScope(); const t = dropDeadPostfix(prep(then)); popScope()
|
|
1660
|
+
if (els != null) { pushScope(); const e = dropDeadPostfix(prep(els)); popScope(); return ['if', c, t, e] }
|
|
1644
1661
|
return ['if', c, t]
|
|
1645
1662
|
},
|
|
1646
1663
|
'while': (cond, body) => {
|
|
1647
1664
|
const c = prep(stripBoolNot(cond))
|
|
1648
|
-
pushScope(); const b = prep(body); popScope()
|
|
1665
|
+
pushScope(); const b = dropDeadPostfix(prep(body)); popScope()
|
|
1649
1666
|
return ['while', c, b]
|
|
1650
1667
|
},
|
|
1651
1668
|
// do { body } while (cond) → flag-guarded while: `flag=true; while (flag||cond) { flag=false; body }`.
|
|
@@ -2123,7 +2140,7 @@ const handlers = {
|
|
|
2123
2140
|
}
|
|
2124
2141
|
}
|
|
2125
2142
|
}
|
|
2126
|
-
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
2143
|
+
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? dropDeadPostfix(prep(step)) : null, dropDeadPostfix(prep(body))]
|
|
2127
2144
|
} else if (Array.isArray(head) && head[0] === 'of') {
|
|
2128
2145
|
// for (let x of arr) → hoist arr (if non-trivial) and arr.length once, iterate by index.
|
|
2129
2146
|
// Divergence from JS: mutating arr during iteration won't extend/shorten the loop.
|
|
@@ -2466,7 +2483,7 @@ function prepareModule(specifier, source) {
|
|
|
2466
2483
|
const savedFuncCount = ctx.func.list.length // track new funcs from this module
|
|
2467
2484
|
const savedModulePrefix = ctx.module.currentPrefix
|
|
2468
2485
|
ctx.scope.chain = derive(savedScope) // inherit parent scope
|
|
2469
|
-
ctx.func.exports =
|
|
2486
|
+
ctx.func.exports = Object.create(null) // name-keyed: prototype-less (see derive)
|
|
2470
2487
|
ctx.module.currentPrefix = prefix
|
|
2471
2488
|
|
|
2472
2489
|
try {
|
package/src/reps.js
CHANGED
|
@@ -57,6 +57,9 @@ export const VAL = {
|
|
|
57
57
|
* @property {number} [arrayElemSchema] element object-schema id for arrays.
|
|
58
58
|
* @property {string} [arrayElemValType] element VAL.* kind for arrays.
|
|
59
59
|
* @property {string} [arrayElemElemValType] nested element VAL.* kind (`X[i][j]`) for arrays of arrays.
|
|
60
|
+
* @property {string} [arrayElemTypedCtor] element TypedArray ctor (`new.Float32Array`) for an
|
|
61
|
+
* array whose elements are all typed arrays of one ctor (`Array.from(n,()=>new Float32Array())`),
|
|
62
|
+
* so `arr[i]` is a known typed array and `arr[i][j]` inlines instead of runtime aux-dispatch.
|
|
60
63
|
* @property {string} [carrier] abi carrier id override (e.g. 'jsstring').
|
|
61
64
|
* @property {boolean} [unsigned] i32 carries an unsigned value (`>>>` result).
|
|
62
65
|
* @property {*} [jsonShape] inferred shape for the JSON.stringify fast path.
|
|
@@ -68,7 +71,7 @@ export const VAL = {
|
|
|
68
71
|
*/
|
|
69
72
|
export const REP_FIELDS = new Set([
|
|
70
73
|
'val', 'ptrKind', 'ptrAux', 'schemaId', 'intConst', 'intCertain', 'notString',
|
|
71
|
-
'arrayElemSchema', 'arrayElemValType', 'arrayElemElemValType', 'carrier', 'unsigned', 'jsonShape',
|
|
74
|
+
'arrayElemSchema', 'arrayElemValType', 'arrayElemElemValType', 'arrayElemTypedCtor', 'carrier', 'unsigned', 'jsonShape',
|
|
72
75
|
'typedCtor', 'wasm', 'nullable', 'neverGrown',
|
|
73
76
|
])
|
|
74
77
|
|
package/src/type.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { isI32, isReassigned } from './ast.js'
|
|
14
14
|
import { ctx } from './ctx.js'
|
|
15
|
+
import { FITS_I32_MAX } from './widen.js'
|
|
15
16
|
import { VAL, lookupValType } from './reps.js'
|
|
16
17
|
import { valTypeOf } from './kind.js'
|
|
18
|
+
import { propValType, CMP_OPS } from './kind-traits.js'
|
|
17
19
|
import { NO_VALUE, staticValue, intLiteralValue } from './static.js'
|
|
18
20
|
import { typedElemAux } from '../layout.js'
|
|
19
21
|
|
|
@@ -362,9 +364,24 @@ export function isTerminator(body) {
|
|
|
362
364
|
return false
|
|
363
365
|
}
|
|
364
366
|
|
|
365
|
-
|
|
367
|
+
// Resolve a name's typed-array element ctor: in-progress local overlay (analyzeBody) →
|
|
368
|
+
// per-func map (post-analyze) → module-global registry. The global fallback matters during
|
|
369
|
+
// analyzeBody/narrow when the per-func map is null, so a read of a *global* typed array
|
|
370
|
+
// (`DX[i]` with `let DX = new Int32Array(...)` at module scope) resolves its element type
|
|
371
|
+
// instead of defaulting to f64. Guard against local shadows / dynamic rewrites (cf. kind.js).
|
|
372
|
+
const typedElemCtorOf = (name, locals) =>
|
|
373
|
+
ctx.func.localTypedElemsOverlay?.get(name) ?? ctx.types.typedElem?.get(name)
|
|
374
|
+
?? (!locals?.has?.(name) && !ctx.types?.dynWriteVars?.has?.(name)
|
|
375
|
+
? ctx.scope?.globalTypedElem?.get(name) : undefined)
|
|
376
|
+
|
|
377
|
+
// An expression whose i32 value carries the unsigned [0, 2^32) magnitude (not a signed i32):
|
|
378
|
+
// `>>>`, an unsigned-result call, or a Uint32Array read (aux 5 — the only typed array whose
|
|
379
|
+
// element can exceed signed-i32 range). The +/-/*/% rules widen these to f64 so `U[i] + 1`
|
|
380
|
+
// near 2^32 doesn't wrap; bitwise/store consumers are ToInt32-exact and keep the i32 bits.
|
|
381
|
+
const isUnsignedI32Expr = (e, locals) => Array.isArray(e) && (
|
|
366
382
|
e[0] === '>>>' ||
|
|
367
|
-
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
383
|
+
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true) ||
|
|
384
|
+
(e[0] === '[]' && typeof e[1] === 'string' && typedElemAux(typedElemCtorOf(e[1], locals)) === 5)
|
|
368
385
|
)
|
|
369
386
|
|
|
370
387
|
/**
|
|
@@ -380,12 +397,16 @@ export function exprType(expr, locals) {
|
|
|
380
397
|
if (locals?.has?.(expr)) return locals.get(expr)
|
|
381
398
|
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
382
399
|
if (paramType) return paramType
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
|
|
400
|
+
// A module-level INTEGER const (`const N = 16384`) is an integer compile-time
|
|
401
|
+
// constant — type it i32 when it fits, regardless of the global's f64 (NaN-box)
|
|
402
|
+
// storage. Otherwise a counter bounded by it (`for (i=0; i<N; i++)`) widens to
|
|
403
|
+
// f64 and `x % N` / `x & N` / `x / N` take the f64 round-trip instead of the
|
|
404
|
+
// native integer path (i32.rem_s / i32.and / i32.shr). Mirrors a literal int.
|
|
405
|
+
const ci = ctx.scope?.constInts?.get?.(expr)
|
|
406
|
+
if (ci != null && isI32(ci)) return 'i32'
|
|
407
|
+
// Module-level numeric consts emitted as wasm globals with a known wasm type.
|
|
408
|
+
// Only propagate primitive numeric kinds — i64 globals are reserved for the
|
|
409
|
+
// NaN-box carrier ABI and shouldn't influence local typing.
|
|
389
410
|
const gt = ctx.scope?.globalTypes?.get?.(expr)
|
|
390
411
|
if (gt === 'i32' || gt === 'f64') return gt
|
|
391
412
|
return 'f64'
|
|
@@ -411,7 +432,10 @@ export function exprType(expr, locals) {
|
|
|
411
432
|
// localTypedElemsOverlay during analyzeBody; post-analyze passes read ctx.types.typedElem.
|
|
412
433
|
if (op === '[]') {
|
|
413
434
|
if (typeof args[0] === 'string') {
|
|
414
|
-
|
|
435
|
+
// Resolve the element ctor across local overlay → per-func map → module-global registry
|
|
436
|
+
// (the global fallback is why `DX[i]` on a module-scope Int32Array types as i32 instead of
|
|
437
|
+
// f64-round-tripping integer accumulation like `ax = ax + DX[i]`). See typedElemCtorOf.
|
|
438
|
+
const ctor = typedElemCtorOf(args[0], locals)
|
|
415
439
|
if (ctor) {
|
|
416
440
|
const aux = typedElemAux(ctor)
|
|
417
441
|
if (aux != null && (aux & 7) <= 5) return 'i32'
|
|
@@ -419,30 +443,20 @@ export function exprType(expr, locals) {
|
|
|
419
443
|
}
|
|
420
444
|
return 'f64'
|
|
421
445
|
}
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
446
|
+
// A sized built-in property on a statically-known receiver (`.length` on
|
|
447
|
+
// STRING/ARRAY/TYPED, `.size` on SET/MAP, `.byteLength`/`.byteOffset` on
|
|
448
|
+
// TYPED/BUFFER) returns i32 directly (`__len`/`__str_byteLen` return i32).
|
|
449
|
+
// Keeping it i32 lets analyzeBody keep the counter local i32, eliminating the
|
|
450
|
+
// per-iteration `f64.convert_i32_s` widen and matching `arr[i]`/`i*k` truncs.
|
|
451
|
+
// The membership lives in one place — `propValType` (src/kind-traits.js).
|
|
427
452
|
if (op === '.') {
|
|
428
|
-
if (args[
|
|
429
|
-
const vt = lookupValType(args[0])
|
|
430
|
-
if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
|
|
431
|
-
}
|
|
432
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
433
|
-
const vt = lookupValType(args[0])
|
|
434
|
-
if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
|
|
435
|
-
}
|
|
436
|
-
if (args[1] === 'byteLength' && typeof args[0] === 'string') {
|
|
437
|
-
const vt = lookupValType(args[0])
|
|
438
|
-
if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
|
|
439
|
-
}
|
|
453
|
+
if (typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER) return 'i32'
|
|
440
454
|
return 'f64'
|
|
441
455
|
}
|
|
442
456
|
// Comparisons, logical-not, and unsigned shift always yield an i32 — a boolean,
|
|
443
457
|
// or a ToUint32 result. True even on BigInt operands (`>>>` throws on bigint, so
|
|
444
458
|
// it never reaches here with one).
|
|
445
|
-
if (
|
|
459
|
+
if (CMP_OPS.has(op) || op === '>>>') return 'i32'
|
|
446
460
|
// Bitwise & signed-shift: i32 on numbers, but f64 when operands are BigInt — the
|
|
447
461
|
// result is a bigint carried in the i64-bits-as-f64 ABI, not a 32-bit int.
|
|
448
462
|
if (['&', '|', '^', '~', '<<', '>>'].includes(op))
|
|
@@ -455,7 +469,7 @@ export function exprType(expr, locals) {
|
|
|
455
469
|
// A uint32 operand ([0, 2^32)) makes the result exceed signed i32 range, so
|
|
456
470
|
// emit widens to f64 (see emit.js `+`/`-`). exprType must agree — else
|
|
457
471
|
// narrowing the result back to i32 would trunc_sat-saturate the f64 to INT32_MAX.
|
|
458
|
-
if (isUnsignedI32Expr(args[0]) || (args[1] != null && isUnsignedI32Expr(args[1]))) return 'f64'
|
|
472
|
+
if (isUnsignedI32Expr(args[0], locals) || (args[1] != null && isUnsignedI32Expr(args[1], locals))) return 'f64'
|
|
459
473
|
return 'i32'
|
|
460
474
|
}
|
|
461
475
|
// `%` is i32 only when emit takes the i32.rem_s path: both operands i32, neither
|
|
@@ -465,7 +479,7 @@ export function exprType(expr, locals) {
|
|
|
465
479
|
if (op === '%') {
|
|
466
480
|
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
467
481
|
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
468
|
-
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
482
|
+
if (isUnsignedI32Expr(args[0], locals) || isUnsignedI32Expr(args[1], locals)) return 'f64'
|
|
469
483
|
const dv = staticValue(args[1])
|
|
470
484
|
return (dv !== NO_VALUE && typeof dv === 'number' && dv !== 0 && Number.isInteger(dv)) ? 'i32' : 'f64'
|
|
471
485
|
}
|
|
@@ -478,11 +492,15 @@ export function exprType(expr, locals) {
|
|
|
478
492
|
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
479
493
|
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
480
494
|
// uint32 operand: product can exceed i32; emit widens to f64 (see emit.js `*`).
|
|
481
|
-
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
495
|
+
if (isUnsignedI32Expr(args[0], locals) || isUnsignedI32Expr(args[1], locals)) return 'f64'
|
|
482
496
|
if (sv !== NO_VALUE && typeof sv === 'number') return isI32(sv) ? 'i32' : 'f64'
|
|
497
|
+
// Shared FITS_I32_MAX threshold (widen.js) keeps this in lock-step with emit's
|
|
498
|
+
// `mulFitsI32`. exprType only proves the static-literal case — a strict SUBSET of
|
|
499
|
+
// emit's i32 verdict (emit also admits masked-bound operands), which is the safe
|
|
500
|
+
// direction: never claim i32 where emit might widen to f64.
|
|
483
501
|
const small = e => {
|
|
484
502
|
const v = staticValue(e)
|
|
485
|
-
return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <=
|
|
503
|
+
return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <= FITS_I32_MAX
|
|
486
504
|
}
|
|
487
505
|
return small(args[0]) || small(args[1]) ? 'i32' : 'f64'
|
|
488
506
|
}
|
|
@@ -531,7 +549,6 @@ export function exprType(expr, locals) {
|
|
|
531
549
|
// === Integer-certainty fixpoint (shared by analyzeIntCertain + program-facts) ===
|
|
532
550
|
|
|
533
551
|
const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
|
|
534
|
-
const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
|
|
535
552
|
const INT_CLOSED_OPS = new Set(['+', '-', '*']) // `%` handled separately — int only for nonzero divisor
|
|
536
553
|
const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
|
|
537
554
|
|
|
@@ -552,7 +569,7 @@ function collectIntDefs(body) {
|
|
|
552
569
|
} else if (op === '=' && typeof args[0] === 'string') {
|
|
553
570
|
pushDef(args[0], args[1])
|
|
554
571
|
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
555
|
-
!
|
|
572
|
+
!CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
|
|
556
573
|
pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
|
|
557
574
|
} else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
558
575
|
pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
|
|
@@ -578,18 +595,9 @@ function makeIsIntExpr(intCertain) {
|
|
|
578
595
|
if (typeof v === 'boolean') return true
|
|
579
596
|
return false
|
|
580
597
|
}
|
|
581
|
-
if (INT_BIT_OPS.has(op) ||
|
|
582
|
-
if (op === '.')
|
|
583
|
-
|
|
584
|
-
const vt = lookupValType(args[0])
|
|
585
|
-
return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
|
|
586
|
-
}
|
|
587
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
588
|
-
const vt = lookupValType(args[0])
|
|
589
|
-
return vt === VAL.SET || vt === VAL.MAP
|
|
590
|
-
}
|
|
591
|
-
return false
|
|
592
|
-
}
|
|
598
|
+
if (INT_BIT_OPS.has(op) || CMP_OPS.has(op)) return true
|
|
599
|
+
if (op === '.')
|
|
600
|
+
return typeof args[0] === 'string' && propValType(args[1], lookupValType(args[0])) === VAL.NUMBER
|
|
593
601
|
if (INT_CLOSED_OPS.has(op)) {
|
|
594
602
|
const a = isIntExpr(args[0])
|
|
595
603
|
const b = args[1] != null ? isIntExpr(args[1]) : a
|
package/src/wat/assemble.js
CHANGED
|
@@ -39,6 +39,7 @@ import { VAL } from '../reps.js'
|
|
|
39
39
|
import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from '../optimize/index.js'
|
|
40
40
|
import { emit, emitVoid } from '../compile/emit.js'
|
|
41
41
|
import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from '../ir.js'
|
|
42
|
+
import { installHelperCounters, instrumentHelperCounter } from '../helper-counters.js'
|
|
42
43
|
|
|
43
44
|
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
44
45
|
const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
|
|
@@ -221,6 +222,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
221
222
|
// table holding only empty schemas is pure dead weight there. __json_obj has
|
|
222
223
|
// no such guard — it must read the table whenever stringify is in play.
|
|
223
224
|
const tblConsumed = hasStringify ||
|
|
225
|
+
ctx.core.includes.has('__obj_clone') ||
|
|
224
226
|
ctx.core.includes.has('__dyn_get') ||
|
|
225
227
|
ctx.core.includes.has('__dyn_get_t') ||
|
|
226
228
|
ctx.core.includes.has('__dyn_get_t_h') ||
|
|
@@ -230,7 +232,13 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
230
232
|
ctx.core.includes.has('__dyn_get_any_t_h') ||
|
|
231
233
|
ctx.core.includes.has('__dyn_get_expr') ||
|
|
232
234
|
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
233
|
-
ctx.core.includes.has('__dyn_get_or')
|
|
235
|
+
ctx.core.includes.has('__dyn_get_or') ||
|
|
236
|
+
// A string runtime-key WRITE `o[k]=v` whose `k` matches a schema field must
|
|
237
|
+
// mirror the value into the fixed schema slot (buildObjectSchemaSetArm), or a
|
|
238
|
+
// later static `o.x` read returns the stale slot. That mirror is gated on
|
|
239
|
+
// `$__schema_tbl != 0`, so a write-only module (no `__dyn_get*`) must still
|
|
240
|
+
// build the table. (needsSchemaTbl below skips it when every schema is empty.)
|
|
241
|
+
ctx.core.includes.has('__dyn_set')
|
|
234
242
|
const needsSchemaTbl = (ctx.schema.list.length && tblConsumed &&
|
|
235
243
|
(hasStringify || ctx.schema.list.some(s => s.length > 0))) ||
|
|
236
244
|
hasJpObj
|
|
@@ -552,6 +560,7 @@ export function appendLateStdlib(moduleArr) {
|
|
|
552
560
|
* Phase: pull stdlib + memory.
|
|
553
561
|
*/
|
|
554
562
|
export function pullStdlib(sec) {
|
|
563
|
+
installHelperCounters()
|
|
555
564
|
resolveIncludes()
|
|
556
565
|
|
|
557
566
|
// Reachability, not inclusion, decides what the output needs. `ctx.core.includes`
|
|
@@ -600,7 +609,26 @@ export function pullStdlib(sec) {
|
|
|
600
609
|
// are pulled in on demand, never eagerly, so they're already minimal and never pruned
|
|
601
610
|
// here (guarding against any reachability blind spot in a dotted-name template).
|
|
602
611
|
for (const n of [...ctx.core.includes]) if (n.startsWith('__') && !reachable.has(n)) ctx.core.includes.delete(n)
|
|
603
|
-
|
|
612
|
+
// Lazy Eisel-Lemire table injection: only when __dec_to_f64 (correctly-rounded
|
|
613
|
+
// decimal→f64, module/number.js) survived pruning — append its trimmed 131-entry
|
|
614
|
+
// (~2KB) power-of-10 table and declare $__el_tbl = that offset. Must run HERE so
|
|
615
|
+
// dataPages (below) accounts for the addition; keeps it out of programs that never
|
|
616
|
+
// parse decimals at runtime.
|
|
617
|
+
if (ctx.core.includes.has('__dec_to_f64') && ctx.runtime.elTable) {
|
|
618
|
+
const elBefore = ctx.runtime.data.length
|
|
619
|
+
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
620
|
+
const elTblOff = ctx.runtime.data.length
|
|
621
|
+
ctx.runtime.data += ctx.runtime.elTable
|
|
622
|
+
declGlobal('__el_tbl', 'i32', elTblOff, { mut: false })
|
|
623
|
+
ctx.runtime.elTable = null // prevent double-injection on re-entry (null-sentinel; jz forbids delete)
|
|
624
|
+
// Reachability here OVER-counts __dec_to_f64: a dead inlined helper's `arr[i] | 0`
|
|
625
|
+
// on an untyped param pulls __to_num → __dec_to_f64, landing the table even when no
|
|
626
|
+
// LIVE code parses decimals. Record the appended span (padding + table, always the
|
|
627
|
+
// data tail — it's the last append) so stripDeadElTable can drop it post-lowering,
|
|
628
|
+
// once reachability is exact. See stripDeadElTable.
|
|
629
|
+
ctx.runtime.elTableLen = ctx.runtime.data.length - elBefore
|
|
630
|
+
}
|
|
631
|
+
if (!needsAlloc) { ctx.scope.globals.delete('__heap'); ctx.scope.globals.delete('__heap_reset') }
|
|
604
632
|
if (needsMemory && ctx.module.modules.core) {
|
|
605
633
|
if (needsAlloc) {
|
|
606
634
|
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) ctx.core.includes.add(fn)
|
|
@@ -608,6 +636,20 @@ export function pullStdlib(sec) {
|
|
|
608
636
|
// etc.) that the initial resolveIncludes did not yet see; re-resolve.
|
|
609
637
|
// No-op when the alloc trio was already present.
|
|
610
638
|
resolveIncludes()
|
|
639
|
+
// Record the post-init heap top into `__heap_reset` so `__clear` rewinds to
|
|
640
|
+
// just above this module's init-time heap state (e.g. the self-host compiler's
|
|
641
|
+
// GLOBALS/atom tables), not into it. Done here — where `__heap` is known to
|
|
642
|
+
// survive — as the last `__start` action before any non-returning timer loop.
|
|
643
|
+
// No `__start` ⇒ no init allocations ⇒ `__heap_reset`'s data-end seed is right.
|
|
644
|
+
if (!ctx.memory.shared && ctx.scope.globals.has('__heap_reset')) {
|
|
645
|
+
const startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
646
|
+
if (startFn) {
|
|
647
|
+
const capture = ['global.set', '$__heap_reset', ['global.get', '$__heap']]
|
|
648
|
+
const tail = startFn[startFn.length - 1]
|
|
649
|
+
if (Array.isArray(tail) && tail[0] === 'call' && tail[1] === '$__timer_loop') startFn.splice(startFn.length - 1, 0, capture)
|
|
650
|
+
else startFn.push(capture)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
611
653
|
}
|
|
612
654
|
// Initial pages must cover the static data segment (it loads at instantiation), not
|
|
613
655
|
// just the default 1 — otherwise a module whose constants exceed 64 KiB emits a data
|
|
@@ -637,7 +679,7 @@ export function pullStdlib(sec) {
|
|
|
637
679
|
}
|
|
638
680
|
}
|
|
639
681
|
for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) err(`internal: stdlib '${n}' was requested but never registered (this is a jz bug — feature pulled in something it can't deliver)`)
|
|
640
|
-
sec.stdlib.push(...[...ctx.core.includes].map(n => parseTemplate(stdlibStr(n))))
|
|
682
|
+
sec.stdlib.push(...[...ctx.core.includes].map(n => instrumentHelperCounter(n, parseTemplate(stdlibStr(n)))))
|
|
641
683
|
}
|
|
642
684
|
|
|
643
685
|
export function syncImports(sec) {
|
|
@@ -706,17 +748,58 @@ export function optimizeModule(sec, profiler) {
|
|
|
706
748
|
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
707
749
|
const heapBase = (dataLen + 7) & ~7
|
|
708
750
|
// Non-shared memory always carries a $__heap global — start it past the
|
|
709
|
-
// static data so the bump allocator never overwrites a literal.
|
|
751
|
+
// static data so the bump allocator never overwrites a literal. `__heap_reset`
|
|
752
|
+
// seeds to the same data end (its runtime value is overwritten by `__start`'s
|
|
753
|
+
// tail capture for modules that init-allocate; this seed serves modules with no
|
|
754
|
+
// `__start`, where the data end IS the correct rewind point). `__clear` reads
|
|
755
|
+
// `$__heap_reset` directly, so no per-function constant patch is needed.
|
|
710
756
|
declGlobal('__heap', 'i32', heapBase, { export: '__heap' })
|
|
757
|
+
if (ctx.scope.globals.has('__heap_reset')) declGlobal('__heap_reset', 'i32', heapBase)
|
|
711
758
|
if (ctx.scope.globals.has('__heap_start')) declGlobal('__heap_start', 'i32', heapBase)
|
|
712
|
-
for (const s of sec.stdlib)
|
|
713
|
-
if (s[0] === 'func' && s[1] === '$__clear')
|
|
714
|
-
for (let i = 2; i < s.length; i++)
|
|
715
|
-
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
716
|
-
s[i][2][1] = `${heapBase}`
|
|
717
759
|
}
|
|
718
760
|
}
|
|
719
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Phase: strip the Eisel-Lemire table when it is dead.
|
|
764
|
+
*
|
|
765
|
+
* pullStdlib injects the ~2 KB power-of-10 table whenever `__dec_to_f64` is *reachable*,
|
|
766
|
+
* but that over-counts: a dead inlined helper's `arr[i] | 0` on an untyped param pulls
|
|
767
|
+
* `__to_num` → `__dec_to_f64`, so the table lands even in a module no live code parses
|
|
768
|
+
* decimals in. watr later treeshakes the dead function + its `$__el_tbl` global, but it
|
|
769
|
+
* does NOT treeshake the data segment — so the orphaned table bloated every module ~2 KB.
|
|
770
|
+
*
|
|
771
|
+
* This runs LAST (after every lowering has emitted its call/ref.func — doing it earlier is
|
|
772
|
+
* unsound: refs like `util.clone` are emitted *after* pullStdlib), so a mark-sweep from the
|
|
773
|
+
* real roots (inline-exported funcs, __start, the closure table, globals/tags/table) gives
|
|
774
|
+
* EXACT liveness. If `__dec_to_f64` is dead, truncate the table from the data tail (it is
|
|
775
|
+
* the last append — see pullStdlib). DATA only: the dead function + global are left for
|
|
776
|
+
* watr, which already removes them. Keeps correctly-rounded decimal parsing wherever it is
|
|
777
|
+
* genuinely live (parseFloat, the self-host compiler's `Number()` on source literals).
|
|
778
|
+
*/
|
|
779
|
+
export function stripDeadElTable(sec) {
|
|
780
|
+
if (!ctx.runtime.elTableLen) return
|
|
781
|
+
const byName = new Map()
|
|
782
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
783
|
+
for (const f of arr || []) if (Array.isArray(f) && f[0] === 'func' && typeof f[1] === 'string') byName.set(f[1], f)
|
|
784
|
+
const live = new Set(), work = []
|
|
785
|
+
const mark = (ref) => { if (typeof ref === 'string' && byName.has(ref) && !live.has(ref)) { live.add(ref); work.push(ref) } }
|
|
786
|
+
const scan = (n) => {
|
|
787
|
+
if (!Array.isArray(n)) return
|
|
788
|
+
if ((n[0] === 'call' || n[0] === 'return_call' || n[0] === 'ref.func') && typeof n[1] === 'string') mark(n[1])
|
|
789
|
+
for (const c of n) scan(c)
|
|
790
|
+
}
|
|
791
|
+
for (const f of sec.funcs) if (f.some(el => Array.isArray(el) && el[0] === 'export')) mark(f[1])
|
|
792
|
+
for (const f of sec.start) scan(f)
|
|
793
|
+
for (const part of [sec.elem, sec.globals, sec.tags, sec.table]) for (const n of part || []) {
|
|
794
|
+
if (!Array.isArray(n)) continue
|
|
795
|
+
for (const c of n) { if (typeof c === 'string' && c[0] === '$') mark(c); else scan(c) }
|
|
796
|
+
}
|
|
797
|
+
while (work.length) scan(byName.get(work.pop()))
|
|
798
|
+
if (live.has('$__dec_to_f64')) return // genuinely parses decimals at runtime — keep it
|
|
799
|
+
ctx.runtime.data = ctx.runtime.data.slice(0, ctx.runtime.data.length - ctx.runtime.elTableLen)
|
|
800
|
+
ctx.runtime.elTableLen = 0
|
|
801
|
+
}
|
|
802
|
+
|
|
720
803
|
/**
|
|
721
804
|
* Phase: strip static-data prefix.
|
|
722
805
|
*/
|
package/src/widen.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric widening thresholds — the SINGLE SOURCE for "when does an i32 arithmetic
|
|
3
|
+
* op stay i32 vs widen to f64", shared by the two phases that must agree:
|
|
4
|
+
* • emit.js DECIDES — emits `i32.mul`/`i32.add` or widens to `f64.mul`/`f64.add`
|
|
5
|
+
* • type.js MIRRORS — exprType predicts the same i32/f64 so locals are typed right
|
|
6
|
+
*
|
|
7
|
+
* SOUNDNESS INVARIANT (one-way, unforgiving): exprType's i32 verdict must be a SUBSET
|
|
8
|
+
* of emit's — exprType may answer i32 only where emit DEFINITELY produces i32. If type
|
|
9
|
+
* says i32 but emit yields f64, the result is `trunc_sat`-narrowed back to i32 → silent
|
|
10
|
+
* miscompile. The two predicates can't share a function (emit reads IR values via
|
|
11
|
+
* isLit/maskBound, type reads AST via staticValue), but they MUST share this threshold,
|
|
12
|
+
* or a future edit to one silently drifts the other out of the safe subset.
|
|
13
|
+
*
|
|
14
|
+
* @module widen
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// JS `*` is an f64 multiply; `i32.mul` agrees only while the exact product stays
|
|
18
|
+
// f64-exact (|product| ≤ 2^53). Against the full i32 range (2^31) of one operand, the
|
|
19
|
+
// other must be bounded |v| ≤ 2^22 for the product to hold within 2^53 — so a literal
|
|
20
|
+
// or provably-masked operand of that magnitude keeps the multiply on `i32.mul`.
|
|
21
|
+
export const FITS_I32_MAX = 0x400000 // 2^22
|