jz 0.8.0 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/src/compile/emit.js
CHANGED
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
import {
|
|
25
25
|
commaList, T, isBlockBody, isReassigned, mutatesArrayLength, isConstLiteral, constLiteralHoistable,
|
|
26
26
|
hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
|
|
27
|
+
ASSIGN_OPS,
|
|
27
28
|
} from '../ast.js'
|
|
28
|
-
import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
|
|
29
|
+
import { ctx, err, inc, warnDeopt, PTR, ssoBitI64Hex, LAYOUT } from '../ctx.js'
|
|
30
|
+
import { i64Hex, encodePtrHi, STR_HCACHE_BIT, typedElemAux } from '../../layout.js'
|
|
31
|
+
import { bodyOnlyCharCodeAtCalls } from '../abi/string.js'
|
|
29
32
|
import { includeForStringOnly } from '../autoload.js'
|
|
30
33
|
import { FITS_I32_MAX } from '../widen.js'
|
|
31
34
|
import { nonNegIntLiteral, intLiteralValue, staticPropertyKey } from '../static.js'
|
|
@@ -35,12 +38,13 @@ import {
|
|
|
35
38
|
containsDeclOf, cloneWithSubst, containsKnownTypedArrayIndex,
|
|
36
39
|
smallConstForTripCount, isTerminator, scanBoundedLoops, inBoundsCharCodeAt,
|
|
37
40
|
exprType, MAX_SMALL_FOR_UNROLL, MAX_NESTED_FOR_UNROLL,
|
|
41
|
+
inBoundsArrIdx, typedIdxProven, versionableTypedNest, idxKey,
|
|
38
42
|
} from '../type.js'
|
|
39
43
|
import { valTypeOf, shapeOf } from '../kind.js'
|
|
40
44
|
import { VAL, lookupValType, repOf, updateRep, repOfGlobal } from '../reps.js'
|
|
41
45
|
import {
|
|
42
46
|
typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
|
|
43
|
-
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
47
|
+
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY, TRUE_NAN, FALSE_NAN, NULL_NAN,
|
|
44
48
|
WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
45
49
|
mkPtrIR, ptrOffsetIR, ptrTypeIR, ptrTypeEq, dispatchByPtrType, sidecarOverride, valKindToPtr,
|
|
46
50
|
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64, toStrI64, maskBound,
|
|
@@ -49,10 +53,11 @@ import {
|
|
|
49
53
|
temp, tempI32, tempI64, allocPtr,
|
|
50
54
|
block64, withTemp,
|
|
51
55
|
boxedAddr, readVar, writeVar, isNullish, isNull, isUndef, isBoolAtom,
|
|
52
|
-
boolBoxIR,
|
|
56
|
+
boolBoxIR, carrierF64,
|
|
53
57
|
isLiteralStr, resolveValType, isFuncRef,
|
|
54
58
|
multiCount, loopTop, flat,
|
|
55
59
|
reconstructArgsWithSpreads, tcoTailRewrite,
|
|
60
|
+
extractF64Bits,
|
|
56
61
|
} from '../ir.js'
|
|
57
62
|
import { isBoundName } from '../ir.js'
|
|
58
63
|
import { extractRefinements, withRefinements } from './flow-types.js'
|
|
@@ -191,16 +196,29 @@ const FIRST_CLASS_UNARY_MATH = {
|
|
|
191
196
|
'math.trunc': 'f64.trunc',
|
|
192
197
|
}
|
|
193
198
|
|
|
199
|
+
// Builtins with a hand-written uniform-ABI body (beyond the single-op math set).
|
|
200
|
+
// Array.isArray: NaN-boxed AND tag==ARRAY → 1/0 — the same f64.convert_i32 form
|
|
201
|
+
// an arrow returning a comparison produces, so callback semantics match
|
|
202
|
+
// `xs.filter(x => Array.isArray(x))` exactly (watr's optimizer passes the bare
|
|
203
|
+
// builtin to .filter; the self-host kernel must compile it).
|
|
204
|
+
const FIRST_CLASS_BUILTIN_BODY = {
|
|
205
|
+
'Array.isArray': () =>
|
|
206
|
+
`(if (result f64) (i32.and (f64.ne (local.get $__a0) (local.get $__a0)) ` +
|
|
207
|
+
`(i32.eq (i32.and (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $__a0)) (i64.const ${LAYOUT.TAG_SHIFT}))) (i32.const ${LAYOUT.TAG_MASK})) (i32.const ${PTR.ARRAY}))) ` +
|
|
208
|
+
`(then (f64.const 1)) (else (f64.const 0)))`,
|
|
209
|
+
}
|
|
210
|
+
|
|
194
211
|
function builtinFunctionValue(name) {
|
|
195
212
|
const op = FIRST_CLASS_UNARY_MATH[name]
|
|
196
|
-
|
|
213
|
+
const bodyGen = FIRST_CLASS_BUILTIN_BODY[name]
|
|
214
|
+
if (!op && !bodyGen) err(`Builtin function '${name}' cannot be used as a first-class value`)
|
|
197
215
|
if (!ctx.closure.table) err(`Builtin function '${name}' used as value requires closure support`)
|
|
198
216
|
const fn = `${T}builtin_${name.replace(/\W/g, '_')}`
|
|
199
217
|
if (!ctx.core.stdlib[fn]) {
|
|
200
218
|
const width = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
201
219
|
const params = ['(param $__env f64)', '(param $__argc i32)']
|
|
202
220
|
for (let i = 0; i < width; i++) params.push(`(param $__a${i} f64)`)
|
|
203
|
-
ctx.core.stdlib[fn] = `(func $${fn} ${params.join(' ')} (result f64) (${op} (local.get $__a0)))`
|
|
221
|
+
ctx.core.stdlib[fn] = `(func $${fn} ${params.join(' ')} (result f64) ${op ? `(${op} (local.get $__a0))` : bodyGen()})`
|
|
204
222
|
inc(fn)
|
|
205
223
|
}
|
|
206
224
|
let idx = ctx.closure.table.indexOf(fn)
|
|
@@ -717,6 +735,10 @@ function unrollForIn(init, cond, step, body) {
|
|
|
717
735
|
if (lookupValType(src) !== VAL.OBJECT) return null
|
|
718
736
|
const keys = ctx.schema.resolve(src)
|
|
719
737
|
if (!keys || !keys.length || keys.length > FORIN_UNROLL_MAX) return null
|
|
738
|
+
// A literal-key write OUTSIDE the schema also adds an enumerable key (it
|
|
739
|
+
// lands in the dyn sidecar) — same proof obligation as computed writes.
|
|
740
|
+
const lw = ctx.types.literalWriteKeys?.get(src)
|
|
741
|
+
if (lw) for (const k of lw) if (!keys.includes(k)) return null
|
|
720
742
|
|
|
721
743
|
const rest = body.slice(2)
|
|
722
744
|
const realBody = rest.length === 1 ? rest[0] : [';', ...rest]
|
|
@@ -880,9 +902,17 @@ function tryIntDivTrunc(aNode, bNode) {
|
|
|
880
902
|
}
|
|
881
903
|
|
|
882
904
|
/** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
|
|
883
|
-
* i32 offset), else falls back to numeric WASM type coercion.
|
|
884
|
-
|
|
905
|
+
* i32 offset), else falls back to numeric WASM type coercion.
|
|
906
|
+
* `node` (the arg's AST, when the caller has it): a statically-BOOL arg headed
|
|
907
|
+
* into an UNTYPED f64 param crosses as its TRUE/FALSE atom box — the callee
|
|
908
|
+
* treats that slot as an opaque value, so identity (typeof/String/strict-eq)
|
|
909
|
+
* must survive. A val-known param (narrow stamped `p.val`) keeps the raw 0/1
|
|
910
|
+
* ABI its body assumes; i32/pointer params are numeric positions. */
|
|
911
|
+
function coerceArg(ir, param, node) {
|
|
885
912
|
if (param?.ptrKind != null) return ptrOffsetIR(ir, param.ptrKind)
|
|
913
|
+
if (node !== undefined && (param == null || (param.type !== 'i32' && param.val == null)) &&
|
|
914
|
+
valTypeOf(node) === VAL.BOOL)
|
|
915
|
+
return carrierF64(node, ir)
|
|
886
916
|
return asParamType(ir, param?.type)
|
|
887
917
|
}
|
|
888
918
|
|
|
@@ -898,7 +928,174 @@ function padArgs(args, params) {
|
|
|
898
928
|
/** Emit a node list as call arguments for the given param list: per-param
|
|
899
929
|
* coercion then arity padding. Used at every direct-call site. */
|
|
900
930
|
function emitCallArgs(argNodes, params) {
|
|
901
|
-
return padArgs(argNodes.map((a, k) => coerceArg(emit(a), params[k])), params)
|
|
931
|
+
return padArgs(argNodes.map((a, k) => coerceArg(emit(a), params[k], a)), params)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Fuse `a + b` when it tops a string-concat chain of ≥3 leaves: evaluate
|
|
935
|
+
* each leaf ONCE to an i64 string box (left-to-right — JS ToString order),
|
|
936
|
+
* measure each with __str_byteLen, allocate the [hash=0][len][bytes]
|
|
937
|
+
* HCACHE header once, and __str_copy each leaf at its cumulative offset.
|
|
938
|
+
* Replaces the pairwise lowering's per-`+` alloc + triangular prefix
|
|
939
|
+
* re-copy. Self-accumulation (`line = line + …`) keeps the head pairwise:
|
|
940
|
+
* the TAIL fuses to one fresh string and the head takes the existing
|
|
941
|
+
* bump-extend concatRaw. A total ≤ 6 yields a short HEAP string where
|
|
942
|
+
* pairwise gave SSO — value-equal (SSO is representation, not semantics). */
|
|
943
|
+
function tryConcatChain(a, b, selfAccum) {
|
|
944
|
+
// A `+` NODE is a string concat iff a side is statically STRING — the exact
|
|
945
|
+
// gate the pairwise lowering uses. (BOOL/OBJECT must NOT qualify a node:
|
|
946
|
+
// `(x===y) + (u===v)` is NUMERIC bool addition; they only stringify as
|
|
947
|
+
// LEAVES once the node qualifies through a genuine STRING side.)
|
|
948
|
+
const isStr = (n) => valTypeOf(n) === VAL.STRING
|
|
949
|
+
if (!(isStr(a) || isStr(b))) return null
|
|
950
|
+
const leaves = []
|
|
951
|
+
const walk = (n) => {
|
|
952
|
+
if (Array.isArray(n) && n[0] === '+' && n.length === 3 && (isStr(n[1]) || isStr(n[2]))) {
|
|
953
|
+
walk(n[1]); walk(n[2])
|
|
954
|
+
} else leaves.push(n)
|
|
955
|
+
}
|
|
956
|
+
walk(a); walk(b)
|
|
957
|
+
// Self-accumulating head: fuse only the tail, join with bump-extend after.
|
|
958
|
+
const headAccum = selfAccum && leaves[0] === a && typeof a === 'string' ? leaves.shift() : null
|
|
959
|
+
if (leaves.length < 3) return null
|
|
960
|
+
// Every leaf must stringify deterministically at this site: known kinds
|
|
961
|
+
// (STRING/OBJECT/BOOL/NUMBER) or unknown-through-__to_str. BIGINT joins
|
|
962
|
+
// numerically elsewhere — bail so the existing lowering keeps its path.
|
|
963
|
+
for (const l of leaves) if (valTypeOf(l) === VAL.BIGINT) return null
|
|
964
|
+
inc('__alloc', '__mkptr', '__sso_norm')
|
|
965
|
+
// LITERAL ASCII leaves (the serializer separators — ',', '\n', 'k=' …) carry
|
|
966
|
+
// their bytes and length at compile time: no box/len temps, no __str_byteLen,
|
|
967
|
+
// no __str_copy — the length const-folds into the total and the bytes store
|
|
968
|
+
// directly at the cursor (grouped 4/2/1-wide; watr folds the const totals).
|
|
969
|
+
// Profiled on strbuild: copy+len calls on 1-6 byte parts were 38.7% of a row.
|
|
970
|
+
const litOf = (n) => {
|
|
971
|
+
if (!Array.isArray(n) || n[0] !== 'str' || typeof n[1] !== 'string' || n[1].length === 0) return null
|
|
972
|
+
for (let i = 0; i < n[1].length; i++) if (n[1].charCodeAt(i) > 0x7f) return null
|
|
973
|
+
return n[1]
|
|
974
|
+
}
|
|
975
|
+
const lits = leaves.map(litOf)
|
|
976
|
+
const bT = [], nT = [], lT = leaves.map((_, k) => lits[k] != null ? null : tempI32('cl'))
|
|
977
|
+
const offT = tempI32('co'), curT = tempI32('cu')
|
|
978
|
+
const seq = []
|
|
979
|
+
let litTotal = 0
|
|
980
|
+
leaves.forEach((n, k) => {
|
|
981
|
+
if (lits[k] != null) { litTotal += lits[k].length; return }
|
|
982
|
+
const vt = valTypeOf(n)
|
|
983
|
+
// BOOL renders through emitBoolStr(node); every other leaf emits its value once here.
|
|
984
|
+
const v = vt === VAL.BOOL ? null : emit(n)
|
|
985
|
+
// i32-PROVEN leaf (exactly toStrI64's __i32_to_str class): keep the raw value,
|
|
986
|
+
// not a temp string — __ilen joins the total and __itoa_s renders the digits
|
|
987
|
+
// directly at the cursor. Drops the per-number __i32_to_str (alloc+itoa+mkstr),
|
|
988
|
+
// __str_byteLen and __str_copy — the whole temp-string round trip.
|
|
989
|
+
if ((vt === VAL.NUMBER || vt == null) && v.type === 'i32' && v.ptrKind == null) {
|
|
990
|
+
inc('__ilen', '__itoa_s')
|
|
991
|
+
nT[k] = tempI32('cn')
|
|
992
|
+
seq.push(['local.set', `$${nT[k]}`, v])
|
|
993
|
+
seq.push(['local.set', `$${lT[k]}`, ['call', '$__ilen', ['local.get', `$${nT[k]}`]]])
|
|
994
|
+
return
|
|
995
|
+
}
|
|
996
|
+
inc('__str_byteLen', '__str_copy')
|
|
997
|
+
bT[k] = tempI64('cc')
|
|
998
|
+
seq.push(['local.set', `$${bT[k]}`,
|
|
999
|
+
vt === VAL.STRING ? ['i64.reinterpret_f64', asF64(v)] :
|
|
1000
|
+
vt === VAL.BOOL ? ['i64.reinterpret_f64', emitBoolStr(n)] :
|
|
1001
|
+
toStrI64(n, v)]) // OBJECT (compile-time ToPrimitive), NUMBER, unknown
|
|
1002
|
+
seq.push(['local.set', `$${lT[k]}`, ['call', '$__str_byteLen', ['local.get', `$${bT[k]}`]]])
|
|
1003
|
+
})
|
|
1004
|
+
const totalIR = () => {
|
|
1005
|
+
let t = ['i32.const', litTotal]
|
|
1006
|
+
for (let k = 0; k < leaves.length; k++) if (lT[k] != null) t = ['i32.add', t, ['local.get', `$${lT[k]}`]]
|
|
1007
|
+
return t
|
|
1008
|
+
}
|
|
1009
|
+
seq.push(['local.set', `$${offT}`, ['call', '$__alloc', ['i32.add', ['i32.const', 8], totalIR()]]])
|
|
1010
|
+
seq.push(['i32.store', ['local.get', `$${offT}`], ['i32.const', 0]]) // lazy hash cell
|
|
1011
|
+
seq.push(['i32.store', 'offset=4', ['local.get', `$${offT}`], totalIR()]) // len
|
|
1012
|
+
seq.push(['local.set', `$${offT}`, ['i32.add', ['local.get', `$${offT}`], ['i32.const', 8]]])
|
|
1013
|
+
seq.push(['local.set', `$${curT}`, ['local.get', `$${offT}`]])
|
|
1014
|
+
leaves.forEach((n, k) => {
|
|
1015
|
+
if (lits[k] != null) {
|
|
1016
|
+
const s = lits[k]
|
|
1017
|
+
let j = 0 // grouped little-endian stores: 4-byte words, 2-byte tail, then 1
|
|
1018
|
+
const at = (o) => o ? [`offset=${o}`, ['local.get', `$${curT}`]] : [['local.get', `$${curT}`]]
|
|
1019
|
+
for (; j + 4 <= s.length; j += 4)
|
|
1020
|
+
seq.push(['i32.store', ...at(j), ['i32.const',
|
|
1021
|
+
(s.charCodeAt(j) | (s.charCodeAt(j + 1) << 8) | (s.charCodeAt(j + 2) << 16) | (s.charCodeAt(j + 3) << 24)) | 0]])
|
|
1022
|
+
if (j + 2 <= s.length) {
|
|
1023
|
+
seq.push(['i32.store16', ...at(j), ['i32.const', s.charCodeAt(j) | (s.charCodeAt(j + 1) << 8)]])
|
|
1024
|
+
j += 2
|
|
1025
|
+
}
|
|
1026
|
+
if (j < s.length)
|
|
1027
|
+
seq.push(['i32.store8', ...at(j), ['i32.const', s.charCodeAt(j)]])
|
|
1028
|
+
if (k < leaves.length - 1)
|
|
1029
|
+
seq.push(['local.set', `$${curT}`, ['i32.add', ['local.get', `$${curT}`], ['i32.const', s.length]]])
|
|
1030
|
+
return
|
|
1031
|
+
}
|
|
1032
|
+
if (nT[k] != null) {
|
|
1033
|
+
// digits render at the cursor; the returned byte count (== $lT) advances it
|
|
1034
|
+
seq.push(k < leaves.length - 1
|
|
1035
|
+
? ['local.set', `$${curT}`, ['i32.add',
|
|
1036
|
+
['call', '$__itoa_s', ['local.get', `$${nT[k]}`], ['local.get', `$${curT}`]], ['local.get', `$${curT}`]]]
|
|
1037
|
+
: ['drop', ['call', '$__itoa_s', ['local.get', `$${nT[k]}`], ['local.get', `$${curT}`]]])
|
|
1038
|
+
return
|
|
1039
|
+
}
|
|
1040
|
+
seq.push(['call', '$__str_copy', ['local.get', `$${bT[k]}`], ['local.get', `$${curT}`], ['local.get', `$${lT[k]}`]])
|
|
1041
|
+
if (k < leaves.length - 1)
|
|
1042
|
+
seq.push(['local.set', `$${curT}`, ['i32.add', ['local.get', `$${curT}`], ['local.get', `$${lT[k]}`]]])
|
|
1043
|
+
})
|
|
1044
|
+
// __sso_norm epilogue: every producer that hand-writes heap bytes must
|
|
1045
|
+
// re-canonicalize — a ≤6-ASCII result MUST be SSO or its hash diverges
|
|
1046
|
+
// from a literal/SSO-built equal string (representation-keyed fast paths:
|
|
1047
|
+
// the SSO arithmetic mix vs the byte-FNV walk) and keyed lookups miss.
|
|
1048
|
+
const fresh = typed(['block', ['result', 'f64'],
|
|
1049
|
+
...seq,
|
|
1050
|
+
['call', '$__sso_norm', mkPtrIR(PTR.STRING, STR_HCACHE_BIT, ['local.get', `$${offT}`])]], 'f64')
|
|
1051
|
+
if (headAccum != null)
|
|
1052
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(headAccum)), fresh, ctx, true), 'f64')
|
|
1053
|
+
return fresh
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Guarded dispatch to a speculative typed clone (narrow's speculateTypedParams).
|
|
1057
|
+
* Args evaluate once, in order, into temps; a single masked NaN-box compare per
|
|
1058
|
+
* speculated position proves tag==TYPED && aux==elem-kind (owned — a view or any
|
|
1059
|
+
* other value falls to the original call unchanged, bit-exact). TYPED headers
|
|
1060
|
+
* never relocate (FORWARDING_MASK), so the proven offset is a bare mask — the
|
|
1061
|
+
* same inlining emitSchemaSlotGuarded does for OBJECT. */
|
|
1062
|
+
const TYPED_HI_MASK = '0xFFFFFFFF00000000'
|
|
1063
|
+
function emitSpeculativeCall(callee, spec, argNodes, func) {
|
|
1064
|
+
const params = func.sig.params
|
|
1065
|
+
const specAt = new Map(spec.guards.map(g => [g.k, g.aux]))
|
|
1066
|
+
const rt = func.sig.results[0] || 'f64'
|
|
1067
|
+
const seq = [], slots = []
|
|
1068
|
+
for (let k = 0; k < params.length; k++) {
|
|
1069
|
+
if (k < argNodes.length) {
|
|
1070
|
+
const ir = coerceArg(emit(argNodes[k]), params[k], argNodes[k])
|
|
1071
|
+
// Temp width follows the PARAM's ABI (coerceArg's contract), not the IR
|
|
1072
|
+
// tag — pointer-ABI coercions (`__ptr_offset`) come back untagged i32.
|
|
1073
|
+
const pt = params[k].ptrKind != null || params[k].type === 'i32' ? 'i32' : 'f64'
|
|
1074
|
+
const t = pt === 'i32' ? tempI32('sa') : temp('sa')
|
|
1075
|
+
seq.push(['local.set', `$${t}`, ir])
|
|
1076
|
+
slots.push({ local: t, type: pt })
|
|
1077
|
+
} else {
|
|
1078
|
+
slots.push(null) // arity pad — fresh per use below
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const get = (k) => slots[k]
|
|
1082
|
+
? typed(['local.get', `$${slots[k].local}`], slots[k].type)
|
|
1083
|
+
: params[k].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr()
|
|
1084
|
+
let cond = null
|
|
1085
|
+
for (const [k, aux] of specAt) {
|
|
1086
|
+
const c = ['i64.eq',
|
|
1087
|
+
['i64.and', ['i64.reinterpret_f64', get(k)], ['i64.const', TYPED_HI_MASK]],
|
|
1088
|
+
['i64.const', i64Hex(BigInt(encodePtrHi(PTR.TYPED, aux)) << 32n)]]
|
|
1089
|
+
cond = cond ? ['i32.and', cond, c] : c
|
|
1090
|
+
}
|
|
1091
|
+
const thenArgs = params.map((p, k) => specAt.has(k)
|
|
1092
|
+
? ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', get(k)], ['i64.const', LAYOUT.OFFSET_MASK]]]
|
|
1093
|
+
: get(k))
|
|
1094
|
+
const elseArgs = params.map((p, k) => get(k))
|
|
1095
|
+
const ifIR = ['if', ['result', rt], cond,
|
|
1096
|
+
['then', ['call', `$${spec.clone}`, ...thenArgs]],
|
|
1097
|
+
['else', ['call', `$${callee}`, ...elseArgs]]]
|
|
1098
|
+
return attachSigMeta(typed(['block', ['result', rt], ...seq, ifIR], rt), func.sig)
|
|
902
1099
|
}
|
|
903
1100
|
|
|
904
1101
|
/** Stamp a `call` IR with the pointer-ABI / sign metadata its signature carries.
|
|
@@ -1079,6 +1276,17 @@ export function emitDecl(...inits) {
|
|
|
1079
1276
|
if (isObjLit) ctx.schema.targetStack.push({ name, active: true })
|
|
1080
1277
|
const val = viewInit || emit(init)
|
|
1081
1278
|
if (isObjLit) ctx.schema.targetStack.pop()
|
|
1279
|
+
// Record the declared name's valTypeOf(init) into the flow overlay right after
|
|
1280
|
+
// emitting init — not just for sibling `let`s in the same block (emitBlockBody used
|
|
1281
|
+
// to do this itself, one statement late), but for decls that live INSIDE a `for`
|
|
1282
|
+
// node's init clause, which emitBlockBody's per-statement loop never sees directly
|
|
1283
|
+
// (e.g. src/prepare/index.js's for-of/for-in desugar: `let arrVar = __iter_arr(node),
|
|
1284
|
+
// idx = 0, len = arrVar.length`). valTypeOf consults ctx.func.refinements first, so
|
|
1285
|
+
// an early-return `Array.isArray` guard on `node` now correctly flows into `arrVar`
|
|
1286
|
+
// (and therefore into `len`'s own init two decls later in the same `let`) — every
|
|
1287
|
+
// downstream `arrVar[i]`/`.length` in the loop then takes the ARRAY-known fast path
|
|
1288
|
+
// instead of falling to the generic __typed_idx/__length dispatch.
|
|
1289
|
+
setFlowVal(name, valTypeOf(init))
|
|
1082
1290
|
// Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
|
|
1083
1291
|
// Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
|
|
1084
1292
|
// isReassigned is conservative across nested arrow shadows — we miss the optimization
|
|
@@ -1113,6 +1321,26 @@ export function emitDecl(...inits) {
|
|
|
1113
1321
|
continue
|
|
1114
1322
|
}
|
|
1115
1323
|
if (isGlobal(name)) {
|
|
1324
|
+
// Module-const array of capture-free closures: record the candidate set for
|
|
1325
|
+
// indexed-call devirt (tryConstFnArrayDispatch). Const-only — a reassignable
|
|
1326
|
+
// binding could point at a different array whose elements we never saw.
|
|
1327
|
+
// NOTE: a dispatch-site arg lattice (argc/numeric row merged into the element
|
|
1328
|
+
// bodies' paramTypes/minArgc, killing their boxed-arg guards) was BUILT and
|
|
1329
|
+
// REVERTED here: prepare-time folds erase element reads (`let p = ops[1]`
|
|
1330
|
+
// pre-evals to the closure ref before program facts see the '[]' shape), so
|
|
1331
|
+
// no AST-level gate can prove the tagged sites are the only callers — the
|
|
1332
|
+
// trusted body then truncates raw box bits on a string arg through the alias
|
|
1333
|
+
// (see test/closures.js "element-as-value alias and arity variance stay
|
|
1334
|
+
// exact", which records the pre-existing string-coercion gap). Bodies keep
|
|
1335
|
+
// their guards; the arm-inline + watr trunc∘convert identities still
|
|
1336
|
+
// collapse the provably-int side.
|
|
1337
|
+
if (val.fnElements && ctx.scope.consts?.has(name))
|
|
1338
|
+
(ctx.scope.constFnArrays ||= new Map()).set(name, val.fnElements)
|
|
1339
|
+
// Const binding of a STATIC array literal: record base/len (+ the box bits as
|
|
1340
|
+
// identity) for optimize's foldStaticConstArrayReads. Same const-only logic.
|
|
1341
|
+
if (val.staticOff != null && ctx.scope.consts?.has(name))
|
|
1342
|
+
(ctx.scope.staticArrs ||= new Map()).set(name,
|
|
1343
|
+
{ off: val.staticOff, len: val.staticLen, bits: extractF64Bits(val) })
|
|
1116
1344
|
// Unboxed pointer const globals carry the raw i32 offset; init coerces via asPtrOffset.
|
|
1117
1345
|
// Only an i32-STORED global is a raw pointer carrier — an f64 global holds a
|
|
1118
1346
|
// NaN-boxed value, so coercing its init to an i32 offset (asPtrOffset → i32.wrap)
|
|
@@ -1412,6 +1640,67 @@ export function emitVoid(node) {
|
|
|
1412
1640
|
return items
|
|
1413
1641
|
}
|
|
1414
1642
|
|
|
1643
|
+
// Record a name's valTypeOf(rhs) fact into the live localValTypesOverlay layer (tier #2
|
|
1644
|
+
// in reps.js's lookup priority — see lookupValType). `let`/`const` decls record this
|
|
1645
|
+
// themselves at their emit site (emitDecl, right after each `emit(init)`); this helper
|
|
1646
|
+
// covers the remaining case emitBlockBody drives directly: a bare `name = rhs`
|
|
1647
|
+
// reassignment statement.
|
|
1648
|
+
function setFlowVal(name, vt) {
|
|
1649
|
+
if (!ctx.func.localValTypesOverlay || !isBoundName(name)) return
|
|
1650
|
+
// A name reassigned at any NESTED position of the current block (inside an
|
|
1651
|
+
// if/loop/closure body, a for's step, …) carries NO overlay fact: the recording
|
|
1652
|
+
// site doesn't dominate the reassignment, so the fact can go stale while the
|
|
1653
|
+
// binding is live — `let x = [7,8]; if (c) x = 5; x.length` read the number 5
|
|
1654
|
+
// through the ARRAY fast path (OOB): a latent pre-existing miscompile, widened
|
|
1655
|
+
// when decl recording moved into emitDecl and began covering for-init decls
|
|
1656
|
+
// (`for (let x = […]; x.length; x = 0)`). Top-level `=` statements stay
|
|
1657
|
+
// recordable — the block driver re-records at each, so the fact always
|
|
1658
|
+
// reflects the latest dominating write.
|
|
1659
|
+
if (ctx.func.flowValBlocked?.has(name)) return
|
|
1660
|
+
if (vt) ctx.func.localValTypesOverlay.set(name, vt)
|
|
1661
|
+
else ctx.func.localValTypesOverlay.delete(name)
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Names assigned at a NESTED position within this block's statements: anything
|
|
1665
|
+
// except top-level `name = rhs` statement heads and top-level decl heads (both
|
|
1666
|
+
// re-recorded by the emit drivers as they pass). Walks into closures too — a
|
|
1667
|
+
// closure assigning an outer name can run between the recording and any later
|
|
1668
|
+
// read. ++/-- count as assignments (conservative: their result is numeric, but
|
|
1669
|
+
// blocking keeps the rule uniform).
|
|
1670
|
+
function collectNestedAssigns(stmts) {
|
|
1671
|
+
const blocked = new Set()
|
|
1672
|
+
const walk = (n) => {
|
|
1673
|
+
if (!Array.isArray(n)) return
|
|
1674
|
+
const op = n[0]
|
|
1675
|
+
// A decl's `['=', name, init]` pairs are DECLARATIONS, not reassignments
|
|
1676
|
+
// (same as isReassigned's let/const handling) — a nested `for (let x = …)`
|
|
1677
|
+
// init must not block x; only a true write in cond/step/body does.
|
|
1678
|
+
if (op === 'let' || op === 'const') {
|
|
1679
|
+
for (let i = 1; i < n.length; i++) {
|
|
1680
|
+
const d = n[i]
|
|
1681
|
+
if (Array.isArray(d) && d[0] === '=' && d[2] != null) walk(d[2])
|
|
1682
|
+
}
|
|
1683
|
+
return
|
|
1684
|
+
}
|
|
1685
|
+
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') && typeof n[1] === 'string') blocked.add(n[1])
|
|
1686
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
1687
|
+
}
|
|
1688
|
+
for (const s of stmts) {
|
|
1689
|
+
if (!Array.isArray(s)) continue
|
|
1690
|
+
const op = s[0]
|
|
1691
|
+
if (op === '=' && typeof s[1] === 'string') { walk(s[2]); continue } // top-level target re-records
|
|
1692
|
+
if (op === 'let' || op === 'const') {
|
|
1693
|
+
for (let i = 1; i < s.length; i++) {
|
|
1694
|
+
const d = s[i]
|
|
1695
|
+
if (Array.isArray(d) && d[0] === '=' && d[2] != null) walk(d[2]) // decl head re-records; walk init
|
|
1696
|
+
}
|
|
1697
|
+
continue
|
|
1698
|
+
}
|
|
1699
|
+
walk(s)
|
|
1700
|
+
}
|
|
1701
|
+
return blocked
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1415
1704
|
/** Emit block body as flat list of WASM instructions. Unwraps {} and delegates to emitVoid per statement.
|
|
1416
1705
|
* Also drives early-return refinement: `if (!guard) return/throw` narrows `guard` for the
|
|
1417
1706
|
* rest of the enclosing block. Refinements added here are rolled back on block exit. */
|
|
@@ -1422,32 +1711,21 @@ export function emitBlockBody(node) {
|
|
|
1422
1711
|
const accumulated = []
|
|
1423
1712
|
const prevValOverlay = ctx.func.localValTypesOverlay
|
|
1424
1713
|
ctx.func.localValTypesOverlay = new Map(prevValOverlay || [])
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (op === '=' && typeof stmt[1] === 'string') {
|
|
1434
|
-
setFlowVal(stmt[1], valTypeOf(stmt[2]))
|
|
1435
|
-
return
|
|
1436
|
-
}
|
|
1437
|
-
if (op === 'let' || op === 'const') {
|
|
1438
|
-
for (let i = 1; i < stmt.length; i++) {
|
|
1439
|
-
const d = stmt[i]
|
|
1440
|
-
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string')
|
|
1441
|
-
setFlowVal(d[1], valTypeOf(d[2]))
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1714
|
+
// Nested-assignment blocklist for this block. Per-block own-scan is sufficient:
|
|
1715
|
+
// an outer name whose fact was blocked in the outer block never entered the
|
|
1716
|
+
// outer overlay (which this block's overlay copies), and a name reassigned at
|
|
1717
|
+
// THIS block's top level re-records right after the assignment (dominating the
|
|
1718
|
+
// rest of this block) — the scan blocks exactly the recordings that don't
|
|
1719
|
+
// dominate their possible staleness point.
|
|
1720
|
+
const prevFlowBlocked = ctx.func.flowValBlocked
|
|
1721
|
+
ctx.func.flowValBlocked = collectNestedAssigns(stmts)
|
|
1445
1722
|
try {
|
|
1446
1723
|
for (let i = 0; i < stmts.length; i++) {
|
|
1447
1724
|
const s = stmts[i]
|
|
1448
1725
|
if (s == null || typeof s === 'number') continue
|
|
1449
1726
|
out.push(...emitVoid(s))
|
|
1450
|
-
|
|
1727
|
+
// `let`/`const` decls self-record via emitDecl; only a bare reassignment needs it here.
|
|
1728
|
+
if (Array.isArray(s) && s[0] === '=' && typeof s[1] === 'string') setFlowVal(s[1], valTypeOf(s[2]))
|
|
1451
1729
|
// After an `if (cond) terminator` (no else), narrow types from !cond for subsequent statements.
|
|
1452
1730
|
// Skip names that are reassigned later — refinement would be unsound past the assignment.
|
|
1453
1731
|
if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
|
|
@@ -1468,6 +1746,7 @@ export function emitBlockBody(node) {
|
|
|
1468
1746
|
}
|
|
1469
1747
|
} finally {
|
|
1470
1748
|
ctx.func.localValTypesOverlay = prevValOverlay
|
|
1749
|
+
ctx.func.flowValBlocked = prevFlowBlocked
|
|
1471
1750
|
// Restore prior refinements on block exit.
|
|
1472
1751
|
for (let i = accumulated.length - 1; i >= 0; i--) {
|
|
1473
1752
|
const [name, prev] = accumulated[i]
|
|
@@ -1508,8 +1787,17 @@ const STRICT_PRIM = new Set([VAL.NUMBER, VAL.BOOL, VAL.STRING, VAL.BIGINT])
|
|
|
1508
1787
|
// nullish literal) can hold null/undefined at runtime, so `x === null` / `x == null`
|
|
1509
1788
|
// must NOT fold to a constant even when `val` is a definite non-null kind. Only bare
|
|
1510
1789
|
// variable reads carry the flag; literals/fresh allocations are inherently non-null.
|
|
1511
|
-
|
|
1512
|
-
|
|
1790
|
+
// An UNPROVEN typed-index read joins the set: `ta[i]` reads `undefined` past the end
|
|
1791
|
+
// (the checked .typed:[] form), while its VT stays NUMBER for numeric dispatch — the
|
|
1792
|
+
// undef box IS a NaN through arithmetic; only these identity folds must stay live.
|
|
1793
|
+
// `ta[i] === undefined` is the idiomatic bounds probe, so folding it kills real code.
|
|
1794
|
+
const nullableOperand = (n) => {
|
|
1795
|
+
if (typeof n === 'string') return !!(repOf(n)?.nullable || repOfGlobal(n)?.nullable)
|
|
1796
|
+
if (Array.isArray(n) && n[0] === '[]' && n.length === 3
|
|
1797
|
+
&& typeof n[1] === 'string' && lookupValType(n[1]) === VAL.TYPED)
|
|
1798
|
+
return !typedIdxProven(n[1], n[2])
|
|
1799
|
+
return false
|
|
1800
|
+
}
|
|
1513
1801
|
|
|
1514
1802
|
// An emitted value whose bit pattern is an i32, paired with how it widens to f64: a
|
|
1515
1803
|
// `f64.convert_i32_s/u(x)` peels to its i32 source `x`; a bare i32 widens signed. Used to compare
|
|
@@ -1553,6 +1841,41 @@ const isCheapPureVal = (n) => {
|
|
|
1553
1841
|
return false
|
|
1554
1842
|
}
|
|
1555
1843
|
|
|
1844
|
+
// Side-effect-free: no writes (assignment / ++ / --), no calls, no closures, no throw. UNLIKE
|
|
1845
|
+
// `isCheapPureVal` this ALLOWS loads, member reads, and `/` `%` — a side-effect-free expr may read
|
|
1846
|
+
// memory or trap. It is the right gate for an `if` CONDITION promoted to a `select` condition: the
|
|
1847
|
+
// condition is evaluated exactly once whether the lowering branches or selects (any trap fires the
|
|
1848
|
+
// same in both, the read order vs the pure value arm is immaterial), so it need only avoid MUTATING
|
|
1849
|
+
// state the value arm could read — i.e. be side-effect-free, not unconditionally-evaluable.
|
|
1850
|
+
const SIDE_EFFECT_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '>>=', '<<=',
|
|
1851
|
+
'>>>=', '||=', '&&=', '??=', '++', '--', '()', '=>', 'throw', 'new', 'await', 'yield'])
|
|
1852
|
+
const isSideEffectFree = (n) => {
|
|
1853
|
+
if (!Array.isArray(n)) return true
|
|
1854
|
+
if (typeof n[0] === 'string' && SIDE_EFFECT_OPS.has(n[0])) return false
|
|
1855
|
+
for (let i = 1; i < n.length; i++) if (!isSideEffectFree(n[i])) return false
|
|
1856
|
+
return true
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const isLit1 = (n) => Array.isArray(n) && n[0] == null && n[1] === 1
|
|
1860
|
+
// A void statement whose whole effect is `x = <cheap pure value>` for a simple local `x` — the
|
|
1861
|
+
// shape if→select can lower to `x = cond ? value : x`. Recognizes the plain assignment plus the
|
|
1862
|
+
// increment forms `++x`/`--x` and their postfix lowerings `(++x) - 1` / `(--x) + 1` (prepare turns
|
|
1863
|
+
// `x++` in statement position into the latter; the discarded ∓1 is dead in void context, so the
|
|
1864
|
+
// net effect is the increment). Returns `{ lhs, val }` or null.
|
|
1865
|
+
function matchVoidLocalStore(s) {
|
|
1866
|
+
if (!Array.isArray(s)) return null
|
|
1867
|
+
if (s[0] === '=' && typeof s[1] === 'string' && isCheapPureVal(s[2])) return { lhs: s[1], val: s[2] }
|
|
1868
|
+
if ((s[0] === '++' || s[0] === '--') && typeof s[1] === 'string')
|
|
1869
|
+
return { lhs: s[1], val: [s[0] === '++' ? '+' : '-', s[1], [, 1]] }
|
|
1870
|
+
// postfix: `x++` → `(++x) - 1`, `x--` → `(--x) + 1`
|
|
1871
|
+
if ((s[0] === '-' || s[0] === '+') && isLit1(s[2]) && Array.isArray(s[1])
|
|
1872
|
+
&& (s[1][0] === '++' || s[1][0] === '--') && typeof s[1][1] === 'string') {
|
|
1873
|
+
const inc = s[1][0] === '++'
|
|
1874
|
+
if ((inc && s[0] === '-') || (!inc && s[0] === '+')) return { lhs: s[1][1], val: [inc ? '+' : '-', s[1][1], [, 1]] }
|
|
1875
|
+
}
|
|
1876
|
+
return null
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1556
1879
|
function emitLooseEq(a, b, negate) {
|
|
1557
1880
|
const eqOp = negate ? 'ne' : 'eq'
|
|
1558
1881
|
const sentinel = emitNum(negate ? 1 : 0)
|
|
@@ -1617,6 +1940,16 @@ function emitLooseEq(a, b, negate) {
|
|
|
1617
1940
|
// spec-on/spec-off differential (zero divergence at optimize 0 and 2).
|
|
1618
1941
|
const strEqResult = (r) => negate ? typed(['i32.eqz', r], 'i32') : r
|
|
1619
1942
|
const aStr = rawA === VAL.STRING, bStr = rawB === VAL.STRING
|
|
1943
|
+
// SSO literal (≤6 ASCII — its NaN-box IS its content, see module/string.js codec):
|
|
1944
|
+
// under the ≤6-ASCII⇒SSO producer invariant, content equality ⟺ bit equality
|
|
1945
|
+
// against ANY operand — an equal string must be the same SSO pattern, a heap
|
|
1946
|
+
// string can't hold ≤6-ASCII content, and a non-string never equals a string
|
|
1947
|
+
// (bit-aliasing NaNs behave identically to the pre-existing bit-eq fast path).
|
|
1948
|
+
// So the whole compare collapses to ONE i64.eq/ne — no call, no fallback.
|
|
1949
|
+
const ssoLit = (n) => ctx.features.sso && isLiteralStr(n) && n[1].length <= 6 && /^[\x00-\x7f]*$/.test(n[1])
|
|
1950
|
+
if ((aStr || bStr) && (rawA == null || aStr) && (rawB == null || bStr) && (ssoLit(a) || ssoLit(b))) {
|
|
1951
|
+
return typed([`i64.${negate ? 'ne' : 'eq'}`, asI64(va), asI64(vb)], 'i32')
|
|
1952
|
+
}
|
|
1620
1953
|
if (aStr && bStr) {
|
|
1621
1954
|
inc('__str_eq')
|
|
1622
1955
|
return strEqResult(typed(['call', '$__str_eq', asI64(va), asI64(vb)], 'i32'))
|
|
@@ -1625,14 +1958,26 @@ function emitLooseEq(a, b, negate) {
|
|
|
1625
1958
|
const uVal = bStr ? va : vb, lVal = bStr ? vb : va // u: unknown side, l: known string
|
|
1626
1959
|
inc('__is_str_key', '__str_eq')
|
|
1627
1960
|
const u = tempI64('seq'), l = tempI64('seq'), uG = ['local.get', `$${u}`], lG = ['local.get', `$${l}`]
|
|
1961
|
+
// On bit-mismatch, an SSO operand can't content-match anything (invariant
|
|
1962
|
+
// above) — one inline bit test skips the __is_str_key/__str_eq tail. Sound
|
|
1963
|
+
// for a non-string u too: the test only ever short-circuits to "not equal",
|
|
1964
|
+
// and a non-string never equals a string.
|
|
1965
|
+
const tail = ctx.features.sso
|
|
1966
|
+
? ['if', ['result', 'i32'],
|
|
1967
|
+
['i64.ne', ['i64.and', ['i64.or', uG, lG], ['i64.const', ssoBitI64Hex()]], ['i64.const', 0]],
|
|
1968
|
+
['then', ['i32.const', 0]],
|
|
1969
|
+
['else', ['if', ['result', 'i32'], ['call', '$__is_str_key', uG],
|
|
1970
|
+
['then', ['call', '$__str_eq', uG, lG]],
|
|
1971
|
+
['else', ['i32.const', 0]]]]]
|
|
1972
|
+
: ['if', ['result', 'i32'], ['call', '$__is_str_key', uG],
|
|
1973
|
+
['then', ['call', '$__str_eq', uG, lG]],
|
|
1974
|
+
['else', ['i32.const', 0]]]
|
|
1628
1975
|
return strEqResult(typed(['block', ['result', 'i32'],
|
|
1629
1976
|
['local.set', `$${u}`, asI64(uVal)],
|
|
1630
1977
|
['local.set', `$${l}`, asI64(lVal)],
|
|
1631
1978
|
['if', ['result', 'i32'], ['i64.eq', uG, lG],
|
|
1632
1979
|
['then', ['i32.const', 1]],
|
|
1633
|
-
['else',
|
|
1634
|
-
['then', ['call', '$__str_eq', uG, lG]],
|
|
1635
|
-
['else', ['i32.const', 0]]]]]], 'i32'))
|
|
1980
|
+
['else', tail]]], 'i32'))
|
|
1636
1981
|
}
|
|
1637
1982
|
inc('__eq')
|
|
1638
1983
|
const call = typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
@@ -1669,6 +2014,24 @@ function emitStrictEq(a, b, negate) {
|
|
|
1669
2014
|
const strictB = resolveValType(b, valTypeOf, lookupValType)
|
|
1670
2015
|
if (strictA && strictB && strictA !== strictB && (STRICT_PRIM.has(strictA) || STRICT_PRIM.has(strictB)))
|
|
1671
2016
|
return emitNum(negate ? 1 : 0)
|
|
2017
|
+
// Both sides statically BOOL: compare TRUTH VALUES, not raw bits — a boolean's
|
|
2018
|
+
// carrier varies by source (raw 0/1 from locals/comparisons, TRUE/FALSE atom out
|
|
2019
|
+
// of slots/hashes/JSON) and truthyIR normalizes both representations.
|
|
2020
|
+
if (strictA === VAL.BOOL && strictB === VAL.BOOL) {
|
|
2021
|
+
const cmp = typed(['i32.eq', truthyIR(emit(a)), truthyIR(emit(b))], 'i32')
|
|
2022
|
+
return negate ? typed(['i32.eqz', cmp], 'i32') : cmp
|
|
2023
|
+
}
|
|
2024
|
+
// One side statically BOOL, other side dynamic-unknown: strict equality is
|
|
2025
|
+
// IDENTITY. An unknown operand carries booleans as their TRUE/FALSE atom
|
|
2026
|
+
// (carrierF64 ingress) while numbers are raw — so `1 === true` must be false
|
|
2027
|
+
// even though the loose lowering's ToNumber would equate them. Compare bits:
|
|
2028
|
+
// the BOOL side boxes to its atom, the unknown side is compared verbatim.
|
|
2029
|
+
if ((strictA === VAL.BOOL) !== (strictB === VAL.BOOL) && (strictA == null || strictB == null)) {
|
|
2030
|
+
const va = strictA === VAL.BOOL ? carrierF64(a, emit(a)) : asF64(emit(a))
|
|
2031
|
+
const vb = strictB === VAL.BOOL ? carrierF64(b, emit(b)) : asF64(emit(b))
|
|
2032
|
+
const cmp = typed(['i64.eq', ['i64.reinterpret_f64', va], ['i64.reinterpret_f64', vb]], 'i32')
|
|
2033
|
+
return negate ? typed(['i32.eqz', cmp], 'i32') : cmp
|
|
2034
|
+
}
|
|
1672
2035
|
// Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
|
|
1673
2036
|
return emitter[negate ? '!=' : '=='](a, b)
|
|
1674
2037
|
}
|
|
@@ -1687,6 +2050,16 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
1687
2050
|
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1688
2051
|
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1689
2052
|
if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
|
|
2053
|
+
// Literal-mixed compare is MATHEMATICAL per spec (BigInt vs Number) — 5n > 3
|
|
2054
|
+
// must not compare raw NaN-box bits. Coerce through f64 (exact for literal
|
|
2055
|
+
// magnitudes); an unknown counterpart keeps the same-rep i64 contract
|
|
2056
|
+
// (kernel carriers' NUMBER is a kind-default, not a proof).
|
|
2057
|
+
if ((vta === VAL.BIGINT) !== (vtb === VAL.BIGINT) && numLiteralNode(vta === VAL.BIGINT ? b : a)) {
|
|
2058
|
+
const conv = (node, v, isBig) => isBig
|
|
2059
|
+
? typed([bigintUnsignedBound(node) ? 'f64.convert_i64_u' : 'f64.convert_i64_s', asI64(v)], 'f64')
|
|
2060
|
+
: toNumF64(node, asF64(v))
|
|
2061
|
+
return typed([`f64.${f64op}`, conv(a, va, vta === VAL.BIGINT), conv(b, vb, vtb === VAL.BIGINT)], 'i32')
|
|
2062
|
+
}
|
|
1690
2063
|
const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
|
|
1691
2064
|
return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
|
|
1692
2065
|
}
|
|
@@ -1919,6 +2292,27 @@ function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1919
2292
|
const acc = `${T}acc${ctx.func.uniq++}`
|
|
1920
2293
|
ctx.func.locals.set(acc, 'f64')
|
|
1921
2294
|
const ir = [['local.set', `$${acc}`, asF64(emit(objArg))]]
|
|
2295
|
+
if (reverse) {
|
|
2296
|
+
// unshift(a, b, ...s): ES yields [a, b, ...s, ...existing]. Per-element
|
|
2297
|
+
// PREPENDS must run right-to-left over the WHOLE argument list — spread
|
|
2298
|
+
// elements first (end→start), the normal args last — or the spread lands
|
|
2299
|
+
// in front of the normals ([...s, a, b, ...] — the order bug that broke
|
|
2300
|
+
// the kernel's own `inject.unshift(setBase, ...stores)`). Argument
|
|
2301
|
+
// EVALUATION order stays left-to-right: normals spill to temps first.
|
|
2302
|
+
const temps = parsed.normal.map((a) => {
|
|
2303
|
+
const t = `${T}usv${ctx.func.uniq++}`
|
|
2304
|
+
ctx.func.locals.set(t, 'f64')
|
|
2305
|
+
ir.push(['local.set', `$${t}`, asF64(emitAsValue(() => emit(a)))])
|
|
2306
|
+
return t
|
|
2307
|
+
})
|
|
2308
|
+
ir.push(...emitSpreadElementLoop(parsed.spreads[0].expr, (arr, idx) => {
|
|
2309
|
+
const body = asF64(emitAsValue(() => methodEmitter(objArg, ['[]', arr, idx])))
|
|
2310
|
+
return [['drop', body]]
|
|
2311
|
+
}, { reverse: true }))
|
|
2312
|
+
if (temps.length) ir.push(['drop', asF64(emitAsValue(() => methodEmitter(objArg, ...temps)))])
|
|
2313
|
+
ir.push(asF64(emit(objArg)))
|
|
2314
|
+
return block64(...ir)
|
|
2315
|
+
}
|
|
1922
2316
|
if (parsed.normal.length > 0) {
|
|
1923
2317
|
const r = asF64(emitAsValue(() => methodEmitter(objArg, ...parsed.normal)))
|
|
1924
2318
|
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
@@ -1943,6 +2337,42 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1943
2337
|
if (acc) ctx.func.locals.set(acc, 'f64')
|
|
1944
2338
|
const recv = inPlace ? objArg : acc
|
|
1945
2339
|
const ir = inPlace ? [] : [['local.set', `$${acc}`, asF64(emit(objArg))]]
|
|
2340
|
+
if (method === 'unshift') {
|
|
2341
|
+
// Prepends compose right-to-left (see emitSingleSpreadMethodCall's reverse
|
|
2342
|
+
// arm). Evaluation order stays left-to-right: spill every segment first —
|
|
2343
|
+
// normal args to value temps, each spread's source array to a temp — then
|
|
2344
|
+
// walk the segments END→START, spreads iterating end→start, each normal
|
|
2345
|
+
// batch prepended through the multi-arg emitter (which lands its own args
|
|
2346
|
+
// in argument order).
|
|
2347
|
+
const segs = []
|
|
2348
|
+
for (const item of combined) {
|
|
2349
|
+
if (Array.isArray(item) && item[0] === '__spread') {
|
|
2350
|
+
const t = `${T}ussp${ctx.func.uniq++}`
|
|
2351
|
+
ctx.func.locals.set(t, 'f64')
|
|
2352
|
+
ir.push(['local.set', `$${t}`, asF64(emitAsValue(() => emit(item[1])))])
|
|
2353
|
+
segs.push(['spread', t])
|
|
2354
|
+
} else {
|
|
2355
|
+
const t = `${T}usv${ctx.func.uniq++}`
|
|
2356
|
+
ctx.func.locals.set(t, 'f64')
|
|
2357
|
+
ir.push(['local.set', `$${t}`, asF64(emitAsValue(() => emit(item)))])
|
|
2358
|
+
if (segs.length && segs[segs.length - 1][0] === 'batch') segs[segs.length - 1].push(t)
|
|
2359
|
+
else segs.push(['batch', t])
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
2363
|
+
const [kind, ...temps] = segs[i]
|
|
2364
|
+
if (kind === 'spread') {
|
|
2365
|
+
ir.push(...emitSpreadElementLoop(temps[0], (arr, idx) => {
|
|
2366
|
+
const body = asF64(emitAsValue(() => methodEmitter(objArg, ['[]', arr, idx])))
|
|
2367
|
+
return [['drop', body]]
|
|
2368
|
+
}, { reverse: true }))
|
|
2369
|
+
} else {
|
|
2370
|
+
ir.push(['drop', asF64(emitAsValue(() => methodEmitter(objArg, ...temps)))])
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
ir.push(asF64(emit(objArg)))
|
|
2374
|
+
return block64(...ir)
|
|
2375
|
+
}
|
|
1946
2376
|
let batch = []
|
|
1947
2377
|
const flushBatch = () => {
|
|
1948
2378
|
if (!batch.length) return
|
|
@@ -1983,11 +2413,14 @@ function emitMethodCallSpread(objArg, methodEmitter, parsed, method) {
|
|
|
1983
2413
|
* the nullish-guard scaffold stays in one place. */
|
|
1984
2414
|
function withNullGuard(headExpr, body, tag = 'ng') {
|
|
1985
2415
|
const t = temp(tag)
|
|
2416
|
+
// asF64 on the taken arm: the continuation may come back i32-narrowed (an
|
|
2417
|
+
// int-certain slot read at O0 kept its raw i32), and the f64-typed if would
|
|
2418
|
+
// fail validation ("type error in fallthru: expected f64, got i32").
|
|
1986
2419
|
return block64(
|
|
1987
2420
|
['local.set', `$${t}`, headExpr],
|
|
1988
2421
|
['if', ['result', 'f64'],
|
|
1989
2422
|
['i32.eqz', isNullish(['local.get', `$${t}`])],
|
|
1990
|
-
['then', body(t)],
|
|
2423
|
+
['then', asF64(body(t))],
|
|
1991
2424
|
['else', undefExpr()]])
|
|
1992
2425
|
}
|
|
1993
2426
|
|
|
@@ -2068,6 +2501,11 @@ function tryFnPropCall(callee, obj, method, parsed) {
|
|
|
2068
2501
|
if (ctx.func.names.has(fname)) {
|
|
2069
2502
|
const func = ctx.func.map.get(fname)
|
|
2070
2503
|
const emittedArgs = emitCallArgs(parsed.normal, func.sig.params)
|
|
2504
|
+
// Drop extras like the plain-call path (emit.js regular-call arm): the dyn
|
|
2505
|
+
// closure ABI absorbed over-arity (`parse.enter?.(p, end)` on a 0-param
|
|
2506
|
+
// hook), but a devirtualized direct call pushes exactly sig arity — extras
|
|
2507
|
+
// would be stack leftovers (asi.js's parse.enter broke the self-host here).
|
|
2508
|
+
if (emittedArgs.length > func.sig.params.length) emittedArgs.length = func.sig.params.length
|
|
2071
2509
|
return attachSigMeta(typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0]), func.sig)
|
|
2072
2510
|
}
|
|
2073
2511
|
}
|
|
@@ -2235,6 +2673,49 @@ function tryGenericEmitter({ obj, method, parsed, vt, callMethod }) {
|
|
|
2235
2673
|
// correctly instead of being hijacked by `Array.prototype.{find,map,…}`.
|
|
2236
2674
|
const objectShadow = vt === VAL.OBJECT || vt === VAL.HASH
|
|
2237
2675
|
if (ctx.core.emit[`.${method}`] && !collectionMisfit && !strIndexMisfit && !objectShadow) {
|
|
2676
|
+
// Statically-UNKNOWN receiver: an OWN property named like the builtin shadows it
|
|
2677
|
+
// (ES prototype semantics) — the runtime analogue of `objectShadow` above. Without
|
|
2678
|
+
// this fork, subscript's `d.map(a)` descriptor mapper (or any user method colliding
|
|
2679
|
+
// with Array.prototype names) is hijacked by the builtin and reads array layout off
|
|
2680
|
+
// an object. Probe the dyn-prop sidecar: own closure wins, else the builtin runs —
|
|
2681
|
+
// emitted ONCE (the builtin bodies are large inline emitters; a dual-arm emission
|
|
2682
|
+
// doubled closure-heavy golden sizes). __dyn_get_expr guards real-number receivers
|
|
2683
|
+
// itself, so no f===f pre-fork is needed. Gated on the string module (the probe key
|
|
2684
|
+
// is a string literal): a string-less program has no user string props to shadow.
|
|
2685
|
+
if (vt == null && ctx.closure.call && !parsed.hasSpread && ctx.core.emit.str) {
|
|
2686
|
+
// HOISTED override probe: for a stable module-global receiver (the same
|
|
2687
|
+
// proof as charCodeAt shape-1b — never assigned in this function, and the
|
|
2688
|
+
// body's only calls are .charCodeAt, so nothing that runs here can change
|
|
2689
|
+
// the receiver or its props), the probe's answer is loop-invariant.
|
|
2690
|
+
// Register a per-(receiver, method) entry-prologue probe (drained by
|
|
2691
|
+
// collectParamInits) and reduce the per-site cost to one predictable
|
|
2692
|
+
// branch on the cached i32 + the lean builtin arm. jessie's space paid
|
|
2693
|
+
// the full 3-frame probe per CHARACTER without this.
|
|
2694
|
+
if (typeof obj === 'string' && ctx.func.charDecompGlobals && isGlobal(obj)
|
|
2695
|
+
&& ctx.func.body && !isReassigned(ctx.func.body, obj) && bodyOnlyCharCodeAtCalls(ctx.func.body)) {
|
|
2696
|
+
const key = `${obj}#${method}`
|
|
2697
|
+
let ph = (ctx.func.probeHoist ??= new Map()).get(key)
|
|
2698
|
+
if (!ph) {
|
|
2699
|
+
const ovr = `${obj}$ovr$${method}`, is = `${obj}$ovrIs$${method}`
|
|
2700
|
+
ctx.func.locals.set(ovr, 'f64')
|
|
2701
|
+
ctx.func.locals.set(is, 'i32')
|
|
2702
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
2703
|
+
ph = { ovr, is, recvIR: () => asF64(emit(obj)), keyIR: () => asI64(emit(['str', method])) }
|
|
2704
|
+
ctx.func.probeHoist.set(key, ph)
|
|
2705
|
+
}
|
|
2706
|
+
return typed(['if', ['result', 'f64'], ['local.get', `$${ph.is}`],
|
|
2707
|
+
['then', ctx.closure.call(typed(['local.get', `$${ph.ovr}`], 'f64'), parsed.normal)],
|
|
2708
|
+
['else', asF64(callMethod(obj, ctx.core.emit[`.${method}`]))]], 'f64')
|
|
2709
|
+
}
|
|
2710
|
+
// Fallback arm: a bare-name receiver re-references the ORIGINAL binding
|
|
2711
|
+
// (variable reads are pure) instead of the probe's spilled temp — so a
|
|
2712
|
+
// module-global string receiver reaches the ABI op as `global.get` and
|
|
2713
|
+
// the charCodeAt shape-1b entry decomposition can fire (the layered-
|
|
2714
|
+
// parser `cur.charCodeAt(idx)` hot shape; a local temp would hide it).
|
|
2715
|
+
return sidecarOverride(emit(obj), asI64(emit(['str', method])),
|
|
2716
|
+
(p) => ctx.closure.call(typed(['local.get', `$${p}`], 'f64'), parsed.normal),
|
|
2717
|
+
(o) => asF64(callMethod(typeof obj === 'string' ? obj : o, ctx.core.emit[`.${method}`])))
|
|
2718
|
+
}
|
|
2238
2719
|
return callMethod(obj, ctx.core.emit[`.${method}`])
|
|
2239
2720
|
}
|
|
2240
2721
|
}
|
|
@@ -2251,7 +2732,15 @@ function tryDynamicPropCall({ obj, method, parsed, vt }) {
|
|
|
2251
2732
|
const propTmp = temp('mprop')
|
|
2252
2733
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2253
2734
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
2254
|
-
|
|
2735
|
+
// Primitive receivers skip the override probe — see sidecarOverride (ir.js).
|
|
2736
|
+
const propRead = typed(['if', ['result', 'f64'],
|
|
2737
|
+
['i32.and',
|
|
2738
|
+
['f64.ne', ['local.get', `$${objTmp}`], ['local.get', `$${objTmp}`]],
|
|
2739
|
+
['i64.ne',
|
|
2740
|
+
['i64.and', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['i64.const', i64Hex(BigInt(LAYOUT.TAG_MASK) << BigInt(LAYOUT.TAG_SHIFT))]],
|
|
2741
|
+
['i64.const', i64Hex(BigInt(PTR.STRING) << BigInt(LAYOUT.TAG_SHIFT))]]],
|
|
2742
|
+
['then', ['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]]],
|
|
2743
|
+
['else', undefExpr()]], 'f64')
|
|
2255
2744
|
const closureOnly = usesDynProps(vt) || ctx.transform.host === 'wasi'
|
|
2256
2745
|
inc('__dyn_get_expr', '__ptr_type')
|
|
2257
2746
|
if (!closureOnly) { inc('__ext_call'); ctx.features.external = true }
|
|
@@ -2275,6 +2764,17 @@ function tryDynamicPropCall({ obj, method, parsed, vt }) {
|
|
|
2275
2764
|
|
|
2276
2765
|
// 12. Unknown callee — assume external method. Total: always returns.
|
|
2277
2766
|
function externalMethodFallback({ obj, method, parsed }) {
|
|
2767
|
+
// A receiver with a KNOWN jz-native kind (linear-memory value) has no host
|
|
2768
|
+
// prototype behind it — every native strategy above declined, so the method
|
|
2769
|
+
// is simply missing and __ext_call could only marshal garbage / return
|
|
2770
|
+
// undefined at runtime. Fail at compile in every mode, like strict does.
|
|
2771
|
+
// OBJECT/HASH are exempt: their property sets are user data, not a closed
|
|
2772
|
+
// builtin table — `o.x()` may resolve to a closure slot at runtime (and when
|
|
2773
|
+
// it doesn't, the documented lowering is undefined, host's TypeError shape).
|
|
2774
|
+
// (Host values carry no static kind, so a null kind keeps the fallback.)
|
|
2775
|
+
const vt = typeof obj === 'string' ? (lookupValType(obj) ?? valTypeOf(obj)) : valTypeOf(obj)
|
|
2776
|
+
if (vt != null && vt !== VAL.OBJECT && vt !== VAL.HASH)
|
|
2777
|
+
err(`\`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` — '${method}' is not implemented for a ${vt} receiver, and jz-native values have no host fallthrough (the call could only yield undefined). Check the method name; if it's a real JS API, it's a missing jz builtin.`)
|
|
2278
2778
|
if (ctx.transform.strict)
|
|
2279
2779
|
err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type falls through to host \`__ext_call\`. Annotate the receiver type or pass { strict: false }.`)
|
|
2280
2780
|
// Under wasi there is no host `__ext_call` — the call lowers to a
|
|
@@ -2424,6 +2924,14 @@ function emitDirectFunctionCall(callee, parsed, callArgs) {
|
|
|
2424
2924
|
|
|
2425
2925
|
// Regular function call without rest params
|
|
2426
2926
|
if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
|
|
2927
|
+
// Speculative typed dispatch (narrow's speculateTypedParams): route the call
|
|
2928
|
+
// through a per-arg tag guard to the typed clone; a miss takes the original
|
|
2929
|
+
// call unchanged. Guard positions must be covered by real args — a site
|
|
2930
|
+
// relying on arity-padding at a speculated position would guard `undefined`
|
|
2931
|
+
// every call, pure loss.
|
|
2932
|
+
const spec = func && ctx.types.specFns?.get(callee)
|
|
2933
|
+
if (spec && func.sig.results.length === 1 && spec.guards.every(g => g.k < parsed.normal.length))
|
|
2934
|
+
return emitSpeculativeCall(callee, spec, parsed.normal, func)
|
|
2427
2935
|
// Pad missing args with `undefined` so default-param init triggers per spec
|
|
2428
2936
|
// (only undefined, not null, should trigger defaults). Drop extras to match
|
|
2429
2937
|
// JS calling convention — emitting them anyway produces an invalid call
|
|
@@ -2432,7 +2940,7 @@ function emitDirectFunctionCall(callee, parsed, callArgs) {
|
|
|
2432
2940
|
// legitimate 0-arity callee isn't bypassed.
|
|
2433
2941
|
const params = func?.sig.params ?? []
|
|
2434
2942
|
const args = func ? emitCallArgs(parsed.normal, params)
|
|
2435
|
-
: parsed.normal.map(a => coerceArg(emit(a), undefined))
|
|
2943
|
+
: parsed.normal.map(a => coerceArg(emit(a), undefined, a))
|
|
2436
2944
|
if (func && args.length > params.length) args.length = params.length
|
|
2437
2945
|
// Multi-value return: materialize as heap array (caller expects single pointer).
|
|
2438
2946
|
// Reuse the canonical comma-wrapped arg slot — materializeMulti re-reads args
|
|
@@ -2484,7 +2992,10 @@ function tryDirectClosureCall(callee, parsed) {
|
|
|
2484
2992
|
mn.set(bodyName, prev === undefined ? n : (n < prev ? n : prev))
|
|
2485
2993
|
// Body signature is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64.
|
|
2486
2994
|
// We pass the closure NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
|
|
2487
|
-
|
|
2995
|
+
// Slots are untyped boxed-value positions: a BOOL arg crosses as its atom box
|
|
2996
|
+
// (the paramTypes numeric lattice above already poisons on non-NUMBER args, so
|
|
2997
|
+
// the body never assumes raw numerics for these slots).
|
|
2998
|
+
const slots = parsed.normal.map(a => carrierF64(a, emit(a)))
|
|
2488
2999
|
while (slots.length < W) slots.push(undefExpr())
|
|
2489
3000
|
return typed(['call', `$${bodyName}`,
|
|
2490
3001
|
asF64(emit(callee)),
|
|
@@ -2492,16 +3003,37 @@ function tryDirectClosureCall(callee, parsed) {
|
|
|
2492
3003
|
...slots], 'f64')
|
|
2493
3004
|
}
|
|
2494
3005
|
|
|
3006
|
+
/** Tag the generic call_indirect of `constFnArr[idx](args)` for the optimizer's
|
|
3007
|
+
* devirtConstFnArrayCalls pass (optimize/index.js). The candidate set — a
|
|
3008
|
+
* module-const array of capture-free arrows — is recorded when the DECL emits,
|
|
3009
|
+
* which happens in buildStartFn AFTER function bodies emit; so emit only marks
|
|
3010
|
+
* the site (receiver name), and the rewrite runs in optimizeFunc where the
|
|
3011
|
+
* facts are complete. */
|
|
3012
|
+
const tagFnArrayDispatch = (ir, arrName) => {
|
|
3013
|
+
const findCI = (n) => {
|
|
3014
|
+
if (!Array.isArray(n)) return null
|
|
3015
|
+
if (n[0] === 'call_indirect') return n
|
|
3016
|
+
for (let i = 1; i < n.length; i++) { const f = findCI(n[i]); if (f) return f }
|
|
3017
|
+
return null
|
|
3018
|
+
}
|
|
3019
|
+
const ci = findCI(ir)
|
|
3020
|
+
if (ci) ci.dvArr = arrName
|
|
3021
|
+
return ir
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2495
3024
|
/** Generic closure call: callee is a value holding a NaN-boxed closure pointer.
|
|
2496
3025
|
* Uniform convention: fn.call packs all args into an array and trampolines. */
|
|
2497
3026
|
function emitGenericClosureCall(callee, parsed) {
|
|
3027
|
+
const dvName = ctx.transform.optimize && !parsed.hasSpread &&
|
|
3028
|
+
Array.isArray(callee) && callee[0] === '[]' && typeof callee[1] === 'string' ? callee[1] : null
|
|
2498
3029
|
if (parsed.hasSpread) {
|
|
2499
3030
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2500
3031
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
2501
3032
|
// Pass pre-built array as single already-emitted arg
|
|
2502
3033
|
return ctx.closure.call(emit(callee), [arrayIR], true)
|
|
2503
3034
|
}
|
|
2504
|
-
|
|
3035
|
+
const ir = ctx.closure.call(emit(callee), parsed.normal)
|
|
3036
|
+
return dvName ? tagFnArrayDispatch(ir, dvName) : ir
|
|
2505
3037
|
}
|
|
2506
3038
|
|
|
2507
3039
|
/** Last-resort fallback: assume `(call $callee args)` against an import / unknown
|
|
@@ -2547,6 +3079,21 @@ function compoundAssign(name, val, f64op, i32op) {
|
|
|
2547
3079
|
return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
|
|
2548
3080
|
}
|
|
2549
3081
|
|
|
3082
|
+
// Ring 0.3 (re-landed after the dispatch rework dropped the uncommitted original):
|
|
3083
|
+
// JS makes BigInt⊕Number arithmetic a TypeError. Enforce it exactly where the mix
|
|
3084
|
+
// is PROVABLE from source — one side proven BIGINT, the other a NUMERIC LITERAL —
|
|
3085
|
+
// and stay permissive otherwise: kernel carriers read NUMBER as a kind-DEFAULT
|
|
3086
|
+
// (not a proof), so rejecting proven-BIGINT × default-NUMBER breaks sound kernels.
|
|
3087
|
+
const numLiteralNode = (n) =>
|
|
3088
|
+
typeof n === 'number' || (Array.isArray(n) && n[0] == null && typeof n[1] === 'number')
|
|
3089
|
+
function bigintMixReject(op, a, b) {
|
|
3090
|
+
if (b === undefined) return
|
|
3091
|
+
const aBig = valTypeOf(a) === VAL.BIGINT, bBig = valTypeOf(b) === VAL.BIGINT
|
|
3092
|
+
if (aBig === bBig) return
|
|
3093
|
+
if (numLiteralNode(aBig ? b : a))
|
|
3094
|
+
err(`Cannot mix BigInt and other types in \`${op}\` (TypeError in JS) — convert explicitly with BigInt() or Number()`)
|
|
3095
|
+
}
|
|
3096
|
+
|
|
2550
3097
|
// === Core emitter dispatch table ===
|
|
2551
3098
|
// ctx.core.emit is seeded with a flat copy of this object on reset;
|
|
2552
3099
|
// language modules add or override ops on ctx.core.emit directly.
|
|
@@ -2584,8 +3131,15 @@ export const emitter = {
|
|
|
2584
3131
|
...results.flatMap(dropSpread),
|
|
2585
3132
|
['f64.const', 0])
|
|
2586
3133
|
}
|
|
2587
|
-
|
|
3134
|
+
const seq = typed(['block', ['result', last.type],
|
|
2588
3135
|
...results.slice(0, -1).flatMap(dropSpread), last], last.type)
|
|
3136
|
+
// The sequence's VALUE is `last` — carry its value metadata, or downstream
|
|
3137
|
+
// coercions misread the carrier: an i32 OBJECT/CLOSURE pointer without its
|
|
3138
|
+
// ptrKind gets f64.convert_i32_s'd (`return (fn.a = 1, fn)` returned the raw
|
|
3139
|
+
// heap offset as a number). Same bug-class as the ternary's tagPtr (below).
|
|
3140
|
+
if (last.ptrKind != null) { seq.ptrKind = last.ptrKind; if (last.ptrAux != null) seq.ptrAux = last.ptrAux }
|
|
3141
|
+
if (last.unsigned) seq.unsigned = last.unsigned
|
|
3142
|
+
return seq
|
|
2589
3143
|
},
|
|
2590
3144
|
'let': emitDecl,
|
|
2591
3145
|
'const': emitDecl,
|
|
@@ -2683,7 +3237,25 @@ export const emitter = {
|
|
|
2683
3237
|
if (expr == null) return [...finalizers, typed(['return', undefExpr()], 'void')]
|
|
2684
3238
|
const rt = ctx.func.current?.results[0] || 'f64'
|
|
2685
3239
|
const pk = ctx.func.current?.ptrKind
|
|
2686
|
-
|
|
3240
|
+
// Emit ONCE, before branching on pk — self-host miscompile: the equivalent inline
|
|
3241
|
+
// form `pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)`
|
|
3242
|
+
// (emit(expr) repeated once per ternary arm, only one ever executing) is behaviorally
|
|
3243
|
+
// identical in JS but the self-hosted kernel drops the f64.convert_i32_s/u rebox on
|
|
3244
|
+
// the taken arm's result — an i32-typed return tail comes back bare (unconverted) in
|
|
3245
|
+
// a non-narrowed (f64-result) function, so the wasm validator sees "expected f64, got
|
|
3246
|
+
// i32" at every return site shaped like `return (expr)|0` inside a function whose
|
|
3247
|
+
// result the narrower left at f64 (e.g. blocked by an unrelated same-name shadow
|
|
3248
|
+
// elsewhere — narrowI32Results itself is unaffected either way). compile/index.js's
|
|
3249
|
+
// sibling call site (`const ir = emit(body); … ptrKind != null ? asPtrOffset(ir, …) :
|
|
3250
|
+
// asParamType(ir, …)`) already used this materialize-then-branch shape and was never
|
|
3251
|
+
// affected — mirroring it here is both the fix and the more idiomatic form (DRY: one
|
|
3252
|
+
// emit call instead of a copy per arm). Root cause not fully localized beyond "the
|
|
3253
|
+
// self-hosted kernel, at every optimize level 0-2, treats a value produced by a call
|
|
3254
|
+
// repeated textually across both arms of a ternary differently from one materialized
|
|
3255
|
+
// to a local first" — pinned in test/parser-bugs.js rather than chased further into
|
|
3256
|
+
// the kernel's own call/branch codegen. See .work/todo.md (groundtruth archive).
|
|
3257
|
+
const emitted = emit(expr)
|
|
3258
|
+
const ir = pk != null ? asPtrOffset(emitted, pk) : asParamType(emitted, rt)
|
|
2687
3259
|
const ty = pk != null ? 'i32' : rt
|
|
2688
3260
|
const tcoed = tcoTailRewrite(ir, ty)
|
|
2689
3261
|
if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
|
|
@@ -2713,7 +3285,15 @@ export const emitter = {
|
|
|
2713
3285
|
inc('__to_num')
|
|
2714
3286
|
return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
|
|
2715
3287
|
}
|
|
2716
|
-
|
|
3288
|
+
// Self-accumulation `x = x + …` (incl. desugared `x += …`): the new value REPLACES x, so x's
|
|
3289
|
+
// old buffer is dead — the one context where a string concat may bump-EXTEND it in place. The
|
|
3290
|
+
// `+` handler reads this flag for its immediate concat; nested operands clear it (not the target).
|
|
3291
|
+
const selfAccum = Array.isArray(val) && val[0] === '+' && val[1] === name
|
|
3292
|
+
const prevSA = ctx.func._selfAccumConcat
|
|
3293
|
+
ctx.func._selfAccumConcat = selfAccum ? name : null
|
|
3294
|
+
const ev = emit(val)
|
|
3295
|
+
ctx.func._selfAccumConcat = prevSA
|
|
3296
|
+
return writeVar(name, ev, void_)
|
|
2717
3297
|
},
|
|
2718
3298
|
|
|
2719
3299
|
// Compound assignments: read-modify-write with type coercion
|
|
@@ -2743,6 +3323,8 @@ export const emitter = {
|
|
|
2743
3323
|
if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
|
|
2744
3324
|
return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
|
|
2745
3325
|
},
|
|
3326
|
+
// `**` is always f64 (and has its own const-exponent lowering) — full desugar.
|
|
3327
|
+
'**=': (name, val) => emit(['=', name, ['**', name, val]]),
|
|
2746
3328
|
|
|
2747
3329
|
// Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
|
|
2748
3330
|
...Object.fromEntries([
|
|
@@ -2803,15 +3385,31 @@ export const emitter = {
|
|
|
2803
3385
|
// Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
|
|
2804
3386
|
'+': (a, b) => {
|
|
2805
3387
|
if (ctx.func._expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
|
|
3388
|
+
// A self-accumulation `a = a + …` lets the concat bump-EXTEND `a` in place (a is dead-after).
|
|
3389
|
+
// Read it for THIS concat, then clear so nested operands (not the accumulation target) stay fresh.
|
|
3390
|
+
const selfAccum = typeof a === 'string' && a === ctx.func._selfAccumConcat
|
|
3391
|
+
ctx.func._selfAccumConcat = null
|
|
3392
|
+
// String concat-CHAIN fusion: `i + ',' + name + ',' + v + '\n'` is
|
|
3393
|
+
// left-associated pairwise `+`, and pairwise lowering re-copies the whole
|
|
3394
|
+
// growing prefix at every step (triangular bytes moved, one fresh heap
|
|
3395
|
+
// buffer per `+`). Flatten the chain and emit ONE measure→alloc→copy pass
|
|
3396
|
+
// instead. Fusion crosses a nested `+` only when a side is statically
|
|
3397
|
+
// string-ish (so a numeric `1 + 2 + s` keeps its numeric ADD as a single
|
|
3398
|
+
// leaf); ToString order stays left-to-right (leaves evaluate in order).
|
|
3399
|
+
// A self-accumulating head (`line = line + a + b`) keeps leaf 0 pairwise
|
|
3400
|
+
// so the O(1) bump-extend accumulator survives — only the tail fuses.
|
|
3401
|
+
{
|
|
3402
|
+
const fused = tryConcatChain(a, b, selfAccum)
|
|
3403
|
+
if (fused) return fused
|
|
3404
|
+
}
|
|
2806
3405
|
// String concatenation: pure string operands skip generic ToString coercion.
|
|
2807
3406
|
const vtA = valTypeOf(a)
|
|
2808
3407
|
const vtB = valTypeOf(b)
|
|
2809
3408
|
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
2810
|
-
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
|
|
2811
|
-
//
|
|
2812
|
-
//
|
|
2813
|
-
|
|
2814
|
-
if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
3409
|
+
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction + generic concat dispatch
|
|
3410
|
+
// when rhs is a string-index. The byte flows straight from __char_at into memory and bump-
|
|
3411
|
+
// EXTENDS the heap-top lhs — so only when proven self-accumulating (else it mutates a live s).
|
|
3412
|
+
if (selfAccum && Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
2815
3413
|
if (valTypeOf(b[1]) === VAL.STRING) {
|
|
2816
3414
|
inc('__str_append_byte', '__char_at')
|
|
2817
3415
|
return typed(['call', '$__str_append_byte',
|
|
@@ -2820,7 +3418,7 @@ export const emitter = {
|
|
|
2820
3418
|
], 'f64')
|
|
2821
3419
|
}
|
|
2822
3420
|
}
|
|
2823
|
-
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
|
|
3421
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx, selfAccum), 'f64')
|
|
2824
3422
|
}
|
|
2825
3423
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
2826
3424
|
// An OBJECT operand coerces via ToPrimitive(string) at compile time —
|
|
@@ -2840,24 +3438,58 @@ export const emitter = {
|
|
|
2840
3438
|
const coercionFree = (vt) => vt === VAL.STRING || vt === VAL.OBJECT || vt === VAL.BOOL
|
|
2841
3439
|
const cfA = coercionFree(vtA), cfB = coercionFree(vtB)
|
|
2842
3440
|
const strI64 = (n) => typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2843
|
-
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2844
|
-
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx), 'f64')
|
|
2845
|
-
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx), 'f64')
|
|
2846
|
-
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
3441
|
+
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
3442
|
+
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx, selfAccum), 'f64')
|
|
3443
|
+
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
3444
|
+
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2847
3445
|
}
|
|
2848
|
-
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
3446
|
+
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT) {
|
|
3447
|
+
bigintMixReject('+', a, b)
|
|
2849
3448
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
3449
|
+
}
|
|
2850
3450
|
// Runtime string dispatch when at least one side could be a string. When one side has
|
|
2851
3451
|
// a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
|
|
2852
3452
|
// chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
|
|
2853
3453
|
// operand needs the runtime check.
|
|
2854
3454
|
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
2855
3455
|
const tA = temp('add'), tB = temp('add')
|
|
3456
|
+
// Fully-untyped `+`: the string arm is a runtime-guarded cold path that the engine reaches
|
|
3457
|
+
// only if BOTH operands are strings at runtime, so it keeps the bump-extend `__str_concat`
|
|
3458
|
+
// (its body stays out-of-line — folding it to the smaller _fresh twin would inline this
|
|
3459
|
+
// never-numeric branch into every hot integer loop). The demonstrated `t = s + "lit"` mutation
|
|
3460
|
+
// is a TYPED concat (handled by concatRaw above); a both-untyped self-mutation stays the
|
|
3461
|
+
// documented rare-aliasing tradeoff. Self-accumulation is still safe to extend.
|
|
2856
3462
|
inc('__str_concat', '__is_str_key')
|
|
2857
|
-
const
|
|
2858
|
-
const
|
|
3463
|
+
const eA = vtA == null ? asF64(emit(a)) : null
|
|
3464
|
+
const eB = vtB == null ? asF64(emit(b)) : null
|
|
3465
|
+
const checkA = eA ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, eA]]] : null
|
|
3466
|
+
const checkB = eB ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, eB]]] : null
|
|
2859
3467
|
const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
|
|
2860
|
-
|
|
3468
|
+
// Numeric arm: an UNKNOWN operand may still be a non-string NaN-box (bool
|
|
3469
|
+
// atom, null) whose ToNumber is not its raw bits — `true + 1` is 2,
|
|
3470
|
+
// `null + 1` is 1. Guard with the self-compare (every non-NaN f64 IS its
|
|
3471
|
+
// own ToNumber; two inline ops on the hot path); the cold arm is the
|
|
3472
|
+
// inline ATOM ladder, not __to_num — strings can't reach here (the
|
|
3473
|
+
// __is_str_key fork above took them) and objects stay jz-permissive NaN
|
|
3474
|
+
// either way, so the full ToNumber (and the number↔string formatter tree
|
|
3475
|
+
// it pins — the dyn-object golden) buys nothing. Skipped when the side is
|
|
3476
|
+
// known-vt (raw carrier by design) or IR-shape numeric (isNumArm — keeps
|
|
3477
|
+
// floatbeat kernels at their box-free ratchet counts).
|
|
3478
|
+
const numSide = (t, e, node) => {
|
|
3479
|
+
if (!e || isNumArm(e, node)) return ['local.get', `$${t}`]
|
|
3480
|
+
const bits = ['i64.reinterpret_f64', ['local.get', `$${t}`]]
|
|
3481
|
+
return ['if', ['result', 'f64'],
|
|
3482
|
+
['f64.eq', ['local.get', `$${t}`], ['local.get', `$${t}`]],
|
|
3483
|
+
['then', ['local.get', `$${t}`]],
|
|
3484
|
+
['else', ['select',
|
|
3485
|
+
['f64.const', 1],
|
|
3486
|
+
['select',
|
|
3487
|
+
['f64.const', 0],
|
|
3488
|
+
['f64.const', 'nan'],
|
|
3489
|
+
['i32.or', ['i64.eq', bits, ['i64.const', FALSE_NAN]], ['i64.eq', bits, ['i64.const', NULL_NAN]]]],
|
|
3490
|
+
['i64.eq', bits, ['i64.const', TRUE_NAN]]]]]
|
|
3491
|
+
}
|
|
3492
|
+
const add = ['f64.add', numSide(tA, eA, a), numSide(tB, eB, b)]
|
|
2861
3493
|
if (checkA && checkB) {
|
|
2862
3494
|
return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
|
|
2863
3495
|
}
|
|
@@ -2882,10 +3514,12 @@ export const emitter = {
|
|
|
2882
3514
|
},
|
|
2883
3515
|
'-': (a, b) => {
|
|
2884
3516
|
if (ctx.func._expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
2885
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3517
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3518
|
+
bigintMixReject('-', a, b)
|
|
2886
3519
|
return b === undefined
|
|
2887
3520
|
? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
2888
3521
|
: fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
|
|
3522
|
+
}
|
|
2889
3523
|
if (b === undefined) return emitNeg(a)
|
|
2890
3524
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
2891
3525
|
if (_f) return _f
|
|
@@ -2898,7 +3532,7 @@ export const emitter = {
|
|
|
2898
3532
|
},
|
|
2899
3533
|
'u+': a => {
|
|
2900
3534
|
if (valTypeOf(a) === VAL.BIGINT)
|
|
2901
|
-
return
|
|
3535
|
+
return err('unary `+` on a BigInt is a TypeError in JS — use Number(x)')
|
|
2902
3536
|
const v = emit(a)
|
|
2903
3537
|
if (v.type === 'i32') return asF64(v)
|
|
2904
3538
|
if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
|
|
@@ -2907,8 +3541,10 @@ export const emitter = {
|
|
|
2907
3541
|
},
|
|
2908
3542
|
'u-': a => emitNeg(a),
|
|
2909
3543
|
'*': (a, b) => {
|
|
2910
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3544
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3545
|
+
bigintMixReject('*', a, b)
|
|
2911
3546
|
return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
|
|
3547
|
+
}
|
|
2912
3548
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
|
|
2913
3549
|
if (_f) return _f
|
|
2914
3550
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
@@ -2924,20 +3560,37 @@ export const emitter = {
|
|
|
2924
3560
|
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
2925
3561
|
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
2926
3562
|
if (isI32Num(va) && isI32Num(vb) && !widensUnsigned(va) && !widensUnsigned(vb) && (mulFitsI32(va, vb) || mulBoundedFaithful(va, vb))) return typed(['i32.mul', va, vb], 'i32')
|
|
3563
|
+
// Typed-element reads arrive PRE-converted (`.typed:[]` returns
|
|
3564
|
+
// f64.convert_i32_{s,u}(loadN)), so the faithful-product gate above never
|
|
3565
|
+
// sees them. Peel the convert to expose the bounded integer source: when
|
|
3566
|
+
// |a|·|b| ≤ 2^31−1 the exact product fits signed i32, so
|
|
3567
|
+
// f64.mul(convert(x), convert(y)) == convert_s(i32.mul(x, y)) in every
|
|
3568
|
+
// consumer context — one int op instead of two converts + f64.mul, and the
|
|
3569
|
+
// i32 product chain is lane-vectorizable. Unsigned converts are safe here
|
|
3570
|
+
// for the same reason: a magnitude-bounded (< 2^31) uint reads the same
|
|
3571
|
+
// signed or unsigned, and the bounded product needs the signed convert.
|
|
3572
|
+
const peeled = (v) => Array.isArray(v) && (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u') && v.length === 2 ? v[1]
|
|
3573
|
+
: isI32Num(v) && !widensUnsigned(v) ? v : null
|
|
3574
|
+
const pa = peeled(va), pb = peeled(vb)
|
|
3575
|
+
if (pa && pb && i32Mag(pa) * i32Mag(pb) <= 0x7fffffff) return typed(['i32.mul', pa, pb], 'i32')
|
|
2927
3576
|
const i32mul = tryI32Arith('i32.mul', '*', a, b, va, vb); if (i32mul) return i32mul
|
|
2928
3577
|
return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2929
3578
|
},
|
|
2930
3579
|
'/': (a, b) => {
|
|
2931
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3580
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3581
|
+
bigintMixReject('/', a, b)
|
|
2932
3582
|
return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
|
|
3583
|
+
}
|
|
2933
3584
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
2934
3585
|
if (_f) return _f
|
|
2935
3586
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
2936
3587
|
return typed(['f64.div', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2937
3588
|
},
|
|
2938
3589
|
'%': (a, b) => {
|
|
2939
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3590
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3591
|
+
bigintMixReject('%', a, b)
|
|
2940
3592
|
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
3593
|
+
}
|
|
2941
3594
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
2942
3595
|
if (_f) return _f
|
|
2943
3596
|
// ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
|
|
@@ -3010,6 +3663,27 @@ export const emitter = {
|
|
|
3010
3663
|
const elseRefs = extractRefinements(a, new Map(), false)
|
|
3011
3664
|
const vb = withRefinements(thenRefs, b, () => emit(b))
|
|
3012
3665
|
const vc = withRefinements(elseRefs, c, () => emit(c))
|
|
3666
|
+
// A BOOL arm beside a non-BOOL, non-NUMBER arm: the merge kills the static
|
|
3667
|
+
// type, so the boolean's identity is observable only through its atom box —
|
|
3668
|
+
// materialize it per-arm here, BEFORE the raw-bit collapses below erase it
|
|
3669
|
+
// (`i ? true : [from, len]` — watr's rec-type marker — must yield TRUE_NAN,
|
|
3670
|
+
// not 1.0). BOOL∪NUMBER stays raw: VT['?:'] carries NUMBER there (the raw
|
|
3671
|
+
// 0/1 IS the bool's ToNumber image — the benign numeric-context lie), and
|
|
3672
|
+
// both-BOOL arms keep vt BOOL and stay raw 0/1 by design.
|
|
3673
|
+
{
|
|
3674
|
+
const vtbM = resolveValType(b, valTypeOf, lookupValType)
|
|
3675
|
+
const vtcM = resolveValType(c, valTypeOf, lookupValType)
|
|
3676
|
+
if ((vtbM === VAL.BOOL) !== (vtcM === VAL.BOOL) &&
|
|
3677
|
+
(vtbM === VAL.BOOL ? vtcM : vtbM) !== VAL.NUMBER) {
|
|
3678
|
+
const fb = vtbM === VAL.BOOL ? boolBoxIR(vb) : asF64(vb)
|
|
3679
|
+
const fc = vtcM === VAL.BOOL ? boolBoxIR(vc) : asF64(vc)
|
|
3680
|
+
const ib = ['i64.reinterpret_f64', fb], ic = ['i64.reinterpret_f64', fc]
|
|
3681
|
+
const bits = isPureIR(fb) && isPureIR(fc)
|
|
3682
|
+
? ['select', ib, ic, cond]
|
|
3683
|
+
: ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
|
|
3684
|
+
return typed(['f64.reinterpret_i64', bits], 'f64')
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3013
3687
|
// `cond ? 1 : 0` is the condition bit itself; `cond ? 0 : 1` its negation. `cond`
|
|
3014
3688
|
// (truthyIR) is already canonical 0/1, so the select + two const arms collapse to
|
|
3015
3689
|
// the bit. (Both arms are literals here, so dropping their emitted IR is side-effect
|
|
@@ -3114,7 +3788,39 @@ export const emitter = {
|
|
|
3114
3788
|
// a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
|
|
3115
3789
|
// (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
|
|
3116
3790
|
const rightRefs = extractRefinements(a, new Map(), true)
|
|
3117
|
-
|
|
3791
|
+
// Guarded indexed-closure dispatch: `(name = V[idx]) && name(args)` —
|
|
3792
|
+
// subscript's parse.step idiom for "look up a handler, call it if present."
|
|
3793
|
+
// `a`'s assignment (emitted normally, above) already set `name` from
|
|
3794
|
+
// `V[idx]`; `b`'s callee, though syntactically a bare identifier, is
|
|
3795
|
+
// PROVABLY that same read — tag the resulting call_indirect the same way
|
|
3796
|
+
// a direct `V[idx](args)` gets tagged (emitGenericClosureCall below), so
|
|
3797
|
+
// devirtConstFnArrayCalls can rewrite it once dyn-closure-tables.js proves
|
|
3798
|
+
// V monomorphic. An unresolved/untagged V just leaves the tag inert.
|
|
3799
|
+
const dvArrName = ctx.transform.optimize && Array.isArray(a) && a[0] === '=' &&
|
|
3800
|
+
typeof a[1] === 'string' && Array.isArray(a[2]) && a[2][0] === '[]' &&
|
|
3801
|
+
typeof a[2][1] === 'string' && ctx.scope.dynFnTableCandidates?.has(a[2][1]) &&
|
|
3802
|
+
Array.isArray(b) && b[0] === '()' && b[1] === a[1] ? a[2][1] : null
|
|
3803
|
+
const emitRight = () => {
|
|
3804
|
+
const vr = withRefinements(rightRefs, b, () => emit(b))
|
|
3805
|
+
return dvArrName ? tagFnArrayDispatch(vr, dvArrName) : vr
|
|
3806
|
+
}
|
|
3807
|
+
// Mixed BOOL/non-NUMBER sides: the merge kills the static type (VT['&&']
|
|
3808
|
+
// returns null), so a surfacing bool must carry its atom box — same rule as
|
|
3809
|
+
// the `?:` arm materialization above. Both-BOOL and BOOL∪NUMBER stay raw.
|
|
3810
|
+
{
|
|
3811
|
+
const vtA = resolveValType(a, valTypeOf, lookupValType)
|
|
3812
|
+
const vtB = resolveValType(b, valTypeOf, lookupValType)
|
|
3813
|
+
if ((vtA === VAL.BOOL) !== (vtB === VAL.BOOL) && (vtA === VAL.BOOL ? vtB : vtA) !== VAL.NUMBER) {
|
|
3814
|
+
const t = temp()
|
|
3815
|
+
const fa = vtA === VAL.BOOL ? boolBoxIR(va) : asF64(va)
|
|
3816
|
+
const fb0 = emitRight()
|
|
3817
|
+
const fb = vtB === VAL.BOOL ? boolBoxIR(fb0) : asF64(fb0)
|
|
3818
|
+
return typed(['if', ['result', 'f64'],
|
|
3819
|
+
toBoolFromEmitted(typed(['local.tee', `$${t}`, fa], 'f64')),
|
|
3820
|
+
['then', fb],
|
|
3821
|
+
['else', ['local.get', `$${t}`]]], 'f64')
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3118
3824
|
// i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
|
|
3119
3825
|
// skip f64 round-trip and __is_truthy call entirely.
|
|
3120
3826
|
if (va.type === 'i32') {
|
|
@@ -3162,6 +3868,21 @@ export const emitter = {
|
|
|
3162
3868
|
// De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
|
|
3163
3869
|
const rightRefs = extractRefinements(a, new Map(), false)
|
|
3164
3870
|
const emitRight = () => withRefinements(rightRefs, b, () => emit(b))
|
|
3871
|
+
// Mixed BOOL/non-NUMBER sides — see `&&`: a surfacing bool carries its atom box.
|
|
3872
|
+
{
|
|
3873
|
+
const vtA = resolveValType(a, valTypeOf, lookupValType)
|
|
3874
|
+
const vtB = resolveValType(b, valTypeOf, lookupValType)
|
|
3875
|
+
if ((vtA === VAL.BOOL) !== (vtB === VAL.BOOL) && (vtA === VAL.BOOL ? vtB : vtA) !== VAL.NUMBER) {
|
|
3876
|
+
const t = temp()
|
|
3877
|
+
const fa = vtA === VAL.BOOL ? boolBoxIR(va) : asF64(va)
|
|
3878
|
+
const fb0 = emitRight()
|
|
3879
|
+
const fb = vtB === VAL.BOOL ? boolBoxIR(fb0) : asF64(fb0)
|
|
3880
|
+
return typed(['if', ['result', 'f64'],
|
|
3881
|
+
toBoolFromEmitted(typed(['local.tee', `$${t}`, fa], 'f64')),
|
|
3882
|
+
['then', ['local.get', `$${t}`]],
|
|
3883
|
+
['else', fb]], 'f64')
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3165
3886
|
if (va.type === 'i32') {
|
|
3166
3887
|
const vb = emitRight()
|
|
3167
3888
|
const t = tempI32()
|
|
@@ -3193,6 +3914,19 @@ export const emitter = {
|
|
|
3193
3914
|
'??': (a, b) => {
|
|
3194
3915
|
const va = emit(a), vb = emit(b)
|
|
3195
3916
|
const t = temp()
|
|
3917
|
+
// Mixed BOOL/non-NUMBER sides — see `&&`: a surfacing bool carries its atom box.
|
|
3918
|
+
{
|
|
3919
|
+
const vtA = resolveValType(a, valTypeOf, lookupValType)
|
|
3920
|
+
const vtB = resolveValType(b, valTypeOf, lookupValType)
|
|
3921
|
+
if ((vtA === VAL.BOOL) !== (vtB === VAL.BOOL) && (vtA === VAL.BOOL ? vtB : vtA) !== VAL.NUMBER) {
|
|
3922
|
+
const fa = vtA === VAL.BOOL ? boolBoxIR(va) : asF64(va)
|
|
3923
|
+
const fb = vtB === VAL.BOOL ? boolBoxIR(vb) : asF64(vb)
|
|
3924
|
+
return typed(['if', ['result', 'f64'],
|
|
3925
|
+
['i32.eqz', isNullish(['local.tee', `$${t}`, fa])],
|
|
3926
|
+
['then', ['local.get', `$${t}`]],
|
|
3927
|
+
['else', fb]], 'f64')
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3196
3930
|
const numA = isNumArm(va, a), numB = isNumArm(vb, b)
|
|
3197
3931
|
// Both arms can surface as the (untyped) result — `a` when non-nullish (a NaN is not
|
|
3198
3932
|
// nullish, so it IS returned), `b` otherwise. Canon a lone-numeric arm; `a` before the
|
|
@@ -3243,8 +3977,10 @@ export const emitter = {
|
|
|
3243
3977
|
...Object.fromEntries([
|
|
3244
3978
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
3245
3979
|
].map(([op, fn]) => [op, (a, b) => {
|
|
3246
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3980
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3981
|
+
bigintMixReject(op, a, b)
|
|
3247
3982
|
return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
|
|
3983
|
+
}
|
|
3248
3984
|
if (op === '|') { // `(x / y) | 0` integer-division idiom → i32.div_s
|
|
3249
3985
|
const divN = intLiteralValue(b) === 0 ? a : intLiteralValue(a) === 0 ? b : null
|
|
3250
3986
|
if (Array.isArray(divN) && divN[0] === '/') { const r = tryIntDivTrunc(divN[1], divN[2]); if (r) return r }
|
|
@@ -3297,13 +4033,17 @@ export const emitter = {
|
|
|
3297
4033
|
// If-conversion (speed tier): `if (cond) x = <cheap pure value>` (no else) → `x = cond ? value
|
|
3298
4034
|
// : x`, which lowers to a branchless `select`. Removes the data-dependent branch (and its
|
|
3299
4035
|
// misprediction) from min/max/clamp reductions — e.g. levenshtein's `if (ins < m) m = ins`,
|
|
3300
|
-
// ~27% faster
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
|
|
4036
|
+
// ~27% faster — and from heapsort's child pick `if (a[c] < a[c+1]) c++`, the canonical
|
|
4037
|
+
// unpredictable compare that costs jz on x86 (Cranelift/V8-x64 keep the branch; Binaryen, which
|
|
4038
|
+
// AS uses, selects it). The condition is evaluated exactly once whether we branch or select, so
|
|
4039
|
+
// it need only be SIDE-EFFECT-FREE (loads allowed — sort's `a[c] < a[c+1]`); only the assigned
|
|
4040
|
+
// VALUE is evaluated unconditionally, hence must be a cheap, trap-free pure expr. `x++`/`x--`
|
|
4041
|
+
// are admitted as `x = x ± 1`. The already-emitted condition `ce` is reused (`__emitted`), so a
|
|
4042
|
+
// load-bearing condition is not emitted twice.
|
|
4043
|
+
if (els == null && ctx.transform.optimize?.boolConvertToSelect && isSideEffectFree(cond)) {
|
|
3304
4044
|
const asg = Array.isArray(then) && then[0] === ';' && then.length === 2 ? then[1] : then
|
|
3305
|
-
|
|
3306
|
-
|
|
4045
|
+
const sel = matchVoidLocalStore(asg)
|
|
4046
|
+
if (sel) return emitVoid(['=', sel.lhs, ['?:', ['__emitted', ce], sel.val, sel.lhs]])
|
|
3307
4047
|
}
|
|
3308
4048
|
const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
|
|
3309
4049
|
// Flow-sensitive type refinement: narrow types within each branch based on the guard.
|
|
@@ -3322,6 +4062,7 @@ export const emitter = {
|
|
|
3322
4062
|
// An enclosing labeled statement (`outer: for …`) hands its label down so `continue outer`
|
|
3323
4063
|
// can target this loop's continue point. The immediately-enclosed loop consumes it.
|
|
3324
4064
|
const myLabel = ctx.func.pendingLabel; ctx.func.pendingLabel = null
|
|
4065
|
+
const bodyNode0 = body // identity for assumption owners — survives the hoist rebind below
|
|
3325
4066
|
const labeledContinue = myLabel != null && hasLabeledContinueTo(body, myLabel)
|
|
3326
4067
|
// Don't unroll a loop that is the target of a `continue <label>` — unrolling would lose the
|
|
3327
4068
|
// continue edge. (Plain loops with no labeled-continue still unroll.)
|
|
@@ -3335,6 +4076,246 @@ export const emitter = {
|
|
|
3335
4076
|
const fu = unrollForIn(init, cond, step, body)
|
|
3336
4077
|
if (fu) return fu
|
|
3337
4078
|
}
|
|
4079
|
+
// Typed-bounds loop VERSIONING (Root F): a countable loop whose body indexes typed
|
|
4080
|
+
// receivers with iv-affine indices no static class proves gets a ONCE-per-entry
|
|
4081
|
+
// runtime extent guard. The fast arm re-emits with those (recv, idx) pairs assumed
|
|
4082
|
+
// in-bounds — bare loads/stores, i.e. the vectorizer's shapes — while the else arm
|
|
4083
|
+
// keeps the checked forms verbatim (also the correct semantics for a failing guard:
|
|
4084
|
+
// OOB reads yield undefined, OOB writes are ignored). Guard arithmetic runs in i64:
|
|
4085
|
+
// a*(B-1)+b overflows i32 near the edge, and a wrapped guard that passes is heap
|
|
4086
|
+
// corruption. `_tbVersioned` brakes the arms' re-entry into this same intercept —
|
|
4087
|
+
// keyed by ctx.func identity so a REUSED AST (same source compiled twice, the
|
|
4088
|
+
// self-host warm path) versions afresh in the next compile instead of silently
|
|
4089
|
+
// skipping.
|
|
4090
|
+
if (!labeledContinue && body._tbVersioned !== ctx.func
|
|
4091
|
+
&& (!ctx.transform.optimize || ctx.transform.optimize.versionTypedBounds !== false)) {
|
|
4092
|
+
const levels = versionableTypedNest(init, cond, step, body, ctx.func.locals)
|
|
4093
|
+
if (levels) {
|
|
4094
|
+
body._tbVersioned = ctx.func
|
|
4095
|
+
// every LIFTED level is proven by THIS guard — brake their own intercepts
|
|
4096
|
+
// (re-versioning per level compounds 2^depth checked twins)
|
|
4097
|
+
for (const vs of levels) if (vs.bodyNode && !vs.partial) vs.bodyNode._tbVersioned = ctx.func
|
|
4098
|
+
const result = []
|
|
4099
|
+
if (init != null) result.push(...emitVoid(init))
|
|
4100
|
+
const i64c = (n) => ['i64.const', n]
|
|
4101
|
+
const ext = (ir) => ['i64.extend_i32_s', ir]
|
|
4102
|
+
const conjs = []
|
|
4103
|
+
// one evaluation per symbolic-offset slot (a stable name or an invariant pure
|
|
4104
|
+
// expr like `y*w`); an 'f64' slot adds `v integral ∧ |v| ≤ 2^31` conjuncts —
|
|
4105
|
+
// the int model of `a*iv + v` is exact only for integral v (trunc does NOT
|
|
4106
|
+
// distribute over f64 sums)
|
|
4107
|
+
const slotKey = (s) => typeof s === 'string' ? s : JSON.stringify(s)
|
|
4108
|
+
const slots = new Map()
|
|
4109
|
+
const slotI64 = (slot, kind) => {
|
|
4110
|
+
const key = slotKey(slot)
|
|
4111
|
+
let s = slots.get(key)
|
|
4112
|
+
if (s) return s
|
|
4113
|
+
if (kind === 'i32') {
|
|
4114
|
+
const nT = tempI64('tvm')
|
|
4115
|
+
result.push(['local.set', `$${nT}`, ext(asI32(emit(slot)))])
|
|
4116
|
+
s = ['local.get', `$${nT}`]
|
|
4117
|
+
} else {
|
|
4118
|
+
const nF = temp('tvn')
|
|
4119
|
+
result.push(['local.set', `$${nF}`, asF64(emit(slot))])
|
|
4120
|
+
conjs.push(['f64.eq', ['local.get', `$${nF}`], ['f64.floor', ['local.get', `$${nF}`]]])
|
|
4121
|
+
conjs.push(['f64.le', ['f64.abs', ['local.get', `$${nF}`]], ['f64.const', 2147483648]])
|
|
4122
|
+
const nT = tempI64('tvm')
|
|
4123
|
+
result.push(['local.set', `$${nT}`, ['i64.trunc_sat_f64_s', ['local.get', `$${nF}`]]])
|
|
4124
|
+
s = ['local.get', `$${nT}`]
|
|
4125
|
+
}
|
|
4126
|
+
slots.set(key, s)
|
|
4127
|
+
return s
|
|
4128
|
+
}
|
|
4129
|
+
const slotSum = (base, list, lo = false) => {
|
|
4130
|
+
let r = base
|
|
4131
|
+
for (const t of list) {
|
|
4132
|
+
// a WRAP atom (toroidal iv ternary ∈ [0, B-1]) is one-sided: B-1 into
|
|
4133
|
+
// the hi extent, nothing into the lo
|
|
4134
|
+
if (t.wrap) {
|
|
4135
|
+
if (!lo) r = ['i64.add', r,
|
|
4136
|
+
['i64.mul', i64c(t.k), ['i64.sub', slotI64(t.e, t.kind), i64c(1)]]]
|
|
4137
|
+
continue
|
|
4138
|
+
}
|
|
4139
|
+
const s = slotI64(t.e, t.kind)
|
|
4140
|
+
r = ['i64.add', r, t.k === 1 ? s : ['i64.mul', i64c(t.k), s]]
|
|
4141
|
+
}
|
|
4142
|
+
return r
|
|
4143
|
+
}
|
|
4144
|
+
// len as ONE inline header load for a RESOLVED elem type (owned byteLen at
|
|
4145
|
+
// base-8, view at descriptor[0]; elemCount = byteLen >> shift) — a call in
|
|
4146
|
+
// the guard costs per LOOP ENTRY on re-entered inner nests (fft measured
|
|
4147
|
+
// 1.35x with calls, parity without); unresolved receivers keep $__len.
|
|
4148
|
+
const len64Of = (recv) => {
|
|
4149
|
+
const aux = typedElemAux(ctx.types.typedElem?.get(recv))
|
|
4150
|
+
if (aux == null) {
|
|
4151
|
+
inc('__len')
|
|
4152
|
+
return ['i64.extend_i32_u', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(recv))]]]
|
|
4153
|
+
}
|
|
4154
|
+
const et = aux & 7, isView = (aux & 8) !== 0
|
|
4155
|
+
const shift = (aux & 16) ? 3 : et <= 1 ? 0 : et <= 3 ? 1 : et <= 6 ? 2 : 3
|
|
4156
|
+
const base = ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', asF64(emit(recv))], ['i64.const', LAYOUT.OFFSET_MASK]]]
|
|
4157
|
+
return ['i64.extend_i32_u', ['i32.shr_u',
|
|
4158
|
+
['i32.load', isView ? base : ['i32.sub', base, ['i32.const', 8]]], ['i32.const', shift]]]
|
|
4159
|
+
}
|
|
4160
|
+
// one guard covers the whole NEST — each level contributes its own max-iv
|
|
4161
|
+
// and extent conjuncts (nested recognizers need the BARE nest in the fast
|
|
4162
|
+
// arm, and one guard per nest beats one per row)
|
|
4163
|
+
const levelInfo = new Map()
|
|
4164
|
+
for (const vs of levels) {
|
|
4165
|
+
// max iv as i64. An 'f64' bound (untyped param, unknown box) converts via
|
|
4166
|
+
// ceil (`<`: the max int iv under B) / floor (`<=`) + trunc_sat — never
|
|
4167
|
+
// traps — with a `|B| ≤ 2^31` conjunct making the conversion exact: NaN and
|
|
4168
|
+
// box bit patterns fail the abs-compare and fall to the checked arm;
|
|
4169
|
+
// saturated garbage past the limit is conjunct-dead. i64 extents then never
|
|
4170
|
+
// overflow (|terms| ≤ 2^31, a is an i32 literal → |hi| < 2^63).
|
|
4171
|
+
// a RANGE-ONLY level guards hull conjuncts alone — no iv, no max-iv
|
|
4172
|
+
if (vs.rangeOnly) {
|
|
4173
|
+
for (const c of vs.cands) {
|
|
4174
|
+
if (c.range.hiName != null) {
|
|
4175
|
+
const cS = slotI64(c.range.hiName, exprType(c.range.hiName, ctx.func.locals) === 'i32' ? 'i32' : 'f64')
|
|
4176
|
+
conjs.push(['i64.ge_s', cS, i64c(c.range.entryHi + 1)])
|
|
4177
|
+
conjs.push(['i64.lt_s', ['i64.add', cS, i64c(c.range.hiBias)], len64Of(c.recv)])
|
|
4178
|
+
} else conjs.push(['i64.lt_s', i64c(c.range[1]), len64Of(c.recv)])
|
|
4179
|
+
}
|
|
4180
|
+
continue
|
|
4181
|
+
}
|
|
4182
|
+
const maxIv = tempI64('tvq')
|
|
4183
|
+
if (vs.bKind === 'f64') {
|
|
4184
|
+
const bF = temp('tvf')
|
|
4185
|
+
result.push(['local.set', `$${bF}`, asF64(emit(vs.bound))])
|
|
4186
|
+
conjs.push(['f64.le', ['f64.abs', ['local.get', `$${bF}`]], ['f64.const', 2147483648]])
|
|
4187
|
+
result.push(['local.set', `$${maxIv}`,
|
|
4188
|
+
['i64.trunc_sat_f64_s', [vs.incl ? 'f64.floor' : 'f64.ceil', ['local.get', `$${bF}`]]]])
|
|
4189
|
+
if (vs.bump - (vs.incl ? 0 : 1)) result.push(['local.set', `$${maxIv}`,
|
|
4190
|
+
['i64.add', ['local.get', `$${maxIv}`], i64c(vs.bump - (vs.incl ? 0 : 1))]])
|
|
4191
|
+
} else {
|
|
4192
|
+
const adj = vs.bump - (vs.incl ? 0 : 1)
|
|
4193
|
+
result.push(['local.set', `$${maxIv}`,
|
|
4194
|
+
adj ? ['i64.add', ext(asI32(emit(vs.bound))), i64c(adj)] : ext(asI32(emit(vs.bound)))])
|
|
4195
|
+
}
|
|
4196
|
+
levelInfo.set(vs, { maxIv, entryIR: () => vs.startC != null ? i64c(vs.startC) : slotI64(vs.iv, vs.ivKind) })
|
|
4197
|
+
// non-unit monotone stride: positivity is the soundness condition
|
|
4198
|
+
if (vs.stepBy?.name != null)
|
|
4199
|
+
conjs.push(['i64.ge_s', slotI64(vs.stepBy.name, vs.stepBy.kind), i64c(1)])
|
|
4200
|
+
// one extent conjunct pair per (recv, a, slots) group: hi = a*maxIv+Σkᵢ·slotᵢ
|
|
4201
|
+
// +maxC < len, plus lo = a*entry+Σkᵢ·slotᵢ+minC ≥ 0 — folded when the static
|
|
4202
|
+
// start proves it, read from the live iv local otherwise (top level only)
|
|
4203
|
+
const groups = new Map(), indGroups = new Map()
|
|
4204
|
+
for (const c of vs.cands) {
|
|
4205
|
+
if (c.range != null) {
|
|
4206
|
+
// interval-hulled idx against a dynamic length (the affine fallback).
|
|
4207
|
+
// Numeric hull: one `hi < len` conjunct. Symbolic hull (wrap cursor vs
|
|
4208
|
+
// a MUTABLE bound C): cursor ∈ [0, C-1] relative to C's runtime value —
|
|
4209
|
+
// `C ≥ entryHi+1` (the entry fits) ∧ `C+bias < len` close it.
|
|
4210
|
+
if (c.range.hiName != null) {
|
|
4211
|
+
const cS = slotI64(c.range.hiName, exprType(c.range.hiName, ctx.func.locals) === 'i32' ? 'i32' : 'f64')
|
|
4212
|
+
conjs.push(['i64.ge_s', cS, i64c(c.range.entryHi + 1)])
|
|
4213
|
+
conjs.push(['i64.lt_s', ['i64.add', cS, i64c(c.range.hiBias)], len64Of(c.recv)])
|
|
4214
|
+
} else conjs.push(['i64.lt_s', i64c(c.range[1]), len64Of(c.recv)])
|
|
4215
|
+
continue
|
|
4216
|
+
}
|
|
4217
|
+
if (c.ind != null) {
|
|
4218
|
+
const gk = c.recv + '\x00' + c.ind
|
|
4219
|
+
if (!indGroups.has(gk)) indGroups.set(gk, c)
|
|
4220
|
+
continue
|
|
4221
|
+
}
|
|
4222
|
+
const gk = c.recv + '\x00' + c.a + '\x00' + c.slots.map(t => t.k + '*' + slotKey(t.e)).join('+')
|
|
4223
|
+
const g = groups.get(gk)
|
|
4224
|
+
if (!g) groups.set(gk, { recv: c.recv, a: c.a, slots: c.slots, maxC: c.bConst, minC: c.bConst })
|
|
4225
|
+
else { g.maxC = Math.max(g.maxC, c.bConst); g.minC = Math.min(g.minC, c.bConst) }
|
|
4226
|
+
}
|
|
4227
|
+
for (const g of groups.values()) {
|
|
4228
|
+
let hi = slotSum(['i64.mul', i64c(g.a), ['local.get', `$${maxIv}`]], g.slots)
|
|
4229
|
+
if (g.maxC) hi = ['i64.add', hi, i64c(g.maxC)]
|
|
4230
|
+
conjs.push(['i64.lt_s', hi, len64Of(g.recv)])
|
|
4231
|
+
if (vs.startC != null && !g.slots.length) continue
|
|
4232
|
+
let lo = slotSum(vs.startC != null ? i64c(g.a * vs.startC)
|
|
4233
|
+
: ['i64.mul', i64c(g.a), slotI64(vs.iv, vs.ivKind)], g.slots, true)
|
|
4234
|
+
if (g.minC) lo = ['i64.add', lo, i64c(g.minC)]
|
|
4235
|
+
conjs.push(['i64.ge_s', lo, i64c(0)])
|
|
4236
|
+
}
|
|
4237
|
+
// induction cursors (`k += step` in a comma step): value at iteration t is
|
|
4238
|
+
// entry + slope*t, t ∈ [0, maxIv - ivEntry] — monotone either direction, so
|
|
4239
|
+
// BOTH endpoints guard in [0, len) and every intermediate value is covered
|
|
4240
|
+
for (const c of indGroups.values()) {
|
|
4241
|
+
const kE = c.entryC != null ? i64c(c.entryC)
|
|
4242
|
+
: slotI64(c.ind, exprType(c.ind, ctx.func.locals) === 'i32' ? 'i32' : 'f64')
|
|
4243
|
+
const slopeLit = intLiteralValue(c.slope)
|
|
4244
|
+
const slope64 = slopeLit != null ? i64c(slopeLit)
|
|
4245
|
+
: slotI64(c.slope, exprType(c.slope, ctx.func.locals) === 'i32' ? 'i32' : 'f64')
|
|
4246
|
+
const ivE = vs.startC != null ? i64c(vs.startC) : slotI64(vs.iv, vs.ivKind)
|
|
4247
|
+
const endT = tempI64('tvi')
|
|
4248
|
+
result.push(['local.set', `$${endT}`, ['i64.add', kE,
|
|
4249
|
+
['i64.mul', slope64, ['i64.sub', ['local.get', `$${maxIv}`], ivE]]]])
|
|
4250
|
+
const len64 = len64Of(c.recv)
|
|
4251
|
+
conjs.push(['i64.ge_s', kE, i64c(0)])
|
|
4252
|
+
conjs.push(['i64.lt_s', kE, len64])
|
|
4253
|
+
conjs.push(['i64.ge_s', ['local.get', `$${endT}`], i64c(0)])
|
|
4254
|
+
conjs.push(['i64.lt_s', ['local.get', `$${endT}`], len64Of(c.recv)])
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
// FLAT-CURSOR endpoint guards: `j++` once per pixel across the nest —
|
|
4258
|
+
// value spans [j0, j0 + slope·(Π trips − (pre ? 1 : 0))]; the steps cap
|
|
4259
|
+
// keeps the slope product overflow-free, a negative trip (empty level)
|
|
4260
|
+
// fails its conjunct into the checked arm
|
|
4261
|
+
for (const cur of levels.cursors ?? []) {
|
|
4262
|
+
const j0 = slotI64(cur.name, cur.kind)
|
|
4263
|
+
let steps = null
|
|
4264
|
+
for (const L of cur.chain) {
|
|
4265
|
+
const info = levelInfo.get(L)
|
|
4266
|
+
if (!info) { steps = null; break }
|
|
4267
|
+
const trip = tempI64('tvt')
|
|
4268
|
+
result.push(['local.set', `$${trip}`,
|
|
4269
|
+
['i64.add', ['i64.sub', ['local.get', `$${info.maxIv}`], info.entryIR()], i64c(1)]])
|
|
4270
|
+
conjs.push(['i64.ge_s', ['local.get', `$${trip}`], i64c(0)])
|
|
4271
|
+
steps = steps ? ['i64.mul', steps, ['local.get', `$${trip}`]] : ['local.get', `$${trip}`]
|
|
4272
|
+
}
|
|
4273
|
+
if (!steps) { cur.dead = true; continue }
|
|
4274
|
+
const stepsT = tempI64('tvs')
|
|
4275
|
+
result.push(['local.set', `$${stepsT}`, steps])
|
|
4276
|
+
conjs.push(['i64.le_s', ['local.get', `$${stepsT}`], i64c(2147483648)])
|
|
4277
|
+
const seen = new Set()
|
|
4278
|
+
for (const c of cur.cands) {
|
|
4279
|
+
const gk = c.recv + '' + c.post
|
|
4280
|
+
if (seen.has(gk)) continue
|
|
4281
|
+
seen.add(gk)
|
|
4282
|
+
const endT = tempI64('tvz')
|
|
4283
|
+
result.push(['local.set', `$${endT}`, ['i64.add', j0,
|
|
4284
|
+
['i64.mul', i64c(cur.slope), c.post ? ['local.get', `$${stepsT}`]
|
|
4285
|
+
: ['i64.sub', ['local.get', `$${stepsT}`], i64c(1)]]]])
|
|
4286
|
+
conjs.push(['i64.ge_s', j0, i64c(0)])
|
|
4287
|
+
conjs.push(['i64.lt_s', ['local.get', `$${endT}`], len64Of(c.recv)])
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
let guard = conjs[0]
|
|
4291
|
+
for (let k = 1; k < conjs.length; k++) guard = ['i32.and', guard, conjs[k]]
|
|
4292
|
+
// arm-scoped assumption MAP key → OWNING loop body: an assumption is honored
|
|
4293
|
+
// only while its loop's frame is on the emission stack (typedIdxProven checks
|
|
4294
|
+
// frame.bodyNode) — a textual twin of an inner access OUTSIDE that loop (the
|
|
4295
|
+
// cursor past its bound) must NOT inherit the proof. Snapshot/RESTORE (not
|
|
4296
|
+
// add/delete): unrolls inside the fast arm stamp clone keys that must not
|
|
4297
|
+
// survive into the checked arm, which runs exactly when the guard failed.
|
|
4298
|
+
const saved = ctx.types.assumedBounds
|
|
4299
|
+
ctx.types.assumedBounds = new Map(saved ?? [])
|
|
4300
|
+
for (const vs of levels)
|
|
4301
|
+
for (const c of vs.cands)
|
|
4302
|
+
// hull cands are position-independent (the hull joins EVERY sighting of
|
|
4303
|
+
// the key) — owned by the TOP arm so unrolled inner loops (no frame of
|
|
4304
|
+
// their own) still validate; affine/induction extents stay level-owned
|
|
4305
|
+
ctx.types.assumedBounds.set(idxKey(c.recv, c.idx), c.range != null ? body : vs.bodyNode ?? body)
|
|
4306
|
+
// cursor claims hold across the WHOLE nest (entry → end) — owned by the top
|
|
4307
|
+
for (const cur of levels.cursors ?? [])
|
|
4308
|
+
if (!cur.dead) for (const c of cur.cands) ctx.types.assumedBounds.set(idxKey(c.recv, c.idx), body)
|
|
4309
|
+
const fast = emitter['for'](null, cond, step, body)
|
|
4310
|
+
ctx.types.assumedBounds = saved
|
|
4311
|
+
const checked = emitter['for'](null, cond, step, body)
|
|
4312
|
+
const stmts = (r) => Array.isArray(r[0]) ? r : [r]
|
|
4313
|
+
result.push(['if', typed(guard, 'i32'),
|
|
4314
|
+
['then', ...stmts(fast)],
|
|
4315
|
+
['else', ...stmts(checked)]])
|
|
4316
|
+
return result
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
3338
4319
|
// Lift constant array/object literals out of the loop (allocate once, not per
|
|
3339
4320
|
// iteration) when they are read-only + non-escaping inside it. Strip them from the
|
|
3340
4321
|
// body up front so freshBoxed / continue analysis see the reduced body.
|
|
@@ -3350,7 +4331,7 @@ export const emitter = {
|
|
|
3350
4331
|
// can target the loop label directly, saving a redundant `block`.
|
|
3351
4332
|
const needsCont = step && (hasOwnContinue(body) || labeledContinue)
|
|
3352
4333
|
const cont = needsCont ? `$cont${id}` : loop
|
|
3353
|
-
ctx.func.stack.push({ brk, loop: cont })
|
|
4334
|
+
ctx.func.stack.push({ brk, loop: cont, bodyNode: bodyNode0 })
|
|
3354
4335
|
const frame = ctx.func.stack[ctx.func.stack.length - 1]
|
|
3355
4336
|
if (myLabel != null) frame.contLabel = myLabel // so `continue <myLabel>` targets this loop's step/test
|
|
3356
4337
|
// Per-iteration fresh cells for boxed locals declared in the body — allocated
|
|
@@ -3566,6 +4547,10 @@ export function emit(node, expect) {
|
|
|
3566
4547
|
if (node.loc != null) ctx.error.loc = node.loc
|
|
3567
4548
|
}
|
|
3568
4549
|
if (node == null) return null
|
|
4550
|
+
// Pre-emitted IR passthrough: `['__emitted', ir]` returns `ir` untouched. Lets a caller that
|
|
4551
|
+
// already emitted a subtree (e.g. the `if` handler's condition) splice it into an AST-shaped
|
|
4552
|
+
// re-emit (a `?:` for if→select conversion) without emitting it a second time.
|
|
4553
|
+
if (Array.isArray(node) && node[0] === '__emitted') return node[1]
|
|
3569
4554
|
// Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
|
|
3570
4555
|
// AST), but their working representation is the plain number 0/1 — identical
|
|
3571
4556
|
// codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
|