jz 0.8.1 → 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.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +299 -44
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +892 -32
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- 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
|
-
|
|
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] =
|
|
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
|
|
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] =
|
|
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}`,
|
|
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
|
}
|