jz 0.3.0 → 0.4.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/README.md +24 -14
- package/cli.js +37 -8
- package/index.js +32 -15
- package/module/array.js +16 -7
- package/module/collection.js +28 -13
- package/module/core.js +16 -1
- package/module/json.js +8 -8
- package/module/math.js +21 -4
- package/module/object.js +104 -9
- package/module/string.js +15 -0
- package/package.json +7 -4
- package/src/analyze.js +2 -2
- package/src/assemble.js +31 -8
- package/src/codegen.js +177 -0
- package/src/compile.js +21 -12
- package/src/emit.js +6 -1
- package/src/fuse.js +159 -0
- package/src/ir.js +11 -4
- package/src/jzify.js +165 -178
- package/src/narrow.js +36 -9
- package/src/optimize.js +182 -22
- package/src/plan.js +10 -1
- package/src/prepare.js +2 -149
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ JZ distills the modern functional core – the "good parts" ([Crockford](https:/
|
|
|
22
22
|
|
|
23
23
|
* **Static AOT** – no runtime, no GC, no dynamic constructs.
|
|
24
24
|
* **Valid jz = valid js** — test in browser, compile to wasm.
|
|
25
|
-
* **Minimal** — output is close to hand-written WAT.
|
|
25
|
+
* **Minimal** — output is close to hand-written WAT; CI gates it to stay at least as small and fast as AssemblyScript and Porffor on the bench corpus ([CONTRIBUTING](CONTRIBUTING.md#performance--size-invariant)).
|
|
26
26
|
<!-- * **Realtime** — compiles faster than `eval`, useful for live-coding and REPL. -->
|
|
27
27
|
|
|
28
28
|
| Good for | Not for |
|
|
@@ -82,7 +82,15 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
|
|
|
82
82
|
|
|
83
83
|
```sh
|
|
84
84
|
# Compile
|
|
85
|
-
jz program.js
|
|
85
|
+
jz program.js # → program.wasm
|
|
86
|
+
jz program.js --wat # → program.wat
|
|
87
|
+
jz program.js -o out.wasm # custom output (- for stdout)
|
|
88
|
+
|
|
89
|
+
# Optimization level: -O0 off, -O1 size, -O2 balanced, -O3 speed
|
|
90
|
+
jz program.js -O3
|
|
91
|
+
|
|
92
|
+
# Runtime-service lowering: js (default) or wasi
|
|
93
|
+
jz program.js --host wasi
|
|
86
94
|
|
|
87
95
|
# Evaluate
|
|
88
96
|
jz -e "1 + 2" # 3
|
|
@@ -91,6 +99,7 @@ jz -e "1 + 2" # 3
|
|
|
91
99
|
jz --help
|
|
92
100
|
```
|
|
93
101
|
|
|
102
|
+
|
|
94
103
|
## Language
|
|
95
104
|
|
|
96
105
|
JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
|
|
@@ -467,18 +476,19 @@ cc program.c -o program
|
|
|
467
476
|
|
|
468
477
|
| | jz | [Node](https://nodejs.org/) | [Porffor](https://github.com/CanadaHonk/porffor) | [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/) |
|
|
469
478
|
|---|---|---|---|---|---|---|---|---|---|---|
|
|
470
|
-
| [biquad](bench/biquad/biquad.js) |
|
|
471
|
-
| [tokenizer](bench/tokenizer/tokenizer.js) | 0.
|
|
472
|
-
| [mat4](bench/mat4/mat4.js) | 2.12ms<br>3.
|
|
473
|
-
| [aos](bench/aos/aos.js) | 1.
|
|
474
|
-
| [mandelbrot](bench/mandelbrot/mandelbrot.js) |
|
|
475
|
-
| [bitwise](bench/bitwise/bitwise.js) |
|
|
476
|
-
| [poly](bench/poly/poly.js) | 0.
|
|
477
|
-
| [callback](bench/callback/callback.js) | 0.
|
|
478
|
-
| [json](bench/json/json.js) | 0.
|
|
479
|
-
| [
|
|
480
|
-
|
|
481
|
-
|
|
479
|
+
| [biquad](bench/biquad/biquad.js) | 6.40ms<br>4.1kB | 12.02ms<br>3.2kB | fails | 8.88ms<br>1.9kB | 6.44ms<br>767 B | 5.30ms | 8.96ms<br>fma | 5.04ms | 5.27ms | 3.09s |
|
|
480
|
+
| [tokenizer](bench/tokenizer/tokenizer.js) | 0.10ms<br>1.9kB | 0.20ms<br>2.0kB | 0.41ms<br>3.2kB | 0.08ms<br>1.6kB | 0.10ms<br>344 B | 0.13ms | 0.08ms | 0.14ms | 0.12ms | 5.13ms |
|
|
481
|
+
| [mat4](bench/mat4/mat4.js) | 2.12ms<br>3.0kB | 11.90ms<br>1.2kB | 88.68ms<br>2.4kB<br>diff | 9.44ms<br>1.6kB | 8.10ms<br>414 B | 2.76ms | 12.51ms | 2.74ms | 1.78ms | 389.44ms |
|
|
482
|
+
| [aos](bench/aos/aos.js) | 1.61ms<br>2.0kB | 1.82ms<br>1.1kB | fails | 1.91ms<br>2.2kB | 1.07ms<br>481 B | 1.20ms | 0.90ms | 0.90ms | 1.20ms | 2.55ms |
|
|
483
|
+
| [mandelbrot](bench/mandelbrot/mandelbrot.js) | 12.24ms<br>1.2kB | 13.53ms<br>1.8kB | 13.47ms<br>3.0kB | 12.23ms<br>1.3kB | — | 12.26ms | 12.46ms | 12.31ms | 12.23ms | — |
|
|
484
|
+
| [bitwise](bench/bitwise/bitwise.js) | 1.62ms<br>1.3kB | 5.25ms<br>1005 B | fails | 11.97ms<br>1.5kB | 4.95ms<br>355 B | 1.30ms | 5.23ms | 4.16ms | 1.30ms | 14.77ms |
|
|
485
|
+
| [poly](bench/poly/poly.js) | 0.37ms<br>1.4kB | 2.29ms<br>1014 B | fails | 1.13ms<br>1.3kB | 0.81ms<br>359 B | 0.52ms | 0.80ms | 0.80ms | 0.57ms | 0.61ms |
|
|
486
|
+
| [callback](bench/callback/callback.js) | 0.05ms<br>1.5kB | 0.86ms<br>828 B | fails | 1.48ms<br>1.9kB | 0.25ms<br>267 B | 0.10ms | 0.20ms | 0.01ms | 0.09ms | 1.81ms |
|
|
487
|
+
| [json](bench/json/json.js) | 0.23ms<br>10.9kB | 0.37ms<br>1.2kB | fails | — | — | 0.21ms | 1.17ms | 0.69ms | 0.68ms | 1.20ms |
|
|
488
|
+
| [sort](bench/sort/sort.js) | 8.44ms<br>1.9kB | 9.98ms<br>1.6kB | fails | 10.18ms<br>1.9kB | — | 8.85ms | 10.36ms | 8.84ms | 9.37ms | 5.05ms |
|
|
489
|
+
| [crc32](bench/crc32/crc32.js) | 11.96ms<br>1.5kB | 13.35ms<br>1.8kB | 80.76ms<br>3.1kB | 12.04ms<br>1.4kB | — | 10.69ms | 9.30ms | 9.45ms | 9.38ms | 0.24ms |
|
|
490
|
+
|
|
491
|
+
_Numbers from `node bench/bench.mjs` on Apple Silicon._ `optimize: 'size'|'speed'|'balanced'` provides a size/speed tradeoff leverage.
|
|
482
492
|
|
|
483
493
|
<details>
|
|
484
494
|
<summary><strong>Optimizations</strong></summary>
|
package/cli.js
CHANGED
|
@@ -10,7 +10,8 @@ import { pathToFileURL } from 'url'
|
|
|
10
10
|
import { execFileSync } from 'child_process'
|
|
11
11
|
import { parse } from 'subscript/feature/jessie'
|
|
12
12
|
import jz, { compile } from './index.js'
|
|
13
|
-
import jzifyFn
|
|
13
|
+
import jzifyFn from './src/jzify.js'
|
|
14
|
+
import { codegen } from './src/codegen.js'
|
|
14
15
|
import { createRequire } from 'module'
|
|
15
16
|
|
|
16
17
|
const jzRequire = createRequire(import.meta.url)
|
|
@@ -32,13 +33,21 @@ Examples:
|
|
|
32
33
|
jz program.js --wat # → program.wat
|
|
33
34
|
jz program.js -o out.wasm # custom output name
|
|
34
35
|
jz program.js -o - # write to stdout
|
|
36
|
+
jz program.js -O3 # aggressive optimization
|
|
37
|
+
jz program.js -Os # optimize for size
|
|
38
|
+
jz program.js --host wasi # emit WASI Preview 1 imports
|
|
35
39
|
jz --strict program.js # strict mode
|
|
36
40
|
jz --jzify lib.js # → lib.jz
|
|
37
41
|
jz -e "1 + 2"
|
|
38
42
|
|
|
39
43
|
Options:
|
|
40
44
|
--output, -o <file> Output file (.wat, .wasm, or - for stdout)
|
|
41
|
-
--
|
|
45
|
+
-O<n>, --optimize <n> Optimization level: 0 off, 1 size-only, 2 default,
|
|
46
|
+
3 aggressive. Aliases: -Os/size, -Ob/balanced, -Of/speed.
|
|
47
|
+
--host <js|wasi> Runtime-service lowering (default js)
|
|
48
|
+
--no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
|
|
49
|
+
--names Emit wasm name section for profilers/debuggers
|
|
50
|
+
--strict Strict jz mode (no auto-transform), reject dynamic fallbacks
|
|
42
51
|
--jzify Transform JS to jz (no compilation)
|
|
43
52
|
--eval, -e Evaluate expression or file
|
|
44
53
|
--wat Output WAT text instead of binary
|
|
@@ -109,16 +118,31 @@ async function handleJzify(args) {
|
|
|
109
118
|
}
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
// -O<n>/-Os/-Ob/-Of and --optimize <val> → value accepted by compile()'s `optimize` opt
|
|
122
|
+
const OPT_ALIAS = { s: 'size', b: 'balanced', f: 'speed' }
|
|
123
|
+
function parseOptimize(v) {
|
|
124
|
+
if (v == null) return undefined
|
|
125
|
+
if (/^\d+$/.test(v)) return +v
|
|
126
|
+
return OPT_ALIAS[v] ?? v
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
async function handleCompile(args) {
|
|
113
130
|
let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false, importsFile = null
|
|
131
|
+
let optimize, host, alloc = true, names = false
|
|
114
132
|
|
|
115
133
|
for (let i = 0; i < args.length; i++) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
else if (
|
|
119
|
-
else if (
|
|
120
|
-
else if (
|
|
121
|
-
else if (
|
|
134
|
+
const a = args[i]
|
|
135
|
+
if (a === '--output' || a === '-o') outputFile = args[++i]
|
|
136
|
+
else if (a === '--wat') wat = true
|
|
137
|
+
else if (a === '--strict') strict = true
|
|
138
|
+
else if (a === '--resolve') resolveNode = true
|
|
139
|
+
else if (a === '--imports') importsFile = args[++i]
|
|
140
|
+
else if (a === '--optimize' || a === '-O') optimize = parseOptimize(args[++i])
|
|
141
|
+
else if (/^-O.+/.test(a)) optimize = parseOptimize(a.slice(2))
|
|
142
|
+
else if (a === '--host') host = args[++i]
|
|
143
|
+
else if (a === '--no-alloc') alloc = false
|
|
144
|
+
else if (a === '--names') names = true
|
|
145
|
+
else if (!inputFile) inputFile = a
|
|
122
146
|
}
|
|
123
147
|
|
|
124
148
|
if (!inputFile) throw new Error('No input file specified')
|
|
@@ -178,7 +202,12 @@ async function handleCompile(args) {
|
|
|
178
202
|
const opts = {
|
|
179
203
|
wat,
|
|
180
204
|
jzify: !strict && !inputFile.endsWith('.jz'),
|
|
205
|
+
strict,
|
|
181
206
|
importMetaUrl: pathToFileURL(resolve(inputFile)).href,
|
|
207
|
+
...(optimize !== undefined && { optimize }),
|
|
208
|
+
...(host && { host }),
|
|
209
|
+
...(alloc === false && { alloc: false }),
|
|
210
|
+
...(names && { profileNames: true }),
|
|
182
211
|
...(Object.keys(modules).length && { modules }),
|
|
183
212
|
}
|
|
184
213
|
|
package/index.js
CHANGED
|
@@ -51,18 +51,11 @@ import {
|
|
|
51
51
|
memory as enhanceMemory, instantiate as instantiateRuntime,
|
|
52
52
|
} from './src/host.js'
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const importsMayReturnExternal = (imports) => {
|
|
60
|
-
if (!imports) return false
|
|
61
|
-
for (const mod of Object.values(imports))
|
|
62
|
-
for (const spec of Object.values(mod || {}))
|
|
63
|
-
if (importSpecMayReturnExternal(spec)) return true
|
|
64
|
-
return false
|
|
65
|
-
}
|
|
54
|
+
// A host import that's a JS function may hand back any value, including a host
|
|
55
|
+
// object — which arrives in wasm as a PTR.EXTERNAL ref. Constants/typed specs can't.
|
|
56
|
+
const importsMayReturnExternal = (imports) =>
|
|
57
|
+
!!imports && Object.values(imports).some(mod =>
|
|
58
|
+
Object.values(mod || {}).some(spec => typeof spec === 'function'))
|
|
66
59
|
|
|
67
60
|
const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
|
|
68
61
|
|
|
@@ -227,8 +220,13 @@ jz.compile = (code, opts = {}) => {
|
|
|
227
220
|
const ast = time('prepare', () => prepare(parsed))
|
|
228
221
|
|
|
229
222
|
// Auto-detect optimization tuning from source characteristics when the user
|
|
230
|
-
// hasn't provided any optimize option.
|
|
231
|
-
|
|
223
|
+
// hasn't provided any optimize option. At the default level its only *live*
|
|
224
|
+
// output is the typed-array scalarization thresholds (every other override —
|
|
225
|
+
// watr off, sortStrPool/hoistPtr on — already matches the level-2 preset), so
|
|
226
|
+
// skip the AST scan entirely unless the program touches typed arrays.
|
|
227
|
+
// NOTE: if the level-2 default ever flips watr on, drop this guard so the
|
|
228
|
+
// machine-generated-code heuristic in detectOptimizeConfig can take effect.
|
|
229
|
+
if (opts.optimize == null && ctx.module.modules.typedarray) {
|
|
232
230
|
const autoCfg = detectOptimizeConfig(ast, code)
|
|
233
231
|
if (Object.keys(autoCfg).length) {
|
|
234
232
|
ctx.transform.optimize = resolveOptimize(autoCfg)
|
|
@@ -253,7 +251,26 @@ jz.compile = (code, opts = {}) => {
|
|
|
253
251
|
}
|
|
254
252
|
|
|
255
253
|
const cfg = ctx.transform.optimize
|
|
256
|
-
|
|
254
|
+
// watr's `loopify` collapses the `(block $brk (loop … (br_if $brk !cond) … (br $loop)))`
|
|
255
|
+
// idiom into `(loop … (if cond …))` — sound, but destroys the exact shape jz's
|
|
256
|
+
// post-watr vectorizer scans for. Disable loopify when vectorize is going to run.
|
|
257
|
+
//
|
|
258
|
+
// watr config:
|
|
259
|
+
// true / 'full' → all default passes (includes `inlineOnce` / `inline` / `coalesce`)
|
|
260
|
+
// 'light' → everything except inlining + coalesce. Default at level 2.
|
|
261
|
+
// `inlineOnce` reshapes codegen the way slot-type / LICM tests scan for,
|
|
262
|
+
// and miscompiles regex `split(/\s+/)`. `coalesceLocals` on its own (i.e.
|
|
263
|
+
// without the cleanup that follows inlining) breaks `/a.+b/.test("ab")`
|
|
264
|
+
// — likely a stale alias the post-inline propagate sweep normally tidies
|
|
265
|
+
// up at L3. Dropping both still keeps treeshake / dedupe / dedupTypes /
|
|
266
|
+
// propagate / packData / fold / peephole / vacuum / mergeBlocks / brif /
|
|
267
|
+
// loopify / offset / unbranch / identity / strength / globals etc.
|
|
268
|
+
// false → skip watr entirely (level 0/1).
|
|
269
|
+
let watrOpts
|
|
270
|
+
if (cfg.watr === 'light') watrOpts = { inline: false, inlineOnce: false, coalesce: false, loopify: !cfg.vectorizeLaneLocal }
|
|
271
|
+
else if (cfg.vectorizeLaneLocal) watrOpts = { loopify: false }
|
|
272
|
+
else watrOpts = true
|
|
273
|
+
const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module, watrOpts)) : module
|
|
257
274
|
// Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
|
|
258
275
|
// (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
|
|
259
276
|
// `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
|
package/module/array.js
CHANGED
|
@@ -251,7 +251,13 @@ export default (ctx) => {
|
|
|
251
251
|
ctx.core.emit['new.Array'] = (len) => {
|
|
252
252
|
const n = tempI32('alen')
|
|
253
253
|
const nIR = ['local.get', `$${n}`]
|
|
254
|
-
|
|
254
|
+
// L3/'speed' bumps the cap floor to skip the first growth cycles (default 0
|
|
255
|
+
// → grow on first push). Length stays exactly what the user requested.
|
|
256
|
+
const minCap = ctx.transform.optimize?.arrayMinCap | 0
|
|
257
|
+
const capIR = minCap > 0
|
|
258
|
+
? ['select', ['i32.const', minCap], nIR, ['i32.gt_s', ['i32.const', minCap], nIR]]
|
|
259
|
+
: nIR
|
|
260
|
+
const out = allocPtr({ type: PTR.ARRAY, len: nIR, cap: capIR, tag: 'newarr' })
|
|
255
261
|
return typed(['block', ['result', 'f64'],
|
|
256
262
|
['local.set', `$${n}`, len == null ? ['i32.const', 0] : asI32(emit(len))],
|
|
257
263
|
out.init,
|
|
@@ -378,7 +384,7 @@ export default (ctx) => {
|
|
|
378
384
|
ctx.core.stdlib['__arr_from'] = `(func $__arr_from (param $src i64) (result f64)
|
|
379
385
|
(local $len i32) (local $dst i32)
|
|
380
386
|
(local.set $len (call $__len (local.get $src)))
|
|
381
|
-
(local.set $dst (call $__alloc_hdr (local.get $len) (local.get $len)
|
|
387
|
+
(local.set $dst (call $__alloc_hdr (local.get $len) (local.get $len)))
|
|
382
388
|
(memory.copy (local.get $dst) (call $__ptr_offset (local.get $src)) (i32.shl (local.get $len) (i32.const 3)))
|
|
383
389
|
(call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $dst)))`
|
|
384
390
|
|
|
@@ -434,7 +440,7 @@ export default (ctx) => {
|
|
|
434
440
|
(i32.lt_u (local.get $off) (i32.const 8)))
|
|
435
441
|
(then
|
|
436
442
|
(local.set $newCap (select (local.get $minCap) (i32.const 4) (i32.gt_s (local.get $minCap) (i32.const 4))))
|
|
437
|
-
(local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap)
|
|
443
|
+
(local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap)))
|
|
438
444
|
(return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))
|
|
439
445
|
(local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
440
446
|
(if (i32.ge_s (local.get $oldCap) (local.get $minCap))
|
|
@@ -444,7 +450,7 @@ export default (ctx) => {
|
|
|
444
450
|
(i32.shl (local.get $oldCap) (i32.const 1))
|
|
445
451
|
(i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
|
|
446
452
|
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
447
|
-
(local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)
|
|
453
|
+
(local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)))
|
|
448
454
|
(memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
|
|
449
455
|
${headerPropsCopyIR()}
|
|
450
456
|
${maybeDynMoveIR()}
|
|
@@ -464,7 +470,7 @@ export default (ctx) => {
|
|
|
464
470
|
(i32.shl (local.get $oldCap) (i32.const 1))
|
|
465
471
|
(i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
|
|
466
472
|
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
467
|
-
(local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)
|
|
473
|
+
(local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)))
|
|
468
474
|
(memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
|
|
469
475
|
${headerPropsCopyIR()}
|
|
470
476
|
${maybeDynMoveIR()}
|
|
@@ -522,7 +528,9 @@ export default (ctx) => {
|
|
|
522
528
|
const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
|
|
523
529
|
if (slots.every(b => b !== null)) return staticArrayPtr(slots)
|
|
524
530
|
}
|
|
525
|
-
|
|
531
|
+
// L3/'speed' bumps the cap floor (default 4) to skip more growth cycles.
|
|
532
|
+
const minCap = Math.max(ctx.transform.optimize?.arrayMinCap | 0, 4)
|
|
533
|
+
const a = allocArray(len, Math.max(len, minCap))
|
|
526
534
|
const body = [...a.setup]
|
|
527
535
|
for (let i = 0; i < len; i++)
|
|
528
536
|
body.push(['f64.store', slotAddr(a.local, i), asF64(emit(elems[i]))])
|
|
@@ -530,7 +538,8 @@ export default (ctx) => {
|
|
|
530
538
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
531
539
|
}
|
|
532
540
|
|
|
533
|
-
const
|
|
541
|
+
const minCap = Math.max(ctx.transform.optimize?.arrayMinCap | 0, 4)
|
|
542
|
+
const a = allocArray(0, Math.max(elems.length, minCap))
|
|
534
543
|
const out = temp('sa'), pos = tempI32('sp')
|
|
535
544
|
inc('__arr_set_idx_ptr')
|
|
536
545
|
|
package/module/collection.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr } from '../src/ir.js'
|
|
12
12
|
import { emit, emitFlat } from '../src/emit.js'
|
|
13
13
|
import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
|
|
14
|
+
import { hasOwnContinue } from '../src/ast.js'
|
|
14
15
|
import { inc, PTR, LAYOUT } from '../src/ctx.js'
|
|
15
16
|
|
|
16
17
|
const SET_ENTRY = 16 // hash + key
|
|
@@ -186,7 +187,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
|
|
|
186
187
|
(if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
|
|
187
188
|
(then
|
|
188
189
|
(local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
|
|
189
|
-
(local.set $newptr (call $
|
|
190
|
+
(local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
|
|
190
191
|
(local.set $i (i32.const 0))
|
|
191
192
|
(block $rd (loop $rl
|
|
192
193
|
(br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
|
|
@@ -335,14 +336,14 @@ export default (ctx) => {
|
|
|
335
336
|
__hash_has: () => ctx.features.external
|
|
336
337
|
? ['__str_hash', '__str_eq', '__ptr_type', '__ext_has']
|
|
337
338
|
: ['__str_hash', '__str_eq', '__ptr_type'],
|
|
338
|
-
__hash_new: ['
|
|
339
|
-
__hash_new_small: ['
|
|
339
|
+
__hash_new: ['__alloc_hdr_n'],
|
|
340
|
+
__hash_new_small: ['__alloc_hdr_n', '__mkptr'],
|
|
340
341
|
__hash_get_local: ['__str_hash', '__str_eq'],
|
|
341
342
|
__hash_get_local_h: ['__str_eq'],
|
|
342
343
|
__hash_set_local_h: ['__str_eq'],
|
|
343
|
-
__hash_set_local: ['__str_hash', '__str_eq'],
|
|
344
|
+
__hash_set_local: ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr'],
|
|
344
345
|
__ihash_get_local: ['__map_hash'],
|
|
345
|
-
__ihash_set_local: ['__map_hash', '
|
|
346
|
+
__ihash_set_local: ['__map_hash', '__alloc_hdr_n', '__mkptr'],
|
|
346
347
|
__dyn_get_t: ['__dyn_get_t_h', '__str_hash'],
|
|
347
348
|
__dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish'],
|
|
348
349
|
__dyn_get: ['__dyn_get_t', '__ptr_type'],
|
|
@@ -437,7 +438,7 @@ export default (ctx) => {
|
|
|
437
438
|
// __map_new() → f64 — allocate empty Map (for JSON.parse, runtime creation)
|
|
438
439
|
ctx.core.stdlib['__map_new'] = `(func $__map_new (result f64)
|
|
439
440
|
(call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
|
|
440
|
-
(call $
|
|
441
|
+
(call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
|
|
441
442
|
|
|
442
443
|
// === Set ===
|
|
443
444
|
|
|
@@ -567,14 +568,17 @@ export default (ctx) => {
|
|
|
567
568
|
|
|
568
569
|
ctx.core.stdlib['__hash_new'] = `(func $__hash_new (result f64)
|
|
569
570
|
(call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
|
|
570
|
-
(call $
|
|
571
|
+
(call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
|
|
571
572
|
|
|
572
573
|
// Small initial capacity for propsPtr-style hashes (per-object dyn props).
|
|
573
574
|
// Most receivers in real code carry 0-2 dyn props; paying 8-slot up-front
|
|
574
575
|
// is wasted memory + probe-loop cache pressure. Grows to 4/8/... on demand.
|
|
576
|
+
// L3/'speed' opts into a larger initial cap (default 8) to skip 2→4→8 growth
|
|
577
|
+
// when AST-style nodes carry 3-5 props (watr.compile's profile).
|
|
578
|
+
const smallCap = Math.max(ctx.transform.optimize?.hashSmallInitCap | 0, 2)
|
|
575
579
|
ctx.core.stdlib['__hash_new_small'] = `(func $__hash_new_small (result f64)
|
|
576
580
|
(call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
|
|
577
|
-
(call $
|
|
581
|
+
(call $__alloc_hdr_n (i32.const 0) (i32.const ${smallCap}) (i32.const ${MAP_ENTRY}))))`
|
|
578
582
|
|
|
579
583
|
ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
|
|
580
584
|
ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
|
|
@@ -709,6 +713,11 @@ export default (ctx) => {
|
|
|
709
713
|
(br_if $haveProps (i32.eq
|
|
710
714
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
711
715
|
(i32.const ${PTR.HASH})))
|
|
716
|
+
;; Fresh array (header propsPtr=0, no shift, no props): skip the
|
|
717
|
+
;; global hash probe — there's nothing to find. Shifted arrays read
|
|
718
|
+
;; forwarding bytes here (low32=newOff, high32=-1) → non-zero, so
|
|
719
|
+
;; this br_if doesn't fire and they fall through to __dyn_props.
|
|
720
|
+
(br_if $dynDone (i64.eqz (local.get $props)))
|
|
712
721
|
(local.set $props (i64.const 0))))
|
|
713
722
|
;; OBJECT: heap-allocated (off >= __heap_start) carries propsPtr at
|
|
714
723
|
;; off-16 from __alloc_hdr. The slot is either 0 (no dyn props yet) or
|
|
@@ -1032,8 +1041,14 @@ export default (ctx) => {
|
|
|
1032
1041
|
const ptrI64 = tempI64('hp'), srcOff = tempI32('hso'), srcType = tempI32('hst')
|
|
1033
1042
|
if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
|
|
1034
1043
|
const id = ctx.func.uniq++
|
|
1044
|
+
const brk = `$brk${id}`, loop = `$loop${id}`, cont = `$cont${id}`
|
|
1035
1045
|
const va = asF64(emit(src))
|
|
1036
|
-
const
|
|
1046
|
+
const needsCont = hasOwnContinue(body)
|
|
1047
|
+
ctx.func.stack.push({ brk, loop: needsCont ? cont : loop })
|
|
1048
|
+
let bodyFlat
|
|
1049
|
+
try { bodyFlat = emitFlat(body) }
|
|
1050
|
+
finally { ctx.func.stack.pop() }
|
|
1051
|
+
const bodyBlock = needsCont ? [['block', cont, ...bodyFlat]] : bodyFlat
|
|
1037
1052
|
inc('__ptr_type')
|
|
1038
1053
|
return [
|
|
1039
1054
|
// Save source ptr as i64
|
|
@@ -1054,16 +1069,16 @@ export default (ctx) => {
|
|
|
1054
1069
|
['local.set', `$${off}`, ['call', '$__ptr_offset', ['local.get', `$${ptrI64}`]]],
|
|
1055
1070
|
['local.set', `$${cap}`, ['call', '$__cap', ['local.get', `$${ptrI64}`]]],
|
|
1056
1071
|
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1057
|
-
['block',
|
|
1058
|
-
['br_if',
|
|
1072
|
+
['block', brk, ['loop', loop,
|
|
1073
|
+
['br_if', brk, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
|
|
1059
1074
|
['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
|
|
1060
1075
|
['i32.mul', ['local.get', `$${i}`], ['i32.const', MAP_ENTRY]]]],
|
|
1061
1076
|
['if', ['i64.ne', ['i64.load', ['local.get', `$${slot}`]], ['i64.const', 0]],
|
|
1062
1077
|
['then',
|
|
1063
1078
|
['local.set', `$${varName}`, ['f64.reinterpret_i64', ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]],
|
|
1064
|
-
...
|
|
1079
|
+
...bodyBlock]],
|
|
1065
1080
|
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1066
|
-
['br',
|
|
1081
|
+
['br', loop]]]]]
|
|
1067
1082
|
]
|
|
1068
1083
|
}
|
|
1069
1084
|
}
|
package/module/core.js
CHANGED
|
@@ -42,6 +42,7 @@ export default (ctx) => {
|
|
|
42
42
|
__length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
|
|
43
43
|
__typeof: ['__ptr_type', '__is_nullish'],
|
|
44
44
|
__alloc_hdr: ['__alloc'],
|
|
45
|
+
__alloc_hdr_n: ['__alloc'],
|
|
45
46
|
})
|
|
46
47
|
|
|
47
48
|
ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
|
|
@@ -334,7 +335,21 @@ export default (ctx) => {
|
|
|
334
335
|
// (NaN-boxed PTR.HASH) for ARRAY/HASH/MAP/SET; 0 means "no dyn props yet". This lets
|
|
335
336
|
// __dyn_get_t / __dyn_set sidestep the global __dyn_props lookup on the hot path.
|
|
336
337
|
// Read offsets relative to the returned data ptr stay unchanged (-8 len, -4 cap).
|
|
337
|
-
|
|
338
|
+
// Default stride=8 (f64 NaN-boxed slot) — used by every Array/HASH/OBJECT alloc.
|
|
339
|
+
// Specialized over a generic (len, cap, stride) helper to drop a fat (i32.const 8)
|
|
340
|
+
// immediate at every call site (~20+) plus a param/local.get pair in the body.
|
|
341
|
+
// Non-8 strides (Set: 16, Map/HASH probe: 24, TypedArray raw: 1) use __alloc_hdr_n.
|
|
342
|
+
ctx.core.stdlib['__alloc_hdr'] = `(func $__alloc_hdr (param $len i32) (param $cap i32) (result i32)
|
|
343
|
+
(local $ptr i32)
|
|
344
|
+
(local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.shl (local.get $cap) (i32.const 3)))))
|
|
345
|
+
(i64.store (local.get $ptr) (i64.const 0))
|
|
346
|
+
(i32.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $len))
|
|
347
|
+
(i32.store (i32.add (local.get $ptr) (i32.const 12)) (local.get $cap))
|
|
348
|
+
(i32.add (local.get $ptr) (i32.const 16)))`
|
|
349
|
+
|
|
350
|
+
// Generic header allocator for non-8 strides: Set (16), Map probe (24), TypedArray raw (1).
|
|
351
|
+
// Same 16-byte header layout as __alloc_hdr; per-entry stride is passed dynamically.
|
|
352
|
+
ctx.core.stdlib['__alloc_hdr_n'] = `(func $__alloc_hdr_n (param $len i32) (param $cap i32) (param $stride i32) (result i32)
|
|
338
353
|
(local $ptr i32)
|
|
339
354
|
(local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.mul (local.get $cap) (local.get $stride)))))
|
|
340
355
|
(i64.store (local.get $ptr) (i64.const 0))
|
package/module/json.js
CHANGED
|
@@ -87,12 +87,12 @@ export default (ctx) => {
|
|
|
87
87
|
const keys = Object.keys(v)
|
|
88
88
|
// Empty object: minimal OBJECT with no slots.
|
|
89
89
|
if (keys.length === 0) {
|
|
90
|
-
return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', 1]
|
|
90
|
+
return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', 1]])
|
|
91
91
|
}
|
|
92
92
|
const schemaId = ctx.schema.register(keys)
|
|
93
93
|
const t = tempI32('jobj')
|
|
94
94
|
const body = [
|
|
95
|
-
['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', keys.length]
|
|
95
|
+
['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', keys.length]]],
|
|
96
96
|
]
|
|
97
97
|
for (let i = 0; i < keys.length; i++) {
|
|
98
98
|
body.push(['f64.store', slotAddr(t, i), asF64(emitJsonConstValue(v[keys[i]]))])
|
|
@@ -582,10 +582,10 @@ export default (ctx) => {
|
|
|
582
582
|
;; miss: register new schema.
|
|
583
583
|
(local.set $sid (global.get $__schema_next))
|
|
584
584
|
(global.set $__schema_next (i32.add (local.get $sid) (i32.const 1)))
|
|
585
|
-
;; Allocate jz Array of n keys. __alloc_hdr(len, cap
|
|
586
|
-
;;
|
|
585
|
+
;; Allocate jz Array of n keys. __alloc_hdr(len, cap) returns base of
|
|
586
|
+
;; slot region with len@-8 and cap@-4. The schema dispatch arm reads
|
|
587
587
|
;; nkeys from -8, so len must equal cap=n.
|
|
588
|
-
(local.set $karr (call $__alloc_hdr (local.get $n) (local.get $n)
|
|
588
|
+
(local.set $karr (call $__alloc_hdr (local.get $n) (local.get $n)))
|
|
589
589
|
(local.set $i (i32.const 0))
|
|
590
590
|
(block $cd (loop $cl
|
|
591
591
|
(br_if $cd (i32.ge_s (local.get $i) (local.get $n)))
|
|
@@ -623,7 +623,7 @@ export default (ctx) => {
|
|
|
623
623
|
(then ${ADV(1)}
|
|
624
624
|
(local.set $sid (call $__jp_schema_get (local.get $kbuf) (i32.const 0) (local.get $hh)))
|
|
625
625
|
(return (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid)
|
|
626
|
-
(call $__alloc_hdr (i32.const 0) (i32.const 1)
|
|
626
|
+
(call $__alloc_hdr (i32.const 0) (i32.const 1))))))
|
|
627
627
|
(block $d (loop $l
|
|
628
628
|
${WS()}
|
|
629
629
|
(if (i32.eq ${PEEK} (i32.const 34))
|
|
@@ -661,7 +661,7 @@ export default (ctx) => {
|
|
|
661
661
|
(local.set $sid (call $__jp_schema_get (local.get $kbuf) (local.get $kn) (local.get $hh)))
|
|
662
662
|
;; Allocate OBJECT slot region: kn × 8 bytes, with header (size at -8,
|
|
663
663
|
;; cap at -4) matching the static-fold path's emitJsonConstValue layout.
|
|
664
|
-
(local.set $obj (call $__alloc_hdr (i32.const 0) (local.get $kn)
|
|
664
|
+
(local.set $obj (call $__alloc_hdr (i32.const 0) (local.get $kn)))
|
|
665
665
|
;; Copy values into OBJECT slots.
|
|
666
666
|
(local.set $i (i32.const 0))
|
|
667
667
|
(block $vd (loop $vl
|
|
@@ -749,7 +749,7 @@ export default (ctx) => {
|
|
|
749
749
|
const sid = ctx.schema.register(keys)
|
|
750
750
|
let body = `${WS()}
|
|
751
751
|
${expect(123)}
|
|
752
|
-
(local.set $${obj} (call $__alloc_hdr (i32.const 0) (i32.const ${Math.max(1, keys.length)})
|
|
752
|
+
(local.set $${obj} (call $__alloc_hdr (i32.const 0) (i32.const ${Math.max(1, keys.length)})))`
|
|
753
753
|
keys.forEach((k, i) => {
|
|
754
754
|
body += `
|
|
755
755
|
${WS()}
|
package/module/math.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @module math
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { typed, asF64, asI32, temp, arrayLoop } from '../src/ir.js'
|
|
16
|
+
import { typed, asF64, asI32, toI32, temp, arrayLoop } from '../src/ir.js'
|
|
17
17
|
import { emit } from '../src/emit.js'
|
|
18
18
|
import { inc } from '../src/ctx.js'
|
|
19
19
|
import { repOf } from '../src/analyze.js'
|
|
@@ -76,7 +76,22 @@ export default (ctx) => {
|
|
|
76
76
|
for (const x of rest) r = typed(['f64.max', r, asF64(emit(x))], 'f64')
|
|
77
77
|
return r
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// f64.nearest is roundTiesToEven; JS Math.round is roundTiesToward+∞. They agree
|
|
80
|
+
// everywhere except exact half-integers n+0.5 with n even (nearest→n, JS→n+1).
|
|
81
|
+
// Detect that one case — `nearest(x) === x - 0.5` — and bump by one. (The −0.5→−0
|
|
82
|
+
// and 0.49999…94→0 edges already match `f64.nearest`.)
|
|
83
|
+
ctx.core.emit['math.round'] = a => {
|
|
84
|
+
if (typeof a === 'string' && repOf(a)?.intCertain === true) return asF64(emit(a))
|
|
85
|
+
const t = temp('rnd'), n = temp('rnd')
|
|
86
|
+
return typed(['block', ['result', 'f64'],
|
|
87
|
+
['local.set', `$${t}`, asF64(emit(a))],
|
|
88
|
+
['local.set', `$${n}`, ['f64.nearest', ['local.get', `$${t}`]]],
|
|
89
|
+
['select',
|
|
90
|
+
['f64.add', ['local.get', `$${n}`], ['f64.const', 1]],
|
|
91
|
+
['local.get', `$${n}`],
|
|
92
|
+
['f64.eq', ['local.get', `$${n}`], ['f64.sub', ['local.get', `$${t}`], ['f64.const', 0.5]]]],
|
|
93
|
+
], 'f64')
|
|
94
|
+
}
|
|
80
95
|
ctx.core.emit['math.fround'] = a => typed(['f64.promote_f32', ['f32.demote_f64', asF64(emit(a))]], 'f64')
|
|
81
96
|
|
|
82
97
|
// Sign
|
|
@@ -129,8 +144,10 @@ export default (ctx) => {
|
|
|
129
144
|
// Integer/bit operations: return i32 directly. Consumers `asF64`-rebox at
|
|
130
145
|
// store/return boundaries; consumers staying in i32 (bit chains, i32 locals)
|
|
131
146
|
// skip the convert/trunc round-trip entirely.
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
// Operands take ECMAScript ToInt32 (wrapping), not saturation — `Math.imul(x, k)`
|
|
148
|
+
// with a literal k ≥ 2³¹ must wrap to negative, matching JS, not clamp to INT_MAX.
|
|
149
|
+
ctx.core.emit['math.clz32'] = a => typed(['i32.clz', toI32(emit(a))], 'i32')
|
|
150
|
+
ctx.core.emit['math.imul'] = (a, b) => typed(['i32.mul', toI32(emit(a)), toI32(emit(b))], 'i32')
|
|
134
151
|
|
|
135
152
|
// Random
|
|
136
153
|
ctx.core.emit['math.random'] = () => (inc('math.random'), typed(['call', '$math.random'], 'f64'))
|