jz 0.1.1 → 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,15 +38,14 @@
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
45
  import compile from './src/compile.js'
46
46
  import { emitter } from './src/emit.js'
47
47
  import { optimizeFunc, resolveOptimize } from './src/optimize.js'
48
48
  import jzify from './src/jzify.js'
49
- import { normalizeSource } from './src/source.js'
50
49
  import {
51
50
  memory as enhanceMemory, instantiate as instantiateRuntime,
52
51
  } from './src/host.js'
@@ -67,7 +66,8 @@ const importsMayReturnExternal = (imports) => {
67
66
  const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
68
67
 
69
68
  const compileProfiler = (profile) => {
70
- if (!profile) return null
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
71
  profile.entries ||= []
72
72
  profile.totals ||= {}
73
73
  return {
@@ -83,6 +83,63 @@ const compileProfiler = (profile) => {
83
83
  }
84
84
  }
85
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
+
86
143
  /**
87
144
  * jz — JS subset → WASM compiler.
88
145
  *
@@ -105,8 +162,12 @@ jz.memory = enhanceMemory
105
162
  * @param {boolean} [opts.strict] - Reject dynamic features (obj[k], for-in, unknown
106
163
  * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
107
164
  * into output; large size win for static programs.
108
- * @param {boolean} [opts.runtimeExports=true] - Export runtime allocator helpers
109
- * (`_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
110
171
  * modules that only call exported wasm functions.
111
172
  * @param {boolean|number|object} [opts.optimize] - Optimization level/config.
112
173
  * - `false` / `0`: nothing. Fastest compile, largest output (live coding).
@@ -117,6 +178,11 @@ jz.memory = enhanceMemory
117
178
  * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
118
179
  * @param {object} [opts.profile] - Optional mutable profile sink populated with
119
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.
120
186
  * @returns {Uint8Array|string}
121
187
  */
122
188
  jz.compile = (code, opts = {}) => {
@@ -126,7 +192,8 @@ jz.compile = (code, opts = {}) => {
126
192
  reset(emitter, GLOBALS)
127
193
  ctx.error.src = code
128
194
 
129
- 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
130
197
  if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
131
198
  if (opts.modules) ctx.module.importSources = opts.modules
132
199
  if (opts.imports) {
@@ -138,24 +205,45 @@ jz.compile = (code, opts = {}) => {
138
205
  if (opts.jzify) ctx.transform.jzify = jzify
139
206
  if (opts.noTailCall) ctx.transform.noTailCall = true
140
207
  if (opts.strict) ctx.transform.strict = true
141
- 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)
142
214
  if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
143
215
  ctx.transform.optimize = resolveOptimize(opts.optimize)
144
216
 
145
217
  if (opts._interp) {
146
218
  for (const [name, fn] of Object.entries(opts._interp)) {
147
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.`)
148
221
  ctx.features.external = true
149
222
  const params = Array(fn.length).fill(['param', 'f64'])
150
223
  ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
151
224
  }
152
225
  }
153
226
 
154
- let parsed = time('parse', () => parse(normalizeSource(code)))
227
+ let parsed = time('parse', () => parse(code))
155
228
  if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
156
229
  const ast = time('prepare', () => prepare(parsed))
157
230
  const module = time('compile', () => compile(ast, profiler))
158
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
+ }
246
+
159
247
  const cfg = ctx.transform.optimize
160
248
  const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module)) : module
161
249
  // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
@@ -163,13 +251,14 @@ jz.compile = (code, opts = {}) => {
163
251
  // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
164
252
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
165
253
  if (cfg.watr) {
254
+ const postCfg = { ...cfg, __phase: 'post' }
166
255
  time('watrReopt', () => {
167
- for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
256
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, postCfg)
168
257
  })
169
258
  }
170
- return opts.wat
171
- ? time('watrPrint', () => watrPrint(optimized))
172
- : time('watrCompile', () => 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
173
262
  }
174
263
 
175
264
  /**