jz 0.2.1 → 0.3.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/src/ctx.js CHANGED
@@ -209,6 +209,7 @@ export function reset(proto, globals) {
209
209
  ctx.error = {
210
210
  src: '',
211
211
  loc: null,
212
+ node: null,
212
213
  }
213
214
 
214
215
  ctx.transform = {
@@ -259,15 +260,42 @@ export function reset(proto, globals) {
259
260
 
260
261
  /** Throw with source location context. */
261
262
  export function err(msg) {
263
+ let detail = msg
264
+
262
265
  if (ctx.error.loc != null && ctx.error.src) {
263
266
  const before = ctx.error.src.slice(0, ctx.error.loc)
264
267
  const line = before.split('\n').length
265
268
  const col = ctx.error.loc - before.lastIndexOf('\n')
266
269
  const src = ctx.error.src.split('\n')[line - 1]
267
- const detail = `${msg}\n at line ${line}:${col}\n ${src}\n ${' '.repeat(col - 1)}^`
268
- const e = new Error(detail)
269
- e.stack = `${e.name}: ${detail}\n${e.stack.split('\n').slice(1).join('\n')}`
270
- throw e
270
+ detail += `\n at line ${line}:${col}\n ${src}\n ${' '.repeat(col - 1)}^`
271
+ }
272
+
273
+ if (ctx.func.current?.name) {
274
+ detail += `\n in function: ${ctx.func.current.name}`
275
+ }
276
+
277
+ if (ctx.error.node != null) {
278
+ detail += `\n current AST: ${formatErrorNode(ctx.error.node)}`
271
279
  }
272
- throw new Error(msg)
280
+
281
+ const e = new Error(detail)
282
+ const stackLines = e.stack.split('\n')
283
+ const firstFrame = stackLines.findIndex(line => line.trimStart().startsWith('at '))
284
+ const frames = firstFrame >= 0 ? stackLines.slice(firstFrame) : stackLines.slice(1)
285
+ e.stack = `${e.name}: ${detail}\n${frames.join('\n')}`
286
+ throw e
287
+ }
288
+
289
+ function formatErrorNode(node) {
290
+ const seen = new WeakSet()
291
+ const json = JSON.stringify(node, (_key, value) => {
292
+ if (typeof value === 'bigint') return `${value}n`
293
+ if (typeof value === 'symbol') return value.toString()
294
+ if (Array.isArray(value)) {
295
+ if (seen.has(value)) return '[Circular]'
296
+ seen.add(value)
297
+ }
298
+ return value
299
+ })
300
+ return json.length > 2000 ? `${json.slice(0, 2000)}...` : json
273
301
  }
package/src/emit.js CHANGED
@@ -23,7 +23,13 @@
23
23
  */
24
24
 
25
25
  import { ctx, err, inc, PTR } from './ctx.js'
26
- import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey } from './analyze.js'
26
+ import { T, VAL, nonNegIntLiteral, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey } from './analyze.js'
27
+ import {
28
+ isReassigned, hasOwnContinue, hasOwnBreakOrContinue, containsNestedClosure,
29
+ containsNestedLoop, nestedSmallLoopBudget, containsDeclOf, cloneWithSubst,
30
+ containsKnownTypedArrayIndex, smallConstForTripCount, isTerminator,
31
+ MAX_SMALL_FOR_UNROLL, MAX_NESTED_FOR_UNROLL,
32
+ } from './ast.js'
27
33
  import {
28
34
  typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
29
35
  NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
@@ -168,11 +174,6 @@ function stringLiteral(node) {
168
174
  return null
169
175
  }
170
176
 
171
- function nonNegIntLiteral(node) {
172
- const n = intLiteralValue(node)
173
- return n != null && n >= 0 ? n : null
174
- }
175
-
176
177
  function emitSingleCharIndexCmp(a, b, negate = false) {
177
178
  const leftLit = stringLiteral(a)
178
179
  const rightLit = stringLiteral(b)
@@ -269,142 +270,6 @@ function extractRefinements(cond, out, sense = true) {
269
270
  return out
270
271
  }
271
272
 
272
- /** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`.
273
- * Conservative over-reject: if unsure, treat as written.
274
- * `let`/`const` declarations are NOT reassignments — only the initializer expressions
275
- * inside them are scanned. (Treating `let g = ...` as a write of `g` would defeat A3.) */
276
- export function isReassigned(body, name) {
277
- if (!Array.isArray(body)) return false
278
- const op = body[0]
279
- if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%='
280
- || op === '&=' || op === '|=' || op === '^=' || op === '<<=' || op === '>>=' || op === '>>>='
281
- || op === '||=' || op === '&&=' || op === '??=') {
282
- if (body[1] === name) return true
283
- }
284
- if ((op === '++' || op === '--') && body[1] === name) return true
285
- if (op === 'let' || op === 'const') {
286
- // Each decl item is either a bare name (string) or `['=', pattern, init]`.
287
- // Only the init expression can contain real reassignments — recurse into it only.
288
- for (let i = 1; i < body.length; i++) {
289
- const d = body[i]
290
- if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
291
- }
292
- return false
293
- }
294
- for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
295
- return false
296
- }
297
-
298
- /** Does `body` contain a `continue` that targets THIS loop?
299
- * A `continue` inside a nested `for`/`while`/`do` targets the inner loop, so we don't count it. */
300
- function hasOwnContinue(body) {
301
- if (!Array.isArray(body)) return false
302
- const op = body[0]
303
- if (op === 'continue') return true
304
- if (op === 'for' || op === 'while' || op === 'do') return false
305
- for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
306
- return false
307
- }
308
-
309
- function hasOwnBreakOrContinue(body) {
310
- if (!Array.isArray(body)) return false
311
- const op = body[0]
312
- if (op === 'break' || op === 'continue') return true
313
- if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
314
- for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
315
- return false
316
- }
317
-
318
- function containsNestedClosure(body) {
319
- if (!Array.isArray(body)) return false
320
- if (body[0] === '=>') return true
321
- for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
322
- return false
323
- }
324
-
325
- function containsNestedLoop(body) {
326
- if (!Array.isArray(body)) return false
327
- const op = body[0]
328
- if (op === 'for' || op === 'while' || op === 'do') return true
329
- if (op === '=>') return false
330
- for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
331
- return false
332
- }
333
-
334
- function nestedSmallLoopBudget(body) {
335
- if (!Array.isArray(body)) return 1
336
- if (body[0] === '=>') return 1
337
- if (body[0] === 'for') {
338
- const [, init, cond, step, loopBody] = body
339
- const n = smallConstForTripCount(init, cond, step)
340
- return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
341
- }
342
- let max = 1
343
- for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
344
- return max
345
- }
346
-
347
- function containsDeclOf(body, name) {
348
- if (!Array.isArray(body)) return false
349
- const op = body[0]
350
- if (op === '=>') return false
351
- if (op === 'let' || op === 'const') {
352
- for (let i = 1; i < body.length; i++) {
353
- const d = body[i]
354
- if (d === name) return true
355
- if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
356
- }
357
- }
358
- for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
359
- return false
360
- }
361
-
362
- function intLiteralValue(expr) {
363
- let v = null
364
- if (typeof expr === 'number') v = expr
365
- else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
366
- else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
367
- else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
368
- return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
369
- }
370
-
371
- function cloneWithSubst(node, name, value) {
372
- if (node === name) return [null, value]
373
- if (!Array.isArray(node)) return node
374
- if (node[0] === '=>') return node
375
- return node.map(x => cloneWithSubst(x, name, value))
376
- }
377
-
378
- const MAX_SMALL_FOR_UNROLL = 8
379
- const MAX_NESTED_FOR_UNROLL = 64
380
-
381
- function containsKnownTypedArrayIndex(body) {
382
- if (!Array.isArray(body)) return false
383
- if (body[0] === '=>') return false
384
- if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
385
- for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
386
- return false
387
- }
388
-
389
- function smallConstForTripCount(init, cond, step) {
390
- if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
391
- const decl = init[1]
392
- if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
393
- const name = decl[1]
394
- const start = intLiteralValue(decl[2])
395
- if (start !== 0) return null
396
-
397
- if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
398
- const end = intLiteralValue(cond[2])
399
- if (end == null || end < 0 || end > MAX_SMALL_FOR_UNROLL) return null
400
-
401
- const stepOk = Array.isArray(step) && (
402
- (step[0] === '++' && step[1] === name) ||
403
- (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
404
- )
405
- return stepOk ? end : null
406
- }
407
-
408
273
  function unrollSmallConstFor(init, cond, step, body) {
409
274
  const end = smallConstForTripCount(init, cond, step)
410
275
  if (end == null) return null
@@ -422,21 +287,23 @@ function unrollSmallConstFor(init, cond, step, body) {
422
287
  return out
423
288
  }
424
289
 
425
- /** Does `body` always exit the enclosing scope (return / throw / break / continue)?
426
- * Used for early-return refinement: after `if (!guard) return`, `guard` holds for the rest. */
427
- function isTerminator(body) {
290
+ function canThrow(body, seen = new Set()) {
428
291
  if (!Array.isArray(body)) return false
429
292
  const op = body[0]
430
- if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
431
- // Block body: {} or ; — terminator if it ends with a terminator statement.
432
- if (op === '{}' || op === ';') {
433
- for (let i = body.length - 1; i >= 1; i--) {
434
- const s = body[i]
435
- if (s == null) continue
436
- return isTerminator(s)
293
+ if (op === 'throw') return true
294
+ if (op === '=>') return false
295
+ if (op === '()') {
296
+ const callee = body[1]
297
+ if (typeof callee === 'string') {
298
+ const bodyName = ctx.func.directClosures?.get(callee)
299
+ const f = ctx.func.map?.get(bodyName || callee)
300
+ if (f?.body && !f.raw && !seen.has(f.name)) {
301
+ seen.add(f.name)
302
+ if (canThrow(f.body, seen)) return true
303
+ }
437
304
  }
438
- return false
439
305
  }
306
+ for (let i = 1; i < body.length; i++) if (canThrow(body[i], seen)) return true
440
307
  return false
441
308
  }
442
309
 
@@ -894,6 +761,23 @@ function needsRelationalToNumber(expr, vt) {
894
761
  return mayReadBoxedValue(expr)
895
762
  }
896
763
 
764
+ function needsLooseEqualityToNumber(expr, vt) {
765
+ if (vt === VAL.STRING) return true
766
+ if (vt != null) return false
767
+ return mayReadBoxedValue(expr)
768
+ }
769
+
770
+ function looseNumberEq(numIR, otherNode, otherIR, negate = false) {
771
+ const t = temp('eq')
772
+ const other = typed(['local.get', `$${t}`], 'f64')
773
+ const cmp = ['f64.eq', asF64(numIR), toNumF64(otherNode, other)]
774
+ return typed(['block', ['result', 'i32'],
775
+ ['local.set', `$${t}`, asF64(otherIR)],
776
+ ['if', ['result', 'i32'], isNullish(other),
777
+ ['then', ['i32.const', negate ? 1 : 0]],
778
+ ['else', negate ? ['i32.eqz', cmp] : cmp]]], 'i32')
779
+ }
780
+
897
781
  function mayReadBoxedValue(expr) {
898
782
  return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
899
783
  }
@@ -966,10 +850,7 @@ export const emitter = {
966
850
  ';': (...args) => {
967
851
  const out = []
968
852
  for (const a of args) {
969
- const r = emit(a, 'void')
970
- if (r == null) continue
971
- out.push(...flat(r))
972
- if (r?.type && r.type !== 'void') out.push('drop')
853
+ out.push(...emitFlat(a))
973
854
  }
974
855
  return out
975
856
  },
@@ -1013,6 +894,8 @@ export const emitter = {
1013
894
  },
1014
895
 
1015
896
  'catch': (body, errName, handler) => {
897
+ if (!canThrow(body)) return emitFlat(body)
898
+
1016
899
  ctx.runtime.throws = true
1017
900
  const id = ctx.func.uniq++
1018
901
  ctx.func.locals.set(errName, 'f64')
@@ -1031,6 +914,14 @@ export const emitter = {
1031
914
  },
1032
915
 
1033
916
  'finally': (body, cleanup) => {
917
+ if (!canThrow(body)) {
918
+ const parentStack = ctx.func.finallyStack || []
919
+ const activeStack = parentStack.concat([cleanup])
920
+ const bodyIR = withFinallyStack(activeStack, () => emitFlat(body))
921
+ const cleanupIR = isTerminator(body) ? [] : withFinallyStack(parentStack, () => emitFlat(cleanup))
922
+ return [...bodyIR, ...cleanupIR]
923
+ }
924
+
1034
925
  ctx.runtime.throws = true
1035
926
  const id = ctx.func.uniq++
1036
927
  const errLocal = temp('err')
@@ -1060,13 +951,14 @@ export const emitter = {
1060
951
 
1061
952
  'return': expr => {
1062
953
  const finalizers = emitFinalizers()
954
+ const finalizerBlock = () => [['block', ...finalizers]]
1063
955
  if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
1064
956
  const vals = expr.slice(1).map(e => asF64(emit(e)))
1065
957
  if (finalizers.length === 0) return typed(['return', ...vals], 'void')
1066
958
  const names = vals.map(() => temp('ret'))
1067
959
  return [
1068
960
  ...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
1069
- ...finalizers,
961
+ ...finalizerBlock(),
1070
962
  typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
1071
963
  ]
1072
964
  }
@@ -1082,7 +974,7 @@ export const emitter = {
1082
974
  const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
1083
975
  return [
1084
976
  ['local.set', `$${name}`, ir],
1085
- ...finalizers,
977
+ ...finalizerBlock(),
1086
978
  typed(['return', ['local.get', `$${name}`]], 'void'),
1087
979
  ]
1088
980
  }
@@ -1354,6 +1246,8 @@ export const emitter = {
1354
1246
 
1355
1247
  // Compound assignments: read-modify-write with type coercion
1356
1248
  '+=': (name, val) => {
1249
+ // Complex LHS (obj.prop, arr[i]) → desugar to side-effect-safe `name = name + val`
1250
+ if (typeof name !== 'string') return emit(['=', name, ['+', name, val]])
1357
1251
  // String concatenation: desugar to name = name + val (+ handler knows about strings).
1358
1252
  // Also desugar when either side has unknown type — the `+` operator picks runtime
1359
1253
  // string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
@@ -1366,17 +1260,24 @@ export const emitter = {
1366
1260
  },
1367
1261
  ...Object.fromEntries([
1368
1262
  ['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
1369
- ].map(([op, fn]) => [op, (name, val) => compoundAssign(name, val,
1370
- (a, b) => typed([`f64.${fn}`, a, b], 'f64'),
1371
- fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1372
- )])),
1373
- '%=': (name, val) => compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32')),
1263
+ ].map(([op, fn]) => [op, (name, val) => {
1264
+ if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1265
+ return compoundAssign(name, val,
1266
+ (a, b) => typed([`f64.${fn}`, a, b], 'f64'),
1267
+ fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1268
+ )
1269
+ }])),
1270
+ '%=': (name, val) => {
1271
+ if (typeof name !== 'string') return emit(['=', name, ['%', name, val]])
1272
+ return compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32'))
1273
+ },
1374
1274
 
1375
1275
  // Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
1376
1276
  ...Object.fromEntries([
1377
1277
  ['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
1378
1278
  ['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
1379
1279
  ].map(([op, fn]) => [op, (name, val) => {
1280
+ if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1380
1281
  if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
1381
1282
  const void_ = _expect === 'void'
1382
1283
  const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
@@ -1566,6 +1467,8 @@ export const emitter = {
1566
1467
  // Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
1567
1468
  const vta = resolveValType(a, valTypeOf, lookupValType)
1568
1469
  const vtb = resolveValType(b, valTypeOf, lookupValType)
1470
+ if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb)
1471
+ if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va)
1569
1472
  if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1570
1473
  // Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
1571
1474
  // JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
@@ -1593,6 +1496,8 @@ export const emitter = {
1593
1496
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
1594
1497
  const vta = resolveValType(a, valTypeOf, lookupValType)
1595
1498
  const vtb = resolveValType(b, valTypeOf, lookupValType)
1499
+ if (vta === VAL.NUMBER && needsLooseEqualityToNumber(b, vtb)) return looseNumberEq(va, b, vb, true)
1500
+ if (vtb === VAL.NUMBER && needsLooseEqualityToNumber(a, vta)) return looseNumberEq(vb, a, va, true)
1596
1501
  if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1597
1502
  if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1598
1503
  return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
@@ -2514,7 +2419,10 @@ function liftOptionalChain(node) {
2514
2419
  */
2515
2420
  export function emit(node, expect) {
2516
2421
  _expect = expect || null
2517
- if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
2422
+ if (Array.isArray(node)) {
2423
+ ctx.error.node = node
2424
+ if (node.loc != null) ctx.error.loc = node.loc
2425
+ }
2518
2426
  if (node == null) return null
2519
2427
  if (node === true) return typed(['i32.const', 1], 'i32')
2520
2428
  if (node === false) return typed(['i32.const', 0], 'i32')
package/src/host.js CHANGED
@@ -607,6 +607,18 @@ const installDefaultEnvImports = (mod, imports, state) => {
607
607
  imports.env.now = (clock) =>
608
608
  clock === 1 ? (typeof performance !== 'undefined' ? performance.now() : Date.now()) : Date.now()
609
609
  }
610
+ if (envFns.has('parseFloat') && !imports.env.parseFloat) {
611
+ imports.env.parseFloat = (valBig) => {
612
+ const s = state.mem.read(i64ToF64(valBig))
613
+ return parseFloat(s)
614
+ }
615
+ }
616
+ if (envFns.has('parseInt') && !imports.env.parseInt) {
617
+ imports.env.parseInt = (valBig, radix) => {
618
+ const s = state.mem.read(i64ToF64(valBig))
619
+ return parseInt(s, radix || undefined)
620
+ }
621
+ }
610
622
  // host: 'js' timer wiring. Wasm calls env.setTimeout/clearTimeout; we drive
611
623
  // callbacks back via the exported __invoke_closure trampoline (state.invoke).
612
624
  // Each id maps to a cancel thunk so set/clear share state without tagging.
package/src/ir.js CHANGED
@@ -688,6 +688,19 @@ export const flat = ir => {
688
688
  * Reconstruct arguments with spreads inserted at correct positions.
689
689
  * Example: normal=[a, c], spreads=[{pos:1, expr:arr}] → [a, __spread(arr), c]
690
690
  */
691
+ /** Find the index of the first body-content child in a (func ...) WAT node.
692
+ * Skips $name, (export …), (import …), (type …), (param …), (result …), (local …). */
693
+ export function findBodyStart(fn) {
694
+ for (let i = 2; i < fn.length; i++) {
695
+ const c = fn[i]
696
+ if (!Array.isArray(c)) continue
697
+ if (c[0] === 'export' || c[0] === 'import' || c[0] === 'type' ||
698
+ c[0] === 'param' || c[0] === 'result' || c[0] === 'local') continue
699
+ return i
700
+ }
701
+ return fn.length
702
+ }
703
+
691
704
  export function reconstructArgsWithSpreads(normal, spreads) {
692
705
  const combined = []
693
706
  let normalIdx = 0