jz 0.1.0 → 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/console.js CHANGED
@@ -1,46 +1,78 @@
1
1
  /**
2
- * WASI module — console.log/warn/error via fd_write.
2
+ * Console + clocks module — two host-mode lowerings.
3
3
  *
4
- * Imports wasi_snapshot_preview1.fd_write standard WASI Preview 1.
5
- * Output .wasm runs natively on wasmtime/wasmer/deno.
6
- * For browser/Node, use jz/wasi polyfill.
4
+ * `host: 'js'` (default): emit `env.print(val: i64, fd: i32, sep: i32)` and
5
+ * `env.now(clock: i32) -> f64`. The JS host (src/host.js) wires both
6
+ * automatically `print` reads the NaN-boxed value via `mem.read`, so
7
+ * stringification happens host-side (no __ftoa / __write_str / __write_val
8
+ * stdlib in the binary). `sep`: 10=newline, 32=space, 0=no separator.
9
+ * `clock`: 0=Date.now (epoch ms), 1=performance.now (monotonic ms).
7
10
  *
8
- * console.log(a, b, c) serialize each arg to string bytes,
9
- * write space-separated to fd=1, append newline.
10
- * console.warn/error fd=2.
11
+ * The val param is i64 (not f64): V8 (notably node 22 on x64) intermittently
12
+ * canonicalizes f64 NaN payloads at the wasm→JS boundary, collapsing the
13
+ * high-mantissa discriminator bits and corrupting NaN-boxed pointers. i64
14
+ * is integer-typed and preserves all 64 bits exactly. Host reinterprets the
15
+ * bits as f64 with a DataView before calling mem.read.
11
16
  *
12
- * @module wasi
17
+ * `host: 'wasi'`: emit `wasi_snapshot_preview1.fd_write` + `clock_time_get`.
18
+ * Output runs natively on wasmtime/wasmer/deno and on browsers/Node via the
19
+ * tiny `jz/wasi` polyfill auto-applied by the `jz()` runtime.
20
+ *
21
+ * console.log/warn/error: variadic. fd=1 for log, fd=2 for warn/error.
22
+ *
23
+ * @module console
13
24
  */
14
25
 
15
- import { emit, typed, asF64, valTypeOf, VAL } from '../src/compile.js'
16
- import { exprType } from '../src/analyze.js'
17
- import { inc, PTR } from '../src/ctx.js'
26
+ import { typed, asF64, asI64, mkPtrIR, NULL_NAN, UNDEF_NAN } from '../src/ir.js'
27
+ import { emit } from '../src/emit.js'
28
+ import { valTypeOf, VAL, exprType } from '../src/analyze.js'
29
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
18
30
 
19
- export default (ctx) => {
31
+ const addImportOnce = (ctx, mod, name, fn) => {
32
+ if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
33
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
34
+ }
35
+
36
+ // Template-literal concat chains (`a${x}b`) lower to ['()', ['.', X, 'concat'], Y]
37
+ // in prepare. Walking left from the chain root recovers the parts in order; if the
38
+ // base is a `['str', ...]` it's a template-shaped chain (vs an arbitrary user
39
+ // .concat call). Returning the parts lets console.log skip __str_concat/__to_str
40
+ // entirely — biquad's only string churn is the perf-summary line.
41
+ const flattenTemplateConcat = (node) => {
42
+ const parts = []
43
+ let n = node
44
+ while (Array.isArray(n) && n[0] === '()' && n.length === 3 &&
45
+ Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'concat') {
46
+ parts.unshift(n[2])
47
+ n = n[1][1]
48
+ }
49
+ if (!(Array.isArray(n) && n[0] === 'str')) return null
50
+ parts.unshift(n)
51
+ return parts
52
+ }
53
+
54
+ const setupWasi = (ctx) => {
20
55
  Object.assign(ctx.core.stdlibDeps, {
21
56
  __write_val: ['__ptr_type', '__write_str', '__write_num', '__write_int', '__write_byte', '__static_str'],
22
57
  __write_num: ['__ftoa', '__write_str'],
23
58
  __write_int: ['__itoa', '__mkstr', '__write_str'],
24
59
  __write_str: ['__sso_char', '__str_len'],
60
+ __read_stdin: ['__mkstr'],
25
61
  })
26
62
 
63
+ const needFdWrite = () => addImportOnce(ctx, 'wasi_snapshot_preview1', 'fd_write',
64
+ ['func', '$__fd_write', ['param', 'i32'], ['param', 'i32'], ['param', 'i32'], ['param', 'i32'], ['result', 'i32']])
27
65
 
28
- // Import fd_write from WASI
29
- ctx.module.imports.push(
30
- ['import', '"wasi_snapshot_preview1"', '"fd_write"',
31
- ['func', '$__fd_write', ['param', 'i32'], ['param', 'i32'], ['param', 'i32'], ['param', 'i32'], ['result', 'i32']]])
66
+ const needFdRead = () => addImportOnce(ctx, 'wasi_snapshot_preview1', 'fd_read',
67
+ ['func', '$__fd_read', ['param', 'i32'], ['param', 'i32'], ['param', 'i32'], ['param', 'i32'], ['result', 'i32']])
32
68
 
33
- // __write_str(fd: i32, ptr: f64) — write a NaN-boxed string to fd via iov
34
- // Handles both SSO and heap strings
35
- ctx.core.stdlib['__write_str'] = `(func $__write_str (param $fd i32) (param $ptr f64)
36
- (local $iov i32) (local $type i32) (local $len i32) (local $off i32) (local $buf i32)
37
- ;; Allocate iov (8 bytes: ptr + len) + nwritten (4 bytes)
69
+ ctx.core.stdlib['__write_str'] = `(func $__write_str (param $fd i32) (param $ptr i64)
70
+ (local $iov i32) (local $aux i32) (local $len i32) (local $off i32) (local $buf i32)
38
71
  (local.set $iov (call $__alloc (i32.const 12)))
39
- (local.set $type (call $__ptr_type (local.get $ptr)))
40
- (if (i32.eq (local.get $type) (i32.const ${PTR.SSO}))
72
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
73
+ (if (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
41
74
  (then
42
- ;; SSO: unpack chars to memory buffer, then write
43
- (local.set $len (call $__ptr_aux (local.get $ptr)))
75
+ (local.set $len (i32.and (local.get $aux) (i32.const 7)))
44
76
  (local.set $buf (call $__alloc (local.get $len)))
45
77
  (local.set $off (i32.const 0))
46
78
  (block $done (loop $loop
@@ -52,13 +84,11 @@ export default (ctx) => {
52
84
  (i32.store (local.get $iov) (local.get $buf))
53
85
  (i32.store (i32.add (local.get $iov) (i32.const 4)) (local.get $len)))
54
86
  (else
55
- ;; Heap string: offset points directly to char data
56
87
  (i32.store (local.get $iov) (call $__ptr_offset (local.get $ptr)))
57
88
  (i32.store (i32.add (local.get $iov) (i32.const 4)) (call $__str_len (local.get $ptr)))))
58
89
  (drop (call $__fd_write (local.get $fd) (local.get $iov) (i32.const 1)
59
90
  (i32.add (local.get $iov) (i32.const 8)))))`
60
91
 
61
- // __write_byte(fd: i32, byte: i32) — write a single byte (space, newline)
62
92
  ctx.core.stdlib['__write_byte'] = `(func $__write_byte (param $fd i32) (param $byte i32)
63
93
  (local $iov i32)
64
94
  (local.set $iov (call $__alloc (i32.const 13)))
@@ -68,72 +98,66 @@ export default (ctx) => {
68
98
  (drop (call $__fd_write (local.get $fd) (local.get $iov) (i32.const 1)
69
99
  (i32.add (local.get $iov) (i32.const 8)))))`
70
100
 
71
- // __write_num(fd: i32, val: f64) — convert number to string, write to fd
72
101
  ctx.core.stdlib['__write_num'] = `(func $__write_num (param $fd i32) (param $val f64)
73
- (call $__write_str (local.get $fd) (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0))))`
74
- // __write_int(fd: i32, val: f64) — convert integer to string, write to fd
75
- // Skips __ftoa (~600 B) for values proven integer by exprType.
102
+ (call $__write_str (local.get $fd) (i64.reinterpret_f64 (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))`
76
103
  ctx.core.stdlib['__write_int'] = `(func $__write_int (param $fd i32) (param $val f64)
77
104
  (local $buf i32)
78
105
  (local.set $buf (call $__alloc (i32.const 12)))
79
106
  (call $__write_str (local.get $fd)
80
- (call $__mkstr (local.get $buf) (call $__itoa (i32.trunc_sat_f64_s (local.get $val)) (local.get $buf)))))`
81
- // __write_val(fd: i32, val: f64) — write any value, auto-detecting type
82
- ctx.core.stdlib['__write_val'] = `(func $__write_val (param $fd i32) (param $val f64)
83
- (local $type i32)
84
- ;; Not NaN plain number
85
- (if (f64.eq (local.get $val) (local.get $val))
86
- (then (call $__write_num (local.get $fd) (local.get $val)) (return)))
87
- ;; NaN: check if it's a pointer (type > 0) or plain NaN (type = 0)
107
+ (i64.reinterpret_f64 (call $__mkstr (local.get $buf) (call $__itoa (i32.trunc_sat_f64_s (local.get $val)) (local.get $buf))))))`
108
+ ctx.core.stdlib['__write_val'] = `(func $__write_val (param $fd i32) (param $val i64)
109
+ (local $type i32) (local $f f64)
110
+ (local.set $f (f64.reinterpret_i64 (local.get $val)))
111
+ (if (f64.eq (local.get $f) (local.get $f))
112
+ (then (call $__write_num (local.get $fd) (local.get $f)) (return)))
113
+ (if (i64.eq (local.get $val) (i64.const ${NULL_NAN}))
114
+ (then (call $__write_str (local.get $fd) (i64.reinterpret_f64 (call $__static_str (i32.const 5)))) (return)))
115
+ (if (i64.eq (local.get $val) (i64.const ${UNDEF_NAN}))
116
+ (then (call $__write_str (local.get $fd) (i64.reinterpret_f64 (call $__static_str (i32.const 6)))) (return)))
88
117
  (local.set $type (call $__ptr_type (local.get $val)))
89
118
  (if (i32.eqz (local.get $type))
90
- (then (call $__write_str (local.get $fd) (call $__static_str (i32.const 0))) (return)))
91
- ;; String pointer
92
- (if (i32.or (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
93
- (i32.eq (local.get $type) (i32.const ${PTR.SSO})))
119
+ (then (call $__write_str (local.get $fd) (i64.reinterpret_f64 (call $__static_str (i32.const 0)))) (return)))
120
+ (if (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
94
121
  (then (call $__write_str (local.get $fd) (local.get $val)) (return)))
95
- ;; Array/Object placeholder
96
- (call $__write_str (local.get $fd) (call $__static_str
122
+ (call $__write_str (local.get $fd) (i64.reinterpret_f64 (call $__static_str
97
123
  (if (result i32) (i32.eq (local.get $type) (i32.const 1))
98
- (then (i32.const 7)) (else (i32.const 8))))))`
99
-
100
- // Template-literal concat chains (`a${x}b`) lower to ['()', ['.', X, 'concat'], Y]
101
- // in prepare. Walking left from the chain root recovers the parts in order; if the
102
- // base is a `['str', ...]` it's a template-shaped chain (vs an arbitrary user
103
- // .concat call). Returning the parts lets console.log skip __str_concat/__to_str
104
- // entirely biquad's only string churn is the perf-summary line.
105
- const flattenTemplateConcat = (node) => {
106
- const parts = []
107
- let n = node
108
- while (Array.isArray(n) && n[0] === '()' && n.length === 3 &&
109
- Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'concat') {
110
- parts.unshift(n[2])
111
- n = n[1][1]
112
- }
113
- if (!(Array.isArray(n) && n[0] === 'str')) return null
114
- parts.unshift(n)
115
- return parts
124
+ (then (i32.const 7)) (else (i32.const 8)))))))`
125
+
126
+ ctx.core.stdlib['__read_stdin'] = `(func $__read_stdin (result f64)
127
+ (local $iov i32) (local $nio i32) (local $buf i32) (local $total i32) (local $n i32)
128
+ (local.set $iov (call $__alloc (i32.const 8)))
129
+ (local.set $nio (call $__alloc (i32.const 4)))
130
+ (local.set $buf (call $__alloc (i32.const 65536)))
131
+ (local.set $total (i32.const 0))
132
+ (block $eof (loop $read
133
+ (i32.store (local.get $iov) (i32.add (local.get $buf) (local.get $total)))
134
+ (i32.store offset=4 (local.get $iov) (i32.sub (i32.const 65536) (local.get $total)))
135
+ (drop (call $__fd_read (i32.const 0) (local.get $iov) (i32.const 1) (local.get $nio)))
136
+ (local.set $n (i32.load (local.get $nio)))
137
+ (br_if $eof (i32.eqz (local.get $n)))
138
+ (local.set $total (i32.add (local.get $total) (local.get $n)))
139
+ (br_if $eof (i32.ge_s (local.get $total) (i32.const 65536)))
140
+ (br $read)))
141
+ (call $__mkstr (local.get $buf) (local.get $total)))`
142
+
143
+ ctx.core.emit['readStdin'] = () => {
144
+ needFdRead()
145
+ inc('__read_stdin')
146
+ return typed(['call', '$__read_stdin'], 'f64')
116
147
  }
117
148
 
118
- // console.log(...args) — variadic, each arg separated by space, followed by newline.
119
- // Per-arg dispatch is monomorphized when valTypeOf proves the arg is a string or
120
- // number — direct __write_str / __write_num skips the polymorphic __write_val
121
- // helper (and its transitive __ptr_type / __static_str / number-or-string-test
122
- // arms), which is the entire stdlib floor for size-conscious benches.
123
149
  const makeConsole = (method, fd) => {
124
150
  ctx.core.emit[`console.${method}`] = (...args) => {
151
+ needFdWrite()
125
152
  inc('__write_byte')
126
153
  const ir = []
127
154
  const writePart = (part) => {
128
- // Empty string segments (template boundaries) write nothing.
129
155
  if (Array.isArray(part) && part[0] === 'str' && part[1] === '') return
130
156
  const vt = valTypeOf(part)
131
157
  if (vt === VAL.STRING) {
132
158
  inc('__write_str')
133
- ir.push(['call', '$__write_str', ['i32.const', fd], asF64(emit(part))])
159
+ ir.push(['call', '$__write_str', ['i32.const', fd], asI64(emit(part))])
134
160
  } else if (vt === VAL.NUMBER) {
135
- // Integer-valued numbers use __write_int (__itoa) instead of __write_num (__ftoa).
136
- // exprType sees i32 for literals, bitwise ops, .length on arrays, Math.imul, etc.
137
161
  if (exprType(part, ctx.func.locals) === 'i32') {
138
162
  inc('__write_int')
139
163
  ir.push(['call', '$__write_int', ['i32.const', fd], asF64(emit(part))])
@@ -143,17 +167,17 @@ export default (ctx) => {
143
167
  }
144
168
  } else {
145
169
  inc('__write_val')
146
- ir.push(['call', '$__write_val', ['i32.const', fd], asF64(emit(part))])
170
+ ir.push(['call', '$__write_val', ['i32.const', fd], asI64(emit(part))])
147
171
  }
148
172
  }
149
173
  for (let i = 0; i < args.length; i++) {
150
- if (i > 0) ir.push(['call', '$__write_byte', ['i32.const', fd], ['i32.const', 32]]) // space
174
+ if (i > 0) ir.push(['call', '$__write_byte', ['i32.const', fd], ['i32.const', 32]])
151
175
  const parts = flattenTemplateConcat(args[i])
152
176
  if (parts) for (const p of parts) writePart(p)
153
177
  else writePart(args[i])
154
178
  }
155
- ir.push(['call', '$__write_byte', ['i32.const', fd], ['i32.const', 10]]) // newline
156
- ir.push(['f64.const', 0]) // return undefined
179
+ ir.push(['call', '$__write_byte', ['i32.const', fd], ['i32.const', 10]])
180
+ ir.push(['f64.const', 0])
157
181
  return typed(['block', ['result', 'f64'], ...ir], 'f64')
158
182
  }
159
183
  }
@@ -162,29 +186,88 @@ export default (ctx) => {
162
186
  makeConsole('warn', 2)
163
187
  makeConsole('error', 2)
164
188
 
165
- // === Date.now / performance.now via WASI clock_time_get ===
189
+ const needClock = () => addImportOnce(ctx, 'wasi_snapshot_preview1', 'clock_time_get',
190
+ ['func', '$__clock_time_get', ['param', 'i32'], ['param', 'i64'], ['param', 'i32'], ['result', 'i32']])
166
191
 
167
- ctx.module.imports.push(
168
- ['import', '"wasi_snapshot_preview1"', '"clock_time_get"',
169
- ['func', '$__clock_time_get', ['param', 'i32'], ['param', 'i64'], ['param', 'i32'], ['result', 'i32']]])
170
-
171
- // __time_ms(clock_id) → f64 milliseconds
172
- // clock_time_get writes i64 nanoseconds to memory, we convert to f64 ms
173
192
  ctx.core.stdlib['__time_ms'] = `(func $__time_ms (param $clock i32) (result f64)
174
193
  (drop (call $__clock_time_get (local.get $clock) (i64.const 1000) (i32.const 0)))
175
194
  (f64.div (f64.convert_i64_u (i64.load (i32.const 0))) (f64.const 1000000)))`
176
195
 
177
196
  ctx.core.emit['Date.now'] = () => {
197
+ needClock()
178
198
  inc('__time_ms')
179
- return typed(['call', '$__time_ms', ['i32.const', 0]], 'f64') // clock 0 = realtime
199
+ return typed(['call', '$__time_ms', ['i32.const', 0]], 'f64')
180
200
  }
181
-
182
201
  ctx.core.emit['performance.now'] = () => {
202
+ needClock()
183
203
  inc('__time_ms')
184
- return typed(['call', '$__time_ms', ['i32.const', 1]], 'f64') // clock 1 = monotonic
204
+ return typed(['call', '$__time_ms', ['i32.const', 1]], 'f64')
185
205
  }
206
+ ctx.core.emit['console.now'] = ctx.core.emit['Date.now']
207
+ ctx.core.emit['console.perfNow'] = ctx.core.emit['performance.now']
208
+ }
186
209
 
187
- // Aliases for explicit import: import { now, perfNow } from 'console'
210
+ const setupJsHost = (ctx) => {
211
+ const needPrint = () => addImportOnce(ctx, 'env', 'print',
212
+ ['func', '$__print', ['param', 'i64'], ['param', 'i32'], ['param', 'i32']])
213
+ const needNow = () => addImportOnce(ctx, 'env', 'now',
214
+ ['func', '$__now', ['param', 'i32'], ['result', 'f64']])
215
+
216
+ // Empty SSO string ("") for zero-arg console.log() — host reads as "".
217
+ const emptyStr = () => mkPtrIR(PTR.STRING, LAYOUT.SSO_BIT, 0)
218
+ const asI64Bits = (e) => ['i64.reinterpret_f64', asF64(emit(e))]
219
+
220
+ const makeConsole = (method, fd) => {
221
+ ctx.core.emit[`console.${method}`] = (...args) => {
222
+ needPrint()
223
+ // Each segment carries its trailing separator (0=none, 32=space, 10=newline).
224
+ // Template-concat chains (`a${x}b`) flatten to per-`${}` segments — the host
225
+ // stringifies, so jz drops __str_concat/__to_str entirely.
226
+ const segments = []
227
+ for (let i = 0; i < args.length; i++) {
228
+ const before = segments.length
229
+ const flat = flattenTemplateConcat(args[i])
230
+ const sub = flat || [args[i]]
231
+ for (const p of sub) {
232
+ if (Array.isArray(p) && p[0] === 'str' && p[1] === '') continue
233
+ segments.push({ expr: asI64Bits(p), sep: 0 })
234
+ }
235
+ // Empty arg (`console.log('', 'a')`) still needs to mark its boundary
236
+ // so the inter-arg space lands in the right place.
237
+ if (segments.length === before) segments.push({ expr: ['i64.reinterpret_f64', emptyStr()], sep: 0 })
238
+ if (i < args.length - 1) segments[segments.length - 1].sep = 32
239
+ }
240
+ const ir = []
241
+ if (segments.length === 0) {
242
+ ir.push(['call', '$__print', ['i64.reinterpret_f64', emptyStr()], ['i32.const', fd], ['i32.const', 10]])
243
+ } else {
244
+ segments[segments.length - 1].sep = 10
245
+ for (const { expr, sep } of segments) {
246
+ ir.push(['call', '$__print', expr, ['i32.const', fd], ['i32.const', sep]])
247
+ }
248
+ }
249
+ ir.push(['f64.const', 0])
250
+ return typed(['block', ['result', 'f64'], ...ir], 'f64')
251
+ }
252
+ }
253
+
254
+ makeConsole('log', 1)
255
+ makeConsole('warn', 2)
256
+ makeConsole('error', 2)
257
+
258
+ ctx.core.emit['Date.now'] = () => {
259
+ needNow()
260
+ return typed(['call', '$__now', ['i32.const', 0]], 'f64')
261
+ }
262
+ ctx.core.emit['performance.now'] = () => {
263
+ needNow()
264
+ return typed(['call', '$__now', ['i32.const', 1]], 'f64')
265
+ }
188
266
  ctx.core.emit['console.now'] = ctx.core.emit['Date.now']
189
267
  ctx.core.emit['console.perfNow'] = ctx.core.emit['performance.now']
190
268
  }
269
+
270
+ export default (ctx) => {
271
+ if (ctx.transform.host === 'wasi') setupWasi(ctx)
272
+ else setupJsHost(ctx)
273
+ }