jz 0.1.1 → 0.2.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 +257 -90
- package/cli.js +34 -8
- package/index.js +102 -13
- package/module/array.js +396 -133
- package/module/collection.js +455 -212
- package/module/console.js +169 -88
- package/module/core.js +246 -144
- package/module/date.js +691 -0
- package/module/function.js +81 -21
- package/module/index.js +2 -1
- package/module/json.js +483 -138
- package/module/math.js +79 -39
- package/module/number.js +188 -78
- package/module/object.js +227 -47
- package/module/regex.js +33 -30
- package/module/schema.js +14 -17
- package/module/string.js +434 -235
- package/module/symbol.js +4 -3
- package/module/timer.js +89 -31
- package/module/typedarray.js +174 -44
- package/package.json +3 -10
- package/src/analyze.js +626 -132
- package/src/autoload.js +14 -6
- package/src/compile.js +399 -70
- package/src/ctx.js +41 -12
- package/src/emit.js +634 -145
- package/src/host.js +244 -51
- package/src/ir.js +81 -31
- package/src/jzify.js +181 -24
- package/src/narrow.js +32 -4
- package/src/optimize.js +628 -42
- package/src/plan.js +1132 -4
- package/src/prepare.js +321 -75
- package/src/vectorize.js +1016 -0
- package/wasi.js +13 -0
- package/src/key.js +0 -73
- package/src/source.js +0 -76
package/src/analyze.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { ctx, err } from './ctx.js'
|
|
27
|
+
import { isLiteralStr, isFuncRef } from './ir.js'
|
|
27
28
|
|
|
28
29
|
export const T = '\uE000'
|
|
29
30
|
|
|
@@ -89,7 +90,7 @@ export const VAL = {
|
|
|
89
90
|
NUMBER: 'number', ARRAY: 'array', STRING: 'string',
|
|
90
91
|
OBJECT: 'object', HASH: 'hash', SET: 'set', MAP: 'map',
|
|
91
92
|
CLOSURE: 'closure', TYPED: 'typed', REGEX: 'regex',
|
|
92
|
-
BIGINT: 'bigint', BUFFER: 'buffer',
|
|
93
|
+
BIGINT: 'bigint', BUFFER: 'buffer', DATE: 'date',
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/**
|
|
@@ -233,6 +234,7 @@ export function valTypeOf(expr) {
|
|
|
233
234
|
// Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint
|
|
234
235
|
if (args.length === 0) return null // undefined literal
|
|
235
236
|
if (args[0] == null) return null // null literal
|
|
237
|
+
if (typeof args[0] === 'symbol') return null // prepared null sentinel
|
|
236
238
|
return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
|
|
237
239
|
}
|
|
238
240
|
|
|
@@ -241,16 +243,22 @@ export function valTypeOf(expr) {
|
|
|
241
243
|
if (op === '=>') return VAL.CLOSURE
|
|
242
244
|
if (op === '//') return VAL.REGEX
|
|
243
245
|
if (op === '{}' && args[0]?.[0] === ':') return VAL.OBJECT
|
|
246
|
+
if (op === '?:') {
|
|
247
|
+
const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
|
|
248
|
+
return ta && ta === tb ? ta : null
|
|
249
|
+
}
|
|
244
250
|
// `[]` op covers both array literals (1 arg) and index access (2 args).
|
|
245
251
|
// Array literal: `[]` → ['[]', null]; `[1,2]` → ['[]', [',', ...]]; `[x]` → ['[]', x].
|
|
246
252
|
// Index access: `arr[i]` → ['[]', arr, i].
|
|
247
253
|
if (op === '[]') {
|
|
248
254
|
if (args.length < 2) return VAL.ARRAY
|
|
249
|
-
// Indexed read on a known typed-array receiver yields
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
// Indexed read on a known typed-array receiver yields Number except for
|
|
256
|
+
// BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
|
|
257
|
+
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
|
|
258
|
+
return typedCtorElemValType(ctx.types.typedElem?.get(args[0])) || VAL.NUMBER
|
|
259
|
+
// Indexed read on a STRING returns a 1-char string (SSO at runtime).
|
|
260
|
+
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.STRING) return VAL.STRING
|
|
261
|
+
if (Array.isArray(args[0]) && valTypeOf(args[0]) === VAL.STRING) return VAL.STRING
|
|
254
262
|
// Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
|
|
255
263
|
// Set by analyzeValTypes from body observations + emitFunc preseed for params.
|
|
256
264
|
if (typeof args[0] === 'string') {
|
|
@@ -267,12 +275,12 @@ export function valTypeOf(expr) {
|
|
|
267
275
|
const slotVT = ctx.schema.slotVT(args[0], args[1])
|
|
268
276
|
if (slotVT) return slotVT
|
|
269
277
|
}
|
|
270
|
-
//
|
|
278
|
+
// OBJECT `.prop` propagation: when the receiver chain roots at a binding
|
|
271
279
|
// sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
|
|
272
280
|
// child's val-type. Generic for any compile-time-known JSON literal.
|
|
273
281
|
if (op === '.' && typeof args[1] === 'string') {
|
|
274
282
|
const sh = shapeOf(args[0])
|
|
275
|
-
if (sh?.vt === VAL.HASH) {
|
|
283
|
+
if (sh?.vt === VAL.OBJECT || sh?.vt === VAL.HASH) {
|
|
276
284
|
const child = sh.props[args[1]]
|
|
277
285
|
if (child) return child.vt
|
|
278
286
|
}
|
|
@@ -301,6 +309,7 @@ export function valTypeOf(expr) {
|
|
|
301
309
|
if (typeof callee === 'string') {
|
|
302
310
|
if (callee === 'new.Set') return VAL.SET
|
|
303
311
|
if (callee === 'new.Map') return VAL.MAP
|
|
312
|
+
if (callee === 'new.Date') return VAL.DATE
|
|
304
313
|
if (callee === 'new.ArrayBuffer') return VAL.BUFFER
|
|
305
314
|
if (callee === 'new.DataView') return VAL.BUFFER
|
|
306
315
|
if (callee.startsWith('new.')) return VAL.TYPED
|
|
@@ -310,7 +319,10 @@ export function valTypeOf(expr) {
|
|
|
310
319
|
const src = jsonConstString(args[1])
|
|
311
320
|
if (src != null) {
|
|
312
321
|
const c = src.trimStart()[0]
|
|
313
|
-
|
|
322
|
+
// Objects emit as fixed-shape OBJECT (slot-based) — see
|
|
323
|
+
// module/json.js:emitJsonConstValue. The downstream `.prop` reads
|
|
324
|
+
// hit emitSchemaSlotRead via ctx.schema.find, bypassing hash probes.
|
|
325
|
+
if (c === '{') return VAL.OBJECT
|
|
314
326
|
if (c === '[') return VAL.ARRAY
|
|
315
327
|
if (c === '"') return VAL.STRING
|
|
316
328
|
if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
|
|
@@ -340,16 +352,21 @@ export function valTypeOf(expr) {
|
|
|
340
352
|
return null
|
|
341
353
|
}
|
|
342
354
|
if (method === 'push') return VAL.ARRAY
|
|
355
|
+
if ((method === 'shift' || method === 'pop') && typeof obj === 'string') {
|
|
356
|
+
const elemVt = ctx.func.repByLocal?.get(obj)?.arrayElemValType
|
|
357
|
+
if (elemVt) return elemVt
|
|
358
|
+
}
|
|
343
359
|
if (method === 'add' || method === 'delete') return VAL.SET
|
|
344
360
|
if (method === 'set') return VAL.MAP
|
|
345
361
|
// String-returning methods
|
|
346
362
|
if (['toUpperCase', 'toLowerCase', 'trim', 'trimStart', 'trimEnd',
|
|
347
|
-
'repeat', 'padStart', 'padEnd', 'replace', 'charAt', 'substring'].includes(method)) return VAL.STRING
|
|
363
|
+
'repeat', 'padStart', 'padEnd', 'replace', 'replaceAll', 'charAt', 'substring'].includes(method)) return VAL.STRING
|
|
364
|
+
if (method === 'split') return VAL.ARRAY
|
|
348
365
|
// slice/concat preserve caller type (string.slice → string, array.slice → array)
|
|
349
366
|
if (method === 'slice' || method === 'concat') {
|
|
350
367
|
const objType = valTypeOf(obj)
|
|
351
|
-
if (objType) return objType
|
|
352
|
-
return
|
|
368
|
+
if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
|
|
369
|
+
return null
|
|
353
370
|
}
|
|
354
371
|
}
|
|
355
372
|
}
|
|
@@ -359,7 +376,22 @@ export function valTypeOf(expr) {
|
|
|
359
376
|
function jsonConstString(expr) {
|
|
360
377
|
if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
|
|
361
378
|
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
|
|
362
|
-
if (typeof expr === 'string')
|
|
379
|
+
if (typeof expr === 'string') {
|
|
380
|
+
// Prefer shapeStrs (broader: includes effectively-const `let` string literals
|
|
381
|
+
// at module scope) over constStrs (const-only). Module/json's static-fold
|
|
382
|
+
// path uses its own constStrs-only resolver to avoid folding `let`-bound
|
|
383
|
+
// initializers — preserving the user-controlled distinction. Shape
|
|
384
|
+
// inference is sound either way: an effectively-const literal's value is
|
|
385
|
+
// invariant, so the parsed shape it produces is too.
|
|
386
|
+
return ctx.scope.shapeStrs?.get(expr) ?? ctx.scope.constStrs?.get(expr) ?? null
|
|
387
|
+
}
|
|
388
|
+
return null
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function jsonShapeStrings(expr) {
|
|
392
|
+
const single = jsonConstString(expr)
|
|
393
|
+
if (single != null) return [single]
|
|
394
|
+
if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
|
|
363
395
|
return null
|
|
364
396
|
}
|
|
365
397
|
|
|
@@ -384,18 +416,19 @@ function shapeOfJsonValue(v) {
|
|
|
384
416
|
}
|
|
385
417
|
if (typeof v === 'object') {
|
|
386
418
|
const props = Object.create(null)
|
|
387
|
-
|
|
419
|
+
const names = Object.keys(v)
|
|
420
|
+
for (const k of names) {
|
|
388
421
|
const s = shapeOfJsonValue(v[k])
|
|
389
422
|
if (s) props[k] = s
|
|
390
423
|
}
|
|
391
|
-
return { vt: VAL.
|
|
424
|
+
return { vt: VAL.OBJECT, props, names }
|
|
392
425
|
}
|
|
393
426
|
return null
|
|
394
427
|
}
|
|
395
428
|
|
|
396
429
|
function shapeUnifies(a, b) {
|
|
397
430
|
if (!a || !b || a.vt !== b.vt) return false
|
|
398
|
-
if (a.vt === VAL.HASH) {
|
|
431
|
+
if (a.vt === VAL.OBJECT || a.vt === VAL.HASH) {
|
|
399
432
|
const ak = Object.keys(a.props), bk = Object.keys(b.props)
|
|
400
433
|
if (ak.length !== bk.length) return false
|
|
401
434
|
for (const k of ak) {
|
|
@@ -409,6 +442,16 @@ function shapeUnifies(a, b) {
|
|
|
409
442
|
return true
|
|
410
443
|
}
|
|
411
444
|
|
|
445
|
+
function shapeLayoutUnifies(a, b) {
|
|
446
|
+
if (!shapeUnifies(a, b)) return false
|
|
447
|
+
if (a.vt === VAL.OBJECT || a.vt === VAL.HASH) {
|
|
448
|
+
if (a.names?.length !== b.names?.length) return false
|
|
449
|
+
for (let i = 0; i < a.names.length; i++) if (a.names[i] !== b.names[i]) return false
|
|
450
|
+
}
|
|
451
|
+
if (a.vt === VAL.ARRAY && a.elem) return shapeLayoutUnifies(a.elem, b.elem)
|
|
452
|
+
return true
|
|
453
|
+
}
|
|
454
|
+
|
|
412
455
|
const _jsonShapeCache = new WeakMap()
|
|
413
456
|
function parseJsonShape(src) {
|
|
414
457
|
if (typeof src !== 'string') return null
|
|
@@ -420,6 +463,18 @@ function parseJsonShape(src) {
|
|
|
420
463
|
return sh
|
|
421
464
|
}
|
|
422
465
|
|
|
466
|
+
function parseUnifiedJsonShape(srcs) {
|
|
467
|
+
if (!srcs?.length) return null
|
|
468
|
+
let out = null
|
|
469
|
+
for (const src of srcs) {
|
|
470
|
+
const sh = parseJsonShape(src)
|
|
471
|
+
if (!sh) return null
|
|
472
|
+
if (!out) out = sh
|
|
473
|
+
else if (!shapeLayoutUnifies(out, sh)) return null
|
|
474
|
+
}
|
|
475
|
+
return out
|
|
476
|
+
}
|
|
477
|
+
|
|
423
478
|
/** Resolve the json shape for an expression by walking name → rep.jsonShape and
|
|
424
479
|
* `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
|
|
425
480
|
export function shapeOf(expr) {
|
|
@@ -427,12 +482,12 @@ export function shapeOf(expr) {
|
|
|
427
482
|
if (!Array.isArray(expr)) return null
|
|
428
483
|
const [op, ...args] = expr
|
|
429
484
|
if (op === '()' && args[0] === 'JSON.parse') {
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
485
|
+
const srcs = jsonShapeStrings(args[1])
|
|
486
|
+
if (srcs) return parseUnifiedJsonShape(srcs)
|
|
432
487
|
}
|
|
433
488
|
if (op === '.' && typeof args[1] === 'string') {
|
|
434
489
|
const parent = shapeOf(args[0])
|
|
435
|
-
if (parent?.vt === VAL.HASH) return parent.props[args[1]] || null
|
|
490
|
+
if (parent?.vt === VAL.OBJECT || parent?.vt === VAL.HASH) return parent.props[args[1]] || null
|
|
436
491
|
}
|
|
437
492
|
if (op === '[]' && args.length === 2) {
|
|
438
493
|
const parent = shapeOf(args[0])
|
|
@@ -442,6 +497,84 @@ export function shapeOf(expr) {
|
|
|
442
497
|
}
|
|
443
498
|
|
|
444
499
|
|
|
500
|
+
/** Static property-key evaluation for computed member names: folds a node into
|
|
501
|
+
* its constant value (numeric, string, boolean, null) and stringifies it. Returns
|
|
502
|
+
* null if any sub-expression isn't statically known. Handles literals, named
|
|
503
|
+
* string constants, String()/Number() casts, ternaries, short-circuit ops, and
|
|
504
|
+
* unary/binary arithmetic and bit ops. */
|
|
505
|
+
const NO_VALUE = Symbol('no-static-property-key')
|
|
506
|
+
|
|
507
|
+
export function staticPropertyKey(node) {
|
|
508
|
+
const value = staticValue(node)
|
|
509
|
+
return value === NO_VALUE ? null : String(value)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function staticValue(node) {
|
|
513
|
+
if (node === undefined) return undefined
|
|
514
|
+
if (node === null || typeof node === 'number' || typeof node === 'boolean') return node
|
|
515
|
+
if (typeof node === 'string') return ctx.scope.constStrs?.get(node) ?? NO_VALUE
|
|
516
|
+
if (!Array.isArray(node)) return NO_VALUE
|
|
517
|
+
|
|
518
|
+
const [op, ...args] = node
|
|
519
|
+
if (op == null) return args.length ? args[0] : undefined
|
|
520
|
+
if (op === 'str') return args[0]
|
|
521
|
+
if (op === '[]' && args.length === 1) return staticValue(args[0])
|
|
522
|
+
if (op === '()' && args[0] === 'String' && args.length === 2) {
|
|
523
|
+
const value = staticValue(args[1])
|
|
524
|
+
return value === NO_VALUE ? NO_VALUE : String(value)
|
|
525
|
+
}
|
|
526
|
+
if (op === '()' && args[0] === 'Number' && args.length === 2) {
|
|
527
|
+
const value = staticValue(args[1])
|
|
528
|
+
return value === NO_VALUE ? NO_VALUE : Number(value)
|
|
529
|
+
}
|
|
530
|
+
if (op === '?:' || op === '?') {
|
|
531
|
+
const cond = staticValue(args[0])
|
|
532
|
+
return cond === NO_VALUE ? NO_VALUE : staticValue(cond ? args[1] : args[2])
|
|
533
|
+
}
|
|
534
|
+
if (op === '&&' || op === '||') {
|
|
535
|
+
const left = staticValue(args[0])
|
|
536
|
+
if (left === NO_VALUE) return NO_VALUE
|
|
537
|
+
return op === '&&' ? (left ? staticValue(args[1]) : left) : (left ? left : staticValue(args[1]))
|
|
538
|
+
}
|
|
539
|
+
if (op === '??') {
|
|
540
|
+
const left = staticValue(args[0])
|
|
541
|
+
return left === NO_VALUE ? NO_VALUE : left == null ? staticValue(args[1]) : left
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (args.length === 1) {
|
|
545
|
+
const value = staticValue(args[0])
|
|
546
|
+
if (value === NO_VALUE) return NO_VALUE
|
|
547
|
+
if (op === 'u+') return +value
|
|
548
|
+
if (op === 'u-') return -value
|
|
549
|
+
if (op === '!') return !value
|
|
550
|
+
if (op === '~') return ~value
|
|
551
|
+
return NO_VALUE
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (args.length === 2) {
|
|
555
|
+
const left = staticValue(args[0])
|
|
556
|
+
const right = staticValue(args[1])
|
|
557
|
+
if (left === NO_VALUE || right === NO_VALUE) return NO_VALUE
|
|
558
|
+
switch (op) {
|
|
559
|
+
case '+': return typeof left === 'string' || typeof right === 'string' ? String(left) + String(right) : Number(left) + Number(right)
|
|
560
|
+
case '-': return Number(left) - Number(right)
|
|
561
|
+
case '*': return Number(left) * Number(right)
|
|
562
|
+
case '/': return Number(left) / Number(right)
|
|
563
|
+
case '%': return Number(left) % Number(right)
|
|
564
|
+
case '**': return Number(left) ** Number(right)
|
|
565
|
+
case '&': return Number(left) & Number(right)
|
|
566
|
+
case '|': return Number(left) | Number(right)
|
|
567
|
+
case '^': return Number(left) ^ Number(right)
|
|
568
|
+
case '<<': return Number(left) << Number(right)
|
|
569
|
+
case '>>': return Number(left) >> Number(right)
|
|
570
|
+
case '>>>': return Number(left) >>> Number(right)
|
|
571
|
+
default: return NO_VALUE
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return NO_VALUE
|
|
576
|
+
}
|
|
577
|
+
|
|
445
578
|
/** Decode a `['{}', ...]` AST's children into `{names, values}`, or null if any
|
|
446
579
|
* property is non-static-key (computed key, spread, shorthand). Matches the
|
|
447
580
|
* emitter's flatten rule for comma-grouped props. Used by collectProgramFacts,
|
|
@@ -457,6 +590,15 @@ export function staticObjectProps(args) {
|
|
|
457
590
|
return names.length ? { names, values } : null
|
|
458
591
|
}
|
|
459
592
|
|
|
593
|
+
function staticArrayElems(expr) {
|
|
594
|
+
if (!Array.isArray(expr)) return null
|
|
595
|
+
if (expr[0] === '[') return expr.slice(1)
|
|
596
|
+
if (expr[0] !== '[]' || expr.length >= 3) return null
|
|
597
|
+
const arg = expr[1]
|
|
598
|
+
if (arg == null) return []
|
|
599
|
+
return Array.isArray(arg) && arg[0] === ',' ? arg.slice(1) : [arg]
|
|
600
|
+
}
|
|
601
|
+
|
|
460
602
|
/** Schema-id for an object literal expression. Returns null on dynamic keys, spread, shorthand. */
|
|
461
603
|
function objLiteralSchemaId(expr) {
|
|
462
604
|
if (!Array.isArray(expr) || expr[0] !== '{}' || !ctx.schema?.register) return null
|
|
@@ -508,7 +650,7 @@ export function typedElemCtor(rhs) {
|
|
|
508
650
|
const _ELEM_AUX = {
|
|
509
651
|
Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
|
|
510
652
|
Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
|
|
511
|
-
BigInt64Array:
|
|
653
|
+
BigInt64Array: 23, BigUint64Array: 23,
|
|
512
654
|
}
|
|
513
655
|
/** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
|
|
514
656
|
* aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView). */
|
|
@@ -529,11 +671,41 @@ const _ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
|
|
|
529
671
|
export function ctorFromElemAux(aux) {
|
|
530
672
|
if (aux == null) return null
|
|
531
673
|
const isView = (aux & 8) !== 0
|
|
532
|
-
const name = _ELEM_NAMES[aux & 7]
|
|
674
|
+
const name = (aux & 16) !== 0 ? 'BigInt64Array' : _ELEM_NAMES[aux & 7]
|
|
533
675
|
if (!name) return null
|
|
534
676
|
return isView ? `new.${name}.view` : `new.${name}`
|
|
535
677
|
}
|
|
536
678
|
|
|
679
|
+
/** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
|
|
680
|
+
* different typed-array ctors — caller should drop any cached entry rather
|
|
681
|
+
* than leave a stale ctor (which would lock the wrong store width). */
|
|
682
|
+
export const MIXED_CTORS = Symbol('MIXED_CTORS')
|
|
683
|
+
|
|
684
|
+
const typedCtorElemValType = (ctor) => {
|
|
685
|
+
if (!ctor) return null
|
|
686
|
+
const isView = ctor.endsWith('.view')
|
|
687
|
+
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
|
|
688
|
+
return name === 'BigInt64Array' || name === 'BigUint64Array' ? VAL.BIGINT : VAL.NUMBER
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** A `?:`/`&&`/`||` expression — value depends on a condition, so its ctor
|
|
692
|
+
* must be derived by walking branches (handled by `ternaryCtorOfRhs`). */
|
|
693
|
+
const isCondExpr = e => Array.isArray(e) && (e[0] === '?:' || e[0] === '&&' || e[0] === '||')
|
|
694
|
+
|
|
695
|
+
/** Walk a `?:`/`&&`/`||` expression and return:
|
|
696
|
+
* - a single ctor string when every branch resolves to the same ctor,
|
|
697
|
+
* - MIXED_CTORS when branches resolve to different ctors,
|
|
698
|
+
* - null when no branch resolves (caller's behavior unchanged). */
|
|
699
|
+
export function ternaryCtorOfRhs(rhs) {
|
|
700
|
+
if (!Array.isArray(rhs)) return null
|
|
701
|
+
const op = rhs[0]
|
|
702
|
+
const lo = op === '?:' ? 2 : (op === '&&' || op === '||') ? 1 : 0
|
|
703
|
+
if (!lo) return null
|
|
704
|
+
const a = ternaryCtorOfRhs(rhs[lo]) ?? typedElemCtor(rhs[lo])
|
|
705
|
+
const b = ternaryCtorOfRhs(rhs[lo + 1]) ?? typedElemCtor(rhs[lo + 1])
|
|
706
|
+
return a && b ? (a === b ? a : MIXED_CTORS) : (a || b || null)
|
|
707
|
+
}
|
|
708
|
+
|
|
537
709
|
// === Cross-call argument inference helpers (used by narrowSignatures fixpoint) ===
|
|
538
710
|
// Each `inferArg*(expr, ...callerCtx)` resolves an argument expression to a single
|
|
539
711
|
// fact (val/schemaId/elem*/typedCtor) using caller-local observations and program
|
|
@@ -668,7 +840,7 @@ export function analyzeBody(body) {
|
|
|
668
840
|
// for any slice and can't be WeakMap-keyed. Return empty maps without caching.
|
|
669
841
|
if (body === null || typeof body !== 'object') return {
|
|
670
842
|
locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
|
|
671
|
-
arrElemValTypes: new Map(), typedElems: new Map(),
|
|
843
|
+
arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
|
|
672
844
|
}
|
|
673
845
|
const hit = _bodyFactsCache.get(body)
|
|
674
846
|
if (hit) return hit
|
|
@@ -678,6 +850,8 @@ export function analyzeBody(body) {
|
|
|
678
850
|
const arrElemSchemas = new Map()
|
|
679
851
|
const arrElemValTypes = new Map()
|
|
680
852
|
const typedElems = new Map()
|
|
853
|
+
const escapes = new Map() // name → bool: local holds allocation, true if it escapes
|
|
854
|
+
const valPoison = new Set()
|
|
681
855
|
|
|
682
856
|
const doSchemas = !!ctx.schema?.register
|
|
683
857
|
// Per-walk local schema map for chained `arr.push(name)` resolution.
|
|
@@ -725,16 +899,48 @@ export function analyzeBody(body) {
|
|
|
725
899
|
return valTypeOf(expr)
|
|
726
900
|
}
|
|
727
901
|
|
|
902
|
+
const typedPoison = new Set()
|
|
903
|
+
const trackVal = (name, vt) => {
|
|
904
|
+
if (valPoison.has(name)) return
|
|
905
|
+
const prev = valTypes.get(name)
|
|
906
|
+
if (!vt) {
|
|
907
|
+
if (prev) valPoison.add(name)
|
|
908
|
+
valTypes.delete(name)
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
if (prev && prev !== vt) {
|
|
912
|
+
valPoison.add(name)
|
|
913
|
+
valTypes.delete(name)
|
|
914
|
+
return
|
|
915
|
+
}
|
|
916
|
+
valTypes.set(name, vt)
|
|
917
|
+
}
|
|
918
|
+
const invalidateTyped = (name) => {
|
|
919
|
+
typedPoison.add(name)
|
|
920
|
+
typedElems.delete(name)
|
|
921
|
+
}
|
|
728
922
|
const trackTyped = (name, rhs) => {
|
|
923
|
+
if (typedPoison.has(name)) return
|
|
924
|
+
const setOrInvalidate = (c) => {
|
|
925
|
+
if (c === MIXED_CTORS) return invalidateTyped(name)
|
|
926
|
+
const prev = typedElems.get(name)
|
|
927
|
+
if (prev && prev !== c) invalidateTyped(name)
|
|
928
|
+
else typedElems.set(name, c)
|
|
929
|
+
}
|
|
729
930
|
const ctor = typedElemCtor(rhs)
|
|
730
|
-
if (ctor)
|
|
931
|
+
if (ctor) return setOrInvalidate(ctor)
|
|
731
932
|
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
732
933
|
const f = ctx.func.map?.get(rhs[1])
|
|
733
934
|
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
734
935
|
const c = ctorFromElemAux(f.sig.ptrAux)
|
|
735
|
-
if (c)
|
|
936
|
+
if (c) setOrInvalidate(c)
|
|
736
937
|
}
|
|
938
|
+
return
|
|
737
939
|
}
|
|
940
|
+
// Heterogeneous ternary: ctors that don't unify must invalidate so a sibling
|
|
941
|
+
// decl (jz hoists `let` to function scope) can't lock in the wrong width.
|
|
942
|
+
const tc = ternaryCtorOfRhs(rhs)
|
|
943
|
+
if (tc) setOrInvalidate(tc)
|
|
738
944
|
}
|
|
739
945
|
|
|
740
946
|
// === Per-decl observation (called for each `let`/`const` `name = rhs`) ===
|
|
@@ -745,8 +951,7 @@ export function analyzeBody(body) {
|
|
|
745
951
|
else if (locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
746
952
|
|
|
747
953
|
// val type (valTypes slice)
|
|
748
|
-
|
|
749
|
-
if (vt) valTypes.set(name, vt); else valTypes.delete(name)
|
|
954
|
+
trackVal(name, valTypeOf(rhs))
|
|
750
955
|
|
|
751
956
|
// typed-array element ctor (typedElems slice)
|
|
752
957
|
trackTyped(name, rhs)
|
|
@@ -755,14 +960,17 @@ export function analyzeBody(body) {
|
|
|
755
960
|
if (doSchemas) {
|
|
756
961
|
const sid = exprSchemaId(rhs, localSchemaMap)
|
|
757
962
|
if (sid != null) localSchemaMap.set(name, sid)
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
963
|
+
{
|
|
964
|
+
const rawElems = staticArrayElems(rhs)
|
|
965
|
+
if (rawElems) {
|
|
966
|
+
const elems = rawElems.filter(e => e != null)
|
|
967
|
+
if (elems.length && elems.length === rawElems.length) {
|
|
968
|
+
let common = exprSchemaId(elems[0], localSchemaMap)
|
|
969
|
+
for (let k = 1; k < elems.length && common != null; k++) {
|
|
970
|
+
if (exprSchemaId(elems[k], localSchemaMap) !== common) common = null
|
|
971
|
+
}
|
|
972
|
+
if (common != null) observeArrSchema(name, common)
|
|
764
973
|
}
|
|
765
|
-
if (common != null) observeArrSchema(name, common)
|
|
766
974
|
}
|
|
767
975
|
}
|
|
768
976
|
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
@@ -780,14 +988,17 @@ export function analyzeBody(body) {
|
|
|
780
988
|
}
|
|
781
989
|
|
|
782
990
|
// arr-elem val type (arrElemValTypes slice) — array-literal init + call return + alias + .map/.filter/.slice/.concat chain
|
|
783
|
-
|
|
784
|
-
const
|
|
785
|
-
if (
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
991
|
+
{
|
|
992
|
+
const rawElems = staticArrayElems(rhs)
|
|
993
|
+
if (rawElems) {
|
|
994
|
+
const elems = rawElems.filter(e => e != null)
|
|
995
|
+
if (elems.length && elems.length === rawElems.length) {
|
|
996
|
+
let common = exprElemSourceVal(elems[0])
|
|
997
|
+
for (let k = 1; k < elems.length && common != null; k++) {
|
|
998
|
+
if (exprElemSourceVal(elems[k]) !== common) common = null
|
|
999
|
+
}
|
|
1000
|
+
if (common != null) observeArrValType(name, common)
|
|
789
1001
|
}
|
|
790
|
-
if (common != null) observeArrValType(name, common)
|
|
791
1002
|
}
|
|
792
1003
|
}
|
|
793
1004
|
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
@@ -805,6 +1016,8 @@ export function analyzeBody(body) {
|
|
|
805
1016
|
if (method === 'filter' || method === 'slice' || method === 'concat') {
|
|
806
1017
|
const v = elemValOf(recvName)
|
|
807
1018
|
if (v) observeArrValType(name, v)
|
|
1019
|
+
} else if (method === 'split' && valTypeOf(recvName) === VAL.STRING) {
|
|
1020
|
+
observeArrValType(name, VAL.STRING)
|
|
808
1021
|
} else if (method === 'map') {
|
|
809
1022
|
const arrowFn = rhs[2]
|
|
810
1023
|
const recvVt = elemValOf(recvName)
|
|
@@ -830,14 +1043,47 @@ export function analyzeBody(body) {
|
|
|
830
1043
|
}
|
|
831
1044
|
}
|
|
832
1045
|
}
|
|
1046
|
+
if (Array.isArray(rhs) && rhs[0] === '()' &&
|
|
1047
|
+
Array.isArray(rhs[1]) && rhs[1][0] === '.' && rhs[1][2] === 'split' &&
|
|
1048
|
+
valTypeOf(rhs[1][1]) === VAL.STRING) {
|
|
1049
|
+
observeArrValType(name, VAL.STRING)
|
|
1050
|
+
}
|
|
833
1051
|
}
|
|
834
1052
|
|
|
835
1053
|
// arrElem invalidation rule — fires on `=` reassign of tracked name to non-array
|
|
836
1054
|
const isArrayProducingRhs = (rhs) =>
|
|
837
|
-
Array.isArray(rhs) && (rhs
|
|
1055
|
+
Array.isArray(rhs) && (staticArrayElems(rhs) != null ||
|
|
838
1056
|
(rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
839
1057
|
(rhs[1][2] === 'slice' || rhs[1][2] === 'concat')))
|
|
840
1058
|
|
|
1059
|
+
const markEscape = (name) => { if (escapes.has(name)) escapes.set(name, true) }
|
|
1060
|
+
|
|
1061
|
+
const isStaticIndex = (key) =>
|
|
1062
|
+
typeof key === 'number' || typeof key === 'string' ||
|
|
1063
|
+
(Array.isArray(key) && ((key[0] == null && Number.isInteger(key[1])) || key[0] === 'str')) ||
|
|
1064
|
+
staticPropertyKey(key) != null
|
|
1065
|
+
|
|
1066
|
+
const markEscapeValue = (expr) => {
|
|
1067
|
+
if (typeof expr === 'string') { markEscape(expr); return }
|
|
1068
|
+
if (!Array.isArray(expr)) return
|
|
1069
|
+
const op = expr[0]
|
|
1070
|
+
if (op === 'str') return
|
|
1071
|
+
if (op === ':') { markEscapeValue(expr[2]); return }
|
|
1072
|
+
if ((op === '.' || op === '?.') && typeof expr[1] === 'string' && escapes.has(expr[1])) return
|
|
1073
|
+
if (op === '[]' && typeof expr[1] === 'string' && escapes.has(expr[1])) {
|
|
1074
|
+
if (!isStaticIndex(expr[2])) markEscape(expr[1])
|
|
1075
|
+
markEscapeValue(expr[2])
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
for (let i = 1; i < expr.length; i++) markEscapeValue(expr[i])
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const markEscapeArgs = (args) => {
|
|
1082
|
+
if (args == null) return
|
|
1083
|
+
const list = Array.isArray(args) && args[0] === ',' ? args.slice(1) : [args]
|
|
1084
|
+
for (const a of list) markEscapeValue(Array.isArray(a) && a[0] === '...' ? a[1] : a)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
841
1087
|
// === Single walk ===
|
|
842
1088
|
function walk(node) {
|
|
843
1089
|
if (!Array.isArray(node)) return
|
|
@@ -858,6 +1104,10 @@ export function analyzeBody(body) {
|
|
|
858
1104
|
}
|
|
859
1105
|
const name = a[1], rhs = a[2]
|
|
860
1106
|
processDecl(name, rhs)
|
|
1107
|
+
if (Array.isArray(rhs) && (rhs[0] === '[' || rhs[0] === '{}')) {
|
|
1108
|
+
escapes.set(name, false)
|
|
1109
|
+
}
|
|
1110
|
+
markEscapeValue(rhs)
|
|
861
1111
|
// Walk rhs only — never enter the `=` node so the reassignment-invalidation
|
|
862
1112
|
// rule won't misfire on the binding's own initializer.
|
|
863
1113
|
walk(rhs)
|
|
@@ -865,6 +1115,14 @@ export function analyzeBody(body) {
|
|
|
865
1115
|
return
|
|
866
1116
|
}
|
|
867
1117
|
|
|
1118
|
+
if (op === 'return' && node[1] != null) {
|
|
1119
|
+
markEscapeValue(node[1])
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (op === '()' && node.length > 2) {
|
|
1123
|
+
markEscapeArgs(node[2])
|
|
1124
|
+
}
|
|
1125
|
+
|
|
868
1126
|
// arr.push(...) — observe both schemas and val types in one pass
|
|
869
1127
|
if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.' && node[1][2] === 'push' && typeof node[1][1] === 'string') {
|
|
870
1128
|
const arr = node[1][1]
|
|
@@ -884,13 +1142,16 @@ export function analyzeBody(body) {
|
|
|
884
1142
|
// arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
|
|
885
1143
|
if (op === '=' && typeof node[1] === 'string') {
|
|
886
1144
|
const name = node[1], rhs = node[2]
|
|
1145
|
+
walk(rhs)
|
|
1146
|
+
markEscape(name)
|
|
1147
|
+
markEscapeValue(rhs)
|
|
887
1148
|
const wt = exprType(rhs, locals)
|
|
888
1149
|
if (locals.has(name) && locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
889
|
-
|
|
890
|
-
if (vt) valTypes.set(name, vt); else valTypes.delete(name)
|
|
1150
|
+
trackVal(name, valTypeOf(rhs))
|
|
891
1151
|
trackTyped(name, rhs)
|
|
892
1152
|
if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
|
|
893
1153
|
if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
|
|
1154
|
+
return
|
|
894
1155
|
}
|
|
895
1156
|
|
|
896
1157
|
// compound-assign widening (locals slice)
|
|
@@ -903,6 +1164,28 @@ export function analyzeBody(body) {
|
|
|
903
1164
|
if (locals.has(node[1])) locals.set(node[1], 'f64')
|
|
904
1165
|
}
|
|
905
1166
|
|
|
1167
|
+
if (op === 'for' || op === 'for-in' || op === 'for-of') {
|
|
1168
|
+
if (node[1] != null) markEscapeValue(node[1])
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if (op === '[' || op === '{}') {
|
|
1172
|
+
for (let i = 1; i < node.length; i++) {
|
|
1173
|
+
const c = node[i]
|
|
1174
|
+
if (Array.isArray(c) && c[0] === ',') {
|
|
1175
|
+
for (let j = 1; j < c.length; j++) {
|
|
1176
|
+
if (Array.isArray(c[j]) && c[j][0] === '...') markEscapeValue(c[j][1])
|
|
1177
|
+
}
|
|
1178
|
+
} else if (Array.isArray(c) && c[0] === '...') {
|
|
1179
|
+
markEscapeValue(c[1])
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (op === '[]' && typeof node[1] === 'string' && escapes.has(node[1])) {
|
|
1185
|
+
const key = node[2]
|
|
1186
|
+
if (!isStaticIndex(key)) markEscape(node[1])
|
|
1187
|
+
}
|
|
1188
|
+
|
|
906
1189
|
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
907
1190
|
}
|
|
908
1191
|
|
|
@@ -910,8 +1193,13 @@ export function analyzeBody(body) {
|
|
|
910
1193
|
// resolve chains (`const a = new TypedArr(); const b = a[0]` → b: NUMBER)
|
|
911
1194
|
// and shorthand-bound `{a}` props see a's type. Restored after walk completes.
|
|
912
1195
|
const prevOverlay = ctx.func.localValTypesOverlay
|
|
1196
|
+
const prevTypedOverlay = ctx.func.localTypedElemsOverlay
|
|
913
1197
|
ctx.func.localValTypesOverlay = valTypes
|
|
914
|
-
|
|
1198
|
+
ctx.func.localTypedElemsOverlay = typedElems
|
|
1199
|
+
try { walk(body) } finally {
|
|
1200
|
+
ctx.func.localValTypesOverlay = prevOverlay
|
|
1201
|
+
ctx.func.localTypedElemsOverlay = prevTypedOverlay
|
|
1202
|
+
}
|
|
915
1203
|
|
|
916
1204
|
// Second pass: widen i32 locals compared against f64.
|
|
917
1205
|
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
|
|
@@ -928,7 +1216,36 @@ export function analyzeBody(body) {
|
|
|
928
1216
|
}
|
|
929
1217
|
widenPass(body)
|
|
930
1218
|
|
|
931
|
-
|
|
1219
|
+
// Re-resolve let-decl RHS types now that widenPass has widened. A `let x2 =
|
|
1220
|
+
// zx*zx` declared at i32 because zx was i32 at scan time must widen if zx
|
|
1221
|
+
// was later re-typed to f64. Without this, integer-init locals shadowed
|
|
1222
|
+
// by f64-arithmetic RHSs end up with `i32.trunc_sat_f64_s` truncating the
|
|
1223
|
+
// fractional value (e.g. mandelbrot escape: `x2 = zx*zx` losing 3.515 → 3).
|
|
1224
|
+
// Monotonic: only widens i32 → f64. Bound by locals count so no infinite loop.
|
|
1225
|
+
let widened = true
|
|
1226
|
+
while (widened) {
|
|
1227
|
+
widened = false
|
|
1228
|
+
const recheck = (node) => {
|
|
1229
|
+
if (!Array.isArray(node)) return
|
|
1230
|
+
const op = node[0]
|
|
1231
|
+
if (op === '=>') return
|
|
1232
|
+
if (op === 'let' || op === 'const') {
|
|
1233
|
+
for (let i = 1; i < node.length; i++) {
|
|
1234
|
+
const a = node[i]
|
|
1235
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
1236
|
+
const name = a[1], rhs = a[2]
|
|
1237
|
+
if (locals.get(name) === 'i32' && exprType(rhs, locals) === 'f64') {
|
|
1238
|
+
locals.set(name, 'f64'); widened = true
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
for (let i = 1; i < node.length; i++) recheck(node[i])
|
|
1244
|
+
}
|
|
1245
|
+
recheck(body)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes }
|
|
932
1249
|
_bodyFactsCache.set(body, result)
|
|
933
1250
|
return result
|
|
934
1251
|
}
|
|
@@ -941,6 +1258,69 @@ export function invalidateLocalsCache(body) {
|
|
|
941
1258
|
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
942
1259
|
}
|
|
943
1260
|
|
|
1261
|
+
// String-only methods: presence of `name.method(...)` is definite evidence that
|
|
1262
|
+
// `name` is VAL.STRING. Excludes ambiguous methods (length, slice, indexOf, [],
|
|
1263
|
+
// concat) that strings share with arrays/typed-arrays.
|
|
1264
|
+
const STRING_ONLY_METHODS = new Set([
|
|
1265
|
+
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
1266
|
+
'toUpperCase', 'toLowerCase', 'normalize', 'localeCompare',
|
|
1267
|
+
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
1268
|
+
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
1269
|
+
])
|
|
1270
|
+
// Array-only methods: presence rules out STRING.
|
|
1271
|
+
const ARRAY_ONLY_METHODS = new Set([
|
|
1272
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
1273
|
+
'flat', 'flatMap', 'copyWithin',
|
|
1274
|
+
])
|
|
1275
|
+
|
|
1276
|
+
/** Infer VAL.STRING for `names` from method-call evidence in `body`.
|
|
1277
|
+
* Descends into nested closures (captured names retain shape) but respects
|
|
1278
|
+
* shadowing: a param of a nested `=>` removes that name from the inference
|
|
1279
|
+
* scope inside that closure. Returns Map<name, VAL.STRING> for names that
|
|
1280
|
+
* have at least one string-only method call and no array-only conflicts. */
|
|
1281
|
+
export function inferStringParams(body, names) {
|
|
1282
|
+
if (!names || names.length === 0) return new Map()
|
|
1283
|
+
const evidence = new Map() // name → 'string' | 'conflict'
|
|
1284
|
+
function walk(node, scope) {
|
|
1285
|
+
if (!Array.isArray(node) || scope.size === 0) return
|
|
1286
|
+
const op = node[0]
|
|
1287
|
+
if (op === '=>') {
|
|
1288
|
+
const shadowed = collectParamNames([node[1]])
|
|
1289
|
+
let inner = scope
|
|
1290
|
+
for (const s of shadowed) {
|
|
1291
|
+
if (inner.has(s)) {
|
|
1292
|
+
if (inner === scope) inner = new Set(scope)
|
|
1293
|
+
inner.delete(s)
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
walk(node[2], inner)
|
|
1297
|
+
return
|
|
1298
|
+
}
|
|
1299
|
+
// `name.method` — observe positive/negative evidence
|
|
1300
|
+
if (op === '.' && typeof node[1] === 'string' && scope.has(node[1])) {
|
|
1301
|
+
const m = node[2]
|
|
1302
|
+
if (typeof m === 'string') {
|
|
1303
|
+
if (STRING_ONLY_METHODS.has(m)) {
|
|
1304
|
+
if (evidence.get(node[1]) !== 'conflict') evidence.set(node[1], 'string')
|
|
1305
|
+
} else if (ARRAY_ONLY_METHODS.has(m)) {
|
|
1306
|
+
evidence.set(node[1], 'conflict')
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// Reassignment to an unambiguously non-string RHS poisons the inference
|
|
1311
|
+
if (op === '=' && typeof node[1] === 'string' && scope.has(node[1])) {
|
|
1312
|
+
const rhs = node[2]
|
|
1313
|
+
const vt = valTypeOf(rhs)
|
|
1314
|
+
if (vt && vt !== VAL.STRING) evidence.set(node[1], 'conflict')
|
|
1315
|
+
}
|
|
1316
|
+
for (let i = 1; i < node.length; i++) walk(node[i], scope)
|
|
1317
|
+
}
|
|
1318
|
+
walk(body, new Set(names))
|
|
1319
|
+
const out = new Map()
|
|
1320
|
+
for (const [n, ev] of evidence) if (ev === 'string') out.set(n, VAL.STRING)
|
|
1321
|
+
return out
|
|
1322
|
+
}
|
|
1323
|
+
|
|
944
1324
|
/** Drop the cached analyzeBody entry. Used after E2-phase valResult narrowing
|
|
945
1325
|
* so the next walk re-evaluates `valTypeOf(call)` with up-to-date `f.valResult`
|
|
946
1326
|
* — required for the D-pass paramReps val/arrayElemSchema re-fixpoint to see
|
|
@@ -955,7 +1335,22 @@ export function invalidateValTypesCache(body) {
|
|
|
955
1335
|
* and schema resolution.
|
|
956
1336
|
*/
|
|
957
1337
|
export function analyzeValTypes(body) {
|
|
958
|
-
const
|
|
1338
|
+
const valPoison = new Set()
|
|
1339
|
+
const setVal = (name, vt) => {
|
|
1340
|
+
if (valPoison.has(name)) return
|
|
1341
|
+
const prev = ctx.func.repByLocal?.get(name)?.val
|
|
1342
|
+
if (!vt) {
|
|
1343
|
+
if (prev) valPoison.add(name)
|
|
1344
|
+
updateRep(name, { val: undefined })
|
|
1345
|
+
return
|
|
1346
|
+
}
|
|
1347
|
+
if (prev && prev !== vt) {
|
|
1348
|
+
valPoison.add(name)
|
|
1349
|
+
updateRep(name, { val: undefined })
|
|
1350
|
+
return
|
|
1351
|
+
}
|
|
1352
|
+
updateRep(name, { val: vt })
|
|
1353
|
+
}
|
|
959
1354
|
const getVal = name => ctx.func.repByLocal?.get(name)?.val
|
|
960
1355
|
// Pre-walk: observe Array<schema> facts so `const p = arr[i]` can bind a schemaId
|
|
961
1356
|
// on `p`, unlocking schema slot reads + skipping str_key dispatch on `.prop` access.
|
|
@@ -967,6 +1362,13 @@ export function analyzeValTypes(body) {
|
|
|
967
1362
|
for (const [name, vt] of facts.arrElemValTypes) {
|
|
968
1363
|
if (vt != null) updateRep(name, { arrayElemValType: vt })
|
|
969
1364
|
}
|
|
1365
|
+
// Propagate body-observed array-elem schemas to repByLocal so analyzePtrUnboxable's
|
|
1366
|
+
// `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
|
|
1367
|
+
// to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
|
|
1368
|
+
// pay an i64.reinterpret/i32.wrap on every slot access (no aliasing → CSE can't fold).
|
|
1369
|
+
for (const [name, sid] of arrElems) {
|
|
1370
|
+
if (sid != null) updateRep(name, { arrayElemSchema: sid })
|
|
1371
|
+
}
|
|
970
1372
|
// Resolve a name's array-elem-schema, preferring rep.arrayElemSchema (set from
|
|
971
1373
|
// paramReps[k].arrayElemSchema at emit start) over local body observations.
|
|
972
1374
|
const arrElemSchemaOf = (name) => {
|
|
@@ -979,20 +1381,42 @@ export function analyzeValTypes(body) {
|
|
|
979
1381
|
function trackRegex(name, rhs) {
|
|
980
1382
|
if (ctx.runtime.regex && Array.isArray(rhs) && rhs[0] === '//') ctx.runtime.regex.vars.set(name, rhs)
|
|
981
1383
|
}
|
|
1384
|
+
// Names whose decls disagree on element ctor (e.g. two `let arr = ...` decls
|
|
1385
|
+
// in different scopes — jz hoists `let` to function scope so they share a name).
|
|
1386
|
+
// Once invalidated, no later setter can re-establish a definite ctor.
|
|
1387
|
+
const typedPoison = new Set()
|
|
1388
|
+
const invalidate = (name) => {
|
|
1389
|
+
typedPoison.add(name)
|
|
1390
|
+
ctx.types.typedElem?.delete(name)
|
|
1391
|
+
}
|
|
982
1392
|
function trackTyped(name, rhs) {
|
|
983
1393
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map() // first use in this function scope
|
|
1394
|
+
if (typedPoison.has(name)) return
|
|
1395
|
+
const setOrInvalidate = (c) => {
|
|
1396
|
+
if (c === MIXED_CTORS) return invalidate(name)
|
|
1397
|
+
const prev = ctx.types.typedElem.get(name)
|
|
1398
|
+
if (prev && prev !== c) invalidate(name)
|
|
1399
|
+
else ctx.types.typedElem.set(name, c)
|
|
1400
|
+
}
|
|
984
1401
|
const ctor = typedElemCtor(rhs)
|
|
985
|
-
if (ctor)
|
|
1402
|
+
if (ctor) return setOrInvalidate(ctor)
|
|
986
1403
|
// TYPED-narrowed call result carries elem aux on f.sig.ptrAux — reverse-map it
|
|
987
1404
|
// back to a canonical ctor string so analyzePtrUnboxable's typedElemAux lookup
|
|
988
1405
|
// (compile.js) restores the same aux on the unboxed local's rep.
|
|
989
1406
|
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
990
1407
|
const f = ctx.func.map?.get(rhs[1])
|
|
991
1408
|
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
992
|
-
const
|
|
993
|
-
if (
|
|
1409
|
+
const c = ctorFromElemAux(f.sig.ptrAux)
|
|
1410
|
+
if (c) setOrInvalidate(c)
|
|
994
1411
|
}
|
|
1412
|
+
return
|
|
995
1413
|
}
|
|
1414
|
+
// Heterogeneous ternary (e.g. `n === 16 ? new Uint8Array(16) : new Uint16Array(8)`):
|
|
1415
|
+
// typedElemCtor returns null for `?:`. When branches don't unify to the same ctor,
|
|
1416
|
+
// poison the name so a later sibling-scope decl (jz hoists `let` to function scope)
|
|
1417
|
+
// can't lock in the wrong store width.
|
|
1418
|
+
const tc = ternaryCtorOfRhs(rhs)
|
|
1419
|
+
if (tc) setOrInvalidate(tc)
|
|
996
1420
|
}
|
|
997
1421
|
function walk(node) {
|
|
998
1422
|
if (!Array.isArray(node)) return
|
|
@@ -1018,7 +1442,10 @@ export function analyzeValTypes(body) {
|
|
|
1018
1442
|
const vt = valTypeOf(a[2])
|
|
1019
1443
|
setVal(a[1], vt)
|
|
1020
1444
|
if (vt === VAL.REGEX) trackRegex(a[1], a[2])
|
|
1021
|
-
|
|
1445
|
+
// VAL gate covers definite-typed RHS; `?:`/`&&`/`||` slip through valTypeOf
|
|
1446
|
+
// returning null but may still need ctor unification (or poisoning when
|
|
1447
|
+
// branches disagree, since jz hoists `let` to function scope).
|
|
1448
|
+
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(a[2])) trackTyped(a[1], a[2])
|
|
1022
1449
|
propagateTyped(a[1], a[2])
|
|
1023
1450
|
// JSON-shape propagation. When the RHS resolves to a known JSON shape
|
|
1024
1451
|
// (root: `JSON.parse(literal)`; nested: `o.meta`, `items[j]` from a known
|
|
@@ -1030,6 +1457,17 @@ export function analyzeValTypes(body) {
|
|
|
1030
1457
|
updateRep(a[1], { jsonShape: sh })
|
|
1031
1458
|
if (sh.vt === VAL.ARRAY && sh.elem?.vt) {
|
|
1032
1459
|
updateRep(a[1], { arrayElemValType: sh.elem.vt })
|
|
1460
|
+
// Array of fixed-shape OBJECTs: register elem schema so `it = items[j]`
|
|
1461
|
+
// → `it.prop` lowers to slot read via the existing arr-elem-schema path.
|
|
1462
|
+
if (sh.elem.vt === VAL.OBJECT && sh.elem.names && ctx.schema.register) {
|
|
1463
|
+
const elemSid = ctx.schema.register(sh.elem.names)
|
|
1464
|
+
updateRep(a[1], { arrayElemSchema: elemSid })
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (sh.vt === VAL.OBJECT && sh.names && ctx.schema.register) {
|
|
1468
|
+
const sid = ctx.schema.register(sh.names)
|
|
1469
|
+
updateRep(a[1], { schemaId: sid })
|
|
1470
|
+
ctx.schema.vars.set(a[1], sid)
|
|
1033
1471
|
}
|
|
1034
1472
|
}
|
|
1035
1473
|
// Propagate schemaId from a narrowed call result so subsequent valTypeOf
|
|
@@ -1055,11 +1493,13 @@ export function analyzeValTypes(body) {
|
|
|
1055
1493
|
}
|
|
1056
1494
|
}
|
|
1057
1495
|
if (op === '=' && typeof args[0] === 'string') {
|
|
1496
|
+
walk(args[1])
|
|
1058
1497
|
const vt = valTypeOf(args[1])
|
|
1059
1498
|
setVal(args[0], vt)
|
|
1060
1499
|
if (vt === VAL.REGEX) trackRegex(args[0], args[1])
|
|
1061
|
-
if (vt === VAL.TYPED || vt === VAL.BUFFER) trackTyped(args[0], args[1])
|
|
1500
|
+
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(args[1])) trackTyped(args[0], args[1])
|
|
1062
1501
|
propagateTyped(args[0], args[1])
|
|
1502
|
+
return
|
|
1063
1503
|
}
|
|
1064
1504
|
// Track property assignments for auto-boxing: x.prop = val
|
|
1065
1505
|
if (op === '=' && Array.isArray(args[0]) && args[0][0] === '.' && typeof args[0][1] === 'string') {
|
|
@@ -1163,15 +1603,19 @@ export function analyzeIntCertain(body) {
|
|
|
1163
1603
|
for (const name of defs.keys()) intCertain.set(name, true)
|
|
1164
1604
|
|
|
1165
1605
|
const isIntExpr = (expr) => {
|
|
1166
|
-
|
|
1606
|
+
// -0 is integer-valued mathematically but i32 has no signed zero — must stay f64.
|
|
1607
|
+
if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
|
|
1167
1608
|
if (typeof expr === 'boolean') return true
|
|
1168
1609
|
if (typeof expr === 'string') return intCertain.get(expr) === true
|
|
1169
1610
|
if (!Array.isArray(expr)) return false
|
|
1611
|
+
// Statically evaluable to -0 (e.g. -1 * 0, 0 / -1) — i32 would lose the sign.
|
|
1612
|
+
const sv = staticValue(expr)
|
|
1613
|
+
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
|
|
1170
1614
|
const [op, ...args] = expr
|
|
1171
1615
|
if (op == null) {
|
|
1172
1616
|
// `[, value]` / `[null, value]` literal form
|
|
1173
1617
|
const v = args[0]
|
|
1174
|
-
if (typeof v === 'number') return Number.isInteger(v)
|
|
1618
|
+
if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
|
|
1175
1619
|
if (typeof v === 'boolean') return true
|
|
1176
1620
|
return false
|
|
1177
1621
|
}
|
|
@@ -1228,11 +1672,19 @@ export function analyzeIntCertain(body) {
|
|
|
1228
1672
|
export function exprType(expr, locals) {
|
|
1229
1673
|
if (expr == null) return 'f64'
|
|
1230
1674
|
if (typeof expr === 'number')
|
|
1231
|
-
return Number.isInteger(expr) && expr >= -2147483648 && expr <= 2147483647 ? 'i32' : 'f64'
|
|
1675
|
+
return Number.isInteger(expr) && expr >= -2147483648 && expr <= 2147483647 && !Object.is(expr, -0) ? 'i32' : 'f64'
|
|
1232
1676
|
if (typeof expr === 'string') {
|
|
1233
1677
|
if (locals?.has?.(expr)) return locals.get(expr)
|
|
1234
1678
|
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
1235
1679
|
if (paramType) return paramType
|
|
1680
|
+
// Module-level numeric consts (top-level `const N = 128`) are emitted as
|
|
1681
|
+
// wasm globals with a known wasm type. Without this lookup, references to
|
|
1682
|
+
// them inside functions fall back to f64, widening counters bounded by the
|
|
1683
|
+
// const (`for (let r = 0; r < N_ROUNDS; r++)`) to f64 via the comparison
|
|
1684
|
+
// pass. Only propagate primitive numeric kinds — i64 globals are reserved
|
|
1685
|
+
// for the NaN-box carrier ABI and shouldn't influence local typing.
|
|
1686
|
+
const gt = ctx.scope?.globalTypes?.get?.(expr)
|
|
1687
|
+
if (gt === 'i32' || gt === 'f64') return gt
|
|
1236
1688
|
return 'f64'
|
|
1237
1689
|
}
|
|
1238
1690
|
if (!Array.isArray(expr)) return 'f64'
|
|
@@ -1240,8 +1692,26 @@ export function exprType(expr, locals) {
|
|
|
1240
1692
|
const [op, ...args] = expr
|
|
1241
1693
|
if (op == null) return exprType(args[0], locals) // literal [, value]
|
|
1242
1694
|
|
|
1695
|
+
// Statically evaluable to -0 (e.g. -1 * 0) — i32 would lose the sign.
|
|
1696
|
+
const sv = staticValue(expr)
|
|
1697
|
+
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return 'f64'
|
|
1698
|
+
|
|
1243
1699
|
// Always f64
|
|
1244
|
-
if (op === '/' || op === '**' || op === '[' || op === '
|
|
1700
|
+
if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
|
|
1701
|
+
// arr[i] — typed integer arrays return i32. Only Int8/Uint8/Int16/Uint16/Int32
|
|
1702
|
+
// (every value fits in signed i32). Skip Uint32: 0..2^32-1 overflows signed.
|
|
1703
|
+
// During analyzeBody the in-progress typedElems is in localTypedElemsOverlay;
|
|
1704
|
+
// post-analyze passes read from ctx.types.typedElem.
|
|
1705
|
+
if (op === '[]') {
|
|
1706
|
+
if (typeof args[0] === 'string') {
|
|
1707
|
+
const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
|
|
1708
|
+
if (ctor) {
|
|
1709
|
+
const aux = typedElemAux(ctor)
|
|
1710
|
+
if (aux != null && (aux & 7) <= 4) return 'i32'
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return 'f64'
|
|
1714
|
+
}
|
|
1245
1715
|
// `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
|
|
1246
1716
|
// both return i32). Letting it stay i32 lets analyzeLocals keep the counter
|
|
1247
1717
|
// local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
|
|
@@ -1339,7 +1809,7 @@ export function analyzePtrUnboxable(body, locals, boxed) {
|
|
|
1339
1809
|
const disqualified = new Set()
|
|
1340
1810
|
const valOf = name => ctx.func.repByLocal?.get(name)?.val
|
|
1341
1811
|
|
|
1342
|
-
const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE])
|
|
1812
|
+
const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.DATE])
|
|
1343
1813
|
|
|
1344
1814
|
// RHS must produce a fresh, non-null pointer of the declared VAL kind.
|
|
1345
1815
|
// OBJECT ← `{…}`
|
|
@@ -1376,6 +1846,7 @@ export function analyzePtrUnboxable(body, locals, boxed) {
|
|
|
1376
1846
|
if (callee.startsWith('new.')) {
|
|
1377
1847
|
if (kind === VAL.SET) return callee === 'new.Set'
|
|
1378
1848
|
if (kind === VAL.MAP) return callee === 'new.Map'
|
|
1849
|
+
if (kind === VAL.DATE) return callee === 'new.Date'
|
|
1379
1850
|
if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer' || callee === 'new.DataView'
|
|
1380
1851
|
if (kind === VAL.TYPED) return callee.endsWith('Array') && callee !== 'new.ArrayBuffer'
|
|
1381
1852
|
}
|
|
@@ -1503,6 +1974,38 @@ export function collectParamNames(raw, out = new Set()) {
|
|
|
1503
1974
|
return out
|
|
1504
1975
|
}
|
|
1505
1976
|
|
|
1977
|
+
/** Observe shared AST facts on a single node (dyn, schema, arity).
|
|
1978
|
+
* Mutates `f`: { anyDyn, dynVars:Set, hasSchemaLiterals, maxDef, maxCall, hasRest, hasSpread }.
|
|
1979
|
+
* Used by both prepare.js walk and analyze.js walkFacts. */
|
|
1980
|
+
export function observeNodeFacts(node, f) {
|
|
1981
|
+
if (!Array.isArray(node)) return
|
|
1982
|
+
const [op, ...args] = node
|
|
1983
|
+
if (op === '[]') {
|
|
1984
|
+
const [obj, idx] = args
|
|
1985
|
+
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
1986
|
+
} else if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
1987
|
+
const [, obj, idx] = args[0]
|
|
1988
|
+
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
1989
|
+
} else if (op === 'for-in') {
|
|
1990
|
+
f.anyDyn = true
|
|
1991
|
+
if (typeof args[1] === 'string') f.dynVars.add(args[1])
|
|
1992
|
+
} else if (op === '{}') {
|
|
1993
|
+
f.hasSchemaLiterals = true
|
|
1994
|
+
} else if (op === '=>') {
|
|
1995
|
+
let fixedN = 0
|
|
1996
|
+
for (const r of extractParams(args[0])) {
|
|
1997
|
+
if (classifyParam(r).kind === 'rest') f.hasRest = true
|
|
1998
|
+
else fixedN++
|
|
1999
|
+
}
|
|
2000
|
+
if (fixedN > f.maxDef) f.maxDef = fixedN
|
|
2001
|
+
} else if (op === '()') {
|
|
2002
|
+
const a = args[1]
|
|
2003
|
+
const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
2004
|
+
if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) f.hasSpread = true
|
|
2005
|
+
if (callArgs.length > f.maxCall) f.maxCall = callArgs.length
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1506
2009
|
/** Find free variables in AST: referenced in node, not in `bound`, present in `scope`. */
|
|
1507
2010
|
export function findFreeVars(node, bound, free, scope) {
|
|
1508
2011
|
if (node == null) return
|
|
@@ -1567,7 +2070,6 @@ export function findMutations(node, names, mutated) {
|
|
|
1567
2070
|
export function analyzeDynKeys(...roots) {
|
|
1568
2071
|
const dynVars = new Set()
|
|
1569
2072
|
let anyDyn = false
|
|
1570
|
-
const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
1571
2073
|
|
|
1572
2074
|
function walk(node) {
|
|
1573
2075
|
if (!Array.isArray(node)) return
|
|
@@ -1579,6 +2081,13 @@ export function analyzeDynKeys(...roots) {
|
|
|
1579
2081
|
if (typeof obj === 'string') dynVars.add(obj)
|
|
1580
2082
|
}
|
|
1581
2083
|
}
|
|
2084
|
+
if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
2085
|
+
const [, obj, idx] = args[0]
|
|
2086
|
+
if (!isLiteralStr(idx)) {
|
|
2087
|
+
anyDyn = true
|
|
2088
|
+
if (typeof obj === 'string') dynVars.add(obj)
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1582
2091
|
// Runtime for-in (compile-time unroll didn't fire) → walks via shadow
|
|
1583
2092
|
if (op === 'for-in') {
|
|
1584
2093
|
anyDyn = true
|
|
@@ -1609,34 +2118,55 @@ export function analyzeBoxedCaptures(body) {
|
|
|
1609
2118
|
const [op, ...args] = node
|
|
1610
2119
|
if (op === '=>') return
|
|
1611
2120
|
if (op === 'let' || op === 'const')
|
|
1612
|
-
|
|
1613
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') outerScope.add(a[1])
|
|
2121
|
+
collectParamNames(args, outerScope)
|
|
1614
2122
|
for (const a of args) collectDecls(a)
|
|
1615
2123
|
})(body)
|
|
1616
2124
|
if (ctx.func.current?.params) for (const p of ctx.func.current.params) outerScope.add(p.name)
|
|
1617
2125
|
if (ctx.func.locals) for (const k of ctx.func.locals.keys()) outerScope.add(k)
|
|
1618
2126
|
|
|
1619
|
-
|
|
2127
|
+
const markArrowCaptures = (node, assignTarget, seen) => {
|
|
2128
|
+
const pnode = node[1]
|
|
2129
|
+
let p = pnode
|
|
2130
|
+
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
2131
|
+
const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
2132
|
+
const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
|
|
2133
|
+
const captures = []
|
|
2134
|
+
findFreeVars(node[2], paramSet, captures, outerScope)
|
|
2135
|
+
if (captures.length === 0) return
|
|
2136
|
+
const captureSet = new Set(captures)
|
|
2137
|
+
const boxed = new Set()
|
|
2138
|
+
findMutations(body, captureSet, boxed)
|
|
2139
|
+
for (const v of captures) if (!seen.has(v)) boxed.add(v)
|
|
2140
|
+
if (assignTarget && captureSet.has(assignTarget)) boxed.add(assignTarget)
|
|
2141
|
+
for (const v of boxed) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
;(function walk(node, assignTarget, seen = new Set(ctx.func.current?.params?.map(p => p.name) || [])) {
|
|
1620
2145
|
if (!Array.isArray(node)) return
|
|
1621
2146
|
const [op, ...args] = node
|
|
1622
2147
|
if (op === '=>') {
|
|
1623
|
-
|
|
1624
|
-
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
1625
|
-
const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
1626
|
-
const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
|
|
1627
|
-
const captures = []
|
|
1628
|
-
findFreeVars(args[1], paramSet, captures, outerScope)
|
|
1629
|
-
if (captures.length === 0) return
|
|
1630
|
-
const captureSet = new Set(captures)
|
|
1631
|
-
const mutated = new Set()
|
|
1632
|
-
findMutations(body, captureSet, mutated)
|
|
1633
|
-
if (assignTarget && captureSet.has(assignTarget)) mutated.add(assignTarget)
|
|
1634
|
-
for (const v of mutated) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
|
|
2148
|
+
markArrowCaptures(node, assignTarget, seen)
|
|
1635
2149
|
return
|
|
1636
2150
|
}
|
|
2151
|
+
|
|
2152
|
+
if (op === ';' || op === '{}') {
|
|
2153
|
+
const blockSeen = new Set(seen)
|
|
2154
|
+
for (const a of args) walk(a, null, blockSeen)
|
|
2155
|
+
return
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (op === 'let' || op === 'const') {
|
|
2159
|
+
for (const decl of args) {
|
|
2160
|
+
if (Array.isArray(decl) && decl[0] === '=') walk(decl[2], typeof decl[1] === 'string' ? decl[1] : null, seen)
|
|
2161
|
+
else walk(decl, null, seen)
|
|
2162
|
+
collectParamNames([decl], seen)
|
|
2163
|
+
}
|
|
2164
|
+
return
|
|
2165
|
+
}
|
|
2166
|
+
|
|
1637
2167
|
if (op === '=' && typeof args[0] === 'string' && Array.isArray(args[1]) && args[1][0] === '=>')
|
|
1638
|
-
return walk(args[1], args[0])
|
|
1639
|
-
for (const a of args) walk(a)
|
|
2168
|
+
return walk(args[1], args[0], seen)
|
|
2169
|
+
for (const a of args) walk(a, null, seen)
|
|
1640
2170
|
})(body)
|
|
1641
2171
|
}
|
|
1642
2172
|
|
|
@@ -1750,56 +2280,24 @@ export function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
|
1750
2280
|
export function collectProgramFacts(ast) {
|
|
1751
2281
|
const paramReps = new Map()
|
|
1752
2282
|
const valueUsed = new Set()
|
|
1753
|
-
const dynVars = new Set()
|
|
1754
|
-
let anyDyn = false
|
|
1755
2283
|
const propMap = new Map()
|
|
1756
2284
|
const callSites = []
|
|
1757
2285
|
const doSchema = ast && ctx.schema.register
|
|
1758
2286
|
const doArity = !!ctx.closure.make
|
|
1759
|
-
|
|
1760
|
-
let maxDef = 0, maxCall = 0, hasRest = false, hasSpread = false
|
|
1761
|
-
const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
2287
|
+
const f = { dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false, maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false }
|
|
1762
2288
|
// Slot-type observation lives in the dedicated `observeProgramSlots` pass below;
|
|
1763
2289
|
// walkFacts only registers schemas (which is local to the AST node).
|
|
1764
2290
|
const walkFacts = (node, full, inArrow, callerFunc) => {
|
|
1765
2291
|
if (!Array.isArray(node)) return
|
|
1766
2292
|
const [op, ...args] = node
|
|
1767
|
-
// dyn
|
|
1768
|
-
|
|
1769
|
-
//
|
|
1770
|
-
if (op === '
|
|
1771
|
-
|
|
1772
|
-
if (!isLiteralStr(idx)) { anyDyn = true; if (typeof obj === 'string') dynVars.add(obj) }
|
|
1773
|
-
} else if (op === 'for-in') {
|
|
1774
|
-
if (ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
|
|
1775
|
-
anyDyn = true
|
|
1776
|
-
if (typeof args[1] === 'string') dynVars.add(args[1])
|
|
1777
|
-
}
|
|
1778
|
-
// Object literal: register schema. Slot val-type observation is deferred to a
|
|
1779
|
-
// second pass (observeSlotsIn below) so that shorthand `{x}` (expanded by prepare
|
|
1780
|
-
// to `[':', x, x]`) resolves x's val type via per-function locals, not just globals.
|
|
2293
|
+
// shared dyn/schema/arity facts (duplicated pattern synced with prepare.js)
|
|
2294
|
+
observeNodeFacts(node, f)
|
|
2295
|
+
// strict for-in check
|
|
2296
|
+
if (op === 'for-in' && ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
|
|
2297
|
+
// schema registration (analyze-specific)
|
|
1781
2298
|
if (op === '{}' && doSchema) {
|
|
1782
2299
|
const parsed = staticObjectProps(args)
|
|
1783
|
-
if (parsed)
|
|
1784
|
-
ctx.schema.register(parsed.names)
|
|
1785
|
-
hasSchemaLiterals = true
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
// closure ABI arity
|
|
1789
|
-
if (doArity) {
|
|
1790
|
-
if (op === '=>') {
|
|
1791
|
-
let fixedN = 0
|
|
1792
|
-
for (const r of extractParams(args[0])) {
|
|
1793
|
-
if (classifyParam(r).kind === 'rest') hasRest = true
|
|
1794
|
-
else fixedN++
|
|
1795
|
-
}
|
|
1796
|
-
if (fixedN > maxDef) maxDef = fixedN
|
|
1797
|
-
} else if (op === '()') {
|
|
1798
|
-
const a = args[1]
|
|
1799
|
-
const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
1800
|
-
if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) hasSpread = true
|
|
1801
|
-
if (callArgs.length > maxCall) maxCall = callArgs.length
|
|
1802
|
-
}
|
|
2300
|
+
if (parsed) ctx.schema.register(parsed.names)
|
|
1803
2301
|
}
|
|
1804
2302
|
// Crossing into a closure body: from now on, no call-site collection (matches the
|
|
1805
2303
|
// pre-fusion scanCalls bailing at '=>'). Still walks children for arity/dyn.
|
|
@@ -1817,26 +2315,22 @@ export function collectProgramFacts(ast) {
|
|
|
1817
2315
|
}
|
|
1818
2316
|
}
|
|
1819
2317
|
// first-class function-value + static-call-site scan
|
|
1820
|
-
if (op === '()' &&
|
|
2318
|
+
if (op === '()' && isFuncRef(args[0], ctx.func.names)) {
|
|
1821
2319
|
if (!inArrow) {
|
|
1822
|
-
// Record call site for the type/schema fixpoint. Filtering by
|
|
1823
|
-
// exported/raw/valueUsed happens later (valueUsed isn't fully populated yet).
|
|
1824
|
-
// `node` is the call AST node itself; specializeBimorphicTyped mutates
|
|
1825
|
-
// node[1] (the callee name) to point at a per-ctor clone.
|
|
1826
2320
|
const a = args[1]
|
|
1827
2321
|
const argList = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
1828
2322
|
callSites.push({ callee: args[0], argList, callerFunc, node })
|
|
1829
2323
|
}
|
|
1830
2324
|
for (let i = 1; i < args.length; i++) {
|
|
1831
2325
|
const a = args[i]
|
|
1832
|
-
if (
|
|
2326
|
+
if (isFuncRef(a, ctx.func.names)) valueUsed.add(a)
|
|
1833
2327
|
else walkFacts(a, true, inArrow, callerFunc)
|
|
1834
2328
|
}
|
|
1835
2329
|
return
|
|
1836
2330
|
}
|
|
1837
|
-
if ((op === '.' || op === '?.') &&
|
|
2331
|
+
if ((op === '.' || op === '?.') && isFuncRef(args[0], ctx.func.names)) return
|
|
1838
2332
|
for (const a of args) {
|
|
1839
|
-
if (
|
|
2333
|
+
if (isFuncRef(a, ctx.func.names)) valueUsed.add(a)
|
|
1840
2334
|
else walkFacts(a, true, inArrow, callerFunc)
|
|
1841
2335
|
}
|
|
1842
2336
|
} else {
|
|
@@ -1848,16 +2342,16 @@ export function collectProgramFacts(ast) {
|
|
|
1848
2342
|
const initFacts = ctx.module.initFacts
|
|
1849
2343
|
if (initFacts) {
|
|
1850
2344
|
if (initFacts.anyDyn) {
|
|
1851
|
-
anyDyn = true
|
|
1852
|
-
for (const v of initFacts.dynVars) dynVars.add(v)
|
|
2345
|
+
f.anyDyn = true
|
|
2346
|
+
for (const v of initFacts.dynVars) f.dynVars.add(v)
|
|
1853
2347
|
}
|
|
1854
2348
|
if (doArity) {
|
|
1855
|
-
if (initFacts.maxDef > maxDef) maxDef = initFacts.maxDef
|
|
1856
|
-
if (initFacts.maxCall > maxCall) maxCall = initFacts.maxCall
|
|
1857
|
-
if (initFacts.hasRest) hasRest = true
|
|
1858
|
-
if (initFacts.hasSpread) hasSpread = true
|
|
2349
|
+
if (initFacts.maxDef > f.maxDef) f.maxDef = initFacts.maxDef
|
|
2350
|
+
if (initFacts.maxCall > f.maxCall) f.maxCall = initFacts.maxCall
|
|
2351
|
+
if (initFacts.hasRest) f.hasRest = true
|
|
2352
|
+
if (initFacts.hasSpread) f.hasSpread = true
|
|
1859
2353
|
}
|
|
1860
|
-
if (doSchema && initFacts.hasSchemaLiterals) hasSchemaLiterals = true
|
|
2354
|
+
if (doSchema && initFacts.hasSchemaLiterals) f.hasSchemaLiterals = true
|
|
1861
2355
|
}
|
|
1862
2356
|
|
|
1863
2357
|
// Slot-type observation pass: walk every `{}` literal with the right scope's
|
|
@@ -1866,12 +2360,12 @@ export function collectProgramFacts(ast) {
|
|
|
1866
2360
|
// through valTypeOf → lookupValType. Skips into closures — they're observed via
|
|
1867
2361
|
// their own func.list entry. The overlay is the per-function analyzeBody.valTypes
|
|
1868
2362
|
// map (already populated with the same overlay-aware walk).
|
|
1869
|
-
if (doSchema && hasSchemaLiterals) observeProgramSlots(ast)
|
|
2363
|
+
if (doSchema && f.hasSchemaLiterals) observeProgramSlots(ast)
|
|
1870
2364
|
|
|
1871
2365
|
return {
|
|
1872
|
-
dynVars, anyDyn, propMap, valueUsed, callSites,
|
|
1873
|
-
maxDef, maxCall, hasRest, hasSpread,
|
|
1874
|
-
paramReps, hasSchemaLiterals,
|
|
2366
|
+
dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
2367
|
+
maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
|
|
2368
|
+
paramReps, hasSchemaLiterals: f.hasSchemaLiterals,
|
|
1875
2369
|
}
|
|
1876
2370
|
}
|
|
1877
2371
|
|