jz 0.5.0 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +281 -142
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +461 -185
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +591 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +600 -205
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -25,18 +25,20 @@
|
|
|
25
25
|
* @module compile
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import
|
|
29
|
-
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT } from '
|
|
28
|
+
import parseWat from 'watr/parse'
|
|
29
|
+
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT, declGlobal } from '../ctx.js'
|
|
30
|
+
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR } from '../ast.js'
|
|
31
|
+
import { intLiteralValue } from '../static.js'
|
|
30
32
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
boxedCaptures, updateRep,
|
|
34
|
-
isBlockBody, analyzeStructInline,
|
|
33
|
+
analyzeBody, unboxablePtrs, cseSafeLoadBases, boxedCaptures,
|
|
34
|
+
analyzeStructInline, invalidateLocalsCache,
|
|
35
35
|
} from './analyze.js'
|
|
36
|
+
import { typedElemAux } from '../../layout.js'
|
|
37
|
+
import { VAL, updateRep, REP_FIELDS } from '../reps.js'
|
|
36
38
|
import { inferLocals } from './infer.js'
|
|
37
|
-
import { optimizeFunc, treeshake } from '
|
|
38
|
-
import { emit, emitter,
|
|
39
|
-
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '
|
|
39
|
+
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
40
|
+
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
41
|
+
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
40
42
|
import {
|
|
41
43
|
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
42
44
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
@@ -49,13 +51,13 @@ import {
|
|
|
49
51
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
50
52
|
valKindToPtr, findBodyStart, tcoTailRewrite,
|
|
51
53
|
boolBoxIR,
|
|
52
|
-
I32_MIN, I32_MAX,
|
|
53
|
-
} from '
|
|
54
|
-
import plan from './plan.js'
|
|
54
|
+
I32_MIN, I32_MAX, dollar,
|
|
55
|
+
} from '../ir.js'
|
|
56
|
+
import plan from './plan/index.js'
|
|
55
57
|
import {
|
|
56
58
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
57
59
|
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
58
|
-
} from '
|
|
60
|
+
} from '../wat/assemble.js'
|
|
59
61
|
|
|
60
62
|
// =============================================================================
|
|
61
63
|
// Single-source export semantics
|
|
@@ -94,17 +96,18 @@ const isExported = f => {
|
|
|
94
96
|
return false
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
/**
|
|
98
|
-
* per-export ABI metadata in custom sections — one entry per
|
|
99
|
-
* since the host (interop.js wrap) keys by export name. */
|
|
100
|
-
function
|
|
99
|
+
/** Collect JS-visible export names that resolve to `funcName` (as an array).
|
|
100
|
+
* Used to emit per-export ABI metadata in custom sections — one entry per
|
|
101
|
+
* JS-visible name, since the host (interop.js wrap) keys by export name. */
|
|
102
|
+
function exportNamesOf(funcName) {
|
|
103
|
+
const names = []
|
|
101
104
|
for (const [key, val] of Object.entries(ctx.func.exports)) {
|
|
102
|
-
if (val === true && key === funcName)
|
|
103
|
-
else if (val === funcName) yield key
|
|
105
|
+
if ((val === true && key === funcName) || val === funcName) names.push(key)
|
|
104
106
|
}
|
|
107
|
+
return names
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
110
|
+
const timePhase = (profiler, name, fn) => profiler?.time ? profiler.time(name, fn) : fn()
|
|
108
111
|
|
|
109
112
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
110
113
|
// populated at compile() entry. Both reset by ctx.js reset() and re-filled here.
|
|
@@ -113,8 +116,7 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
113
116
|
// emit-calling ones (toBool, emitTypeofCmp, emitDecl, materializeMulti,
|
|
114
117
|
// buildArrayWithSpreads) moved to src/emit.js.
|
|
115
118
|
|
|
116
|
-
// AST-analysis primitives
|
|
117
|
-
// infer* cross-call inference, collectProgramFacts) moved to src/analyze.js.
|
|
119
|
+
// AST-analysis primitives live in kind.js, type.js, static.js, program-facts.js.
|
|
118
120
|
|
|
119
121
|
/**
|
|
120
122
|
* Boundary-wrap predicate: exports whose body-driven result OR any param narrowed
|
|
@@ -133,9 +135,62 @@ const isBoundaryWrapped = (func) => {
|
|
|
133
135
|
// A boolean result rides the 0/1 number carrier internally; the export thunk
|
|
134
136
|
// boxes it to the TRUE_NAN/FALSE_NAN atom so the host sees a real boolean.
|
|
135
137
|
if (func.valResult === VAL.BOOL) return true
|
|
138
|
+
// A bigint result rides the i64-reinterpreted-f64 carrier internally; the export thunk converts
|
|
139
|
+
// it to a real Number so a JS host doesn't see raw i64 bits (`() => 100n` was returning 4.94e-322).
|
|
140
|
+
if (func.valResult === VAL.BIGINT) return true
|
|
136
141
|
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null)
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
// Static-string intern index (the `internStrings` pass). Open-addressing table
|
|
145
|
+
// over the deduped static string literals (5–32 bytes): [hash u32][ptr u32]
|
|
146
|
+
// pairs appended to the data segment, FNV-1a matching __str_hash's heap branch.
|
|
147
|
+
// __str_slice/__str_slice_view probe it so a runtime substring whose content
|
|
148
|
+
// equals any source literal returns the CANONICAL static pointer — string
|
|
149
|
+
// equality then short-circuits on bit-eq instead of walking bytes (a compiler
|
|
150
|
+
// or parser compares each token against tag literals many times; ~25% of
|
|
151
|
+
// self-host compile time was __str_eq/__eq/__str_hash volume). Built before
|
|
152
|
+
// pullStdlib (the slice thunks emit the probe only when `__internBase` exists);
|
|
153
|
+
// stripStaticDataPrefix shifts the stored ptr slots like every other static
|
|
154
|
+
// reference. Misses cost one FNV + one probe per slice; the table is read-only.
|
|
155
|
+
function buildInternTable() {
|
|
156
|
+
const cfg = ctx.transform.optimize
|
|
157
|
+
if (!cfg || cfg.internStrings === false) return
|
|
158
|
+
if (ctx.memory.shared || !ctx.runtime.dataDedup?.size) return
|
|
159
|
+
const enc = new TextEncoder()
|
|
160
|
+
const entries = []
|
|
161
|
+
for (const [str, off] of ctx.runtime.dataDedup) {
|
|
162
|
+
const b = enc.encode(str)
|
|
163
|
+
if (b.length < 5 || b.length > 32) continue
|
|
164
|
+
let h = 0x811c9dc5 | 0
|
|
165
|
+
for (let i = 0; i < b.length; i++) h = Math.imul(h ^ b[i], 0x01000193) | 0
|
|
166
|
+
if (h <= 1) h = (h + 2) | 0 // mirror __str_hash's empty/tombstone clamp
|
|
167
|
+
entries.push([h >>> 0, off + 8])
|
|
168
|
+
}
|
|
169
|
+
if (!entries.length) return
|
|
170
|
+
let size = 4
|
|
171
|
+
while (size < entries.length * 2) size = (size * 2) | 0
|
|
172
|
+
const mask = size - 1
|
|
173
|
+
const slots = new Uint32Array(size * 2)
|
|
174
|
+
for (let e = 0; e < entries.length; e++) {
|
|
175
|
+
const h = entries[e][0], off = entries[e][1]
|
|
176
|
+
let i = h & mask
|
|
177
|
+
while (slots[i * 2 + 1] !== 0) i = (i + 1) & mask
|
|
178
|
+
slots[i * 2] = h
|
|
179
|
+
slots[i * 2 + 1] = off
|
|
180
|
+
}
|
|
181
|
+
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
182
|
+
const base = ctx.runtime.data.length
|
|
183
|
+
let s = ''
|
|
184
|
+
for (let i = 0; i < slots.length; i++) {
|
|
185
|
+
const v = slots[i]
|
|
186
|
+
s += String.fromCharCode(v & 0xFF, (v >>> 8) & 0xFF, (v >>> 16) & 0xFF, (v >>> 24) & 0xFF)
|
|
187
|
+
}
|
|
188
|
+
ctx.runtime.data += s
|
|
189
|
+
ctx.runtime.internTable = { base, size }
|
|
190
|
+
declGlobal('__internBase', 'i32', base, { mut: false })
|
|
191
|
+
declGlobal('__internMask', 'i32', mask, { mut: false })
|
|
192
|
+
}
|
|
193
|
+
|
|
139
194
|
const ensureThrowRuntime = (sec) => {
|
|
140
195
|
// A pulled stdlib helper may throw $__jz_err even when no user `throw` set the
|
|
141
196
|
// flag (e.g. __to_num on a Symbol). Detect it from the included stdlib bodies
|
|
@@ -147,7 +202,7 @@ const ensureThrowRuntime = (sec) => {
|
|
|
147
202
|
if (!ctx.runtime.throws) return
|
|
148
203
|
|
|
149
204
|
if (!ctx.scope.globals.has('__jz_last_err_bits'))
|
|
150
|
-
|
|
205
|
+
declGlobal('__jz_last_err_bits', 'i64')
|
|
151
206
|
if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'tag' && t[1] === '$__jz_err'))
|
|
152
207
|
sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
|
|
153
208
|
if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))
|
|
@@ -181,13 +236,12 @@ const pruneUnusedThrowRuntime = (sec) => {
|
|
|
181
236
|
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
182
237
|
|
|
183
238
|
/** Serialize a ValueRep entry into a plain object for inspect output.
|
|
184
|
-
* Omits undefined fields so consumers can JSON-stringify without noise.
|
|
239
|
+
* Omits undefined fields so consumers can JSON-stringify without noise.
|
|
240
|
+
* Iterates REP_FIELDS (the closed shape in reps.js) so it can't drift. */
|
|
185
241
|
const repView = (rep) => {
|
|
186
242
|
if (!rep) return null
|
|
187
243
|
const out = {}
|
|
188
|
-
for (const k of
|
|
189
|
-
if (rep[k] != null) out[k] = rep[k]
|
|
190
|
-
}
|
|
244
|
+
for (const k of REP_FIELDS) if (rep[k] != null) out[k] = rep[k]
|
|
191
245
|
return Object.keys(out).length ? out : null
|
|
192
246
|
}
|
|
193
247
|
|
|
@@ -230,18 +284,79 @@ function captureFuncInspect(func, facts, programFacts) {
|
|
|
230
284
|
}
|
|
231
285
|
}
|
|
232
286
|
|
|
287
|
+
function scanAndTagNonEscapingClosures(body) {
|
|
288
|
+
if (!body) return
|
|
289
|
+
const onlyCalledNotReferenced = (node, name) => {
|
|
290
|
+
if (typeof node === 'string') return node !== name
|
|
291
|
+
if (!Array.isArray(node)) return true
|
|
292
|
+
const op = node[0]
|
|
293
|
+
if (op === 'str') return true
|
|
294
|
+
if (op === '=>') {
|
|
295
|
+
return !refsName(node[1], name, REFS_IN_EXPR) && !refsName(node[2], name, REFS_IN_EXPR)
|
|
296
|
+
}
|
|
297
|
+
if (op === '=' && node[1] === name) {
|
|
298
|
+
return onlyCalledNotReferenced(node[2], name)
|
|
299
|
+
}
|
|
300
|
+
if (op === '()' && node[1] === name) {
|
|
301
|
+
for (let i = 2; i < node.length; i++) {
|
|
302
|
+
if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
303
|
+
}
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
|
|
307
|
+
if (op === ':') return onlyCalledNotReferenced(node[2], name)
|
|
308
|
+
for (let i = 1; i < node.length; i++) {
|
|
309
|
+
if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
310
|
+
}
|
|
311
|
+
return true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const walk = (node) => {
|
|
315
|
+
if (!Array.isArray(node)) return
|
|
316
|
+
const op = node[0]
|
|
317
|
+
if (op === 'let' || op === 'const') {
|
|
318
|
+
for (const decl of node.slice(1)) {
|
|
319
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string') {
|
|
320
|
+
const name = decl[1]
|
|
321
|
+
const init = decl[2]
|
|
322
|
+
if (Array.isArray(init) && init[0] === '=>') {
|
|
323
|
+
const arrow_body = init[2]
|
|
324
|
+
if (arrow_body && typeof arrow_body === 'object' && !ctx.func.boxed?.has(name) && !isGlobal(name) && !isReassigned(body, name) && onlyCalledNotReferenced(body, name)) {
|
|
325
|
+
arrow_body._nonEscaping = name
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} else if (op === '=' && typeof node[1] === 'string' && Array.isArray(node[2]) && node[2][0] === '=>') {
|
|
331
|
+
const name = node[1]
|
|
332
|
+
const init = node[2]
|
|
333
|
+
const arrow_body = init[2]
|
|
334
|
+
if (arrow_body && typeof arrow_body === 'object' && !ctx.func.boxed?.has(name) && !isGlobal(name) && !isReassigned(body, name) && onlyCalledNotReferenced(body, name)) {
|
|
335
|
+
arrow_body._nonEscaping = name
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
339
|
+
}
|
|
340
|
+
walk(body)
|
|
341
|
+
}
|
|
342
|
+
|
|
233
343
|
// Reset per-function emit-frame state — the single source of frame entry.
|
|
234
344
|
// `emitFunc`, `analyzeFuncForEmit`, and `emitClosureBody` all route through
|
|
235
345
|
// here. Top-level funcs start `uniq` at 0; closures pass a higher base so
|
|
236
346
|
// their synthetic labels can't collide with the parent frame's.
|
|
237
347
|
function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
238
348
|
ctx.func.stack = []
|
|
349
|
+
ctx.func.maybeNullish = new Set() // bindings assigned a nullish literal → coerce in arithmetic (null-flow)
|
|
350
|
+
ctx.func.pendingLabel = null // label awaiting its loop, for `continue <label>`
|
|
239
351
|
ctx.func.uniq = uniq
|
|
240
352
|
ctx.func.current = sig
|
|
241
353
|
ctx.func.body = body
|
|
242
354
|
ctx.func.directClosures = directClosures
|
|
243
355
|
ctx.func.localProps = null
|
|
244
356
|
ctx.func.charDecomp = null
|
|
357
|
+
if (ctx.transform.optimize) {
|
|
358
|
+
scanAndTagNonEscapingClosures(body)
|
|
359
|
+
}
|
|
245
360
|
}
|
|
246
361
|
|
|
247
362
|
// Allocate + null-init a heap cell for every boxed local that isn't seeded
|
|
@@ -288,6 +403,27 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
288
403
|
if (r.intConst != null) updateRep(pname, { intConst: r.intConst })
|
|
289
404
|
}
|
|
290
405
|
}
|
|
406
|
+
// Trust numeric export params. An exported f64 param used only in numeric
|
|
407
|
+
// positions is marked VAL.NUMBER so its uses skip the `__to_num` coercion
|
|
408
|
+
// entirely (not just hoist it). External callers reach jz through interop's
|
|
409
|
+
// `mem.wrapVal`, which passes a JS number straight to f64 — so the coercion
|
|
410
|
+
// only ever fired for a *string* arg to a numeric param (a type misuse). When
|
|
411
|
+
// that lone coercion is the only `__to_num` consumer, dropping it lets the whole
|
|
412
|
+
// ToNumber string-parse dep tree (`__to_str`→`__itoa`/`__toExp`/`__mkstr`/…)
|
|
413
|
+
// treeshake away — a ~4× module shrink that, decisively, lets V8 tier the hot
|
|
414
|
+
// fill loop up properly (the bloated module JITs the *identical* loop ~2× slower).
|
|
415
|
+
if (func.exported && block) {
|
|
416
|
+
for (const p of sig.params) {
|
|
417
|
+
if (p.type === 'f64' && p.ptrKind == null && !p.jsstring
|
|
418
|
+
&& !func.defaults?.[p.name] && !ctx.func.boxed?.has(p.name)
|
|
419
|
+
&& !ctx.func.localReps?.get(p.name)?.val
|
|
420
|
+
&& paramAllUsesNumeric(body, p.name))
|
|
421
|
+
updateRep(p.name, { val: VAL.NUMBER })
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (block) {
|
|
425
|
+
seedLocalIntConsts(body)
|
|
426
|
+
}
|
|
291
427
|
// Drop any earlier-cached analyzeBody.locals slice for this body —
|
|
292
428
|
// narrowSignatures called it before our pre-seed, when params still had no
|
|
293
429
|
// inferred VAL.TYPED, so the cached widths reflect the pre-narrow state.
|
|
@@ -295,6 +431,9 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
295
431
|
invalidateLocalsCache(body)
|
|
296
432
|
const bodyFacts = block ? analyzeBody(body) : null
|
|
297
433
|
ctx.func.locals = bodyFacts ? bodyFacts.locals : new Map()
|
|
434
|
+
if (bodyFacts?.valTypes) {
|
|
435
|
+
for (const [name, vt] of bodyFacts.valTypes) updateRep(name, { val: vt })
|
|
436
|
+
}
|
|
298
437
|
// Proven uint32 accumulator locals — readVar tags reads `.unsigned` so the
|
|
299
438
|
// f64 round-trip widens with convert_i32_u (not _s).
|
|
300
439
|
if (bodyFacts?.unsignedLocals) for (const n of bodyFacts.unsignedLocals) updateRep(n, { unsigned: true })
|
|
@@ -351,6 +490,9 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
351
490
|
if (p.ptrAux != null) fields.ptrAux = p.ptrAux
|
|
352
491
|
updateRep(p.name, fields)
|
|
353
492
|
}
|
|
493
|
+
for (const p of sig.params) {
|
|
494
|
+
if (p.jsstring) updateRep(p.name, { carrier: 'jsstring', val: VAL.STRING })
|
|
495
|
+
}
|
|
354
496
|
|
|
355
497
|
// CSE-safe load bases — pointer locals whose memory reads `cseScalarLoad`
|
|
356
498
|
// may scalar-replace. Computed last: needs every `let`/param ptrKind in place.
|
|
@@ -358,10 +500,23 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
358
500
|
? cseSafeLoadBases(body, ctx.func.locals, ctx.func.localReps)
|
|
359
501
|
: new Set()
|
|
360
502
|
|
|
503
|
+
// Closure-capture narrowing: a boxed var whose every defining RHS — owner
|
|
504
|
+
// body AND nested arrows, the narrower's intCertain contract — is integer-
|
|
505
|
+
// valued keeps its CELL in i32, so readVar/writeVar skip the f64↔i32
|
|
506
|
+
// round-trip per access. Params are excluded: their cell is seeded from the
|
|
507
|
+
// raw f64 param value, which would desync an i32-read cell. Same asm.js-style
|
|
508
|
+
// range contract as plain intCertain locals.
|
|
509
|
+
const cellTypes = new Set()
|
|
510
|
+
for (const name of ctx.func.boxed.keys()) {
|
|
511
|
+
if (sig.params.some(p => p.name === name)) continue
|
|
512
|
+
if (ctx.func.localReps?.get(name)?.intCertain === true) cellTypes.add(name)
|
|
513
|
+
}
|
|
514
|
+
|
|
361
515
|
return {
|
|
362
516
|
block,
|
|
363
517
|
locals: new Map(ctx.func.locals),
|
|
364
518
|
boxed: new Map(ctx.func.boxed),
|
|
519
|
+
cellTypes,
|
|
365
520
|
flatObjects: new Map(ctx.func.flatObjects),
|
|
366
521
|
sliceViews: new Set(ctx.func.sliceViews),
|
|
367
522
|
cseLoadBases,
|
|
@@ -370,6 +525,156 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
370
525
|
}
|
|
371
526
|
}
|
|
372
527
|
|
|
528
|
+
function seedLocalIntConsts(body) {
|
|
529
|
+
const walk = (node) => {
|
|
530
|
+
if (!Array.isArray(node)) return
|
|
531
|
+
const [op, ...args] = node
|
|
532
|
+
if (op === '=>') return
|
|
533
|
+
if (op === 'let' || op === 'const') {
|
|
534
|
+
for (const decl of args) {
|
|
535
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
536
|
+
const value = intLiteralValue(decl[2])
|
|
537
|
+
if (value != null && !isReassigned(body, decl[1])) updateRep(decl[1], { intConst: value })
|
|
538
|
+
}
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
for (const arg of args) walk(arg)
|
|
542
|
+
}
|
|
543
|
+
walk(body)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── Loop-invariant exported-param coercion hoist ────────────────────────────
|
|
547
|
+
//
|
|
548
|
+
// An exported numeric param arrives as a NaN-box (jz's value ABI), so each use
|
|
549
|
+
// in an arithmetic context emits `__to_num(p)`. When the param is never
|
|
550
|
+
// reassigned and *every* use is an unconditional-ToNumber arithmetic operand,
|
|
551
|
+
// the coercion is loop-invariant: do it once at entry and let every use read the
|
|
552
|
+
// already-unboxed f64. This flips a serial recurrence like the de Jong attractor
|
|
553
|
+
// (4 `__to_num`/iter × millions) from ~parity to a clear win over V8.
|
|
554
|
+
//
|
|
555
|
+
// Self-gating: the rewrite only fires when the emitted body ALREADY contains
|
|
556
|
+
// `__to_num(p)` calls — meaning the helper is loaded for other reasons (global
|
|
557
|
+
// typed-array assigns, strings, …). A provably-numeric program (`(a,b)=>a*b`)
|
|
558
|
+
// never loads the helper, has no pattern to match, and is left byte-for-byte
|
|
559
|
+
// alone, preserving the minimal-bundle / golden-size guarantee.
|
|
560
|
+
|
|
561
|
+
// `=`/`+=`/`++`/… targets — reassigning the param breaks the coerce-once premise.
|
|
562
|
+
const PARAM_REASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=',
|
|
563
|
+
'^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=', '++', '--'])
|
|
564
|
+
// Binary ops that unconditionally ToNumber BOTH operands, so a bare param operand
|
|
565
|
+
// is a pure numeric use. `+` is excluded (may concatenate); comparisons / `===`
|
|
566
|
+
// are excluded (they branch on type, never coerce a string operand to number).
|
|
567
|
+
const NUM_BIN_OPS = new Set(['*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'])
|
|
568
|
+
|
|
569
|
+
/** True iff every use of param `name` in `body` is an unconditional-numeric
|
|
570
|
+
* operand, so coercing it to a number once at entry is observationally exact.
|
|
571
|
+
* Rejects conservatively: reassignment and any appearance outside a numeric
|
|
572
|
+
* operator (member/index/call-arg/return/compare/concat). Two transparencies:
|
|
573
|
+
* - copy aliases: `let x = name` makes `x` carry the same value, so `x`'s uses
|
|
574
|
+
* must be numeric too (fixpoint-collected). Catches `let T = t` then `…T…`.
|
|
575
|
+
* - captured closures: a non-shadowing inner arrow captures the binding by
|
|
576
|
+
* reference, so its body's uses count — we recurse instead of rejecting.
|
|
577
|
+
* Catches floatbeat helpers `let s=(f)=>…t…` that read the param numerically. */
|
|
578
|
+
function paramAllUsesNumeric(body, name) {
|
|
579
|
+
if (body == null) return false
|
|
580
|
+
// Fixpoint-collect copy aliases: `let/const x = <name-or-alias>`.
|
|
581
|
+
const names = new Set([name])
|
|
582
|
+
for (let grew = true; grew;) {
|
|
583
|
+
grew = false
|
|
584
|
+
const collect = (node) => {
|
|
585
|
+
if (!Array.isArray(node)) return
|
|
586
|
+
if ((node[0] === 'let' || node[0] === 'const') && node.length === 2
|
|
587
|
+
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string'
|
|
588
|
+
&& typeof node[1][2] === 'string' && names.has(node[1][2]) && !names.has(node[1][1])) {
|
|
589
|
+
names.add(node[1][1]); grew = true
|
|
590
|
+
}
|
|
591
|
+
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
592
|
+
}
|
|
593
|
+
collect(body)
|
|
594
|
+
}
|
|
595
|
+
let ok = true
|
|
596
|
+
const walk = (node) => {
|
|
597
|
+
if (!ok) return
|
|
598
|
+
if (typeof node === 'string') { if (names.has(node)) ok = false; return } // bare use → reject
|
|
599
|
+
if (!Array.isArray(node)) return
|
|
600
|
+
const op = node[0]
|
|
601
|
+
// single `let/const x = init`: x is a binding (not a use). A pure copy of an
|
|
602
|
+
// alias is consumed (already in `names`); otherwise the init must be numeric.
|
|
603
|
+
if ((op === 'let' || op === 'const') && node.length === 2
|
|
604
|
+
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string') {
|
|
605
|
+
const init = node[1][2]
|
|
606
|
+
if (typeof init === 'string' && names.has(init)) return
|
|
607
|
+
walk(init)
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
if (op === '=>') { // closure capture: recurse unless shadowed
|
|
611
|
+
const ps = node[1]
|
|
612
|
+
const shadowed = Array.isArray(ps)
|
|
613
|
+
? ps.some(p => names.has(p) || (Array.isArray(p) && names.has(p[1])))
|
|
614
|
+
: names.has(ps)
|
|
615
|
+
if (!shadowed) { walk(node[1]); walk(node[2]) } // defaults + body; param names aren't in `names`
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
if (PARAM_REASSIGN_OPS.has(op) && names.has(node[1])) { ok = false; return }
|
|
619
|
+
if (NUM_BIN_OPS.has(op) && node.length === 3) { // numeric binary: operands are ToNumber'd
|
|
620
|
+
if (!names.has(node[1])) walk(node[1])
|
|
621
|
+
if (!names.has(node[2])) walk(node[2])
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
if (op === '-' && node.length === 2) { if (!names.has(node[1])) walk(node[1]); return } // unary negate
|
|
625
|
+
if (op === '-' && node.length === 3) { if (!names.has(node[1])) walk(node[1]); if (!names.has(node[2])) walk(node[2]); return }
|
|
626
|
+
// `u-`/`u+` are the normalized unary minus/plus (prepare rewrites `-x`/`+x`); both ToNumber.
|
|
627
|
+
if ((op === 'u-' || op === 'u+') && node.length === 2) { if (!names.has(node[1])) walk(node[1]); return }
|
|
628
|
+
if (op === '+' && node.length === 2) { if (!names.has(node[1])) walk(node[1]); return } // unary + = ToNumber
|
|
629
|
+
if (op === '~' && node.length === 2) { if (!names.has(node[1])) walk(node[1]); return }
|
|
630
|
+
for (let i = 1; i < node.length; i++) walk(node[i]) // bare param reaching here → rejected above
|
|
631
|
+
}
|
|
632
|
+
walk(body)
|
|
633
|
+
return ok
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Hoist each eligible param's `__to_num` coercion to a single entry `local.set`,
|
|
637
|
+
* rewriting per-use calls in `stmts` to a bare typed `local.get`. Mutates
|
|
638
|
+
* `stmts` in place; returns the prologue inits to splice ahead of the body.
|
|
639
|
+
* Only fires for params whose coercion appears inside a loop (or ≥2×) — a lone
|
|
640
|
+
* straight-line coercion isn't worth the rebind. */
|
|
641
|
+
function hoistInvariantParamCoercions(stmts, func) {
|
|
642
|
+
const inits = []
|
|
643
|
+
const defaults = func.defaults || {}
|
|
644
|
+
for (const p of func.sig.params) {
|
|
645
|
+
if (p.type !== 'f64' || p.ptrKind != null || p.jsstring) continue
|
|
646
|
+
if (ctx.func.boxed?.has(p.name)) continue
|
|
647
|
+
if (p.name in defaults) continue
|
|
648
|
+
if (!paramAllUsesNumeric(func.body, p.name)) continue
|
|
649
|
+
const pat = (n) => Array.isArray(n) && n[0] === 'call' && n[1] === '$__to_num'
|
|
650
|
+
&& Array.isArray(n[2]) && n[2][0] === 'i64.reinterpret_f64'
|
|
651
|
+
&& Array.isArray(n[2][1]) && n[2][1][0] === 'local.get' && n[2][1][1] === `$${p.name}`
|
|
652
|
+
let total = 0, inLoop = 0
|
|
653
|
+
const count = (node, depth) => {
|
|
654
|
+
if (!Array.isArray(node)) return
|
|
655
|
+
const d = node[0] === 'loop' ? depth + 1 : depth
|
|
656
|
+
for (let i = 1; i < node.length; i++) {
|
|
657
|
+
if (pat(node[i])) { total++; if (d > 0) inLoop++ }
|
|
658
|
+
else count(node[i], d)
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const s of stmts) count(s, 0)
|
|
662
|
+
if (total === 0 || (inLoop === 0 && total < 2)) continue
|
|
663
|
+
const strip = (node) => {
|
|
664
|
+
if (!Array.isArray(node)) return
|
|
665
|
+
for (let i = 1; i < node.length; i++) {
|
|
666
|
+
if (pat(node[i])) node[i] = typed(['local.get', `$${p.name}`], 'f64')
|
|
667
|
+
else strip(node[i])
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const s of stmts) strip(s)
|
|
671
|
+
inits.push(['local.set', `$${p.name}`,
|
|
672
|
+
typed(['call', '$__to_num', ['i64.reinterpret_f64', typed(['local.get', `$${p.name}`], 'f64')]], 'f64')])
|
|
673
|
+
inc('__to_num')
|
|
674
|
+
}
|
|
675
|
+
return inits
|
|
676
|
+
}
|
|
677
|
+
|
|
373
678
|
/**
|
|
374
679
|
* Phase: emit one user function to WAT IR.
|
|
375
680
|
*
|
|
@@ -390,6 +695,7 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
390
695
|
const block = funcFacts.block
|
|
391
696
|
ctx.func.locals = new Map(funcFacts.locals)
|
|
392
697
|
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
698
|
+
ctx.func.cellTypes = new Set(funcFacts.cellTypes)
|
|
393
699
|
ctx.func.flatObjects = new Map(funcFacts.flatObjects)
|
|
394
700
|
ctx.func.sliceViews = new Set(funcFacts.sliceViews)
|
|
395
701
|
ctx.func.localReps = cloneRepMap(funcFacts.localReps)
|
|
@@ -431,7 +737,7 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
431
737
|
// Boundary-wrapped exports also defer the attribute to the synthesized
|
|
432
738
|
// wrapper ($${name}$exp) that reboxes the narrowed result back to f64.
|
|
433
739
|
if (exported && !isBoundaryWrapped(func)) fn.push(['export', `"${name}"`])
|
|
434
|
-
fn.push(...sig.params.map(p => ['param',
|
|
740
|
+
fn.push(...sig.params.map(p => ['param', dollar(p.name), p.type]))
|
|
435
741
|
fn.push(...sig.results.map(t => ['result', t]))
|
|
436
742
|
|
|
437
743
|
// Default params: ES spec says default applies only when arg is `undefined`
|
|
@@ -497,9 +803,11 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
497
803
|
}
|
|
498
804
|
|
|
499
805
|
if (block) {
|
|
500
|
-
const stmts =
|
|
806
|
+
const stmts = emitBlockBody(body)
|
|
807
|
+
// Hoist loop-invariant `__to_num(param)` coercions to a single entry rebind.
|
|
808
|
+
const numCoerceInits = hoistInvariantParamCoercions(stmts, func)
|
|
501
809
|
const paramInits = collectParamInits()
|
|
502
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
810
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
503
811
|
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
504
812
|
const lastStmt = stmts.at(-1)
|
|
505
813
|
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
@@ -510,16 +818,16 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
510
818
|
const fallthrough = endsWithReturn ? []
|
|
511
819
|
: sig.results.length === 1 && sig.results[0] === 'f64' ? [undefExpr()]
|
|
512
820
|
: sig.results.map(t => [`${t}.const`, 0])
|
|
513
|
-
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...stmts, ...fallthrough)
|
|
821
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...numCoerceInits, ...stmts, ...fallthrough)
|
|
514
822
|
} else if (multi && body[0] === '[') {
|
|
515
823
|
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
516
824
|
const paramInits = collectParamInits()
|
|
517
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
825
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
518
826
|
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
519
827
|
} else {
|
|
520
828
|
const ir = emit(body)
|
|
521
829
|
const paramInits = collectParamInits()
|
|
522
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
830
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
523
831
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
524
832
|
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
525
833
|
}
|
|
@@ -560,17 +868,17 @@ function synthesizeBoundaryWrappers() {
|
|
|
560
868
|
for (const func of ctx.func.list) {
|
|
561
869
|
if (!isBoundaryWrapped(func)) continue
|
|
562
870
|
const { name, sig } = func
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
871
|
+
// Quiet NaN-box ABI: every boundary value is f64. A number is a plain f64; a
|
|
872
|
+
// tagged value (heap pointer, null/undef/bool atom) is an f64 whose quiet-NaN
|
|
873
|
+
// (0x7FF8…) payload carries the tag. Quiet-NaN payloads are preserved across the
|
|
874
|
+
// JS↔wasm call boundary by every real engine (and non-JS hosts don't canonicalize
|
|
875
|
+
// at all), so no i64 carrier is needed — the wasm signature is self-describing
|
|
876
|
+
// (f64 everywhere) and a consumer discriminates a tagged value by the NaN prefix.
|
|
877
|
+
// Env requirement: a non-canonicalizing NaN boundary. To support a canonicalizing
|
|
878
|
+
// engine, a per-position i64 carrier would re-enter here (param/result type i64 +
|
|
879
|
+
// `i64.reinterpret_f64`) plus a `jz:i64exp` section for interop.js.
|
|
572
880
|
const resultBool = func.valResult === VAL.BOOL && sig.ptrKind == null
|
|
573
|
-
const
|
|
881
|
+
const resultBigint = func.valResult === VAL.BIGINT && sig.ptrKind == null
|
|
574
882
|
// Inline `(export ...)` attribute only when the func decl carried the
|
|
575
883
|
// inline-export keyword (`export function foo`). For re-exports
|
|
576
884
|
// (`function foo; export { foo as bar }`) the `name` is the *internal*
|
|
@@ -580,19 +888,17 @@ function synthesizeBoundaryWrappers() {
|
|
|
580
888
|
const wrapNode = func.exported
|
|
581
889
|
? ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
582
890
|
: ['func', `$${name}$exp`]
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
wrapNode.push(['param', `$${p.name}`,
|
|
891
|
+
// jsstring params flow as externref end-to-end; every other boundary value is f64.
|
|
892
|
+
sig.params.forEach((p) => {
|
|
893
|
+
wrapNode.push(['param', `$${p.name}`, p.jsstring ? 'externref' : 'f64'])
|
|
586
894
|
})
|
|
587
|
-
wrapNode.push(['result',
|
|
588
|
-
const args = sig.params.map((p
|
|
895
|
+
wrapNode.push(['result', resultBigint ? 'i64' : 'f64'])
|
|
896
|
+
const args = sig.params.map((p) => {
|
|
589
897
|
const get = ['local.get', `$${p.name}`]
|
|
590
898
|
// jsstring: externref flows through unchanged — inner func also takes externref.
|
|
591
899
|
if (p.jsstring) return get
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return ['i32.wrap_i64', get]
|
|
595
|
-
}
|
|
900
|
+
// ptrKind param: the f64 NaN-box carries the pointer — extract the i32 offset.
|
|
901
|
+
if (p.ptrKind != null) return ['i32.wrap_i64', ['i64.reinterpret_f64', get]]
|
|
596
902
|
if (p.type === 'f64') return get
|
|
597
903
|
// Numeric narrowing: f64 → i32 truncate
|
|
598
904
|
return ['i32.trunc_sat_f64_s', get]
|
|
@@ -603,17 +909,26 @@ function synthesizeBoundaryWrappers() {
|
|
|
603
909
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
604
910
|
body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
|
|
605
911
|
} else if (resultBool) {
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
912
|
+
// The inner func returns a clean 0/1 boolean carrier — never NaN. The i32
|
|
913
|
+
// carrier already takes truthyIR's identity path; the f64 carrier would
|
|
914
|
+
// otherwise fall through to the full __is_truthy NaN-discrimination, every
|
|
915
|
+
// arm of which is dead for a boolean. Pull the bit out with one f64.ne so
|
|
916
|
+
// boolBoxIR boxes `4|bit` straight into the TRUE_NAN/FALSE_NAN atom.
|
|
917
|
+
const carrier = sig.results[0] === 'i32'
|
|
918
|
+
? typed(callIR, 'i32')
|
|
919
|
+
: typed(['f64.ne', callIR, ['f64.const', 0]], 'i32')
|
|
920
|
+
body = boolBoxIR(carrier)
|
|
921
|
+
} else if (resultBigint) {
|
|
922
|
+
// BigInt rides the i64-reinterpret-f64 carrier internally; expose the raw i64 at the JS
|
|
923
|
+
// boundary so the host receives a real, lossless BigInt (wasm i64 <-> JS BigInt). Internal
|
|
924
|
+
// callers use `$name` (the f64 carrier) untouched; only the `$exp` export result is i64.
|
|
925
|
+
body = ['i64.reinterpret_f64', callIR]
|
|
609
926
|
} else if (sig.results[0] === 'i32') {
|
|
610
927
|
body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
|
|
611
928
|
} else {
|
|
612
929
|
body = callIR
|
|
613
930
|
}
|
|
614
|
-
wrapNode.push(
|
|
615
|
-
func._exportUsesI64 = resultI64 || paramI64.some(Boolean)
|
|
616
|
-
func._exportI64Sig = { params: paramI64, result: resultI64 }
|
|
931
|
+
wrapNode.push(body)
|
|
617
932
|
// Track externref param positions so interop.js can pass JS values
|
|
618
933
|
// raw (skipping `mem.wrapVal`) at those slots. Today this only fires
|
|
619
934
|
// for `jsstring`-tagged params; future externref carriers wire here too.
|
|
@@ -649,6 +964,7 @@ function emitClosureBody(cb) {
|
|
|
649
964
|
ctx.func.localReps = null
|
|
650
965
|
if (cb.intConsts) for (const [name, v] of cb.intConsts) updateRep(name, { intConst: v })
|
|
651
966
|
if (cb.intCertain) for (const name of cb.intCertain) updateRep(name, { intCertain: true })
|
|
967
|
+
if (cb.nullables) for (const name of cb.nullables) updateRep(name, { nullable: true })
|
|
652
968
|
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
653
969
|
if (cb.schemaVars) {
|
|
654
970
|
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
@@ -664,6 +980,9 @@ function emitClosureBody(cb) {
|
|
|
664
980
|
}
|
|
665
981
|
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
666
982
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
983
|
+
// i32-narrowed cells: the owner decided the cell width (funcFacts.cellTypes);
|
|
984
|
+
// every body sharing the cell must read/write it at that width.
|
|
985
|
+
ctx.func.cellTypes = new Set(cb.cellI32 || [])
|
|
667
986
|
const parentBoxedCaptures = new Set(cb.boxed || [])
|
|
668
987
|
ctx.func.preboxed = new Set()
|
|
669
988
|
// Bare `;`-sequence bodies (no enclosing `{}`) reach us when callers built a
|
|
@@ -693,6 +1012,19 @@ function emitClosureBody(cb) {
|
|
|
693
1012
|
|
|
694
1013
|
// Params are locals, assigned directly from inline slots
|
|
695
1014
|
for (const p of cb.params) ctx.func.locals.set(p, 'f64')
|
|
1015
|
+
// Mark params that every direct call site passed a number (seeded by
|
|
1016
|
+
// tryDirectClosureCall) VAL.NUMBER — their body uses then skip the __to_num
|
|
1017
|
+
// coercion. All direct calls were emitted before this body (module end), so the
|
|
1018
|
+
// lattice is complete; a `false`/unobserved slot leaves the param boxed.
|
|
1019
|
+
const ptRow = ctx.closure.paramTypes?.get(cb.name)
|
|
1020
|
+
// A param numeric at every call site is typed NUMBER so its body uses skip __to_num. If some
|
|
1021
|
+
// call omits it (index ≥ minArgc) it can hold UNDEF_NAN, so also flag it nullable — that keeps
|
|
1022
|
+
// the boxing win yet stops `x === undefined` mis-folding to false (it bit-compares instead).
|
|
1023
|
+
const minArgc = ctx.closure.minArgc?.get(cb.name) ?? 0
|
|
1024
|
+
if (ptRow) for (let i = 0; i < cb.params.length; i++) {
|
|
1025
|
+
if (ptRow[i] === true && !ctx.func.localReps?.get(cb.params[i])?.val)
|
|
1026
|
+
updateRep(cb.params[i], i < minArgc ? { val: VAL.NUMBER } : { val: VAL.NUMBER, nullable: true })
|
|
1027
|
+
}
|
|
696
1028
|
|
|
697
1029
|
// Register captured variable locals: boxed = i32 cell pointer, otherwise f64 value
|
|
698
1030
|
for (let i = 0; i < cb.captures.length; i++) {
|
|
@@ -726,7 +1058,7 @@ function emitClosureBody(cb) {
|
|
|
726
1058
|
ctx.func.locals.set(name, 'i32')
|
|
727
1059
|
updateRep(name, fields)
|
|
728
1060
|
}
|
|
729
|
-
bodyIR =
|
|
1061
|
+
bodyIR = emitBlockBody(cb.body)
|
|
730
1062
|
} else {
|
|
731
1063
|
bodyIR = [asF64(emit(cb.body))]
|
|
732
1064
|
}
|
|
@@ -734,13 +1066,15 @@ function emitClosureBody(cb) {
|
|
|
734
1066
|
// Pre-allocate cache locals for env unpacking
|
|
735
1067
|
const envBase = cb.captures.length > 0 ? `${T}envBase${ctx.func.uniq++}` : null
|
|
736
1068
|
if (envBase) ctx.func.locals.set(envBase, 'i32')
|
|
737
|
-
// Rest param: allocate helper locals (len + offset) before emitting decls
|
|
738
|
-
let restOff, restLen
|
|
1069
|
+
// Rest param: allocate helper locals (len + offset + spill loop index) before emitting decls
|
|
1070
|
+
let restOff, restLen, restIdx
|
|
739
1071
|
if (cb.rest) {
|
|
740
1072
|
restOff = `${T}restOff${ctx.func.uniq++}`
|
|
741
1073
|
restLen = `${T}restLen${ctx.func.uniq++}`
|
|
1074
|
+
restIdx = `${T}restIdx${ctx.func.uniq++}`
|
|
742
1075
|
ctx.func.locals.set(restOff, 'i32')
|
|
743
1076
|
ctx.func.locals.set(restLen, 'i32')
|
|
1077
|
+
ctx.func.locals.set(restIdx, 'i32')
|
|
744
1078
|
inc('__alloc_hdr', '__mkptr')
|
|
745
1079
|
}
|
|
746
1080
|
|
|
@@ -777,7 +1111,7 @@ function emitClosureBody(cb) {
|
|
|
777
1111
|
}
|
|
778
1112
|
}
|
|
779
1113
|
|
|
780
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
1114
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
781
1115
|
|
|
782
1116
|
// Load captures from env: boxed → i32.load (raw cell pointer), immutable → f64.load value.
|
|
783
1117
|
// env is the CLOSURE pointer (PTR.CLOSURE) — never an ARRAY, no forwarding chain.
|
|
@@ -814,20 +1148,20 @@ function emitClosureBody(cb) {
|
|
|
814
1148
|
}
|
|
815
1149
|
}
|
|
816
1150
|
|
|
817
|
-
// Rest param: pack
|
|
818
|
-
// len =
|
|
819
|
-
//
|
|
820
|
-
//
|
|
1151
|
+
// Rest param: pack args a[fixedParams..argc-1] into a fresh array.
|
|
1152
|
+
// len = max(argc - fixedParams, 0). The first `restSlots = width - fixedParams`
|
|
1153
|
+
// come from the inline arg slots; any overflow (argc > width, only reachable via a
|
|
1154
|
+
// spread call) is read straight from the caller's full args array, whose offset the
|
|
1155
|
+
// spread path published in $__closure_spill. This gives unbounded variadic arity.
|
|
821
1156
|
if (cb.rest) {
|
|
822
1157
|
const fixedN = fixedParamN
|
|
823
1158
|
const restSlots = W - fixedN
|
|
1159
|
+
declGlobal('__closure_spill', 'i32')
|
|
824
1160
|
fn.push(['local.set', `$${restLen}`,
|
|
825
1161
|
['select',
|
|
826
1162
|
['i32.sub', ['local.get', '$__argc'], ['i32.const', fixedN]],
|
|
827
1163
|
['i32.const', 0],
|
|
828
1164
|
['i32.gt_s', ['local.get', '$__argc'], ['i32.const', fixedN]]]])
|
|
829
|
-
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', restSlots]],
|
|
830
|
-
['then', ['local.set', `$${restLen}`, ['i32.const', restSlots]]]])
|
|
831
1165
|
fn.push(['local.set', `$${restOff}`,
|
|
832
1166
|
['call', '$__alloc_hdr',
|
|
833
1167
|
['local.get', `$${restLen}`], ['local.get', `$${restLen}`]]])
|
|
@@ -837,6 +1171,21 @@ function emitClosureBody(cb) {
|
|
|
837
1171
|
['i32.add', ['local.get', `$${restOff}`], ['i32.const', i * 8]],
|
|
838
1172
|
['local.get', `$__a${fixedN + i}`]]]])
|
|
839
1173
|
}
|
|
1174
|
+
// Overflow beyond the inline slots: copy args[width..argc-1] from the spill array
|
|
1175
|
+
// (set by the spread-call site). rest[i] = spill[(fixedN+i)*8] for i in [restSlots, restLen).
|
|
1176
|
+
const rid = ctx.func.uniq++
|
|
1177
|
+
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', restSlots]],
|
|
1178
|
+
['then',
|
|
1179
|
+
['local.set', `$${restIdx}`, ['i32.const', restSlots]],
|
|
1180
|
+
['block', `$restEnd${rid}`,
|
|
1181
|
+
['loop', `$restLoop${rid}`,
|
|
1182
|
+
['br_if', `$restEnd${rid}`, ['i32.ge_s', ['local.get', `$${restIdx}`], ['local.get', `$${restLen}`]]],
|
|
1183
|
+
['f64.store',
|
|
1184
|
+
['i32.add', ['local.get', `$${restOff}`], ['i32.mul', ['local.get', `$${restIdx}`], ['i32.const', 8]]],
|
|
1185
|
+
['f64.load', ['i32.add', ['global.get', '$__closure_spill'],
|
|
1186
|
+
['i32.mul', ['i32.add', ['local.get', `$${restIdx}`], ['i32.const', fixedN]], ['i32.const', 8]]]]],
|
|
1187
|
+
['local.set', `$${restIdx}`, ['i32.add', ['local.get', `$${restIdx}`], ['i32.const', 1]]],
|
|
1188
|
+
['br', `$restLoop${rid}`]]]]])
|
|
840
1189
|
const restValue = ['call', '$__mkptr', ['i32.const', PTR.ARRAY], ['i32.const', 0], ['local.get', `$${restOff}`]]
|
|
841
1190
|
if (boxedParamNames.has(cb.rest)) {
|
|
842
1191
|
fn.push(
|
|
@@ -866,6 +1215,9 @@ function emitClosureBody(cb) {
|
|
|
866
1215
|
* @returns {Array} Complete WASM module as S-expression
|
|
867
1216
|
*/
|
|
868
1217
|
export default function compile(ast, profiler) {
|
|
1218
|
+
// Contract: callers (jzCompileInner / scripts/self.js compileSelf) must set
|
|
1219
|
+
// ctx.transform.optimize before reaching here — every optimize-gated pass below
|
|
1220
|
+
// reads `cfg && cfg.x === false`, so a null cfg silently runs every pass.
|
|
869
1221
|
// Populate known function names + lookup map on ctx.func for direct call detection
|
|
870
1222
|
ctx.func.names.clear()
|
|
871
1223
|
ctx.func.map.clear()
|
|
@@ -891,7 +1243,7 @@ export default function compile(ast, profiler) {
|
|
|
891
1243
|
|
|
892
1244
|
// Check user globals don't conflict with runtime globals (modules loaded after user decls)
|
|
893
1245
|
for (const name of ctx.scope.userGlobals)
|
|
894
|
-
if (!ctx.scope.globals.get(name)?.
|
|
1246
|
+
if (!(ctx.scope.globals.get(name)?.mut && ctx.scope.globals.get(name)?.type === 'f64'))
|
|
895
1247
|
err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
896
1248
|
|
|
897
1249
|
// Pre-fold const globals: evaluate constant initializers before function compilation
|
|
@@ -903,6 +1255,12 @@ export default function compile(ast, profiler) {
|
|
|
903
1255
|
if (ast) {
|
|
904
1256
|
const evalConst = n => {
|
|
905
1257
|
if (typeof n === 'number') return n
|
|
1258
|
+
// A reference to an already-folded integer const (`const NEW = CALL + 1`):
|
|
1259
|
+
// resolve it from constInts so const-referencing-const initializers fold too.
|
|
1260
|
+
// Without this they stay unfolded → decl defaults to 0 AND emitDecl skips the
|
|
1261
|
+
// (const) runtime init → the binding reads 0 (e.g. subscript's NEW=CALL+1 → the
|
|
1262
|
+
// `new` keyword registers with precedence 0 and never dispatches).
|
|
1263
|
+
if (typeof n === 'string') return ctx.scope.constInts?.get(n) ?? null
|
|
906
1264
|
if (Array.isArray(n) && n[0] == null && typeof n[1] === 'number') return n[1]
|
|
907
1265
|
if (!Array.isArray(n)) return null
|
|
908
1266
|
const [op, a, b] = n
|
|
@@ -922,27 +1280,35 @@ export default function compile(ast, profiler) {
|
|
|
922
1280
|
: Array.isArray(n) && n[0] === 'const' ? [n] : []
|
|
923
1281
|
const stmts = [...topStmts(ast)]
|
|
924
1282
|
for (const mi of ctx.module.moduleInits || []) stmts.push(...topStmts(mi))
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1283
|
+
// Fixpoint: a const may reference one declared later or in another module
|
|
1284
|
+
// (`NEW = CALL + 1`). Each pass folds every now-resolvable initializer (its refs
|
|
1285
|
+
// already in constInts); repeat until none change so order/cross-module refs resolve.
|
|
1286
|
+
const foldedDecls = new Set()
|
|
1287
|
+
let changed = true
|
|
1288
|
+
while (changed) {
|
|
1289
|
+
changed = false
|
|
1290
|
+
for (const s of stmts) {
|
|
1291
|
+
if (!Array.isArray(s) || s[0] !== 'const') continue
|
|
1292
|
+
for (const decl of s.slice(1)) {
|
|
1293
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
1294
|
+
const [, name, init] = decl
|
|
1295
|
+
if (foldedDecls.has(name)) continue
|
|
1296
|
+
if (!ctx.scope.globals.has(name) || !ctx.scope.consts?.has(name)) continue
|
|
1297
|
+
const v = evalConst(init)
|
|
1298
|
+
if (v == null || !isFinite(v)) continue
|
|
1299
|
+
foldedDecls.add(name)
|
|
1300
|
+
changed = true
|
|
1301
|
+
const isInt = Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX
|
|
1302
|
+
declGlobal(name, isInt ? 'i32' : 'f64', v, { mut: false })
|
|
1303
|
+
// Cache integer values for cross-call const-arg propagation: `f(N)` where
|
|
1304
|
+
// `const N = 8` should observe the param as intConst=8.
|
|
1305
|
+
if (isInt) (ctx.scope.constInts ||= new Map()).set(name, v)
|
|
1306
|
+
}
|
|
941
1307
|
}
|
|
942
1308
|
}
|
|
943
1309
|
}
|
|
944
1310
|
|
|
945
|
-
const programFacts = timePhase(profiler, 'plan', () => plan(ast))
|
|
1311
|
+
const programFacts = timePhase(profiler, 'plan', () => plan(ast, profiler))
|
|
946
1312
|
|
|
947
1313
|
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
948
1314
|
// Initialized here (post-plan) so paramReps and schema.list are stable, populated
|
|
@@ -950,28 +1316,30 @@ export default function compile(ast, profiler) {
|
|
|
950
1316
|
if (ctx.transform.inspect) ctx.inspect = { functions: {}, schemas: ctx.schema.list.map(s => s.slice()) }
|
|
951
1317
|
|
|
952
1318
|
const funcFacts = new Map()
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1319
|
+
timePhase(profiler, 'analyzeFuncs', () => {
|
|
1320
|
+
for (const func of ctx.func.list) {
|
|
1321
|
+
if (func.raw) continue
|
|
1322
|
+
const facts = analyzeFuncForEmit(func, programFacts)
|
|
1323
|
+
funcFacts.set(func, facts)
|
|
1324
|
+
captureFuncInspect(func, facts, programFacts)
|
|
1325
|
+
}
|
|
1326
|
+
})
|
|
959
1327
|
// Whole-program SRoA: pick the schemas whose `Array<S>` instances use the
|
|
960
1328
|
// `structInline` carrier. Runs once the per-function reps have settled (they
|
|
961
1329
|
// are codegen truth) and before any function is emitted.
|
|
962
|
-
analyzeStructInline(funcFacts, programFacts)
|
|
963
|
-
const funcs = ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts))
|
|
1330
|
+
timePhase(profiler, 'structInline', () => analyzeStructInline(funcFacts, programFacts))
|
|
1331
|
+
const funcs = timePhase(profiler, 'emitFuncs', () => ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts)))
|
|
964
1332
|
funcs.push(...synthesizeBoundaryWrappers())
|
|
965
1333
|
|
|
966
1334
|
const closureFuncs = []
|
|
967
1335
|
let compiledBodyCount = 0
|
|
968
|
-
const compilePendingClosures = () => {
|
|
1336
|
+
const compilePendingClosures = () => timePhase(profiler, 'emitClosures', () => {
|
|
969
1337
|
const bodies = ctx.closure.bodies || []
|
|
970
1338
|
for (let bodyIndex = compiledBodyCount; bodyIndex < bodies.length; bodyIndex++) {
|
|
971
1339
|
closureFuncs.push(emitClosureBody(bodies[bodyIndex]))
|
|
972
1340
|
}
|
|
973
1341
|
compiledBodyCount = bodies.length
|
|
974
|
-
}
|
|
1342
|
+
})
|
|
975
1343
|
compilePendingClosures()
|
|
976
1344
|
|
|
977
1345
|
// `wasm:js-string` imports — drained from `ctx.core.jsstring`, one
|
|
@@ -1055,7 +1423,15 @@ export default function compile(ast, profiler) {
|
|
|
1055
1423
|
if (ctx.closure.table?.length)
|
|
1056
1424
|
sec.elem.push(['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)])
|
|
1057
1425
|
|
|
1058
|
-
buildStartFn(ast, sec, closureFuncs, compilePendingClosures)
|
|
1426
|
+
timePhase(profiler, 'buildStart', () => buildStartFn(ast, sec, closureFuncs, compilePendingClosures))
|
|
1427
|
+
|
|
1428
|
+
// Host globals (globalThis/process/WebAssembly/…) referenced as values are
|
|
1429
|
+
// recorded in ctx.core.hostGlobals during emit; register them as env imports
|
|
1430
|
+
// now (assembly owns ctx.module.imports). Drained after buildStartFn so a
|
|
1431
|
+
// host global first used in a top-level statement (emitted into __start) is
|
|
1432
|
+
// captured; syncImports below merges them into sec.imports.
|
|
1433
|
+
for (const name of ctx.core.hostGlobals)
|
|
1434
|
+
ctx.module.imports.push(['import', '"env"', `"${name}"`, ['global', `$${name}`, 'i64']])
|
|
1059
1435
|
|
|
1060
1436
|
syncImports(sec)
|
|
1061
1437
|
|
|
@@ -1063,16 +1439,22 @@ export default function compile(ast, profiler) {
|
|
|
1063
1439
|
|
|
1064
1440
|
finalizeClosureTable(sec)
|
|
1065
1441
|
|
|
1066
|
-
|
|
1442
|
+
buildInternTable()
|
|
1443
|
+
|
|
1444
|
+
timePhase(profiler, 'pullStdlib', () => pullStdlib(sec))
|
|
1067
1445
|
|
|
1068
1446
|
stripStaticDataPrefix(sec)
|
|
1069
1447
|
|
|
1070
1448
|
ensureThrowRuntime(sec)
|
|
1071
1449
|
|
|
1072
|
-
optimizeModule(sec)
|
|
1450
|
+
timePhase(profiler, 'optimizeModule', () => optimizeModule(sec, profiler))
|
|
1073
1451
|
|
|
1074
|
-
// Populate globals (after __start — const folding may update declarations)
|
|
1075
|
-
|
|
1452
|
+
// Populate globals (after __start — const folding may update declarations).
|
|
1453
|
+
// Records build IR directly — no WAT-text parse-back.
|
|
1454
|
+
sec.globals.push(...[...ctx.scope.globals].filter(([, g]) => g).map(([n, g]) => ['global', `$${n}`,
|
|
1455
|
+
...(g.export ? [['export', `"${g.export}"`]] : []),
|
|
1456
|
+
g.mut ? ['mut', g.type] : g.type,
|
|
1457
|
+
[`${g.type}.const`, g.init]]))
|
|
1076
1458
|
|
|
1077
1459
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1078
1460
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
@@ -1124,26 +1506,6 @@ export default function compile(ast, profiler) {
|
|
|
1124
1506
|
if (restParamFuncs.length)
|
|
1125
1507
|
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1126
1508
|
|
|
1127
|
-
// Custom section: per-export i64 ABI map. Each entry describes an export
|
|
1128
|
-
// whose boundary wrapper carries NaN-boxed pointers via i64 (rather than
|
|
1129
|
-
// f64) to dodge V8's NaN canonicalization. Format: { name, p, r } where p
|
|
1130
|
-
// is an array of i64 param indices and r is 1 if result is i64. host.js
|
|
1131
|
-
// wrap() reinterprets BigInt↔f64 at i64 positions; numeric f64 positions
|
|
1132
|
-
// stay as Numbers on the JS side.
|
|
1133
|
-
const i64Exports = []
|
|
1134
|
-
for (const f of ctx.func.list) {
|
|
1135
|
-
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportUsesI64) continue
|
|
1136
|
-
const p = []
|
|
1137
|
-
f._exportI64Sig.params.forEach((b, i) => { if (b) p.push(i) })
|
|
1138
|
-
const r = f._exportI64Sig.result ? 1 : 0
|
|
1139
|
-
// One entry per JS-visible export name (inline + non-aliased + aliased
|
|
1140
|
-
// re-exports). `exportNamesOf` yields every name that resolves to f —
|
|
1141
|
-
// wrap() looks up by export name, not by internal symbol.
|
|
1142
|
-
for (const exportName of exportNamesOf(f.name)) i64Exports.push({ name: exportName, p, r })
|
|
1143
|
-
}
|
|
1144
|
-
if (i64Exports.length)
|
|
1145
|
-
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1146
|
-
|
|
1147
1509
|
// Custom section: per-export externref param positions. interop.js reads
|
|
1148
1510
|
// this to pass JS arguments straight through at those positions (no
|
|
1149
1511
|
// `mem.wrapVal`, no SSO encoding). Format: { name, p, d? } where p lists
|
|
@@ -1158,12 +1520,22 @@ export default function compile(ast, profiler) {
|
|
|
1158
1520
|
f._exportExtParams.forEach((b, i) => {
|
|
1159
1521
|
if (!b) return
|
|
1160
1522
|
p.push(i)
|
|
1161
|
-
|
|
1523
|
+
// String-key the index: object property keys are conceptually strings (JSON renders
|
|
1524
|
+
// `{"0":…}` either way), and the self-host kernel's objects don't enumerate a numeric
|
|
1525
|
+
// key — `d[0]=…` stores but Object.keys(d) misses it, so the `d` map would read empty
|
|
1526
|
+
// and the default never reach the jz:extparam section. Same coercion as the optimize
|
|
1527
|
+
// LEVEL_PRESETS lookup. (Native is unaffected: numeric keys auto-stringify.)
|
|
1528
|
+
if (typeof b === 'object' && b.def != null) d[String(i)] = b.def
|
|
1162
1529
|
})
|
|
1163
1530
|
if (!p.length) continue
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1531
|
+
// Build each export entry as a direct literal — no `entry.d = d` after the fact and no
|
|
1532
|
+
// `{...entry, name}` spread. The self-host kernel's fixed-schema objects don't enumerate
|
|
1533
|
+
// a key added to a non-empty literal (JSON.stringify/spread would silently drop a post-hoc
|
|
1534
|
+
// `d`), and spreading a 3-key literal mis-resolves the merged schema there. Constructing
|
|
1535
|
+
// the final shape directly sidesteps both.
|
|
1536
|
+
const hasDefaults = Object.keys(d).length > 0
|
|
1537
|
+
for (const exportName of exportNamesOf(f.name))
|
|
1538
|
+
extExports.push(hasDefaults ? { name: exportName, p, d } : { name: exportName, p })
|
|
1167
1539
|
}
|
|
1168
1540
|
if (extExports.length)
|
|
1169
1541
|
sec.customs.push(['@custom', '"jz:extparam"', `"${JSON.stringify(extExports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|