jz 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1156 -984
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +29 -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 +26 -3
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +162 -156
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +29 -29
- 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 +253 -23
- package/src/compile/index.js +198 -61
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +169 -32
- 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 +960 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1292 -144
- package/src/prepare/index.js +11 -7
- 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
|
@@ -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)
|
package/src/kind-traits.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { VAL } from './reps.js'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Comparison / logical-not ops — result is a 0|1 boolean carried as i32. The one
|
|
9
|
+
// source of truth for "this operator yields a boolean": valTypeOf reads it as
|
|
10
|
+
// VAL.BOOL, exprType/isIntExpr read it as integer-certain. `in`/`instanceof` also
|
|
11
|
+
// yield a boolean but are membership ops, kept out of the integer-certainty set
|
|
12
|
+
// (they throw on BigInt operands and never reach numeric analysis).
|
|
13
|
+
export const CMP_OPS = new Set(['!', '<', '<=', '>', '>=', '==', '!=', '===', '!=='])
|
|
14
|
+
export const BOOL_OPS = new Set([...CMP_OPS, 'in', 'instanceof'])
|
|
11
15
|
|
|
12
16
|
export const NUMERIC_BINARY_OPS = ['-', 'u-', '*', '/', '%', '&', '|', '^', '<<', '>>']
|
|
13
17
|
export const NUMERIC_UNARY_OPS = new Set(['**', '++', '--', '~', '>>>', 'u+'])
|
package/src/kind.js
CHANGED
|
@@ -137,7 +137,16 @@ VT['?:'] = (args) => {
|
|
|
137
137
|
const truthy = literalTruthiness(args[0])
|
|
138
138
|
if (truthy != null) return valTypeOf(truthy ? args[1] : args[2])
|
|
139
139
|
const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
|
|
140
|
-
|
|
140
|
+
if (ta && ta === tb) return ta
|
|
141
|
+
// A boolean branch coerces to 0/1 in numeric context (same rule as &&/||/?? below):
|
|
142
|
+
// when the other branch has a known non-boolean type, the conditional carries it.
|
|
143
|
+
// Without this, `num + (cond ? num : num>k)` sees a null-typed operand and emits the
|
|
144
|
+
// polymorphic string-concat dispatch on two pure-numeric subexprs — which pins the
|
|
145
|
+
// whole number→string formatter (__str_concat → __to_str → __static_str), a pure-int
|
|
146
|
+
// program ballooning 1 → ~19 funcs (see test/wat-invariants.js, .work/todo.md).
|
|
147
|
+
if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
|
|
148
|
+
if (tb === VAL.BOOL && ta && ta !== VAL.BOOL) return ta
|
|
149
|
+
return null
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
// Value-preserving logical: `&&`/`||` return one of their operands.
|
|
@@ -162,6 +171,10 @@ VT['&&'] = VT['||'] = VT['??'] = (args) => {
|
|
|
162
171
|
// Index access: `arr[i]` → ['[]', arr, i].
|
|
163
172
|
VT['[]'] = (args) => {
|
|
164
173
|
if (args.length < 2) return VAL.ARRAY
|
|
174
|
+
// A literal NEGATIVE index is always out of range → reads undefined, not the
|
|
175
|
+
// element type. Returning a numeric elem type here would let `a[-1] === undefined`
|
|
176
|
+
// fold to false (a NUMBER can't be undefined), silently dropping the guard.
|
|
177
|
+
{ const li = intLiteralValue(args[1]); if (li != null && li < 0) return null }
|
|
165
178
|
// SRoA flat-array slot read: `a[k]` (static index) where `a` dissolved into
|
|
166
179
|
// scalar `a#i` locals (scanFlatObjects). A write-once slot's value-type is its
|
|
167
180
|
// element literal's — same numeric-binding as the `VT['.']` object case, so
|
package/src/op-policy.js
CHANGED
|
@@ -22,7 +22,10 @@ export const REJECT_OPS = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Bare identifiers prepare rejects (no jzify lowering). */
|
|
25
|
-
|
|
25
|
+
// Prototype-less (Object.create(null)): a plain `{}` inherits Object.prototype in V8, so
|
|
26
|
+
// `REJECT_IDENTS['valueOf']` would return the inherited method (truthy) and wrongly reject
|
|
27
|
+
// a user identifier named like an Object method. Kernel objects are already prototype-less.
|
|
28
|
+
export const REJECT_IDENTS = Object.assign(Object.create(null), {
|
|
26
29
|
with: '`with` not supported',
|
|
27
30
|
class: '`class` not supported',
|
|
28
31
|
yield: '`yield` not supported',
|
|
@@ -36,7 +39,7 @@ export const REJECT_IDENTS = {
|
|
|
36
39
|
// identifier in sloppy JS (`var let = 5`), so rejecting it would refuse valid JS
|
|
37
40
|
// (test262 language/expressions/object/let-non-strict-*).
|
|
38
41
|
const: '`const` is a reserved word, not a valid name',
|
|
39
|
-
}
|
|
42
|
+
})
|
|
40
43
|
|
|
41
44
|
/** jzify-only errors for class lowering (no prepare counterpart). */
|
|
42
45
|
export const JZIFY_CLASS_ERRORS = {
|
package/src/ops.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// === AST op tags — integer-tagged-union representation ===
|
|
2
|
+
// internOps (prepare->compile boundary) converts array node[0] from op STRING to its
|
|
3
|
+
// integer tag; the compile half then dispatches via integer-keyed (eventually array)
|
|
4
|
+
// tables, removing the per-node string-hash lookup in the self-host kernel.
|
|
5
|
+
// OP: string -> int (1-based; 0 reserved => a missing tag is falsy). OPS: int -> string.
|
|
6
|
+
export const OP = {
|
|
7
|
+
"!": 1,
|
|
8
|
+
"!=": 2,
|
|
9
|
+
"!==": 3,
|
|
10
|
+
"%": 4,
|
|
11
|
+
"%=": 5,
|
|
12
|
+
"&": 6,
|
|
13
|
+
"&&": 7,
|
|
14
|
+
"&&=": 8,
|
|
15
|
+
"&=": 9,
|
|
16
|
+
"(": 10,
|
|
17
|
+
"()": 11,
|
|
18
|
+
"*": 12,
|
|
19
|
+
"**": 13,
|
|
20
|
+
"*=": 14,
|
|
21
|
+
"+": 15,
|
|
22
|
+
"++": 16,
|
|
23
|
+
"+=": 17,
|
|
24
|
+
",": 18,
|
|
25
|
+
"-": 19,
|
|
26
|
+
"--": 20,
|
|
27
|
+
"-=": 21,
|
|
28
|
+
".": 22,
|
|
29
|
+
"...": 23,
|
|
30
|
+
"/": 24,
|
|
31
|
+
"//": 25,
|
|
32
|
+
"/=": 26,
|
|
33
|
+
";": 27,
|
|
34
|
+
"<": 28,
|
|
35
|
+
"<<": 29,
|
|
36
|
+
"<<=": 30,
|
|
37
|
+
"<=": 31,
|
|
38
|
+
"=": 32,
|
|
39
|
+
"==": 33,
|
|
40
|
+
"===": 34,
|
|
41
|
+
"=>": 35,
|
|
42
|
+
">": 36,
|
|
43
|
+
">=": 37,
|
|
44
|
+
">>": 38,
|
|
45
|
+
">>=": 39,
|
|
46
|
+
">>>": 40,
|
|
47
|
+
">>>=": 41,
|
|
48
|
+
"?": 42,
|
|
49
|
+
"?:": 43,
|
|
50
|
+
"??": 44,
|
|
51
|
+
"??=": 45,
|
|
52
|
+
"[": 46,
|
|
53
|
+
"[]": 47,
|
|
54
|
+
"^": 48,
|
|
55
|
+
"^=": 49,
|
|
56
|
+
"async": 50,
|
|
57
|
+
"await": 51,
|
|
58
|
+
"bigint": 52,
|
|
59
|
+
"block": 53,
|
|
60
|
+
"bool": 54,
|
|
61
|
+
"break": 55,
|
|
62
|
+
"call": 56,
|
|
63
|
+
"catch": 57,
|
|
64
|
+
"const": 58,
|
|
65
|
+
"continue": 59,
|
|
66
|
+
"default": 60,
|
|
67
|
+
"delete": 61,
|
|
68
|
+
"export": 62,
|
|
69
|
+
"finally": 63,
|
|
70
|
+
"for": 64,
|
|
71
|
+
"if": 65,
|
|
72
|
+
"import": 66,
|
|
73
|
+
"in": 67,
|
|
74
|
+
"instanceof": 68,
|
|
75
|
+
"label": 69,
|
|
76
|
+
"let": 70,
|
|
77
|
+
"nan": 71,
|
|
78
|
+
"new": 72,
|
|
79
|
+
"return": 73,
|
|
80
|
+
"spread": 74,
|
|
81
|
+
"str": 75,
|
|
82
|
+
"strcat": 76,
|
|
83
|
+
"switch": 77,
|
|
84
|
+
"throw": 78,
|
|
85
|
+
"typeof": 79,
|
|
86
|
+
"u+": 80,
|
|
87
|
+
"u-": 81,
|
|
88
|
+
"void": 82,
|
|
89
|
+
"while": 83,
|
|
90
|
+
"yield": 84,
|
|
91
|
+
"{": 85,
|
|
92
|
+
"{}": 86,
|
|
93
|
+
"|": 87,
|
|
94
|
+
"|=": 88,
|
|
95
|
+
"||": 89,
|
|
96
|
+
"||=": 90,
|
|
97
|
+
"~": 91,
|
|
98
|
+
}
|
|
99
|
+
export const OPS = [null, "!", "!=", "!==", "%", "%=", "&", "&&", "&&=", "&=", "(", "()", "*", "**", "*=", "+", "++", "+=", ",", "-", "--", "-=", ".", "...", "/", "//", "/=", ";", "<", "<<", "<<=", "<=", "=", "==", "===", "=>", ">", ">=", ">>", ">>=", ">>>", ">>>=", "?", "?:", "??", "??=", "[", "[]", "^", "^=", "async", "await", "bigint", "block", "bool", "break", "call", "catch", "const", "continue", "default", "delete", "export", "finally", "for", "if", "import", "in", "instanceof", "label", "let", "nan", "new", "return", "spread", "str", "strcat", "switch", "throw", "typeof", "u+", "u-", "void", "while", "yield", "{", "{}", "|", "|=", "||", "||=", "~"]
|
|
100
|
+
export const OP_COUNT = 92
|
|
101
|
+
|
|
102
|
+
// Normalize an op tag to its string form for op-Set membership / switch checks,
|
|
103
|
+
// so `SET.has(opStr(node[0]))` and `switch (opStr(op))` work whether node[0] is
|
|
104
|
+
// still a string (intern off / non-interned op like ':') or an integer (intern
|
|
105
|
+
// on). Self-host-safe: the typeof guard avoids indexing the OPS array by a string
|
|
106
|
+
// (jz arrays trap on non-integer indices), and it never grows a Set.
|
|
107
|
+
export const opStr = (op) => typeof op === "number" ? OPS[op] : op
|
|
108
|
+
|
|
109
|
+
// Convert array node[0] from op-STRING to integer tag, recursively. Once per node at
|
|
110
|
+
// the prepare boundary. Unknown op-strings stay strings (dual-keyed tables handle them
|
|
111
|
+
// until the int-only phase). node[0]===null (numeric literal) stays null; identifiers
|
|
112
|
+
// (bare strings at n[1+]) untouched.
|
|
113
|
+
export const internOps = (n) => {
|
|
114
|
+
if (!Array.isArray(n)) return n
|
|
115
|
+
const t = n[0]
|
|
116
|
+
if (typeof t === "string") { const id = OP[t]; if (id !== undefined) n[0] = id }
|
|
117
|
+
for (let i = 1; i < n.length; i++) if (Array.isArray(n[i])) internOps(n[i])
|
|
118
|
+
return n
|
|
119
|
+
}
|