jz 0.1.1 → 0.2.1

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,48 +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 { typed, asF64 } from '../src/ir.js'
26
+ import { typed, asF64, asI64, mkPtrIR, NULL_NAN, UNDEF_NAN } from '../src/ir.js'
16
27
  import { emit } from '../src/emit.js'
17
- import { valTypeOf, VAL } from '../src/analyze.js'
18
- import { exprType } from '../src/analyze.js'
19
- import { inc, PTR } from '../src/ctx.js'
28
+ import { valTypeOf, VAL, exprType } from '../src/analyze.js'
29
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
20
30
 
21
- 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) => {
22
55
  Object.assign(ctx.core.stdlibDeps, {
23
56
  __write_val: ['__ptr_type', '__write_str', '__write_num', '__write_int', '__write_byte', '__static_str'],
24
57
  __write_num: ['__ftoa', '__write_str'],
25
58
  __write_int: ['__itoa', '__mkstr', '__write_str'],
26
59
  __write_str: ['__sso_char', '__str_len'],
60
+ __read_stdin: ['__mkstr'],
27
61
  })
28
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']])
29
65
 
30
- // Import fd_write from WASI
31
- ctx.module.imports.push(
32
- ['import', '"wasi_snapshot_preview1"', '"fd_write"',
33
- ['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']])
34
68
 
35
- // __write_str(fd: i32, ptr: f64) — write a NaN-boxed string to fd via iov
36
- // Handles both SSO and heap strings
37
- ctx.core.stdlib['__write_str'] = `(func $__write_str (param $fd i32) (param $ptr f64)
38
- (local $iov i32) (local $type i32) (local $len i32) (local $off i32) (local $buf i32)
39
- ;; 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)
40
71
  (local.set $iov (call $__alloc (i32.const 12)))
41
- (local.set $type (call $__ptr_type (local.get $ptr)))
42
- (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}))
43
74
  (then
44
- ;; SSO: unpack chars to memory buffer, then write
45
- (local.set $len (call $__ptr_aux (local.get $ptr)))
75
+ (local.set $len (i32.and (local.get $aux) (i32.const 7)))
46
76
  (local.set $buf (call $__alloc (local.get $len)))
47
77
  (local.set $off (i32.const 0))
48
78
  (block $done (loop $loop
@@ -54,13 +84,11 @@ export default (ctx) => {
54
84
  (i32.store (local.get $iov) (local.get $buf))
55
85
  (i32.store (i32.add (local.get $iov) (i32.const 4)) (local.get $len)))
56
86
  (else
57
- ;; Heap string: offset points directly to char data
58
87
  (i32.store (local.get $iov) (call $__ptr_offset (local.get $ptr)))
59
88
  (i32.store (i32.add (local.get $iov) (i32.const 4)) (call $__str_len (local.get $ptr)))))
60
89
  (drop (call $__fd_write (local.get $fd) (local.get $iov) (i32.const 1)
61
90
  (i32.add (local.get $iov) (i32.const 8)))))`
62
91
 
63
- // __write_byte(fd: i32, byte: i32) — write a single byte (space, newline)
64
92
  ctx.core.stdlib['__write_byte'] = `(func $__write_byte (param $fd i32) (param $byte i32)
65
93
  (local $iov i32)
66
94
  (local.set $iov (call $__alloc (i32.const 13)))
@@ -70,72 +98,66 @@ export default (ctx) => {
70
98
  (drop (call $__fd_write (local.get $fd) (local.get $iov) (i32.const 1)
71
99
  (i32.add (local.get $iov) (i32.const 8)))))`
72
100
 
73
- // __write_num(fd: i32, val: f64) — convert number to string, write to fd
74
101
  ctx.core.stdlib['__write_num'] = `(func $__write_num (param $fd i32) (param $val f64)
75
- (call $__write_str (local.get $fd) (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0))))`
76
- // __write_int(fd: i32, val: f64) — convert integer to string, write to fd
77
- // 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)))))`
78
103
  ctx.core.stdlib['__write_int'] = `(func $__write_int (param $fd i32) (param $val f64)
79
104
  (local $buf i32)
80
105
  (local.set $buf (call $__alloc (i32.const 12)))
81
106
  (call $__write_str (local.get $fd)
82
- (call $__mkstr (local.get $buf) (call $__itoa (i32.trunc_sat_f64_s (local.get $val)) (local.get $buf)))))`
83
- // __write_val(fd: i32, val: f64) — write any value, auto-detecting type
84
- ctx.core.stdlib['__write_val'] = `(func $__write_val (param $fd i32) (param $val f64)
85
- (local $type i32)
86
- ;; Not NaN plain number
87
- (if (f64.eq (local.get $val) (local.get $val))
88
- (then (call $__write_num (local.get $fd) (local.get $val)) (return)))
89
- ;; 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)))
90
117
  (local.set $type (call $__ptr_type (local.get $val)))
91
118
  (if (i32.eqz (local.get $type))
92
- (then (call $__write_str (local.get $fd) (call $__static_str (i32.const 0))) (return)))
93
- ;; String pointer
94
- (if (i32.or (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
95
- (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}))
96
121
  (then (call $__write_str (local.get $fd) (local.get $val)) (return)))
97
- ;; Array/Object placeholder
98
- (call $__write_str (local.get $fd) (call $__static_str
122
+ (call $__write_str (local.get $fd) (i64.reinterpret_f64 (call $__static_str
99
123
  (if (result i32) (i32.eq (local.get $type) (i32.const 1))
100
- (then (i32.const 7)) (else (i32.const 8))))))`
101
-
102
- // Template-literal concat chains (`a${x}b`) lower to ['()', ['.', X, 'concat'], Y]
103
- // in prepare. Walking left from the chain root recovers the parts in order; if the
104
- // base is a `['str', ...]` it's a template-shaped chain (vs an arbitrary user
105
- // .concat call). Returning the parts lets console.log skip __str_concat/__to_str
106
- // entirely biquad's only string churn is the perf-summary line.
107
- const flattenTemplateConcat = (node) => {
108
- const parts = []
109
- let n = node
110
- while (Array.isArray(n) && n[0] === '()' && n.length === 3 &&
111
- Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'concat') {
112
- parts.unshift(n[2])
113
- n = n[1][1]
114
- }
115
- if (!(Array.isArray(n) && n[0] === 'str')) return null
116
- parts.unshift(n)
117
- 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')
118
147
  }
119
148
 
120
- // console.log(...args) — variadic, each arg separated by space, followed by newline.
121
- // Per-arg dispatch is monomorphized when valTypeOf proves the arg is a string or
122
- // number — direct __write_str / __write_num skips the polymorphic __write_val
123
- // helper (and its transitive __ptr_type / __static_str / number-or-string-test
124
- // arms), which is the entire stdlib floor for size-conscious benches.
125
149
  const makeConsole = (method, fd) => {
126
150
  ctx.core.emit[`console.${method}`] = (...args) => {
151
+ needFdWrite()
127
152
  inc('__write_byte')
128
153
  const ir = []
129
154
  const writePart = (part) => {
130
- // Empty string segments (template boundaries) write nothing.
131
155
  if (Array.isArray(part) && part[0] === 'str' && part[1] === '') return
132
156
  const vt = valTypeOf(part)
133
157
  if (vt === VAL.STRING) {
134
158
  inc('__write_str')
135
- ir.push(['call', '$__write_str', ['i32.const', fd], asF64(emit(part))])
159
+ ir.push(['call', '$__write_str', ['i32.const', fd], asI64(emit(part))])
136
160
  } else if (vt === VAL.NUMBER) {
137
- // Integer-valued numbers use __write_int (__itoa) instead of __write_num (__ftoa).
138
- // exprType sees i32 for literals, bitwise ops, .length on arrays, Math.imul, etc.
139
161
  if (exprType(part, ctx.func.locals) === 'i32') {
140
162
  inc('__write_int')
141
163
  ir.push(['call', '$__write_int', ['i32.const', fd], asF64(emit(part))])
@@ -145,17 +167,17 @@ export default (ctx) => {
145
167
  }
146
168
  } else {
147
169
  inc('__write_val')
148
- ir.push(['call', '$__write_val', ['i32.const', fd], asF64(emit(part))])
170
+ ir.push(['call', '$__write_val', ['i32.const', fd], asI64(emit(part))])
149
171
  }
150
172
  }
151
173
  for (let i = 0; i < args.length; i++) {
152
- 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]])
153
175
  const parts = flattenTemplateConcat(args[i])
154
176
  if (parts) for (const p of parts) writePart(p)
155
177
  else writePart(args[i])
156
178
  }
157
- ir.push(['call', '$__write_byte', ['i32.const', fd], ['i32.const', 10]]) // newline
158
- 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])
159
181
  return typed(['block', ['result', 'f64'], ...ir], 'f64')
160
182
  }
161
183
  }
@@ -164,29 +186,88 @@ export default (ctx) => {
164
186
  makeConsole('warn', 2)
165
187
  makeConsole('error', 2)
166
188
 
167
- // === 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']])
168
191
 
169
- ctx.module.imports.push(
170
- ['import', '"wasi_snapshot_preview1"', '"clock_time_get"',
171
- ['func', '$__clock_time_get', ['param', 'i32'], ['param', 'i64'], ['param', 'i32'], ['result', 'i32']]])
172
-
173
- // __time_ms(clock_id) → f64 milliseconds
174
- // clock_time_get writes i64 nanoseconds to memory, we convert to f64 ms
175
192
  ctx.core.stdlib['__time_ms'] = `(func $__time_ms (param $clock i32) (result f64)
176
193
  (drop (call $__clock_time_get (local.get $clock) (i64.const 1000) (i32.const 0)))
177
194
  (f64.div (f64.convert_i64_u (i64.load (i32.const 0))) (f64.const 1000000)))`
178
195
 
179
196
  ctx.core.emit['Date.now'] = () => {
197
+ needClock()
180
198
  inc('__time_ms')
181
- return typed(['call', '$__time_ms', ['i32.const', 0]], 'f64') // clock 0 = realtime
199
+ return typed(['call', '$__time_ms', ['i32.const', 0]], 'f64')
182
200
  }
183
-
184
201
  ctx.core.emit['performance.now'] = () => {
202
+ needClock()
185
203
  inc('__time_ms')
186
- return typed(['call', '$__time_ms', ['i32.const', 1]], 'f64') // clock 1 = monotonic
204
+ return typed(['call', '$__time_ms', ['i32.const', 1]], 'f64')
187
205
  }
206
+ ctx.core.emit['console.now'] = ctx.core.emit['Date.now']
207
+ ctx.core.emit['console.perfNow'] = ctx.core.emit['performance.now']
208
+ }
188
209
 
189
- // 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
+ }
190
266
  ctx.core.emit['console.now'] = ctx.core.emit['Date.now']
191
267
  ctx.core.emit['console.perfNow'] = ctx.core.emit['performance.now']
192
268
  }
269
+
270
+ export default (ctx) => {
271
+ if (ctx.transform.host === 'wasi') setupWasi(ctx)
272
+ else setupJsHost(ctx)
273
+ }