@llui/vite-plugin 0.0.42 → 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/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))
@@ -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
- // 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];
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
- caseDirty |= topLevelBits.get(field) ?? 0xffffffff | 0;
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 (topLevelBits.has(fieldName)) {
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 (topLevelBits.has(fieldName)) {
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
- 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, [
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) === 0) continue;
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.EqualsEqualsEqualsToken, f.createNumericLiteral(0))), f.createContinueStatement()),
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
- bindings.push([mask === 0 && readsState ? 0xffffffff | 0 : mask, kind, resolvedKey, value]);
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
- stmts.push(f.createExpressionStatement(f.createCallExpression(f.createIdentifier('__bind'), undefined, [
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
- // 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, [
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
@@ -3427,22 +3570,23 @@ function isHoistedPerItem(node) {
3427
3570
  // See `NON_DELEGATION_HELPERS` in collect-deps.ts — same set of names
3428
3571
  // that aren't followed when scanning for `helper(s)` delegation calls.
3429
3572
  const NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml']);
3430
- function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3573
+ function computeAccessorMask(accessor, fieldBits, visited = new Set(), fieldBitsHi) {
3431
3574
  if (visited.has(accessor))
3432
- return { mask: 0, readsState: false };
3575
+ return { mask: 0, maskHi: 0, readsState: false };
3433
3576
  visited.add(accessor);
3434
3577
  if (accessor.parameters.length === 0)
3435
- return { mask: 0xffffffff | 0, readsState: false };
3578
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
3436
3579
  const paramName = accessor.parameters[0].name;
3437
3580
  if (!ts.isIdentifier(paramName))
3438
- return { mask: 0xffffffff | 0, readsState: false };
3581
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
3439
3582
  // FunctionDeclaration always has a body (we never resolve overloads here);
3440
3583
  // ArrowFunction's body may be a single expression. Both shapes are walked
3441
3584
  // identically by ts.forEachChild, so no special-casing is needed below.
3442
3585
  if (!accessor.body)
3443
- return { mask: 0xffffffff | 0, readsState: false };
3586
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: false };
3444
3587
  const stateParam = paramName.text;
3445
3588
  let mask = 0;
3589
+ let maskHi = 0;
3446
3590
  let readsState = false;
3447
3591
  // `inNestedFn` gates only the delegation-recursion. Property-access
3448
3592
  // path extraction happens everywhere — inner-arrow callbacks like
@@ -3472,9 +3616,13 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3472
3616
  const chain = resolveChain(node, stateParam);
3473
3617
  if (chain) {
3474
3618
  const bit = fieldBits.get(chain);
3619
+ const bitHi = fieldBitsHi?.get(chain);
3475
3620
  if (bit !== undefined) {
3476
3621
  mask |= bit;
3477
3622
  }
3623
+ else if (bitHi !== undefined) {
3624
+ maskHi |= bitHi;
3625
+ }
3478
3626
  else {
3479
3627
  // Match paths that overlap our chain in either direction:
3480
3628
  // - `path` extends `chain` — fieldBits has finer-grained paths
@@ -3490,6 +3638,15 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3490
3638
  mask |= b;
3491
3639
  }
3492
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
+ }
3493
3650
  }
3494
3651
  }
3495
3652
  }
@@ -3506,8 +3663,9 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3506
3663
  if (arg0 && ts.isIdentifier(arg0) && arg0.text === stateParam) {
3507
3664
  const resolved = resolveAccessorBody(node.expression);
3508
3665
  if (resolved) {
3509
- const inner = computeAccessorMask(resolved, fieldBits, visited);
3666
+ const inner = computeAccessorMask(resolved, fieldBits, visited, fieldBitsHi);
3510
3667
  mask |= inner.mask;
3668
+ maskHi |= inner.maskHi;
3511
3669
  if (inner.readsState)
3512
3670
  readsState = true;
3513
3671
  }
@@ -3518,10 +3676,10 @@ function computeAccessorMask(accessor, fieldBits, visited = new Set()) {
3518
3676
  ts.forEachChild(node, (child) => walk(child, inNestedFn || enteringNested));
3519
3677
  }
3520
3678
  walk(accessor.body, false);
3521
- if (mask === 0 && readsState) {
3522
- return { mask: 0xffffffff | 0, readsState: true };
3679
+ if (mask === 0 && maskHi === 0 && readsState) {
3680
+ return { mask: 0xffffffff | 0, maskHi: 0, readsState: true };
3523
3681
  }
3524
- return { mask, readsState };
3682
+ return { mask, maskHi, readsState };
3525
3683
  }
3526
3684
  function resolveChain(node, paramName) {
3527
3685
  const parts = [];