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.
@@ -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
+ }
@@ -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
+ }