jz 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -38,11 +38,12 @@
38
38
  * @module jz
39
39
  */
40
40
 
41
- import { parse } from 'subscript/jessie'
41
+ import { parse } from 'subscript/feature/jessie'
42
42
  import { compile as watrCompile, print as watrPrint, optimize as watrOptimize } from "watr";
43
- import { ctx, reset } from './src/ctx.js'
43
+ import { ctx, reset, err } from './src/ctx.js'
44
44
  import prepare, { GLOBALS } from './src/prepare.js'
45
- import compile, { emitter } from './src/compile.js'
45
+ import compile from './src/compile.js'
46
+ import { emitter } from './src/emit.js'
46
47
  import { optimizeFunc, resolveOptimize } from './src/optimize.js'
47
48
  import jzify from './src/jzify.js'
48
49
  import {
@@ -62,6 +63,83 @@ const importsMayReturnExternal = (imports) => {
62
63
  return false
63
64
  }
64
65
 
66
+ const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
67
+
68
+ const compileProfiler = (profile) => {
69
+ if (profile == null) return null
70
+ if (typeof profile !== 'object') throw new TypeError('opts.profile must be an object sink; use { profile: { names: true } } to emit a wasm name section')
71
+ profile.entries ||= []
72
+ profile.totals ||= {}
73
+ return {
74
+ time(name, fn) {
75
+ const start = nowMs()
76
+ try { return fn() }
77
+ finally {
78
+ const ms = nowMs() - start
79
+ profile.entries.push({ name, ms })
80
+ profile.totals[name] = (profile.totals[name] || 0) + ms
81
+ }
82
+ },
83
+ }
84
+ }
85
+
86
+ const uleb = (n) => {
87
+ const out = []
88
+ do {
89
+ let b = n & 0x7f
90
+ n >>>= 7
91
+ if (n) b |= 0x80
92
+ out.push(b)
93
+ } while (n)
94
+ return out
95
+ }
96
+
97
+ const utf8Bytes = (s) => [...new TextEncoder().encode(s)]
98
+ const nameBytes = (s) => {
99
+ const bytes = utf8Bytes(s)
100
+ return [...uleb(bytes.length), ...bytes]
101
+ }
102
+
103
+ const watName = (s) => typeof s === 'string' && s.startsWith('$') ? s.slice(1) : null
104
+ const quotedName = (s) => typeof s === 'string' && /^".*"$/.test(s) ? s.slice(1, -1) : null
105
+
106
+ const importFuncName = (node) => {
107
+ if (!Array.isArray(node) || node[0] !== 'import') return null
108
+ const desc = node[3]
109
+ if (!Array.isArray(desc) || desc[0] !== 'func') return null
110
+ return watName(desc[1]) || quotedName(node[2])
111
+ }
112
+
113
+ const functionNameSection = (module) => {
114
+ const entries = []
115
+ let funcIdx = 0
116
+ for (const node of module) {
117
+ if (!Array.isArray(node)) continue
118
+ if (node[0] === 'import') {
119
+ const name = importFuncName(node)
120
+ if (name != null) entries.push([funcIdx++, name])
121
+ } else if (node[0] === 'func') {
122
+ const name = watName(node[1])
123
+ if (name != null) entries.push([funcIdx, name])
124
+ funcIdx++
125
+ }
126
+ }
127
+ if (!entries.length) return null
128
+ const map = [...uleb(entries.length)]
129
+ for (const [idx, name] of entries) map.push(...uleb(idx), ...nameBytes(name))
130
+ const payload = [...nameBytes('name'), 1, ...uleb(map.length), ...map]
131
+ return Uint8Array.from([0, ...uleb(payload.length), ...payload])
132
+ }
133
+
134
+ const appendFunctionNames = (wasm, module) => {
135
+ const section = functionNameSection(module)
136
+ if (!section) return wasm
137
+ const out = new Uint8Array(wasm.length + section.length)
138
+ out.set(wasm)
139
+ out.set(section, wasm.length)
140
+ return out
141
+ }
142
+
65
143
  /**
66
144
  * jz — JS subset → WASM compiler.
67
145
  *
@@ -84,8 +162,12 @@ jz.memory = enhanceMemory
84
162
  * @param {boolean} [opts.strict] - Reject dynamic features (obj[k], for-in, unknown
85
163
  * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
86
164
  * into output; large size win for static programs.
87
- * @param {boolean} [opts.runtimeExports=true] - Export runtime allocator helpers
88
- * (`_alloc`, `_reset`) for JS memory wrapping. Set false for standalone host-run
165
+ * @param {WebAssembly.Memory|number} [opts.memory] - Shared memory import, or
166
+ * initial page count for owned memory when a number is passed.
167
+ * @param {number} [opts.memoryPages] - Legacy alias for owned/imported memory
168
+ * initial page count. Prefer `memory: N` for owned memory.
169
+ * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
170
+ * (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
89
171
  * modules that only call exported wasm functions.
90
172
  * @param {boolean|number|object} [opts.optimize] - Optimization level/config.
91
173
  * - `false` / `0`: nothing. Fastest compile, largest output (live coding).
@@ -94,13 +176,24 @@ jz.memory = enhanceMemory
94
176
  * - `3`: reserved for future aggressive passes (currently == 2).
95
177
  * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
96
178
  * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
179
+ * @param {object} [opts.profile] - Optional mutable profile sink populated with
180
+ * `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
181
+ * Set `profile.names = true` to also emit a standard wasm `name` custom section
182
+ * for profiler/debugger symbolication.
183
+ * @param {boolean} [opts.profileNames] - Legacy alias for `profile.names`.
184
+ * @param {string} [opts.importMetaUrl] - Module URL used to lower `import.meta.url`
185
+ * and static `import.meta.resolve("...")` expressions.
97
186
  * @returns {Uint8Array|string}
98
187
  */
99
188
  jz.compile = (code, opts = {}) => {
189
+ const profiler = compileProfiler(opts.profile)
190
+ const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
191
+
100
192
  reset(emitter, GLOBALS)
101
193
  ctx.error.src = code
102
194
 
103
- if (opts.memory) ctx.memory.shared = true
195
+ if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
196
+ else if (opts.memory) ctx.memory.shared = true
104
197
  if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
105
198
  if (opts.modules) ctx.module.importSources = opts.modules
106
199
  if (opts.imports) {
@@ -112,34 +205,60 @@ jz.compile = (code, opts = {}) => {
112
205
  if (opts.jzify) ctx.transform.jzify = jzify
113
206
  if (opts.noTailCall) ctx.transform.noTailCall = true
114
207
  if (opts.strict) ctx.transform.strict = true
115
- if (opts.runtimeExports === false) ctx.transform.runtimeExports = false
208
+ if (opts.host) {
209
+ if (opts.host !== 'js' && opts.host !== 'wasi') err(`Invalid host '${opts.host}'. Expected 'js' or 'wasi'.`)
210
+ ctx.transform.host = opts.host
211
+ }
212
+ if (opts.alloc === false) ctx.transform.alloc = false
213
+ if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
116
214
  if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
117
215
  ctx.transform.optimize = resolveOptimize(opts.optimize)
118
216
 
119
217
  if (opts._interp) {
120
218
  for (const [name, fn] of Object.entries(opts._interp)) {
121
219
  if (name.startsWith('__ext_')) continue;
220
+ if (ctx.transform.host === 'wasi') throw new Error(`host:'wasi' does not support _interp['${name}']: env imports are unavailable in WASI. Implement it natively.`)
122
221
  ctx.features.external = true
123
222
  const params = Array(fn.length).fill(['param', 'f64'])
124
223
  ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
125
224
  }
126
225
  }
127
226
 
128
- let parsed = parse(code)
129
- if (opts.jzify) parsed = jzify(parsed)
130
- const ast = prepare(parsed)
131
- const module = compile(ast)
227
+ let parsed = time('parse', () => parse(code))
228
+ if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
229
+ const ast = time('prepare', () => prepare(parsed))
230
+ const module = time('compile', () => compile(ast, profiler))
231
+
232
+ // host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
233
+ // only to defer to a JS host's value-aware semantics; in a wasmtime/wasmer/deno
234
+ // sandbox the imports either go unsatisfied or are stubbed out and silently produce
235
+ // wrong output. Surface the gap at compile so the caller can pick a comparator,
236
+ // type-annotate the receiver, or wait for native lowering. Read `extImports`
237
+ // (populated in pullStdlib) — `core.includes` has had these removed by then.
238
+ if (ctx.transform.host === 'wasi' && ctx.core.extImports?.size) {
239
+ const ext = [...ctx.core.extImports].sort()
240
+ err(
241
+ `host: 'wasi' — compiled wasm would require JS-host imports that wasmtime/wasmer/deno cannot satisfy:\n ` +
242
+ ext.map(n => `env.${n}`).join('\n ') +
243
+ `\nThis happens when jz falls through to dynamic dispatch for a method or property without a native lowering. ` +
244
+ `Either annotate the receiver type, switch to a natively-supported method, or compile with the default host.`)
245
+ }
132
246
 
133
247
  const cfg = ctx.transform.optimize
134
- const optimized = cfg.watr ? watrOptimize(module) : module
248
+ const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module)) : module
135
249
  // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
136
250
  // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
137
251
  // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
138
252
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
139
253
  if (cfg.watr) {
140
- for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
254
+ const postCfg = { ...cfg, __phase: 'post' }
255
+ time('watrReopt', () => {
256
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, postCfg)
257
+ })
141
258
  }
142
- return opts.wat ? watrPrint(optimized) : watrCompile(optimized)
259
+ if (opts.wat) return time('watrPrint', () => watrPrint(optimized))
260
+ const wasm = time('watrCompile', () => watrCompile(optimized))
261
+ return (opts.profileNames || opts.profile?.names) ? appendFunctionNames(wasm, optimized) : wasm
143
262
  }
144
263
 
145
264
  /**