jz 0.1.1 → 0.2.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.
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,10 @@ 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. Prefer `memory: N` for owned memory.
167
+ * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
168
+ * (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
110
169
  * modules that only call exported wasm functions.
111
170
  * @param {boolean|number|object} [opts.optimize] - Optimization level/config.
112
171
  * - `false` / `0`: nothing. Fastest compile, largest output (live coding).
@@ -117,6 +176,11 @@ jz.memory = enhanceMemory
117
176
  * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
118
177
  * @param {object} [opts.profile] - Optional mutable profile sink populated with
119
178
  * `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
179
+ * Set `profile.names = true` to also emit a standard wasm `name` custom section
180
+ * for profiler/debugger symbolication.
181
+ * @param {boolean} [opts.profileNames] - Legacy alias for `profile.names`.
182
+ * @param {string} [opts.importMetaUrl] - Module URL used to lower `import.meta.url`
183
+ * and static `import.meta.resolve("...")` expressions.
120
184
  * @returns {Uint8Array|string}
121
185
  */
122
186
  jz.compile = (code, opts = {}) => {
@@ -126,8 +190,8 @@ jz.compile = (code, opts = {}) => {
126
190
  reset(emitter, GLOBALS)
127
191
  ctx.error.src = code
128
192
 
129
- if (opts.memory) ctx.memory.shared = true
130
- if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
193
+ if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
194
+ else if (opts.memory) ctx.memory.shared = true
131
195
  if (opts.modules) ctx.module.importSources = opts.modules
132
196
  if (opts.imports) {
133
197
  ctx.module.hostImports = opts.imports
@@ -138,24 +202,45 @@ jz.compile = (code, opts = {}) => {
138
202
  if (opts.jzify) ctx.transform.jzify = jzify
139
203
  if (opts.noTailCall) ctx.transform.noTailCall = true
140
204
  if (opts.strict) ctx.transform.strict = true
141
- if (opts.runtimeExports === false) ctx.transform.runtimeExports = false
205
+ if (opts.host) {
206
+ if (opts.host !== 'js' && opts.host !== 'wasi') err(`Invalid host '${opts.host}'. Expected 'js' or 'wasi'.`)
207
+ ctx.transform.host = opts.host
208
+ }
209
+ if (opts.alloc === false) ctx.transform.alloc = false
210
+ if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
142
211
  if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
143
212
  ctx.transform.optimize = resolveOptimize(opts.optimize)
144
213
 
145
214
  if (opts._interp) {
146
215
  for (const [name, fn] of Object.entries(opts._interp)) {
147
216
  if (name.startsWith('__ext_')) continue;
217
+ 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
218
  ctx.features.external = true
149
219
  const params = Array(fn.length).fill(['param', 'f64'])
150
220
  ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
151
221
  }
152
222
  }
153
223
 
154
- let parsed = time('parse', () => parse(normalizeSource(code)))
224
+ let parsed = time('parse', () => parse(code))
155
225
  if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
156
226
  const ast = time('prepare', () => prepare(parsed))
157
227
  const module = time('compile', () => compile(ast, profiler))
158
228
 
229
+ // host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
230
+ // only to defer to a JS host's value-aware semantics; in a wasmtime/wasmer/deno
231
+ // sandbox the imports either go unsatisfied or are stubbed out and silently produce
232
+ // wrong output. Surface the gap at compile so the caller can pick a comparator,
233
+ // type-annotate the receiver, or wait for native lowering. Read `extImports`
234
+ // (populated in pullStdlib) — `core.includes` has had these removed by then.
235
+ if (ctx.transform.host === 'wasi' && ctx.core.extImports?.size) {
236
+ const ext = [...ctx.core.extImports].sort()
237
+ err(
238
+ `host: 'wasi' — compiled wasm would require JS-host imports that wasmtime/wasmer/deno cannot satisfy:\n ` +
239
+ ext.map(n => `env.${n}`).join('\n ') +
240
+ `\nThis happens when jz falls through to dynamic dispatch for a method or property without a native lowering. ` +
241
+ `Either annotate the receiver type, switch to a natively-supported method, or compile with the default host.`)
242
+ }
243
+
159
244
  const cfg = ctx.transform.optimize
160
245
  const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module)) : module
161
246
  // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
@@ -163,13 +248,14 @@ jz.compile = (code, opts = {}) => {
163
248
  // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
164
249
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
165
250
  if (cfg.watr) {
251
+ const postCfg = { ...cfg, __phase: 'post' }
166
252
  time('watrReopt', () => {
167
- for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
253
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, postCfg)
168
254
  })
169
255
  }
170
- return opts.wat
171
- ? time('watrPrint', () => watrPrint(optimized))
172
- : time('watrCompile', () => watrCompile(optimized))
256
+ if (opts.wat) return time('watrPrint', () => watrPrint(optimized))
257
+ const wasm = time('watrCompile', () => watrCompile(optimized))
258
+ return (opts.profileNames || opts.profile?.names) ? appendFunctionNames(wasm, optimized) : wasm
173
259
  }
174
260
 
175
261
  /**