jz 0.1.1 → 0.2.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/module/symbol.js CHANGED
@@ -4,9 +4,10 @@
4
4
  * Type=0 (ATOM): aux=atomId, offset=0.
5
5
  *
6
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
7
+ * 0 = reserved
8
+ * 1 = null (NULL_NAN sentinel)
9
+ * 2 = undefined (UNDEF_NAN sentinel)
10
+ * 3-15 = reserved
10
11
  *
11
12
  * User symbols start at ID 16.
12
13
  * Symbol('name') → unique atom per call site (compile-time)
package/module/timer.js CHANGED
@@ -1,10 +1,19 @@
1
1
  /**
2
2
  * Timer module — setTimeout/setInterval/clearTimeout/clearInterval.
3
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.
4
+ * Two host-mode lowerings:
6
5
  *
7
- * Timer queue: heap-allocated array of entries in linear memory.
6
+ * `host: 'js'` (default): emit `env.setTimeout(cb: f64, delay: f64, repeat: i32) -> f64`
7
+ * and `env.clearTimeout(id: f64) -> f64`. The JS host (src/host.js) drives both
8
+ * via global setTimeout/setInterval and calls back into wasm through the
9
+ * exported `__invoke_closure(clos: i64) -> f64` trampoline. No queue, no
10
+ * polling — the host's event loop does the scheduling.
11
+ *
12
+ * `host: 'wasi'`: pure-WASM timer queue using WASI clock_time_get for
13
+ * deadlines. Runs inline after __start (or via __timer_loop on wasmtime/
14
+ * wasmer). No JS host needed.
15
+ *
16
+ * WASI queue layout: heap-allocated array of entries in linear memory.
8
17
  * Each entry (40 bytes):
9
18
  * [0] id (i32) — unique timer ID
10
19
  * [4] pad (i32)
@@ -17,14 +26,32 @@
17
26
  * @module timer
18
27
  */
19
28
 
20
- import { typed, asF64, UNDEF_NAN, MAX_CLOSURE_ARITY, temp } from '../src/ir.js'
29
+ import { typed, asF64, asI64, UNDEF_NAN, MAX_CLOSURE_ARITY, temp, tempI64 } from '../src/ir.js'
21
30
  import { emit } from '../src/emit.js'
22
- import { inc, PTR } from '../src/ctx.js'
31
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
23
32
 
24
33
  const MAX_TIMERS = 64
25
34
  const ENTRY_SIZE = 40
26
35
 
27
- export default (ctx) => {
36
+ const addImportOnce = (ctx, mod, name, fn) => {
37
+ if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
38
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
39
+ }
40
+
41
+ // Shared "fire a NaN-boxed closure with 0 args" trampoline. Funcref index lives
42
+ // in upper 16 bits of the pointer payload; remaining $ftN slots get UNDEF_NAN.
43
+ // Closure is also passed as $__env so captures resolve via env-load.
44
+ // `exported` adds (export "__invoke_closure") so the JS host can call it.
45
+ const invokeClosureFn = (exported) => `(func $__invoke_closure${exported ? ' (export "__invoke_closure")' : ''} (param $clos i64) (result f64)
46
+ (call_indirect (type \$ftN)
47
+ (f64.reinterpret_i64 (local.get $clos))
48
+ (i32.const 0)
49
+ ${Array.from({length: MAX_CLOSURE_ARITY}, () => `(f64.const nan:${UNDEF_NAN})`).join('\n ')}
50
+ (i32.wrap_i64 (i64.and
51
+ (i64.shr_u (local.get $clos) (i64.const ${LAYOUT.AUX_SHIFT}))
52
+ (i64.const ${LAYOUT.AUX_MASK})))))`
53
+
54
+ const setupWasi = (ctx) => {
28
55
  // Always include init + tick + loop when timer module loads (structural, not per-emitter)
29
56
  inc('__timer_init', '__timer_tick', '__timer_loop')
30
57
 
@@ -32,7 +59,7 @@ export default (ctx) => {
32
59
  __timer_init: ['__alloc'],
33
60
  __timer_add: ['__time_ns'],
34
61
  __timer_cancel: [],
35
- __timer_dispatch: [],
62
+ __timer_dispatch: ['__invoke_closure'],
36
63
  __timer_tick: ['__time_ns', '__timer_dispatch'],
37
64
  __timer_loop: ['__time_ns', '__timer_dispatch'],
38
65
  })
@@ -41,12 +68,8 @@ export default (ctx) => {
41
68
  // call_indirect always matches the $ftN type (env, argc, a0..a7)
42
69
  ctx.closure.floor = MAX_CLOSURE_ARITY
43
70
 
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
- }
71
+ addImportOnce(ctx, 'wasi_snapshot_preview1', 'clock_time_get',
72
+ ['func', '$__clock_time_get', ['param', 'i32'], ['param', 'i64'], ['param', 'i32'], ['result', 'i32']])
50
73
 
51
74
  // __time_ns() → i64 — current monotonic nanoseconds
52
75
  // Reuses address 0-7 for the i64 output (same as __time_ms in console.js)
@@ -63,9 +86,9 @@ export default (ctx) => {
63
86
  (global.set $__timer_queue (call $__alloc (i32.const ${MAX_TIMERS * ENTRY_SIZE})))
64
87
  (memory.fill (global.get $__timer_queue) (i32.const 0) (i32.const ${MAX_TIMERS * ENTRY_SIZE})))`
65
88
 
66
- // __timer_add(closure_ptr: f64, delay_ms: f64, interval: i32) → f64 (timer ID)
89
+ // __timer_add(closure_ptr: i64, delay_ms: f64, interval: i32) → f64 (timer ID)
67
90
  // 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)
91
+ ctx.core.stdlib['__timer_add'] = `(func $__timer_add (param $clos i64) (param $delay f64) (param $is_interval i32) (result f64)
69
92
  (local $id i32)
70
93
  (local $slot i32)
71
94
  (local $base i32)
@@ -97,7 +120,7 @@ export default (ctx) => {
97
120
  (i32.store (i32.add (local.get $base) (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})))
98
121
  (local.get $id))
99
122
  ;; 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)))
123
+ (i64.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 8)))
101
124
  (local.get $clos))
102
125
  ;; deadline_ns @ offset+16
103
126
  (i64.store (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 16)))
@@ -143,7 +166,7 @@ export default (ctx) => {
143
166
  (local $slot i32)
144
167
  (local $base i32)
145
168
  (local $dispatched i32)
146
- (local $clos f64)
169
+ (local $clos i64)
147
170
  (local $interval f64)
148
171
  (local $id i32)
149
172
  (local.set $dispatched (i32.const 0))
@@ -160,7 +183,7 @@ export default (ctx) => {
160
183
  (local.get $now))
161
184
  (then
162
185
  ;; 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)))))
186
+ (local.set $clos (i64.load (i32.add (local.get $base) (i32.add (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})) (i32.const 8)))))
164
187
  (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
188
  (local.set $id (i32.load (i32.add (local.get $base) (i32.mul (local.get $slot) (i32.const ${ENTRY_SIZE})))))
166
189
  ;; Interval? Reschedule
@@ -173,15 +196,8 @@ export default (ctx) => {
173
196
  ;; Timeout: mark dead
174
197
  (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
198
  (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)))))
199
+ ;; Fire closure with 0 args (shared trampoline)
200
+ (drop (call $__invoke_closure (local.get $clos)))
185
201
  (local.set $dispatched (i32.add (local.get $dispatched) (i32.const 1)))))))
186
202
  (local.set $slot (i32.add (local.get $slot) (i32.const 1)))
187
203
  (br $scan)))
@@ -213,6 +229,8 @@ export default (ctx) => {
213
229
  ;; Loop
214
230
  (br $poll))))`
215
231
 
232
+ ctx.core.stdlib['__invoke_closure'] = invokeClosureFn(false)
233
+
216
234
  // Register globals for timer state
217
235
  // $__timer_queue: i32 — base address of timer array
218
236
  // $__timer_next_id: i32 — next timer ID
@@ -224,18 +242,18 @@ export default (ctx) => {
224
242
  // Emitter: setTimeout(closure, delay) → timer_id
225
243
  ctx.core.emit['setTimeout'] = (closureExpr, delayExpr) => {
226
244
  inc('__timer_add')
227
- const t = temp('tc')
245
+ const t = tempI64('tc')
228
246
  return typed(['block', ['result', 'f64'],
229
- ['local.set', `$${t}`, asF64(emit(closureExpr))],
247
+ ['local.set', `$${t}`, asI64(emit(closureExpr))],
230
248
  ['call', '$__timer_add', ['local.get', `$${t}`], asF64(emit(delayExpr)), ['i32.const', 0]]], 'f64')
231
249
  }
232
250
 
233
251
  // Emitter: setInterval(closure, delay) → timer_id
234
252
  ctx.core.emit['setInterval'] = (closureExpr, delayExpr) => {
235
253
  inc('__timer_add')
236
- const t = temp('tc')
254
+ const t = tempI64('tc')
237
255
  return typed(['block', ['result', 'f64'],
238
- ['local.set', `$${t}`, asF64(emit(closureExpr))],
256
+ ['local.set', `$${t}`, asI64(emit(closureExpr))],
239
257
  ['call', '$__timer_add', ['local.get', `$${t}`], asF64(emit(delayExpr)), ['i32.const', 1]]], 'f64')
240
258
  }
241
259
 
@@ -251,3 +269,43 @@ export default (ctx) => {
251
269
  return typed(['call', '$__timer_cancel', asF64(emit(idExpr))], 'f64')
252
270
  }
253
271
  }
272
+
273
+ const setupJsHost = (ctx) => {
274
+ // Timer callbacks are invoked through __invoke_closure, which always pads to
275
+ // MAX_CLOSURE_ARITY. Set the ABI floor before plan() resolves $ftN width.
276
+ ctx.closure.floor = MAX_CLOSURE_ARITY
277
+
278
+ // env.setTimeout's cb param is i64 (NaN-box bits) to dodge V8's f64 NaN
279
+ // canonicalization at the wasm→JS boundary (same reason as env.print —
280
+ // see module/console.js header). delay is a real numeric f64 (no NaN-box
281
+ // hazard), repeat is i32, return is the timer id (numeric int).
282
+ const needSetTimeout = () => addImportOnce(ctx, 'env', 'setTimeout',
283
+ ['func', '$__set_timeout', ['param', 'i64'], ['param', 'f64'], ['param', 'i32'], ['result', 'f64']])
284
+ const needClearTimeout = () => addImportOnce(ctx, 'env', 'clearTimeout',
285
+ ['func', '$__clear_timeout', ['param', 'f64'], ['result', 'f64']])
286
+
287
+ ctx.core.stdlib['__invoke_closure'] = invokeClosureFn(true)
288
+
289
+ const emitSet = (closureExpr, delayExpr, repeat) => {
290
+ needSetTimeout()
291
+ inc('__invoke_closure')
292
+ return typed(['call', '$__set_timeout',
293
+ ['i64.reinterpret_f64', asF64(emit(closureExpr))],
294
+ asF64(emit(delayExpr)),
295
+ ['i32.const', repeat]], 'f64')
296
+ }
297
+ ctx.core.emit['setTimeout'] = (c, d) => emitSet(c, d, 0)
298
+ ctx.core.emit['setInterval'] = (c, d) => emitSet(c, d, 1)
299
+
300
+ const emitClear = (idExpr) => {
301
+ needClearTimeout()
302
+ return typed(['call', '$__clear_timeout', asF64(emit(idExpr))], 'f64')
303
+ }
304
+ ctx.core.emit['clearTimeout'] = emitClear
305
+ ctx.core.emit['clearInterval'] = emitClear
306
+ }
307
+
308
+ export default (ctx) => {
309
+ if (ctx.transform.host === 'wasi') setupWasi(ctx)
310
+ else setupJsHost(ctx)
311
+ }