jz 0.8.0 → 0.9.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.
Files changed (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
package/src/ctx.js CHANGED
@@ -202,6 +202,20 @@ export function resolveIncludes() {
202
202
  for (const dep of autoDepsOf(name)) add(dep)
203
203
  }
204
204
  }
205
+ // Self-host divergence diagnostics (scripts/self.js compileDiag): snapshot
206
+ // what THIS side resolved, so a host-vs-kernel JSON diff names the first
207
+ // differing fact instead of leaving byte-drift archaeology. Near-zero cost
208
+ // when the sink is absent (one truthiness test per call).
209
+ if (ctx.core.diagSink) {
210
+ if (!ctx.core.diagSink.resolve) ctx.core.diagSink.resolve = []
211
+ ctx.core.diagSink.resolve.push({
212
+ includes: [...ctx.core.includes].sort().join(' '),
213
+ autoAlloc: autoDepsOf('__alloc').join(' '),
214
+ memShared: !!ctx.memory.shared,
215
+ memSharedRaw: String(ctx.memory.shared),
216
+ allocOwned: typeof stdlib['__alloc'] === 'string' && stdlib['__alloc'].indexOf('global.get $__heap') >= 0,
217
+ })
218
+ }
205
219
  }
206
220
 
207
221
  /** Reset all compilation state. Called once per jz() invocation. */
@@ -298,6 +312,7 @@ export function reset(proto, globals, bridge) {
298
312
  dynKeyVars: null,
299
313
  dynWriteVars: null,
300
314
  anyDynKey: false,
315
+ literalWriteKeys: null, // Map<var, Set<key>> — literal-key prop writes per bare-var receiver (plan/index.js)
301
316
  }
302
317
 
303
318
  ctx.schema = {
@@ -321,6 +336,21 @@ export function reset(proto, globals, bridge) {
321
336
  // returns the slot's kind for `.prop` AST nodes, letting
322
337
  // `+`/`===`/method dispatch elide `__is_str_key` checks
323
338
  // on numeric properties of known shapes.
339
+ slotTypedCtors: new Map(), // schemaId → Array<ctor-string | null | undefined>
340
+ // undefined: no observation, null: ≥2 distinct
341
+ // ctors, string: every observed value of the slot
342
+ // is that typed-array kind. The elem-width sibling
343
+ // of slotTypes' VAL.TYPED — populated by
344
+ // observeProgramSlots on object literals; read by
345
+ // ctx.schema.slotTypedCtorAt (gated on the prop
346
+ // never being WRITTEN program-wide) so `plan.twRe`
347
+ // keeps its concrete Float64Array kind through
348
+ // field provenance (bench: provenance, fftplan).
349
+ slotIntLevels: new Map(), // schemaId → Array<0|1|2 | undefined> — the int
350
+ // census's WORKING state (type.js lattice:
351
+ // 1 integral, 2 strict-int32). Consumers read
352
+ // the two projections below, published when
353
+ // `analyzeSchemaSlotIntCertain`'s rounds settle.
324
354
  slotIntCertain: new Map(), // schemaId → Array<boolean | undefined>
325
355
  // undefined: no write observed, true: all observed
326
356
  // writes are integer-shaped, false: poisoned by at
@@ -330,6 +360,25 @@ export function reset(proto, globals, bridge) {
330
360
  // writes). Read by `ctx.schema.slotIntCertainAt`
331
361
  // so Math.floor/toNumF64/intIndexIR consumers fire
332
362
  // on `.prop` reads of provably-integer slots.
363
+ slotI32Certain: new Map(), // schemaId → Array<boolean> — the strict (=2)
364
+ // projection: every write is exactly-int32 and
365
+ // never -0, so `i32.trunc_sat_f64_s` of the slot's
366
+ // f64 is an exact round-trip. Read by
367
+ // `ctx.schema.slotI32CertainAt` → raw i32 slot
368
+ // loads (module/core.js) + i32 local typing
369
+ // (type.js exprType '.').
370
+ externSlotSids: new Set(), // schemaId set — sids whose slot VALUES can be
371
+ // written by machinery the write censuses never
372
+ // see: the JSON const emitter / shaped runtime
373
+ // parser (arbitrary runtime JSON into a sid
374
+ // shared with source literals) and spread /
375
+ // Object.assign slot copies across schemas.
376
+ // Populated by plan's markExternSlotSids sweep
377
+ // (+ belts at the emit registration sites); the
378
+ // slot censuses pre-poison these sids and every
379
+ // census reader (slotVT / slotTypedCtor* /
380
+ // slotIntCertainAt / guardedNumSlot stamp)
381
+ // answers null/false for them.
333
382
  inlineArray: new Set(), // schemaId set — schemas whose `Array<S>` instances
334
383
  // use the `structInline` SRoA carrier (K f64
335
384
  // fields inlined per element, no per-row object).
@@ -548,16 +597,22 @@ export function err(msg, cause) {
548
597
  throw e
549
598
  }
550
599
 
600
+ // Recursive walk, NOT a stringify replacer — the kernel drops replacers, so
601
+ // in-kernel error nodes would print with bigints/cycles unhandled. Cold path.
551
602
  function formatErrorNode(node) {
552
- const seen = new WeakSet()
553
- const json = JSON.stringify(node, (_key, value) => {
554
- if (typeof value === 'bigint') return `${value}n`
555
- if (typeof value === 'symbol') return value.toString()
556
- if (Array.isArray(value)) {
557
- if (seen.has(value)) return '[Circular]'
558
- seen.add(value)
603
+ const seen = new Set()
604
+ const fmt = (v) => {
605
+ if (typeof v === 'bigint') return `"${v}n"`
606
+ if (typeof v === 'string') return JSON.stringify(v)
607
+ if (Array.isArray(v)) {
608
+ if (seen.has(v)) return '"[Circular]"'
609
+ seen.add(v)
610
+ let s = '['
611
+ for (let i = 0; i < v.length; i++) s += (i ? ',' : '') + fmt(v[i])
612
+ return s + ']'
559
613
  }
560
- return value
561
- })
614
+ return v === undefined ? 'null' : String(v)
615
+ }
616
+ const json = fmt(node)
562
617
  return json.length > 2000 ? `${json.slice(0, 2000)}...` : json
563
618
  }
@@ -22,6 +22,12 @@ export const HELPER_COUNTERS = [
22
22
  ['__dyn_get', 'dyn_get'],
23
23
  ['__dyn_get_t', 'dyn_get_t'],
24
24
  ['__dyn_get_t_h', 'dyn_get_t_h'],
25
+ ['__dyn_get_expr', 'dyn_get_expr'],
26
+ ['__dyn_get_expr_t', 'dyn_get_expr_t'],
27
+ ['__dyn_get_expr_t_h', 'dyn_get_expr_t_h'],
28
+ ['__dyn_get_any', 'dyn_get_any'],
29
+ ['__dyn_get_any_t', 'dyn_get_any_t'],
30
+ ['__dyn_get_any_t_h', 'dyn_get_any_t_h'],
25
31
  ['__dyn_set', 'dyn_set'],
26
32
  ['__arr_grow', 'arr_grow'],
27
33
  ['__arr_grow_known', 'arr_grow_known'],
@@ -52,7 +58,7 @@ ${[...COUNTER_BY_HELPER.values()].map(counter => ` (global.set $${counter} (i
52
58
 
53
59
  // Bump the helper's counter once on entry. NOTE the semantics: this counts FUNCTION
54
60
  // ENTRIES at runtime — a call site that jz inlined or specialized away (fusedRewrite,
55
- // specializeMkptr/specializePtrBase, …) never enters the function and is NOT counted. So
61
+ // specializeMkptr, …) never enters the function and is NOT counted. So
56
62
  // the numbers are a relative ranking / lower bound for picking hot helpers, not exact
57
63
  // operation counts. Good enough to choose targets; don't read them as call totals.
58
64
  export function instrumentHelperCounter(helper, fn) {
package/src/ir.js CHANGED
@@ -338,6 +338,13 @@ export const fromI64 = n => {
338
338
  * At the JS boundary, null and undefined preserve their identity for interop. */
339
339
  export const NULL_NAN = atomNanHex(1)
340
340
  export const UNDEF_NAN = atomNanHex(2)
341
+ /** Zombie-entry key sentinel for the durable-slot heal (__durable_slot_heal,
342
+ * module/core.js): written over a healed durable dict entry's KEY so probes and
343
+ * enumeration skip it. Unforgeable: ATOM tag with a saturated aux+offset no
344
+ * boxing path ever produces (real atom ids are tiny). Every equality family is
345
+ * deref-free on it: i64.eq mismatches, __str_eq bails on the non-STRING tag,
346
+ * __same_value_zero's atom arm is bit-equality. */
347
+ export const TOMB_NAN = '0x7FF87FFFFFFFFFFF'
341
348
  /** Boxed-boolean carrier. `false`/`true` are reserved atoms — materialized only
342
349
  * where boolean identity is observed (typeof/String/JSON/host boundary); in
343
350
  * branch/arithmetic position booleans stay raw i32/f64 0/1. The atomId encodes
@@ -354,7 +361,18 @@ export const NULL_IR = ['f64.const', `nan:${NULL_NAN}`]
354
361
  export const UNDEF_IR = ['f64.const', `nan:${UNDEF_NAN}`]
355
362
  export const FALSE_IR = ['f64.const', `nan:${FALSE_NAN}`]
356
363
  export const TRUE_IR = ['f64.const', `nan:${TRUE_NAN}`]
357
- export const nullExpr = () => typed(NULL_IR, 'f64')
364
+ // .slice() before typed(): NULL_IR is a shared module-level template (like its
365
+ // UNDEF_IR/FALSE_IR/TRUE_IR siblings below, which already copy) — typed() tags
366
+ // `.type` onto the node it's given, so calling it on the shared array directly
367
+ // mutates ONE instance repeatedly. Natively harmless (same idempotent value each
368
+ // time, plain GC heap). In the self-hosted kernel `.type=` is a dynamic-key write
369
+ // that lazily allocates a per-object props sidecar the FIRST time it's called —
370
+ // which happens well after module-init (`__start`), so that sidecar lives ABOVE
371
+ // `__heap_reset` in the bump arena and dangles after `_clear` rewinds it: the
372
+ // NEXT `nullExpr()` call (next compile) reads NULL_IR's now-stale header propsPtr
373
+ // and corrupts memory. A missing `.slice()` this whole time — surfaced only by
374
+ // warm-instance reuse actually re-invoking it post-`_clear`.
375
+ export const nullExpr = () => typed(NULL_IR.slice(), 'f64')
358
376
  export const undefExpr = () => typed(UNDEF_IR.slice(), 'f64')
359
377
 
360
378
  /** Materialize the boxed-boolean carrier from a 0/1-valued expression. The atom
@@ -367,6 +385,17 @@ export function boolBoxIR(e) {
367
385
  return mkPtrIR(['i32.const', PTR.ATOM], ['i32.or', ['i32.const', BOOL_ATOM_BASE], i], ['i32.const', 0])
368
386
  }
369
387
 
388
+ /** Value-preserving f64 carrier for a value entering an untyped slot — container
389
+ * stores, collection keys/values, dyn-prop writes, generic call args. A boolean
390
+ * keeps its identity as the TRUE/FALSE atom box (typeof/String/strict-eq survive
391
+ * the round-trip); everything else takes the plain asF64 box. Never use in branch
392
+ * or arithmetic position — truthyIR/toNumF64 own those (raw 0/1 there by design).
393
+ * Callers emit(node) ONCE and pass both (emitting per-arm inside a ternary wrapped
394
+ * by different coercions is the self-host-fragile shape — see emit.js 'return'). */
395
+ export function carrierF64(node, emitted) {
396
+ return valTypeOf(node) === VAL.BOOL ? boolBoxIR(emitted) : asF64(emitted)
397
+ }
398
+
370
399
  /** Recover the 0/1 i32 value of a known boxed-boolean f64 expression: `aux & 1`. */
371
400
  export function unboxBoolIR(f64expr) {
372
401
  if (Array.isArray(f64expr) && f64expr[0] === 'f64.const') {
@@ -693,10 +722,25 @@ export function ptrTypeEq(f64Expr, ptr) {
693
722
  export function sidecarOverride(objIR, nameIR, onOverride, onFallback) {
694
723
  const o = temp('vo'), p = temp('vp')
695
724
  inc('__dyn_get_expr', '__ptr_type')
725
+ // Primitive receivers can never carry an own property that shadows a
726
+ // builtin (numbers: no props at all; strings: property writes drop —
727
+ // module/collection.js STRING arms), so the override probe is statically
728
+ // futile for them. One inline number test + tag test skips the 3-frame
729
+ // __dyn_get_expr chain — parser loops calling s.charCodeAt through an
730
+ // unproven receiver were paying it per character (jessie: 1.19M/run at
731
+ // one site). The or's second operand reads garbage tag bits when the
732
+ // first is true (real number) — harmless, the or is already decided.
696
733
  return block64(
697
734
  ['local.set', `$${o}`, asF64(objIR)],
698
- ['local.set', `$${p}`, ['f64.reinterpret_i64',
699
- ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${o}`]], nameIR]]],
735
+ ['local.set', `$${p}`, ['if', ['result', 'f64'],
736
+ ['i32.and',
737
+ ['f64.ne', ['local.get', `$${o}`], ['local.get', `$${o}`]],
738
+ ['i64.ne',
739
+ ['i64.and', ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['i64.const', i64Hex(BigInt(LAYOUT.TAG_MASK) << BigInt(LAYOUT.TAG_SHIFT))]],
740
+ ['i64.const', i64Hex(BigInt(PTR.STRING) << BigInt(LAYOUT.TAG_SHIFT))]]],
741
+ ['then', ['f64.reinterpret_i64',
742
+ ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${o}`]], nameIR]]],
743
+ ['else', undefExpr()]]],
700
744
  ['if', ['result', 'f64'],
701
745
  ptrTypeEq(['local.get', `$${p}`], PTR.CLOSURE),
702
746
  ['then', onOverride(p, o)],
@@ -774,7 +818,18 @@ function toPrimitiveChain(node, v, order) {
774
818
  return typed(['block', blk, ...body], 'i64')
775
819
  }
776
820
 
777
- const cloneIR = (n) => Array.isArray(n) ? n.map(cloneIR) : n
821
+ /** Structural clone of an already-emitted IR node. Two DIFFERENT positions in the
822
+ * final tree must never share one node object: a later pass (CSE, peephole, local
823
+ * renumbering) that walks the tree and mutates/tags a node in place only sees ONE
824
+ * of the two logical occurrences and silently mutates both — the exact "IR-aliasing"
825
+ * hazard multiple emit sites must avoid when a value is READ into more than one
826
+ * branch of an if/else or more than one argument position. Caller's responsibility:
827
+ * the node must be side-effect-free to duplicate (a local/cell read, not a call) —
828
+ * see coerceNullishToNum below for the established idiom. Does not preserve `.type`/
829
+ * `.ptrKind` (a plain array map): every consumer of a cloned node already re-derives
830
+ * those from context (`node?.type ? node : typed(node, 'f64')`), so this only needs
831
+ * to reproduce the value-computing shape, not its cached metadata. */
832
+ export const cloneIR = (n) => Array.isArray(n) ? n.map(cloneIR) : n
778
833
 
779
834
  /** ToNumber for a runtime value that may carry a nullish sentinel: null→+0, undefined→NaN,
780
835
  * anything else → itself. `valIR` must be side-effect-free (a local read) — it is duplicated,
@@ -839,6 +894,22 @@ export function toNumF64(node, v) {
839
894
  if (Array.isArray(node) && node[0] === '.' && typeof node[1] === 'string' && typeof node[2] === 'string') {
840
895
  if (ctx.schema.slotIntCertainAt?.(node[1], node[2]) === true) return asF64(v)
841
896
  }
897
+ // Guarded schema-slot read whose ONE schema censuses the slot NUMBER
898
+ // (emitSchemaSlotGuarded's stamp): SINK the coercion into the arms — the
899
+ // guard-HIT raw load is already a plain number; only the dyn-miss arm pays
900
+ // __to_num. The shapes-dispatch pattern (`measure(o)` over 8 schemas) drops
901
+ // a per-field ToNumber call from every hot read this way.
902
+ if (v.guardedNumSlot && Array.isArray(v) && v[0] === 'if') {
903
+ const out = v.map((c, i) => {
904
+ if (Array.isArray(c) && c[0] === 'else' && c.length === 2) {
905
+ if (!ctx.core.stdlib['__to_num']) return c
906
+ inc('__to_num')
907
+ return ['else', typed(['call', '$__to_num', asI64(typed(c[1], 'f64'))], 'f64')]
908
+ }
909
+ return c
910
+ })
911
+ return typed(out, 'f64')
912
+ }
842
913
  // IR-level shapes that produce real f64 numbers (never NaN-boxed pointers):
843
914
  // i32→f64 conversions, stdlib clock helper, length/ptr helpers.
844
915
  // Skip the __to_num call wrapper for these — they always return plain f64.
@@ -880,6 +951,20 @@ export function toNumF64(node, v) {
880
951
  return f
881
952
  }
882
953
  inc('__to_num')
954
+ // Inline number fast path (the engines' move): every non-NaN f64 IS its own
955
+ // ToNumber — only NaN bit patterns (all NaN-boxed pointers + sentinels, plus
956
+ // genuine NaN) take the call. One self-compare against a call per site; the
957
+ // dictionary-count idiom (`o[k] | 0` on a number-or-undefined slot) drops a
958
+ // per-token call this way. Optimize-gated: the O0 tier keeps the compact call.
959
+ if (ctx.transform.optimize) {
960
+ const t = temp('tnum')
961
+ return typed(['block', ['result', 'f64'],
962
+ ['local.set', `$${t}`, asF64(v)],
963
+ ['if', ['result', 'f64'],
964
+ ['f64.eq', ['local.get', `$${t}`], ['local.get', `$${t}`]],
965
+ ['then', ['local.get', `$${t}`]],
966
+ ['else', ['call', '$__to_num', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]], 'f64')
967
+ }
883
968
  return typed(['call', '$__to_num', asI64(v)], 'f64')
884
969
  }
885
970
 
@@ -1073,12 +1158,24 @@ export function boxedAddr(name) {
1073
1158
  // construction is a map hit and every downstream comparison is bit-eq.
1074
1159
  // Module-level: in-kernel it lives per instance (arena strings are immortal),
1075
1160
  // natively it is a plain cross-compile cache; the name vocabulary is bounded.
1076
- const DOLLAR = new Map()
1161
+ let DOLLAR = new Map()
1077
1162
  export const dollar = (name) => {
1078
1163
  let v = DOLLAR.get(name)
1079
1164
  if (v === undefined) { v = '$' + name; DOLLAR.set(name, v) }
1080
1165
  return v
1081
1166
  }
1167
+ // Self-host-only: DOLLAR's keys/values are both arena strings built during compile
1168
+ // (the `name`s come from the source being compiled) AND the Map's own backing
1169
+ // table is itself an arena allocation. Natively the arena is the host GC heap, so
1170
+ // stale entries (or a `.clear()`) are enough — the old backing store just becomes
1171
+ // garbage. In-kernel the arena is a bump allocator that `_clear` rewinds between
1172
+ // compiles: `.clear()` alone leaves the Map pointing at its OLD backing table,
1173
+ // which a later allocation can overwrite while still "owned" by DOLLAR (as opposed
1174
+ // to the entries becoming merely unreachable) — so a warm-instance compile loop
1175
+ // must swap in a FRESH Map (not just empty this one) after every `_clear`
1176
+ // (see scripts/self.js setupSelf). Verified empirically: `.clear()` alone still
1177
+ // trapped `__hash_set_local` on the 2nd compile of a warm instance.
1178
+ export const clearDollar = () => { DOLLAR = new Map() }
1082
1179
 
1083
1180
  /** Read variable value: boxed → f64.load, global → global.get, local → local.get.
1084
1181
  * Unboxed pointer locals (repOf(name).ptrKind) tag the returned node with `.ptrKind`
@@ -1098,6 +1195,11 @@ export function readVar(name) {
1098
1195
  // — an f64 consumer widens the i32.const via convert, which folds back to f64.const.
1099
1196
  const ci = ctx.scope.constInts?.get?.(name)
1100
1197
  if (ci != null && isI32(ci)) return typed(['i32.const', ci], 'i32')
1198
+ // Fractional pre-folded const (`const nv = 2610/16384`): same immutability
1199
+ // argument as the integer arm — substitute the literal so downstream
1200
+ // compile-time folds (constant-exponent pow, ranges) see the value.
1201
+ const cn = ctx.scope.constNums?.get?.(name)
1202
+ if (cn != null) { const node = typed(['f64.const', cn], 'f64'); node.valKind = VAL.NUMBER; return node }
1101
1203
  const gt = ctx.scope.globalTypes.get(name) || 'f64'
1102
1204
  const node = typed(['global.get', dollar(name)], gt)
1103
1205
  const grep = repOfGlobal(name)
@@ -1171,6 +1273,12 @@ export function writeVar(name, valIR, void_) {
1171
1273
  ['global.set', dollar(name), ['local.get', `$${t}`]],
1172
1274
  ['local.get', `$${t}`]], gt)
1173
1275
  }
1276
+ // NOTE: an unknown name is NOT minted here — a write-legalized binding lets a
1277
+ // later-emitted read of the same undeclared name resolve to 0 instead of
1278
+ // rejecting (test262 pins the ReferenceError: `x = x`, `x++`, `x + (x = 1)`
1279
+ // — 50 in-scope failures from an unconditional mint). The one structural
1280
+ // write-only binder, a bare undeclared `for (k in o)` head, is declared at
1281
+ // its prepare lowering instead.
1174
1282
  const t = ctx.func.locals.get(name) || 'f64'
1175
1283
  const ptrKind = repOf(name)?.ptrKind
1176
1284
  let coerced
@@ -49,11 +49,61 @@ export const CALLEE_VAL = {
49
49
  'BigInt.asUintN': VAL.BIGINT,
50
50
  'performance.now': VAL.NUMBER,
51
51
  'Date.now': VAL.NUMBER,
52
+ // ES2024 groupBy: a dictionary (HASH) keyed by ToPropertyKey strings, and a
53
+ // real Map keyed by SameValueZero — result reads dispatch to the right table.
54
+ 'Object.groupBy': VAL.HASH,
55
+ 'Map.groupBy': VAL.MAP,
56
+ 'RegExp.escape': VAL.STRING,
57
+ // Predicate builtins return booleans (raw 0/1 carrier) — same classification
58
+ // BOOL_METHODS gives includes/some/every: without it `isFinite(x) === false`
59
+ // falls to the unknown-identity path and bit-compares 0.0 against the FALSE
60
+ // atom (always false). typeof/String/JSON/host boundary observe it faithfully.
61
+ isNaN: VAL.BOOL,
62
+ isFinite: VAL.BOOL,
63
+ 'Array.isArray': VAL.BOOL,
64
+ 'Number.isNaN': VAL.BOOL,
65
+ 'Number.isFinite': VAL.BOOL,
66
+ 'Number.isInteger': VAL.BOOL,
67
+ 'Number.isSafeInteger': VAL.BOOL,
68
+ 'Object.is': VAL.BOOL,
69
+ 'Object.hasOwn': VAL.BOOL,
70
+ 'Object.isFrozen': VAL.BOOL,
71
+ 'Object.isSealed': VAL.BOOL,
72
+ 'Object.isExtensible': VAL.BOOL,
73
+ 'ArrayBuffer.isView': VAL.BOOL,
74
+ // jzify-synthesized `instanceof Map/Set/TypedArray` predicates (autoload
75
+ // CALL_MODULES) — same boolean-carrier classification as the ops they lower.
76
+ __is_map: VAL.BOOL,
77
+ __is_set: VAL.BOOL,
78
+ __is_typed: VAL.BOOL,
79
+ // Atomics (module/atomics.js): wait → result string, isLockFree → boolean,
80
+ // notify → count. Value ops resolve by RECEIVER width in calleeValType below
81
+ // (Int32Array → NUMBER, BigInt64Array → BIGINT).
82
+ 'Atomics.notify': VAL.NUMBER,
83
+ 'Atomics.wait': VAL.STRING, 'Atomics.isLockFree': VAL.BOOL,
52
84
  }
53
85
 
86
+ const ATOMICS_VALUE_OPS = new Set(['Atomics.load', 'Atomics.store', 'Atomics.add',
87
+ 'Atomics.sub', 'Atomics.and', 'Atomics.or', 'Atomics.xor', 'Atomics.exchange',
88
+ 'Atomics.compareExchange'])
89
+
54
90
  export function calleeValType(callee, _args, ctx) {
55
91
  if (typeof callee !== 'string') return null
56
92
  if (callee in CALLEE_VAL) return CALLEE_VAL[callee]
93
+ // Atomics value ops: the result kind follows the receiver's element width —
94
+ // a proven BigInt64Array receiver yields BIGINT (raw i64 carrier), else NUMBER.
95
+ if (ATOMICS_VALUE_OPS.has(callee)) {
96
+ // _args is the '()' node tail: [callee, argsNode] — the receiver is the
97
+ // first real argument (unwrap a ','-group).
98
+ const a1 = _args?.[1]
99
+ const arr = Array.isArray(a1) && a1[0] === ',' ? a1[1] : a1
100
+ const ctor = Array.isArray(arr) && arr[0] === 'new' ? 'new.' + arr[1]
101
+ : typeof arr === 'string'
102
+ ? (ctx.func?.localTypedElemsOverlay?.get(arr) ?? ctx.types?.typedElem?.get(arr)
103
+ ?? ctx.func?.localReps?.get(arr)?.typedCtor ?? ctx.scope?.globalTypedElem?.get(arr))
104
+ : null
105
+ return ctor === 'new.BigInt64Array' || ctor === 'new.BigInt64Array.view' ? VAL.BIGINT : VAL.NUMBER
106
+ }
57
107
  if (callee.startsWith('new.')) return VAL.TYPED
58
108
  if (callee.startsWith('math.')) return VAL.NUMBER
59
109
  const hostVT = ctx.module.hostImportValTypes?.get(callee)
@@ -89,8 +139,16 @@ export function methodValType(method, obj, objType, ctx) {
89
139
  // Mirrors the objType guard on map/filter/slice/concat.
90
140
  if (method === 'add') return objType === VAL.SET ? VAL.SET : null
91
141
  if (method === 'set') return objType === VAL.MAP ? VAL.MAP : null
142
+ // jz's valueOf is a receiver passthrough (module/string.js `.valueOf`), so the
143
+ // result kind IS the receiver kind — `Boolean(x).valueOf() === true` needs it.
144
+ if (method === 'valueOf') return objType ?? null
92
145
  if (BOOL_METHODS.has(method)) return VAL.BOOL
93
146
  if ((method === 'has' || method === 'delete') && (objType === VAL.MAP || objType === VAL.SET)) return VAL.BOOL
147
+ // ES2025 Set algebra — proven-SET receiver only (same guard rationale as add/set).
148
+ if ((method === 'union' || method === 'intersection' || method === 'difference' ||
149
+ method === 'symmetricDifference') && objType === VAL.SET) return VAL.SET
150
+ if ((method === 'isSubsetOf' || method === 'isSupersetOf' || method === 'isDisjointFrom') &&
151
+ objType === VAL.SET) return VAL.BOOL
94
152
  if (STRING_METHODS.has(method)) return VAL.STRING
95
153
  if (NUMBER_METHODS.has(method)) return VAL.NUMBER
96
154
  if (method === 'split') return VAL.ARRAY
@@ -98,9 +156,14 @@ export function methodValType(method, obj, objType, ctx) {
98
156
  if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
99
157
  return null
100
158
  }
101
- // .subarray / .toReversed / .toSorted / .with all return a typed array of the same kind.
102
- if (method === 'subarray' || method === 'toReversed' || method === 'toSorted' || method === 'with')
103
- return objType === VAL.TYPED ? VAL.TYPED : null
159
+ // .subarray returns a typed-array view (no plain-array analog). The ES2023
160
+ // change-by-copy trio return a fresh value of the RECEIVER's kind: a typed
161
+ // array from a typed receiver, a plain array from a plain-array receiver.
162
+ if (method === 'subarray') return objType === VAL.TYPED ? VAL.TYPED : null
163
+ if (method === 'toReversed' || method === 'toSorted' || method === 'with')
164
+ return objType === VAL.TYPED ? VAL.TYPED : objType === VAL.ARRAY ? VAL.ARRAY : null
165
+ // copyWithin mutates and returns the receiver.
166
+ if (method === 'copyWithin') return objType === VAL.TYPED || objType === VAL.ARRAY ? objType : null
104
167
  return null
105
168
  }
106
169
 
package/src/kind.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { ctx } from './ctx.js'
10
- import { VAL, lookupValType } from './reps.js'
10
+ import { VAL, lookupValType, repOf } from './reps.js'
11
11
  import { intLiteralValue, staticIndexKey } from './static.js'
12
12
  import {
13
13
  BOOL_OPS, NUMERIC_BINARY_OPS, NUMERIC_UNARY_OPS, COMPOUND_NUMERIC_OPS,
@@ -138,17 +138,46 @@ VT['?:'] = (args) => {
138
138
  if (truthy != null) return valTypeOf(truthy ? args[1] : args[2])
139
139
  const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
140
140
  if (ta && ta === tb) return ta
141
- // A boolean branch coerces to 0/1 in numeric context (same rule as &&/||/?? below):
142
- // when the other branch has a known non-boolean type, the conditional carries it.
143
- // Without this, `num + (cond ? num : num>k)` sees a null-typed operand and emits the
144
- // polymorphic string-concat dispatch on two pure-numeric subexprs — which pins the
145
- // whole number→string formatter (__str_concat → __to_str → __static_str), a pure-int
146
- // program ballooning 1 → ~19 funcs (see test/wat-invariants.js, .work/todo.md).
147
- if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
148
- if (tb === VAL.BOOL && ta && ta !== VAL.BOOL) return ta
141
+ // A boolean branch coerces to 0/1 in NUMERIC context: when the other branch is a
142
+ // known NUMBER, the conditional carries NUMBER the raw 0/1 bool carrier IS its
143
+ // ToNumber image, so the claim is benign and keeps `num + (cond ? num : num>k)`
144
+ // off the polymorphic string-concat dispatch (which pins the whole number→string
145
+ // formatter __str_concat → __to_str → __static_str, a pure-int program
146
+ // ballooning 1 → ~19 funcs; see test/wat-invariants.js, .work/todo.md).
147
+ // Any OTHER mix is null: both ternary arms are "the value", so claiming the
148
+ // non-bool arm's kind would let strict-eq's differing-class fold constant-fold
149
+ // `x === true` on a value that IS sometimes a boolean (watr's `i ? true :
150
+ // [from,len]` rec marker); the bool arm materializes as its atom at emit
151
+ // (emit.js '?:') and stays observable. (&&/||/?? below keep the full carry —
152
+ // there the bool side is a GUARD whose value surfaces only when falsy, and the
153
+ // carry is what types `cond && typedArr` guarded-use idioms.)
154
+ if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb === VAL.NUMBER ? VAL.NUMBER : null
155
+ if (tb === VAL.BOOL && ta && ta !== VAL.BOOL) return ta === VAL.NUMBER ? VAL.NUMBER : null
156
+ // BIGINT arm + nullish-LITERAL arm carries BIGINT. BIGINT is the one kind
157
+ // with NO runtime tag — raw i64 bits ride the f64 slot, indistinguishable
158
+ // from a number — so a dispatcher that loses the static kind has no runtime
159
+ // fork to fall back on: tryRuntimeStringFork's non-NaN arm claimed
160
+ // `(c ? BigInt(x) : null).toString(16)` as NUMBER and formatted the bits as
161
+ // a denormal ("0.000…"), watr's `cb ? BigInt(cb.value) : null` folder shape.
162
+ // Sound where the bool-arm carry above is not: a nullish receiver is
163
+ // TypeError-class in JS (no method table to mis-pick), the nullish arm
164
+ // materializes as its ATOM whose bits the sentinel compare still matches at
165
+ // runtime, and the decl-site mayBeNullish flag (analyze.js) plus
166
+ // nullableOperand (emit.js) keep `x == null` folds honest — narrow.js
167
+ // re-derives that nullability across call boundaries for BIGINT params.
168
+ // Tagged kinds stay null here on purpose: their runtime fork handles the
169
+ // mix soundly and their eq-folds stay maximally live.
170
+ if (ta === VAL.BIGINT && nullishArm(args[2])) return VAL.BIGINT
171
+ if (tb === VAL.BIGINT && nullishArm(args[1])) return VAL.BIGINT
149
172
  return null
150
173
  }
151
174
 
175
+ // AST nullish literal — mirrors ir.js isNullishLit ([null,null] = null literal,
176
+ // [] = undefined) plus the bare `undefined` name form recordGlobalRep accepts;
177
+ // local copy because ir.js already imports valTypeOf from here (cycle).
178
+ const nullishArm = (n) => n === 'undefined' ||
179
+ (Array.isArray(n) && ((n.length === 2 && n[0] == null && n[1] == null) || n.length === 0))
180
+
152
181
  // Value-preserving logical: `&&`/`||` return one of their operands.
153
182
  // When both sides share a type, return it. When one side is boolean
154
183
  // (a condition/guard) and the other has a known non-boolean type,
@@ -175,6 +204,18 @@ VT['[]'] = (args) => {
175
204
  // element type. Returning a numeric elem type here would let `a[-1] === undefined`
176
205
  // fold to false (a NUMBER can't be undefined), silently dropping the guard.
177
206
  { const li = intLiteralValue(args[1]); if (li != null && li < 0) return null }
207
+ // A non-numeric STRING-literal key is a PROPERTY read, not an element read:
208
+ // on arrays/typed arrays it yields undefined (or a builtin method), never the
209
+ // element kind. Typing it by elem let `a['@@iterator'] != null` fold TRUE on
210
+ // a known array — the drain/GetIterator guards then called undefined (table
211
+ // OOB). Canonical numeric strings ('0','1',…) DO address elements
212
+ // (ToPropertyKey) and keep the elem typing below.
213
+ {
214
+ const k = args[1]
215
+ const lit = Array.isArray(k) && k.length === 2 && k[0] == null ? k[1]
216
+ : Array.isArray(k) && k[0] === 'str' ? k[1] : undefined
217
+ if (typeof lit === 'string' && !/^(0|[1-9][0-9]*)$/.test(lit)) return null
218
+ }
178
219
  // SRoA flat-array slot read: `a[k]` (static index) where `a` dissolved into
179
220
  // scalar `a#i` locals (scanFlatObjects). A write-once slot's value-type is its
180
221
  // element literal's — same numeric-binding as the `VT['.']` object case, so
@@ -191,6 +232,11 @@ VT['[]'] = (args) => {
191
232
  }
192
233
  // Indexed read on a known typed-array receiver yields Number except for
193
234
  // BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
235
+ // An UNPROVEN index can read past the end (= undefined per spec), but the undef
236
+ // box is a NaN bit-pattern, so it COINCIDES with ToNumber(undefined) through
237
+ // every numeric path — the NUMBER claim stays sound for dispatch (numeric arms,
238
+ // the vectorizer). Only identity observations diverge; those folds consult
239
+ // typedReadMaybeOob below and keep the runtime compare.
194
240
  if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
195
241
  return typedCtorElemValType(ctx.types.typedElem?.get(args[0])) || VAL.NUMBER
196
242
  // Indexed read on a STRING returns a 1-char string (SSO at runtime).
@@ -260,10 +306,25 @@ VT['.'] = (args) => {
260
306
  // OBJECT `.prop` propagation: when the receiver chain roots at a binding
261
307
  // sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
262
308
  // child's val-type. Generic for any compile-time-known JSON literal.
309
+ // The shape's per-prop kind is a DECL-SITE fact — writes can invalidate it:
310
+ // - a sid-bound receiver whose schema declares the prop: the slot census
311
+ // above (slotVT) is authoritative — it saw every resolvable write and
312
+ // answered null on clash/poison, so the stale decl kind must not revive
313
+ // (`o.x = 'oops'; o.x + 1` skipped concat dispatch — live miscompile);
314
+ // - otherwise, the write-hazard sets cover unresolvable-receiver writes
315
+ // that could reach this object through an alias.
263
316
  const sh = shapeOf(args[0])
264
317
  if (sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) {
265
318
  const child = sh.props[args[1]]
266
- if (child) return child.val
319
+ if (child) {
320
+ const sid = typeof args[0] === 'string'
321
+ ? (repOf(args[0])?.schemaId ?? ctx.schema?.vars?.get(args[0])) : null
322
+ if (sid != null && ctx.schema?.list?.[sid]?.indexOf(args[1]) >= 0) return null
323
+ const hz = ctx.schema?.slotWriteHazards
324
+ if (hz && (hz.all || hz.props.has(args[1]) ||
325
+ (hz.numeric && /^(0|[1-9][0-9]*)$/.test(args[1])))) return null
326
+ return child.val
327
+ }
267
328
  }
268
329
  // Built-in property on a known sized kind — `.length` on STRING/ARRAY/TYPED,
269
330
  // `.size` on SET/MAP, `.byteLength`/`.byteOffset` on TYPED/BUFFER. These are
@@ -339,7 +400,11 @@ VT['()'] = (args) => {
339
400
  if (c === '{') return VAL.OBJECT
340
401
  if (c === '[') return VAL.ARRAY
341
402
  if (c === '"') return VAL.STRING
342
- if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
403
+ // 't'/'f' boolean: the parser mints the TRUE/FALSE atom (module/json.js
404
+ // litCase), NOT a raw 0/1 — claiming NUMBER here would let numeric fast
405
+ // paths raw-add the atom bits.
406
+ if (c === 't' || c === 'f') return VAL.BOOL
407
+ if (c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
343
408
  }
344
409
  } else {
345
410
  const vt = calleeValType(callee, args, ctx)
@@ -487,7 +552,14 @@ export function shapeOf(expr) {
487
552
  }
488
553
  if (op === '[]' && args.length === 2) {
489
554
  const parent = shapeOf(args[0])
490
- if (parent?.val === VAL.ARRAY) return parent.elem || null
555
+ if (parent?.val === VAL.ARRAY) {
556
+ // non-numeric string-literal key = PROPERTY read, not an element (see VT['[]'])
557
+ const k = args[1]
558
+ const lit = Array.isArray(k) && k.length === 2 && k[0] == null ? k[1]
559
+ : Array.isArray(k) && k[0] === 'str' ? k[1] : undefined
560
+ if (typeof lit === 'string' && !/^(0|[1-9][0-9]*)$/.test(lit)) return null
561
+ return parent.elem || null
562
+ }
491
563
  }
492
564
  return null
493
565
  }
package/src/op-policy.js CHANGED
@@ -10,10 +10,13 @@
10
10
 
11
11
  /** Ops prepare rejects when they appear in the AST (handler or identifier). */
12
12
  export const REJECT_OPS = {
13
- async: 'async/await not supported: WASM is synchronous',
14
- await: 'async/await not supported: WASM is synchronous',
13
+ async: 'this `async` shape needs the jzify lowering (async fns/arrows are supported in default mode; async methods are not yet)',
14
+ await: '`await` outside an async function (or in a shape the jzify lowering does not cover)',
15
15
  class: 'class not supported: use object literals',
16
- yield: 'generators not supported: use loops',
16
+ yield: 'yield outside a generator body (or in an unsupported position — see jzify/generators.js v1 surface)',
17
+ 'yield*': 'yield* not supported yet: loop over the inner iterator and yield each value',
18
+ 'new.target': '`new.target` not supported: no constructor reflection',
19
+ using: '`using` declarations need the jzify lowering (try/finally + [Symbol.dispose]) — not in the strict canonical subset',
17
20
  instanceof: 'instanceof not supported: use typeof',
18
21
  with: '`with` not supported: deprecated',
19
22
  ':': 'labeled statements not supported',
@@ -48,7 +51,7 @@ export const JZIFY_CLASS_ERRORS = {
48
51
  computedField: 'non-constant computed/destructured class fields are not supported',
49
52
  computedStaticMember: 'non-constant computed static class member names are not supported',
50
53
  accessor: 'class getters/setters are not supported — jz objects have no accessors',
51
- staticMember: '`static` class members are not supported yet',
54
+ staticMember: 'this `static` member shape is not supported (static fields, methods and blocks are)',
52
55
  superProp: '`super` property access is not supported yet',
53
56
  }
54
57