jz 0.8.1 → 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 -6271
- 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 +299 -44
- 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 +892 -32
- 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 +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- 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 +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- 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 +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- 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
|
|
@@ -1559,7 +1847,7 @@ const isCheapPureVal = (n) => {
|
|
|
1559
1847
|
// condition is evaluated exactly once whether the lowering branches or selects (any trap fires the
|
|
1560
1848
|
// same in both, the read order vs the pure value arm is immaterial), so it need only avoid MUTATING
|
|
1561
1849
|
// state the value arm could read — i.e. be side-effect-free, not unconditionally-evaluable.
|
|
1562
|
-
const SIDE_EFFECT_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=',
|
|
1850
|
+
const SIDE_EFFECT_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '>>=', '<<=',
|
|
1563
1851
|
'>>>=', '||=', '&&=', '??=', '++', '--', '()', '=>', 'throw', 'new', 'await', 'yield'])
|
|
1564
1852
|
const isSideEffectFree = (n) => {
|
|
1565
1853
|
if (!Array.isArray(n)) return true
|
|
@@ -1652,6 +1940,16 @@ function emitLooseEq(a, b, negate) {
|
|
|
1652
1940
|
// spec-on/spec-off differential (zero divergence at optimize 0 and 2).
|
|
1653
1941
|
const strEqResult = (r) => negate ? typed(['i32.eqz', r], 'i32') : r
|
|
1654
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
|
+
}
|
|
1655
1953
|
if (aStr && bStr) {
|
|
1656
1954
|
inc('__str_eq')
|
|
1657
1955
|
return strEqResult(typed(['call', '$__str_eq', asI64(va), asI64(vb)], 'i32'))
|
|
@@ -1660,14 +1958,26 @@ function emitLooseEq(a, b, negate) {
|
|
|
1660
1958
|
const uVal = bStr ? va : vb, lVal = bStr ? vb : va // u: unknown side, l: known string
|
|
1661
1959
|
inc('__is_str_key', '__str_eq')
|
|
1662
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]]]
|
|
1663
1975
|
return strEqResult(typed(['block', ['result', 'i32'],
|
|
1664
1976
|
['local.set', `$${u}`, asI64(uVal)],
|
|
1665
1977
|
['local.set', `$${l}`, asI64(lVal)],
|
|
1666
1978
|
['if', ['result', 'i32'], ['i64.eq', uG, lG],
|
|
1667
1979
|
['then', ['i32.const', 1]],
|
|
1668
|
-
['else',
|
|
1669
|
-
['then', ['call', '$__str_eq', uG, lG]],
|
|
1670
|
-
['else', ['i32.const', 0]]]]]], 'i32'))
|
|
1980
|
+
['else', tail]]], 'i32'))
|
|
1671
1981
|
}
|
|
1672
1982
|
inc('__eq')
|
|
1673
1983
|
const call = typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
|
|
@@ -1704,6 +2014,24 @@ function emitStrictEq(a, b, negate) {
|
|
|
1704
2014
|
const strictB = resolveValType(b, valTypeOf, lookupValType)
|
|
1705
2015
|
if (strictA && strictB && strictA !== strictB && (STRICT_PRIM.has(strictA) || STRICT_PRIM.has(strictB)))
|
|
1706
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
|
+
}
|
|
1707
2035
|
// Same type (or dynamic-unknown): identical bits to loose `==`/`!=`.
|
|
1708
2036
|
return emitter[negate ? '!=' : '=='](a, b)
|
|
1709
2037
|
}
|
|
@@ -1722,6 +2050,16 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
|
1722
2050
|
const vta = numericVal(resolveValType(a, valTypeOf, lookupValType))
|
|
1723
2051
|
const vtb = numericVal(resolveValType(b, valTypeOf, lookupValType))
|
|
1724
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
|
+
}
|
|
1725
2063
|
const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
|
|
1726
2064
|
return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
|
|
1727
2065
|
}
|
|
@@ -1954,6 +2292,27 @@ function emitSingleSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1954
2292
|
const acc = `${T}acc${ctx.func.uniq++}`
|
|
1955
2293
|
ctx.func.locals.set(acc, 'f64')
|
|
1956
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
|
+
}
|
|
1957
2316
|
if (parsed.normal.length > 0) {
|
|
1958
2317
|
const r = asF64(emitAsValue(() => methodEmitter(objArg, ...parsed.normal)))
|
|
1959
2318
|
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
@@ -1978,6 +2337,42 @@ function emitMultiSpreadMethodCall(objArg, parsed, method, methodEmitter) {
|
|
|
1978
2337
|
if (acc) ctx.func.locals.set(acc, 'f64')
|
|
1979
2338
|
const recv = inPlace ? objArg : acc
|
|
1980
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
|
+
}
|
|
1981
2376
|
let batch = []
|
|
1982
2377
|
const flushBatch = () => {
|
|
1983
2378
|
if (!batch.length) return
|
|
@@ -2018,11 +2413,14 @@ function emitMethodCallSpread(objArg, methodEmitter, parsed, method) {
|
|
|
2018
2413
|
* the nullish-guard scaffold stays in one place. */
|
|
2019
2414
|
function withNullGuard(headExpr, body, tag = 'ng') {
|
|
2020
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").
|
|
2021
2419
|
return block64(
|
|
2022
2420
|
['local.set', `$${t}`, headExpr],
|
|
2023
2421
|
['if', ['result', 'f64'],
|
|
2024
2422
|
['i32.eqz', isNullish(['local.get', `$${t}`])],
|
|
2025
|
-
['then', body(t)],
|
|
2423
|
+
['then', asF64(body(t))],
|
|
2026
2424
|
['else', undefExpr()]])
|
|
2027
2425
|
}
|
|
2028
2426
|
|
|
@@ -2103,6 +2501,11 @@ function tryFnPropCall(callee, obj, method, parsed) {
|
|
|
2103
2501
|
if (ctx.func.names.has(fname)) {
|
|
2104
2502
|
const func = ctx.func.map.get(fname)
|
|
2105
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
|
|
2106
2509
|
return attachSigMeta(typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0]), func.sig)
|
|
2107
2510
|
}
|
|
2108
2511
|
}
|
|
@@ -2270,6 +2673,49 @@ function tryGenericEmitter({ obj, method, parsed, vt, callMethod }) {
|
|
|
2270
2673
|
// correctly instead of being hijacked by `Array.prototype.{find,map,…}`.
|
|
2271
2674
|
const objectShadow = vt === VAL.OBJECT || vt === VAL.HASH
|
|
2272
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
|
+
}
|
|
2273
2719
|
return callMethod(obj, ctx.core.emit[`.${method}`])
|
|
2274
2720
|
}
|
|
2275
2721
|
}
|
|
@@ -2286,7 +2732,15 @@ function tryDynamicPropCall({ obj, method, parsed, vt }) {
|
|
|
2286
2732
|
const propTmp = temp('mprop')
|
|
2287
2733
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2288
2734
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
2289
|
-
|
|
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')
|
|
2290
2744
|
const closureOnly = usesDynProps(vt) || ctx.transform.host === 'wasi'
|
|
2291
2745
|
inc('__dyn_get_expr', '__ptr_type')
|
|
2292
2746
|
if (!closureOnly) { inc('__ext_call'); ctx.features.external = true }
|
|
@@ -2310,6 +2764,17 @@ function tryDynamicPropCall({ obj, method, parsed, vt }) {
|
|
|
2310
2764
|
|
|
2311
2765
|
// 12. Unknown callee — assume external method. Total: always returns.
|
|
2312
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.`)
|
|
2313
2778
|
if (ctx.transform.strict)
|
|
2314
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 }.`)
|
|
2315
2780
|
// Under wasi there is no host `__ext_call` — the call lowers to a
|
|
@@ -2459,6 +2924,14 @@ function emitDirectFunctionCall(callee, parsed, callArgs) {
|
|
|
2459
2924
|
|
|
2460
2925
|
// Regular function call without rest params
|
|
2461
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)
|
|
2462
2935
|
// Pad missing args with `undefined` so default-param init triggers per spec
|
|
2463
2936
|
// (only undefined, not null, should trigger defaults). Drop extras to match
|
|
2464
2937
|
// JS calling convention — emitting them anyway produces an invalid call
|
|
@@ -2467,7 +2940,7 @@ function emitDirectFunctionCall(callee, parsed, callArgs) {
|
|
|
2467
2940
|
// legitimate 0-arity callee isn't bypassed.
|
|
2468
2941
|
const params = func?.sig.params ?? []
|
|
2469
2942
|
const args = func ? emitCallArgs(parsed.normal, params)
|
|
2470
|
-
: parsed.normal.map(a => coerceArg(emit(a), undefined))
|
|
2943
|
+
: parsed.normal.map(a => coerceArg(emit(a), undefined, a))
|
|
2471
2944
|
if (func && args.length > params.length) args.length = params.length
|
|
2472
2945
|
// Multi-value return: materialize as heap array (caller expects single pointer).
|
|
2473
2946
|
// Reuse the canonical comma-wrapped arg slot — materializeMulti re-reads args
|
|
@@ -2519,7 +2992,10 @@ function tryDirectClosureCall(callee, parsed) {
|
|
|
2519
2992
|
mn.set(bodyName, prev === undefined ? n : (n < prev ? n : prev))
|
|
2520
2993
|
// Body signature is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64.
|
|
2521
2994
|
// We pass the closure NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
|
|
2522
|
-
|
|
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)))
|
|
2523
2999
|
while (slots.length < W) slots.push(undefExpr())
|
|
2524
3000
|
return typed(['call', `$${bodyName}`,
|
|
2525
3001
|
asF64(emit(callee)),
|
|
@@ -2527,16 +3003,37 @@ function tryDirectClosureCall(callee, parsed) {
|
|
|
2527
3003
|
...slots], 'f64')
|
|
2528
3004
|
}
|
|
2529
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
|
+
|
|
2530
3024
|
/** Generic closure call: callee is a value holding a NaN-boxed closure pointer.
|
|
2531
3025
|
* Uniform convention: fn.call packs all args into an array and trampolines. */
|
|
2532
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
|
|
2533
3029
|
if (parsed.hasSpread) {
|
|
2534
3030
|
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
2535
3031
|
const arrayIR = buildArrayWithSpreads(combined)
|
|
2536
3032
|
// Pass pre-built array as single already-emitted arg
|
|
2537
3033
|
return ctx.closure.call(emit(callee), [arrayIR], true)
|
|
2538
3034
|
}
|
|
2539
|
-
|
|
3035
|
+
const ir = ctx.closure.call(emit(callee), parsed.normal)
|
|
3036
|
+
return dvName ? tagFnArrayDispatch(ir, dvName) : ir
|
|
2540
3037
|
}
|
|
2541
3038
|
|
|
2542
3039
|
/** Last-resort fallback: assume `(call $callee args)` against an import / unknown
|
|
@@ -2582,6 +3079,21 @@ function compoundAssign(name, val, f64op, i32op) {
|
|
|
2582
3079
|
return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
|
|
2583
3080
|
}
|
|
2584
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
|
+
|
|
2585
3097
|
// === Core emitter dispatch table ===
|
|
2586
3098
|
// ctx.core.emit is seeded with a flat copy of this object on reset;
|
|
2587
3099
|
// language modules add or override ops on ctx.core.emit directly.
|
|
@@ -2619,8 +3131,15 @@ export const emitter = {
|
|
|
2619
3131
|
...results.flatMap(dropSpread),
|
|
2620
3132
|
['f64.const', 0])
|
|
2621
3133
|
}
|
|
2622
|
-
|
|
3134
|
+
const seq = typed(['block', ['result', last.type],
|
|
2623
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
|
|
2624
3143
|
},
|
|
2625
3144
|
'let': emitDecl,
|
|
2626
3145
|
'const': emitDecl,
|
|
@@ -2718,7 +3237,25 @@ export const emitter = {
|
|
|
2718
3237
|
if (expr == null) return [...finalizers, typed(['return', undefExpr()], 'void')]
|
|
2719
3238
|
const rt = ctx.func.current?.results[0] || 'f64'
|
|
2720
3239
|
const pk = ctx.func.current?.ptrKind
|
|
2721
|
-
|
|
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)
|
|
2722
3259
|
const ty = pk != null ? 'i32' : rt
|
|
2723
3260
|
const tcoed = tcoTailRewrite(ir, ty)
|
|
2724
3261
|
if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
|
|
@@ -2786,6 +3323,8 @@ export const emitter = {
|
|
|
2786
3323
|
if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
|
|
2787
3324
|
return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
|
|
2788
3325
|
},
|
|
3326
|
+
// `**` is always f64 (and has its own const-exponent lowering) — full desugar.
|
|
3327
|
+
'**=': (name, val) => emit(['=', name, ['**', name, val]]),
|
|
2789
3328
|
|
|
2790
3329
|
// Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
|
|
2791
3330
|
...Object.fromEntries([
|
|
@@ -2850,6 +3389,19 @@ export const emitter = {
|
|
|
2850
3389
|
// Read it for THIS concat, then clear so nested operands (not the accumulation target) stay fresh.
|
|
2851
3390
|
const selfAccum = typeof a === 'string' && a === ctx.func._selfAccumConcat
|
|
2852
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
|
+
}
|
|
2853
3405
|
// String concatenation: pure string operands skip generic ToString coercion.
|
|
2854
3406
|
const vtA = valTypeOf(a)
|
|
2855
3407
|
const vtB = valTypeOf(b)
|
|
@@ -2891,8 +3443,10 @@ export const emitter = {
|
|
|
2891
3443
|
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2892
3444
|
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2893
3445
|
}
|
|
2894
|
-
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
3446
|
+
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT) {
|
|
3447
|
+
bigintMixReject('+', a, b)
|
|
2895
3448
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
3449
|
+
}
|
|
2896
3450
|
// Runtime string dispatch when at least one side could be a string. When one side has
|
|
2897
3451
|
// a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
|
|
2898
3452
|
// chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
|
|
@@ -2906,10 +3460,36 @@ export const emitter = {
|
|
|
2906
3460
|
// is a TYPED concat (handled by concatRaw above); a both-untyped self-mutation stays the
|
|
2907
3461
|
// documented rare-aliasing tradeoff. Self-accumulation is still safe to extend.
|
|
2908
3462
|
inc('__str_concat', '__is_str_key')
|
|
2909
|
-
const
|
|
2910
|
-
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
|
|
2911
3467
|
const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
|
|
2912
|
-
|
|
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)]
|
|
2913
3493
|
if (checkA && checkB) {
|
|
2914
3494
|
return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
|
|
2915
3495
|
}
|
|
@@ -2934,10 +3514,12 @@ export const emitter = {
|
|
|
2934
3514
|
},
|
|
2935
3515
|
'-': (a, b) => {
|
|
2936
3516
|
if (ctx.func._expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
2937
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3517
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3518
|
+
bigintMixReject('-', a, b)
|
|
2938
3519
|
return b === undefined
|
|
2939
3520
|
? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
2940
3521
|
: fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
|
|
3522
|
+
}
|
|
2941
3523
|
if (b === undefined) return emitNeg(a)
|
|
2942
3524
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
|
|
2943
3525
|
if (_f) return _f
|
|
@@ -2950,7 +3532,7 @@ export const emitter = {
|
|
|
2950
3532
|
},
|
|
2951
3533
|
'u+': a => {
|
|
2952
3534
|
if (valTypeOf(a) === VAL.BIGINT)
|
|
2953
|
-
return
|
|
3535
|
+
return err('unary `+` on a BigInt is a TypeError in JS — use Number(x)')
|
|
2954
3536
|
const v = emit(a)
|
|
2955
3537
|
if (v.type === 'i32') return asF64(v)
|
|
2956
3538
|
if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
|
|
@@ -2959,8 +3541,10 @@ export const emitter = {
|
|
|
2959
3541
|
},
|
|
2960
3542
|
'u-': a => emitNeg(a),
|
|
2961
3543
|
'*': (a, b) => {
|
|
2962
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3544
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3545
|
+
bigintMixReject('*', a, b)
|
|
2963
3546
|
return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
|
|
3547
|
+
}
|
|
2964
3548
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
|
|
2965
3549
|
if (_f) return _f
|
|
2966
3550
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
@@ -2976,20 +3560,37 @@ export const emitter = {
|
|
|
2976
3560
|
// `.unsigned` operand is a uint32 ([0, 2^32)); its product can exceed i32, so
|
|
2977
3561
|
// `i32.mul` would wrap ((2^32-1)*2 → -2). Widen to f64 — see `+` above.
|
|
2978
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')
|
|
2979
3576
|
const i32mul = tryI32Arith('i32.mul', '*', a, b, va, vb); if (i32mul) return i32mul
|
|
2980
3577
|
return typed(['f64.mul', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2981
3578
|
},
|
|
2982
3579
|
'/': (a, b) => {
|
|
2983
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3580
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3581
|
+
bigintMixReject('/', a, b)
|
|
2984
3582
|
return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
|
|
3583
|
+
}
|
|
2985
3584
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
|
|
2986
3585
|
if (_f) return _f
|
|
2987
3586
|
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
2988
3587
|
return typed(['f64.div', stripCanon(toNumF64(a, va)), stripCanon(toNumF64(b, vb))], 'f64')
|
|
2989
3588
|
},
|
|
2990
3589
|
'%': (a, b) => {
|
|
2991
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3590
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3591
|
+
bigintMixReject('%', a, b)
|
|
2992
3592
|
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
3593
|
+
}
|
|
2993
3594
|
const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
|
|
2994
3595
|
if (_f) return _f
|
|
2995
3596
|
// ES remainder by zero is NaN; only the f64 path yields that (a - trunc(a/0)*0).
|
|
@@ -3062,6 +3663,27 @@ export const emitter = {
|
|
|
3062
3663
|
const elseRefs = extractRefinements(a, new Map(), false)
|
|
3063
3664
|
const vb = withRefinements(thenRefs, b, () => emit(b))
|
|
3064
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
|
+
}
|
|
3065
3687
|
// `cond ? 1 : 0` is the condition bit itself; `cond ? 0 : 1` its negation. `cond`
|
|
3066
3688
|
// (truthyIR) is already canonical 0/1, so the select + two const arms collapse to
|
|
3067
3689
|
// the bit. (Both arms are literals here, so dropping their emitted IR is side-effect
|
|
@@ -3166,7 +3788,39 @@ export const emitter = {
|
|
|
3166
3788
|
// a is truthy in the right-arm — narrow b accordingly. Matches `?:`'s then-arm threading
|
|
3167
3789
|
// (`Array.isArray(x) && x[0]` → x[0] sees x as ARRAY, eliding union-rep fallbacks).
|
|
3168
3790
|
const rightRefs = extractRefinements(a, new Map(), true)
|
|
3169
|
-
|
|
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
|
+
}
|
|
3170
3824
|
// i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
|
|
3171
3825
|
// skip f64 round-trip and __is_truthy call entirely.
|
|
3172
3826
|
if (va.type === 'i32') {
|
|
@@ -3214,6 +3868,21 @@ export const emitter = {
|
|
|
3214
3868
|
// De Morgan'd via the sense=false branch of extractRefinements (mirrors the ?: else-arm).
|
|
3215
3869
|
const rightRefs = extractRefinements(a, new Map(), false)
|
|
3216
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
|
+
}
|
|
3217
3886
|
if (va.type === 'i32') {
|
|
3218
3887
|
const vb = emitRight()
|
|
3219
3888
|
const t = tempI32()
|
|
@@ -3245,6 +3914,19 @@ export const emitter = {
|
|
|
3245
3914
|
'??': (a, b) => {
|
|
3246
3915
|
const va = emit(a), vb = emit(b)
|
|
3247
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
|
+
}
|
|
3248
3930
|
const numA = isNumArm(va, a), numB = isNumArm(vb, b)
|
|
3249
3931
|
// Both arms can surface as the (untyped) result — `a` when non-nullish (a NaN is not
|
|
3250
3932
|
// nullish, so it IS returned), `b` otherwise. Canon a lone-numeric arm; `a` before the
|
|
@@ -3295,8 +3977,10 @@ export const emitter = {
|
|
|
3295
3977
|
...Object.fromEntries([
|
|
3296
3978
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
3297
3979
|
].map(([op, fn]) => [op, (a, b) => {
|
|
3298
|
-
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
3980
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT) {
|
|
3981
|
+
bigintMixReject(op, a, b)
|
|
3299
3982
|
return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
|
|
3983
|
+
}
|
|
3300
3984
|
if (op === '|') { // `(x / y) | 0` integer-division idiom → i32.div_s
|
|
3301
3985
|
const divN = intLiteralValue(b) === 0 ? a : intLiteralValue(a) === 0 ? b : null
|
|
3302
3986
|
if (Array.isArray(divN) && divN[0] === '/') { const r = tryIntDivTrunc(divN[1], divN[2]); if (r) return r }
|
|
@@ -3378,6 +4062,7 @@ export const emitter = {
|
|
|
3378
4062
|
// An enclosing labeled statement (`outer: for …`) hands its label down so `continue outer`
|
|
3379
4063
|
// can target this loop's continue point. The immediately-enclosed loop consumes it.
|
|
3380
4064
|
const myLabel = ctx.func.pendingLabel; ctx.func.pendingLabel = null
|
|
4065
|
+
const bodyNode0 = body // identity for assumption owners — survives the hoist rebind below
|
|
3381
4066
|
const labeledContinue = myLabel != null && hasLabeledContinueTo(body, myLabel)
|
|
3382
4067
|
// Don't unroll a loop that is the target of a `continue <label>` — unrolling would lose the
|
|
3383
4068
|
// continue edge. (Plain loops with no labeled-continue still unroll.)
|
|
@@ -3391,6 +4076,246 @@ export const emitter = {
|
|
|
3391
4076
|
const fu = unrollForIn(init, cond, step, body)
|
|
3392
4077
|
if (fu) return fu
|
|
3393
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
|
+
}
|
|
3394
4319
|
// Lift constant array/object literals out of the loop (allocate once, not per
|
|
3395
4320
|
// iteration) when they are read-only + non-escaping inside it. Strip them from the
|
|
3396
4321
|
// body up front so freshBoxed / continue analysis see the reduced body.
|
|
@@ -3406,7 +4331,7 @@ export const emitter = {
|
|
|
3406
4331
|
// can target the loop label directly, saving a redundant `block`.
|
|
3407
4332
|
const needsCont = step && (hasOwnContinue(body) || labeledContinue)
|
|
3408
4333
|
const cont = needsCont ? `$cont${id}` : loop
|
|
3409
|
-
ctx.func.stack.push({ brk, loop: cont })
|
|
4334
|
+
ctx.func.stack.push({ brk, loop: cont, bodyNode: bodyNode0 })
|
|
3410
4335
|
const frame = ctx.func.stack[ctx.func.stack.length - 1]
|
|
3411
4336
|
if (myLabel != null) frame.contLabel = myLabel // so `continue <myLabel>` targets this loop's step/test
|
|
3412
4337
|
// Per-iteration fresh cells for boxed locals declared in the body — allocated
|