jz 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/module/core.js +26 -0
- package/module/math.js +51 -3
- package/module/string.js +5 -0
- package/package.json +1 -1
- package/src/emit.js +49 -6
- package/src/jzify.js +74 -47
- package/src/prepare.js +11 -3
package/module/core.js
CHANGED
|
@@ -722,6 +722,32 @@ export default (ctx) => {
|
|
|
722
722
|
if (tpEmitter && tpEmitter.length <= 1) return tpEmitter(obj)
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
+
// valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
|
|
726
|
+
// property shadows. On a heap receiver carrying a dynamic-prop sidecar
|
|
727
|
+
// (array/typed/object), reading `obj.valueOf`/`obj.toString` must return an
|
|
728
|
+
// assigned override when present, else the inherited builtin. Without this,
|
|
729
|
+
// the arity-1 builtin emitter below (returns the receiver) masks the
|
|
730
|
+
// override. The method-call path in src/emit.js runs the parallel probe and
|
|
731
|
+
// additionally covers statically-unknown receivers (e.g. `arr[0].valueOf()`);
|
|
732
|
+
// a bare read of an unknown-type receiver can't yield a builtin-as-value
|
|
733
|
+
// here anyway, so this read path stays scoped to known sidecar types.
|
|
734
|
+
if ((prop === 'valueOf' || prop === 'toString') && ctx.closure.call &&
|
|
735
|
+
(ptVt === VAL.ARRAY || ptVt === VAL.TYPED || ptVt === VAL.OBJECT)) {
|
|
736
|
+
const builtin = ctx.core.emit[`.${ptVt}:${prop}`] || ctx.core.emit[`.${prop}`]
|
|
737
|
+
if (builtin && builtin.length <= 1) {
|
|
738
|
+
const o = temp('vo'), p = temp('vp')
|
|
739
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
740
|
+
return typed(['block', ['result', 'f64'],
|
|
741
|
+
['local.set', `$${o}`, asF64(emit(obj))],
|
|
742
|
+
['local.set', `$${p}`, ['f64.reinterpret_i64',
|
|
743
|
+
['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${o}`]], asI64(emit(['str', prop]))]]],
|
|
744
|
+
['if', ['result', 'f64'],
|
|
745
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${p}`]]], ['i32.const', PTR.CLOSURE]],
|
|
746
|
+
['then', ['local.get', `$${p}`]],
|
|
747
|
+
['else', asF64(builtin(o))]]], 'f64')
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
725
751
|
// Module-registered property emitter (.size, etc.)
|
|
726
752
|
const propKey = `.${prop}`
|
|
727
753
|
const propEmitter = ctx.core.emit[propKey]
|
package/module/math.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @module math
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { typed, asF64, asI32, toI32, toNumF64, temp, arrayLoop, isLit, litVal } from '../src/ir.js'
|
|
15
|
+
import { typed, asF64, asI32, toI32, toNumF64, temp, arrayLoop, isLit, litVal, isPureIR } from '../src/ir.js'
|
|
16
16
|
import { emit } from '../src/emit.js'
|
|
17
17
|
import { emitter } from '../src/ctx.js'
|
|
18
18
|
import { repOf } from '../src/analyze.js'
|
|
@@ -182,8 +182,56 @@ export default (ctx) => {
|
|
|
182
182
|
ctx.core.emit['math.log10'] = emitter(['math.log10'], a => call('math.log10', a))
|
|
183
183
|
ctx.core.emit['math.log1p'] = emitter(['math.log1p'], a => call('math.log1p', a))
|
|
184
184
|
|
|
185
|
-
// Power
|
|
186
|
-
|
|
185
|
+
// Power. Constant-integer-exponent `Math.pow(x,n)` / `x ** n` (|n| ≤ POW_FOLD_MAX)
|
|
186
|
+
// lower to inline square-and-multiply instead of a $math.pow call. The fold is
|
|
187
|
+
// bit-identical to $math.pow's integer fast path: that path runs the same LSB-first
|
|
188
|
+
// square-and-multiply, and an f64 product's magnitude is the rounded product of the
|
|
189
|
+
// operand magnitudes regardless of sign — so multiplying the *signed* base reproduces
|
|
190
|
+
// both the exact bits and the result sign (negative iff x<0 ∧ n odd, which is exactly
|
|
191
|
+
// its `neg_base`). A program whose only pow use is folded then never pulls the
|
|
192
|
+
// math.pow/exp/log stdlib. `**`'s exponent is parsed as a bare number (incl. negatives).
|
|
193
|
+
const POW_FOLD_MAX = 8
|
|
194
|
+
const get = name => ['local.get', `$${name}`]
|
|
195
|
+
const constInt = b => {
|
|
196
|
+
const v = typeof b === 'number' ? b
|
|
197
|
+
: (Array.isArray(b) && b.length === 2 && b[0] == null && typeof b[1] === 'number') ? b[1]
|
|
198
|
+
: null
|
|
199
|
+
return v != null && Number.isInteger(v) ? v : null
|
|
200
|
+
}
|
|
201
|
+
const foldPow = (a, n) => {
|
|
202
|
+
const baseIR = toNumF64(a, emit(a))
|
|
203
|
+
// pow(x,0) === 1 for every x (NaN/±0/±Inf included). Keep the base's side
|
|
204
|
+
// effects (a call, a throwing valueOf), discard its value, yield 1.
|
|
205
|
+
if (n === 0) return isPureIR(baseIR)
|
|
206
|
+
? typed(['f64.const', 1], 'f64')
|
|
207
|
+
: typed(['block', ['result', 'f64'], ['drop', baseIR], ['f64.const', 1]], 'f64')
|
|
208
|
+
const b = temp('pw')
|
|
209
|
+
const stmts = [['local.set', `$${b}`, baseIR]]
|
|
210
|
+
// square-and-multiply, LSB-first — mirrors $math.pow's loop association exactly,
|
|
211
|
+
// so the rounding tree (and thus the last bit) matches.
|
|
212
|
+
let sq = b, res = null, minted = false
|
|
213
|
+
for (let m = Math.abs(n); m > 0; m >>= 1) {
|
|
214
|
+
if (m & 1) {
|
|
215
|
+
if (res === null) res = sq // lowest set bit: result := this square (skip ×1)
|
|
216
|
+
else { const r = temp('pw'); stmts.push(['local.set', `$${r}`, ['f64.mul', get(res), get(sq)]]); res = r; minted = true }
|
|
217
|
+
}
|
|
218
|
+
if (m >> 1) { const s = temp('pw'); stmts.push(['local.set', `$${s}`, ['f64.mul', get(sq), get(sq)]]); sq = s; minted = true }
|
|
219
|
+
}
|
|
220
|
+
let result = get(res)
|
|
221
|
+
if (n < 0) { result = ['f64.div', ['f64.const', 1], result]; minted = true } // y<0 → reciprocal, as $math.pow does
|
|
222
|
+
// A NaN minted by f64.mul/div has a platform-nondeterministic sign; jz's value
|
|
223
|
+
// model requires the one canonical number-NaN, so `canon` folds it back. Skip when
|
|
224
|
+
// the base provably can't be NaN (same test min/max uses) or when no op was minted
|
|
225
|
+
// (|n|=1 hands the base straight through, already canonical).
|
|
226
|
+
const inner = typed(['block', ['result', 'f64'], ...stmts, result], 'f64')
|
|
227
|
+
return (minted && !neverNaN(a, baseIR)) ? canon(inner) : inner
|
|
228
|
+
}
|
|
229
|
+
const powCall = emitter(['math.pow'], (a, b) => call('math.pow', a, b))
|
|
230
|
+
ctx.core.emit['math.pow'] = (a, b) => {
|
|
231
|
+
const n = constInt(b)
|
|
232
|
+
return n !== null && Math.abs(n) <= POW_FOLD_MAX ? foldPow(a, n) : powCall(a, b)
|
|
233
|
+
}
|
|
234
|
+
ctx.core.emit['math.pow'].deps = powCall.deps // metadata parity (tabulation/analysis)
|
|
187
235
|
ctx.core.emit['**'] = ctx.core.emit['math.pow']
|
|
188
236
|
ctx.core.emit['math.cbrt'] = emitter(['math.cbrt'], a => call('math.cbrt', a))
|
|
189
237
|
ctx.core.emit['math.hypot'] = emitter(['math.hypot'], (a, b, ...rest) => {
|
package/module/string.js
CHANGED
|
@@ -1628,6 +1628,11 @@ export default (ctx) => {
|
|
|
1628
1628
|
|
|
1629
1629
|
ctx.core.emit['.encode'] = (obj, str) => {
|
|
1630
1630
|
inc('__str_encode')
|
|
1631
|
+
// .encode() yields a runtime PTR.TYPED/u8 array (see __mkptr above). Downstream
|
|
1632
|
+
// indexing/spread dispatch through __typed_idx, whose element-unaware fallback
|
|
1633
|
+
// (f64.load, stride 8) is only valid when no typed array can flow in. Enabling
|
|
1634
|
+
// the feature pulls the element-aware variant — same invariant `.length` follows.
|
|
1635
|
+
ctx.features.typedarray = true
|
|
1631
1636
|
return typed(['call', '$__str_encode', asI64(emit(str))], 'f64')
|
|
1632
1637
|
}
|
|
1633
1638
|
|
package/package.json
CHANGED
package/src/emit.js
CHANGED
|
@@ -789,11 +789,16 @@ function emitSpreadCopy(dest, posLocal, srcLocal, srcLenLocal, staticVT) {
|
|
|
789
789
|
['then', (inc('__str_idx'), ['call', '$__str_idx', srcI64(), ['local.get', `$${sidx}`]])],
|
|
790
790
|
['else', (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])]]
|
|
791
791
|
: (inc('__typed_idx'), ['call', '$__typed_idx', srcI64(), ['local.get', `$${sidx}`]])
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
['
|
|
792
|
+
// Reset the counter on each entry — WASM zeroes locals once at function
|
|
793
|
+
// entry, but this loop re-executes when the spread sits inside a JS loop;
|
|
794
|
+
// a stale `sidx` (= prior srcLen) would skip the copy entirely.
|
|
795
|
+
return ['block', `$break${loopId}`,
|
|
796
|
+
['local.set', `$${sidx}`, ['i32.const', 0]],
|
|
797
|
+
['loop', `$loop${loopId}`,
|
|
798
|
+
['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${srcLenLocal}`]]],
|
|
799
|
+
['f64.store', destAddr(['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${sidx}`]]), elem],
|
|
800
|
+
['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
|
|
801
|
+
['br', `$loop${loopId}`]]]
|
|
797
802
|
}
|
|
798
803
|
const advance = ['local.set', `$${posLocal}`,
|
|
799
804
|
['i32.add', ['local.get', `$${posLocal}`], ['local.get', `$${srcLenLocal}`]]]
|
|
@@ -2067,7 +2072,16 @@ export const emitter = {
|
|
|
2067
2072
|
// i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
|
|
2068
2073
|
// the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
|
|
2069
2074
|
// unknown type) routes through __to_num so "2026" | 0 === 2026.
|
|
2070
|
-
|
|
2075
|
+
// `~~x` is the idiomatic int32 truncation: the two xor-with-(-1) cancel, leaving
|
|
2076
|
+
// a single toI32 (whose NaN/Infinity guard runs once, unchanged). Fold it here so
|
|
2077
|
+
// DSP/bytebeat `~~` doesn't emit a dead double-xor watr won't remove.
|
|
2078
|
+
'~': a => {
|
|
2079
|
+
if (Array.isArray(a) && a[0] === '~') {
|
|
2080
|
+
const inner = a[1], iv = emit(inner)
|
|
2081
|
+
return isLit(iv) ? emitNum(~~litVal(iv)) : typed(toI32(isI32Num(iv) ? iv : toNumF64(inner, iv)), 'i32')
|
|
2082
|
+
}
|
|
2083
|
+
const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(isI32Num(v) ? v : toNumF64(a, v)), typed(['i32.const', -1], 'i32')], 'i32')
|
|
2084
|
+
},
|
|
2071
2085
|
...Object.fromEntries([
|
|
2072
2086
|
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
2073
2087
|
].map(([op, fn]) => [op, (a, b) => {
|
|
@@ -2571,6 +2585,35 @@ export const emitter = {
|
|
|
2571
2585
|
}
|
|
2572
2586
|
}
|
|
2573
2587
|
|
|
2588
|
+
// valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
|
|
2589
|
+
// property shadows. An assigned `obj.valueOf`/`obj.toString` must win over
|
|
2590
|
+
// the builtin emitter for any receiver that can carry a dynamic-prop
|
|
2591
|
+
// sidecar — a sidecar-bearing static type (array/typed/object) OR a
|
|
2592
|
+
// statically-unknown receiver (e.g. an array-element read `arr[0]`, whose
|
|
2593
|
+
// type is only known at runtime). Probe the sidecar and call it when it
|
|
2594
|
+
// holds a closure, else fall back to the builtin (generic when untyped:
|
|
2595
|
+
// `.valueOf` returns the receiver, `.toString` runs type-aware __to_str).
|
|
2596
|
+
// Parallels the member-READ check in module/core.js emitPropAccess (which
|
|
2597
|
+
// stays scoped to known sidecar types). (watr's `str()` attaches
|
|
2598
|
+
// `bytes.valueOf = () => s`, recovered via `.valueOf()`.)
|
|
2599
|
+
if ((method === 'valueOf' || method === 'toString') && ctx.closure.call
|
|
2600
|
+
&& !parsed.hasSpread && parsed.normal.length === 0
|
|
2601
|
+
&& (vt === VAL.ARRAY || vt === VAL.TYPED || vt === VAL.OBJECT || !vt)) {
|
|
2602
|
+
const builtin = (vt && ctx.core.emit[`.${vt}:${method}`]) || ctx.core.emit[`.${method}`]
|
|
2603
|
+
if (builtin) {
|
|
2604
|
+
const objTmp = temp('vobj'), propTmp = temp('vprop')
|
|
2605
|
+
inc('__dyn_get_expr', '__ptr_type')
|
|
2606
|
+
return typed(['block', ['result', 'f64'],
|
|
2607
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
2608
|
+
['local.set', `$${propTmp}`, ['f64.reinterpret_i64',
|
|
2609
|
+
['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]]],
|
|
2610
|
+
['if', ['result', 'f64'],
|
|
2611
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
|
|
2612
|
+
['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [])],
|
|
2613
|
+
['else', asF64(callMethod(objTmp, builtin))]]], 'f64')
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2574
2617
|
// Known type → static dispatch
|
|
2575
2618
|
if (vt && ctx.core.emit[`.${vt}:${method}`]) {
|
|
2576
2619
|
return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
|
package/src/jzify.js
CHANGED
|
@@ -368,6 +368,18 @@ function functionBodyBlock(body) {
|
|
|
368
368
|
/** Prototype identity check: X.prototype.Y */
|
|
369
369
|
const isProto = n => Array.isArray(n) && n[0] === '.' && Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'prototype'
|
|
370
370
|
|
|
371
|
+
/** `obj.M <eq> Ctor.prototype.M` is not a real reference comparison — jz has no
|
|
372
|
+
* prototype objects — it asks whether `obj` overrides `M`, which is exactly
|
|
373
|
+
* `obj.hasOwnProperty('M')`. Returns that call node when the shape matches
|
|
374
|
+
* (a member access `obj.M` against a same-named prototype method), else null. */
|
|
375
|
+
const methodOverrideHasOwn = (a, b) => {
|
|
376
|
+
const proto = isProto(a) ? a : isProto(b) ? b : null
|
|
377
|
+
if (!proto) return null
|
|
378
|
+
const other = proto === a ? b : a
|
|
379
|
+
if (!Array.isArray(other) || other[0] !== '.' || other[2] !== proto[2]) return null
|
|
380
|
+
return ['()', ['.', transform(other[1]), 'hasOwnProperty'], [null, proto[2]]]
|
|
381
|
+
}
|
|
382
|
+
|
|
371
383
|
const TYPED_ARRAYS = new Set(['Float64Array','Float32Array','Int32Array','Uint32Array',
|
|
372
384
|
'Int16Array','Uint16Array','Int8Array','Uint8Array',
|
|
373
385
|
'ArrayBuffer','BigInt64Array','BigUint64Array','DataView'])
|
|
@@ -906,18 +918,8 @@ const handlers = {
|
|
|
906
918
|
|
|
907
919
|
'switch'(disc, ...cases) {
|
|
908
920
|
const clean = cases.map(c => {
|
|
909
|
-
if (c[0] === 'case'
|
|
910
|
-
|
|
911
|
-
const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
|
|
912
|
-
return ['case', c[1], stripped]
|
|
913
|
-
}
|
|
914
|
-
if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
|
|
915
|
-
const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
|
|
916
|
-
const stripped = stripTerminalSwitchBreak(body.length === 1 ? body[0] : [';', ...body])
|
|
917
|
-
return ['default', stripped]
|
|
918
|
-
}
|
|
919
|
-
if (c[0] === 'case') return ['case', c[1], stripTerminalSwitchBreak(c[2])]
|
|
920
|
-
if (c[0] === 'default') return ['default', stripTerminalSwitchBreak(c[1])]
|
|
921
|
+
if (c[0] === 'case') return ['case', c[1], normalizeCaseBody(c[2])]
|
|
922
|
+
if (c[0] === 'default') return ['default', normalizeCaseBody(c[1])]
|
|
921
923
|
return c
|
|
922
924
|
})
|
|
923
925
|
return transformSwitch(disc, clean)
|
|
@@ -925,12 +927,14 @@ const handlers = {
|
|
|
925
927
|
|
|
926
928
|
// Equality keeps the JS loose/strict distinction (jz core now lowers both):
|
|
927
929
|
// `==`/`!=` stay loose, `===`/`!==` stay strict. A comparison against a prototype
|
|
928
|
-
// object
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
'
|
|
932
|
-
'
|
|
933
|
-
'
|
|
930
|
+
// object folds to a boolean — jz has no prototype objects, so identity against one
|
|
931
|
+
// is decided statically. The one exception is `obj.M <eq> Ctor.prototype.M`: that
|
|
932
|
+
// probes whether `obj` overrides the builtin `M`, so it lowers to a runtime
|
|
933
|
+
// `obj.hasOwnProperty('M')` (equal ⇒ not overridden, unequal ⇒ overridden).
|
|
934
|
+
'=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; return isProto(a) || isProto(b) ? 1 : ['==', transform(a), transform(b)] },
|
|
935
|
+
'!='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; return isProto(a) || isProto(b) ? 0 : ['!=', transform(a), transform(b)] },
|
|
936
|
+
'==='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; if (isProto(a) || isProto(b)) return 1 },
|
|
937
|
+
'!=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; if (isProto(a) || isProto(b)) return 0 },
|
|
934
938
|
|
|
935
939
|
// new → call (keep TypedArrays)
|
|
936
940
|
'new'(ctor, ...cargs) {
|
|
@@ -1474,18 +1478,12 @@ function cloneAst(node) {
|
|
|
1474
1478
|
return node.map(cloneAst)
|
|
1475
1479
|
}
|
|
1476
1480
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
return ['{}', Array.isArray(inner) && inner[0] === ';' ? inner : [';', inner]]
|
|
1484
|
-
}
|
|
1485
|
-
if (body[0] !== ';') return body
|
|
1486
|
-
|
|
1487
|
-
const stmts = body.slice(1)
|
|
1488
|
-
if (Array.isArray(stmts.at(-1)) && stmts.at(-1)[0] === 'break') stmts.pop()
|
|
1481
|
+
/** Flatten a switch clause body to a single node, dropping ASI position markers.
|
|
1482
|
+
* Unlike a plain statement list this keeps any `break` intact — transformSwitch
|
|
1483
|
+
* needs the breaks to gate fall-through; it rewrites them to a sticky flag. */
|
|
1484
|
+
function normalizeCaseBody(body) {
|
|
1485
|
+
if (!Array.isArray(body) || body[0] !== ';') return body
|
|
1486
|
+
const stmts = body.slice(1).filter(s => s != null && typeof s !== 'number')
|
|
1489
1487
|
return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
|
|
1490
1488
|
}
|
|
1491
1489
|
|
|
@@ -1523,31 +1521,60 @@ function rewriteSwitchBreaks(node, flag) {
|
|
|
1523
1521
|
return node.map((part, i) => i === 0 ? part : rewriteSwitchBreaks(part, flag))
|
|
1524
1522
|
}
|
|
1525
1523
|
|
|
1526
|
-
/** Transform switch
|
|
1524
|
+
/** Transform a switch into structured control flow with faithful fall-through.
|
|
1525
|
+
*
|
|
1526
|
+
* A pure if/else-if chain (the former lowering) can't express fall-through,
|
|
1527
|
+
* stacked labels, or a `default` clause that isn't last \u2014 it ran only the first
|
|
1528
|
+
* matching body. The correct model is two-phase, evaluated once with no goto:
|
|
1529
|
+
*
|
|
1530
|
+
* 1. ENTRY \u2014 compare the discriminant against each `case` label in source
|
|
1531
|
+
* order; the first `===` match fixes the entry index. No case matches \u2192
|
|
1532
|
+
* entry = the `default` clause's source index (or past-end if none).
|
|
1533
|
+
* 2. RUN \u2014 walk clauses in source order; `entry <= i` runs clause i, so every
|
|
1534
|
+
* clause from the entry onward executes (fall-through). A `break` flips the
|
|
1535
|
+
* sticky `brk` flag (via rewriteSwitchBreaks) and gates the rest.
|
|
1536
|
+
*
|
|
1537
|
+
* The discriminant is bound to a temp only when re-reading it isn't free/safe; a
|
|
1538
|
+
* bare identifier is compared directly \u2014 a synthetic temp would shed its STRING
|
|
1539
|
+
* val-type and mis-fold string `case`s to `false` under strict-=== folding. */
|
|
1527
1540
|
let swIdx = 0
|
|
1528
1541
|
function transformSwitch(discriminant, cases) {
|
|
1529
1542
|
const disc = transform(discriminant)
|
|
1530
|
-
const
|
|
1543
|
+
const simple = typeof disc === 'string' || (Array.isArray(disc) && disc[0] == null)
|
|
1544
|
+
const tmp = simple ? disc : `\uE000sw${swIdx++}`
|
|
1545
|
+
const start = `\uE000swst${swIdx++}`
|
|
1531
1546
|
const needsBreakFlag = cases.some(c => hasOwnSwitchBreak(c[0] === 'case' ? c[2] : c[1]))
|
|
1532
1547
|
const brk = needsBreakFlag ? `\uE000swbrk${swIdx++}` : null
|
|
1533
1548
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1549
|
+
const n = cases.length
|
|
1550
|
+
let defaultIdx = -1
|
|
1551
|
+
const bodies = cases.map((c, i) => {
|
|
1552
|
+
if (c[0] === 'default') { defaultIdx = i; return transform(c[1]) }
|
|
1553
|
+
return transform(c[2])
|
|
1554
|
+
})
|
|
1538
1555
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1556
|
+
const stmts = []
|
|
1557
|
+
if (!simple) stmts.push(['let', ['=', tmp, disc]])
|
|
1558
|
+
|
|
1559
|
+
// Phase 1 \u2014 entry index. Init to default's position (or n = "no clause runs"),
|
|
1560
|
+
// then let the first matching label override it via an if/else-if chain.
|
|
1561
|
+
stmts.push(['let', ['=', start, [null, defaultIdx >= 0 ? defaultIdx : n]]])
|
|
1562
|
+
let chain = null
|
|
1563
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1564
|
+
if (cases[i][0] !== 'case') continue
|
|
1565
|
+
const hit = ['=', start, [null, i]]
|
|
1566
|
+
const cond = ['===', tmp, transform(cases[i][1])]
|
|
1567
|
+
chain = chain != null ? ['if', cond, hit, chain] : ['if', cond, hit]
|
|
1550
1568
|
}
|
|
1551
1569
|
if (chain) stmts.push(chain)
|
|
1570
|
+
if (brk) stmts.push(['let', ['=', brk, [null, false]]])
|
|
1571
|
+
|
|
1572
|
+
// Phase 2 \u2014 run clauses from the entry index, falling through until a break.
|
|
1573
|
+
for (let i = 0; i < n; i++) {
|
|
1574
|
+
if (bodies[i] == null) continue
|
|
1575
|
+
const body = brk ? rewriteSwitchBreaks(bodies[i], brk) : bodies[i]
|
|
1576
|
+
const reached = ['<=', start, [null, i]]
|
|
1577
|
+
stmts.push(['if', brk ? ['&&', ['!', brk], reached] : reached, body])
|
|
1578
|
+
}
|
|
1552
1579
|
return [';', ...stmts]
|
|
1553
1580
|
}
|
package/src/prepare.js
CHANGED
|
@@ -91,6 +91,13 @@ const addHostImport = (mod, name, alias, spec) => {
|
|
|
91
91
|
|
|
92
92
|
const isImportMeta = node => Array.isArray(node) && node[0] === '.' && node[1] === 'import' && node[2] === 'meta'
|
|
93
93
|
const isImportMetaProp = (node, prop) => Array.isArray(node) && node[0] === '.' && isImportMeta(node[1]) && node[2] === prop
|
|
94
|
+
// In a pure boolean position (consumer reads only truthiness) `!!e` is exactly `e`.
|
|
95
|
+
// Drop redundant double-negation; recurse so `!!!!e → e`. NOT valid for `&&`/`||`
|
|
96
|
+
// operands — those are value-preserving (`!!a && b` returns `false`, not `a`).
|
|
97
|
+
const stripBoolNot = c => {
|
|
98
|
+
while (Array.isArray(c) && c[0] === '!' && Array.isArray(c[1]) && c[1][0] === '!') c = c[1][1]
|
|
99
|
+
return c
|
|
100
|
+
}
|
|
94
101
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
95
102
|
const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
96
103
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
@@ -1357,13 +1364,13 @@ const handlers = {
|
|
|
1357
1364
|
|
|
1358
1365
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
1359
1366
|
'if': (cond, then, els) => {
|
|
1360
|
-
const c = prep(cond)
|
|
1367
|
+
const c = prep(stripBoolNot(cond))
|
|
1361
1368
|
pushScope(); const t = prep(then); popScope()
|
|
1362
1369
|
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
1363
1370
|
return ['if', c, t]
|
|
1364
1371
|
},
|
|
1365
1372
|
'while': (cond, body) => {
|
|
1366
|
-
const c = prep(cond)
|
|
1373
|
+
const c = prep(stripBoolNot(cond))
|
|
1367
1374
|
pushScope(); const b = prep(body); popScope()
|
|
1368
1375
|
return ['while', c, b]
|
|
1369
1376
|
},
|
|
@@ -1555,7 +1562,7 @@ const handlers = {
|
|
|
1555
1562
|
},
|
|
1556
1563
|
|
|
1557
1564
|
// Ternary: parser emits '?' not '?:'
|
|
1558
|
-
'?'(cond, then, els) { return ['?:', prep(cond), prep(then), prep(els)] },
|
|
1565
|
+
'?'(cond, then, els) { return ['?:', prep(stripBoolNot(cond)), prep(then), prep(els)] },
|
|
1559
1566
|
|
|
1560
1567
|
// ++/-- prefix vs postfix: parser sends trailing null for postfix
|
|
1561
1568
|
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
|
|
@@ -1721,6 +1728,7 @@ const handlers = {
|
|
|
1721
1728
|
let r
|
|
1722
1729
|
if (Array.isArray(head) && head[0] === ';') {
|
|
1723
1730
|
let [, init, cond, step] = head
|
|
1731
|
+
cond = stripBoolNot(cond)
|
|
1724
1732
|
// Hoist .length / .size / .byteLength from for-condition:
|
|
1725
1733
|
// `i < arr.length` → `let __len = arr.length | 0; ... i < __len`
|
|
1726
1734
|
// The `| 0` forces i32 even for unknown-typed receivers (where __length
|