jz 0.7.0 → 0.8.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.
Files changed (55) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1366 -1101
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +49 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +167 -117
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +295 -171
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +40 -35
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +324 -34
  28. package/src/compile/index.js +204 -61
  29. package/src/compile/infer.js +8 -1
  30. package/src/compile/loop-divmod.js +12 -58
  31. package/src/compile/loop-model.js +91 -0
  32. package/src/compile/loop-recurrence.js +167 -0
  33. package/src/compile/loop-square.js +102 -0
  34. package/src/compile/narrow.js +180 -34
  35. package/src/compile/peel-stencil.js +18 -64
  36. package/src/compile/plan/common.js +29 -0
  37. package/src/compile/plan/index.js +4 -1
  38. package/src/compile/plan/inline.js +176 -21
  39. package/src/compile/plan/literals.js +93 -19
  40. package/src/ctx.js +51 -12
  41. package/src/helper-counters.js +137 -0
  42. package/src/ir.js +102 -13
  43. package/src/kind-traits.js +7 -3
  44. package/src/kind.js +14 -1
  45. package/src/op-policy.js +5 -2
  46. package/src/ops.js +119 -0
  47. package/src/optimize/index.js +1125 -136
  48. package/src/optimize/recurse.js +182 -0
  49. package/src/optimize/vectorize.js +1302 -144
  50. package/src/prepare/index.js +29 -12
  51. package/src/reps.js +4 -1
  52. package/src/type.js +53 -45
  53. package/src/wat/assemble.js +92 -9
  54. package/src/widen.js +21 -0
  55. package/src/wat/optimize.js +0 -3938
package/module/number.js CHANGED
@@ -145,12 +145,224 @@ const sciExponent = (tail) => `
145
145
  ${tail}))`
146
146
 
147
147
  // Apply the accumulated base-10 exponent to $result via __pow10.
148
+ // Used as fallback when EL cannot determine a unique rounding.
148
149
  const POW10_SCALE = `
149
150
  (if (i32.gt_s (local.get $decExp) (i32.const 0))
150
151
  (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
151
152
  (if (i32.lt_s (local.get $decExp) (i32.const 0))
152
153
  (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))`
153
154
 
155
+ // Eisel-Lemire correctly-rounded decimal-to-f64.
156
+ // Used in place of FINISH_SIGNIFICAND + POW10_SCALE. Keeps $mant as i64 until after
157
+ // sciExponent finalizes $decExp, then calls $__dec_to_f64 with both.
158
+ // Falls back to f64.convert_i64_u + POW10_SCALE when EL returns NaN (ambiguous).
159
+ // The caller handles sign INSIDE this fragment (so the final return is the signed result).
160
+ const EL_SCALE = `
161
+ (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
162
+ (if (local.get $round) (then (local.set $mant (i64.add (local.get $mant) (i64.const 1)))))
163
+ (local.set $result (call $__dec_to_f64 (local.get $mant) (local.get $decExp)))
164
+ (if (f64.ne (local.get $result) (local.get $result))
165
+ (then
166
+ (local.set $result (f64.convert_i64_u (local.get $mant)))
167
+ ${POW10_SCALE}))
168
+ (if (local.get $neg) (then (local.set $result (f64.neg (local.get $result)))))`
169
+
170
+ // $__dec_to_f64: Eisel-Lemire correctly-rounded f64 from (mant × 10^exp10).
171
+ // Returns NaN (as sentinel) for ambiguous cases — caller falls back to POW10_SCALE.
172
+ // Reads 651-entry 128-bit power-of-ten table from global $__el_tbl (injected at compile time).
173
+ // Table layout: entry[i] = lo_i64_LE || hi_i64_LE, i = exp10 + 342.
174
+ //
175
+ // Algorithm: normalize mant to 64 bits, multiply by 128-bit table entry to get 128-bit
176
+ // product (prodHi:prodLo), extract 52-bit IEEE mantissa with correct rounding.
177
+ // Handles subnormals, overflow to Infinity, and early-exit for 0/trivial ranges.
178
+ const DEC_TO_F64_WAT = `(func $__dec_to_f64
179
+ (param $mant i64) (param $exp10 i32)
180
+ (result f64)
181
+ (local $mBits i32)
182
+ (local $w i64)
183
+ (local $tbl i32)
184
+ (local $tblHi i64) (local $tblLo i64)
185
+ ;; 128-bit product: prodHi × 2^64 + prodLo
186
+ (local $p1hi i64) (local $p1lo i64)
187
+ (local $p2hi i64) (local $p2lo i64)
188
+ (local $prodHi i64) (local $prodLo i64)
189
+ (local $carry i64)
190
+ (local $lz i32)
191
+ (local $log2floor i32)
192
+ (local $exp2 i32) (local $biased i32)
193
+ (local $roundShift i32)
194
+ (local $roundBit i64)
195
+ (local $sticky i64)
196
+ (local $mant52 i64)
197
+ (local $subnShift i32) (local $totalShift i32)
198
+ (local $snRound i64) (local $snSticky i64) (local $snMant i64)
199
+ ;; 32-bit limb temps for mul64
200
+ (local $a0 i32) (local $a1 i32) (local $b0 i32) (local $b1 i32)
201
+ (local $t00 i64) (local $t01 i64) (local $t10 i64) (local $t11 i64)
202
+ (local $mid i64) (local $mid_carry i64)
203
+ ;; Zero check
204
+ (if (i64.eqz (local.get $mant)) (then (return (f64.const 0))))
205
+ ;; Compute bit length of mant (1..64) by normalizing to 64 bits via clz
206
+ ;; mBits = 64 - clz64(mant)
207
+ (local.set $mBits (i32.sub (i32.const 64) (i32.wrap_i64 (i64.clz (local.get $mant)))))
208
+ ;; Normalize mant to 64-bit: w = mant << (64 - mBits)
209
+ (local.set $w (i64.shl (local.get $mant) (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $mBits)))))
210
+ ;; Range checks: the table is TRIMMED to exp10 in [-65..65] (covers every
211
+ ;; real-world / source / JSON constant; fft/synth's coefficients are ~10^-23).
212
+ ;; Outside that, return NaN → caller falls back to POW10_SCALE (the pre-EL path,
213
+ ;; ~1 ULP for moderate exponents, exact only near the f64 limits no real literal
214
+ ;; reaches). This keeps the data segment ~2KB instead of ~10KB on every program
215
+ ;; that does an untyped numeric coercion (which pulls __to_num).
216
+ (if (i32.or (i32.lt_s (local.get $exp10) (i32.const -65))
217
+ (i32.gt_s (local.get $exp10) (i32.const 65)))
218
+ (then (return (f64.const nan))))
219
+ ;; Load 128-bit table entry for exp10: tbl = $__el_tbl + (exp10 + 65) * 16
220
+ (local.set $tbl (i32.add (global.get $__el_tbl) (i32.shl (i32.add (local.get $exp10) (i32.const 65)) (i32.const 4))))
221
+ (local.set $tblLo (i64.load (local.get $tbl)))
222
+ (local.set $tblHi (i64.load (i32.add (local.get $tbl) (i32.const 8))))
223
+ ;; ─── Two-product: (prodHi:prodLo) = w × tblHi + (w × tblLo >> 64) ──────────
224
+ ;; mul64(w, tblHi) → p1hi:p1lo
225
+ ;; Split w into 32-bit halves
226
+ (local.set $a0 (i32.wrap_i64 (i64.and (local.get $w) (i64.const 0xFFFFFFFF))))
227
+ (local.set $a1 (i32.wrap_i64 (i64.shr_u (local.get $w) (i64.const 32))))
228
+ (local.set $b0 (i32.wrap_i64 (i64.and (local.get $tblHi) (i64.const 0xFFFFFFFF))))
229
+ (local.set $b1 (i32.wrap_i64 (i64.shr_u (local.get $tblHi) (i64.const 32))))
230
+ (local.set $t00 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b0))))
231
+ (local.set $t01 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b1))))
232
+ (local.set $t10 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b0))))
233
+ (local.set $t11 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b1))))
234
+ ;; mid = t01 + (t00>>32), track carry; then mid += t10, track carry
235
+ ;; Each addition can carry at most 1 bit; total carry ≤ 2 → hi += carry<<32
236
+ (local.set $mid (i64.add (local.get $t01) (i64.shr_u (local.get $t00) (i64.const 32))))
237
+ (local.set $mid_carry (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t01))))
238
+ (local.set $mid (i64.add (local.get $mid) (local.get $t10)))
239
+ (local.set $mid_carry (i64.add (local.get $mid_carry)
240
+ (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t10)))))
241
+ (local.set $p1hi (i64.add
242
+ (i64.add (local.get $t11) (i64.shr_u (local.get $mid) (i64.const 32)))
243
+ (i64.shl (local.get $mid_carry) (i64.const 32))))
244
+ (local.set $p1lo (i64.or
245
+ (i64.and (local.get $t00) (i64.const 0xFFFFFFFF))
246
+ (i64.shl (i64.and (local.get $mid) (i64.const 0xFFFFFFFF)) (i64.const 32))))
247
+ ;; mul64(w, tblLo) → p2hi:p2lo
248
+ (local.set $b0 (i32.wrap_i64 (i64.and (local.get $tblLo) (i64.const 0xFFFFFFFF))))
249
+ (local.set $b1 (i32.wrap_i64 (i64.shr_u (local.get $tblLo) (i64.const 32))))
250
+ (local.set $t00 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b0))))
251
+ (local.set $t01 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b1))))
252
+ (local.set $t10 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b0))))
253
+ (local.set $t11 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b1))))
254
+ (local.set $mid (i64.add (local.get $t01) (i64.shr_u (local.get $t00) (i64.const 32))))
255
+ (local.set $mid_carry (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t01))))
256
+ (local.set $mid (i64.add (local.get $mid) (local.get $t10)))
257
+ (local.set $mid_carry (i64.add (local.get $mid_carry)
258
+ (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t10)))))
259
+ (local.set $p2hi (i64.add
260
+ (i64.add (local.get $t11) (i64.shr_u (local.get $mid) (i64.const 32)))
261
+ (i64.shl (local.get $mid_carry) (i64.const 32))))
262
+ (local.set $p2lo (i64.or
263
+ (i64.and (local.get $t00) (i64.const 0xFFFFFFFF))
264
+ (i64.shl (i64.and (local.get $mid) (i64.const 0xFFFFFFFF)) (i64.const 32))))
265
+ ;; Combine: prodHi:prodLo = p1hi:(p1lo + p2hi) with carry propagation
266
+ (local.set $carry (i64.add (local.get $p1lo) (local.get $p2hi)))
267
+ ;; Check if carry propagated (carry < p1lo → overflow into prodHi)
268
+ (local.set $prodHi (i64.add (local.get $p1hi)
269
+ (i64.extend_i32_u (i64.lt_u (local.get $carry) (local.get $p1lo)))))
270
+ (local.set $prodLo (local.get $carry))
271
+ ;; ─── Determine lz (leading-zero flag): 1 if MSB of prodHi is 0 ──────────────
272
+ (local.set $lz (i32.wrap_i64 (i64.xor (i64.shr_u (local.get $prodHi) (i64.const 63)) (i64.const 1))))
273
+ ;; ─── Compute biased exponent ─────────────────────────────────────────────────
274
+ ;; floor(exp10 * log2(10)) via fixed-point: (exp10 * 14267572527) >> 32
275
+ ;; 14267572527 = floor(log2(10) * 2^32) — correct for all exp10 in -342..308.
276
+ (local.set $log2floor (i32.wrap_i64 (i64.shr_s
277
+ (i64.mul (i64.extend_i32_s (local.get $exp10)) (i64.const 14267572527))
278
+ (i64.const 32))))
279
+ ;; exp2 = mBits + floor(exp10 * log2(10)) - lz
280
+ (local.set $exp2 (i32.sub (i32.add (local.get $mBits) (local.get $log2floor)) (local.get $lz)))
281
+ (local.set $biased (i32.add (local.get $exp2) (i32.const 1023)))
282
+ ;; Overflow → Infinity
283
+ (if (i32.ge_s (local.get $biased) (i32.const 2047)) (then (return (f64.const inf))))
284
+ ;; ─── Subnormal path (biased ≤ 0) ────────────────────────────────────────────
285
+ (if (i32.le_s (local.get $biased) (i32.const 0))
286
+ (then
287
+ ;; total_shift = 11 - lz + (1 - biased) = 12 - lz - biased
288
+ (local.set $totalShift (i32.sub (i32.sub (i32.const 12) (local.get $lz)) (local.get $biased)))
289
+ ;; If totalShift >= 64: prodHi >> totalShift == 0 in BigInt, but here:
290
+ ;; For totalShift in [64..127]: snMant = (prodHi >> (totalShift-64)) >> 64... = 0
291
+ ;; We just use BigInt-style: clamp to 0 if >=64 (prodHi is 64-bit)
292
+ (if (i32.ge_u (local.get $totalShift) (i32.const 64))
293
+ (then
294
+ ;; All mantissa bits are 0; only rounding could give min subnormal.
295
+ ;; round bit: at position (totalShift-1) of prodHi → always 0 for totalShift>=65
296
+ ;; For totalShift==64: round bit = bit 63 of prodHi = MSB
297
+ (if (i32.eq (local.get $totalShift) (i32.const 64))
298
+ (then
299
+ (local.set $snRound (i64.shr_u (local.get $prodHi) (i64.const 63)))
300
+ (local.set $snSticky (i64.or (local.get $prodLo) (local.get $p2lo)))
301
+ ;; Return min-subnormal if snRound=1 AND snSticky!=0 (boolean AND, not bitwise)
302
+ (if (i32.and
303
+ (i32.wrap_i64 (local.get $snRound))
304
+ (i64.ne (local.get $snSticky) (i64.const 0)))
305
+ (then (return (f64.reinterpret_i64 (i64.const 1)))))
306
+ ))
307
+ (return (f64.const 0))))
308
+ ;; totalShift in [1..63]: extract directly from prodHi
309
+ (local.set $snMant (i64.and
310
+ (i64.shr_u (local.get $prodHi) (i64.extend_i32_u (local.get $totalShift)))
311
+ (i64.const 0x000FFFFFFFFFFFFF)))
312
+ (local.set $snRound
313
+ (i64.and (i64.shr_u (local.get $prodHi)
314
+ (i64.extend_i32_u (i32.sub (local.get $totalShift) (i32.const 1))))
315
+ (i64.const 1)))
316
+ (local.set $snSticky (i64.or
317
+ (i64.and (local.get $prodHi)
318
+ (i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (i32.sub (local.get $totalShift) (i32.const 1))))
319
+ (i64.const 1)))
320
+ (i64.or (local.get $prodLo) (local.get $p2lo))))
321
+ ;; Round: snMant++ if roundBit && (sticky > 0 || snMant is odd)
322
+ (if (i64.ne (i64.and (local.get $snRound)
323
+ (i64.or (i64.extend_i32_u (i64.ne (local.get $snSticky) (i64.const 0)))
324
+ (i64.and (local.get $snMant) (i64.const 1))))
325
+ (i64.const 0))
326
+ (then (local.set $snMant (i64.add (local.get $snMant) (i64.const 1)))))
327
+ ;; Overflow of subnormal mantissa → minimum normal (biased=1, mant=0)
328
+ (if (i64.ge_u (local.get $snMant) (i64.const 0x0010000000000000))
329
+ (then (return (f64.reinterpret_i64 (i64.const 0x0010000000000000)))))
330
+ (return (f64.reinterpret_i64 (local.get $snMant)))))
331
+ ;; ─── Normal path ─────────────────────────────────────────────────────────────
332
+ ;; roundShift = 10 - lz (bit position of round bit in prodHi)
333
+ (local.set $roundShift (i32.sub (i32.const 10) (local.get $lz)))
334
+ (local.set $roundBit (i64.and
335
+ (i64.shr_u (local.get $prodHi) (i64.extend_i32_u (local.get $roundShift)))
336
+ (i64.const 1)))
337
+ ;; sticky = bits below roundBit in prodHi, plus all of prodLo and p2lo
338
+ (local.set $sticky (i64.or
339
+ (i64.and (local.get $prodHi)
340
+ (i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (local.get $roundShift)))
341
+ (i64.const 1)))
342
+ (i64.or (local.get $prodLo) (local.get $p2lo))))
343
+ ;; mant52 = (prodHi >> (11 - lz)) & MASK52
344
+ (local.set $mant52 (i64.and
345
+ (i64.shr_u (local.get $prodHi) (i64.extend_i32_u (i32.sub (i32.const 11) (local.get $lz))))
346
+ (i64.const 0x000FFFFFFFFFFFFF)))
347
+ ;; Round: mant52++ if roundBit && (sticky > 0 || mant52 is odd)
348
+ (if (i64.ne (i64.and (local.get $roundBit)
349
+ (i64.or (i64.extend_i32_u (i64.ne (local.get $sticky) (i64.const 0)))
350
+ (i64.and (local.get $mant52) (i64.const 1))))
351
+ (i64.const 0))
352
+ (then (local.set $mant52 (i64.add (local.get $mant52) (i64.const 1)))))
353
+ ;; Mantissa overflow: carry into exponent
354
+ (if (i64.ge_u (local.get $mant52) (i64.const 0x0010000000000000))
355
+ (then
356
+ (local.set $mant52 (i64.const 0))
357
+ (local.set $biased (i32.add (local.get $biased) (i32.const 1)))))
358
+ ;; Final overflow check after rounding
359
+ (if (i32.ge_s (local.get $biased) (i32.const 2047)) (then (return (f64.const inf))))
360
+ ;; Assemble IEEE 754 bits: biased_exp << 52 | mant52
361
+ (f64.reinterpret_i64
362
+ (i64.or
363
+ (i64.shl (i64.extend_i32_u (local.get $biased)) (i64.const 52))
364
+ (local.get $mant52))))`
365
+
154
366
  export default (ctx) => {
155
367
  deps({
156
368
  __mkstr: ['__alloc'],
@@ -159,11 +371,11 @@ export default (ctx) => {
159
371
  __toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
160
372
  __radix_str: ['__mkstr'],
161
373
  __num_radix: ['__ftoa', '__mkstr'],
162
- __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str', '__skipws', '__ptr_aux'],
374
+ __to_num: ['__char_at', '__str_byteLen', '__pow10', '__dec_to_f64', '__to_str', '__skipws', '__ptr_aux'],
163
375
  __skipws: ['__char_at', '__strws'],
164
376
  __to_bigint: ['__char_at', '__str_byteLen', '__num_to_bigint'],
165
377
  __parseInt: ['__char_at', '__str_byteLen'],
166
- __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
378
+ __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__dec_to_f64', '__to_str'],
167
379
  })
168
380
 
169
381
 
@@ -354,10 +566,11 @@ export default (ctx) => {
354
566
  (then
355
567
  (local.set $b (i32.load8_u (i32.add (local.get $buf) (local.get $i))))
356
568
  (br_if $heap (i32.ge_u (local.get $b) (i32.const 0x80)))
357
- (local.set $packed (i32.or (local.get $packed) (i32.shl (local.get $b) (i32.shl (local.get $i) (i32.const 3)))))
569
+ ;; 7-bit ASCII SSO: char i at bit i*7 (chars fit the offset for len ≤4); len at aux bits 10-12.
570
+ (local.set $packed (i32.or (local.get $packed) (i32.shl (local.get $b) (i32.mul (local.get $i) (i32.const 7)))))
358
571
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
359
572
  (br $pk))))
360
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $packed))))))
573
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $len) (i32.const 10))) (local.get $packed))))))
361
574
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
362
575
  (i32.store (local.get $off) (local.get $len))
363
576
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -644,8 +857,31 @@ export default (ctx) => {
644
857
  ctx.runtime.staticDataLen = staticStr.length
645
858
  ctx.runtime.data = (ctx.runtime.data || '') + staticStr
646
859
 
860
+ // Eisel-Lemire power-of-10 table: 131 entries × 16 bytes = 2096 bytes,
861
+ // staticStr.length = 57, pad 7 → table starts at byte 64 (always constant).
862
+ // The table bytes are stashed in ctx.runtime.elTable. src/compile/index.js appends
863
+ // them to ctx.runtime.data (padded to 8-byte boundary) only when $__dec_to_f64 is
864
+ // actually pulled in, then declares global $__el_tbl = that offset. This keeps the
865
+ // 2096-byte data segment out of modules that don't do decimal→f64 conversion.
866
+ // TRIMMED to exp10 in [-65..65] (see __dec_to_f64 range check). Each entry: 8
867
+ // bytes tblLo (LE) + 8 bytes tblHi (LE), for exp10 = -65 + i.
868
+ const EL_TABLE_HEX = 'e9b4c29f1247e998eaba94ea52bbcc862462b347d798233fa5e939a527ea7fa8ad3aa0190d7fec8e0e64888eb1e49fd2ac24043068cf5319893e15f9eeeea383d72d053c42c3a85f2b8e5ab7aaea8ca44d7906cb12f49237b63131655525b0cdd00be4be8bd8bbe211bf3e5f55178e80c40e9daeaece6a5bd66e0eb72a9db1a07552445a5a8245f28b0ad2647504dec81267d5f0f0e2d6ee2e8d06be928515fb6b608596d64d46553d18c4b67b73ed9c86b8263c4ce197aa4c1e75a45ad028c4a866304b9fd93dd5df65924d710433f52940fe8e03a846e5ab7f7bd0c6e23f9933d0bd72045298de965f9a8478db8fbf40446d8f85663e967cf7c0a556d273efa84aa4791300e7ddad9a98277663a895525d0d5818c0605559c17eb1537c12bba6b4106e1ef0b8aaaf71de9d681bd7e9e870ca041396b3ca0d07ab6221712692220dfdc5977b603dd1c855bb690db0b66a507cb77d9ab88c053b2b2ac4105ce442b2ad928e60f377e3045b9a7a8ab98ed31e5937b238f0551cc6f14019ed67b288662fc5de466c6ba3372e915fe801df15a03d3b4bac2323c6e2bcba3b31618b1a080d0a5e97ecab771b6ca98a7d39ae214a908c35bde7965522c753eddcc7d9542eda7741d6507e75755c5414ea1c88e9b9d0d5d10be5ddd2927369992424aa64e8444bc64e5e958777d0c3bf2dadd43e110bef3bf15abdb44a62da973cec848ed5cdea8aadb1ec61ddfad0bd4b27a6f24a81a5ed18de67ba943945ad1eb1cfd7ce708794cfea80f4fc434b2cb3ce818d024da9798325a131fc145ef75f42a23043a01358e46e093e3b9a35f5f7d2cafc5388186e9dca8b0dca0083f2b587fd7d3455cf64a25e77487ee091b7d1749e9d812a03fe4a3695da9d5876250612c60422f583bddd833a51c5eed3ae8796f742357972966a92c4523b7544cd14be9a9382170f3c05b775278a9295009a6dc13863dd128bc62453b12cf7ba8000c9f1035ecaeb16fcf6d3ee7bda7450a01d9784f5bca61cbbf488ea1a11926408e5bce5326cd0e3e9312ba56195b67d4a1eeccf9f43622e32ff3a075d1d928eee9293c287d4fab9febe0949b4a43632aa77b8b3a9897968be2e4c5be14dc4be9495e6100af64b01379d0fd9acb03af77c1d90948cf39ec18484530fd85c0935dc24b4b96fb006f2a56528130eb44b42132ee1d3452e44b7873ff9cb88506f09ccbc8c48d73915a5698ff7feaa24cb0bffebaf1b4d885a0e4473b5bed5edbdcefee6db303095f8880a683197a5b436415f70893d7cba362b0dc2fdfcce61841177ccab4c1b69047690323dbc427ae5d594bfd60fb1c1c2499a3fa6b5696caf05bd3786531d7233dc80cf0f2384471b47acc5a7a8a44e401361c3d32b6519e25817b7d1e9263108ac1c5a643bdf4f8d976e1283a3703d0ad7a3703d0ad7a3703d0ad7a3cccccccccccccccccccccccccccccccc00000000000000000000000000000080000000000000000000000000000000a0000000000000000000000000000000c8000000000000000000000000000000fa0000000000000000000000000000409c000000000000000000000000000050c3000000000000000000000000000024f4000000000000000000000000008096980000000000000000000000000020bcbe00000000000000000000000000286bee00000000000000000000000000f9029500000000000000000000000040b743ba00000000000000000000000010a5d4e80000000000000000000000002ae78491000000000000000000000080f420e6b50000000000000000000000a031a95fe3000000000000000000000004bfc91b8e0000000000000000000000c52ebca2b10000000000000000000040763a6b0bde00000000000000000000e8890423c78a0000000000000000000062acc5eb78ad000000000000000000807a17b726d7d800000000000000000090ac6e32788687000000000000000000b4570a3f1668a9000000000000000000a1edccce1bc2d30000000000000000a0841440615159840000000000000000c8a51990b9a56fa500000000000000003a0f20f4278fcbce0000000000000040840994f878393f810000000000000050e50bb936d7078fa100000000000000a4de4e6704cdc9f2c9000000000000004d96228145407c6ffc00000000000020f09db5702ba8adc59d000000000000286c05e34c36121937c500000000000032c7c61be0c356df84f60000000000407f3c5c116c3a960b139a0000000000109f4bb31507c97bce97c00000000000d4861e20db48bb1ac2bdf00000000080441413f4880db55099769600000000a055d91731eb50e2a43f14bc0000000008abcf5dfd25e51a8e4f19eb00000000e5caa15abe37cfd0b8d1ef92000000409e3d4af1ad05030527c6abb7000000d005cd9c6d19c743c6b0b796e5000000a2230082e46f5cea7bce327e8f0000808a2c80a2dd8bf3e41a82bf5db3000020ad37200bd56e309ea1622f35e0000034cc22f4264545de02a59d3d218c0000417f2bb17096d695430e058d29af0040115f76dd0c3c4c7bd45146f0f3da00c86afb690a88a50fcd24f32b76d888007a457a040dea8e5300eeefb6930eab80d8d6984590a4726880e9aba438d2d55047867f2bdaa64741f071eb6663a38524d9675fb6909099516c4ea6403c0ca76dcf41f7e3b4f4ff6507e2cf504bcfd0a421897a0ef1f8bf9f44ed81128f81820d6a2b19522df7afc7956822d7f221a39044769fa6f8f49b39bb02eb8c6feacbb4d55347d036f202086ac325700be5fe9065942c4262d70145229a1726274f9ff57eb9b7d23a4d42d6aa809deff022c7b2dea7658789e0d28bd5e0842badebf82feb889ff455cc6377850c333b4c939bfb256bc7716bbf3cd5a6cfff491f78c27aef45394e46ef8b8a90c37f1c2716f3'
869
+ // Pre-decode the EL table bytes once and stash in ctx.runtime.
870
+ // src/compile/index.js appends elTable to ctx.runtime.data only when
871
+ // __dec_to_f64 is actually pulled in via deps (lazy, keeps small modules clean).
872
+ let elTableBytes = ''
873
+ for (let i = 0; i < EL_TABLE_HEX.length; i += 2)
874
+ elTableBytes += String.fromCharCode(parseInt(EL_TABLE_HEX.slice(i, i + 2), 16))
875
+ ctx.runtime.elTable = elTableBytes
876
+
877
+ // Register the stdlib function (no data appended here — see compile/index.js hook)
878
+ ctx.core.stdlib['__dec_to_f64'] = DEC_TO_F64_WAT
879
+
647
880
  // === Number constants ===
648
881
 
882
+ // Each folds to inline (f64.const …), no stdlib dep. Written out (not a table
883
+ // loop) to stay within the self-host subset. `NaN` uses the `nan` token (not raw
884
+ // NaN) so it survives self-host IR marshalling — see emitNum.
649
885
  ctx.core.emit['Number.MAX_SAFE_INTEGER'] = () => typed(['f64.const', 9007199254740991], 'f64')
650
886
  ctx.core.emit['Number.MIN_SAFE_INTEGER'] = () => typed(['f64.const', -9007199254740991], 'f64')
651
887
  ctx.core.emit['Number.EPSILON'] = () => typed(['f64.const', 2.220446049250313e-16], 'f64')
@@ -653,7 +889,7 @@ export default (ctx) => {
653
889
  ctx.core.emit['Number.MIN_VALUE'] = () => typed(['f64.const', 5e-324], 'f64')
654
890
  ctx.core.emit['Number.POSITIVE_INFINITY'] = () => typed(['f64.const', Infinity], 'f64')
655
891
  ctx.core.emit['Number.NEGATIVE_INFINITY'] = () => typed(['f64.const', -Infinity], 'f64')
656
- ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64') // `nan` token, not raw NaN (survives self-host IR marshalling — see emitNum)
892
+ ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64')
657
893
 
658
894
  // === Number static methods ===
659
895
 
@@ -950,9 +1186,6 @@ export default (ctx) => {
950
1186
  ;; Decimal significand. Keep 18 significant decimal digits, track the
951
1187
  ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
952
1188
  ${DEC_SIGNIFICAND}
953
- ;; No digits — the literal was a bare sign or stray text ("abc", "+") → NaN.
954
- ;; (Empty / all-whitespace strings already returned +0 above.)
955
- ${FINISH_SIGNIFICAND}
956
1189
  ;; Scientific notation. 'e'/'E' commits to an ExponentPart — at least one
957
1190
  ;; digit must follow ("1e", "5e+" are NaN).
958
1191
  ${sciExponent(`(if (i32.eqz (local.get $expDigits)) (then (return (f64.const nan))))
@@ -962,8 +1195,9 @@ export default (ctx) => {
962
1195
  ;; Reject trailing non-whitespace ("5px", numeric separators "1_0", …).
963
1196
  (local.set $i (call $__skipws (local.get $v) (local.get $i) (local.get $len)))
964
1197
  (if (i32.lt_s (local.get $i) (local.get $len)) (then (return (f64.const nan))))
965
- ${POW10_SCALE}
966
- (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
1198
+ ;; Eisel-Lemire exact rounding; fallback to __pow10 for ambiguous cases.
1199
+ ${EL_SCALE}
1200
+ (local.get $result))`
967
1201
 
968
1202
  // NumberToBigInt: a RangeError unless n is an integral Number — finite and
969
1203
  // equal to its own truncation. NaN fails the f64.eq integrality test;
@@ -1089,15 +1323,15 @@ export default (ctx) => {
1089
1323
  ;; Decimal significand. Keep 18 significant decimal digits, track the
1090
1324
  ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
1091
1325
  ${DEC_SIGNIFICAND}
1092
- ${FINISH_SIGNIFICAND}
1093
1326
  ;; Scientific notation.
1094
1327
  ${sciExponent(`(if (local.get $expDigits)
1095
1328
  (then
1096
1329
  (if (local.get $expNeg)
1097
1330
  (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
1098
1331
  (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))`)}
1099
- ${POW10_SCALE}
1100
- (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
1332
+ ;; Eisel-Lemire exact rounding; fallback to __pow10 for ambiguous cases.
1333
+ ${EL_SCALE}
1334
+ (local.get $result))`
1101
1335
 
1102
1336
  // ToString(arg) for the string-input builtins. A statically-known boolean must
1103
1337
  // render as "true"/"false" (spec step 1: ToString) before parsing — otherwise
package/module/object.js CHANGED
@@ -790,11 +790,17 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
790
790
  else allKnown = false
791
791
  } else if (Array.isArray(p) && p[0] === ':') addName(p[1])
792
792
  }
793
- // Single unknown spread `{ ...opts }` → alias the source directly. Safe for
794
- // read-only use; mutation would affect the source (a true clone takes the
795
- // dynamic-merge path below).
796
- if (!allKnown && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...')
797
- return typed(asF64(emit(props[0][1])), 'f64')
793
+ // Single unknown spread `{ ...src }` → shallow-clone src at runtime, preserving
794
+ // its type (OBJECT→OBJECT, HASH→HASH). Aliasing src (the old shortcut) leaked
795
+ // every later write to the result back into the source — a real correctness bug
796
+ // (jz's own narrow.js had to hand-route around it). __obj_clone keys off the
797
+ // box's runtime schemaId, so it copies static-segment sources too; the schema
798
+ // table it reads must exist, so declare + force it (assemble.js).
799
+ if (!allKnown && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...') {
800
+ inc('__obj_clone')
801
+ if (!ctx.scope.globals.has('__schema_tbl')) declGlobal('__schema_tbl', 'i32')
802
+ return typed(['call', '$__obj_clone', asF64(emit(props[0][1]))], 'f64')
803
+ }
798
804
  if (!allKnown) return emitDynamicSpread(props)
799
805
 
800
806
  const schemaId = ctx.schema.register(allNames)
package/module/regex.js CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { typed, asF64, asI64, UNDEF_NAN, NULL_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
11
  import { emit, deps } from '../src/bridge.js'
12
- import { ctx, err, inc, PTR, LAYOUT, getter, declGlobal } from '../src/ctx.js'
12
+ import { ctx, err, inc, PTR, LAYOUT, registerGetter, declGlobal } from '../src/ctx.js'
13
13
  import { valTypeOf } from '../src/kind.js'
14
14
  import { VAL } from '../src/reps.js'
15
15
 
@@ -746,13 +746,14 @@ export default (ctx) => {
746
746
  (if (i32.eqz (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT})))
747
747
  (then (return (call $__ptr_offset (local.get $ptr)))))
748
748
  (local.set $off (call $__ptr_offset (local.get $ptr)))
749
- (local.set $len (i32.and (local.get $aux) (i32.const 7)))
749
+ (local.set $len (i32.and (i32.shr_u (local.get $aux) (i32.const 10)) (i32.const 7)))
750
750
  (local.set $buf (call $__alloc (local.get $len)))
751
751
  (local.set $i (i32.const 0))
752
752
  (block $done (loop $next
753
753
  (br_if $done (i32.ge_u (local.get $i) (local.get $len)))
754
+ ;; 7-bit ASCII SSO: char i at payload bit i*7 (read from the full i64 ptr; chars 4-5 span aux).
754
755
  (i32.store8 (i32.add (local.get $buf) (local.get $i))
755
- (i32.and (i32.shr_u (local.get $off) (i32.mul (local.get $i) (i32.const 8))) (i32.const 0xFF)))
756
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7))) (i64.const 0x7f))))
756
757
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
757
758
  (br $next)))
758
759
  (local.get $buf))`
@@ -911,14 +912,14 @@ export default (ctx) => {
911
912
  // RegExp.prototype.source — the pattern text. A literal stores it verbatim
912
913
  // (already grammar-escaped), so `/A/.source` is the 6-char "A".
913
914
  // An empty pattern serializes to "(?:)" so the result re-parses to a regex.
914
- ctx.core.emit['.regex:source'] = getter((obj) => {
915
+ registerGetter('.regex:source', (obj) => {
915
916
  const a = regexAstOf(obj)
916
917
  return emit(['str', (a && a[1]) || '(?:)'])
917
918
  })
918
919
 
919
920
  // RegExp.prototype.flags — flag characters in canonical order (sec-get-regexp.prototype.flags).
920
921
  const FLAG_ORDER = 'dgimsvy'
921
- ctx.core.emit['.regex:flags'] = getter((obj) => {
922
+ registerGetter('.regex:flags', (obj) => {
922
923
  const f = flagsOf(obj)
923
924
  return emit(['str', [...FLAG_ORDER].filter(c => f.includes(c)).join('')])
924
925
  })
@@ -927,10 +928,10 @@ export default (ctx) => {
927
928
  for (const [prop, ch] of [
928
929
  ['global', 'g'], ['ignoreCase', 'i'], ['multiline', 'm'], ['dotAll', 's'],
929
930
  ['unicode', 'u'], ['sticky', 'y'], ['hasIndices', 'd'], ['unicodeSets', 'v'],
930
- ]) ctx.core.emit[`.regex:${prop}`] = getter((obj) => typed(['f64.const', flagsOf(obj).includes(ch) ? 1 : 0], 'f64'))
931
+ ]) registerGetter(`.regex:${prop}`, (obj) => typed(['f64.const', flagsOf(obj).includes(ch) ? 1 : 0], 'f64'))
931
932
 
932
933
  // lastIndex — for /g and /y regexes, reads the mutable global; others always 0.
933
- ctx.core.emit['.regex:lastIndex'] = getter((obj) => {
934
+ registerGetter('.regex:lastIndex', (obj) => {
934
935
  const id = resolveRegex(obj)
935
936
  if (id != null) {
936
937
  const flags = flagsOf(obj)