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
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
some, T, stmtList, refsName, REFS_IN_EXPR, ASSIGN_OPS, isReassigned, hasControlTransfer,
|
|
29
29
|
} from '../../ast.js'
|
|
30
30
|
import {
|
|
31
|
-
intLiteralValue, constIntExpr, staticObjectProps, staticPropertyKey,
|
|
31
|
+
intLiteralValue, nonNegIntLiteral, constIntExpr, staticObjectProps, staticPropertyKey,
|
|
32
32
|
} from '../../static.js'
|
|
33
33
|
import {
|
|
34
34
|
smallConstForTripCount, containsDeclOf, cloneWithSubst,
|
|
@@ -37,7 +37,7 @@ import { VAL } from '../../reps.js'
|
|
|
37
37
|
import { includeModule } from '../../autoload.js'
|
|
38
38
|
import { analyzeBody, invalidateLocalsCache } from '../analyze.js'
|
|
39
39
|
import {
|
|
40
|
-
isSimpleArg, fixedScalarTypedArray, fixedTypedArraysInBody, maxScalarTypedArrayLen,
|
|
40
|
+
isSimpleArg, fixedScalarTypedArray, fixedTypedArraysInBody, maxScalarTypedArrayLen, freshTypedArrayLocals,
|
|
41
41
|
} from './common.js'
|
|
42
42
|
|
|
43
43
|
// === Loop unrolling & scalarization ===
|
|
@@ -526,6 +526,44 @@ export const scalarizeFunctionTypedArrays = (programFacts) => {
|
|
|
526
526
|
return changed
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
// Param-distinctness (alias analysis). Marks a function's typed-array params MUTUALLY DISTINCT
|
|
530
|
+
// (provably different buffers) when EVERY call site passes a distinct fresh `new TypedArray` local
|
|
531
|
+
// for each of them. This is what lets the optimizer's LICM hoist a load from one such param across
|
|
532
|
+
// a store to another (the load can't be clobbered) — the alias-analysis-enabled LICM that
|
|
533
|
+
// rust/clang get for free (raytrace's sphere loads vs the framebuffer store). Sound because:
|
|
534
|
+
// • a fresh `new TypedArray(N)` is a unique buffer (the allocator returns fresh memory);
|
|
535
|
+
// • requiring ALL typed-array-param args to be fresh-new excludes views/subarrays (not fresh)
|
|
536
|
+
// and forwarded params (not fresh), the only ways two args could alias;
|
|
537
|
+
// • pairwise-distinct arg names rule out the same buffer passed twice;
|
|
538
|
+
// • scalar (non-TYPED) params are ignored — they can't alias a buffer.
|
|
539
|
+
// Conservatively all-or-nothing per function: any non-fresh/duplicate typed arg ⇒ no fact.
|
|
540
|
+
export const analyzeParamDistinctness = (programFacts) => {
|
|
541
|
+
const freshByFunc = new Map(ctx.func.list.map(func => [func, freshTypedArrayLocals(func.body)]))
|
|
542
|
+
const sitesByCallee = new Map()
|
|
543
|
+
for (const site of programFacts.callSites) {
|
|
544
|
+
if (!site.callerFunc) continue
|
|
545
|
+
const l = sitesByCallee.get(site.callee); l ? l.push(site) : sitesByCallee.set(site.callee, [site])
|
|
546
|
+
}
|
|
547
|
+
for (const func of ctx.func.list) {
|
|
548
|
+
const params = func.sig?.params, sites = sitesByCallee.get(func.name)
|
|
549
|
+
if (!params || !sites?.length) continue
|
|
550
|
+
const typedIdx = []
|
|
551
|
+
for (let i = 0; i < params.length; i++) if (params[i].ptrKind === VAL.TYPED) typedIdx.push(i)
|
|
552
|
+
if (typedIdx.length < 2) continue // distinctness only matters with ≥2 typed-array params
|
|
553
|
+
let ok = true
|
|
554
|
+
for (const site of sites) {
|
|
555
|
+
const seen = new Set()
|
|
556
|
+
for (const i of typedIdx) {
|
|
557
|
+
const arg = site.argList?.[i]
|
|
558
|
+
if (typeof arg !== 'string' || !freshByFunc.get(site.callerFunc)?.has(arg) || seen.has(arg)) { ok = false; break }
|
|
559
|
+
seen.add(arg)
|
|
560
|
+
}
|
|
561
|
+
if (!ok) break
|
|
562
|
+
}
|
|
563
|
+
if (ok) func.distinctParams = new Set(typedIdx.map(i => params[i].name))
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
529
567
|
const scalarizeArrayLiteralSeq = (seq) => {
|
|
530
568
|
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
531
569
|
let changed = false
|
|
@@ -940,13 +978,41 @@ const _intArrayLitElems = (expr) => {
|
|
|
940
978
|
return out
|
|
941
979
|
}
|
|
942
980
|
|
|
981
|
+
// Arithmetic and bitwise operators that always produce a numeric result —
|
|
982
|
+
// regardless of operand types — so `name[expr]` is index-safe after promotion.
|
|
983
|
+
// Bitwise ops (`&`, `|`, etc.) ToInt32 their operands; pure-arithmetic ops with
|
|
984
|
+
// numeric leaves stay numeric. `+` is excluded: `"a" + "b"` is a string.
|
|
985
|
+
const _NUMERIC_INDEX_OPS = new Set(['-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>', 'u-', 'u+'])
|
|
986
|
+
|
|
987
|
+
// Returns true when `key` is provably a numeric index at plan time: an integer
|
|
988
|
+
// literal, a local name whose val-type is VAL.NUMBER in `valTypes`, or a
|
|
989
|
+
// compound expression that always produces a number (arithmetic/bitwise ops).
|
|
990
|
+
// Mirrors the `idxNumericName` / `intIndexIR` guard in emit so that the same
|
|
991
|
+
// index shapes that skip `__is_str_key` at emit-time also pass here. Used by
|
|
992
|
+
// `_disqualifyPromotion` to gate index reads: a non-numeric key on a promoted
|
|
993
|
+
// Int32Array NaN-coerces to 0 (trunc_sat_f64_s(NaN) = 0) instead of returning
|
|
994
|
+
// undefined — the correct JS behaviour for an out-of-range or string index.
|
|
995
|
+
const _isNumericKey = (key, valTypes) => {
|
|
996
|
+
if (key == null) return false
|
|
997
|
+
if (nonNegIntLiteral(key) != null) return true // literal integer
|
|
998
|
+
if (typeof key === 'string') return valTypes?.get(key) === VAL.NUMBER
|
|
999
|
+
if (!Array.isArray(key)) return false
|
|
1000
|
+
const op = key[0]
|
|
1001
|
+
if (op == null) return typeof key[1] === 'number' // [null, n] literal
|
|
1002
|
+
if (_NUMERIC_INDEX_OPS.has(op)) return true // always produces Number
|
|
1003
|
+
// `+` is numeric only when both operands are proven numeric.
|
|
1004
|
+
if (op === '+' && key.length === 3)
|
|
1005
|
+
return _isNumericKey(key[1], valTypes) && _isNumericKey(key[2], valTypes)
|
|
1006
|
+
return false
|
|
1007
|
+
}
|
|
1008
|
+
|
|
943
1009
|
// Walks `node` and disqualifies every candidate name that appears in an
|
|
944
1010
|
// unsafe context. `initSet` holds the candidate's own init-decl AST nodes
|
|
945
1011
|
// (their LHS reference is the binding being defined, not an escape).
|
|
946
|
-
const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
1012
|
+
const _disqualifyPromotion = (node, candidates, disqualified, initSet, valTypes) => {
|
|
947
1013
|
if (initSet.has(node)) {
|
|
948
1014
|
// The init decl itself: only walk the RHS (skip the LHS `name`).
|
|
949
|
-
return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1015
|
+
return _disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
950
1016
|
}
|
|
951
1017
|
if (typeof node === 'string') {
|
|
952
1018
|
// Bare identifier outside any handled parent context — escape.
|
|
@@ -972,7 +1038,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
972
1038
|
Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') &&
|
|
973
1039
|
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
974
1040
|
disqualified.add(node[1][1])
|
|
975
|
-
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1041
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet, valTypes)
|
|
976
1042
|
return
|
|
977
1043
|
}
|
|
978
1044
|
|
|
@@ -994,7 +1060,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
994
1060
|
typeof callee[1] === 'string' && candidates.has(callee[1])) {
|
|
995
1061
|
if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
|
|
996
1062
|
// Walk method args (skip the receiver — already validated above).
|
|
997
|
-
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1063
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet, valTypes)
|
|
998
1064
|
return
|
|
999
1065
|
}
|
|
1000
1066
|
// Array.isArray flips true→false under promotion.
|
|
@@ -1003,7 +1069,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
1003
1069
|
const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
|
|
1004
1070
|
for (const a of list) {
|
|
1005
1071
|
if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
|
|
1006
|
-
else _disqualifyPromotion(a, candidates, disqualified, initSet)
|
|
1072
|
+
else _disqualifyPromotion(a, candidates, disqualified, initSet, valTypes)
|
|
1007
1073
|
}
|
|
1008
1074
|
return
|
|
1009
1075
|
}
|
|
@@ -1011,10 +1077,15 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
1011
1077
|
// bare-name leaf above and disqualify.
|
|
1012
1078
|
}
|
|
1013
1079
|
|
|
1014
|
-
// Index read `name[k]` —
|
|
1015
|
-
//
|
|
1080
|
+
// Index read `name[k]` — TYPED-safe only when the key is provably numeric.
|
|
1081
|
+
// A string or unknown key on a promoted Int32Array would NaN-coerce to 0
|
|
1082
|
+
// (i32.trunc_sat_f64_s(NaN) = 0) instead of returning undefined — silently
|
|
1083
|
+
// wrong. Mirror the `idxNumericName` / `intIndexIR` guard in emit: disqualify
|
|
1084
|
+
// the candidate unless `k` is an integer literal, a VAL.NUMBER local, or an
|
|
1085
|
+
// expression that always produces a Number (bitwise/arithmetic ops).
|
|
1016
1086
|
if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
1017
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1087
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
1088
|
+
if (!_isNumericKey(node[2], valTypes)) disqualified.add(node[1])
|
|
1018
1089
|
return
|
|
1019
1090
|
}
|
|
1020
1091
|
|
|
@@ -1023,15 +1094,15 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
1023
1094
|
if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
|
|
1024
1095
|
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
1025
1096
|
disqualified.add(node[1][1])
|
|
1026
|
-
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
|
|
1027
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1097
|
+
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet, valTypes)
|
|
1098
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
1028
1099
|
return
|
|
1029
1100
|
}
|
|
1030
1101
|
|
|
1031
1102
|
// Whole-binding reassign: `name = …` / `name += …` / etc.
|
|
1032
1103
|
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
1033
1104
|
disqualified.add(node[1])
|
|
1034
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1105
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
1035
1106
|
return
|
|
1036
1107
|
}
|
|
1037
1108
|
|
|
@@ -1041,7 +1112,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
1041
1112
|
if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
|
|
1042
1113
|
if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
|
|
1043
1114
|
disqualified.add(t[1])
|
|
1044
|
-
_disqualifyPromotion(t[2], candidates, disqualified, initSet)
|
|
1115
|
+
_disqualifyPromotion(t[2], candidates, disqualified, initSet, valTypes)
|
|
1045
1116
|
return
|
|
1046
1117
|
}
|
|
1047
1118
|
}
|
|
@@ -1056,9 +1127,9 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
1056
1127
|
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
|
|
1057
1128
|
candidates.has(d[1]) && !initSet.has(d)) {
|
|
1058
1129
|
disqualified.add(d[1])
|
|
1059
|
-
_disqualifyPromotion(d[2], candidates, disqualified, initSet)
|
|
1130
|
+
_disqualifyPromotion(d[2], candidates, disqualified, initSet, valTypes)
|
|
1060
1131
|
} else {
|
|
1061
|
-
_disqualifyPromotion(d, candidates, disqualified, initSet)
|
|
1132
|
+
_disqualifyPromotion(d, candidates, disqualified, initSet, valTypes)
|
|
1062
1133
|
}
|
|
1063
1134
|
}
|
|
1064
1135
|
return
|
|
@@ -1080,14 +1151,14 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
1080
1151
|
for (let i = 1; i < node.length; i++) {
|
|
1081
1152
|
const child = node[i]
|
|
1082
1153
|
if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
|
|
1083
|
-
_disqualifyPromotion(child, candidates, disqualified, initSet)
|
|
1154
|
+
_disqualifyPromotion(child, candidates, disqualified, initSet, valTypes)
|
|
1084
1155
|
}
|
|
1085
1156
|
return
|
|
1086
1157
|
}
|
|
1087
1158
|
|
|
1088
1159
|
// Generic — recurse into children. Bare-name refs at unhandled positions
|
|
1089
1160
|
// hit the string-leaf branch above and disqualify on contact.
|
|
1090
|
-
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1161
|
+
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet, valTypes)
|
|
1091
1162
|
}
|
|
1092
1163
|
|
|
1093
1164
|
// Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
|
|
@@ -1155,8 +1226,11 @@ const promoteIntArrayLiteralsInBody = (body) => {
|
|
|
1155
1226
|
if (!candidates.size) return { node: body, changed: false }
|
|
1156
1227
|
const initSet = new Set()
|
|
1157
1228
|
for (const { initDecl } of candidates.values()) initSet.add(initDecl)
|
|
1229
|
+
// valTypes from analyzeBody gives per-local VAL.* kinds, used by
|
|
1230
|
+
// _disqualifyPromotion to prove index keys are numeric (see _isNumericKey).
|
|
1231
|
+
const { valTypes } = analyzeBody(body)
|
|
1158
1232
|
const disqualified = new Set()
|
|
1159
|
-
_disqualifyPromotion(body, candidates, disqualified, initSet)
|
|
1233
|
+
_disqualifyPromotion(body, candidates, disqualified, initSet, valTypes)
|
|
1160
1234
|
const validated = new Set()
|
|
1161
1235
|
for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
|
|
1162
1236
|
if (!validated.size) return { node: body, changed: false }
|
package/src/ctx.js
CHANGED
|
@@ -38,11 +38,11 @@ export { HEAP, LAYOUT, PTR, ATOM, FORWARDING_MASK, nanPrefixHex, atomNanHex, sso
|
|
|
38
38
|
// |-----------|----------|---------------------------------|---------------------------|
|
|
39
39
|
// | core | compile | reset, modules, inc(), emit* | emit, compile, modules |
|
|
40
40
|
// | module | compile | prepare, index.js | prepare, compile, emit |
|
|
41
|
-
// | scope | compile | analyze, compile, plan, modules | compile, emit
|
|
42
|
-
// | func | function | compile, narrow
|
|
41
|
+
// | scope | compile | analyze, compile, plan, modules, assemble | compile, emit |
|
|
42
|
+
// | func | function | compile, narrow, assemble | emit, modules |
|
|
43
43
|
// | types | function | analyze, plan | emit, modules |
|
|
44
44
|
// | schema | compile | prepare, analyze, compile | prepare, analyze, emit |
|
|
45
|
-
// | closure | init | modules (fn plugin)
|
|
45
|
+
// | closure | init | modules (fn plugin), plan, emit | emit, compile |
|
|
46
46
|
// | runtime | compile | emit, modules | emit, compile |
|
|
47
47
|
// | memory | compile | index.js | compile |
|
|
48
48
|
// | error | compile | prepare, compile, emit | err() |
|
|
@@ -63,6 +63,11 @@ export { HEAP, LAYOUT, PTR, ATOM, FORWARDING_MASK, nanPrefixHex, atomNanHex, sso
|
|
|
63
63
|
// narrow-phase writers: narrowSignatures (under plan) temporarily swaps
|
|
64
64
|
// ctx.func.{localReps, locals, current} per-function with save/restore
|
|
65
65
|
// so per-call-site signature inference sees the right scope.
|
|
66
|
+
// assemble-phase writers: buildStartFn (wat/assemble.js) re-owns the ctx.func frame
|
|
67
|
+
// (locals/stack/refinements/…) to emit the module-init `start` fn, save/restoring
|
|
68
|
+
// around it; the data pass also const-folds ctx.scope.globals (mut→false) and
|
|
69
|
+
// declares the __heap* globals. emit seeds ctx.closure.{paramTypes,paramTypedCtors}
|
|
70
|
+
// at direct-call sites (read by emitClosureBody); plan sets ctx.closure.{floor,width}.
|
|
66
71
|
export const ctx = {
|
|
67
72
|
core: {}, // emitter table + stdlib registry (seeded by reset + modules)
|
|
68
73
|
module: {}, // module graph: imports, resolved sources, module-init blocks
|
|
@@ -92,9 +97,14 @@ export const ctx = {
|
|
|
92
97
|
// the defaults; see reset() for the field list and who flips each.
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
/** Create a child scope via shallow flat copy
|
|
100
|
+
/** Create a child scope via shallow flat copy with NO prototype chain. Critical:
|
|
101
|
+
* `{ ...parent }` would inherit Object.prototype in V8 (jz.js), so a name-keyed lookup
|
|
102
|
+
* like `chain['valueOf']`/`emit['toString']` returns the inherited method instead of
|
|
103
|
+
* undefined — corrupting resolution of any identifier named like an Object method. The
|
|
104
|
+
* kernel's jz objects are already prototype-less, so this was a jz.js-ONLY footgun. A
|
|
105
|
+
* prototype-less dict (Object.create(null) + assign) is correct in both engines.
|
|
96
106
|
* Mutations to the child do not affect the parent; lookups work via direct property access. */
|
|
97
|
-
export const derive = (parent) => (
|
|
107
|
+
export const derive = (parent) => Object.assign(Object.create(null), parent)
|
|
98
108
|
|
|
99
109
|
/** Include stdlib names for emission. */
|
|
100
110
|
export const inc = (...names) => names.forEach(n => ctx.core.includes.add(n))
|
|
@@ -133,12 +143,19 @@ export const emitter = (deps, fn) => {
|
|
|
133
143
|
* carry it as `.argc`; bare ones expose it as the function's own `.length`. */
|
|
134
144
|
export const emitArity = (h) => h?.argc ?? h?.length
|
|
135
145
|
|
|
136
|
-
/**
|
|
137
|
-
* property is *read* (`re.source`, `m.size`), so the `.`-read
|
|
138
|
-
* Untagged handlers are methods: a bare read of
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
|
|
146
|
+
/** Register `fn` as a property-GETTER emitter for `key` — it yields a value when
|
|
147
|
+
* the property is *read* (`re.source`, `m.size`, `a.byteOffset`), so the `.`-read
|
|
148
|
+
* path fires it. (Untagged `ctx.core.emit` handlers are methods: a bare read of
|
|
149
|
+
* `m.values` must NOT invoke them — that would materialize a view — they fire only
|
|
150
|
+
* from the method-call path.) Getter-ness lives in `ctx.core.getters` (a plain Set),
|
|
151
|
+
* NOT as a flag on the emitter closure: the self-host kernel can't reliably read a
|
|
152
|
+
* dynamic property off a closure returned via a dynamic-key lookup, so a closure tag
|
|
153
|
+
* silently read `undefined` and every getter fell through to `__dyn_get`. A Set
|
|
154
|
+
* key-lookup is kernel-safe. Dispatch (module/core.js) checks `ctx.core.getters.has(key)`. */
|
|
155
|
+
export const registerGetter = (key, fn) => {
|
|
156
|
+
ctx.core.emit[key] = fn
|
|
157
|
+
ctx.core.getters.add(key)
|
|
158
|
+
}
|
|
142
159
|
|
|
143
160
|
/** Expand ctx.core.includes transitively via ctx.core.stdlibDeps. Call before WASM assembly.
|
|
144
161
|
* Each module co-locates its own deps with its stdlib registrations at init time. */
|
|
@@ -207,6 +224,13 @@ export function reset(proto, globals, bridge) {
|
|
|
207
224
|
// `(import "env" "name" (global $name i64))` at assembly. Same
|
|
208
225
|
// usage-gated pattern as jsstring — emit records, assembly owns
|
|
209
226
|
// the ctx.module.imports write.
|
|
227
|
+
getters: new Set(), // keys of emit entries that are property getters — the
|
|
228
|
+
// kernel-safe authority for getter dispatch (a closure-attached
|
|
229
|
+
// flag was unreadable in the self-host kernel after a dynamic-key
|
|
230
|
+
// lookup, so every getter silently fell through to __dyn_get).
|
|
231
|
+
// MUST remain last: adding fields before stdlib/stdlibDeps/… shifts
|
|
232
|
+
// their slot indices and breaks the self-host compiled kernel's reads.
|
|
233
|
+
// Populated by registerGetter(); checked by module/core.js dispatch.
|
|
210
234
|
}
|
|
211
235
|
|
|
212
236
|
|
|
@@ -234,6 +258,10 @@ export function reset(proto, globals, bridge) {
|
|
|
234
258
|
globalTypedElem: null,
|
|
235
259
|
globalReps: null, // Map<name, ValueRep> — module-level pointer reps (TYPED const globals stored as raw i32 offset, etc.)
|
|
236
260
|
consts: null,
|
|
261
|
+
constInts: null, // Map<name, int> — module const folded to an integer literal (prepare/plan seed; static/ir read)
|
|
262
|
+
constStrs: null, // Map<name, string> — module const folded to a string literal
|
|
263
|
+
shapeStrs: null, // Map<expr, string> / shapeStrArrays: Map<name, string[]> — schema-shape string folds
|
|
264
|
+
shapeStrArrays: null,
|
|
237
265
|
}
|
|
238
266
|
|
|
239
267
|
ctx.func = {
|
|
@@ -241,7 +269,7 @@ export function reset(proto, globals, bridge) {
|
|
|
241
269
|
names: new Set(), // Set<string> — known func names (list + imported funcs); populated at compile() start
|
|
242
270
|
map: new Map(), // Map<string, func> — name → func entry; populated at compile() start
|
|
243
271
|
multiProp: new Set(), // Set<"obj.prop"> — function-properties assigned >1× (wrapper composition); suppresses the static fn.prop() direct call
|
|
244
|
-
exports:
|
|
272
|
+
exports: Object.create(null), // name-keyed: prototype-less (see derive) — `export let valueOf` must not hit Object.prototype
|
|
245
273
|
current: null,
|
|
246
274
|
locals: new Map(),
|
|
247
275
|
localReps: null,
|
|
@@ -324,6 +352,8 @@ export function reset(proto, globals, bridge) {
|
|
|
324
352
|
minArgc: null, // Map<closureBodyName, number> — fewest args any direct call passed.
|
|
325
353
|
// A slot at index ≥ minArgc is omitted by some call (→ may be undefined),
|
|
326
354
|
// so it must NOT be typed NUMBER, else `x === undefined` mis-folds to false.
|
|
355
|
+
floor: null, // min closure-table arity (modules: fn/timer/typedarray/array; read in plan). null ⇒ 0.
|
|
356
|
+
width: null, // closure call/make signature width (plan/scope sets; emit/assemble read). null ⇒ MAX_CLOSURE_ARITY.
|
|
327
357
|
}
|
|
328
358
|
|
|
329
359
|
ctx.runtime = {
|
|
@@ -370,6 +400,15 @@ export function reset(proto, globals, bridge) {
|
|
|
370
400
|
inspect: false, // when true, compile() additionally populates ctx.inspect with the inferred
|
|
371
401
|
// per-function signatures, locals, and JSON shapes — readable by editor
|
|
372
402
|
// hosts for inlay hints / hover types without re-running the analyzer.
|
|
403
|
+
helperCounters: false, // internal profiling mode: export mutable i64 counters for selected
|
|
404
|
+
// runtime helpers and instrument their entry blocks. Build-time opt-in
|
|
405
|
+
// only; normal output is byte-identical and pays no counter cost.
|
|
406
|
+
helperCallsites: false, // profiling-only: export mutable i64 counters for selected runtime
|
|
407
|
+
// helper callsites after optimization, so hot helpers can be traced
|
|
408
|
+
// back to the compiled function that calls them.
|
|
409
|
+
loopXformId: 0, // monotonic id for the per-function loop transforms' generated locals
|
|
410
|
+
// (loop-model freshLoopId). Per-compile (reset here), not a module-global —
|
|
411
|
+
// so compile(P) is deterministic regardless of prior compiles in the process.
|
|
373
412
|
}
|
|
374
413
|
|
|
375
414
|
// Inspection sink. Populated by compile() only when transform.inspect is true.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ctx, declGlobal, inc } from './ctx.js'
|
|
2
|
+
import { findBodyStart } from './ir.js'
|
|
3
|
+
|
|
4
|
+
export const HELPER_COUNTERS = [
|
|
5
|
+
['__eq', 'eq'],
|
|
6
|
+
['__same_value_zero', 'same_value_zero'],
|
|
7
|
+
['__is_truthy', 'is_truthy'],
|
|
8
|
+
['__ptr_type', 'ptr_type'],
|
|
9
|
+
['__ptr_offset', 'ptr_offset'],
|
|
10
|
+
['__len', 'len'],
|
|
11
|
+
['__length', 'length'],
|
|
12
|
+
['__typed_idx', 'typed_idx'],
|
|
13
|
+
['__str_len', 'str_len'],
|
|
14
|
+
['__str_eq', 'str_eq'],
|
|
15
|
+
['__str_eq_cold', 'str_eq_cold'],
|
|
16
|
+
['__str_hash', 'str_hash'],
|
|
17
|
+
['__map_hash', 'map_hash'],
|
|
18
|
+
['__hash_get_local', 'hash_get_local'],
|
|
19
|
+
['__hash_set_local', 'hash_set_local'],
|
|
20
|
+
['__ihash_get_local', 'ihash_get_local'],
|
|
21
|
+
['__ihash_set_local', 'ihash_set_local'],
|
|
22
|
+
['__dyn_get', 'dyn_get'],
|
|
23
|
+
['__dyn_get_t', 'dyn_get_t'],
|
|
24
|
+
['__dyn_get_t_h', 'dyn_get_t_h'],
|
|
25
|
+
['__dyn_set', 'dyn_set'],
|
|
26
|
+
['__arr_grow', 'arr_grow'],
|
|
27
|
+
['__arr_grow_known', 'arr_grow_known'],
|
|
28
|
+
['__arr_push1', 'arr_push1'],
|
|
29
|
+
['__arr_shift', 'arr_shift'],
|
|
30
|
+
['__alloc', 'alloc'],
|
|
31
|
+
['__alloc_hdr', 'alloc_hdr'],
|
|
32
|
+
['__alloc_hdr_n', 'alloc_hdr_n'],
|
|
33
|
+
['__memgrow', 'memgrow'],
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const COUNTER_BY_HELPER = new Map(HELPER_COUNTERS.map(([helper, label]) => [helper, `__hc_${label}`]))
|
|
37
|
+
const LABEL_BY_WAT_HELPER = new Map(HELPER_COUNTERS.map(([helper, label]) => [`$${helper}`, label]))
|
|
38
|
+
|
|
39
|
+
export const HELPER_SITE_PREFIX = '__hcs_'
|
|
40
|
+
|
|
41
|
+
export const helperCounterName = helper => COUNTER_BY_HELPER.get(helper)
|
|
42
|
+
|
|
43
|
+
export function installHelperCounters() {
|
|
44
|
+
if (!ctx.transform.helperCounters) return
|
|
45
|
+
for (const counter of COUNTER_BY_HELPER.values()) {
|
|
46
|
+
if (!ctx.scope.globals.has(counter)) declGlobal(counter, 'i64', 0, { export: counter })
|
|
47
|
+
}
|
|
48
|
+
ctx.core.stdlib.__helper_counts_reset = `(func $__helper_counts_reset (export "__helper_counts_reset")
|
|
49
|
+
${[...COUNTER_BY_HELPER.values()].map(counter => ` (global.set $${counter} (i64.const 0))`).join('\n')})`
|
|
50
|
+
inc('__helper_counts_reset')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bump the helper's counter once on entry. NOTE the semantics: this counts FUNCTION
|
|
54
|
+
// ENTRIES at runtime — a call site that jz inlined or specialized away (fusedRewrite,
|
|
55
|
+
// specializeMkptr/specializePtrBase, …) never enters the function and is NOT counted. So
|
|
56
|
+
// the numbers are a relative ranking / lower bound for picking hot helpers, not exact
|
|
57
|
+
// operation counts. Good enough to choose targets; don't read them as call totals.
|
|
58
|
+
export function instrumentHelperCounter(helper, fn) {
|
|
59
|
+
const counter = ctx.transform.helperCounters && helperCounterName(helper)
|
|
60
|
+
if (!counter || !Array.isArray(fn) || fn[0] !== 'func') return fn
|
|
61
|
+
fn.splice(findBodyStart(fn), 0,
|
|
62
|
+
['global.set', `$${counter}`, ['i64.add', ['global.get', `$${counter}`], ['i64.const', 1]]])
|
|
63
|
+
return fn
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const safeExportPart = name => String(name || 'anon')
|
|
67
|
+
.replace(/^\$/, '')
|
|
68
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '_')
|
|
69
|
+
.slice(0, 80) || 'anon'
|
|
70
|
+
|
|
71
|
+
const helperSiteFilter = () => {
|
|
72
|
+
const opt = ctx.transform.helperCallsites
|
|
73
|
+
if (opt === true) return null
|
|
74
|
+
const raw = Array.isArray(opt) ? opt : String(opt || '').split(',')
|
|
75
|
+
const labels = raw.map(s => String(s).trim()).filter(Boolean).map(s => s.replace(/^\$?__/, ''))
|
|
76
|
+
return labels.length ? new Set(labels) : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const funcResults = fn => {
|
|
80
|
+
const out = []
|
|
81
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return out
|
|
82
|
+
for (let i = 2; i < fn.length; i++) {
|
|
83
|
+
const n = fn[i]
|
|
84
|
+
if (Array.isArray(n) && n[0] === 'result') out.push(...n.slice(1))
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const bumpCounter = counter =>
|
|
90
|
+
['global.set', `$${counter}`, ['i64.add', ['global.get', `$${counter}`], ['i64.const', 1]]]
|
|
91
|
+
|
|
92
|
+
// Profiling-only helper-callsite counters. Unlike instrumentHelperCounter(), this
|
|
93
|
+
// answers "which compiled function executed the helper call?" by wrapping each
|
|
94
|
+
// final `(call $__helper ...)` with a tiny counter block:
|
|
95
|
+
// (block (result T) (global.set $__hcs_N ...) (call $__helper ...))
|
|
96
|
+
//
|
|
97
|
+
// This intentionally runs after whole-module optimization. The profile should
|
|
98
|
+
// observe final codegen, while production output remains byte-identical because
|
|
99
|
+
// ctx.transform.helperCallsites is build-time opt-in.
|
|
100
|
+
export function instrumentHelperCallsites(funcs) {
|
|
101
|
+
if (!ctx.transform.helperCallsites) return 0
|
|
102
|
+
const only = helperSiteFilter()
|
|
103
|
+
|
|
104
|
+
const resultsByName = new Map()
|
|
105
|
+
for (const fn of funcs) {
|
|
106
|
+
if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
|
|
107
|
+
resultsByName.set(fn[1], funcResults(fn))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let id = 0
|
|
111
|
+
const wrap = (node, owner) => {
|
|
112
|
+
if (!Array.isArray(node)) return node
|
|
113
|
+
for (let i = 1; i < node.length; i++) node[i] = wrap(node[i], owner)
|
|
114
|
+
|
|
115
|
+
if (node[0] !== 'call' || typeof node[1] !== 'string') return node
|
|
116
|
+
const label = LABEL_BY_WAT_HELPER.get(node[1])
|
|
117
|
+
if (!label) return node
|
|
118
|
+
if (only && !only.has(label) && !only.has(node[1].replace(/^\$?__/, ''))) return node
|
|
119
|
+
const results = resultsByName.get(node[1])
|
|
120
|
+
if (!results) return node
|
|
121
|
+
|
|
122
|
+
const counter = `${HELPER_SITE_PREFIX}${id++}`
|
|
123
|
+
const ownerPart = safeExportPart(owner)
|
|
124
|
+
declGlobal(counter, 'i64', 0, { export: `${counter}:${label}:${ownerPart}` })
|
|
125
|
+
const block = ['block']
|
|
126
|
+
for (const type of results) block.push(['result', type])
|
|
127
|
+
block.push(bumpCounter(counter), node)
|
|
128
|
+
return block
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const fn of funcs) {
|
|
132
|
+
if (!Array.isArray(fn) || fn[0] !== 'func' || typeof fn[1] !== 'string') continue
|
|
133
|
+
const bodyStart = findBodyStart(fn)
|
|
134
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = wrap(fn[i], fn[1])
|
|
135
|
+
}
|
|
136
|
+
return id
|
|
137
|
+
}
|
package/src/ir.js
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { ctx, err, inc, PTR, LAYOUT } from './ctx.js'
|
|
24
|
-
import { ptrBoxPrefixBigInt, atomNanHex, nanPrefixHex } from '../layout.js'
|
|
25
|
-
import { I32_MIN, I32_MAX, isI32, isLiteralStr, isFuncRef } from './ast.js'
|
|
24
|
+
import { ptrBoxPrefixBigInt, ptrBits, i64Hex, atomNanHex, nanPrefixHex } from '../layout.js'
|
|
25
|
+
import { I32_MIN, I32_MAX, isI32, isLiteralStr, isFuncRef, isLeaf } from './ast.js'
|
|
26
26
|
import { VAL, lookupValType, repOf, repOfGlobal } from './reps.js'
|
|
27
27
|
import { valTypeOf } from './kind.js'
|
|
28
28
|
import { T } from './ast.js'
|
|
@@ -182,6 +182,82 @@ const narrowI32 = (x, isRoot) => {
|
|
|
182
182
|
return null
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
// Conservative VALUE-RANGE for a pure f64 expression tree: returns { lo, hi } bounding
|
|
186
|
+
// every value the node can take, or null when unknown. SOUND by construction — each rule
|
|
187
|
+
// over-approximates using the SAME f64 ops the runtime uses (f64 +/−/× are monotone in
|
|
188
|
+
// each argument, so combining endpoint-bounds with plain JS doubles yields bounds that
|
|
189
|
+
// contain the true value). A null/non-finite endpoint anywhere collapses to null, so a
|
|
190
|
+
// non-null result also PROVES the value is finite (never NaN, never ±∞). Used by toI32 to
|
|
191
|
+
// drop the +∞-guard `select` (and the i64 round-trip when the value fits i32) — the guard
|
|
192
|
+
// exists only to remap +∞→0, so a proof of finiteness retires it.
|
|
193
|
+
const fin = (lo, hi) => (Number.isFinite(lo) && Number.isFinite(hi) && lo <= hi) ? { lo, hi } : null
|
|
194
|
+
// Range of the i32 that feeds an `f64.convert_i32_*`, refined by a narrowing load width.
|
|
195
|
+
const convRange = (child, signed) => {
|
|
196
|
+
if (Array.isArray(child)) {
|
|
197
|
+
const o = child[0]
|
|
198
|
+
if (o === 'i32.load8_u') return { lo: 0, hi: 255 }
|
|
199
|
+
if (o === 'i32.load8_s') return { lo: -128, hi: 127 }
|
|
200
|
+
if (o === 'i32.load16_u') return { lo: 0, hi: 65535 }
|
|
201
|
+
if (o === 'i32.load16_s') return { lo: -32768, hi: 32767 }
|
|
202
|
+
if (o === 'i32.const' && typeof child[1] === 'number') return signed ? { lo: child[1] | 0, hi: child[1] | 0 } : { lo: child[1] >>> 0, hi: child[1] >>> 0 }
|
|
203
|
+
}
|
|
204
|
+
return signed ? { lo: I32_MIN, hi: I32_MAX } : { lo: 0, hi: 4294967295 }
|
|
205
|
+
}
|
|
206
|
+
// `get`, when supplied, resolves a `(local.get $V)` to $V's single defining value node
|
|
207
|
+
// (a `name → defExpr | null` map/function built from a one-def-per-local scan). This lets the
|
|
208
|
+
// range see through the temps that inlining introduces — e.g. `floor(mul(convert($px),0.03125))`
|
|
209
|
+
// stashed in `$xi` before truncation — so the i32-fit proof survives the indirection. SOUND
|
|
210
|
+
// without code motion: a single-textual-def local holds exactly the value its def computes, so
|
|
211
|
+
// the def's range bounds every value the local can take, even if the def's inputs vary across
|
|
212
|
+
// iterations. A self-referential (loop-carried) single def is caught by the `seen` cycle guard
|
|
213
|
+
// and yields null (unknown), which is conservative.
|
|
214
|
+
export const f64Range = (n, get) => {
|
|
215
|
+
const seen = get ? new Set() : null
|
|
216
|
+
const r = (n) => {
|
|
217
|
+
if (!Array.isArray(n)) return null
|
|
218
|
+
const op = n[0]
|
|
219
|
+
if (op === 'local.get' && get && typeof n[1] === 'string') {
|
|
220
|
+
if (seen.has(n[1])) return null // loop-carried / cyclic def → unknown
|
|
221
|
+
const def = typeof get === 'function' ? get(n[1]) : get.get(n[1])
|
|
222
|
+
if (!def) return null
|
|
223
|
+
seen.add(n[1]); const rng = r(def); seen.delete(n[1])
|
|
224
|
+
return rng
|
|
225
|
+
}
|
|
226
|
+
if (op === 'f64.const') return typeof n[1] === 'number' ? fin(n[1], n[1]) : null // `nan:…`/Inf literal strings → null
|
|
227
|
+
if (op === 'f64.convert_i32_s') return convRange(n[1], true)
|
|
228
|
+
if (op === 'f64.convert_i32_u') return convRange(n[1], false)
|
|
229
|
+
if (op === 'f64.neg') { const a = r(n[1]); return a && fin(-a.hi, -a.lo) }
|
|
230
|
+
if (op === 'f64.abs') { const a = r(n[1]); return a && fin(a.lo > 0 ? a.lo : a.hi < 0 ? -a.hi : 0, Math.max(-a.lo, a.hi)) }
|
|
231
|
+
if (op === 'f64.sqrt') { const a = r(n[1]); return a && a.lo >= 0 && fin(Math.sqrt(a.lo), Math.sqrt(a.hi)) }
|
|
232
|
+
// Rounding ops preserve finiteness and are monotonic, so the range maps elementwise. This lets
|
|
233
|
+
// `Math.floor(x)|0` over a bounded x (every grid/image/audio index: `px*scale`, perm[] lookups)
|
|
234
|
+
// drop the +∞-guard + i64 round-trip in toI32 down to a single i32.trunc_sat_f64_s. `nearest`
|
|
235
|
+
// (round-half-to-even) lands in {floor,ceil} so its bounds are floor(lo)..ceil(hi).
|
|
236
|
+
if (op === 'f64.floor') { const a = r(n[1]); return a && fin(Math.floor(a.lo), Math.floor(a.hi)) }
|
|
237
|
+
if (op === 'f64.ceil') { const a = r(n[1]); return a && fin(Math.ceil(a.lo), Math.ceil(a.hi)) }
|
|
238
|
+
if (op === 'f64.trunc') { const a = r(n[1]); return a && fin(Math.trunc(a.lo), Math.trunc(a.hi)) }
|
|
239
|
+
if (op === 'f64.nearest') { const a = r(n[1]); return a && fin(Math.floor(a.lo), Math.ceil(a.hi)) }
|
|
240
|
+
if (op === 'f64.add') { const a = r(n[1]), b = r(n[2]); return a && b && fin(a.lo + b.lo, a.hi + b.hi) }
|
|
241
|
+
if (op === 'f64.sub') { const a = r(n[1]), b = r(n[2]); return a && b && fin(a.lo - b.hi, a.hi - b.lo) }
|
|
242
|
+
if (op === 'f64.mul') {
|
|
243
|
+
const a = r(n[1]), b = r(n[2]); if (!a || !b) return null
|
|
244
|
+
const p = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi]
|
|
245
|
+
return fin(Math.min(...p), Math.max(...p))
|
|
246
|
+
}
|
|
247
|
+
if (op === 'f64.div') {
|
|
248
|
+
const c = Array.isArray(n[2]) && n[2][0] === 'f64.const' && typeof n[2][1] === 'number' ? n[2][1] : null
|
|
249
|
+
if (c == null || c === 0) return null // variable / zero divisor → may be ±∞
|
|
250
|
+
const a = r(n[1]); if (!a) return null
|
|
251
|
+
const p = [a.lo / c, a.hi / c]
|
|
252
|
+
return fin(Math.min(...p), Math.max(...p))
|
|
253
|
+
}
|
|
254
|
+
if (op === 'f64.min') { const a = r(n[1]), b = r(n[2]); return a && b && fin(Math.min(a.lo, b.lo), Math.min(a.hi, b.hi)) }
|
|
255
|
+
if (op === 'f64.max') { const a = r(n[1]), b = r(n[2]); return a && b && fin(Math.max(a.lo, b.lo), Math.max(a.hi, b.hi)) }
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
return r(n)
|
|
259
|
+
}
|
|
260
|
+
|
|
185
261
|
/** Coerce node to i32 with wrapping (JS `|0` semantics: values > 2^31 wrap to negative).
|
|
186
262
|
* Per ECMAScript ToInt32, NaN and ±∞ map to 0. `i64.trunc_sat_f64_s` handles NaN
|
|
187
263
|
* and -∞ correctly, but +∞ saturates to i64_max which wraps to -1 — guard +∞ via
|
|
@@ -204,15 +280,27 @@ export const toI32 = n => {
|
|
|
204
280
|
// computes in i32 (mod-2^32 ring) — no trunc/guard at all.
|
|
205
281
|
const nw = narrowI32(n, true)
|
|
206
282
|
if (nw) return nw.node
|
|
283
|
+
// Value-range narrowing: a NON-integer f64 tree (e.g. `10 + 200·(u8[i]/255)`) the ring
|
|
284
|
+
// path rejects, but whose value is PROVABLY FINITE — so the +∞-guard `select` is dead.
|
|
285
|
+
// When the value also provably fits i32, a single `i32.trunc_sat_f64_s` IS exact ToInt32
|
|
286
|
+
// (no saturation can fire in-range, no NaN, no ±∞) — dropping the i64 round-trip AND the
|
|
287
|
+
// guard. Pervasive in pixel/colour packing: `(base + scale·v)|0`.
|
|
288
|
+
const rng = f64Range(n)
|
|
289
|
+
if (rng) {
|
|
290
|
+
if (rng.lo >= I32_MIN && rng.hi <= I32_MAX) return typed(['i32.trunc_sat_f64_s', n], 'i32')
|
|
291
|
+
// Finite and within (−2^63, 2^63): keep the mod-2^32 wrap, drop the (now-dead) +∞ guard.
|
|
292
|
+
// i64.trunc_sat does not saturate in this window, so wrap_i64 == ToInt32. Beyond ±2^63 we
|
|
293
|
+
// fall through to the guarded path (which already saturates there — the documented boundary).
|
|
294
|
+
if (rng.lo >= -9223372036854775808 && rng.hi < 9223372036854775808)
|
|
295
|
+
return typed(['i32.wrap_i64', ['i64.trunc_sat_f64_s', n]], 'i32')
|
|
296
|
+
}
|
|
207
297
|
// Leaf nodes are cheap to duplicate; for everything else, evaluate once via local.tee.
|
|
208
|
-
const isLeaf = Array.isArray(n) && n.length <= 2 &&
|
|
209
|
-
(n[0] === 'f64.const' || n[0] === 'local.get' || n[0] === 'global.get')
|
|
210
298
|
// `i32.wrap_i64(i64.trunc_sat_f64_s x)` is exact ToInt32 for |x| < 2^63 (the
|
|
211
299
|
// overwhelming common range), maps NaN/−∞→0, and +∞ is guarded to 0 by the
|
|
212
300
|
// select. For |x| ≥ 2^63 it saturates rather than wrapping mod 2^32 — a
|
|
213
301
|
// deliberately-allowed asm.js-style boundary (no per-`|0` helper/guard cost).
|
|
214
302
|
const wrap = x => typed(['i32.wrap_i64', ['i64.trunc_sat_f64_s', x]], 'i32')
|
|
215
|
-
if (isLeaf) {
|
|
303
|
+
if (isLeaf(n)) {
|
|
216
304
|
return typed(['select', wrap(n), ['i32.const', 0], ['f64.ne', n, ['f64.const', Infinity]]], 'i32')
|
|
217
305
|
}
|
|
218
306
|
const t = temp('inf')
|
|
@@ -317,13 +405,7 @@ export const BOXED_MUTATORS = new Set(['push', 'pop', 'shift', 'unshift', 'splic
|
|
|
317
405
|
const litI32 = n => Array.isArray(n) && n[0] === 'i32.const' && typeof n[1] === 'number' ? n[1] : null
|
|
318
406
|
|
|
319
407
|
/** Pack (type, aux, offset) into the f64 NaN-box bit pattern as a hex string. */
|
|
320
|
-
|
|
321
|
-
const bits = LAYOUT.NAN_PREFIX_BITS
|
|
322
|
-
| ((BigInt(type) & BigInt(LAYOUT.TAG_MASK)) << BigInt(LAYOUT.TAG_SHIFT))
|
|
323
|
-
| ((BigInt(aux) & BigInt(LAYOUT.AUX_MASK)) << BigInt(LAYOUT.AUX_SHIFT))
|
|
324
|
-
| (BigInt(offset >>> 0) & BigInt(LAYOUT.OFFSET_MASK))
|
|
325
|
-
return '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
|
|
326
|
-
}
|
|
408
|
+
const packPtrBits = (type, aux, offset) => i64Hex(ptrBits(type, aux, offset))
|
|
327
409
|
|
|
328
410
|
/** Build `__mkptr(type, aux, offset)` IR. Folds to `(f64.const nan:0x...)` — 9 bytes
|
|
329
411
|
* vs 12 for `f64.reinterpret_i64 (i64.const ...)` — when all args are i32 literals.
|
|
@@ -384,7 +466,7 @@ export function ptrTypeIR(valIR, valType) {
|
|
|
384
466
|
// op on it misdispatches; BigInt64Array/BigUint64Array views and
|
|
385
467
|
// DataView.{get,set}BigUint64 are a legacy f64-value shim there. Strings are
|
|
386
468
|
// tagged and survive every boundary; BigInt math happens only inside single
|
|
387
|
-
// expressions. (Same contract as
|
|
469
|
+
// expressions. (Same contract as watr/optimize's i64 VALUE CONTRACT.)
|
|
388
470
|
const _F64_BITS_BUF = new ArrayBuffer(8)
|
|
389
471
|
const _F64_BITS_F = new Float64Array(_F64_BITS_BUF)
|
|
390
472
|
const _F64_BITS_U32 = new Uint32Array(_F64_BITS_BUF) // LE halves: [0]=lo, [1]=hi
|
|
@@ -1009,6 +1091,13 @@ export function readVar(name) {
|
|
|
1009
1091
|
return typed(['f64.load', boxedAddr(name)], 'f64')
|
|
1010
1092
|
}
|
|
1011
1093
|
if (isGlobal(name)) {
|
|
1094
|
+
// A module-level integer const (`const N = 16384`) is an immutable compile-time
|
|
1095
|
+
// value: emit i32.const directly (when it fits i32) so `x % N` / `x & N` / `x / N`
|
|
1096
|
+
// and counters bounded by N take the native integer path, instead of the global
|
|
1097
|
+
// folding to an f64 constant and routing through the f64 round-trip. Value-preserving
|
|
1098
|
+
// — an f64 consumer widens the i32.const via convert, which folds back to f64.const.
|
|
1099
|
+
const ci = ctx.scope.constInts?.get?.(name)
|
|
1100
|
+
if (ci != null && isI32(ci)) return typed(['i32.const', ci], 'i32')
|
|
1012
1101
|
const gt = ctx.scope.globalTypes.get(name) || 'f64'
|
|
1013
1102
|
const node = typed(['global.get', dollar(name)], gt)
|
|
1014
1103
|
const grep = repOfGlobal(name)
|