jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -12,8 +12,8 @@
12
12
  * @module assemble
13
13
  */
14
14
 
15
- import { parse as parseWat } from 'watr'
16
- import { ctx, inc, resolveIncludes, err, PTR, LAYOUT } from './ctx.js'
15
+ import parseWat from 'watr/parse'
16
+ import { ctx, inc, resolveIncludes, err, PTR, LAYOUT, HEAP, declGlobal } from '../ctx.js'
17
17
 
18
18
  // Stdlib WAT templates are fixed text (or feature-keyed text from a factory) —
19
19
  // `parseWat` of the same string always yields the same tree. Parsing is the
@@ -33,10 +33,12 @@ const parseTemplate = (str) => {
33
33
  if (tmpl === undefined) stdlibParseCache.set(str, tmpl = parseWat(str))
34
34
  return cloneTemplate(tmpl)
35
35
  }
36
- import { T, VAL, analyzeValTypes } from './analyze.js'
37
- import { optimizeFunc, collectVolatileGlobals, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
38
- import { emit } from './emit.js'
39
- import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from './ir.js'
36
+ import { T } from '../ast.js'
37
+ import { analyzeValTypes, analyzeBody } from '../compile/analyze.js'
38
+ import { VAL } from '../reps.js'
39
+ import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from '../optimize/index.js'
40
+ import { emit } from '../compile/emit.js'
41
+ import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from '../ir.js'
40
42
 
41
43
  // NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
42
44
  const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
@@ -46,16 +48,16 @@ const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
46
48
  const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
47
49
  const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
48
50
 
49
- // memory[1020] holds the heap pointer only for shared memory (wasm globals are
51
+ // memory[HEAP.PTR_ADDR] holds the heap pointer only for shared memory (wasm globals are
50
52
  // per-instance — see module/core.js comment). Non-shared memory uses $__heap.
51
53
  const heapUsesMem = () => ctx.memory.shared
52
54
 
53
55
  const heapGetIR = () => heapUsesMem()
54
- ? ['i32.load', ['i32.const', 1020]]
56
+ ? ['i32.load', ['i32.const', HEAP.PTR_ADDR]]
55
57
  : ['global.get', '$__heap']
56
58
 
57
59
  const heapSetIR = value => heapUsesMem()
58
- ? ['i32.store', ['i32.const', 1020], value]
60
+ ? ['i32.store', ['i32.const', HEAP.PTR_ADDR], value]
59
61
  : ['global.set', '$__heap', value]
60
62
 
61
63
  const ARENA_SAFE_CALLS = new Set([
@@ -134,8 +136,20 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
134
136
  ctx.func.locals = new Map()
135
137
  ctx.func.localReps = null
136
138
  ctx.func.boxed = new Map()
139
+ ctx.func.cellTypes = new Set()
137
140
  ctx.func.stack = []
138
141
  ctx.func.current = { params: [], results: [] }
142
+ // Reserve prepare-generated temp names (for-of `arrVar`/`idx`/`len`,
143
+ // destructure scratch, …) in the __start frame so emit's temp()/tempI32()
144
+ // skip them — the same pre-seed analyzeFuncForEmit gives every function frame.
145
+ // Only T-sentinel names: they're always __start locals (user module-scope
146
+ // bindings become globals and can't contain T). Without this, prepare's
147
+ // `${T}arr${n}` collides with an emit-time tempI32('arr') at the same uniq,
148
+ // declaring the array pointer's local i32 and corrupting it via convert_i32_s.
149
+ const seedGeneratedLocals = (body) => {
150
+ for (const [n, t] of analyzeBody(body).locals)
151
+ if (n.includes(T) && !ctx.func.locals.has(n)) ctx.func.locals.set(n, t)
152
+ }
139
153
  analyzeValTypes(ast)
140
154
  const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
141
155
 
@@ -143,9 +157,11 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
143
157
  if (ctx.module.moduleInits) {
144
158
  for (const mi of ctx.module.moduleInits) {
145
159
  analyzeValTypes(mi)
160
+ seedGeneratedLocals(mi)
146
161
  moduleInits.push(...normalizeIR(emit(mi)))
147
162
  }
148
163
  }
164
+ seedGeneratedLocals(ast)
149
165
  const init = emit(ast)
150
166
 
151
167
  // Module-scope object literals can create closure bodies while `emit(ast)`
@@ -342,8 +358,12 @@ export function finalizeClosureTable(sec) {
342
358
  }
343
359
  for (const fn of sec.funcs) { scan(fn); if (indirectUsed) break }
344
360
  if (!indirectUsed) for (const fn of sec.start) scan(fn)
345
- if (!indirectUsed) for (const s of Object.keys(ctx.core.stdlib)) {
346
- if (ctx.core.stdlib[s]?.includes?.('call_indirect')) { indirectUsed = true; break }
361
+ // stdlib values are mixed: WAT-template strings + lazy generator functions.
362
+ // Only the string templates can carry a literal `call_indirect`; a typeof
363
+ // guard skips the generators (where `.includes` is meaningless — and on a jz
364
+ // closure receiver would read the closure pointer as a string, out of bounds).
365
+ if (!indirectUsed) for (const tpl of Object.values(ctx.core.stdlib)) {
366
+ if (typeof tpl === 'string' && tpl.includes('call_indirect')) { indirectUsed = true; break }
347
367
  }
348
368
  if (indirectUsed) {
349
369
  if (!ctx.closure.table) ctx.closure.table = []
@@ -409,7 +429,11 @@ export function pullStdlib(sec) {
409
429
  const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
410
430
  if (!needsMemory) ctx.scope.globals.delete('__heap')
411
431
  if (needsMemory && ctx.module.modules.core) {
412
- for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
432
+ for (const fn of ['__alloc', '__alloc_hdr', '__clear']) ctx.core.includes.add(fn)
433
+ // Late-add of allocators may pull in transitive deps (__alloc → __memgrow,
434
+ // etc.) that the initial resolveIncludes did not yet see; re-resolve.
435
+ // No-op when the alloc trio was already present.
436
+ resolveIncludes()
413
437
  const pages = ctx.memory.pages || 1
414
438
  if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
415
439
  else sec.memory.push(['memory', ['export', '"memory"'], pages])
@@ -443,31 +467,34 @@ export function syncImports(sec) {
443
467
  /**
444
468
  * Phase: whole-module + per-function optimization passes.
445
469
  */
446
- export function optimizeModule(sec) {
470
+ export function optimizeModule(sec, profiler) {
471
+ const t = profiler?.time ? (name, fn) => profiler.time(`optMod:${name}`, fn) : (_, fn) => fn()
447
472
  const cfg = ctx.transform.optimize
448
- if (!cfg || cfg.specializeMkptr !== false)
449
- specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
450
- if (!cfg || cfg.specializePtrBase !== false)
451
- specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
452
- if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) {
473
+ if (!cfg || cfg.specializeMkptr !== false) t('specializeMkptr', () =>
474
+ specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat))
475
+ if (!cfg || cfg.specializePtrBase !== false) t('specializePtrBase', () =>
476
+ specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat))
477
+ if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) t('sortStrPool', () => {
453
478
  const poolRef = { pool: ctx.runtime.strPool }
454
479
  sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
455
480
  ctx.runtime.strPool = poolRef.pool
456
- }
457
- // Backfill globalTypes for runtime globals declared only in ctx.scope.globals
458
- // (e.g., __schema_tbl, __strBase). Parses the WAT string to infer i32/f64/i64.
459
- if (ctx.scope.globals) {
460
- for (const [name, wat] of ctx.scope.globals) {
461
- if (!wat || ctx.scope.globalTypes.has(name)) continue
462
- const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
463
- if (m) ctx.scope.globalTypes.set(name, m[1])
464
- }
465
- }
481
+ })
482
+ // (globalTypes backfill gone: declGlobal sets the type at declaration.)
466
483
  // Build global name→type map from ctx.scope.globalTypes (keys without $) for promoteGlobals
467
484
  const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
468
485
  const allFuncs = [...sec.funcs, ...sec.stdlib, ...sec.start]
469
- const volatileGlobals = collectVolatileGlobals(allFuncs)
470
- for (const s of allFuncs) optimizeFunc(s, cfg, globalTypesMap, volatileGlobals)
486
+ const volatileGlobals = t('volatileGlobals', () => collectVolatileGlobals(allFuncs))
487
+ const reachableWrites = t('reachableWrites', () => collectReachableGlobalWrites(allFuncs))
488
+ // Offset-hoist BEFORE promoteGlobals (inside optimizeFunc): value-promoting a
489
+ // stable-pointee global to a $_pg local would destroy the global.get pattern
490
+ // this pass matches, reverting rfft/diffusion to per-iteration resolves. After
491
+ // the hoist, the surviving global.get count is 1 (the entry snap) — naturally
492
+ // below promoteGlobals' threshold, so the two passes compose either way.
493
+ if (!cfg || cfg.hoistGlobalPtrOffset !== false) t('hoistGlobalPtr', () => {
494
+ const stable = stablePtrGlobalNames()
495
+ if (stable.size) for (const s of allFuncs) hoistGlobalPtrOffset(s, stable, reachableWrites)
496
+ })
497
+ t('optimizeFuncs', () => { for (const s of allFuncs) optimizeFunc(s, cfg, globalTypesMap, volatileGlobals, 'pre', reachableWrites) })
471
498
  if (!cfg || cfg.arenaRewind !== false) {
472
499
  const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
473
500
  const fnByName = new Map()
@@ -481,11 +508,7 @@ export function optimizeModule(sec) {
481
508
  }
482
509
  }
483
510
  if (!cfg || cfg.hoistConstantPool !== false)
484
- hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => {
485
- ctx.scope.globals.set(name, wat)
486
- const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
487
- if (m) ctx.scope.globalTypes.set(name, m[1])
488
- })
511
+ hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, lit) => declGlobal(name, 'f64', lit))
489
512
 
490
513
  // Second promoteGlobals pass disabled: promoting hoistConstantPool's __fc*
491
514
  // globals regressed the watr perf micro-pin (WASM compile time increased).
@@ -502,12 +525,8 @@ export function optimizeModule(sec) {
502
525
  const heapBase = (dataLen + 7) & ~7
503
526
  // Non-shared memory always carries a $__heap global — start it past the
504
527
  // static data so the bump allocator never overwrites a literal.
505
- ctx.scope.globals.set('__heap', `(global $__heap (export "__heap") (mut i32) (i32.const ${heapBase}))`)
506
- ctx.scope.globalTypes.set('__heap', 'i32')
507
- if (ctx.scope.globals.has('__heap_start')) {
508
- ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
509
- ctx.scope.globalTypes.set('__heap_start', 'i32')
510
- }
528
+ declGlobal('__heap', 'i32', heapBase, { export: '__heap' })
529
+ if (ctx.scope.globals.has('__heap_start')) declGlobal('__heap_start', 'i32', heapBase)
511
530
  for (const s of sec.stdlib)
512
531
  if (s[0] === 'func' && s[1] === '$__clear')
513
532
  for (let i = 2; i < s.length; i++)
@@ -528,18 +547,33 @@ export function stripStaticDataPrefix(sec) {
528
547
  for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i)
529
548
  const dv = new DataView(buf.buffer)
530
549
  if (ctx.runtime.staticPtrSlots) {
550
+ // u32-half reads/writes — DataView's BigInt accessors are unfaithful in the
551
+ // self-host kernel; the offset lives entirely in the LE low word and the
552
+ // tag/aux fields entirely in the high word, so plain number math suffices.
531
553
  for (const slotOff of ctx.runtime.staticPtrSlots) {
532
554
  if (slotOff < prefix) continue
533
- const bits = dv.getBigUint64(slotOff, true)
534
- if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
535
- const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
555
+ const hi = dv.getUint32(slotOff + 4, true)
556
+ if (((hi >>> 16) & 0xFFF8) !== LAYOUT.NAN_PREFIX) continue
557
+ const ty = (hi >>> 15) & 15
536
558
  if (!SHIFTABLE.has(ty)) continue
537
- if (ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG)) continue
538
- const off = Number(bits & OFFSET_MASK_BIG)
559
+ if (ty === PTR.STRING && ((hi >>> (LAYOUT.AUX_SHIFT - 32)) & LAYOUT.SSO_BIT)) continue
560
+ const off = dv.getUint32(slotOff, true)
539
561
  if (off < prefix) continue
540
- const hi = bits & ~OFFSET_MASK_BIG
541
- dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
562
+ dv.setUint32(slotOff, off - prefix, true)
563
+ }
564
+ }
565
+ // The intern index (buildInternTable) stores raw static-string ptrs as u32
566
+ // slots — shift each occupied slot like every other static reference, and
567
+ // re-declare the (already-declared) base global at its post-strip position.
568
+ if (ctx.runtime.internTable) {
569
+ const { base, size } = ctx.runtime.internTable
570
+ for (let i = 0; i < size; i++) {
571
+ const slot = base + i * 8 + 4
572
+ const off = dv.getUint32(slot, true)
573
+ if (off >= prefix) dv.setUint32(slot, off - prefix, true)
542
574
  }
575
+ ctx.runtime.internTable.base = base - prefix
576
+ declGlobal('__internBase', 'i32', base - prefix, { mut: false })
543
577
  }
544
578
  let s = ''
545
579
  for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
@@ -559,6 +593,10 @@ export function stripStaticDataPrefix(sec) {
559
593
  Array.isArray(child[3]) && child[3][0] === 'i32.const' &&
560
594
  typeof child[3][1] === 'number' && (child[3][1] & LAYOUT.SSO_BIT)
561
595
  if (!isSsoString) child[4][1] -= prefix
596
+ } else if (typeof child[0] === 'string' && child[0].endsWith('.store') &&
597
+ Array.isArray(child[1]) && child[1][0] === 'i32.const' &&
598
+ typeof child[1][1] === 'number' && child[1][1] >= prefix) {
599
+ child[1][1] -= prefix
562
600
  } else if (child[0] === 'f64.const' &&
563
601
  typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
564
602
  const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n