-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathindex.js
More file actions
922 lines (878 loc) · 48.9 KB
/
Copy pathindex.js
File metadata and controls
922 lines (878 loc) · 48.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
/**
* jz - JS subset → WASM compiler.
*
* # Pipeline stages + contracts
*
* source (string)
* ↓ parse (subscript/jessie) — lexing + expression-oriented AST
* raw AST: nested arrays `[op, ...args]`, no ctx mutation
* ↓ jzify (default-on; skipped under opts.strict) — lower full-JS subset (var/function/class/switch) to jz-native
* desugared AST: arrow functions + let/const/if only
* ↓ prepare — validate (reject disallowed ops), normalize (++/--→+=/-=, scope rename),
* extract (functions→ctx.func.list with sig), resolve (imports→ctx.module.imports),
* track (object-literal schemas via ctx.schema.register)
* prepared AST: normalized, with `ctx.func.list` / `ctx.module.imports` / `ctx.schema.list`
* populated. Arrow bodies carry no type info yet.
* ↓ compile — drives per-function emit, interleaves analysis (locals/valTypes/captures/
* narrowing fixpoint) with IR generation via the emitter table (src/compile/emit.js).
* Writes: `ctx.func.valTypes`/`.locals`, `ctx.types.*`, `ctx.runtime.*`, `ctx.core.includes`.
* The emit phase (src/wat/assemble.js optimizeModule) then runs jz's ONLY optimizer pass —
* optimizeFunc (src/optimize/index.js): `hoistPtrType` + fused peephole/inline/memarg walk +
* auto-vectorization. All lowering, incl. SIMD, happens here — BEFORE watr.
* WAT IR: watr S-expression `['module', ...sections]`, every instruction node carries `.type`.
* ↓ watOptimize (opt-out via opts.optimize=false) — the SOLE, FINAL optimizer: CSE, DCE, const
* fold, inline, coalesce. Runs ONCE, as a fixpoint. No jz pass touches WAT after it (bar the
* stable-global-offset hoist, a phase-2 watr-migration candidate). See .work/research.md.
* ↓ watrPrint (opts.wat=true) → WAT text, or watrCompile → Uint8Array binary
*
* # State
* Single shared `ctx` (src/ctx.js). Reset at compile() entry via `reset(emitter, GLOBALS)`.
* Each subkey has a declared lifecycle + ownership — see ctx.js docstring for the table.
*
* # Extension
* Modules in module/ register operator handlers on ctx.core.emit and stdlibs on ctx.core.stdlib.
* Feature flags (ctx.features.*) gate conditional stdlib branches for dead-code elimination.
* Capability hooks (ctx.schema.register, ctx.closure.make) are installed by capability modules.
*
* Interop host layer (memory marshaling, wrap, instantiate) lives in
* interop.js — also exported as the standalone `jz/interop` subpath for
* hosts that want to run prebuilt jz wasm without pulling the compiler.
*
* @module jz
*/
import { parse } from './src/parse.js'
import watrCompile from "watr/compile";
import { snapshotInit } from "./src/snapshot.js";
import watrPrint from "watr/print";
import watOptimize from "watr/optimize";
import { ctx, reset, err, warn, initWarnings, assertCtxInvariants } from './src/ctx.js'
import prepare, { GLOBALS } from './src/prepare/index.js'
import { liftIIFEs } from './src/prepare/lift-iife.js'
import { preEval } from './src/prepare/pre-eval.js'
import compile from './src/compile/index.js'
import { resetProgramFactsCache } from './src/compile/program-facts.js'
import { emit, emitter, emitVoid as flat, emitBlockBody as body, emitBoolStr as bool, emitIndex as idx, buildArrayWithSpreads as spread } from './src/compile/emit.js'
import {
collectReachableGlobalWrites, collectReachableMemoryWrites,
hoistGlobalPtrOffset, hoistStableGlobalConstLoads, guardMaskedVectorSuffix, hasIROp, stablePtrGlobalNames,
resolveOptimize, SIMD_PINNED,
} from './src/optimize/index.js'
import { VAL } from './src/reps.js'
import jzify from './jzify/index.js'
import { T } from './src/ast.js'
import {
memory as enhanceMemory, instantiate as instantiateRuntime, toModule,
} from './interop.js'
// A host import that's a JS function may hand back any value, including a host
// object — which arrives in wasm as a PTR.EXTERNAL ref. Constants/typed specs can't.
const importsMayReturnExternal = (imports) =>
!!imports && Object.values(imports).some(mod =>
Object.values(mod || {}).some(spec => typeof spec === 'function'))
// WHATWG URL resolution for compile-time import.meta lowering. Injected into
// ctx.transform (like parse/jzify) so prepare never references the `URL` global
// directly — keeps the self-host kernel free of host-only built-ins.
const resolveUrl = (spec, base) => new URL(spec, base).href
// Serialize a JS value to a jz source literal (numbers/booleans/strings/null/
// undefined + literal arrays/objects). Returns null for anything not expressible
// as a compile-time literal (functions, host objects, circular). Shared by the
// `jz\`…${val}\`` template tag (hoists complex args) and opts.define.
const serialize = (v) => {
if (v === undefined) return 'undefined'
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
if (v === null) return 'null'
if (typeof v === 'string') return JSON.stringify(v)
if (Array.isArray(v)) {
const elems = v.map(serialize)
return elems.every(e => e !== null) ? `[${elems.join(', ')}]` : null
}
if (typeof v === 'object') {
const props = Object.keys(v).map(k => {
const s = serialize(v[k])
return s !== null ? `${k}: ${s}` : null
})
return props.every(p => p !== null) ? `{${props.join(', ')}}` : null
}
return null
}
// opts.define → a one-line `let K = V; …` prelude prepended to source. Kept on a
// single line (no trailing newline) so user line numbers past line 1 stay exact —
// same convention the template tag uses for hoisted literals.
const defineBindings = (define) => {
const parts = []
for (const [k, v] of Object.entries(define)) {
const s = serialize(v)
if (s === null) err(`opts.define['${k}'] is not a compile-time constant — use a number, boolean, string, null, or a literal array/object`)
parts.push(`let ${k} = ${s}`)
}
return parts.length ? parts.join('; ') + '; ' : ''
}
const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
const compileProfiler = (profile) => {
if (profile == null) return null
if (typeof profile !== 'object') throw new TypeError('opts.profile must be an object sink (populated with compile-phase entries/totals); for a wasm name section use opts.names')
profile.entries ||= []
profile.totals ||= {}
return {
time(name, fn) {
const start = nowMs()
try { return fn() }
finally {
const ms = nowMs() - start
profile.entries.push({ name, ms })
profile.totals[name] = (profile.totals[name] || 0) + ms
}
},
}
}
const uleb = (n) => {
const out = []
do {
let b = n & 0x7f
n >>>= 7
if (n) b |= 0x80
out.push(b)
} while (n)
return out
}
const utf8Bytes = (s) => [...new TextEncoder().encode(s)]
const nameBytes = (s) => {
const bytes = utf8Bytes(s)
return [...uleb(bytes.length), ...bytes]
}
const watName = (s) => typeof s === 'string' && s.startsWith('$') ? s.slice(1) : null
const quotedName = (s) => typeof s === 'string' && /^".*"$/.test(s) ? s.slice(1, -1) : null
const importFuncName = (node) => {
if (!Array.isArray(node) || node[0] !== 'import') return null
const desc = node[3]
if (!Array.isArray(desc) || desc[0] !== 'func') return null
return watName(desc[1]) || quotedName(node[2])
}
const functionNameSection = (module) => {
const entries = []
let funcIdx = 0
for (const node of module) {
if (!Array.isArray(node)) continue
if (node[0] === 'import') {
const name = importFuncName(node)
if (name != null) entries.push([funcIdx++, name])
} else if (node[0] === 'func') {
const name = watName(node[1])
if (name != null) entries.push([funcIdx, name])
funcIdx++
}
}
if (!entries.length) return null
const map = [...uleb(entries.length)]
for (const [idx, name] of entries) map.push(...uleb(idx), ...nameBytes(name))
const payload = [...nameBytes('name'), 1, ...uleb(map.length), ...map]
return Uint8Array.from([0, ...uleb(payload.length), ...payload])
}
const appendFunctionNames = (wasm, module) => {
const section = functionNameSection(module)
if (!section) return wasm
const out = new Uint8Array(wasm.length + section.length)
out.set(wasm)
out.set(section, wasm.length)
return out
}
/**
* jz — JS subset → WASM compiler.
*
* jz('code') or jz`code` → { exports, memory, instance, module }
* jz.compile('code') → Uint8Array (raw WASM binary)
* jz.compile('code', { wat: true }) → string (WAT text)
* jz.memory([src]) → enhanced WebAssembly.Memory (read/write JS↔WASM values)
*
* @example
* const { exports: { add } } = jz('export let add = (a, b) => a + b')
* add(2, 3) // 5
*/
jz.memory = enhanceMemory
/**
* jz.pool(source, opts) — SPMD worker pool over ONE shared memory (Workers v1).
*
* Compiles `source` with { sharedMemory: true }, instantiates it on the main
* thread AND in `threads` node worker_threads, all linked to the same
* WebAssembly.Memory({ shared: true }). Kernels coordinate through shared
* typed arrays + Atomics (module/atomics.js); strings/objects stay
* thread-local by the v1 contract.
*
* Worker instances link `env.memory` ONLY — a kernel needing other host
* imports fails instantiation loudly (pure-compute contract).
*
* const p = await jz.pool(src, { threads: 4, pages: 16, maxPages: 256 })
* p.exports.setup(...) // main-thread instance
* await p.run('tile') // every worker: tile(workerIndex, threads)
* await p.run('tile', a, b) // extra args broadcast to each worker
* p.memory // the shared, jz-enhanced memory
* await p.terminate()
*
* @param {string} source - jz source (compiled once; module shared with workers)
* @param {object} [opts] - { threads=4, pages=16, maxPages=16384, ...compile opts }
* @returns {Promise<{exports, memory, module, threads, run, terminate}>}
*/
jz.pool = async function pool(source, opts = {}) {
const { threads = 4, pages = 16, maxPages = 16384, ...rest } = opts
const memory = new WebAssembly.Memory({ initial: pages, maximum: maxPages, shared: true })
const main = jz(source, { ...rest, sharedMemory: true, memory })
// Computed specifier keeps bundlers (esbuild web dist) from resolving the
// node builtin statically; browsers get a clear v1 error instead.
const { Worker } = await import('node:' + 'worker_threads').catch(() => {
throw new Error('jz.pool v1 runs on node worker_threads; browser Worker support is a follow-up')
})
// The worker shim honors the jz:i64exp lane map (exact-bits ABI): an i64 lane
// takes a BigInt — scalars convert to their f64 bits, boxed pointers (BigInt
// args from jz.memory.* on the main thread) pass through; i64 results
// reinterpret back to numbers (v1 kernels return scalars).
const workerSrc = `
const { parentPort, workerData } = require('node:worker_threads')
const inst = new WebAssembly.Instance(workerData.module, { env: { memory: workerData.memory } })
const dv = new DataView(new ArrayBuffer(8))
const bits = (x) => (dv.setFloat64(0, x), dv.getBigUint64(0))
const unbits = (b) => (dv.setBigUint64(0, BigInt.asUintN(64, b)), dv.getFloat64(0))
const lanes = new Map()
for (const s of WebAssembly.Module.customSections(workerData.module, 'jz:i64exp'))
try { for (const e of JSON.parse(new TextDecoder().decode(s))) lanes.set(e.name, e) } catch {}
parentPort.on('message', (m) => {
try {
const sig = lanes.get(m.fn), p = new Set(sig?.p || [])
const args = m.args.map((x, i) => p.has(i) && typeof x === 'number' ? bits(x) : x)
let r = inst.exports[m.fn](...args)
if (sig?.r && typeof r === 'bigint') r = unbits(r)
parentPort.postMessage({ id: m.id, r })
} catch (e) { parentPort.postMessage({ id: m.id, e: String(e) }) }
})
parentPort.postMessage({ ready: true })`
const workers = Array.from({ length: threads }, () =>
new Worker(workerSrc, { eval: true, workerData: { module: main.module, memory } }))
await Promise.all(workers.map(w => new Promise((res, rej) => {
w.once('message', res); w.once('error', rej)
})))
let seq = 0
const call = (w, fn, args) => new Promise((res, rej) => {
const id = seq++
const on = (m) => { if (m.id !== id) return; w.off('message', on); m.e ? rej(new Error(m.e)) : res(m.r) }
w.on('message', on)
w.postMessage({ id, fn, args })
})
return {
exports: main.exports, memory: main.memory, module: main.module, threads,
run: (fn, ...args) => Promise.all(workers.map((w, i) => call(w, fn, [i, threads, ...args]))),
terminate: () => Promise.all(workers.map(w => w.terminate())),
}
}
/**
* Compile jz source to WASM binary or WAT text. Low-level — no instantiation.
* @param {string} code - jz source
* @param {object} [opts]
* @param {boolean} [opts.wat] - Return WAT text instead of binary
* @param {boolean} [opts.strict] - Enforce the pure canonical subset: skip jzify
* (so full-JS syntax like var/function/class is rejected, not lowered) and reject
* dynamic features (obj[k], for-in, unknown receiver method calls) at compile time.
* Avoids pulling dynamic-dispatch stdlib into output; large size win for static programs.
* @param {WebAssembly.Memory|number} [opts.memory] - Owned memory's initial page
* count (`memory: N`, 64 KiB/page), or a `WebAssembly.Memory` to share across modules.
* @param {number} [opts.maxMemory] - Maximum memory pages — emits a ceiling on the
* memory type so growth traps past it (sandbox cap). Must be ≥ the initial size.
* Default: unbounded.
* @param {boolean} [opts.sharedMemory] - Import `env.memory` as a SHARED memory (wasm
* threads): atomic heap bump, `shared` memtype (max defaults to the 4 GiB ceiling).
* Link with `new WebAssembly.Memory({ initial, maximum, shared: true })`.
* @param {boolean} [opts.importMemory] - Import `env.memory` instead of exporting an
* owned memory. For embedding into a host that provides the memory.
* @param {boolean} [opts.alloc=true] - Export raw allocator helpers
* (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
* modules that only call exported wasm functions.
* @param {Object<string,*>} [opts.define] - Compile-time constants injected as
* top-level bindings before parse, e.g. `{ DEBUG: false, PORT: 8080 }`. Values may
* be numbers, booleans, strings, null, or literal arrays/objects.
* @param {boolean|number|string|object} [opts.optimize] - Optimization level/config.
* - `false` / `0`: nothing. Fastest compile, largest output (live coding).
* - `1`: encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
* - `true` / `2` (default): every stable jz pass + full watr (inlineOnce +
* coalesce on; `inline` stays off per watr's own default).
* - `3` / `'speed'`: level 2 + larger array/hash initial caps (`arrayMinCap`,
* `hashSmallInitCap`) + `hoistConstantPool` off (inline `f64.const` over
* mutable globals); trades size for speed.
* - `'size'`: full passes with unrolling/SIMD off and tight scalar caps — smallest wasm.
* - `{ level?, <pass>?: bool, ... }`: per-pass overrides on top of a base level.
* INTERNAL/unstable — pass names track compiler internals (PASS_NAMES in
* src/optimize/index.js) and change between versions; prefer the level/string forms.
* @param {boolean} [opts.noSimd] - Disable auto-vectorization (no jz-emitted v128) for
* engines without the SIMD proposal. Explicit f32x4/i32x4 intrinsics still compile.
* @param {boolean} [opts.whyNotSimd] - Diagnostic: emit a `simd-why-not` warning (via
* opts.warnings) for each canonical loop the auto-vectorizer declined, naming the
* first blocking op. Finds loops one op away from SIMD. Noisy — off by default.
* @param {boolean} [opts.experimentalStencil] - Opt-in: vectorize neighbour-load
* stencils (`b[i]=f(a[i-1],a[i],a[i+1])`, 2-D 5-point) to f64x2. Bit-exact vs scalar.
* Unstable — off by default until proven across the corpus.
* @param {boolean} [opts.experimentalOuterStrip] - Opt-in: strip-mine a pixel loop whose
* per-pixel value is an inner reduction (metaballs-shape) into f64x2 lanes (2 pixels at
* once). Bit-exact vs scalar. Unstable — off by default.
* @param {boolean} [opts.experimentalToneMap] - Mixed-lane log-tonemap vectorizer: a flat
* `i32 dens[i] → f64 Math.log → i32 pack → px[i]` loop lifts to a 2-wide f64x2 island
* (fern/bifurcation/attractors). Bit-exact vs scalar; default-on at speed, pass `false` to disable.
* @param {object} [opts.warnings] - Optional mutable warning sink populated with
* `entries: [{ code, message, fn?, line?, column? }]`. Heap-growth advisories
* fire when a module uses the bump allocator and an export or loop retains
* allocations without a host-side memory.reset().
* @param {object} [opts.profile] - Optional mutable profile sink populated with
* `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
* @param {boolean} [opts.names] - Emit a standard wasm `name` custom section (function
* symbols) for profiler/debugger symbolication. (Legacy: `profile.names = true`.)
* @param {Object<string,string>} [opts.modules] - Map of module specifier → source
* for compile-time `import`/`export` bundling: jz resolves the module graph
* in-process from this map instead of reading from disk.
* @param {boolean} [opts.noTailCall] - Disable proper-tail-call emission (self/mutual
* recursion uses ordinary call frames). For engines/tools without the tail-call proposal.
* @param {boolean} [opts.nativeTimers] - Emit a blocking `__timer_loop` in `_start` so
* setTimeout/setInterval fire under a standalone runtime (e.g. the wasmtime CLI) that
* has no host event loop. Default: timers defer to the JS host.
* @param {string} [opts.importMetaUrl] - Module URL used to lower `import.meta.url`
* and static `import.meta.resolve("...")` expressions.
* @param {number|boolean} [opts.randomSeed] - Seed for `Math.random`. Default: seeded
* once from host entropy on first use (crypto under `host:'js'`, `random_get` under
* WASI) — non-reproducible. Pass a number for a fixed, reproducible seed; `true` forces
* entropy explicitly. The randomness syscall is emitted only when `Math.random` is used.
* @param {boolean} [opts.inspect] - When true, return `{ wasm, inspect }`
* (or `{ wat, inspect }` with `opts.wat`) instead of the bare output.
* `inspect` carries per-function inferred shapes (params, locals, JSON shapes,
* cross-call paramReps) for editor hosts to drive inlay hints / hover types
* without re-running the analyzer. Pays a small serialization cost; off by default.
* @returns {Uint8Array|string|{wasm: Uint8Array, inspect: object}|{wat: string, inspect: object}}
*/
// Test-only compile target. When set (by test/index.js under JZ_TEST_TARGET=jz.wasm)
// every jz.compile / compile / jz() call routes through it instead of the in-process
// compiler — used to run the whole suite against dist/jz.wasm (the jz compiler
// compiled to wasm by jz). null in production: one boolean check on a cold path.
let compileTarget = null
export const _setCompileTarget = (fn) => { compileTarget = fn }
jz.compile = (code, opts = {}) => {
if (compileTarget) return compileTarget(code, opts)
try {
return jzCompileInner(code, opts)
} catch (e) {
// Any uncaught native exception (TypeError, ReferenceError, etc.) is a jz
// codegen leak — surface it as an internal compile error with the source
// location we were standing on. err()-thrown errors already pass through.
if (e?.name === 'TypeError' || e?.name === 'ReferenceError' || e?.name === 'RangeError') {
// Pass `e` as the cause so the original stack (the real codegen site) survives.
err(`internal: ${e.message} (jz hit an unsupported case while compiling${ctx.error.node ? '; the AST node above shows the trigger' : ''}). This is a jz bug — please report.`, e)
}
throw e
}
}
// =============================================================================
// Optimization auto-tuning: scan prepared AST + ctx.func.list to infer program
// properties, then emit per-pass overrides. When the user does not explicitly
// configure individual passes, the result is merged in before resolveOptimize()
// so the compiler self-tunes.
// =============================================================================
const AUTO_CFG_LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
const AUTO_CFG_TYPED_CTORS = new Set([
'new.Float32Array', 'new.Float64Array', 'new.Int8Array', 'new.Int16Array',
'new.Int32Array', 'new.Uint8Array', 'new.Uint16Array', 'new.Uint32Array',
'new.Uint8ClampedArray',
])
const autoCfgNodeSize = (node) => {
if (!Array.isArray(node)) return 1
let n = 1
for (let i = 1; i < node.length; i++) n += autoCfgNodeSize(node[i])
return n
}
const autoCfgScanNode = (node, stats, loopDepth) => {
if (!Array.isArray(node)) return
stats.totalNodes++
const op = node[0]
if (AUTO_CFG_LOOP_OPS.has(op)) {
stats.loopCount++
const d = loopDepth + 1
if (d > stats.maxLoopDepth) stats.maxLoopDepth = d
for (let i = 1; i < node.length; i++) autoCfgScanNode(node[i], stats, d)
return
}
if (op === '()') {
stats.callSites++
const callee = node[1]
if (typeof callee === 'string' && AUTO_CFG_TYPED_CTORS.has(callee)) {
stats.typedArrayCount++
const args = node[2]
const argList = args == null ? [] : (Array.isArray(args) && args[0] === ',') ? args.slice(1) : [args]
const lenLit = typeof argList[0] === 'number' ? argList[0] : null
if (lenLit != null && lenLit > stats.maxTypedArrayLen) stats.maxTypedArrayLen = lenLit
}
}
if (op === 'str') stats.stringLiteralCount++
if (op === '=>') stats.closureCount++
for (let i = 1; i < node.length; i++) autoCfgScanNode(node[i], stats, loopDepth)
}
/** Detect optimization config from source characteristics.
* Returns an object of pass overrides; empty object means "use defaults". */
const detectOptimizeConfig = (ast, code) => {
const s = {
totalNodes: 0,
funcCount: 0, maxFuncBodySize: 0,
loopCount: 0, maxLoopDepth: 0,
typedArrayCount: 0, maxTypedArrayLen: 0,
stringLiteralCount: 0, closureCount: 0, callSites: 0,
}
if (ctx.func?.list) {
s.funcCount = ctx.func.list.length
for (const f of ctx.func.list) {
if (f.body) {
const sz = autoCfgNodeSize(f.body)
if (sz > s.maxFuncBodySize) s.maxFuncBodySize = sz
autoCfgScanNode(f.body, s, 0)
}
}
}
if (ast) autoCfgScanNode(ast, s, 0)
const cfg = {}
// Machine-generated or large code: watr's WAT-level CSE/DCE/inline fights
// jz's already-optimized IR and inflates output. Disable it automatically —
// EXCEPT when the module uses SIMD intrinsics. A v128 helper call (e.g.
// f64x2.sin → $math.sin2) leaves v128 params/results across the call boundary;
// watr's inliner folds the helper into the loop and coalesces those vectors,
// and without it the v128 spills to memory every iteration (~2× slower on the
// trig-bound attractors kernel). Explicit SIMD is a perf opt-in — keep watr on.
const usesSimd = !!ctx.module?.modules?.simd
// AST-node metric, NEVER raw source length: a 5 KB comment must not change
// codegen (measured 2.9× size delta pre-fix — the formatting-invariance
// test pins this). ~500 nodes ≈ the old 4000-char threshold on idiomatic
// source, calibrated against the same corpus.
const isLarge = s.totalNodes > 500 || s.funcCount > 40 || s.maxFuncBodySize > 300
const isMachineLike = s.callSites > 300 && s.stringLiteralCount < 10
if ((isLarge || isMachineLike) && !usesSimd) { cfg.watr = false; cfg.splitCharScan = false }
// Typed-array heavy: tighten scalarization thresholds when we see large
// fixed-size arrays; keep defaults for small/dynamic ones.
if (s.typedArrayCount > 0 && s.maxTypedArrayLen > 0) {
cfg.scalarTypedArrayLen = Math.min(32, Math.max(8, s.maxTypedArrayLen + 4))
cfg.scalarTypedLoopUnroll = s.maxLoopDepth > 1 ? 8 : 16
cfg.scalarTypedNestedUnroll = s.maxLoopDepth > 1 ? 32 : 128
}
// Closure-heavy: ptr hoists pay off.
if (s.closureCount > 4) {
cfg.hoistPtrType = true
cfg.hoistInvariantPtrOffset = true
}
return cfg
}
// Test-matrix bridge: when JZ_TEST_* env vars are set, inject them as default
// opts so the npm test suite can be re-run under varying configurations (opt
// levels, host, jzify, …) without source changes. User-supplied opts always win
// — env defaults fill only what the caller left unset. Resolved once at module
// load; no-op (single boolean check) when the env is empty (production path).
const TEST_ENV_DEFAULTS = (() => {
// Guard bare `process` — the compiler bundle must load in browsers/Workers too
// (this IIFE runs at module import; an unguarded ref throws ReferenceError there).
const e = (typeof process !== 'undefined' && process.env) || {}
const out = {}
if (e.JZ_TEST_OPTIMIZE != null) {
const v = e.JZ_TEST_OPTIMIZE
out.optimize = /^-?\d+$/.test(v) ? Number(v) : v === 'false' ? false : v
}
if (e.JZ_TEST_HOST) out.host = e.JZ_TEST_HOST
if (e.JZ_TEST_STRICT) out.strict = e.JZ_TEST_STRICT === '1'
return out
})()
const HAS_TEST_ENV = Object.keys(TEST_ENV_DEFAULTS).length > 0
// Shared front-half: reset ctx, wire opts → ctx.transform/memory/module/features,
// and inject parse/resolveUrl. Called by `jzCompileInner` (the only entry point
// today). The self-host entry (scripts/self.js) drives reset itself rather than
// going through this path, since it needs only a minimal, interop-free setup.
const setupCtx = (code, opts) => {
if (HAS_TEST_ENV) {
const merged = { ...opts }
for (const k of Object.keys(TEST_ENV_DEFAULTS)) if (merged[k] == null) merged[k] = TEST_ENV_DEFAULTS[k]
opts = merged
}
reset(emitter, GLOBALS, { emit, flat, body, bool, idx, spread })
resetProgramFactsCache()
ctx.error.src = code
initWarnings(opts.warnings)
if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
else if (opts.memory) ctx.memory.shared = true
if (opts.importMemory) ctx.memory.shared = true // import env.memory instead of exporting own
// True cross-thread sharing (Workers v1): import env.memory declared with the
// wasm `shared` memtype and switch the heap bump to atomic RMW. Distinct from
// importMemory — a plain imported (non-shared) Memory must NOT declare shared
// or linking fails in the other direction.
if (opts.sharedMemory) { ctx.memory.shared = true; ctx.memory.atomic = true }
if (opts.maxMemory != null) {
if (!Number.isInteger(opts.maxMemory) || opts.maxMemory < 1)
err(`opts.maxMemory must be a positive integer page count (each page is 64 KiB); got ${opts.maxMemory}`)
const initialPages = ctx.memory.pages || 1
if (opts.maxMemory < initialPages)
err(`opts.maxMemory (${opts.maxMemory}) is below the initial memory size (${initialPages} pages)`)
ctx.memory.max = opts.maxMemory
}
if (opts.modules) ctx.module.importSources = opts.modules
if (opts.imports) {
ctx.module.hostImports = opts.imports
if (importsMayReturnExternal(opts.imports)) ctx.features.external = true
}
// Parser for compile-time import bundling (prepareModule). Injected, not
// imported by prepare — see ctx.transform.parse note in prepare/index.js.
ctx.transform.parse = parse
ctx.transform.resolveUrl = resolveUrl
// jzify runs by default — accept the full JS subset (function/var/switch lowered to
// arrows/let/if). `strict: true` skips it, so prepare rejects disallowed JS features
// and the pure canonical subset is enforced. subscript handles ASI natively.
if (!opts.strict) ctx.transform.jzify = jzify
if (opts.noTailCall) ctx.transform.noTailCall = true
if (opts.strict) ctx.transform.strict = true
if (opts.host) {
if (opts.host === 'gc') err(`host:'gc' is reserved for a planned wasm-gc backend, not yet implemented. Use 'js' (default — JS host with externref/js-string interop) or 'wasi' (standalone runtimes — no env imports).`)
if (opts.host !== 'js' && opts.host !== 'wasi') err(`Invalid host '${opts.host}'. Expected 'js' (default) or 'wasi'.`)
ctx.transform.host = opts.host
}
if (opts.alloc === false) ctx.transform.alloc = false
if (opts.inspect) ctx.transform.inspect = true
if (opts.helperCounters) ctx.transform.helperCounters = true
if (opts.helperCallsites) ctx.transform.helperCallsites = opts.helperCallsites
if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
if (opts.randomSeed !== undefined) {
if (opts.randomSeed !== true && !Number.isFinite(opts.randomSeed))
err(`opts.randomSeed must be a finite number (fixed seed — reproducible) or true (seed Math.random from host entropy on first use); got ${typeof opts.randomSeed}`)
ctx.transform.randomSeed = opts.randomSeed
}
if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
ctx.transform.optimize = resolveOptimize(opts.optimize)
if (opts._interp) {
for (const [name, fn] of Object.entries(opts._interp)) {
if (name.startsWith('__ext_')) continue
if (ctx.transform.host === 'wasi') throw new Error(`host:'wasi' does not support _interp['${name}']: env imports are unavailable in WASI. Implement it natively.`)
ctx.features.external = true
const params = Array(fn.length).fill(['param', 'f64'])
ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
}
}
}
// U+E000 (T) prefixes every jz-generated local. The JS spec forbids it in
// identifiers, but subscript's parser is lenient and accepts it — so a user name
// carrying it could silently alias a compiler temp. Reject it in identifier
// position on the RAW parse (before jzify, which legitimately mints T-prefixed
// temps of its own). String-literal nodes are `[null, …]` and skipped, so
// `"……"` data is fine; only walked when the char is present in source.
const rejectReservedPrefix = (node) => {
if (!Array.isArray(node)) return
if (node.length === 2 && node[0] == null) return // [null, X] — value literal, not an identifier
for (let i = 1; i < node.length; i++) {
const v = node[i]
if (typeof v === 'string') {
if (v.includes(T)) err(`identifier '${v.split(T).join('\\uE000')}' contains the reserved compiler prefix (U+E000) — jz uses it for generated locals; rename it`)
} else rejectReservedPrefix(v)
}
}
const jzCompileInner = (code, opts = {}) => {
if (opts.define) code = defineBindings(opts.define) + code
const profiler = compileProfiler(opts.profile)
const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
setupCtx(code, opts)
assertCtxInvariants('post-reset')
let parsed = time('parse', () => parse(code))
if (typeof code === 'string' && code.includes(T)) rejectReservedPrefix(parsed)
// Lambda-lift immediately-invoked arrow literals to typed direct calls — lets SIMD
// flow through the f64-only closure ABI and drops the closure for every IIFE. Runs
// BEFORE jzify so it only sees USER arrow IIFEs, not jzify's synthetic wrapper IIFEs
// (named/recursive function expressions, method shorthand), which keep the closure
// path. A no-op when there are none.
parsed = time('liftIIFE', () => liftIIFEs(parsed))
if (!opts.strict) parsed = time('jzify', () => jzify(parsed))
let ast = time('prepare', () => prepare(parsed))
assertCtxInvariants('post-prepare')
// preEval: fold every statically-evaluable construct (numeric/string/bool chains,
// pure Math.* calls, zero-arg pure calls incl. lift-iife's IIFEs) down to literals,
// over the prepared AST + every ctx.func.list body, before compile ever sees them.
ast = time('preEval', () => preEval(ast))
// Auto-detect optimization tuning from source characteristics when the user
// hasn't provided any optimize option. detectOptimizeConfig has two *live*
// overrides over the level-2 preset: the typed-array scalarization thresholds,
// and `watr: false` for large/machine-generated code (whose WAT-level CSE/DCE
// fights jz's already-optimized IR and inflates output). watr is ON at level 2,
// so that switch is a real override — run the scan when the program either
// touches typed arrays or is large enough for the machine-code heuristic to
// bite. `code.length` is the one signal free without scanning; gating on it
// keeps small programs (the common case) on the no-scan fast path.
// No source-length activation gate: the decision metrics are AST-shaped, and
// the activation must be too, or a comment could flip whether the scan runs
// (and with it the output bytes). One linear walk on default-level compiles.
if (opts.optimize == null) {
const autoCfg = detectOptimizeConfig(ast, code)
if (Object.keys(autoCfg).length) {
ctx.transform.optimize = resolveOptimize(autoCfg)
}
}
// opts.noSimd: force auto-vectorization off regardless of opt level — a
// portability escape hatch for engines without the SIMD proposal (parallels
// opts.noTailCall). Must suppress EVERY jz-emitted v128, which is two passes:
// vectorizeLaneLocal (lane maps, reductions incl. reduceUnroll, byte scans) AND
// the SLP store-pair packer (within-iteration f64x2). Both off, so `noSimd` is a
// true scalar baseline — the oracle SIMD-vs-scalar correctness tests compare
// against (missing one let an SLP miscompile pass as "SIMD == scalar"). Explicit
// f32x4/i32x4 intrinsics in source are the user's own opt-in and stay. Applied
// after auto-config so it wins over any re-resolved preset.
if (opts.noSimd) {
ctx.transform.optimize.vectorizeLaneLocal = false
ctx.transform.optimize.experimentalSlp = false
}
// opts.whyNotSimd (CLI --why-not-simd): emit a `simd-why-not` warning per
// canonical loop that the auto-vectorizer declined, naming the blocking op —
// a diagnostic to find loops that are "one op away" from SIMD. Rides the
// resolved optimize cfg to the vectorizer; off by default (the report is noisy).
if (opts.whyNotSimd && ctx.transform.optimize) ctx.transform.optimize.whyNotSimd = true
// opts.experimentalStencil: the neighbour-load stencil vectorizer (a[i±1] / 2-D 5-point).
// Now default-on at optimize:'speed' (proven bit-exact corpus-wide); the opt is two-way so an
// explicit `false` can still disable it (e.g. to A/B against the scalar path).
if (opts.experimentalStencil !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalStencil = !!opts.experimentalStencil
// opts.experimentalOuterStrip: the outer-loop strip-mine vectorizer (2 adjacent pixels in f64x2
// lanes over an inner reduction). Default-on at speed; two-way like experimentalStencil.
if (opts.experimentalOuterStrip !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalOuterStrip = !!opts.experimentalOuterStrip
// opts.experimentalToneMap: the mixed-lane log-tonemap vectorizer (i32 dens[i] → f64 log →
// i32 pack → px[i], 2-wide f64x2 island). Default-on at speed; two-way like the others.
if (opts.experimentalToneMap !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalToneMap = !!opts.experimentalToneMap
const module = time('compile', () => compile(ast, profiler))
assertCtxInvariants('post-compile')
// host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
// only to defer to a JS host's value-aware semantics; in a wasmtime/wasmer/deno
// sandbox the imports either go unsatisfied or are stubbed out and silently produce
// wrong output. Surface the gap at compile so the caller can pick a comparator,
// type-annotate the receiver, or wait for native lowering. Read `extImports`
// (populated in pullStdlib) — `core.includes` has had these removed by then.
if (ctx.transform.host === 'wasi' && ctx.core.extImports?.size) {
const ext = [...ctx.core.extImports].sort()
err(
`host: 'wasi' — compiled wasm would require JS-host imports that wasmtime/wasmer/deno cannot satisfy:\n ` +
ext.map(n => `env.${n}`).join('\n ') +
`\nThis happens when jz falls through to dynamic dispatch for a method or property without a native lowering. ` +
`Either annotate the receiver type, switch to a natively-supported method, or compile with the default host.`)
}
const cfg = ctx.transform.optimize
let watrOpts = typeof cfg.watr === 'object' ? { ...cfg.watr } : true
if (cfg.vectorizeLaneLocal) {
// Off at the speed tier: watr's loopify collapses the while-idiom to
// `loop { if C { …; br } }` (an UNfused back-jump — no win) AND would mangle the
// lane-vectorizer's loop shape. jz's own `rotateLoops` (optimize/index.js, post
// phase) does the rotation instead, emitting the FUSED `br_if $loop C` back-edge.
if (watrOpts === true) watrOpts = { loopify: false }
else if (typeof watrOpts === 'object' && watrOpts.loopify === undefined) watrOpts.loopify = false
}
if (cfg.devirtIndirect) {
if (watrOpts === true) watrOpts = { devirt: true }
else if (typeof watrOpts === 'object' && watrOpts.devirt === undefined) watrOpts.devirt = true
}
// Multi-caller small-function inlining (size-for-speed): on at the 'speed'/level-3
// tier only, like devirt above. Removes call overhead from hot inner loops at the
// cost of duplicating tiny bodies; level 2 (and the size budgets measured there)
// stay untouched.
if (cfg.inlineFns) {
if (watrOpts === true) watrOpts = { inline: 'simd' }
else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline = 'simd'
}
// Wrapper elision (speed tier): dissolve adapter frames — closure-ABI
// trampolines for functions-as-values, thin dispatch heads like
// __dyn_get_expr — into their target at the wrapper site. The recorded
// "table entries native against ftN" restructure, done mechanically in the
// optimizer instead of per-case emit surgery (one wrapper = one duplicated
// target; treeshake collects whichever copy ends up unreferenced).
if (cfg.inlineFns) {
if (typeof watrOpts === 'object' && watrOpts.inlineWrappers === undefined) watrOpts.inlineWrappers = true
}
// watr LICM (speed tier): hoist loop-invariant pure arithmetic AFTER watr's inlining exposes it
// (the raytrace per-iteration NaN/Inf guard pairs). Mechanical — lives in watr, jz just enables it.
if (cfg.watrLicm) {
if (watrOpts === true) watrOpts = { licm: true }
else if (typeof watrOpts === 'object' && watrOpts.licm === undefined) watrOpts.licm = true
}
// guardRefine folds jz's NaN-box tag reads — default-off in watr (general WAT
// never matches it), so jz always enables it explicitly.
if (watrOpts === true) watrOpts = { guardRefine: true }
else if (typeof watrOpts === 'object' && watrOpts.guardRefine === undefined) watrOpts.guardRefine = true
// watr's ifset (one-armed conditional update → select) rides jz's select
// tiering: an unconditional-update trade belongs to the speed tier exactly
// like boolConvertToSelect. jz's DEFAULT tier also passes watrProfile:'speed'
// (for the outline/tailmerge shape), which would enable ifset via the
// profile — the explicit flag here overrides it in both directions.
if (typeof watrOpts === 'object' && watrOpts.ifset === undefined)
watrOpts.ifset = cfg.boolConvertToSelect === true || cfg.watrIfset === true
// jz's promise is runtime speed, but watr's OWN profile default leans size — outline/
// tailmerge/rettail fold repeated sequences into out-of-line calls (measured 1.433→1.316
// on the self-host kernel with them off, watr ≥5.2.0). Every speed-tier preset carries
// watrProfile:'speed' (src/optimize/index.js LEVEL_PRESETS); the 'size' preset keeps
// watr's size-leaning default. An explicit user profile always wins.
if (watrOpts.profile === undefined && cfg.watrProfile) watrOpts.profile = cfg.watrProfile
// Speed tier waives watr's size-revert guard (two full binary encodes per
// optimize — its costliest fixed overhead): inlining growth is the shape this
// tier asks for, and jz's own perf/size gates own the size budget. Level 2
// (default) and 'size' keep the guard — watr's never-inflate contract stands
// where the user didn't opt into speed-for-size.
if (watrOpts.guard === undefined && cfg.watrGuard === false) watrOpts.guard = false
// Pin jz's scalar transcendentals (the PPC_CALL2 keys the auto-vectorizer rewrites to f64x2
// mirrors) so watr's inline passes don't dissolve the call nodes the lift needs. The protection
// policy lives here in jz — watr just honours the `pin` list (no jz names hardcoded in watr).
if (cfg.watr) {
if (watrOpts === true) watrOpts = {}
watrOpts.pin = watrOpts.pin ? [...watrOpts.pin, ...SIMD_PINNED] : SIMD_PINNED
// Partial unrolling overlaps branch latency in compact codecs, but duplicates
// too many cold compiler/parser loops in large module graphs and loses the
// warm self-host I-cache race. Users may still opt in explicitly.
if (watrOpts.unroll2 == null && ctx.func.list.length > 64) watrOpts.unroll2 = false
// Keep only JS-boundary vectorized functions intact: their `$name$exp`
// wrapper must not swallow the body/markers structural host tests inspect.
// Internal lifted helpers remain inlineable — SIMD survives the splice,
// while caller-level constant propagation and hot-call removal become live.
if (cfg._vectorizedFnNames?.size) {
const boundaryPins = [...cfg._vectorizedFnNames].filter(name => ctx.func.map.get(name.slice(1))?.exported)
if (boundaryPins.length) watrOpts.pin = [...watrOpts.pin, ...boundaryPins]
}
}
const optimized = cfg.watr ? time('watOptimize', () => watOptimize(module, watrOpts)) : module
// Narrow whole-module proof repairs may consume the final address/control shape watr exposes:
// stable pointer offsets, immutable fixed typed cells, and all-false masked SIMD suffixes. These
// are idempotent, fail-closed module rewrites — not a second generic optimization pipeline.
if (cfg.hoistGlobalPtrOffset !== false || cfg.hoistGlobalConstLoads !== false || cfg.maskedSuffixGuard !== false) {
const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
const stableGlobals = cfg.hoistGlobalPtrOffset !== false ? stablePtrGlobalNames() : new Set()
let reach = null
if (stableGlobals.size) {
reach = collectReachableGlobalWrites(funcs)
for (const node of funcs) hoistGlobalPtrOffset(node, stableGlobals, reach)
}
const wantLoads = cfg.hoistGlobalConstLoads !== false && !!ctx.scope.globalTypedLen?.size
const mayHaveMasks = cfg.maskedSuffixGuard !== false && funcs.some(fn =>
fn.some(node => Array.isArray(node) && node[0] === 'local' && node[2] === 'v128'))
const wantMasks = mayHaveMasks && hasIROp(funcs, 'v128.bitselect')
if (wantLoads || wantMasks) {
if (wantLoads && !reach) reach = collectReachableGlobalWrites(funcs)
const memoryWrites = collectReachableMemoryWrites(funcs)
for (const node of funcs) {
if (wantLoads) hoistStableGlobalConstLoads(node, memoryWrites, reach)
if (wantMasks) guardMaskedVectorSuffix(node, memoryWrites)
}
}
}
// NO post-watr generic optimizer. jz does all lowering — including auto-vectorization — before
// watr (src/wat/assemble.js optimizeModule → optimizeFunc); watr is the sole generic fixpoint and
// runs exactly once. Re-running jz's leaf pipeline here dropped a reassigned-param local.tee and
// corrupted divergent-escape SIMD. The proof repairs above are the deliberately narrow exception.
// Pre-eval tier 3 — module-init snapshotting (src/snapshot.js): run __start once
// NOW, bake the post-init heap image + global values into the artifact, delete
// __start. Opt-in (optimize.snapshotInit); declined cleanly (dynamically-proven
// hermeticity) when init touches the host, loops forever, or memory is shared.
if (cfg.snapshotInit) {
const took = time('snapshotInit', () => snapshotInit(optimized, watrCompile))
if (!took && opts.warnings) warn('snapshot-declined', 'init snapshot declined (host-touching, timer, or shared-memory init) — compiled without it')
}
try {
if (opts.wat) {
const wat = time('watrPrint', () => watrPrint(optimized))
return opts.inspect ? { wat, inspect: ctx.inspect } : wat
}
const wasm = time('watrCompile', () => watrCompile(optimized))
let bytes = wasm
// opts.names emits a wasm `name` custom section (symbols for profilers/
// debuggers). opts.profile.names is the older spelling — still honored.
if (opts.names || opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
return opts.inspect ? { wasm: bytes, inspect: ctx.inspect } : bytes
} catch (e) {
// watr surfaces dangling identifiers as "Unknown local|func|global|table|memory $X".
// That's always a jz codegen leak — we emitted IR that references something never
// declared (typically: a built-in / stdlib we don't implement). Rewrite to a clean
// user-facing message instead of leaking watr internals.
const m = /Unknown (local|func|global|table|memory|type) \$?(\S+)/.exec(e?.message || '')
if (m) {
const [, kind, name] = m
const friendly = kind === 'func' ? `'${name}' is not a known function or built-in`
: kind === 'global' ? `'${name}' is not a known global or imported binding`
: `'${name}' is not in scope`
err(`${friendly} — jz emitted a reference it cannot resolve (likely an unsupported built-in or missing import).`)
}
throw e
}
}
/**
* Compile, instantiate, and wrap. Works as both jz('code') and jz`code ${val}`.
* @param {string|TemplateStringsArray} code
* @param {...any} args - Interpolation values (template tag) or options (string call)
* @returns {{exports, memory, instance, module}}
*/
export default function jz(code, ...args) {
// Template tag: jz`code ${val}` — numbers, functions, strings, arrays, objects
if (Array.isArray(code)) {
const interp = {}, data = {}, hoisted = []
let src = code[0]
for (let i = 0; i < args.length; i++) {
const v = args[i]
if (typeof v === 'function') {
const key = `$$${i}`; interp[key] = v; src += key
} else {
const s = serialize(v)
if (s !== null && (typeof v === 'number' || typeof v === 'boolean')) {
// Scalars inline directly
src += s
} else if (s !== null) {
// Strings, arrays, objects — hoist as compile-time literal
const key = `$$${i}`
hoisted.push(`let ${key} = ${s}`)
src += key
} else {
// Non-serializable (host objects, etc.) — post-instantiation getter
const key = `$$${i}`, ref = { ptr: 0 }
data[key] = { val: v, ref }; interp[key] = () => ref.ptr
src += `${key}()`
}
}
src += code[i + 1]
}
if (hoisted.length) src = hoisted.join('; ') + '; ' + src
const hasInterp = Object.keys(interp).length
const tplOpts = { _interp: hasInterp ? interp : null }
const result = instantiateRuntime(jz.compile(src, tplOpts), tplOpts)
// Patch data getters: allocate values in WASM memory, update closure refs
for (const [, { val, ref }] of Object.entries(data)) {
if (typeof val === 'string') ref.ptr = result.memory.String(val)
else if (Array.isArray(val)) ref.ptr = result.memory.Array(val)
else ref.ptr = result.memory.Object(val)
}
return result
}
// String call: jz('code', opts?) — compile + instantiate + wrap
const callOpts = args[0] || {}
const out = jz.compile(code, callOpts)
const wasm = out && typeof out === 'object' && 'wasm' in out ? out.wasm : out
const result = instantiateRuntime(wasm, callOpts)
const extra = {}
if (callOpts.inspect && out && typeof out === 'object' && 'inspect' in out) extra.inspect = out.inspect
if (callOpts.warnings) extra.warnings = callOpts.warnings.entries
return Object.keys(extra).length ? Object.assign(result, extra) : result
}
export { jz }
const jzCompile = jz.compile
export { jzCompile as compile }
/**
* Compile source to a cached `WebAssembly.Module` (compile + validate once).
* Pair with `instantiate(module, opts)` to spin up many instances without
* re-validating the bytes each time — the AOT compile is paid once, so repeated
* instantiation is cheap. Returns a `WebAssembly.Module`, built with the native
* `wasm:js-string` builtins fast path where the engine supports it.
*
* import { compileModule, instantiate } from 'jz'
* const mod = compileModule('export let f = x => x * 2')
* const { exports } = instantiate(mod) // repeat cheaply, no recompile
*
* @param {string} code
* @param {object} [opts] - same options as `compile()`
* @returns {WebAssembly.Module}
*/
export const compileModule = (code, opts = {}) => {
const out = jzCompile(code, opts)
return toModule(out && typeof out === 'object' && 'wasm' in out ? out.wasm : out)
}
export { instantiateRuntime as instantiate }
/**
* jzify as a standalone source→source transform: full JS in, canonical jz out.
* Same jzify/ module the compiler runs on every parse (default-on) — re-exported
* here so browser bundles (dist/jz.js: REPL auto-jzify on paste) and node users
* (`import { transform } from 'jz'`, also `jz/transform`) share one lowering.
*/
export { default as transform } from './transform.js'