jz 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +381 -0
- package/cli.js +163 -0
- package/index.js +217 -0
- package/module/array.js +1317 -0
- package/module/collection.js +791 -0
- package/module/console.js +190 -0
- package/module/core.js +642 -0
- package/module/function.js +180 -0
- package/module/index.js +15 -0
- package/module/json.js +504 -0
- package/module/math.js +389 -0
- package/module/number.js +605 -0
- package/module/object.js +357 -0
- package/module/regex.js +913 -0
- package/module/schema.js +104 -0
- package/module/string.js +928 -0
- package/module/symbol.js +54 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +711 -0
- package/package.json +54 -5
- package/src/analyze.js +1906 -0
- package/src/compile.js +2175 -0
- package/src/ctx.js +243 -0
- package/src/emit.js +2095 -0
- package/src/host.js +524 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +391 -0
- package/src/optimize.js +1352 -0
- package/src/prepare.js +1598 -0
- package/wasi.js +74 -0
package/module/symbol.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol module — interned atoms via NaN-boxing.
|
|
3
|
+
*
|
|
4
|
+
* Type=0 (ATOM): aux=atomId, offset=0.
|
|
5
|
+
*
|
|
6
|
+
* Reserved atom IDs (0-15):
|
|
7
|
+
* 0 = reserved
|
|
8
|
+
* 1 = null/undefined (type=0, aux=1, offset=0 — the nullish NaN sentinel)
|
|
9
|
+
* 2-15 = reserved
|
|
10
|
+
*
|
|
11
|
+
* User symbols start at ID 16.
|
|
12
|
+
* Symbol('name') → unique atom per call site (compile-time)
|
|
13
|
+
* Symbol.for('name') → interned by name (same name = same ID across call sites)
|
|
14
|
+
*
|
|
15
|
+
* Symbols are compared by identity (ptr equality), not by name.
|
|
16
|
+
*
|
|
17
|
+
* @module symbol
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { emit, typed, asF64, mkPtrIR } from '../src/compile.js'
|
|
21
|
+
import { err, inc, PTR } from '../src/ctx.js'
|
|
22
|
+
|
|
23
|
+
const RESERVED = 16 // first user atom ID
|
|
24
|
+
|
|
25
|
+
export default (ctx) => {
|
|
26
|
+
inc('__mkptr')
|
|
27
|
+
|
|
28
|
+
// Intern table: name → atomId (shared across compilation)
|
|
29
|
+
if (!ctx.runtime.atom) {
|
|
30
|
+
ctx.runtime.atom = { table: new Map(), next: RESERVED }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Allocate a new unique atom ID. */
|
|
34
|
+
const nextAtom = () => ctx.runtime.atom.next++
|
|
35
|
+
|
|
36
|
+
/** Get or create interned atom ID for name. */
|
|
37
|
+
const internAtom = (name) => {
|
|
38
|
+
if (ctx.runtime.atom.table.has(name)) return ctx.runtime.atom.table.get(name)
|
|
39
|
+
const id = nextAtom()
|
|
40
|
+
ctx.runtime.atom.table.set(name, id)
|
|
41
|
+
return id
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Symbol('name') → unique atom (each call site gets a different ID)
|
|
45
|
+
ctx.core.emit['Symbol'] = (nameExpr) => mkPtrIR(PTR.ATOM, nextAtom(), 0)
|
|
46
|
+
|
|
47
|
+
// Symbol.for('name') → interned atom (same name = same ID)
|
|
48
|
+
ctx.core.emit['Symbol.for'] = (nameExpr) => {
|
|
49
|
+
// Name must be a string literal at compile time
|
|
50
|
+
if (!Array.isArray(nameExpr) || nameExpr[0] !== 'str')
|
|
51
|
+
err('Symbol.for requires a string literal')
|
|
52
|
+
return mkPtrIR(PTR.ATOM, internAtom(nameExpr[1]), 0)
|
|
53
|
+
}
|
|
54
|
+
}
|
package/module/timer.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timer module — setTimeout/setInterval/clearTimeout/clearInterval.
|
|
3
|
+
*
|
|
4
|
+
* Pure-WASM timer queue using WASI clock_time_get for deadlines.
|
|
5
|
+
* Runs inline after __start — no JS host, no Rust runner needed.
|
|
6
|
+
*
|
|
7
|
+
* Timer queue: heap-allocated array of entries in linear memory.
|
|
8
|
+
* Each entry (40 bytes):
|
|
9
|
+
* [0] id (i32) — unique timer ID
|
|
10
|
+
* [4] pad (i32)
|
|
11
|
+
* [8] closure_ptr (f64) — NaN-boxed closure to invoke
|
|
12
|
+
* [16] deadline_ns (i64) — absolute nanoseconds (monotonic clock)
|
|
13
|
+
* [24] interval_ms (f64) — 0 for setTimeout, >0 for setInterval
|
|
14
|
+
* [32] alive (i32) — 1=active, 0=cleared
|
|
15
|
+
* [36] pad (i32)
|
|
16
|
+
*
|
|
17
|
+
* @module timer
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { emit, typed, asF64, UNDEF_NAN, MAX_CLOSURE_ARITY } from '../src/compile.js'
|
|
21
|
+
import { inc, PTR } from '../src/ctx.js'
|
|
22
|
+
import { temp } from '../src/ir.js'
|
|
23
|
+
|
|
24
|
+
const MAX_TIMERS = 64
|
|
25
|
+
const ENTRY_SIZE = 40
|
|
26
|
+
|
|
27
|
+
export default (ctx) => {
|
|
28
|
+
// Always include init + tick + loop when timer module loads (structural, not per-emitter)
|
|
29
|
+
inc('__timer_init', '__timer_tick', '__timer_loop')
|
|
30
|
+
|
|
31
|
+
Object.assign(ctx.core.stdlibDeps, {
|
|
32
|
+
__timer_init: ['__alloc'],
|
|
33
|
+
__timer_add: ['__time_ns'],
|
|
34
|
+
__timer_cancel: [],
|
|
35
|
+
__timer_dispatch: [],
|
|
36
|
+
__timer_tick: ['__time_ns', '__timer_dispatch'],
|
|
37
|
+
__timer_loop: ['__time_ns', '__timer_dispatch'],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Force closure ABI width to MAX_CLOSURE_ARITY so __timer_dispatch's
|
|
41
|
+
// call_indirect always matches the $ftN type (env, argc, a0..a7)
|
|
42
|
+
ctx.closure.floor = MAX_CLOSURE_ARITY
|
|
43
|
+
|
|
44
|
+
// Import WASI clock_time_get if not already imported (console.js may have done it)
|
|
45
|
+
if (!ctx.module.imports.some(i => i[1] === '"wasi_snapshot_preview1"' && i[2] === '"clock_time_get"')) {
|
|
46
|
+
ctx.module.imports.push(
|
|
47
|
+
['import', '"wasi_snapshot_preview1"', '"clock_time_get"',
|
|
48
|
+
['func', '$__clock_time_get', ['param', 'i32'], ['param', 'i64'], ['param', 'i32'], ['result', 'i32']]])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// __time_ns() → i64 — current monotonic nanoseconds
|
|
52
|
+
// Reuses address 0-7 for the i64 output (same as __time_ms in console.js)
|
|
53
|
+
ctx.core.stdlib['__time_ns'] = `(func $__time_ns (result i64)
|
|
54
|
+
(drop (call $__clock_time_get (i32.const 1) (i64.const 1) (i32.const 0)))
|
|
55
|
+
(i64.load (i32.const 0)))`
|
|
56
|
+
|
|
57
|
+
// __timer_init() — allocate timer queue, zero-fill, init next_id
|
|
58
|
+
// Queue layout: [next_id i32 @ -4] [entry0 .. entry{MAX_TIMERS-1}]
|
|
59
|
+
// We store next_id as a global to survive across calls
|
|
60
|
+
ctx.core.stdlib['__timer_init'] = `(func $__timer_init
|
|
61
|
+
(global.set $__timer_next_id (i32.const 1))
|
|
62
|
+
(global.set $__timer_count (i32.const 0))
|
|
63
|
+
(global.set $__timer_queue (call $__alloc (i32.const ${MAX_TIMERS * ENTRY_SIZE})))
|
|
64
|
+
(memory.fill (global.get $__timer_queue) (i32.const 0) (i32.const ${MAX_TIMERS * ENTRY_SIZE})))`
|
|
65
|
+
|
|
66
|
+
// __timer_add(closure_ptr: f64, delay_ms: f64, interval: i32) → f64 (timer ID)
|
|
67
|
+
// interval=0 → setTimeout, interval=1 → setInterval
|
|
68
|
+
ctx.core.stdlib['__timer_add'] = `(func $__timer_add (param $clos f64) (param $delay f64) (param $is_interval i32) (result f64)
|
|
69
|
+
(local $id i32)
|
|
70
|
+
(local $slot i32)
|
|
71
|
+
(local $base i32)
|
|
72
|
+
(local $deadline i64)
|
|
73
|
+
;; Find free slot
|
|
74
|
+
(local.set $slot (i32.const -1))
|
|
75
|
+
(local.set $base (global.get $__timer_queue))
|
|
76
|
+
(block $found (loop $scan
|
|
77
|
+
;; slot starts at -1, increment first
|
|
78
|
+
(local.set $slot (i32.add (local.get $slot) (i32.const 1)))
|
|
79
|
+
(br_if $found (i32.ge_s (local.get $slot) (i32.const ${MAX_TIMERS})))
|
|
80
|
+
;; Check alive field at slot*ENTRY_SIZE + 32
|
|
81
|
+
(br_if $scan (i32.load (i32.add (local.get $base)
|
|
82
|
+
(i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 32)))))
|
|
83
|
+
;; alive==0 → free slot, break
|
|
84
|
+
(br $found)))
|
|
85
|
+
;; No free slot? Return 0 (error)
|
|
86
|
+
(if (i32.ge_s (local.get $slot) (i32.const ${MAX_TIMERS}))
|
|
87
|
+
(then (return (f64.const 0))))
|
|
88
|
+
;; Compute deadline = now + delay_ms * 1_000_000
|
|
89
|
+
(local.set $deadline (i64.add
|
|
90
|
+
(call $__time_ns)
|
|
91
|
+
(i64.trunc_f64_u (f64.mul (local.get $delay) (f64.const 1000000)))))
|
|
92
|
+
;; Allocate ID
|
|
93
|
+
(local.set $id (global.get $__timer_next_id))
|
|
94
|
+
(global.set $__timer_next_id (i32.add (local.get $id) (i32.const 1)))
|
|
95
|
+
;; Write entry
|
|
96
|
+
;; id @ offset+0
|
|
97
|
+
(i32.store (i32.add (local.get $base) (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})))
|
|
98
|
+
(local.get $id))
|
|
99
|
+
;; closure_ptr @ offset+8
|
|
100
|
+
(f64.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 8)))
|
|
101
|
+
(local.get $clos))
|
|
102
|
+
;; deadline_ns @ offset+16
|
|
103
|
+
(i64.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 16)))
|
|
104
|
+
(local.get $deadline))
|
|
105
|
+
;; interval_ms @ offset+24 — store delay for intervals, 0 for timeouts
|
|
106
|
+
(f64.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 24)))
|
|
107
|
+
(select (local.get $delay) (f64.const 0) (local.get $is_interval)))
|
|
108
|
+
;; alive @ offset+32
|
|
109
|
+
(i32.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 32)))
|
|
110
|
+
(i32.const 1))
|
|
111
|
+
;; Increment active count
|
|
112
|
+
(global.set $__timer_count (i32.add (global.get $__timer_count) (i32.const 1)))
|
|
113
|
+
;; Return ID as f64
|
|
114
|
+
(f64.convert_i32_u (local.get $id)))`
|
|
115
|
+
|
|
116
|
+
// __timer_cancel(id: f64) → f64 (0)
|
|
117
|
+
ctx.core.stdlib['__timer_cancel'] = `(func $__timer_cancel (param $id f64) (result f64)
|
|
118
|
+
(local $slot i32)
|
|
119
|
+
(local $base i32)
|
|
120
|
+
(local $target i32)
|
|
121
|
+
(local.set $target (i32.trunc_f64_u (local.get $id)))
|
|
122
|
+
(local.set $base (global.get $__timer_queue))
|
|
123
|
+
(local.set $slot (i32.const 0))
|
|
124
|
+
(block $done (loop $scan
|
|
125
|
+
(br_if $done (i32.ge_s (local.get $slot) (i32.const ${MAX_TIMERS})))
|
|
126
|
+
;; Check if id matches and alive
|
|
127
|
+
(if (i32.and
|
|
128
|
+
(i32.eq (i32.load (i32.add (local.get $base) (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})))) (local.get $target))
|
|
129
|
+
(i32.load (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 32)))))
|
|
130
|
+
(then
|
|
131
|
+
;; Mark dead
|
|
132
|
+
(i32.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 32))) (i32.const 0))
|
|
133
|
+
;; Decrement count
|
|
134
|
+
(global.set $__timer_count (i32.sub (global.get $__timer_count) (i32.const 1)))
|
|
135
|
+
(br $done)))
|
|
136
|
+
(local.set $slot (i32.add (local.get $slot) (i32.const 1)))
|
|
137
|
+
(br $scan)))
|
|
138
|
+
(f64.const 0))`
|
|
139
|
+
|
|
140
|
+
// __timer_dispatch(now_ns: i64) → i32 (number of callbacks dispatched)
|
|
141
|
+
// Finds all due timers (deadline <= now), invokes them, reschedules intervals
|
|
142
|
+
ctx.core.stdlib['__timer_dispatch'] = `(func $__timer_dispatch (param $now i64) (result i32)
|
|
143
|
+
(local $slot i32)
|
|
144
|
+
(local $base i32)
|
|
145
|
+
(local $dispatched i32)
|
|
146
|
+
(local $clos f64)
|
|
147
|
+
(local $interval f64)
|
|
148
|
+
(local $id i32)
|
|
149
|
+
(local.set $dispatched (i32.const 0))
|
|
150
|
+
(local.set $base (global.get $__timer_queue))
|
|
151
|
+
(local.set $slot (i32.const 0))
|
|
152
|
+
(block $done (loop $scan
|
|
153
|
+
(br_if $done (i32.ge_s (local.get $slot) (i32.const ${MAX_TIMERS})))
|
|
154
|
+
;; Check alive
|
|
155
|
+
(if (i32.load (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 32))))
|
|
156
|
+
(then
|
|
157
|
+
;; Check deadline <= now
|
|
158
|
+
(if (i64.le_u
|
|
159
|
+
(i64.load (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 16))))
|
|
160
|
+
(local.get $now))
|
|
161
|
+
(then
|
|
162
|
+
;; Read closure and interval before potentially clearing
|
|
163
|
+
(local.set $clos (f64.load (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 8)))))
|
|
164
|
+
(local.set $interval (f64.load (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 24)))))
|
|
165
|
+
(local.set $id (i32.load (i32.add (local.get $base) (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})))))
|
|
166
|
+
;; Interval? Reschedule
|
|
167
|
+
(if (f64.gt (local.get $interval) (f64.const 0))
|
|
168
|
+
(then
|
|
169
|
+
;; New deadline = now + interval_ms * 1_000_000
|
|
170
|
+
(i64.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 16)))
|
|
171
|
+
(i64.add (local.get $now) (i64.trunc_f64_u (f64.mul (local.get $interval) (f64.const 1000000))))))
|
|
172
|
+
(else
|
|
173
|
+
;; Timeout: mark dead
|
|
174
|
+
(i32.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 32))) (i32.const 0))
|
|
175
|
+
(global.set $__timer_count (i32.sub (global.get $__timer_count) (i32.const 1)))))
|
|
176
|
+
;; Invoke closure via call_indirect on $ftN
|
|
177
|
+
;; (env f64, argc i32, a0..a7 f64) → f64
|
|
178
|
+
(drop (call_indirect (type \$ftN)
|
|
179
|
+
(local.get $clos) ;; env = closure pointer itself
|
|
180
|
+
(i32.const 0) ;; argc = 0
|
|
181
|
+
${Array.from({length: MAX_CLOSURE_ARITY}, () => `(f64.const nan:${UNDEF_NAN})`).join('\n ')}
|
|
182
|
+
(i32.wrap_i64 (i64.and
|
|
183
|
+
(i64.shr_u (i64.reinterpret_f64 (local.get $clos)) (i64.const 32))
|
|
184
|
+
(i64.const 0x7FFF)))))
|
|
185
|
+
(local.set $dispatched (i32.add (local.get $dispatched) (i32.const 1)))))))
|
|
186
|
+
(local.set $slot (i32.add (local.get $slot) (i32.const 1)))
|
|
187
|
+
(br $scan)))
|
|
188
|
+
(local.get $dispatched))`
|
|
189
|
+
|
|
190
|
+
// __timer_tick() → i32 — non-blocking tick. Dispatches due timers, returns remaining active count.
|
|
191
|
+
// Called by JS runtime (host.js) via setInterval to drive timers without blocking.
|
|
192
|
+
ctx.core.stdlib['__timer_tick'] = `(func $__timer_tick (export "__timer_tick") (result i32)
|
|
193
|
+
(local $now i64)
|
|
194
|
+
(if (i32.le_s (global.get $__timer_count) (i32.const 0))
|
|
195
|
+
(then (return (i32.const 0))))
|
|
196
|
+
(local.set $now (call $__time_ns))
|
|
197
|
+
(drop (call $__timer_dispatch (local.get $now)))
|
|
198
|
+
(global.get $__timer_count))`
|
|
199
|
+
|
|
200
|
+
// __timer_loop() — blocking event loop. Polls clock, dispatches due timers.
|
|
201
|
+
// Exits when no active timers remain (all timeouts fired, all intervals cleared).
|
|
202
|
+
// Intended for CLI/wasmtime/wasmer where JS event loop is unavailable.
|
|
203
|
+
ctx.core.stdlib['__timer_loop'] = `(func $__timer_loop (export "__timer_loop")
|
|
204
|
+
(local $now i64)
|
|
205
|
+
(local $any i32)
|
|
206
|
+
(block $exit (loop $poll
|
|
207
|
+
;; Exit if no active timers
|
|
208
|
+
(br_if $exit (i32.le_s (global.get $__timer_count) (i32.const 0)))
|
|
209
|
+
;; Get current time
|
|
210
|
+
(local.set $now (call $__time_ns))
|
|
211
|
+
;; Dispatch due timers
|
|
212
|
+
(drop (call $__timer_dispatch (local.get $now)))
|
|
213
|
+
;; Loop
|
|
214
|
+
(br $poll))))`
|
|
215
|
+
|
|
216
|
+
// Register globals for timer state
|
|
217
|
+
// $__timer_queue: i32 — base address of timer array
|
|
218
|
+
// $__timer_next_id: i32 — next timer ID
|
|
219
|
+
// $__timer_count: i32 — number of active timers
|
|
220
|
+
ctx.scope.globals.set('__timer_queue', '(global $__timer_queue (mut i32) (i32.const 0))')
|
|
221
|
+
ctx.scope.globals.set('__timer_next_id', '(global $__timer_next_id (mut i32) (i32.const 0))')
|
|
222
|
+
ctx.scope.globals.set('__timer_count', '(global $__timer_count (mut i32) (i32.const 0))')
|
|
223
|
+
|
|
224
|
+
// Emitter: setTimeout(closure, delay) → timer_id
|
|
225
|
+
ctx.core.emit['setTimeout'] = (closureExpr, delayExpr) => {
|
|
226
|
+
inc('__timer_add')
|
|
227
|
+
const t = temp('tc')
|
|
228
|
+
return typed(['block', ['result', 'f64'],
|
|
229
|
+
['local.set', `$${t}`, asF64(emit(closureExpr))],
|
|
230
|
+
['call', '$__timer_add', ['local.get', `$${t}`], asF64(emit(delayExpr)), ['i32.const', 0]]], 'f64')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Emitter: setInterval(closure, delay) → timer_id
|
|
234
|
+
ctx.core.emit['setInterval'] = (closureExpr, delayExpr) => {
|
|
235
|
+
inc('__timer_add')
|
|
236
|
+
const t = temp('tc')
|
|
237
|
+
return typed(['block', ['result', 'f64'],
|
|
238
|
+
['local.set', `$${t}`, asF64(emit(closureExpr))],
|
|
239
|
+
['call', '$__timer_add', ['local.get', `$${t}`], asF64(emit(delayExpr)), ['i32.const', 1]]], 'f64')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Emitter: clearTimeout(id) → undefined
|
|
243
|
+
ctx.core.emit['clearTimeout'] = (idExpr) => {
|
|
244
|
+
inc('__timer_cancel')
|
|
245
|
+
return typed(['call', '$__timer_cancel', asF64(emit(idExpr))], 'f64')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Emitter: clearInterval(id) → undefined
|
|
249
|
+
ctx.core.emit['clearInterval'] = (idExpr) => {
|
|
250
|
+
inc('__timer_cancel')
|
|
251
|
+
return typed(['call', '$__timer_cancel', asF64(emit(idExpr))], 'f64')
|
|
252
|
+
}
|
|
253
|
+
}
|