@llui/vite-plugin 0.0.41 → 0.1.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 +2 -2
- package/dist/collect-deps.d.ts +16 -2
- package/dist/collect-deps.d.ts.map +1 -1
- package/dist/collect-deps.js +99 -18
- package/dist/collect-deps.js.map +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +262 -64
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
package/dist/transform.js
CHANGED
|
@@ -227,7 +227,9 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
|
|
|
227
227
|
// function is generated per-component, so bit assignments in other files
|
|
228
228
|
// won't match. Files without component() get FULL_MASK on all bindings.
|
|
229
229
|
const fileHasComponent = hasComponentDef(sourceFile, lluiImport);
|
|
230
|
-
const
|
|
230
|
+
const { lo: fieldBits, hi: fieldBitsHi } = fileHasComponent
|
|
231
|
+
? collectDeps(source)
|
|
232
|
+
: { lo: new Map(), hi: new Map() };
|
|
231
233
|
if (verbose && fileHasComponent) {
|
|
232
234
|
const pairs = [...fieldBits.entries()]
|
|
233
235
|
.map(([path, bit]) => `${path}=${bit === -1 ? 'FULL' : bit}`)
|
|
@@ -301,7 +303,7 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
|
|
|
301
303
|
}
|
|
302
304
|
// Pass 1: Transform element helper calls to elSplit or elTemplate
|
|
303
305
|
if (ts.isCallExpression(node)) {
|
|
304
|
-
const transformed = tryTransformElementCall(node, importedHelpers, fieldBits, compiledHelpers, bailedHelpers, f);
|
|
306
|
+
const transformed = tryTransformElementCall(node, importedHelpers, fieldBits, compiledHelpers, bailedHelpers, f, fieldBitsHi);
|
|
305
307
|
if (transformed) {
|
|
306
308
|
if (ts.isIdentifier(transformed.expression)) {
|
|
307
309
|
if (transformed.expression.text === 'elTemplate')
|
|
@@ -332,7 +334,7 @@ export function transformLlui(source, _filename, devMode = false, emitAgentMetad
|
|
|
332
334
|
}
|
|
333
335
|
// Pass 2: Inject __dirty, __update, and __msgSchema into component() calls
|
|
334
336
|
if (ts.isCallExpression(node) && isComponentCall(node, lluiImport)) {
|
|
335
|
-
let result = tryInjectDirty(node, fieldBits, f);
|
|
337
|
+
let result = tryInjectDirty(node, fieldBits, f, fieldBitsHi);
|
|
336
338
|
if (result)
|
|
337
339
|
usesApplyBinding = true;
|
|
338
340
|
// Extract schema data once — used both for devMode injections and the
|
|
@@ -767,7 +769,7 @@ function emitStaticProp(staticProps, f, kind, resolvedKey, value) {
|
|
|
767
769
|
}
|
|
768
770
|
}
|
|
769
771
|
// ── Pass 1: Element → elSplit ────────────────────────────────────
|
|
770
|
-
function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f) {
|
|
772
|
+
function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f, fieldBitsHi = new Map()) {
|
|
771
773
|
if (!ts.isIdentifier(node.expression))
|
|
772
774
|
return null;
|
|
773
775
|
const localName = node.expression.text;
|
|
@@ -890,7 +892,7 @@ function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f)
|
|
|
890
892
|
{
|
|
891
893
|
const kind = classifyKind(key);
|
|
892
894
|
const resolvedKey = resolveKey(key, kind);
|
|
893
|
-
const { mask, readsState } = computeAccessorMask(classified.accessor, fieldBits);
|
|
895
|
+
const { mask, maskHi, readsState } = computeAccessorMask(classified.accessor, fieldBits, undefined, fieldBitsHi);
|
|
894
896
|
// Zero-mask constant folding only applies to inline arrows whose body
|
|
895
897
|
// we can safely call at compile time. For identifier-bound forms
|
|
896
898
|
// (`accessor !== value`) we skip the fold — calling the identifier's
|
|
@@ -899,16 +901,25 @@ function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f)
|
|
|
899
901
|
if (classified.kind === 'arrow' &&
|
|
900
902
|
classified.accessor === value &&
|
|
901
903
|
mask === 0 &&
|
|
904
|
+
maskHi === 0 &&
|
|
902
905
|
!readsState) {
|
|
903
906
|
emitStaticProp(staticProps, f, kind, resolvedKey, f.createCallExpression(classified.accessor, undefined, []));
|
|
904
907
|
continue;
|
|
905
908
|
}
|
|
906
|
-
|
|
907
|
-
|
|
909
|
+
const effectiveMask = mask === 0 && maskHi === 0 && readsState ? 0xffffffff | 0 : mask;
|
|
910
|
+
// Emit a 5-tuple only when the accessor reads a high-word
|
|
911
|
+
// prefix (positions 31..61). For the common ≤31-prefix case
|
|
912
|
+
// the emit stays byte-identical to the pre-multi-word baseline,
|
|
913
|
+
// and stale runtime bundles ignore the 5th slot.
|
|
914
|
+
const tupleEls = [
|
|
915
|
+
createMaskLiteral(f, effectiveMask),
|
|
908
916
|
f.createStringLiteral(kind),
|
|
909
917
|
f.createStringLiteral(resolvedKey),
|
|
910
918
|
classified.valueForBinding,
|
|
911
|
-
]
|
|
919
|
+
];
|
|
920
|
+
if (maskHi !== 0)
|
|
921
|
+
tupleEls.push(createMaskLiteral(f, maskHi));
|
|
922
|
+
bindings.push(f.createArrayLiteralExpression(tupleEls));
|
|
912
923
|
}
|
|
913
924
|
}
|
|
914
925
|
}
|
|
@@ -924,7 +935,7 @@ function tryTransformElementCall(node, helpers, fieldBits, compiled, bailed, f)
|
|
|
924
935
|
compiled.add(localName);
|
|
925
936
|
// Subtree collapse: if children contain nested element helpers,
|
|
926
937
|
// collapse the entire tree into a single elTemplate() call
|
|
927
|
-
const analyzed = analyzeSubtree(node, helpers, fieldBits, []);
|
|
938
|
+
const analyzed = analyzeSubtree(node, helpers, fieldBits, [], fieldBitsHi);
|
|
928
939
|
if (analyzed && hasNestedElements(analyzed)) {
|
|
929
940
|
// Mark all descendant helpers as compiled for import cleanup
|
|
930
941
|
collectUsedHelpers(analyzed, compiled);
|
|
@@ -1072,8 +1083,8 @@ function tryInjectStructuralMask(node, viewHelperNames, viewHelperAliases, field
|
|
|
1072
1083
|
...node.arguments.slice(1),
|
|
1073
1084
|
]);
|
|
1074
1085
|
}
|
|
1075
|
-
function tryInjectDirty(node, fieldBits, f) {
|
|
1076
|
-
if (fieldBits.size === 0)
|
|
1086
|
+
function tryInjectDirty(node, fieldBits, f, fieldBitsHi = new Map()) {
|
|
1087
|
+
if (fieldBits.size === 0 && fieldBitsHi.size === 0)
|
|
1077
1088
|
return null;
|
|
1078
1089
|
const configArg = node.arguments[0];
|
|
1079
1090
|
if (!configArg || !ts.isObjectLiteralExpression(configArg))
|
|
@@ -1095,6 +1106,16 @@ function tryInjectDirty(node, fieldBits, f) {
|
|
|
1095
1106
|
const topField = path.split('.')[0];
|
|
1096
1107
|
topLevelBits.set(topField, (topLevelBits.get(topField) ?? 0) | bit);
|
|
1097
1108
|
}
|
|
1109
|
+
// Parallel high-word map: top-level fields whose paths fall at bit
|
|
1110
|
+
// positions 31..61 contribute here instead of (or in addition to)
|
|
1111
|
+
// `topLevelBits`. A field with sub-paths split across both words —
|
|
1112
|
+
// e.g., `user.name` at position 30 and `user.email` at position 31 —
|
|
1113
|
+
// ends up in both maps under the same top-level key.
|
|
1114
|
+
const topLevelBitsHi = new Map();
|
|
1115
|
+
for (const [path, bit] of fieldBitsHi) {
|
|
1116
|
+
const topField = path.split('.')[0];
|
|
1117
|
+
topLevelBitsHi.set(topField, (topLevelBitsHi.get(topField) ?? 0) | bit);
|
|
1118
|
+
}
|
|
1098
1119
|
const comparisons = [];
|
|
1099
1120
|
for (const [field, bit] of topLevelBits) {
|
|
1100
1121
|
const oAccess = buildAccess(f, 'o', [field]);
|
|
@@ -1130,23 +1151,65 @@ function tryInjectDirty(node, fieldBits, f) {
|
|
|
1130
1151
|
const structuralMask = computeStructuralMask(configArg, fieldBits);
|
|
1131
1152
|
const phase2Mask = computePhase2Mask(configArg, fieldBits);
|
|
1132
1153
|
const updateBody = buildUpdateBody(f, structuralMask, phase2Mask);
|
|
1154
|
+
// `dHi` is the high-word dirty mask, appended as the trailing
|
|
1155
|
+
// positional arg so stale 5-param compiled bundles continue to gate
|
|
1156
|
+
// correctly: the runtime calls `__update(s, d, b, bl, p, dHi)`,
|
|
1157
|
+
// old bundles' 5-param arrow ignores the extra arg (for ≤31-prefix
|
|
1158
|
+
// components dHi is always 0 anyway). New bundles use it for
|
|
1159
|
+
// precise two-word Phase 1 gating.
|
|
1133
1160
|
const updateFn = f.createArrowFunction(undefined, undefined, [
|
|
1134
1161
|
f.createParameterDeclaration(undefined, undefined, 's'),
|
|
1135
1162
|
f.createParameterDeclaration(undefined, undefined, 'd'),
|
|
1136
1163
|
f.createParameterDeclaration(undefined, undefined, 'b'),
|
|
1137
1164
|
f.createParameterDeclaration(undefined, undefined, 'bl'),
|
|
1138
1165
|
f.createParameterDeclaration(undefined, undefined, 'p'),
|
|
1166
|
+
f.createParameterDeclaration(undefined, undefined, 'dHi', undefined, undefined, f.createNumericLiteral(0)),
|
|
1139
1167
|
], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), updateBody);
|
|
1140
1168
|
const updateProp = f.createPropertyAssignment('__update', updateFn);
|
|
1141
1169
|
// __handlers: per-message-type specialized update functions.
|
|
1142
1170
|
// Analyzes the update() switch/case and generates direct handlers
|
|
1143
1171
|
// that bypass the generic Phase 1/2 pipeline for single-message updates.
|
|
1144
|
-
const handlersProp = tryBuildHandlers(configArg, topLevelBits, structuralMask, f);
|
|
1145
|
-
//
|
|
1146
|
-
// mask
|
|
1147
|
-
|
|
1172
|
+
const handlersProp = tryBuildHandlers(configArg, topLevelBits, topLevelBitsHi, structuralMask, f);
|
|
1173
|
+
// Both `__update` and `__handlers` carry two-word gates now —
|
|
1174
|
+
// `__update`'s Phase 1 block loop uses `(mask & d) | (maskHi & dHi)`
|
|
1175
|
+
// with `dHi` as the trailing parameter (defaults to 0 for backward
|
|
1176
|
+
// compat with old 5-arg call sites), and `__handlers` passes
|
|
1177
|
+
// `caseDirtyHi` to `_handleMsg` which gates blocks against both
|
|
1178
|
+
// words. Safe to emit for any prefix count.
|
|
1179
|
+
// `__dirty` emission was removed in 2026-05: `__prefixes` is strictly
|
|
1180
|
+
// more precise (per-prefix rather than per-top-level-field), supports
|
|
1181
|
+
// 62 paths via two-word masks, and the runtime throws if it sees a
|
|
1182
|
+
// hand-authored `__dirty`. `legendProp` (`__maskLegend`) is still
|
|
1183
|
+
// emitted for the agent layer's introspection — it surfaces the
|
|
1184
|
+
// top-level-field-to-bit mapping for `whyDidUpdate` / dispatch
|
|
1185
|
+
// tracing without depending on `__dirty` at runtime.
|
|
1186
|
+
void dirtyFn;
|
|
1187
|
+
void dirtyProp;
|
|
1188
|
+
const extraProps = [legendProp, updateProp];
|
|
1148
1189
|
if (handlersProp)
|
|
1149
1190
|
extraProps.push(handlersProp);
|
|
1191
|
+
// __prefixes: opt-in path-keyed reactivity (see
|
|
1192
|
+
// docs/proposals/unified-composition-model.md). One closure per
|
|
1193
|
+
// distinct path that an accessor reads, hoisted into a stable array;
|
|
1194
|
+
// the array position IS the bit position used by the path's bindings.
|
|
1195
|
+
// The runtime prefers __prefixes when present and computes the
|
|
1196
|
+
// combinedDirty mask by reference-comparing `prefix(prev)` vs
|
|
1197
|
+
// `prefix(next)` for each entry — strictly more precise than the
|
|
1198
|
+
// top-level-conflated __dirty (which always co-fires bindings sharing
|
|
1199
|
+
// a top-level field even when only one sub-path actually mutated).
|
|
1200
|
+
//
|
|
1201
|
+
// Emit `__prefixes` whenever any reactive paths are present. For
|
|
1202
|
+
// components with ≤31 paths, the runtime's
|
|
1203
|
+
// `computeDirtyFromPrefixes` returns a single `number`; for
|
|
1204
|
+
// 32..61-path components it returns a `[lo, hi]` tuple that the
|
|
1205
|
+
// runtime fans out into `combinedDirty` + `combinedDirtyHi`. The
|
|
1206
|
+
// binding-level mask gating is still single-word at the compiler
|
|
1207
|
+
// emit layer today, so high-position bindings still re-evaluate
|
|
1208
|
+
// every cycle — but the dirty computation itself is now precise,
|
|
1209
|
+
// which lets memo()'d aggregates short-circuit correctly.
|
|
1210
|
+
const prefixesProp = buildPrefixesProp(fieldBits, fieldBitsHi, f);
|
|
1211
|
+
if (prefixesProp)
|
|
1212
|
+
extraProps.push(prefixesProp);
|
|
1150
1213
|
const newConfig = f.createObjectLiteralExpression([...configArg.properties, ...extraProps], true);
|
|
1151
1214
|
return f.createCallExpression(node.expression, node.typeArguments, [
|
|
1152
1215
|
newConfig,
|
|
@@ -1164,8 +1227,8 @@ function tryInjectDirty(node, fieldBits, f) {
|
|
|
1164
1227
|
* Conservative: only generates handlers for cases where the field
|
|
1165
1228
|
* modifications are statically determinable. Complex cases are skipped.
|
|
1166
1229
|
*/
|
|
1167
|
-
function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
|
|
1168
|
-
if (topLevelBits.size === 0)
|
|
1230
|
+
function tryBuildHandlers(configArg, topLevelBits, topLevelBitsHi, structuralMask, f) {
|
|
1231
|
+
if (topLevelBits.size === 0 && topLevelBitsHi.size === 0)
|
|
1169
1232
|
return null;
|
|
1170
1233
|
// Find the update function in the component config
|
|
1171
1234
|
let updateFn = null;
|
|
@@ -1241,7 +1304,7 @@ function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
|
|
|
1241
1304
|
let bailOut = false;
|
|
1242
1305
|
for (const returnExpr of returnExprs) {
|
|
1243
1306
|
const stateExpr = returnExpr.elements[0];
|
|
1244
|
-
const fields = analyzeModifiedFields(stateExpr, stateName, topLevelBits);
|
|
1307
|
+
const fields = analyzeModifiedFields(stateExpr, stateName, topLevelBits, topLevelBitsHi);
|
|
1245
1308
|
if (!fields) {
|
|
1246
1309
|
bailOut = true;
|
|
1247
1310
|
break;
|
|
@@ -1252,14 +1315,29 @@ function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
|
|
|
1252
1315
|
if (bailOut)
|
|
1253
1316
|
continue; // at least one return path was too complex
|
|
1254
1317
|
const modifiedFields = Array.from(allModified);
|
|
1255
|
-
// Compute the dirty mask for this case
|
|
1318
|
+
// Compute the dirty mask for this case across both words. Fields
|
|
1319
|
+
// tracked in `topLevelBitsHi` contribute to `caseDirtyHi`; fields
|
|
1320
|
+
// tracked nowhere (`undefined` lookup in both) fall back to
|
|
1321
|
+
// FULL_MASK in the low word — same conservative behavior as
|
|
1322
|
+
// before, just preserved per-word now.
|
|
1256
1323
|
let caseDirty = 0;
|
|
1324
|
+
let caseDirtyHi = 0;
|
|
1257
1325
|
for (const field of modifiedFields) {
|
|
1258
|
-
|
|
1326
|
+
const lo = topLevelBits.get(field);
|
|
1327
|
+
const hi = topLevelBitsHi.get(field);
|
|
1328
|
+
if (lo === undefined && hi === undefined) {
|
|
1329
|
+
caseDirty |= 0xffffffff | 0;
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
if (lo !== undefined)
|
|
1333
|
+
caseDirty |= lo;
|
|
1334
|
+
if (hi !== undefined)
|
|
1335
|
+
caseDirtyHi |= hi;
|
|
1336
|
+
}
|
|
1259
1337
|
}
|
|
1260
1338
|
// Detect array operation pattern for structural block optimization
|
|
1261
1339
|
const arrayOp = detectArrayOp(clause, stateName, modifiedFields, structuralMask, caseDirty);
|
|
1262
|
-
const handler = buildCaseHandler(f, caseDirty, arrayOp);
|
|
1340
|
+
const handler = buildCaseHandler(f, caseDirty, caseDirtyHi, arrayOp);
|
|
1263
1341
|
handlers.push(f.createPropertyAssignment(f.createStringLiteral(msgType), handler));
|
|
1264
1342
|
}
|
|
1265
1343
|
if (handlers.length === 0)
|
|
@@ -1445,7 +1523,12 @@ function hasSliceAssignment(clause, stateName, varName) {
|
|
|
1445
1523
|
* Analyze which top-level state fields are modified in a return expression.
|
|
1446
1524
|
* Returns the set of field names, or null if too complex to determine.
|
|
1447
1525
|
*/
|
|
1448
|
-
function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
|
|
1526
|
+
function analyzeModifiedFields(stateExpr, stateName, topLevelBits, topLevelBitsHi = new Map()) {
|
|
1527
|
+
// Recognize fields tracked in EITHER the low-word or high-word map.
|
|
1528
|
+
// 32..61-prefix components have their overflow paths in
|
|
1529
|
+
// `topLevelBitsHi`; the case handler's `caseDirty` / `caseDirtyHi`
|
|
1530
|
+
// logic depends on us recognizing those fields here.
|
|
1531
|
+
const isTracked = (name) => topLevelBits.has(name) || topLevelBitsHi.has(name);
|
|
1449
1532
|
// Pattern: { ...state, field1: ..., field2: ... } or { field1: ..., field2: ... }
|
|
1450
1533
|
if (ts.isObjectLiteralExpression(stateExpr)) {
|
|
1451
1534
|
const modified = [];
|
|
@@ -1464,14 +1547,14 @@ function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
|
|
|
1464
1547
|
}
|
|
1465
1548
|
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
1466
1549
|
const fieldName = prop.name.text;
|
|
1467
|
-
if (
|
|
1550
|
+
if (isTracked(fieldName)) {
|
|
1468
1551
|
modified.push(fieldName);
|
|
1469
1552
|
}
|
|
1470
1553
|
}
|
|
1471
1554
|
// Handle shorthand: { ...state, rows } where rows is a local variable
|
|
1472
1555
|
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
1473
1556
|
const fieldName = prop.name.text;
|
|
1474
|
-
if (
|
|
1557
|
+
if (isTracked(fieldName)) {
|
|
1475
1558
|
modified.push(fieldName);
|
|
1476
1559
|
}
|
|
1477
1560
|
}
|
|
@@ -1504,7 +1587,7 @@ function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
|
|
|
1504
1587
|
* Build a handler that delegates to __handleMsg(inst, msg, dirty, method).
|
|
1505
1588
|
* method: 0=reconcile, 1=reconcileItems, 2=reconcileClear, 3=reconcileRemove, -1=skip blocks
|
|
1506
1589
|
*/
|
|
1507
|
-
function buildCaseHandler(f, caseDirty, arrayOp) {
|
|
1590
|
+
function buildCaseHandler(f, caseDirty, caseDirtyHi, arrayOp) {
|
|
1508
1591
|
const method = typeof arrayOp === 'object' && arrayOp.type === 'strided'
|
|
1509
1592
|
? 10 + arrayOp.stride // reconcileChanged with stride
|
|
1510
1593
|
: arrayOp === 'none'
|
|
@@ -1516,18 +1599,24 @@ function buildCaseHandler(f, caseDirty, arrayOp) {
|
|
|
1516
1599
|
: arrayOp === 'remove'
|
|
1517
1600
|
? 3
|
|
1518
1601
|
: 0; // general
|
|
1519
|
-
// (inst, msg) => __handleMsg(inst, msg, dirty, method)
|
|
1520
|
-
|
|
1521
|
-
f.createParameterDeclaration(undefined, undefined, 'inst'),
|
|
1522
|
-
f.createParameterDeclaration(undefined, undefined, 'msg'),
|
|
1523
|
-
], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createCallExpression(f.createIdentifier('__handleMsg'), undefined, [
|
|
1602
|
+
// (inst, msg) => __handleMsg(inst, msg, dirty, method, [dirtyHi])
|
|
1603
|
+
const args = [
|
|
1524
1604
|
f.createIdentifier('inst'),
|
|
1525
1605
|
f.createIdentifier('msg'),
|
|
1526
1606
|
createMaskLiteral(f, caseDirty),
|
|
1527
1607
|
method >= 0
|
|
1528
1608
|
? f.createNumericLiteral(method)
|
|
1529
1609
|
: f.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f.createNumericLiteral(1)),
|
|
1530
|
-
]
|
|
1610
|
+
];
|
|
1611
|
+
// Emit the 5th positional arg only when the case touches a high-word
|
|
1612
|
+
// field. Stale runtime bundles' _handleMsg signatures ignored that
|
|
1613
|
+
// slot anyway; new ones (defaulted to 0) make it explicit when needed.
|
|
1614
|
+
if (caseDirtyHi !== 0)
|
|
1615
|
+
args.push(createMaskLiteral(f, caseDirtyHi));
|
|
1616
|
+
return f.createArrowFunction(undefined, undefined, [
|
|
1617
|
+
f.createParameterDeclaration(undefined, undefined, 'inst'),
|
|
1618
|
+
f.createParameterDeclaration(undefined, undefined, 'msg'),
|
|
1619
|
+
], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createCallExpression(f.createIdentifier('__handleMsg'), undefined, args));
|
|
1531
1620
|
}
|
|
1532
1621
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1533
1622
|
function _deadCode_legacyCaseHandler(f, caseDirty, arrayOp) {
|
|
@@ -2060,9 +2149,15 @@ function buildUpdateBody(f, structuralMask, _phase2Mask) {
|
|
|
2060
2149
|
const phase1Stmts = [];
|
|
2061
2150
|
// for (let i = 0; i < bl.length; i++) {
|
|
2062
2151
|
// const bk = bl[i];
|
|
2063
|
-
// if (!bk || (bk.mask & d)
|
|
2064
|
-
// bk.reconcile(s, d)
|
|
2152
|
+
// if (!bk || !((bk.mask & d) | (bk.maskHi & dHi))) continue;
|
|
2153
|
+
// bk.reconcile(s, d, dHi)
|
|
2065
2154
|
// }
|
|
2155
|
+
// Two-word gate matches the runtime's `genericUpdate`: bits 0..30
|
|
2156
|
+
// in `d`, bits 31..61 in `dHi`. For ≤31-prefix components both
|
|
2157
|
+
// `bk.maskHi` and `dHi` are 0, so V8's inline cache collapses the
|
|
2158
|
+
// OR back to the single-word check. >31-prefix components use the
|
|
2159
|
+
// high word for precise gating.
|
|
2160
|
+
//
|
|
2066
2161
|
// Re-read bl.length each iteration and null-check bk — a branch's
|
|
2067
2162
|
// reconcile may dispose the old scope, whose disposers splice child
|
|
2068
2163
|
// structural blocks out of this shared array mid-iteration.
|
|
@@ -2070,8 +2165,8 @@ function buildUpdateBody(f, structuralMask, _phase2Mask) {
|
|
|
2070
2165
|
f.createVariableStatement(undefined, f.createVariableDeclarationList([
|
|
2071
2166
|
f.createVariableDeclaration('bk', undefined, undefined, f.createElementAccessExpression(f.createIdentifier('bl'), f.createIdentifier('i'))),
|
|
2072
2167
|
], ts.NodeFlags.Const)),
|
|
2073
|
-
f.createIfStatement(f.createBinaryExpression(f.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, f.createIdentifier('bk')), ts.SyntaxKind.BarBarToken, f.createBinaryExpression(f.createParenthesizedExpression(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'mask'), ts.SyntaxKind.AmpersandToken, f.createIdentifier('d'))), ts.SyntaxKind.
|
|
2074
|
-
f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'reconcile'), undefined, [f.createIdentifier('s'), f.createIdentifier('d')])),
|
|
2168
|
+
f.createIfStatement(f.createBinaryExpression(f.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, f.createIdentifier('bk')), ts.SyntaxKind.BarBarToken, f.createPrefixUnaryExpression(ts.SyntaxKind.ExclamationToken, f.createParenthesizedExpression(f.createBinaryExpression(f.createParenthesizedExpression(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'mask'), ts.SyntaxKind.AmpersandToken, f.createIdentifier('d'))), ts.SyntaxKind.BarToken, f.createParenthesizedExpression(f.createBinaryExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'maskHi'), ts.SyntaxKind.AmpersandToken, f.createIdentifier('dHi'))))))), f.createContinueStatement()),
|
|
2169
|
+
f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'reconcile'), undefined, [f.createIdentifier('s'), f.createIdentifier('d'), f.createIdentifier('dHi')])),
|
|
2075
2170
|
], true));
|
|
2076
2171
|
phase1Stmts.push(blockLoop);
|
|
2077
2172
|
// Compaction: if (b.length > p || (p > 0 && b[0].dead)) { ... }
|
|
@@ -2097,15 +2192,50 @@ function buildUpdateBody(f, structuralMask, _phase2Mask) {
|
|
|
2097
2192
|
stmts.push(...phase1Stmts);
|
|
2098
2193
|
}
|
|
2099
2194
|
}
|
|
2100
|
-
// Phase 2: delegate to shared runtime — __runPhase2(s, d, b, p)
|
|
2195
|
+
// Phase 2: delegate to shared runtime — __runPhase2(s, d, dHi, b, p)
|
|
2101
2196
|
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__runPhase2'), undefined, [
|
|
2102
2197
|
f.createIdentifier('s'),
|
|
2103
2198
|
f.createIdentifier('d'),
|
|
2199
|
+
f.createIdentifier('dHi'),
|
|
2104
2200
|
f.createIdentifier('b'),
|
|
2105
2201
|
f.createIdentifier('p'),
|
|
2106
2202
|
])));
|
|
2107
2203
|
return f.createBlock(stmts, true);
|
|
2108
2204
|
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Build the `__prefixes` property assignment from path → bit maps.
|
|
2207
|
+
*
|
|
2208
|
+
* Emits one arrow `(s) => s.<path>` per distinct path. Array index =
|
|
2209
|
+
* the path's bit position: positions 0..30 come from `fieldBits` (low
|
|
2210
|
+
* word), positions 31..61 from `fieldBitsHi` (high word). The runtime
|
|
2211
|
+
* walks this array and reference-compares `prefix(prev)` vs
|
|
2212
|
+
* `prefix(next)` per entry, fanning bits into a `(lo, hi)` pair when
|
|
2213
|
+
* the array length exceeds 31.
|
|
2214
|
+
*
|
|
2215
|
+
* Returns null if no paths are present.
|
|
2216
|
+
*/
|
|
2217
|
+
function buildPrefixesProp(fieldBits, fieldBitsHi, f) {
|
|
2218
|
+
if (fieldBits.size === 0 && fieldBitsHi.size === 0)
|
|
2219
|
+
return null;
|
|
2220
|
+
// Sort paths by bit value within each word. Bits are powers of two
|
|
2221
|
+
// inside their word (1, 2, 4, …, 1<<30), so sorting numerically gives
|
|
2222
|
+
// ascending bit position. FULL_MASK (-1) entries from past-61
|
|
2223
|
+
// overflow shouldn't drive a prefix entry — defensively skip them.
|
|
2224
|
+
const orderedLo = [...fieldBits.entries()]
|
|
2225
|
+
.filter(([, bit]) => bit > 0)
|
|
2226
|
+
.sort(([, a], [, b]) => a - b);
|
|
2227
|
+
const orderedHi = [...fieldBitsHi.entries()].sort(([, a], [, b]) => a - b);
|
|
2228
|
+
const buildArrow = (path) => {
|
|
2229
|
+
const parts = path.split('.');
|
|
2230
|
+
const body = buildAccess(f, 's', parts);
|
|
2231
|
+
return f.createArrowFunction(undefined, undefined, [f.createParameterDeclaration(undefined, undefined, 's')], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body);
|
|
2232
|
+
};
|
|
2233
|
+
const arrows = [
|
|
2234
|
+
...orderedLo.map(([path]) => buildArrow(path)),
|
|
2235
|
+
...orderedHi.map(([path]) => buildArrow(path)),
|
|
2236
|
+
];
|
|
2237
|
+
return f.createPropertyAssignment('__prefixes', f.createArrayLiteralExpression(arrows, false));
|
|
2238
|
+
}
|
|
2109
2239
|
function buildAccess(f, root, parts) {
|
|
2110
2240
|
let expr = f.createIdentifier(root);
|
|
2111
2241
|
for (const part of parts) {
|
|
@@ -2817,7 +2947,7 @@ const VOID_ELEMENTS = new Set([
|
|
|
2817
2947
|
* Try to analyze an element call and all its descendants as a collapsible subtree.
|
|
2818
2948
|
* Returns null if any part of the tree is not eligible for collapse.
|
|
2819
2949
|
*/
|
|
2820
|
-
function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
2950
|
+
function analyzeSubtree(node, helpers, fieldBits, path, fieldBitsHi = new Map()) {
|
|
2821
2951
|
if (!ts.isIdentifier(node.expression))
|
|
2822
2952
|
return null;
|
|
2823
2953
|
const localName = node.expression.text;
|
|
@@ -2871,8 +3001,8 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
|
2871
3001
|
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
|
|
2872
3002
|
const kind = classifyKind(key);
|
|
2873
3003
|
const resolvedKey = resolveKey(key, kind);
|
|
2874
|
-
const { mask, readsState } = computeAccessorMask(value, fieldBits);
|
|
2875
|
-
if (mask === 0 && !readsState) {
|
|
3004
|
+
const { mask, maskHi, readsState } = computeAccessorMask(value, fieldBits, undefined, fieldBitsHi);
|
|
3005
|
+
if (mask === 0 && maskHi === 0 && !readsState) {
|
|
2876
3006
|
// Constant fold — treat as static if we can extract a string
|
|
2877
3007
|
const staticVal = tryExtractStaticString(value);
|
|
2878
3008
|
if (staticVal !== null) {
|
|
@@ -2881,21 +3011,22 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
|
2881
3011
|
continue;
|
|
2882
3012
|
}
|
|
2883
3013
|
}
|
|
2884
|
-
|
|
3014
|
+
const finalMask = mask === 0 && maskHi === 0 && readsState ? 0xffffffff | 0 : mask;
|
|
3015
|
+
bindings.push([finalMask, maskHi, kind, resolvedKey, value]);
|
|
2885
3016
|
continue;
|
|
2886
3017
|
}
|
|
2887
3018
|
// Per-item accessor call
|
|
2888
3019
|
if (ts.isCallExpression(value) && isPerItemCall(value)) {
|
|
2889
3020
|
const kind = classifyKind(key);
|
|
2890
3021
|
const resolvedKey = resolveKey(key, kind);
|
|
2891
|
-
bindings.push([0xffffffff | 0, kind, resolvedKey, value]);
|
|
3022
|
+
bindings.push([0xffffffff | 0, 0, kind, resolvedKey, value]);
|
|
2892
3023
|
continue;
|
|
2893
3024
|
}
|
|
2894
3025
|
// Per-item property access: item.field (or hoisted __a0/__a1/…)
|
|
2895
3026
|
if (isPerItemFieldAccess(value) || isHoistedPerItem(value)) {
|
|
2896
3027
|
const kind = classifyKind(key);
|
|
2897
3028
|
const resolvedKey = resolveKey(key, kind);
|
|
2898
|
-
bindings.push([0xffffffff | 0, kind, resolvedKey, value]);
|
|
3029
|
+
bindings.push([0xffffffff | 0, 0, kind, resolvedKey, value]);
|
|
2899
3030
|
continue;
|
|
2900
3031
|
}
|
|
2901
3032
|
// Static literal prop
|
|
@@ -2944,11 +3075,12 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
|
2944
3075
|
// Reactive text — accessor is first arg
|
|
2945
3076
|
const accessor = child.arguments[0];
|
|
2946
3077
|
if (ts.isArrowFunction(accessor) || ts.isFunctionExpression(accessor)) {
|
|
2947
|
-
const { mask, readsState } = computeAccessorMask(accessor, fieldBits);
|
|
3078
|
+
const { mask, maskHi, readsState } = computeAccessorMask(accessor, fieldBits, undefined, fieldBitsHi);
|
|
2948
3079
|
children.push({
|
|
2949
3080
|
type: 'reactiveText',
|
|
2950
3081
|
accessor,
|
|
2951
|
-
mask: mask === 0 && readsState ? 0xffffffff | 0 : mask,
|
|
3082
|
+
mask: mask === 0 && maskHi === 0 && readsState ? 0xffffffff | 0 : mask,
|
|
3083
|
+
maskHi,
|
|
2952
3084
|
childIdx,
|
|
2953
3085
|
});
|
|
2954
3086
|
childIdx++; // placeholder text node in template
|
|
@@ -2960,6 +3092,7 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
|
2960
3092
|
type: 'reactiveText',
|
|
2961
3093
|
accessor,
|
|
2962
3094
|
mask: 0xffffffff | 0,
|
|
3095
|
+
maskHi: 0,
|
|
2963
3096
|
childIdx,
|
|
2964
3097
|
});
|
|
2965
3098
|
childIdx++; // placeholder text node in template
|
|
@@ -2972,6 +3105,7 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
|
2972
3105
|
type: 'reactiveText',
|
|
2973
3106
|
accessor,
|
|
2974
3107
|
mask: 0xffffffff | 0,
|
|
3108
|
+
maskHi: 0,
|
|
2975
3109
|
childIdx,
|
|
2976
3110
|
});
|
|
2977
3111
|
childIdx++;
|
|
@@ -2983,7 +3117,7 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
|
|
|
2983
3117
|
if (ts.isCallExpression(child) &&
|
|
2984
3118
|
ts.isIdentifier(child.expression) &&
|
|
2985
3119
|
helpers.has(child.expression.text)) {
|
|
2986
|
-
const childNode = analyzeSubtree(child, helpers, fieldBits, [...path, childIdx]);
|
|
3120
|
+
const childNode = analyzeSubtree(child, helpers, fieldBits, [...path, childIdx], fieldBitsHi);
|
|
2987
3121
|
if (!childNode)
|
|
2988
3122
|
return null;
|
|
2989
3123
|
children.push({ type: 'element', node: childNode });
|
|
@@ -3196,24 +3330,33 @@ function emitSubtreeTemplate(analyzed, fieldBits, f) {
|
|
|
3196
3330
|
], ts.NodeFlags.Const)));
|
|
3197
3331
|
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier(cVar), 'parentNode'), 'replaceChild'), undefined, [f.createIdentifier(tVar), f.createIdentifier(cVar)])));
|
|
3198
3332
|
}
|
|
3199
|
-
// __bind(__t0, mask, 'text', undefined, accessor)
|
|
3200
|
-
|
|
3333
|
+
// __bind(__t0, mask, 'text', undefined, accessor, [maskHi])
|
|
3334
|
+
const rtArgs = [
|
|
3201
3335
|
f.createIdentifier(tVar),
|
|
3202
3336
|
createMaskLiteral(f, rt.mask),
|
|
3203
3337
|
f.createStringLiteral('text'),
|
|
3204
3338
|
f.createIdentifier('undefined'),
|
|
3205
3339
|
rt.accessor,
|
|
3206
|
-
]
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3340
|
+
];
|
|
3341
|
+
// Only pass the 6th positional arg when the accessor reads a
|
|
3342
|
+
// high-word prefix. Keeps the emit byte-identical to the
|
|
3343
|
+
// pre-multi-word baseline for the common case.
|
|
3344
|
+
if (rt.maskHi !== 0)
|
|
3345
|
+
rtArgs.push(createMaskLiteral(f, rt.maskHi));
|
|
3346
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, rtArgs)));
|
|
3347
|
+
}
|
|
3348
|
+
// Reactive bindings — __bind(node, mask, kind, key, accessor, [maskHi])
|
|
3349
|
+
for (const [mask, maskHi, kind, key, accessor] of op.bindings) {
|
|
3350
|
+
const args = [
|
|
3211
3351
|
nodeRef,
|
|
3212
3352
|
createMaskLiteral(f, mask),
|
|
3213
3353
|
f.createStringLiteral(kind),
|
|
3214
3354
|
key ? f.createStringLiteral(key) : f.createIdentifier('undefined'),
|
|
3215
3355
|
accessor,
|
|
3216
|
-
]
|
|
3356
|
+
];
|
|
3357
|
+
if (maskHi !== 0)
|
|
3358
|
+
args.push(createMaskLiteral(f, maskHi));
|
|
3359
|
+
stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, args)));
|
|
3217
3360
|
}
|
|
3218
3361
|
}
|
|
3219
3362
|
// Emit delegated event listeners on root
|
|
@@ -3424,21 +3567,32 @@ function isHoistedPerItem(node) {
|
|
|
3424
3567
|
// mask = 0 + readsState = false → constant (can fold to static)
|
|
3425
3568
|
// mask = 0 + readsState = true → unresolvable state access (FULL_MASK)
|
|
3426
3569
|
// mask > 0 → precise mask
|
|
3427
|
-
|
|
3570
|
+
// See `NON_DELEGATION_HELPERS` in collect-deps.ts — same set of names
|
|
3571
|
+
// that aren't followed when scanning for `helper(s)` delegation calls.
|
|
3572
|
+
const NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml']);
|
|
3573
|
+
function computeAccessorMask(accessor, fieldBits, visited = new Set(), fieldBitsHi) {
|
|
3574
|
+
if (visited.has(accessor))
|
|
3575
|
+
return { mask: 0, maskHi: 0, readsState: false };
|
|
3576
|
+
visited.add(accessor);
|
|
3428
3577
|
if (accessor.parameters.length === 0)
|
|
3429
|
-
return { mask: 0xffffffff | 0, readsState: false };
|
|
3578
|
+
return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
|
|
3430
3579
|
const paramName = accessor.parameters[0].name;
|
|
3431
3580
|
if (!ts.isIdentifier(paramName))
|
|
3432
|
-
return { mask: 0xffffffff | 0, readsState: false };
|
|
3581
|
+
return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
|
|
3433
3582
|
// FunctionDeclaration always has a body (we never resolve overloads here);
|
|
3434
3583
|
// ArrowFunction's body may be a single expression. Both shapes are walked
|
|
3435
3584
|
// identically by ts.forEachChild, so no special-casing is needed below.
|
|
3436
3585
|
if (!accessor.body)
|
|
3437
|
-
return { mask: 0xffffffff | 0, readsState: false };
|
|
3586
|
+
return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
|
|
3438
3587
|
const stateParam = paramName.text;
|
|
3439
3588
|
let mask = 0;
|
|
3589
|
+
let maskHi = 0;
|
|
3440
3590
|
let readsState = false;
|
|
3441
|
-
|
|
3591
|
+
// `inNestedFn` gates only the delegation-recursion. Property-access
|
|
3592
|
+
// path extraction happens everywhere — inner-arrow callbacks like
|
|
3593
|
+
// `s.items.filter((i) => i.includes(s.filter))` close over our
|
|
3594
|
+
// state, and their `s.filter` reads contribute to the mask.
|
|
3595
|
+
function walk(node, inNestedFn) {
|
|
3442
3596
|
// `node.parent` can be undefined for synthetic nodes produced by
|
|
3443
3597
|
// earlier AST-transform passes (the row-factory rewrite and the
|
|
3444
3598
|
// per-item heuristic both build new sub-trees whose inner nodes
|
|
@@ -3462,26 +3616,70 @@ function computeAccessorMask(accessor, fieldBits) {
|
|
|
3462
3616
|
const chain = resolveChain(node, stateParam);
|
|
3463
3617
|
if (chain) {
|
|
3464
3618
|
const bit = fieldBits.get(chain);
|
|
3619
|
+
const bitHi = fieldBitsHi?.get(chain);
|
|
3465
3620
|
if (bit !== undefined) {
|
|
3466
3621
|
mask |= bit;
|
|
3467
3622
|
}
|
|
3623
|
+
else if (bitHi !== undefined) {
|
|
3624
|
+
maskHi |= bitHi;
|
|
3625
|
+
}
|
|
3468
3626
|
else {
|
|
3627
|
+
// Match paths that overlap our chain in either direction:
|
|
3628
|
+
// - `path` extends `chain` — fieldBits has finer-grained paths
|
|
3629
|
+
// than we're reading (e.g. chain='user', fieldBits has
|
|
3630
|
+
// 'user.email').
|
|
3631
|
+
// - `chain` extends `path` — we're reading deeper than what
|
|
3632
|
+
// fieldBits tracks (e.g. chain='items.filter' from
|
|
3633
|
+
// `s.items.filter(...)`, fieldBits has 'items'). Both ends
|
|
3634
|
+
// must mask in: a change to `items` invalidates anything
|
|
3635
|
+
// downstream of it.
|
|
3469
3636
|
for (const [path, b] of fieldBits) {
|
|
3470
|
-
if (path.startsWith(chain + '.') || path
|
|
3637
|
+
if (path === chain || path.startsWith(chain + '.') || chain.startsWith(path + '.')) {
|
|
3471
3638
|
mask |= b;
|
|
3472
3639
|
}
|
|
3473
3640
|
}
|
|
3641
|
+
if (fieldBitsHi) {
|
|
3642
|
+
for (const [path, b] of fieldBitsHi) {
|
|
3643
|
+
if (path === chain ||
|
|
3644
|
+
path.startsWith(chain + '.') ||
|
|
3645
|
+
chain.startsWith(path + '.')) {
|
|
3646
|
+
maskHi |= b;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3474
3650
|
}
|
|
3475
3651
|
}
|
|
3476
3652
|
}
|
|
3477
3653
|
}
|
|
3478
|
-
|
|
3654
|
+
// Delegation: `helper(s)` where `s` matches our state param.
|
|
3655
|
+
// Recurse into the helper's body so its state-path reads
|
|
3656
|
+
// contribute to our mask. Only at top level — inside a nested
|
|
3657
|
+
// function body, `s` may be shadowed and the call isn't
|
|
3658
|
+
// unambiguously handing our state in.
|
|
3659
|
+
if (!inNestedFn && ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
3660
|
+
const calleeName = node.expression.text;
|
|
3661
|
+
if (!NON_DELEGATION_HELPERS.has(calleeName)) {
|
|
3662
|
+
const arg0 = node.arguments[0];
|
|
3663
|
+
if (arg0 && ts.isIdentifier(arg0) && arg0.text === stateParam) {
|
|
3664
|
+
const resolved = resolveAccessorBody(node.expression);
|
|
3665
|
+
if (resolved) {
|
|
3666
|
+
const inner = computeAccessorMask(resolved, fieldBits, visited, fieldBitsHi);
|
|
3667
|
+
mask |= inner.mask;
|
|
3668
|
+
maskHi |= inner.maskHi;
|
|
3669
|
+
if (inner.readsState)
|
|
3670
|
+
readsState = true;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
const enteringNested = ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node);
|
|
3676
|
+
ts.forEachChild(node, (child) => walk(child, inNestedFn || enteringNested));
|
|
3479
3677
|
}
|
|
3480
|
-
walk(accessor.body);
|
|
3481
|
-
if (mask === 0 && readsState) {
|
|
3482
|
-
return { mask: 0xffffffff | 0, readsState: true };
|
|
3678
|
+
walk(accessor.body, false);
|
|
3679
|
+
if (mask === 0 && maskHi === 0 && readsState) {
|
|
3680
|
+
return { mask: 0xffffffff | 0, maskHi: 0, readsState: true };
|
|
3483
3681
|
}
|
|
3484
|
-
return { mask, readsState };
|
|
3682
|
+
return { mask, maskHi, readsState };
|
|
3485
3683
|
}
|
|
3486
3684
|
function resolveChain(node, paramName) {
|
|
3487
3685
|
const parts = [];
|