jz 0.2.0 → 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
 
@@ -867,6 +734,20 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
867
734
  inc('__str_cmp')
868
735
  return typed([`i32.${i32op}`, ['call', '$__str_cmp', asI64(va), asI64(vb)], ['i32.const', 0]], 'i32')
869
736
  }
737
+ if (vta === VAL.DATE || vtb === VAL.DATE) {
738
+ const dateNum = (node, v, vt) => {
739
+ if (vt !== VAL.DATE) return toNumF64(node, v)
740
+ const ptr = v.ptrKind === VAL.DATE
741
+ ? v
742
+ : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
743
+ return typed(['f64.load', ptr], 'f64')
744
+ }
745
+ return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
746
+ }
747
+ if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
748
+ return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
749
+ if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
750
+ return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
870
751
  const ai = intConstValue(a), bi = intConstValue(b)
871
752
  if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
872
753
  if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
@@ -874,6 +755,33 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
874
755
  ? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
875
756
  }
876
757
 
758
+ function needsRelationalToNumber(expr, vt) {
759
+ if (vt === VAL.STRING) return true
760
+ if (vt != null) return false
761
+ return mayReadBoxedValue(expr)
762
+ }
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
+
781
+ function mayReadBoxedValue(expr) {
782
+ return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
783
+ }
784
+
877
785
  function intConstValue(expr) {
878
786
  if (typeof expr === 'number' && Number.isInteger(expr)) return expr
879
787
  if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
@@ -942,10 +850,7 @@ export const emitter = {
942
850
  ';': (...args) => {
943
851
  const out = []
944
852
  for (const a of args) {
945
- const r = emit(a, 'void')
946
- if (r == null) continue
947
- out.push(...flat(r))
948
- if (r?.type && r.type !== 'void') out.push('drop')
853
+ out.push(...emitFlat(a))
949
854
  }
950
855
  return out
951
856
  },
@@ -989,6 +894,8 @@ export const emitter = {
989
894
  },
990
895
 
991
896
  'catch': (body, errName, handler) => {
897
+ if (!canThrow(body)) return emitFlat(body)
898
+
992
899
  ctx.runtime.throws = true
993
900
  const id = ctx.func.uniq++
994
901
  ctx.func.locals.set(errName, 'f64')
@@ -1007,6 +914,14 @@ export const emitter = {
1007
914
  },
1008
915
 
1009
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
+
1010
925
  ctx.runtime.throws = true
1011
926
  const id = ctx.func.uniq++
1012
927
  const errLocal = temp('err')
@@ -1036,13 +951,14 @@ export const emitter = {
1036
951
 
1037
952
  'return': expr => {
1038
953
  const finalizers = emitFinalizers()
954
+ const finalizerBlock = () => [['block', ...finalizers]]
1039
955
  if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
1040
956
  const vals = expr.slice(1).map(e => asF64(emit(e)))
1041
957
  if (finalizers.length === 0) return typed(['return', ...vals], 'void')
1042
958
  const names = vals.map(() => temp('ret'))
1043
959
  return [
1044
960
  ...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
1045
- ...finalizers,
961
+ ...finalizerBlock(),
1046
962
  typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
1047
963
  ]
1048
964
  }
@@ -1058,7 +974,7 @@ export const emitter = {
1058
974
  const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
1059
975
  return [
1060
976
  ['local.set', `$${name}`, ir],
1061
- ...finalizers,
977
+ ...finalizerBlock(),
1062
978
  typed(['return', ['local.get', `$${name}`]], 'void'),
1063
979
  ]
1064
980
  }
@@ -1330,6 +1246,8 @@ export const emitter = {
1330
1246
 
1331
1247
  // Compound assignments: read-modify-write with type coercion
1332
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]])
1333
1251
  // String concatenation: desugar to name = name + val (+ handler knows about strings).
1334
1252
  // Also desugar when either side has unknown type — the `+` operator picks runtime
1335
1253
  // string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
@@ -1342,17 +1260,24 @@ export const emitter = {
1342
1260
  },
1343
1261
  ...Object.fromEntries([
1344
1262
  ['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
1345
- ].map(([op, fn]) => [op, (name, val) => compoundAssign(name, val,
1346
- (a, b) => typed([`f64.${fn}`, a, b], 'f64'),
1347
- fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
1348
- )])),
1349
- '%=': (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
+ },
1350
1274
 
1351
1275
  // Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
1352
1276
  ...Object.fromEntries([
1353
1277
  ['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
1354
1278
  ['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
1355
1279
  ].map(([op, fn]) => [op, (name, val) => {
1280
+ if (typeof name !== 'string') return emit(['=', name, [op.slice(0, -1), name, val]])
1356
1281
  if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
1357
1282
  const void_ = _expect === 'void'
1358
1283
  const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
@@ -1542,6 +1467,8 @@ export const emitter = {
1542
1467
  // Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
1543
1468
  const vta = resolveValType(a, valTypeOf, lookupValType)
1544
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)
1545
1472
  if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1546
1473
  // Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
1547
1474
  // JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
@@ -1569,6 +1496,8 @@ export const emitter = {
1569
1496
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
1570
1497
  const vta = resolveValType(a, valTypeOf, lookupValType)
1571
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)
1572
1501
  if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1573
1502
  if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1574
1503
  return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
@@ -2490,7 +2419,10 @@ function liftOptionalChain(node) {
2490
2419
  */
2491
2420
  export function emit(node, expect) {
2492
2421
  _expect = expect || null
2493
- 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
+ }
2494
2426
  if (node == null) return null
2495
2427
  if (node === true) return typed(['i32.const', 1], 'i32')
2496
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
@@ -347,6 +347,12 @@ export function toNumF64(node, v) {
347
347
  if (v.type === 'i32' || isLit(v)) return asF64(v)
348
348
  const vt = keyValType(node)
349
349
  if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
350
+ if (vt === VAL.DATE) {
351
+ const ptr = v.ptrKind === VAL.DATE
352
+ ? v
353
+ : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
354
+ return typed(['f64.load', ptr], 'f64')
355
+ }
350
356
  // intCertain locals: every reachable def is integer-valued, so the binding
351
357
  // never carries a NaN-boxed pointer — skip the __to_num wrapper.
352
358
  if (typeof node === 'string' && repOf(node)?.intCertain === true) return asF64(v)
@@ -682,6 +688,19 @@ export const flat = ir => {
682
688
  * Reconstruct arguments with spreads inserted at correct positions.
683
689
  * Example: normal=[a, c], spreads=[{pos:1, expr:arr}] → [a, __spread(arr), c]
684
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
+
685
704
  export function reconstructArgsWithSpreads(normal, spreads) {
686
705
  const combined = []
687
706
  let normalIdx = 0