jz 0.1.0 → 0.1.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/README.md CHANGED
@@ -17,15 +17,17 @@ dist(3, 4) // 5
17
17
 
18
18
  ## Why?
19
19
 
20
- **Write plain JS, compile to WASM** – fast, portable and long-lasting. JZ distills the modern functional core – the "good parts" [Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk) – from legacy semantics, features overhead and perf quirks.
20
+ **Write plain JS, compile to WASM** – fast, portable and long-lasting. JZ distills the modern functional core – the "good parts" [Crockford](https://www.youtube.com/watch?v=_DKkVvOt6dk) – from legacy semantics, specs evolution and perf quirks.
21
21
 
22
22
  * **Static** – no runtime, no GC, no dynamic constructs.
23
23
  * **Valid jz = valid js** — test in browser, compile to wasm.
24
24
  * **Minimal** — output is close to hand-written WAT.
25
25
  <!-- * **Realtime** — compiles faster than `eval`, useful for live-coding and REPL. -->
26
26
 
27
- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
28
- <!-- Used internally by: web-audio-api, color-space, audiojs -->
27
+ **Used by**: [web-audio-api](https://github.com/audiojs/web-audio-api), [color-space](https://github.com/colorjs/color-space), [audiojs](https://github.com/colorjs/audiojs). Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
28
+ <!-- * [audio-filter](https://github.com/audiojs/audio-filter)
29
+ * [digital-filter](https://github.com/audiojs/digital-filter)
30
+ * [time-stretch](https://github.com/audiojs/time-stretch) -->
29
31
 
30
32
  | Good for | Not for |
31
33
  |-----------------------------|----------------------------|
@@ -78,7 +80,7 @@ flowchart TB
78
80
  subgraph JS[JS — not supported]
79
81
  subgraph JZify[JZ + jzify]
80
82
  subgraph JZ[JZ strict]
81
- j1["let/const, arrows, default/rest params, flow, break/continue, try/catch, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
83
+ j1["let/const, arrows, default/rest params, flow, break/continue, try/catch/finally, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
82
84
  end
83
85
  z1["var, function, arguments, switch, new Foo(), ==, !=, instanceof"]:::plain
84
86
  end
@@ -94,17 +96,17 @@ flowchart TB
94
96
 
95
97
  ```
96
98
  ┌────────────────────────────────────────────────────────────────────────┐
97
- │ JZify test262: 68%
99
+ │ JZify
98
100
  │ var function arguments switch new Foo() │
99
101
  │ == != instanceof undefined │
100
102
  │ │
101
103
  │ ┌────────────────────────────────────────────────────────────────────┐ │
102
- │ │ JZ test262: 54% │ │
103
- │ │ let/const => ...xs destructuring import/export │ │
104
+ │ │ JZ │ │
105
+ │ │ let/const => ...xs destructuring import/export `${}` │ │
104
106
  │ │ if/else for/while/do-while/of/in break/continue │ │
105
107
  │ │ try/catch/finally throw │ │
106
- │ │ operators strings booleans numbers arrays objects `${}` │ │
107
- │ │ Math Number String Array Object JSON RegExp Symbol null │ │
108
+ │ │ operators strings booleans numbers arrays objects null │ │
109
+ │ │ Math Number String Array Object JSON RegExp Symbol │ │
108
110
  │ │ ArrayBuffer DataView TypedArray Map Set │ │
109
111
  │ │ console setTimeout/setInterval Date performance │ │
110
112
  │ └────────────────────────────────────────────────────────────────────┘ │
@@ -118,6 +120,24 @@ Not supported
118
120
  ```
119
121
 
120
122
 
123
+ ## Benchmark
124
+
125
+ | | **jz** | [Node](https://nodejs.org/) | [AS](https://github.com/AssemblyScript/assemblyscript) | WAT | C | [Go](https://go.dev/) | [Rust](https://www.rust-lang.org/) |
126
+ |---|---|---|---|---|---|---|---|
127
+ | [**biquad**](bench/biquad/biquad.js) | **6.44 ms**<br>**3.4 kB** | 12.30 ms<br>3.2 kB | 9.04 ms<br>1.9 kB | 6.48 ms<br>767 B | 5.43 ms<br>32.7 kB | 9.03 ms<br>1.60 MB<br>fma | 5.33 ms<br>380.7 kB |
128
+ | [**tokenizer**](bench/tokenizer/tokenizer.js) | **0.10 ms**<br>**1.6 kB** | 0.18 ms<br>1.4 kB | 0.08 ms<br>1.5 kB | — | 0.13 ms<br>32.9 kB | 0.07 ms<br>1.60 MB | 0.12 ms<br>380.7 kB |
129
+ | [**mat4**](bench/mat4/mat4.js) | **4.00 ms**<br>**1.7 kB** | 11.64 ms<br>1.1 kB | 9.18 ms<br>1.5 kB | 7.99 ms<br>353 B | 2.62 ms<br>32.8 kB | 11.93 ms<br>1.60 MB | 0.80 ms<br>380.7 kB |
130
+ | [**aos**](bench/aos/aos.js) | **1.50 ms**<br>**2.3 kB** | 1.81 ms<br>1.1 kB | 1.91 ms<br>2.2 kB | — | 1.22 ms<br>32.9 kB | 0.90 ms<br>1.60 MB | 1.20 ms<br>380.7 kB |
131
+ | [**bitwise**](bench/bitwise/bitwise.js) | **4.93 ms**<br>**1.2 kB** | 5.31 ms<br>1005 B | 12.36 ms<br>1.5 kB | 4.96 ms<br>355 B | 1.31 ms<br>32.9 kB | 5.24 ms<br>1.60 MB | 1.30 ms<br>380.7 kB |
132
+ | [**poly**](bench/poly/poly.js) | **1.13 ms**<br>**1.3 kB** | 2.31 ms<br>1014 B | 1.14 ms<br>1.3 kB | — | 0.52 ms<br>32.9 kB | 0.80 ms<br>1.60 MB | 0.52 ms<br>380.7 kB |
133
+ | [**callback**](bench/callback/callback.js) | **0.01 ms**<br>**1.5 kB** | 1.03 ms<br>828 B | 1.48 ms<br>1.9 kB | — | 0.09 ms<br>32.9 kB | 0.20 ms<br>1.60 MB | 0.08 ms<br>380.7 kB |
134
+ | [**json**](bench/json/json.js) | **0.20 ms**<br>**2.8 kB** | 0.38 ms<br>923 B | — | — | 0.02 ms<br>32.8 kB | 1.06 ms<br>1.97 MB | 0.03 ms<br>380.7 kB |
135
+ | [**watr**](bench/watr/watr.js) | **1.82 ms**<br>**137.1 kB** | 1.50 ms<br>2.6 kB | — | — | — | — | — |
136
+
137
+ _Numbers from `node bench/bench.mjs` on Apple Silicon, May 2026. `jz` uses host imports for benchmark timing/logging (measures wasm without WASI console/perf bloat). Raw-JS size is the entry file; jz size is the bundled wasm artifact. `fma` is the documented Go arm64 fused-multiply-add checksum class. See [benchmark](./bench/)._
138
+
139
+
140
+
121
141
 
122
142
  ## FAQ
123
143
 
@@ -139,9 +159,8 @@ memory.Array([1, 2, 3]) // → array pointer
139
159
  memory.Float64Array([1.0, 2.0]) // → typed array pointer
140
160
  memory.Int32Array([10, 20, 30]) // all typed array constructors available
141
161
 
142
- // Objects: keys and order must match the jz source declaration.
162
+ // Objects: keys and order must match the jz source declaration.
143
163
  // jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
144
- // If the jz source declares `{ x, y }`, you must pass `{ x, y }` in that order.
145
164
  memory.Object({ x: 3, y: 4 }) // → object pointer
146
165
 
147
166
  // Strings/arrays inside objects are auto-wrapped to pointers:
@@ -205,47 +224,26 @@ jz`export let f = () => ${{label: 'origin', x: 0, y: 0}}.label.length` // 6
205
224
 
206
225
  Functions are imported as host calls. Non-serializable values (host objects, class instances) fall back to post-instantiation getters automatically.
207
226
 
208
- ### Does it support ES module imports?
227
+ ### Does it support imports?
209
228
 
210
- Yes — standard ES `import` syntax is bundled at compile-time into a single WASM.
229
+ Yes — standard ES `import` syntax, bundled at compile-time into one WASM.
211
230
 
212
231
  ```js
232
+ // modules: jz source bundled at compile time
213
233
  const { exports } = jz(
214
234
  'import { add } from "./math.jz"; export let f = (a, b) => add(a, b)',
215
235
  { modules: { './math.jz': 'export let add = (a, b) => a + b' } }
216
236
  )
217
- ```
218
-
219
- Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
220
-
221
- **CLI** resolves filesystem imports automatically.
222
-
223
- ```sh
224
- jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
225
- ```
226
237
 
227
- **Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
228
-
229
- ```js
230
- // Transitive bundling — all merged into one WASM
231
- const { exports } = jz(mainSrc, { modules: {
232
- './math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
233
- // Fetch sources yourself, pass them in
234
- './utils.jz': await fetch('./util.jz').then(r => r.text())
235
- } })
236
- ```
237
-
238
- ### Can I call JS/host functions from jz?
239
-
240
- Yes — JS functions are wired at instantiation via the `imports` option:
241
-
242
- ```js
238
+ // imports: JS functions wired at instantiation
243
239
  const { exports } = jz(
244
240
  'import { log } from "host"; export let f = (x) => { log(x); return x }',
245
241
  { imports: { host: { log: console.log } } }
246
242
  )
247
243
  ```
248
244
 
245
+ Transitive imports work (main → math → utils → …). Circular imports error at compile time. Output is always one WASM binary — no runtime resolution.
246
+
249
247
  You can also pass whole host environment objects — `Math`, `Date`, `window`, `console`, or any custom namespace object. jz extracts the functions it needs via `Object.getOwnPropertyNames`, so non-enumerable built-ins (like `Math.sin`) work automatically:
250
248
 
251
249
  ```js
@@ -268,6 +266,23 @@ const { exports } = jz(
268
266
  )
269
267
  ```
270
268
 
269
+ **CLI** resolves filesystem imports automatically.
270
+
271
+ ```sh
272
+ jz main.jz -o main.wasm # reads ./math.jz, ./utils.jz automatically
273
+ ```
274
+
275
+ **Browser**: fetch sources yourself, pass via `{ modules }`. The compiler stays synchronous and pure — no I/O.
276
+
277
+ ```js
278
+ // Transitive bundling — all merged into one WASM
279
+ const { exports } = jz(mainSrc, { modules: {
280
+ './math.jz': 'import { sq } from "./utils.jz"; export let dist = (x, y) => (sq(x) + sq(y)) ** 0.5',
281
+ // Fetch sources yourself, pass them in
282
+ './utils.jz': await fetch('./util.jz').then(r => r.text())
283
+ } })
284
+ ```
285
+
271
286
  ### Can two modules share data?
272
287
 
273
288
  Yes — `jz.memory()` creates a shared memory that modules compile into. Schemas accumulate automatically, so objects created in one module are readable by another:
@@ -305,14 +320,23 @@ wasmer run program.wasm
305
320
  deno run program.wasm
306
321
  ```
307
322
 
308
- `console.log` compiles to WASI `fd_write` works natively on wasmtime/wasmer/deno without polyfills.
323
+ Pure numeric modules have no imports and instantiate with standard
324
+ `WebAssembly.Module` / `WebAssembly.Instance`, which is the right shape for JS
325
+ hosts such as EdgeJS. Compile once at startup or build time, then reuse the
326
+ module; do not compile JZ source per request.
327
+
328
+ `console.log` compiles to WASI `fd_write` by default — works natively on
329
+ wasmtime/wasmer/deno without polyfills. In JS hosts, `jz()` auto-applies the
330
+ small `jz/wasi` polyfill; pass `{ write(fd, text) { ... } }` to capture or route
331
+ stdout/stderr without depending on `process.stdout`.
309
332
 
310
333
 
311
334
  ### What host features are supported?
312
335
 
313
336
  The compiled `.wasm` uses one import namespace:
314
337
 
315
- - `wasi_snapshot_preview1`standard WASI Preview 1 calls. Run natively on wasmtime, wasmer, deno; for browsers/Node, jz ships a tiny polyfill (`jz/wasi`) auto-applied by the `jz()` runtime.
338
+ - nonepure scalar/compute modules. Instantiate directly with standard WebAssembly APIs.
339
+ - `wasi_snapshot_preview1` — standard WASI Preview 1 calls. Run natively on wasmtime, wasmer, deno; for browsers/Node/EdgeJS-like hosts, jz ships a tiny polyfill (`jz/wasi`) auto-applied by the `jz()` runtime.
316
340
 
317
341
  | JS API | Maps to | Notes |
318
342
  |--------|---------|-------|
@@ -323,21 +347,6 @@ The compiled `.wasm` uses one import namespace:
323
347
  | `setTimeout`, `clearTimeout` | WASM timer queue + `__timer_tick` | JS runtime drives tick via `setInterval`; wasmtime uses blocking `__timer_loop` |
324
348
  | `setInterval`, `clearInterval` | WASM timer queue + `__timer_tick` | Same — native WASM implementation, no host imports |
325
349
 
326
- ### How do I add custom operators / extend the stdlib?
327
-
328
- jz's emitter table (`ctx.core.emit`) maps AST operators → WASM IR generators. Module files in `module/` register handlers on it. To add your own:
329
-
330
- ```js
331
- import { emitter } from 'jz/src/compile.js'
332
-
333
- // Register a custom operator: my.double(x) → x * 2
334
- emitter['my.double'] = (x) => {
335
- return ['f64.mul', ['f64.const', 2], x]
336
- }
337
- ```
338
-
339
- The naming convention follows the AST path: `Math.sin` → `math.sin`, `arr.push` → `.push`, typed variants like `.f64:push`. See any file in `module/` for the full pattern — each exports a function that registers emitters and stdlib on `ctx`.
340
-
341
350
  ### Can I compile jz to C?
342
351
 
343
352
  Yes, via [wasm2c](https://github.com/WebAssembly/wabt/blob/main/wasm2c) or [w2c2](https://github.com/turbolent/w2c2):
@@ -349,23 +358,6 @@ cc program.c -o program
349
358
  ```
350
359
 
351
360
 
352
- ## Benchmark
353
-
354
- | | **jz** | [Node](https://nodejs.org/) | [AS](https://github.com/AssemblyScript/assemblyscript) | WAT | C | [Go](https://go.dev/) | [Zig](https://ziglang.org/) | [Rust](https://www.rust-lang.org/) | [NumPy](https://numpy.org/) | [Porffor](https://github.com/CanadaHonk/porffor) |
355
- |---|---|---|---|---|---|---|---|---|---|---|
356
- | [**biquad**](bench/biquad/biquad.js) | **6.44ms**<br>**3.4kB** | 12.30ms<br>3.2kB | 9.04ms<br>1.9kB | 6.48ms<br>767 B | 5.43ms | 9.03ms<br>fma | 5.09ms | 5.33ms | 3.15s | — |
357
- | [**tokenizer**](bench/tokenizer/tokenizer.js) | **0.10ms**<br>**1.6kB** | 0.18ms<br>1.4kB | 0.08ms<br>1.5kB | — | 0.13ms | 0.07ms | 0.12ms | 0.12ms | 5.21ms | 0.46ms<br>2.6kB |
358
- | [**mat4**](bench/mat4/mat4.js) | **4.00ms**<br>**1.7kB** | 11.64ms<br>1.1kB | 9.18ms<br>1.5kB | 7.99ms<br>353 B | 2.62ms | 11.93ms | 2.60ms | 0.80ms | 323.69ms | 87.65ms<br>2.3kB |
359
- | [**aos**](bench/aos/aos.js) | **1.50ms**<br>**2.3kB** | 1.81ms<br>1.1kB | 1.91ms<br>2.2kB | — | 1.22ms | 0.90ms | 0.99ms | 1.20ms | 2.23ms | — |
360
- | [**bitwise**](bench/bitwise/bitwise.js) | **4.93ms**<br>**1.2kB** | 5.31ms<br>1005 B | 12.36ms<br>1.5kB | 4.96ms<br>355 B | 1.31ms | 5.24ms | 4.26ms | 1.30ms | 14.89ms | — |
361
- | [**poly**](bench/poly/poly.js) | **1.13ms**<br>**1.3kB** | 2.31ms<br>1014 B | 1.14ms<br>1.3kB | — | 0.52ms | 0.80ms | — | 0.52ms | 0.60ms | — |
362
- | [**callback**](bench/callback/callback.js) | **0.01ms**<br>**1.5kB** | 1.03ms<br>828 B | 1.48ms<br>1.9kB | — | 0.09ms | 0.20ms | 0.01ms | 0.08ms | 1.84ms | — |
363
- | [**json**](bench/json/json.js) | **0.20ms**<br>**2.8kB** | 0.38ms<br>923 B | — | — | 0.02ms | 1.06ms | — | 0.03ms | 1.19ms | — |
364
- | [**watr**](bench/watr/watr.js) | **1.82ms**<br>**137.1kB** | 1.50ms<br>85.3kB | — | — | — | — | — | — | — | — |
365
-
366
- _Numbers from `node bench/bench.mjs` on Apple Silicon._
367
-
368
-
369
361
  ## Alternatives
370
362
 
371
363
  * [porffor](https://github.com/CanadaHonk/porffor) — ahead-of-time JS→WASM compiler targeting full TC39 semantics. Implements the spec progressively (test262). Where jz restricts the language for performance, porffor aims for completeness.
package/index.js CHANGED
@@ -42,9 +42,11 @@ import { parse } from 'subscript/jessie'
42
42
  import { compile as watrCompile, print as watrPrint, optimize as watrOptimize } from "watr";
43
43
  import { ctx, reset } 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'
49
+ import { normalizeSource } from './src/source.js'
48
50
  import {
49
51
  memory as enhanceMemory, instantiate as instantiateRuntime,
50
52
  } from './src/host.js'
@@ -62,6 +64,25 @@ const importsMayReturnExternal = (imports) => {
62
64
  return false
63
65
  }
64
66
 
67
+ const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
68
+
69
+ const compileProfiler = (profile) => {
70
+ if (!profile) return null
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
+
65
86
  /**
66
87
  * jz — JS subset → WASM compiler.
67
88
  *
@@ -94,9 +115,14 @@ jz.memory = enhanceMemory
94
115
  * - `3`: reserved for future aggressive passes (currently == 2).
95
116
  * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
96
117
  * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
118
+ * @param {object} [opts.profile] - Optional mutable profile sink populated with
119
+ * `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
97
120
  * @returns {Uint8Array|string}
98
121
  */
99
122
  jz.compile = (code, opts = {}) => {
123
+ const profiler = compileProfiler(opts.profile)
124
+ const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
125
+
100
126
  reset(emitter, GLOBALS)
101
127
  ctx.error.src = code
102
128
 
@@ -125,21 +151,25 @@ jz.compile = (code, opts = {}) => {
125
151
  }
126
152
  }
127
153
 
128
- let parsed = parse(code)
129
- if (opts.jzify) parsed = jzify(parsed)
130
- const ast = prepare(parsed)
131
- const module = compile(ast)
154
+ let parsed = time('parse', () => parse(normalizeSource(code)))
155
+ if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
156
+ const ast = time('prepare', () => prepare(parsed))
157
+ const module = time('compile', () => compile(ast, profiler))
132
158
 
133
159
  const cfg = ctx.transform.optimize
134
- const optimized = cfg.watr ? watrOptimize(module) : module
160
+ const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module)) : module
135
161
  // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
136
162
  // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
137
163
  // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
138
164
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
139
165
  if (cfg.watr) {
140
- for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
166
+ time('watrReopt', () => {
167
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, cfg)
168
+ })
141
169
  }
142
- return opts.wat ? watrPrint(optimized) : watrCompile(optimized)
170
+ return opts.wat
171
+ ? time('watrPrint', () => watrPrint(optimized))
172
+ : time('watrCompile', () => watrCompile(optimized))
143
173
  }
144
174
 
145
175
  /**
package/module/array.js CHANGED
@@ -8,8 +8,11 @@
8
8
  * @module array
9
9
  */
10
10
 
11
- import { emit, typed, asF64, asI32, valTypeOf, lookupValType, VAL, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, extractParams, multiCount, materializeMulti, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, updateRep } from '../src/compile.js'
11
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr } from '../src/ir.js'
12
+ import { emit, materializeMulti } from '../src/emit.js'
13
+ import { valTypeOf, lookupValType, VAL, extractParams, updateRep } from '../src/analyze.js'
12
14
  import { ctx, inc, err, PTR } from '../src/ctx.js'
15
+ import { staticPropertyKey } from '../src/key.js'
13
16
  import { strHashLiteral } from './collection.js'
14
17
 
15
18
 
@@ -466,7 +469,9 @@ export default (ctx) => {
466
469
  if (r) return r
467
470
  }
468
471
  // Literal string key on schema-known object → direct payload slot read (skip __dyn_get)
469
- const litKey = Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string' ? idx[1] : null
472
+ const litKey = Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string' ? idx[1]
473
+ : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
474
+ : null
470
475
  if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
471
476
  const slot = ctx.schema.find(arr, litKey)
472
477
  if (slot >= 0) {
@@ -8,7 +8,9 @@
8
8
  * @module collection
9
9
  */
10
10
 
11
- import { emit, emitFlat, typed, asF64, asI32, valTypeOf, lookupValType, VAL, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr } from '../src/compile.js'
11
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr } from '../src/ir.js'
12
+ import { emit, emitFlat } from '../src/emit.js'
13
+ import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
12
14
  import { inc, PTR } from '../src/ctx.js'
13
15
 
14
16
  const SET_ENTRY = 16 // hash + key
package/module/console.js CHANGED
@@ -12,7 +12,9 @@
12
12
  * @module wasi
13
13
  */
14
14
 
15
- import { emit, typed, asF64, valTypeOf, VAL } from '../src/compile.js'
15
+ import { typed, asF64 } from '../src/ir.js'
16
+ import { emit } from '../src/emit.js'
17
+ import { valTypeOf, VAL } from '../src/analyze.js'
16
18
  import { exprType } from '../src/analyze.js'
17
19
  import { inc, PTR } from '../src/ctx.js'
18
20
 
package/module/core.js CHANGED
@@ -9,7 +9,9 @@
9
9
  * @module core
10
10
  */
11
11
 
12
- import { emit, typed, asF64, asI32, valTypeOf, lookupValType, VAL, T, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, repOf, updateRep } from '../src/compile.js'
12
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish } from '../src/ir.js'
13
+ import { emit } from '../src/emit.js'
14
+ import { valTypeOf, lookupValType, VAL, T, repOf, updateRep } from '../src/analyze.js'
13
15
  import { err, inc, PTR } from '../src/ctx.js'
14
16
  import { initSchema } from './schema.js'
15
17
  import { strHashLiteral } from './collection.js'
@@ -10,13 +10,14 @@
10
10
  * @module fn
11
11
  */
12
12
 
13
- import { emit, typed, asF64, asI32, T, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN, lookupValType, repOf } from '../src/compile.js'
14
- import { isReassigned } from '../src/emit.js'
13
+ import { typed, asF64, asI32, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN } from '../src/ir.js'
14
+ import { emit, isReassigned } from '../src/emit.js'
15
+ import { T, lookupValType, repOf } from '../src/analyze.js'
15
16
  import { PTR, inc, err } from '../src/ctx.js'
16
17
 
17
18
 
18
19
  export default (ctx) => {
19
- inc('__mkptr', '__alloc', '__len', '__ptr_offset')
20
+ inc('__mkptr', '__alloc', '__len', '__ptr_offset', '__ptr_type')
20
21
 
21
22
  // Uniform closure convention: (env f64, argc i32, a0..a{MAX-1} f64) → f64
22
23
  if (!ctx.closure.types) ctx.closure.types = new Set()
package/module/json.js CHANGED
@@ -8,7 +8,9 @@
8
8
  * @module json
9
9
  */
10
10
 
11
- import { emit, typed, asF64, T, temp, nullExpr, allocPtr, slotAddr } from '../src/compile.js'
11
+ import { typed, asF64, temp, nullExpr, allocPtr, slotAddr } from '../src/ir.js'
12
+ import { emit } from '../src/emit.js'
13
+ import { T } from '../src/analyze.js'
12
14
  import { err, inc, PTR } from '../src/ctx.js'
13
15
  import { strHashLiteral } from './collection.js'
14
16
 
package/module/math.js CHANGED
@@ -13,7 +13,8 @@
13
13
  * @module math
14
14
  */
15
15
 
16
- import { emit, typed, asF64, asI32, temp, arrayLoop } from '../src/compile.js'
16
+ import { typed, asF64, asI32, temp, arrayLoop } from '../src/ir.js'
17
+ import { emit } from '../src/emit.js'
17
18
  import { inc } from '../src/ctx.js'
18
19
  import { repOf } from '../src/analyze.js'
19
20
 
package/module/number.js CHANGED
@@ -9,7 +9,9 @@
9
9
  * @module number
10
10
  */
11
11
 
12
- import { emit, typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, valTypeOf, VAL, temp, tempI32, tempI64 } from '../src/compile.js'
12
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64 } from '../src/ir.js'
13
+ import { emit } from '../src/emit.js'
14
+ import { valTypeOf, VAL } from '../src/analyze.js'
13
15
  import { inc, PTR } from '../src/ctx.js'
14
16
 
15
17
  export default (ctx) => {
@@ -557,12 +559,6 @@ export default (ctx) => {
557
559
  ['i32.const', 1]]]]], 'f64')
558
560
  }
559
561
 
560
- ctx.core.emit['String'] = (x) => {
561
- inc('__ftoa')
562
- if (Array.isArray(x) && x[0] === 'str') return emit(x)
563
- return typed(['call', '$__ftoa', asF64(emit(x)), ['i32.const', 0], ['i32.const', 0]], 'f64')
564
- }
565
-
566
562
  // Number(x) — identity for numbers, i64→f64 conversion for BigInt
567
563
  ctx.core.emit['Number'] = (x) => {
568
564
  if (valTypeOf(x) === VAL.BIGINT)
package/module/object.js CHANGED
@@ -7,7 +7,9 @@
7
7
  * @module object
8
8
  */
9
9
 
10
- import { emit, typed, asF64, valTypeOf, lookupValType, VAL, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, repOf, updateRep } from '../src/compile.js'
10
+ import { typed, asF64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr } from '../src/ir.js'
11
+ import { emit } from '../src/emit.js'
12
+ import { valTypeOf, lookupValType, VAL, repOf, updateRep } from '../src/analyze.js'
11
13
  import { ctx, err, inc, PTR } from '../src/ctx.js'
12
14
 
13
15
 
package/module/regex.js CHANGED
@@ -7,7 +7,8 @@
7
7
  * @module regex
8
8
  */
9
9
 
10
- import { emit, typed, asF64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/compile.js'
10
+ import { typed, asF64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
+ import { emit } from '../src/emit.js'
11
12
  import { err, inc, PTR } from '../src/ctx.js'
12
13
 
13
14
  // Build IR that constructs a match array: [full, cap1, cap2, ...]
package/module/schema.js CHANGED
@@ -7,7 +7,9 @@
7
7
  * @module schema
8
8
  */
9
9
 
10
- import { emit, typed, asF64, VAL, lookupValType, repOf } from '../src/compile.js'
10
+ import { typed, asF64 } from '../src/ir.js'
11
+ import { emit } from '../src/emit.js'
12
+ import { VAL, lookupValType, repOf } from '../src/analyze.js'
11
13
  import { err, inc } from '../src/ctx.js'
12
14
 
13
15
  /** Initialize schema helpers on ctx. Called once per compilation from core module. */
package/module/string.js CHANGED
@@ -10,7 +10,9 @@
10
10
  * @module string
11
11
  */
12
12
 
13
- import { emit, typed, asF64, asI32, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/compile.js'
13
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
14
+ import { emit } from '../src/emit.js'
15
+ import { valTypeOf, VAL } from '../src/analyze.js'
14
16
  import { inc, PTR } from '../src/ctx.js'
15
17
 
16
18
 
@@ -52,7 +54,8 @@ export default (ctx) => {
52
54
  for (let i = 0; i < str.length; i++) packed |= str.charCodeAt(i) << (i * 8)
53
55
  return mkPtrIR(PTR.SSO, str.length, packed)
54
56
  }
55
- const len = str.length
57
+ const bytes = new TextEncoder().encode(str)
58
+ const len = bytes.length
56
59
  if (!ctx.memory.shared) {
57
60
  // Own memory: place in static data segment (no runtime allocation)
58
61
  if (!ctx.runtime.data) ctx.runtime.data = ''
@@ -61,7 +64,7 @@ export default (ctx) => {
61
64
  while (ctx.runtime.data.length % 4 !== 0) ctx.runtime.data += '\0'
62
65
  const offset = ctx.runtime.data.length
63
66
  ctx.runtime.data += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
64
- for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(str.charCodeAt(i))
67
+ for (let i = 0; i < len; i++) ctx.runtime.data += String.fromCharCode(bytes[i])
65
68
  ctx.runtime.dataDedup.set(str, offset)
66
69
  return mkPtrIR(PTR.STRING, 0, offset + 4)
67
70
  }
@@ -75,10 +78,10 @@ export default (ctx) => {
75
78
  }
76
79
  let off = ctx.runtime.strPoolDedup.get(str)
77
80
  if (off === undefined) {
78
- // Pack length header then bytes; offset points PAST the length (at the data).
81
+ // Pack length header then UTF-8 bytes; offset points PAST the length (at the data).
79
82
  ctx.runtime.strPool += String.fromCharCode(len & 0xFF, (len >> 8) & 0xFF, (len >> 16) & 0xFF, (len >> 24) & 0xFF)
80
83
  off = ctx.runtime.strPool.length
81
- ctx.runtime.strPool += str
84
+ for (let i = 0; i < len; i++) ctx.runtime.strPool += String.fromCharCode(bytes[i])
82
85
  ctx.runtime.strPoolDedup.set(str, off)
83
86
  }
84
87
  return mkPtrIR(PTR.STRING, 0, ['i32.add', ['global.get', '$__strBase'], ['i32.const', off]])
@@ -486,12 +489,19 @@ export default (ctx) => {
486
489
  (br $loop)))
487
490
  (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $off)))`
488
491
 
489
- // Coerce value to string: numbers → __ftoa, plain NaN "NaN", arrays → join(","), other pointers pass through
492
+ // Coerce value to string: numbers → __ftoa, nullishstatic strings,
493
+ // plain NaN → "NaN", arrays → join(","), other string-like pointers pass through.
490
494
  ctx.core.stdlib['__to_str'] = `(func $__to_str (param $val f64) (result f64)
491
495
  (local $type i32)
496
+ (local $bits i64)
492
497
  ;; Not NaN → number, convert
493
498
  (if (f64.eq (local.get $val) (local.get $val))
494
499
  (then (return (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))
500
+ (local.set $bits (i64.reinterpret_f64 (local.get $val)))
501
+ (if (i64.eq (local.get $bits) (i64.const ${NULL_NAN}))
502
+ (then (return (call $__static_str (i32.const 5)))))
503
+ (if (i64.eq (local.get $bits) (i64.const ${UNDEF_NAN}))
504
+ (then (return (call $__static_str (i32.const 6)))))
495
505
  (local.set $type (call $__ptr_type (local.get $val)))
496
506
  ;; Plain NaN (type=0) → "NaN" string
497
507
  (if (i32.eqz (local.get $type))
@@ -774,6 +784,42 @@ export default (ctx) => {
774
784
  return result
775
785
  }
776
786
 
787
+ ctx.core.emit['strcat'] = (...parts) => {
788
+ inc('__to_str', '__str_byteLen', '__alloc', '__mkptr', '__str_copy')
789
+ if (!parts.length) return mkPtrIR(PTR.SSO, 0, 0)
790
+ if (parts.length === 1) return typed(['call', '$__to_str', asF64(emit(parts[0]))], 'f64')
791
+
792
+ const vals = parts.map(() => temp('s'))
793
+ const lens = parts.map(() => tempI32('sl'))
794
+ const total = tempI32('st')
795
+ const off = tempI32('so')
796
+ const dst = tempI32('sd')
797
+ const ir = []
798
+
799
+ for (let i = 0; i < parts.length; i++) {
800
+ ir.push(['local.set', `$${vals[i]}`, ['call', '$__to_str', asF64(emit(parts[i]))]])
801
+ ir.push(['local.set', `$${lens[i]}`, ['call', '$__str_byteLen', ['local.get', `$${vals[i]}`]]])
802
+ }
803
+ ir.push(['local.set', `$${total}`, ['i32.const', 0]])
804
+ for (const len of lens)
805
+ ir.push(['local.set', `$${total}`, ['i32.add', ['local.get', `$${total}`], ['local.get', `$${len}`]]])
806
+ const alloc = [
807
+ ['local.set', `$${off}`, ['call', '$__alloc', ['i32.add', ['i32.const', 4], ['local.get', `$${total}`]]]],
808
+ ['i32.store', ['local.get', `$${off}`], ['local.get', `$${total}`]],
809
+ ['local.set', `$${off}`, ['i32.add', ['local.get', `$${off}`], ['i32.const', 4]]],
810
+ ['local.set', `$${dst}`, ['local.get', `$${off}`]],
811
+ ]
812
+ for (let i = 0; i < parts.length; i++) {
813
+ alloc.push(['call', '$__str_copy', ['local.get', `$${vals[i]}`], ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]])
814
+ alloc.push(['local.set', `$${dst}`, ['i32.add', ['local.get', `$${dst}`], ['local.get', `$${lens[i]}`]]])
815
+ }
816
+ alloc.push(['call', '$__mkptr', ['i32.const', PTR.STRING], ['i32.const', 0], ['local.get', `$${off}`]])
817
+ ir.push(['if', ['result', 'f64'], ['i32.eqz', ['local.get', `$${total}`]],
818
+ ['then', mkPtrIR(PTR.SSO, 0, 0)],
819
+ ['else', ['block', ['result', 'f64'], ...alloc]]])
820
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
821
+ }
822
+
777
823
  ctx.core.emit['.padStart'] = (str, len, pad) => {
778
824
  inc('__str_pad')
779
825
  const vpad = pad != null ? asF64(emit(pad)) : mkPtrIR(PTR.SSO, 1, 32)
@@ -806,6 +852,17 @@ export default (ctx) => {
806
852
  }
807
853
 
808
854
  // String.fromCharCode(code) → 1-char SSO string
855
+ ctx.core.emit['String'] = (value) => {
856
+ if (value === undefined) return emit(['str', ''])
857
+ if (valTypeOf(value) === VAL.STRING) return emit(value)
858
+ if (valTypeOf(value) === VAL.NUMBER) {
859
+ inc('__ftoa')
860
+ return typed(['call', '$__ftoa', asF64(emit(value)), ['i32.const', 0], ['i32.const', 0]], 'f64')
861
+ }
862
+ inc('__to_str')
863
+ return typed(['call', '$__to_str', asF64(emit(value))], 'f64')
864
+ }
865
+
809
866
  ctx.core.emit['String.fromCharCode'] = (code) => mkPtrIR(PTR.SSO, 1, asI32(emit(code)))
810
867
 
811
868
  // String.fromCodePoint(cp) → UTF-8 encoded string
package/module/symbol.js CHANGED
@@ -17,7 +17,8 @@
17
17
  * @module symbol
18
18
  */
19
19
 
20
- import { emit, typed, asF64, mkPtrIR } from '../src/compile.js'
20
+ import { typed, asF64, mkPtrIR } from '../src/ir.js'
21
+ import { emit } from '../src/emit.js'
21
22
  import { err, inc, PTR } from '../src/ctx.js'
22
23
 
23
24
  const RESERVED = 16 // first user atom ID
package/module/timer.js CHANGED
@@ -17,9 +17,9 @@
17
17
  * @module timer
18
18
  */
19
19
 
20
- import { emit, typed, asF64, UNDEF_NAN, MAX_CLOSURE_ARITY } from '../src/compile.js'
20
+ import { typed, asF64, UNDEF_NAN, MAX_CLOSURE_ARITY, temp } from '../src/ir.js'
21
+ import { emit } from '../src/emit.js'
21
22
  import { inc, PTR } from '../src/ctx.js'
22
- import { temp } from '../src/ir.js'
23
23
 
24
24
  const MAX_TIMERS = 64
25
25
  const ENTRY_SIZE = 40