@llui/vite-plugin 0.0.42 → 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/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 fieldBits = fileHasComponent ? collectDeps(source) : new Map();
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
- bindings.push(f.createArrayLiteralExpression([
907
- createMaskLiteral(f, mask === 0 && readsState ? 0xffffffff | 0 : mask),
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))
@@ -1086,36 +1097,20 @@ function tryInjectDirty(node, fieldBits, f) {
1086
1097
  return null;
1087
1098
  }
1088
1099
  }
1089
- // Build __dirty: (o, n) => (Object.is(o.field, n.field) ? 0 : bit) | ...
1090
- // Compare at top-level field (depth 1) nested path changes within a
1091
- // field must trigger the bit even if the specific sub-path isn't tracked.
1092
- // e.g., route.page tracked but route.data changes must fire.
1100
+ // Top-level field aggregated bit mask. Sub-paths under one field
1101
+ // (`route.page`, `route.data`) collapse into a single entry so
1102
+ // `tryBuildHandlers` and `__maskLegend` can reason per-field. Positions
1103
+ // 0..30 live here; 31..61 in the parallel high-word map below.
1093
1104
  const topLevelBits = new Map();
1094
1105
  for (const [path, bit] of fieldBits) {
1095
1106
  const topField = path.split('.')[0];
1096
1107
  topLevelBits.set(topField, (topLevelBits.get(topField) ?? 0) | bit);
1097
1108
  }
1098
- const comparisons = [];
1099
- for (const [field, bit] of topLevelBits) {
1100
- const oAccess = buildAccess(f, 'o', [field]);
1101
- const nAccess = buildAccess(f, 'n', [field]);
1102
- comparisons.push(f.createParenthesizedExpression(f.createConditionalExpression(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('Object'), 'is'), undefined, [oAccess, nAccess]), f.createToken(ts.SyntaxKind.QuestionToken), f.createNumericLiteral(0), f.createToken(ts.SyntaxKind.ColonToken), createMaskLiteral(f, bit))));
1103
- }
1104
- let dirtyBody = comparisons[0];
1105
- for (let i = 1; i < comparisons.length; i++) {
1106
- dirtyBody = f.createBinaryExpression(dirtyBody, ts.SyntaxKind.BarToken, comparisons[i]);
1107
- }
1108
- // Fallback: if no tracked bit fired but the state reference changed, some
1109
- // untracked field must have changed — return FULL_MASK so bindings whose
1110
- // accessors came from external modules (spread parts) still fire.
1111
- // tracked || (Object.is(o, n) ? 0 : FULL_MASK)
1112
- const fallback = f.createParenthesizedExpression(f.createConditionalExpression(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('Object'), 'is'), undefined, [f.createIdentifier('o'), f.createIdentifier('n')]), f.createToken(ts.SyntaxKind.QuestionToken), f.createNumericLiteral(0), f.createToken(ts.SyntaxKind.ColonToken), createMaskLiteral(f, -1)));
1113
- dirtyBody = f.createBinaryExpression(f.createParenthesizedExpression(dirtyBody), ts.SyntaxKind.BarBarToken, fallback);
1114
- const dirtyFn = f.createArrowFunction(undefined, undefined, [
1115
- f.createParameterDeclaration(undefined, undefined, 'o'),
1116
- f.createParameterDeclaration(undefined, undefined, 'n'),
1117
- ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), dirtyBody);
1118
- const dirtyProp = f.createPropertyAssignment('__dirty', dirtyFn);
1109
+ const topLevelBitsHi = new Map();
1110
+ for (const [path, bit] of fieldBitsHi) {
1111
+ const topField = path.split('.')[0];
1112
+ topLevelBitsHi.set(topField, (topLevelBitsHi.get(topField) ?? 0) | bit);
1113
+ }
1119
1114
  // __maskLegend: maps each top-level state field to the bit(s) that fire when
1120
1115
  // it changes. Lets introspection tools decode runtime dirty masks to field names.
1121
1116
  const legendProps = [];
@@ -1128,25 +1123,59 @@ function tryInjectDirty(node, fieldBits, f) {
1128
1123
  const legendProp = f.createPropertyAssignment('__maskLegend', f.createObjectLiteralExpression(legendProps, false));
1129
1124
  // Structural mask — used by both __update and __handlers
1130
1125
  const structuralMask = computeStructuralMask(configArg, fieldBits);
1131
- const phase2Mask = computePhase2Mask(configArg, fieldBits);
1132
- const updateBody = buildUpdateBody(f, structuralMask, phase2Mask);
1126
+ const updateBody = buildUpdateBody(f, structuralMask);
1127
+ // `dHi` is the high-word dirty mask, appended as the trailing
1128
+ // positional arg so stale 5-param compiled bundles continue to gate
1129
+ // correctly: the runtime calls `__update(s, d, b, bl, p, dHi)`,
1130
+ // old bundles' 5-param arrow ignores the extra arg (for ≤31-prefix
1131
+ // components dHi is always 0 anyway). New bundles use it for
1132
+ // precise two-word Phase 1 gating.
1133
1133
  const updateFn = f.createArrowFunction(undefined, undefined, [
1134
1134
  f.createParameterDeclaration(undefined, undefined, 's'),
1135
1135
  f.createParameterDeclaration(undefined, undefined, 'd'),
1136
1136
  f.createParameterDeclaration(undefined, undefined, 'b'),
1137
1137
  f.createParameterDeclaration(undefined, undefined, 'bl'),
1138
1138
  f.createParameterDeclaration(undefined, undefined, 'p'),
1139
+ f.createParameterDeclaration(undefined, undefined, 'dHi', undefined, undefined, f.createNumericLiteral(0)),
1139
1140
  ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), updateBody);
1140
1141
  const updateProp = f.createPropertyAssignment('__update', updateFn);
1141
1142
  // __handlers: per-message-type specialized update functions.
1142
1143
  // Analyzes the update() switch/case and generates direct handlers
1143
1144
  // that bypass the generic Phase 1/2 pipeline for single-message updates.
1144
- const handlersProp = tryBuildHandlers(configArg, topLevelBits, structuralMask, f);
1145
- // Keep __update even when __handlers is present it provides Phase 1
1146
- // mask gating for multi-message batches that bypass __handlers.
1147
- const extraProps = [dirtyProp, legendProp, updateProp];
1145
+ const handlersProp = tryBuildHandlers(configArg, topLevelBits, topLevelBitsHi, structuralMask, f);
1146
+ // Both `__update` and `__handlers` carry two-word gates: `__update`'s
1147
+ // Phase 1 block loop uses `(mask & d) | (maskHi & dHi)`, and
1148
+ // `__handlers` passes `caseDirtyHi` to `_handleMsg` which gates blocks
1149
+ // against both words. `dHi` defaults to 0 so any stale 5-arg call site
1150
+ // still works. `__dirty` is no longer emitted — `__prefixes` (below)
1151
+ // is strictly more precise, and the runtime throws on hand-authored
1152
+ // `__dirty`. `__maskLegend` survives because the agent layer uses it
1153
+ // to decode runtime dirty masks back to top-level field names.
1154
+ const extraProps = [legendProp, updateProp];
1148
1155
  if (handlersProp)
1149
1156
  extraProps.push(handlersProp);
1157
+ // __prefixes: opt-in path-keyed reactivity (see
1158
+ // docs/proposals/unified-composition-model.md). One closure per
1159
+ // distinct path that an accessor reads, hoisted into a stable array;
1160
+ // the array position IS the bit position used by the path's bindings.
1161
+ // The runtime prefers __prefixes when present and computes the
1162
+ // combinedDirty mask by reference-comparing `prefix(prev)` vs
1163
+ // `prefix(next)` for each entry — strictly more precise than the
1164
+ // top-level-conflated __dirty (which always co-fires bindings sharing
1165
+ // a top-level field even when only one sub-path actually mutated).
1166
+ //
1167
+ // Emit `__prefixes` whenever any reactive paths are present. For
1168
+ // components with ≤31 paths, the runtime's
1169
+ // `computeDirtyFromPrefixes` returns a single `number`; for
1170
+ // 32..61-path components it returns a `[lo, hi]` tuple that the
1171
+ // runtime fans out into `combinedDirty` + `combinedDirtyHi`. The
1172
+ // binding-level mask gating is still single-word at the compiler
1173
+ // emit layer today, so high-position bindings still re-evaluate
1174
+ // every cycle — but the dirty computation itself is now precise,
1175
+ // which lets memo()'d aggregates short-circuit correctly.
1176
+ const prefixesProp = buildPrefixesProp(fieldBits, fieldBitsHi, f);
1177
+ if (prefixesProp)
1178
+ extraProps.push(prefixesProp);
1150
1179
  const newConfig = f.createObjectLiteralExpression([...configArg.properties, ...extraProps], true);
1151
1180
  return f.createCallExpression(node.expression, node.typeArguments, [
1152
1181
  newConfig,
@@ -1164,8 +1193,8 @@ function tryInjectDirty(node, fieldBits, f) {
1164
1193
  * Conservative: only generates handlers for cases where the field
1165
1194
  * modifications are statically determinable. Complex cases are skipped.
1166
1195
  */
1167
- function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
1168
- if (topLevelBits.size === 0)
1196
+ function tryBuildHandlers(configArg, topLevelBits, topLevelBitsHi, structuralMask, f) {
1197
+ if (topLevelBits.size === 0 && topLevelBitsHi.size === 0)
1169
1198
  return null;
1170
1199
  // Find the update function in the component config
1171
1200
  let updateFn = null;
@@ -1241,7 +1270,7 @@ function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
1241
1270
  let bailOut = false;
1242
1271
  for (const returnExpr of returnExprs) {
1243
1272
  const stateExpr = returnExpr.elements[0];
1244
- const fields = analyzeModifiedFields(stateExpr, stateName, topLevelBits);
1273
+ const fields = analyzeModifiedFields(stateExpr, stateName, topLevelBits, topLevelBitsHi);
1245
1274
  if (!fields) {
1246
1275
  bailOut = true;
1247
1276
  break;
@@ -1252,14 +1281,29 @@ function tryBuildHandlers(configArg, topLevelBits, structuralMask, f) {
1252
1281
  if (bailOut)
1253
1282
  continue; // at least one return path was too complex
1254
1283
  const modifiedFields = Array.from(allModified);
1255
- // Compute the dirty mask for this case
1284
+ // Compute the dirty mask for this case across both words. Fields
1285
+ // tracked in `topLevelBitsHi` contribute to `caseDirtyHi`; fields
1286
+ // tracked nowhere (`undefined` lookup in both) fall back to
1287
+ // FULL_MASK in the low word — same conservative behavior as
1288
+ // before, just preserved per-word now.
1256
1289
  let caseDirty = 0;
1290
+ let caseDirtyHi = 0;
1257
1291
  for (const field of modifiedFields) {
1258
- caseDirty |= topLevelBits.get(field) ?? 0xffffffff | 0;
1292
+ const lo = topLevelBits.get(field);
1293
+ const hi = topLevelBitsHi.get(field);
1294
+ if (lo === undefined && hi === undefined) {
1295
+ caseDirty |= 0xffffffff | 0;
1296
+ }
1297
+ else {
1298
+ if (lo !== undefined)
1299
+ caseDirty |= lo;
1300
+ if (hi !== undefined)
1301
+ caseDirtyHi |= hi;
1302
+ }
1259
1303
  }
1260
1304
  // Detect array operation pattern for structural block optimization
1261
1305
  const arrayOp = detectArrayOp(clause, stateName, modifiedFields, structuralMask, caseDirty);
1262
- const handler = buildCaseHandler(f, caseDirty, arrayOp);
1306
+ const handler = buildCaseHandler(f, caseDirty, caseDirtyHi, arrayOp);
1263
1307
  handlers.push(f.createPropertyAssignment(f.createStringLiteral(msgType), handler));
1264
1308
  }
1265
1309
  if (handlers.length === 0)
@@ -1445,7 +1489,12 @@ function hasSliceAssignment(clause, stateName, varName) {
1445
1489
  * Analyze which top-level state fields are modified in a return expression.
1446
1490
  * Returns the set of field names, or null if too complex to determine.
1447
1491
  */
1448
- function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
1492
+ function analyzeModifiedFields(stateExpr, stateName, topLevelBits, topLevelBitsHi = new Map()) {
1493
+ // Recognize fields tracked in EITHER the low-word or high-word map.
1494
+ // 32..61-prefix components have their overflow paths in
1495
+ // `topLevelBitsHi`; the case handler's `caseDirty` / `caseDirtyHi`
1496
+ // logic depends on us recognizing those fields here.
1497
+ const isTracked = (name) => topLevelBits.has(name) || topLevelBitsHi.has(name);
1449
1498
  // Pattern: { ...state, field1: ..., field2: ... } or { field1: ..., field2: ... }
1450
1499
  if (ts.isObjectLiteralExpression(stateExpr)) {
1451
1500
  const modified = [];
@@ -1464,14 +1513,14 @@ function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
1464
1513
  }
1465
1514
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1466
1515
  const fieldName = prop.name.text;
1467
- if (topLevelBits.has(fieldName)) {
1516
+ if (isTracked(fieldName)) {
1468
1517
  modified.push(fieldName);
1469
1518
  }
1470
1519
  }
1471
1520
  // Handle shorthand: { ...state, rows } where rows is a local variable
1472
1521
  if (ts.isShorthandPropertyAssignment(prop)) {
1473
1522
  const fieldName = prop.name.text;
1474
- if (topLevelBits.has(fieldName)) {
1523
+ if (isTracked(fieldName)) {
1475
1524
  modified.push(fieldName);
1476
1525
  }
1477
1526
  }
@@ -1504,7 +1553,7 @@ function analyzeModifiedFields(stateExpr, stateName, topLevelBits) {
1504
1553
  * Build a handler that delegates to __handleMsg(inst, msg, dirty, method).
1505
1554
  * method: 0=reconcile, 1=reconcileItems, 2=reconcileClear, 3=reconcileRemove, -1=skip blocks
1506
1555
  */
1507
- function buildCaseHandler(f, caseDirty, arrayOp) {
1556
+ function buildCaseHandler(f, caseDirty, caseDirtyHi, arrayOp) {
1508
1557
  const method = typeof arrayOp === 'object' && arrayOp.type === 'strided'
1509
1558
  ? 10 + arrayOp.stride // reconcileChanged with stride
1510
1559
  : arrayOp === 'none'
@@ -1516,18 +1565,24 @@ function buildCaseHandler(f, caseDirty, arrayOp) {
1516
1565
  : arrayOp === 'remove'
1517
1566
  ? 3
1518
1567
  : 0; // general
1519
- // (inst, msg) => __handleMsg(inst, msg, dirty, method)
1520
- return f.createArrowFunction(undefined, undefined, [
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, [
1568
+ // (inst, msg) => __handleMsg(inst, msg, dirty, method, [dirtyHi])
1569
+ const args = [
1524
1570
  f.createIdentifier('inst'),
1525
1571
  f.createIdentifier('msg'),
1526
1572
  createMaskLiteral(f, caseDirty),
1527
1573
  method >= 0
1528
1574
  ? f.createNumericLiteral(method)
1529
1575
  : f.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f.createNumericLiteral(1)),
1530
- ]));
1576
+ ];
1577
+ // Emit the 5th positional arg only when the case touches a high-word
1578
+ // field. Stale runtime bundles' _handleMsg signatures ignored that
1579
+ // slot anyway; new ones (defaulted to 0) make it explicit when needed.
1580
+ if (caseDirtyHi !== 0)
1581
+ args.push(createMaskLiteral(f, caseDirtyHi));
1582
+ return f.createArrowFunction(undefined, undefined, [
1583
+ f.createParameterDeclaration(undefined, undefined, 'inst'),
1584
+ f.createParameterDeclaration(undefined, undefined, 'msg'),
1585
+ ], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), f.createCallExpression(f.createIdentifier('__handleMsg'), undefined, args));
1531
1586
  }
1532
1587
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1533
1588
  function _deadCode_legacyCaseHandler(f, caseDirty, arrayOp) {
@@ -2010,17 +2065,6 @@ function computeStructuralMask(configArg, fieldBits) {
2010
2065
  walk(viewProp.initializer);
2011
2066
  return foundStructural ? mask || 0xffffffff | 0 : 0;
2012
2067
  }
2013
- /**
2014
- * Compute the OR of all component-level binding masks from text() calls
2015
- * and element bindings in the view. Returns 0 if no component-level bindings.
2016
- */
2017
- function computePhase2Mask(_configArg, _fieldBits) {
2018
- // For now, return FULL_MASK — a future pass can analyze all binding sites
2019
- // in the view to compute the precise aggregate. The key optimization is
2020
- // already in Phase 1 gating: when structuralMask doesn't intersect dirty,
2021
- // the entire reconciliation is skipped.
2022
- return 0xffffffff | 0;
2023
- }
2024
2068
  /**
2025
2069
  * Build the __update function body:
2026
2070
  * {
@@ -2053,16 +2097,22 @@ function computePhase2Mask(_configArg, _fieldBits) {
2053
2097
  * }
2054
2098
  * }
2055
2099
  */
2056
- function buildUpdateBody(f, structuralMask, _phase2Mask) {
2100
+ function buildUpdateBody(f, structuralMask) {
2057
2101
  const stmts = [];
2058
2102
  // Phase 1: structural block reconciliation, gated by aggregate mask
2059
2103
  if (structuralMask !== 0) {
2060
2104
  const phase1Stmts = [];
2061
2105
  // for (let i = 0; i < bl.length; i++) {
2062
2106
  // const bk = bl[i];
2063
- // if (!bk || (bk.mask & d) === 0) continue;
2064
- // bk.reconcile(s, d)
2107
+ // if (!bk || !((bk.mask & d) | (bk.maskHi & dHi))) continue;
2108
+ // bk.reconcile(s, d, dHi)
2065
2109
  // }
2110
+ // Two-word gate matches the runtime's `genericUpdate`: bits 0..30
2111
+ // in `d`, bits 31..61 in `dHi`. For ≤31-prefix components both
2112
+ // `bk.maskHi` and `dHi` are 0, so V8's inline cache collapses the
2113
+ // OR back to the single-word check. >31-prefix components use the
2114
+ // high word for precise gating.
2115
+ //
2066
2116
  // Re-read bl.length each iteration and null-check bk — a branch's
2067
2117
  // reconcile may dispose the old scope, whose disposers splice child
2068
2118
  // structural blocks out of this shared array mid-iteration.
@@ -2070,8 +2120,8 @@ function buildUpdateBody(f, structuralMask, _phase2Mask) {
2070
2120
  f.createVariableStatement(undefined, f.createVariableDeclarationList([
2071
2121
  f.createVariableDeclaration('bk', undefined, undefined, f.createElementAccessExpression(f.createIdentifier('bl'), f.createIdentifier('i'))),
2072
2122
  ], 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.EqualsEqualsEqualsToken, f.createNumericLiteral(0))), f.createContinueStatement()),
2074
- f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'reconcile'), undefined, [f.createIdentifier('s'), f.createIdentifier('d')])),
2123
+ 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()),
2124
+ f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createIdentifier('bk'), 'reconcile'), undefined, [f.createIdentifier('s'), f.createIdentifier('d'), f.createIdentifier('dHi')])),
2075
2125
  ], true));
2076
2126
  phase1Stmts.push(blockLoop);
2077
2127
  // Compaction: if (b.length > p || (p > 0 && b[0].dead)) { ... }
@@ -2097,15 +2147,50 @@ function buildUpdateBody(f, structuralMask, _phase2Mask) {
2097
2147
  stmts.push(...phase1Stmts);
2098
2148
  }
2099
2149
  }
2100
- // Phase 2: delegate to shared runtime — __runPhase2(s, d, b, p)
2150
+ // Phase 2: delegate to shared runtime — __runPhase2(s, d, dHi, b, p)
2101
2151
  stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__runPhase2'), undefined, [
2102
2152
  f.createIdentifier('s'),
2103
2153
  f.createIdentifier('d'),
2154
+ f.createIdentifier('dHi'),
2104
2155
  f.createIdentifier('b'),
2105
2156
  f.createIdentifier('p'),
2106
2157
  ])));
2107
2158
  return f.createBlock(stmts, true);
2108
2159
  }
2160
+ /**
2161
+ * Build the `__prefixes` property assignment from path → bit maps.
2162
+ *
2163
+ * Emits one arrow `(s) => s.<path>` per distinct path. Array index =
2164
+ * the path's bit position: positions 0..30 come from `fieldBits` (low
2165
+ * word), positions 31..61 from `fieldBitsHi` (high word). The runtime
2166
+ * walks this array and reference-compares `prefix(prev)` vs
2167
+ * `prefix(next)` per entry, fanning bits into a `(lo, hi)` pair when
2168
+ * the array length exceeds 31.
2169
+ *
2170
+ * Returns null if no paths are present.
2171
+ */
2172
+ function buildPrefixesProp(fieldBits, fieldBitsHi, f) {
2173
+ if (fieldBits.size === 0 && fieldBitsHi.size === 0)
2174
+ return null;
2175
+ // Sort paths by bit value within each word. Bits are powers of two
2176
+ // inside their word (1, 2, 4, …, 1<<30), so sorting numerically gives
2177
+ // ascending bit position. FULL_MASK (-1) entries from past-61
2178
+ // overflow shouldn't drive a prefix entry — defensively skip them.
2179
+ const orderedLo = [...fieldBits.entries()]
2180
+ .filter(([, bit]) => bit > 0)
2181
+ .sort(([, a], [, b]) => a - b);
2182
+ const orderedHi = [...fieldBitsHi.entries()].sort(([, a], [, b]) => a - b);
2183
+ const buildArrow = (path) => {
2184
+ const parts = path.split('.');
2185
+ const body = buildAccess(f, 's', parts);
2186
+ return f.createArrowFunction(undefined, undefined, [f.createParameterDeclaration(undefined, undefined, 's')], undefined, f.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body);
2187
+ };
2188
+ const arrows = [
2189
+ ...orderedLo.map(([path]) => buildArrow(path)),
2190
+ ...orderedHi.map(([path]) => buildArrow(path)),
2191
+ ];
2192
+ return f.createPropertyAssignment('__prefixes', f.createArrayLiteralExpression(arrows, false));
2193
+ }
2109
2194
  function buildAccess(f, root, parts) {
2110
2195
  let expr = f.createIdentifier(root);
2111
2196
  for (const part of parts) {
@@ -2817,7 +2902,7 @@ const VOID_ELEMENTS = new Set([
2817
2902
  * Try to analyze an element call and all its descendants as a collapsible subtree.
2818
2903
  * Returns null if any part of the tree is not eligible for collapse.
2819
2904
  */
2820
- function analyzeSubtree(node, helpers, fieldBits, path) {
2905
+ function analyzeSubtree(node, helpers, fieldBits, path, fieldBitsHi = new Map()) {
2821
2906
  if (!ts.isIdentifier(node.expression))
2822
2907
  return null;
2823
2908
  const localName = node.expression.text;
@@ -2871,8 +2956,8 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2871
2956
  if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
2872
2957
  const kind = classifyKind(key);
2873
2958
  const resolvedKey = resolveKey(key, kind);
2874
- const { mask, readsState } = computeAccessorMask(value, fieldBits);
2875
- if (mask === 0 && !readsState) {
2959
+ const { mask, maskHi, readsState } = computeAccessorMask(value, fieldBits, undefined, fieldBitsHi);
2960
+ if (mask === 0 && maskHi === 0 && !readsState) {
2876
2961
  // Constant fold — treat as static if we can extract a string
2877
2962
  const staticVal = tryExtractStaticString(value);
2878
2963
  if (staticVal !== null) {
@@ -2881,21 +2966,22 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2881
2966
  continue;
2882
2967
  }
2883
2968
  }
2884
- bindings.push([mask === 0 && readsState ? 0xffffffff | 0 : mask, kind, resolvedKey, value]);
2969
+ const finalMask = mask === 0 && maskHi === 0 && readsState ? 0xffffffff | 0 : mask;
2970
+ bindings.push([finalMask, maskHi, kind, resolvedKey, value]);
2885
2971
  continue;
2886
2972
  }
2887
2973
  // Per-item accessor call
2888
2974
  if (ts.isCallExpression(value) && isPerItemCall(value)) {
2889
2975
  const kind = classifyKind(key);
2890
2976
  const resolvedKey = resolveKey(key, kind);
2891
- bindings.push([0xffffffff | 0, kind, resolvedKey, value]);
2977
+ bindings.push([0xffffffff | 0, 0, kind, resolvedKey, value]);
2892
2978
  continue;
2893
2979
  }
2894
2980
  // Per-item property access: item.field (or hoisted __a0/__a1/…)
2895
2981
  if (isPerItemFieldAccess(value) || isHoistedPerItem(value)) {
2896
2982
  const kind = classifyKind(key);
2897
2983
  const resolvedKey = resolveKey(key, kind);
2898
- bindings.push([0xffffffff | 0, kind, resolvedKey, value]);
2984
+ bindings.push([0xffffffff | 0, 0, kind, resolvedKey, value]);
2899
2985
  continue;
2900
2986
  }
2901
2987
  // Static literal prop
@@ -2944,11 +3030,12 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2944
3030
  // Reactive text — accessor is first arg
2945
3031
  const accessor = child.arguments[0];
2946
3032
  if (ts.isArrowFunction(accessor) || ts.isFunctionExpression(accessor)) {
2947
- const { mask, readsState } = computeAccessorMask(accessor, fieldBits);
3033
+ const { mask, maskHi, readsState } = computeAccessorMask(accessor, fieldBits, undefined, fieldBitsHi);
2948
3034
  children.push({
2949
3035
  type: 'reactiveText',
2950
3036
  accessor,
2951
- mask: mask === 0 && readsState ? 0xffffffff | 0 : mask,
3037
+ mask: mask === 0 && maskHi === 0 && readsState ? 0xffffffff | 0 : mask,
3038
+ maskHi,
2952
3039
  childIdx,
2953
3040
  });
2954
3041
  childIdx++; // placeholder text node in template
@@ -2960,6 +3047,7 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2960
3047
  type: 'reactiveText',
2961
3048
  accessor,
2962
3049
  mask: 0xffffffff | 0,
3050
+ maskHi: 0,
2963
3051
  childIdx,
2964
3052
  });
2965
3053
  childIdx++; // placeholder text node in template
@@ -2972,6 +3060,7 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2972
3060
  type: 'reactiveText',
2973
3061
  accessor,
2974
3062
  mask: 0xffffffff | 0,
3063
+ maskHi: 0,
2975
3064
  childIdx,
2976
3065
  });
2977
3066
  childIdx++;
@@ -2983,7 +3072,7 @@ function analyzeSubtree(node, helpers, fieldBits, path) {
2983
3072
  if (ts.isCallExpression(child) &&
2984
3073
  ts.isIdentifier(child.expression) &&
2985
3074
  helpers.has(child.expression.text)) {
2986
- const childNode = analyzeSubtree(child, helpers, fieldBits, [...path, childIdx]);
3075
+ const childNode = analyzeSubtree(child, helpers, fieldBits, [...path, childIdx], fieldBitsHi);
2987
3076
  if (!childNode)
2988
3077
  return null;
2989
3078
  children.push({ type: 'element', node: childNode });
@@ -3196,24 +3285,33 @@ function emitSubtreeTemplate(analyzed, fieldBits, f) {
3196
3285
  ], ts.NodeFlags.Const)));
3197
3286
  stmts.push(f.createExpressionStatement(f.createCallExpression(f.createPropertyAccessExpression(f.createPropertyAccessExpression(f.createIdentifier(cVar), 'parentNode'), 'replaceChild'), undefined, [f.createIdentifier(tVar), f.createIdentifier(cVar)])));
3198
3287
  }
3199
- // __bind(__t0, mask, 'text', undefined, accessor)
3200
- stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, [
3288
+ // __bind(__t0, mask, 'text', undefined, accessor, [maskHi])
3289
+ const rtArgs = [
3201
3290
  f.createIdentifier(tVar),
3202
3291
  createMaskLiteral(f, rt.mask),
3203
3292
  f.createStringLiteral('text'),
3204
3293
  f.createIdentifier('undefined'),
3205
3294
  rt.accessor,
3206
- ])));
3207
- }
3208
- // Reactive bindings __bind(node, mask, kind, key, accessor)
3209
- for (const [mask, kind, key, accessor] of op.bindings) {
3210
- stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, [
3295
+ ];
3296
+ // Only pass the 6th positional arg when the accessor reads a
3297
+ // high-word prefix. Keeps the emit byte-identical to the
3298
+ // pre-multi-word baseline for the common case.
3299
+ if (rt.maskHi !== 0)
3300
+ rtArgs.push(createMaskLiteral(f, rt.maskHi));
3301
+ stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, rtArgs)));
3302
+ }
3303
+ // Reactive bindings — __bind(node, mask, kind, key, accessor, [maskHi])
3304
+ for (const [mask, maskHi, kind, key, accessor] of op.bindings) {
3305
+ const args = [
3211
3306
  nodeRef,
3212
3307
  createMaskLiteral(f, mask),
3213
3308
  f.createStringLiteral(kind),
3214
3309
  key ? f.createStringLiteral(key) : f.createIdentifier('undefined'),
3215
3310
  accessor,
3216
- ])));
3311
+ ];
3312
+ if (maskHi !== 0)
3313
+ args.push(createMaskLiteral(f, maskHi));
3314
+ stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, args)));
3217
3315
  }
3218
3316
  }
3219
3317
  // Emit delegated event listeners on root
@@ -3427,22 +3525,23 @@ function isHoistedPerItem(node) {
3427
3525
  // See `NON_DELEGATION_HELPERS` in collect-deps.ts — same set of names
3428
3526
  // that aren't followed when scanning for `helper(s)` delegation calls.
3429
3527
  const NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml']);
3430
- function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3528
+ function computeAccessorMask(accessor, fieldBits, visited = new Set(), fieldBitsHi) {
3431
3529
  if (visited.has(accessor))
3432
- return { mask: 0, readsState: false };
3530
+ return { mask: 0, maskHi: 0, readsState: false };
3433
3531
  visited.add(accessor);
3434
3532
  if (accessor.parameters.length === 0)
3435
- return { mask: 0xffffffff | 0, readsState: false };
3533
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
3436
3534
  const paramName = accessor.parameters[0].name;
3437
3535
  if (!ts.isIdentifier(paramName))
3438
- return { mask: 0xffffffff | 0, readsState: false };
3536
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
3439
3537
  // FunctionDeclaration always has a body (we never resolve overloads here);
3440
3538
  // ArrowFunction's body may be a single expression. Both shapes are walked
3441
3539
  // identically by ts.forEachChild, so no special-casing is needed below.
3442
3540
  if (!accessor.body)
3443
- return { mask: 0xffffffff | 0, readsState: false };
3541
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
3444
3542
  const stateParam = paramName.text;
3445
3543
  let mask = 0;
3544
+ let maskHi = 0;
3446
3545
  let readsState = false;
3447
3546
  // `inNestedFn` gates only the delegation-recursion. Property-access
3448
3547
  // path extraction happens everywhere — inner-arrow callbacks like
@@ -3472,9 +3571,13 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3472
3571
  const chain = resolveChain(node, stateParam);
3473
3572
  if (chain) {
3474
3573
  const bit = fieldBits.get(chain);
3574
+ const bitHi = fieldBitsHi?.get(chain);
3475
3575
  if (bit !== undefined) {
3476
3576
  mask |= bit;
3477
3577
  }
3578
+ else if (bitHi !== undefined) {
3579
+ maskHi |= bitHi;
3580
+ }
3478
3581
  else {
3479
3582
  // Match paths that overlap our chain in either direction:
3480
3583
  // - `path` extends `chain` — fieldBits has finer-grained paths
@@ -3490,6 +3593,15 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3490
3593
  mask |= b;
3491
3594
  }
3492
3595
  }
3596
+ if (fieldBitsHi) {
3597
+ for (const [path, b] of fieldBitsHi) {
3598
+ if (path === chain ||
3599
+ path.startsWith(chain + '.') ||
3600
+ chain.startsWith(path + '.')) {
3601
+ maskHi |= b;
3602
+ }
3603
+ }
3604
+ }
3493
3605
  }
3494
3606
  }
3495
3607
  }
@@ -3506,8 +3618,9 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3506
3618
  if (arg0 && ts.isIdentifier(arg0) && arg0.text === stateParam) {
3507
3619
  const resolved = resolveAccessorBody(node.expression);
3508
3620
  if (resolved) {
3509
- const inner = computeAccessorMask(resolved, fieldBits, visited);
3621
+ const inner = computeAccessorMask(resolved, fieldBits, visited, fieldBitsHi);
3510
3622
  mask |= inner.mask;
3623
+ maskHi |= inner.maskHi;
3511
3624
  if (inner.readsState)
3512
3625
  readsState = true;
3513
3626
  }
@@ -3518,10 +3631,10 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3518
3631
  ts.forEachChild(node, (child) => walk(child, inNestedFn || enteringNested));
3519
3632
  }
3520
3633
  walk(accessor.body, false);
3521
- if (mask === 0 && readsState) {
3522
- return { mask: 0xffffffff | 0, readsState: true };
3634
+ if (mask === 0 && maskHi === 0 && readsState) {
3635
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: true };
3523
3636
  }
3524
- return { mask, readsState };
3637
+ return { mask, maskHi, readsState };
3525
3638
  }
3526
3639
  function resolveChain(node, paramName) {
3527
3640
  const parts = [];