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/module/regex.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @module regex
8
8
  */
9
9
 
10
- import { typed, asF64, asI64, UNDEF_NAN, NULL_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
10
+ import { typed, asF64, asI64, UNDEF_NAN, NULL_NAN, mkPtrIR, temp, tempI32, toStrI64 } from '../src/ir.js'
11
11
  import { emit, deps } from '../src/bridge.js'
12
12
  import { ctx, err, inc, PTR, LAYOUT, registerGetter, declGlobal } from '../src/ctx.js'
13
13
  import { valTypeOf } from '../src/kind.js'
@@ -96,9 +96,26 @@ export const parseRegex = (pattern, flags = '') => {
96
96
  if (flags) ast.flags = flags
97
97
  ast.groups = groupNum
98
98
  if (groupNames.length) ast.groupNames = groupNames
99
+ resolveNamedBackrefs(ast, groupNames)
99
100
  return ast
100
101
  }
101
102
 
103
+ // Rewrite ['\k', name] placeholders → the SAME ['\N'] node numbered backrefs
104
+ // produce (in place, post-parse so forward references resolve). The match VM
105
+ // supports \1–\9 — a named group past index 9 can't be referenced by name.
106
+ const resolveNamedBackrefs = (node, names) => {
107
+ if (!Array.isArray(node)) return
108
+ if (node[0] === '\\k') {
109
+ const i = names.indexOf(node[1])
110
+ if (i < 0) perr(`Named backreference to undefined group '${node[1]}'`)
111
+ if (i > 9) perr('Named backreference to a group past index 9 unsupported')
112
+ node.length = 1
113
+ node[0] = '\\' + i
114
+ return
115
+ }
116
+ for (let j = 1; j < node.length; j++) resolveNamedBackrefs(node[j], names)
117
+ }
118
+
102
119
  const parseAlt = () => {
103
120
  const alts = [parseSeq()]
104
121
  while (cur() === PIPE) { skip(); alts.push(parseSeq()) }
@@ -180,6 +197,7 @@ const parseClassChar = () => {
180
197
  if (cur() === BSLASH) {
181
198
  skip(); const c = peek()
182
199
  if ('dDwWsS'.includes(c)) { skip(); return ['\\' + c] }
200
+ if (c === 'p' || c === 'P') perr('Unicode property escape \\p{…} unsupported')
183
201
  return parseEscapeChar()
184
202
  }
185
203
  return skip()
@@ -189,7 +207,13 @@ const parseEscape = () => {
189
207
  skip()
190
208
  const c = peek()
191
209
  if (c >= '1' && c <= '9') { skip(); return ['\\' + c] }
192
- if (c === 'k' && src.charCodeAt(idx + 1) === LT) perr('Named backreference unsupported')
210
+ // \k<name> parse to a placeholder; parseRegex resolves it to the group's
211
+ // NUMBERED backref node once all groups are known (forward refs included),
212
+ // so the compile/match VM needs no new cases.
213
+ if (c === 'k' && src.charCodeAt(idx + 1) === LT) { skip(); skip(); return ['\\k', parseGroupName()] }
214
+ // \p{…}/\P{…} need the Unicode property tables (multi-KB) — out of scope.
215
+ // Falling through would match the LITERAL text "p{…}" — silently wrong.
216
+ if (c === 'p' || c === 'P') perr('Unicode property escape \\p{…} unsupported')
193
217
  if ('dDwWsS'.includes(c)) { skip(); return ['\\' + c] }
194
218
  if (c === 'b' || c === 'B') { skip(); return ['\\' + c] }
195
219
  return parseEscapeChar()
@@ -733,8 +757,86 @@ const patternMinLen = node => {
733
757
  export default (ctx) => {
734
758
  deps({
735
759
  __str_to_buf: ['__str_byteLen', '__char_at'],
760
+ __regexp_escape: ['__to_str', '__str_byteLen', '__char_at', '__alloc', '__mkptr', '__sso_norm'],
736
761
  })
737
762
 
763
+ // RegExp.escape (ES2025) — escape a string for literal use inside a pattern.
764
+ // Spec sets, as jz UTF-8 byte tests (non-ASCII bytes are never regex-special
765
+ // and pass through; the spec's astral/whitespace \u-escapes don't arise in a
766
+ // byte-wise engine): SyntaxCharacter+`/` get a backslash; t/n/v/f/r their
767
+ // control escape; other punctuators + space `\xHH` (lowercase); an ASCII
768
+ // alnum FIRST char `\xHH` (so concatenating into a pattern can't fuse with a
769
+ // preceding token). Worst case 4 bytes per input byte.
770
+ const or = (tests) => tests.length === 1 ? tests[0] : `(i32.or ${tests[0]} ${or(tests.slice(1))})`
771
+ const eqAny = (codes) => or(codes.map(n => `(i32.eq (local.get $c) (i32.const ${n}))`))
772
+ const SYNTAX = [94, 36, 92, 46, 42, 43, 63, 40, 41, 91, 93, 123, 125, 124, 47] // ^$\.*+?()[]{}| /
773
+ const CTRL = [[9, 116], [10, 110], [11, 118], [12, 102], [13, 114]] // \t \n \v \f \r
774
+ const HEXED = [44, 45, 61, 60, 62, 35, 38, 33, 37, 58, 59, 64, 126, 39, 96, 34, 32] // ,-=<>#&!%:;@~'`" SP
775
+ const alnum = `(i32.or (i32.or
776
+ (i32.and (i32.ge_u (local.get $c) (i32.const 65)) (i32.le_u (local.get $c) (i32.const 90)))
777
+ (i32.and (i32.ge_u (local.get $c) (i32.const 97)) (i32.le_u (local.get $c) (i32.const 122))))
778
+ (i32.and (i32.ge_u (local.get $c) (i32.const 48)) (i32.le_u (local.get $c) (i32.const 57))))`
779
+ // \xHH writer (lowercase hex): "\\x" + hi + lo
780
+ const hexOut = `
781
+ (i32.store8 (i32.add (local.get $out) (local.get $j)) (i32.const 92))
782
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 1))) (i32.const 120))
783
+ (local.set $hi (i32.shr_u (local.get $c) (i32.const 4)))
784
+ (local.set $lo (i32.and (local.get $c) (i32.const 15)))
785
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 2)))
786
+ (i32.add (local.get $hi) (select (i32.const 87) (i32.const 48) (i32.gt_u (local.get $hi) (i32.const 9)))))
787
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 3)))
788
+ (i32.add (local.get $lo) (select (i32.const 87) (i32.const 48) (i32.gt_u (local.get $lo) (i32.const 9)))))
789
+ (local.set $j (i32.add (local.get $j) (i32.const 4)))`
790
+ const ctrlArms = CTRL.map(([code, esc]) => `
791
+ (if (i32.eq (local.get $c) (i32.const ${code})) (then
792
+ (i32.store8 (i32.add (local.get $out) (local.get $j)) (i32.const 92))
793
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 1))) (i32.const ${esc}))
794
+ (local.set $j (i32.add (local.get $j) (i32.const 2)))
795
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
796
+ (br $loop)))`).join('')
797
+ ctx.core.stdlib['__regexp_escape'] = `(func $__regexp_escape (param $val i64) (result f64)
798
+ (local $str i64) (local $slen i32) (local $base i32) (local $out i32)
799
+ (local $i i32) (local $j i32) (local $c i32) (local $hi i32) (local $lo i32)
800
+ (local.set $str (call $__to_str (local.get $val)))
801
+ (local.set $slen (call $__str_byteLen (local.get $str)))
802
+ (if (i32.eqz (local.get $slen))
803
+ (then (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.const ${LAYOUT.SSO_BIT}) (i32.const 0)))))
804
+ (local.set $base (call $__alloc (i32.add (i32.const 4) (i32.mul (local.get $slen) (i32.const 4)))))
805
+ (local.set $out (i32.add (local.get $base) (i32.const 4)))
806
+ (block $done (loop $loop
807
+ (br_if $done (i32.ge_u (local.get $i) (local.get $slen)))
808
+ (local.set $c (call $__char_at (local.get $str) (local.get $i)))
809
+ ;; first char alnum → \\xHH
810
+ (if (i32.and (i32.eqz (local.get $i)) ${alnum}) (then
811
+ ${hexOut}
812
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
813
+ (br $loop)))
814
+ ;; syntax chars + '/' → backslash-prefixed
815
+ (if ${eqAny(SYNTAX)} (then
816
+ (i32.store8 (i32.add (local.get $out) (local.get $j)) (i32.const 92))
817
+ (i32.store8 (i32.add (local.get $out) (i32.add (local.get $j) (i32.const 1))) (local.get $c))
818
+ (local.set $j (i32.add (local.get $j) (i32.const 2)))
819
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
820
+ (br $loop)))
821
+ ;; control escapes${ctrlArms}
822
+ ;; other punctuators + space → \\xHH
823
+ (if ${eqAny(HEXED)} (then
824
+ ${hexOut}
825
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
826
+ (br $loop)))
827
+ ;; passthrough
828
+ (i32.store8 (i32.add (local.get $out) (local.get $j)) (local.get $c))
829
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
830
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
831
+ (br $loop)))
832
+ (i32.store (local.get $base) (local.get $j))
833
+ (call $__sso_norm (call $__mkptr (i32.const ${PTR.STRING}) (i32.const 0) (local.get $out))))`
834
+ ctx.core.emit['RegExp.escape'] = (value) => {
835
+ inc('__regexp_escape')
836
+ return typed(['call', '$__regexp_escape',
837
+ value === undefined ? ['i64.const', UNDEF_NAN] : asI64(emit(value))], 'f64')
838
+ }
839
+
738
840
  ctx.runtime.regex = { count: 0, vars: new Map(), compiled: new Map(), groups: new Map(), groupNames: new Map() }
739
841
 
740
842
  // SSO → heap normalizer: returns data offset (i32) for direct byte access.
@@ -776,9 +878,21 @@ export default (ctx) => {
776
878
  ctx.runtime.regex.groupNames.set(id, collectGroupNames(ast))
777
879
  ctx.core.stdlib[funcName] = compileRegex(ast, funcName)
778
880
 
779
- // Search wrapper: tries match at each position, returns (match_start, match_end) via locals
881
+ // Search wrapper: tries match at each position, returns (match_start, match_end) via locals.
882
+ // A STICKY regex (/y) anchors: exactly one attempt at the start position — a
883
+ // forward scan is /g semantics, not /y (was silently identical before).
884
+ const sticky = (flags || '').includes('y')
780
885
  const searchName = `__regex_search_${id}`
781
- ctx.core.stdlib[searchName] = `(func $${searchName} (param $str i64) (result i32 i32)
886
+ ctx.core.stdlib[searchName] = sticky
887
+ ? `(func $${searchName} (param $str i64) (result i32 i32)
888
+ (local $off i32) (local $len i32) (local $result i32)
889
+ (local.set $off (call $__str_to_buf (local.get $str)))
890
+ (local.set $len (call $__str_byteLen (local.get $str)))
891
+ (local.set $result (call $${funcName} (local.get $off) (local.get $len) (i32.const 0)))
892
+ (if (i32.ge_s (local.get $result) (i32.const 0))
893
+ (then (return (i32.const 0) (local.get $result))))
894
+ (i32.const -1) (i32.const -1))`
895
+ : `(func $${searchName} (param $str i64) (result i32 i32)
782
896
  (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
783
897
  (local.set $off (call $__str_to_buf (local.get $str)))
784
898
  (local.set $len (call $__str_byteLen (local.get $str)))
@@ -792,9 +906,21 @@ export default (ctx) => {
792
906
  (br $next)))
793
907
  (i32.const -1) (i32.const -1))`
794
908
 
795
- // search_from: like search but starts scanning at $fromPos (used by global exec)
909
+ // search_from: like search but starts at $fromPos (used by stateful exec).
910
+ // Sticky: one attempt exactly at $fromPos (out-of-range → no match).
796
911
  const searchFromName = `__regex_search_from_${id}`
797
- ctx.core.stdlib[searchFromName] = `(func $${searchFromName} (param $str i64) (param $fromPos i32) (result i32 i32)
912
+ ctx.core.stdlib[searchFromName] = sticky
913
+ ? `(func $${searchFromName} (param $str i64) (param $fromPos i32) (result i32 i32)
914
+ (local $off i32) (local $len i32) (local $result i32)
915
+ (local.set $off (call $__str_to_buf (local.get $str)))
916
+ (local.set $len (call $__str_byteLen (local.get $str)))
917
+ (if (i32.gt_s (local.get $fromPos) (local.get $len))
918
+ (then (return (i32.const -1) (i32.const -1))))
919
+ (local.set $result (call $${funcName} (local.get $off) (local.get $len) (local.get $fromPos)))
920
+ (if (i32.ge_s (local.get $result) (i32.const 0))
921
+ (then (return (local.get $fromPos) (local.get $result))))
922
+ (i32.const -1) (i32.const -1))`
923
+ : `(func $${searchFromName} (param $str i64) (param $fromPos i32) (result i32 i32)
798
924
  (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
799
925
  (local.set $off (call $__str_to_buf (local.get $str)))
800
926
  (local.set $len (call $__str_byteLen (local.get $str)))
@@ -1123,10 +1249,34 @@ export default (ctx) => {
1123
1249
  ctx.core.emit['.string:matchAll'] = (str, search) => {
1124
1250
  const id = resolveRegex(search)
1125
1251
  if (id == null) err('matchAll requires a regex argument')
1252
+ // ES 22.1.3.14: matchAll throws TypeError without /g — flags are static here,
1253
+ // so fail at compile instead of scanning once like /g anyway (silent-wrong).
1254
+ if (!flagsOf(search).includes('g')) err('matchAll requires the /g flag (TypeError in JS)')
1255
+ const nGroups = ctx.runtime.regex.groups.get(id) || 0
1256
+ const groupNames = ctx.runtime.regex.groupNames.get(id) || []
1257
+ inc('__str_to_buf', '__str_byteLen', '__alloc', '__mkptr', `__regex_${id}`)
1258
+ const s = temp('mas'), outArr = tempI32('mao')
1259
+ return matchAllImpl(asF64(emit(str)), id, nGroups, groupNames, s, outArr)
1260
+ }
1261
+ // Generic twin (unknown receiver): ES String.prototype.matchAll ToString-
1262
+ // coerces its receiver, so route through the coercion into the same scan.
1263
+ // Its presence also arms emit's runtime string-fork — with ONLY the
1264
+ // `.string:` key, an untyped receiver (`let src = tbl[k]` narrowed by a
1265
+ // typeof-continue guard the static types can't follow) fell through to the
1266
+ // dyn-prop probe, yielded `undefined`, and for-of swallowed it SILENTLY —
1267
+ // the self-host global-snapshot sweep scanned nothing (byte-parity root #2,
1268
+ // pinned in test/regex.js).
1269
+ ctx.core.emit['.matchAll'] = (str, search) => {
1270
+ const id = resolveRegex(search)
1271
+ if (id == null) err('matchAll requires a regex argument')
1272
+ if (!flagsOf(search).includes('g')) err('matchAll requires the /g flag (TypeError in JS)')
1126
1273
  const nGroups = ctx.runtime.regex.groups.get(id) || 0
1127
1274
  const groupNames = ctx.runtime.regex.groupNames.get(id) || []
1128
1275
  inc('__str_to_buf', '__str_byteLen', '__alloc', '__mkptr', `__regex_${id}`)
1129
1276
  const s = temp('mas'), outArr = tempI32('mao')
1277
+ return matchAllImpl(typed(['f64.reinterpret_i64', toStrI64(null, asF64(emit(str)))], 'f64'), id, nGroups, groupNames, s, outArr)
1278
+ }
1279
+ function matchAllImpl(recvIR, id, nGroups, groupNames, s, outArr) {
1130
1280
  const off = tempI32('maof'), len = tempI32('maln'), pos = tempI32('maps')
1131
1281
  const res = tempI32('mars'), cnt = tempI32('macn'), wi = tempI32('mawi')
1132
1282
  const ms = tempI32('mams'), me = tempI32('mame')
@@ -1148,7 +1298,7 @@ export default (ctx) => {
1148
1298
  // globals just before buildMatchArr reads them — correct per-match captures.
1149
1299
  const matchArr = buildMatchArr(s, ms, me, nGroups, groupNames)
1150
1300
  return typed(['block', ['result', 'f64'],
1151
- ['local.set', `$${s}`, asF64(emit(str))],
1301
+ ['local.set', `$${s}`, recvIR],
1152
1302
  ['local.set', `$${off}`, ['call', '$__str_to_buf', sI64()]],
1153
1303
  ['local.set', `$${len}`, ['call', '$__str_byteLen', sI64()]],
1154
1304
  ['local.set', `$${cnt}`, ['i32.const', 0]],
package/module/schema.js CHANGED
@@ -106,6 +106,28 @@ export function initSchema(ctx) {
106
106
  return slot
107
107
  }
108
108
 
109
+ /** Devirtualization guard for a receiver whose static type is fully unknown
110
+ * (valTypeOf gives up entirely — no OBJECT proof at all, unlike `slotOf`'s
111
+ * structural fallback which *requires* one). Returns `{sid, slot}` when
112
+ * `prop` names a field on exactly ONE registered schema anywhere in the
113
+ * program — the subscript dispatch-descriptor pattern (`d.op`/`d.l`/`d.word`)
114
+ * and jz's own emit-table/IR-node reads under self-host both flow through a
115
+ * parameter or array element the static analysis never pins to VAL.OBJECT,
116
+ * even though every value that ever reaches the read is, in practice, that
117
+ * one schema. Unlike `slotOf`, the caller MUST runtime-guard the read (a
118
+ * masked NaN-box compare proving tag==OBJECT && aux==sid at once — see
119
+ * module/core.js's emitSchemaSlotGuarded) rather than trusting it
120
+ * unconditionally: with no static OBJECT evidence, the receiver could
121
+ * legitimately be anything, and the guard is what makes that safe. Two or
122
+ * more distinct schemas sharing the name (bucket.length > 1) is left to
123
+ * dynamic dispatch — a multi-way guard chases diminishing returns the
124
+ * common case (a genuine program-wide-unique field name) doesn't need. */
125
+ ctx.schema.guardedSlotOf = (prop) => {
126
+ const bucket = byProp.get(prop)
127
+ if (!bucket || bucket.length !== 1) return null
128
+ return { sid: bucket[0].id, slot: bucket[0].slot }
129
+ }
130
+
109
131
  /** Resolve the monomorphic slot value-type for `varName.prop`, or null.
110
132
  * Precise path only: requires the variable to have a bound `schemaId`
111
133
  * (ValueRep or `ctx.schema.vars`). Structural-subtyping is intentionally
@@ -114,22 +136,98 @@ export function initSchema(ctx) {
114
136
  * kinds) when in fact the holder isn't an object of any registered
115
137
  * schema. That mistyping then routes downstream property accesses
116
138
  * through __hash_get instead of __dyn_get_any, growing the binary. */
139
+ // Post-census belt for every slot-fact reader: a sid registered AFTER the
140
+ // censuses ran (the JSON emitters, spread/assign merge — extern slot
141
+ // writers) or hazarded by the write scan answers null/false even though the
142
+ // census maps never saw it. Census-time poisoning covers plan-known sids;
143
+ // this covers emit-time registrations at O(1) per read.
144
+ // `kindSafeOk` (slotVT only): kind-safe sids' sample KINDS were observed
145
+ // into slotTypes at the census, so the kind reader may trust the map —
146
+ // unless the entry is a null-kinds emit-belt fallback. Value-level readers
147
+ // (intCertain, elem-ctors) always fail closed on kind-safe sids: the JSON
148
+ // parser writes arbitrary doubles/values within the sample's kinds.
149
+ const slotHazarded = (id, prop, kindSafeOk = false) => {
150
+ if (ctx.schema.externSlotSids?.has(id)) return true
151
+ const hz = ctx.schema.slotWriteHazards
152
+ if (!hz) return false
153
+ if (hz.kindSafeSids?.has(id) && (!kindSafeOk || hz.kindSafeSids.get(id) == null)) return true
154
+ return hz.all || hz.sids.has(id) || hz.props.has(prop) ||
155
+ (hz.numeric && /^(0|[1-9][0-9]*)$/.test(String(prop)))
156
+ }
157
+
117
158
  ctx.schema.slotVT = (varName, prop) => {
118
159
  const id = ctx.schema.idOf(varName)
119
- if (id == null) return null
160
+ if (id == null || slotHazarded(id, prop, true)) return null
120
161
  const idx = ctx.schema.list[id]?.indexOf(prop)
121
162
  return idx >= 0 ? (ctx.schema.slotTypes.get(id)?.[idx] ?? null) : null
122
163
  }
123
164
 
165
+ /** Resolve the monomorphic typed-array ctor for `varName.prop`, or null.
166
+ * Same precise-path discipline as slotVT. Additionally gated on the prop
167
+ * never appearing as a WRITE target anywhere in the program
168
+ * (ctx.types.writtenProps): the ctor drives raw typed loads/stores, so a
169
+ * single `o.twRe = somethingElse` anywhere must keep the dynamic path —
170
+ * object-literal initial values are not writes and don't poison. */
171
+ ctx.schema.slotTypedCtorAt = (varName, prop) =>
172
+ ctx.schema.slotTypedCtorBySid(ctx.schema.idOf(varName), prop)
173
+
174
+ /** Raw by-sid form for callers that resolve the receiver's schema themselves
175
+ * (narrow's per-caller localSids — live reps aren't trustworthy there). */
176
+ ctx.schema.slotTypedCtorBySid = (id, prop) => {
177
+ // fail CLOSED: without the program-wide write census the ctor can't be trusted
178
+ if (!ctx.types.writtenProps || ctx.types.writtenProps.has(prop)) return null
179
+ if (id == null || slotHazarded(id, prop)) return null
180
+ const idx = ctx.schema.list[id]?.indexOf(prop)
181
+ if (idx == null || idx < 0) return null
182
+ return ctx.schema.slotTypedCtors.get(id)?.[idx] ?? null
183
+ }
184
+
185
+ /** Program-wide census ctor for a bare `.prop` read with NO receiver evidence
186
+ * — the SPECULATIVE sibling of slotTypedCtorBySid (guardedSlotOf's contract):
187
+ * every schema that declares `prop` must census the same typed ctor, and the
188
+ * consumer MUST runtime-guard the value (it could legitimately be anything).
189
+ * Feeds narrow's speculateTypedParams, never an unguarded load. */
190
+ ctx.schema.slotTypedCtorByProp = (prop) => {
191
+ if (!ctx.types.writtenProps || ctx.types.writtenProps.has(prop)) return null
192
+ const bucket = byProp.get(prop)
193
+ if (!bucket?.length) return null
194
+ let ctor = null
195
+ for (const b of bucket) {
196
+ if (slotHazarded(b.id, prop)) return null
197
+ const c = ctx.schema.slotTypedCtors.get(b.id)?.[b.slot] ?? null
198
+ if (!c || (ctor && c !== ctor)) return null
199
+ ctor = c
200
+ }
201
+ return ctor
202
+ }
203
+
124
204
  /** Resolve per-slot intCertain: returns true iff every observed write to
125
205
  * `varName.prop` is integer-shaped. Precise path only — requires `varName`
126
206
  * to have a bound `schemaId`. Consumers (Math.floor elision, toNumF64 skip,
127
207
  * intIndexIR) treat `false`/`null` identically (no narrowing). */
128
208
  ctx.schema.slotIntCertainAt = (varName, prop) => {
129
209
  const id = ctx.schema.idOf(varName)
130
- if (id == null) return false
210
+ if (id == null || slotHazarded(id, prop)) return false
131
211
  const idx = ctx.schema.list[id]?.indexOf(prop)
132
212
  if (idx < 0) return false
133
213
  return ctx.schema.slotIntCertain.get(id)?.[idx] === true
134
214
  }
215
+
216
+ /** Strict-int32 sibling of slotIntCertainAt: every write is exactly-int32
217
+ * and never -0, so `i32.trunc_sat_f64_s(f64.load(slot))` is value-exact.
218
+ * Feeds raw i32 slot loads + i32 local typing — a wrong answer here is a
219
+ * wrong VALUE (saturation), so it shares every fail-closed belt. */
220
+ ctx.schema.slotI32CertainAt = (varName, prop) => {
221
+ const id = ctx.schema.idOf(varName)
222
+ if (id == null || slotHazarded(id, prop)) return false
223
+ const idx = ctx.schema.list[id]?.indexOf(prop)
224
+ if (idx < 0) return false
225
+ return ctx.schema.slotI32Certain.get(id)?.[idx] === true
226
+ }
227
+ ctx.schema.slotI32CertainBySid = (id, prop) => {
228
+ if (id == null || slotHazarded(id, prop)) return false
229
+ const idx = ctx.schema.list[id]?.indexOf(prop)
230
+ if (idx == null || idx < 0) return false
231
+ return ctx.schema.slotI32Certain.get(id)?.[idx] === true
232
+ }
135
233
  }