octane 0.1.30 → 0.1.31

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.
@@ -3466,9 +3466,11 @@ function containsComponentCallOrControlFlow(stmts) {
3466
3466
  }
3467
3467
 
3468
3468
  /**
3469
- * A host-only conditional can share its keyed row's immutable item/dependency
3470
- * proof, but entering that conditional still needs the row's active scope. Keep
3471
- * components and every other control-flow boundary on their existing path.
3469
+ * A host-only conditional can share its containing row's immutable
3470
+ * item/dependency proof, including through a narrowly proven nested keyed list.
3471
+ * Entering either construct still needs the row's active scope. Lists without a
3472
+ * conditional, components, opaque list expressions, and every other
3473
+ * control-flow boundary remain on their existing path.
3472
3474
  */
3473
3475
  function hasOnlyHostConditionalItemBodies(stmts) {
3474
3476
  let hasConditional = false;
@@ -3497,6 +3499,28 @@ function hasOnlyHostConditionalItemBodies(stmts) {
3497
3499
  }
3498
3500
  if (type === 'IfStatement' || type === 'JSXIfExpression') {
3499
3501
  hasConditional = true;
3502
+ } else if (type === 'JSXForExpression') {
3503
+ const declaration = node.left;
3504
+ const item = declaration?.declarations?.[0]?.id;
3505
+ const key = unwrapTsExpr(node.key);
3506
+ if (
3507
+ node.await === true ||
3508
+ node.index != null ||
3509
+ node.nativeArrayMap !== undefined ||
3510
+ declaration?.type !== 'VariableDeclaration' ||
3511
+ declaration.declarations?.length !== 1 ||
3512
+ item?.type !== 'Identifier' ||
3513
+ !isAutoMemoCalculationDependency(node.right) ||
3514
+ key?.type !== 'MemberExpression' ||
3515
+ key.computed === true ||
3516
+ key.optional === true ||
3517
+ key.object?.type !== 'Identifier' ||
3518
+ key.object.name !== item.name ||
3519
+ key.property?.type !== 'Identifier'
3520
+ ) {
3521
+ disallowed = true;
3522
+ return;
3523
+ }
3500
3524
  } else if (
3501
3525
  type === 'ForStatement' ||
3502
3526
  type === 'ForInStatement' ||
@@ -3506,7 +3530,6 @@ function hasOnlyHostConditionalItemBodies(stmts) {
3506
3530
  type === 'TryStatement' ||
3507
3531
  type === 'SwitchStatement' ||
3508
3532
  type === 'ActivityStatement' ||
3509
- type === 'JSXForExpression' ||
3510
3533
  type === 'JSXTryExpression' ||
3511
3534
  type === 'JSXSwitchExpression' ||
3512
3535
  type === 'JSXActivityExpression' ||
@@ -4144,11 +4167,12 @@ function containsDeferredRefRead(root) {
4144
4167
  return found;
4145
4168
  }
4146
4169
 
4147
- // Both probes visit the identical node set, so one pass answers both. The ref
4148
- // probe loses its early exit; it finds nothing in 97% of components anyway.
4170
+ // Imported component tags rendered anywhere in the body — the auto-memo
4171
+ // region's runtime witnesses. (Deferred-ref reads are no longer collected
4172
+ // body-wide here: they gate per call site and per local through
4173
+ // collectAutoMemoLocalHazards.)
4149
4174
  function scanComponentBody(root, importedNames) {
4150
4175
  const importedComponents = new Set();
4151
- let readsDeferredRef = false;
4152
4176
  const seen = new WeakSet();
4153
4177
  function walk(node) {
4154
4178
  if (!node || typeof node !== 'object') return;
@@ -4158,7 +4182,6 @@ function scanComponentBody(root, importedNames) {
4158
4182
  }
4159
4183
  if (seen.has(node)) return;
4160
4184
  seen.add(node);
4161
- if (!readsDeferredRef && isDeferredRefRead(node)) readsDeferredRef = true;
4162
4185
  const imported = importedComponentTag(node, importedNames);
4163
4186
  if (imported !== null) importedComponents.add(imported);
4164
4187
  for (const key in node) {
@@ -4167,7 +4190,7 @@ function scanComponentBody(root, importedNames) {
4167
4190
  }
4168
4191
  }
4169
4192
  walk(root);
4170
- return { importedComponents, readsDeferredRef };
4193
+ return { importedComponents };
4171
4194
  }
4172
4195
 
4173
4196
  // A JSX member chain bottoms out at a JSXIdentifier, so the base test below
@@ -4203,9 +4226,137 @@ function containsImportedMemberRead(root, importedNames) {
4203
4226
  return found;
4204
4227
  }
4205
4228
 
4229
+ /**
4230
+ * Component locals whose values a region's dependency tuple cannot witness.
4231
+ *
4232
+ * A region dep on a LOCAL compares the local's per-render value. Locals
4233
+ * re-initialize on every body entry, so a plain value local — however it is
4234
+ * conditionally reassigned during render — is always an exact witness of
4235
+ * itself. What it CANNOT witness is a value drawn from mutable state whose
4236
+ * container identity hides the drift:
4237
+ *
4238
+ * - a ref read (`.current` / any computed member — `isDeferredRefRead`'s
4239
+ * definition): ref contents mutate outside render without scheduling one,
4240
+ * and a stable-identity closure (`useCallback(() => ref.current, [])`)
4241
+ * hands a skipped subtree a LIVE read whose changes nothing witnesses —
4242
+ * so the classification descends into nested function values;
4243
+ * - an imported binding's member read: a live import's property can move
4244
+ * while the import's identity (the only thing a dep can hold) stays
4245
+ * fixed.
4246
+ *
4247
+ * A name is tainted when such a read reaches its value through ANY feed —
4248
+ * declaration initializers at every nesting depth, destructuring patterns
4249
+ * that carry the read themselves (`const { current: el } = ref`, a default or
4250
+ * computed key reading a ref or live import), assignment right-hand sides, or
4251
+ * a for-of/for-in source — directly or through another tainted local (fixed
4252
+ * point, so declaration order and arrow-hoisted back-references cannot hide a
4253
+ * hazard). Nested and loop-scoped names are not component locals, so tainting
4254
+ * them is inert at call sites directly (unknown names already fail closed),
4255
+ * but it is what lets a hazard PROPAGATE through them into a top-level local
4256
+ * (`let out; for (const x of ref.current.items) out = x`). Shadowing is
4257
+ * folded by name — an inner binding's hazard taints its outer namesake —
4258
+ * which only ever declines more. Props params and same-module function
4259
+ * declarations never taint: the former are the witnessed snapshot itself, the
4260
+ * latter are immutable identities whose render-time CALLS the per-site
4261
+ * render-call gate already rejects.
4262
+ *
4263
+ * The direct forms of these reads inside a call site's own props are rejected
4264
+ * per site (containsAutoMemoUnsafeStructure / containsImportedMemberRead);
4265
+ * this set covers the same reads laundered through a local.
4266
+ */
4267
+ function collectAutoMemoLocalHazards(stmts, importedNames) {
4268
+ const hazards = new Set();
4269
+ // name -> union of free identifiers across its clean value sources; a name
4270
+ // with a directly hazardous source moves to `hazards` and leaves this map.
4271
+ const sourceFrees = new Map();
4272
+ const feed = (pattern, rhs) => {
4273
+ if (!rhs) return;
4274
+ const names = new Set();
4275
+ collectBindings(pattern, names);
4276
+ if (names.size === 0) return;
4277
+ // The pattern side can carry the read itself: a `current`/computed
4278
+ // binding key destructures ref contents off the source, and a default's
4279
+ // expression evaluates at destructure time. Both walks descend the whole
4280
+ // pattern, so nested shapes and defaults are covered together.
4281
+ const direct =
4282
+ containsDeferredRefRead(rhs) ||
4283
+ containsImportedMemberRead(rhs, importedNames) ||
4284
+ containsDeferredRefRead(pattern) ||
4285
+ containsImportedMemberRead(pattern, importedNames);
4286
+ // Default expressions inside the pattern read values too; their free
4287
+ // identifiers (which include the pattern's own bound names — inert
4288
+ // self-loops) join the clean-source union.
4289
+ const frees = direct
4290
+ ? null
4291
+ : [...collectFreeIdentifiers(rhs, []), ...collectFreeIdentifiers(pattern, [])];
4292
+ for (const name of names) {
4293
+ if (hazards.has(name)) continue;
4294
+ if (direct) {
4295
+ hazards.add(name);
4296
+ sourceFrees.delete(name);
4297
+ continue;
4298
+ }
4299
+ let set = sourceFrees.get(name);
4300
+ if (set === undefined) sourceFrees.set(name, (set = new Set()));
4301
+ for (const id of frees) set.add(id);
4302
+ }
4303
+ };
4304
+ // One walk feeds every binding form at every depth, including inside
4305
+ // nested function values: a deferred write targets a PREVIOUS render's
4306
+ // binding and can never leak into the next render's dep snapshot, but
4307
+ // walking uniformly is cheaper than proving which writes are deferred, and
4308
+ // only hazardous sources taint anyway.
4309
+ const seen = new WeakSet();
4310
+ (function walk(n) {
4311
+ if (!n) return;
4312
+ if (Array.isArray(n)) {
4313
+ for (const x of n) walk(x);
4314
+ return;
4315
+ }
4316
+ if (typeof n !== 'object' || !n.type || seen.has(n)) return;
4317
+ seen.add(n);
4318
+ if (n.type === 'VariableDeclaration') {
4319
+ for (const d of n.declarations || []) feed(d.id, d.init);
4320
+ } else if (n.type === 'AssignmentExpression' && n.left?.type !== 'MemberExpression') {
4321
+ feed(n.left, n.right);
4322
+ } else if ((n.type === 'ForOfStatement' || n.type === 'ForInStatement') && n.left) {
4323
+ // The loop source feeds the bound names whether the left declares
4324
+ // (`for (const x of src)` — declarator inits are null, so the
4325
+ // declaration arm above cannot see src) or reuses an outer binding.
4326
+ if (n.left.type === 'VariableDeclaration') {
4327
+ for (const d of n.left.declarations || []) feed(d.id, n.right);
4328
+ } else {
4329
+ feed(n.left, n.right);
4330
+ }
4331
+ }
4332
+ for (const key in n) {
4333
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4334
+ walk(n[key]);
4335
+ }
4336
+ })(stmts);
4337
+ let changed = true;
4338
+ while (changed) {
4339
+ changed = false;
4340
+ for (const [name, free] of sourceFrees) {
4341
+ for (const id of free) {
4342
+ if (!hazards.has(id)) continue;
4343
+ hazards.add(name);
4344
+ sourceFrees.delete(name);
4345
+ changed = true;
4346
+ break;
4347
+ }
4348
+ }
4349
+ }
4350
+ return hazards;
4351
+ }
4352
+
4206
4353
  function containsAutoMemoUnsafeStructure(stmts) {
4207
4354
  let found = false;
4208
4355
  const seen = new WeakSet();
4356
+ // Spread bags on HOST elements, marked admissible by the owning element's
4357
+ // visit below. Parents walk before their attributes, so membership is
4358
+ // decided before the spread node itself is reached.
4359
+ const hostSpreads = new WeakSet();
4209
4360
  function walk(n) {
4210
4361
  if (found || !n) return;
4211
4362
  if (Array.isArray(n)) {
@@ -4237,13 +4388,29 @@ function containsAutoMemoUnsafeStructure(stmts) {
4237
4388
  t === 'YieldExpression' ||
4238
4389
  t === 'ThrowStatement' ||
4239
4390
  t === 'TryStatement' ||
4240
- t === 'JSXTryExpression' ||
4241
- t === 'SpreadAttribute' ||
4242
- t === 'JSXSpreadAttribute'
4391
+ t === 'JSXTryExpression'
4243
4392
  ) {
4244
4393
  found = true;
4245
4394
  return;
4246
4395
  }
4396
+ if (t === 'SpreadAttribute' || t === 'JSXSpreadAttribute') {
4397
+ // A HOST element's spread bag is one setSpread binding re-diffed on every
4398
+ // region entry. Its argument is composed of deps the region guard already
4399
+ // witnesses (unwitnessed names fail closed independently), so a skip is
4400
+ // the no-op a re-entry would have been — the same immutable-snapshot
4401
+ // argument that admits `class={e.cls}` member reads; runtime keys the
4402
+ // walk cannot see (ref/on*/value) diff per entry with full parity to
4403
+ // their direct-attribute forms. A COMPONENT tag's spread instead builds
4404
+ // the child's props snapshot (getter evaluation, hidden prop names) and
4405
+ // stays rejected, matching the callSiteOk exclusion — which
4406
+ // isAutoMemoPropsParam's rest admission relies on. The argument still
4407
+ // walks below: accessors, computed members, and ref reads inside it keep
4408
+ // failing closed.
4409
+ if (!hostSpreads.has(n)) {
4410
+ found = true;
4411
+ return;
4412
+ }
4413
+ }
4247
4414
  if (t === 'UnaryExpression' && n.operator === 'delete') {
4248
4415
  found = true;
4249
4416
  return;
@@ -4311,6 +4478,13 @@ function containsAutoMemoUnsafeStructure(stmts) {
4311
4478
  found = true;
4312
4479
  return;
4313
4480
  }
4481
+ if (!isComponentTag(n)) {
4482
+ for (const attr of n.attributes || n.openingElement?.attributes || []) {
4483
+ if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
4484
+ hostSpreads.add(attr);
4485
+ }
4486
+ }
4487
+ }
4314
4488
  }
4315
4489
  for (const key in n) {
4316
4490
  if (AST_WALK_SKIP_KEYS.has(key)) continue;
@@ -4321,6 +4495,54 @@ function containsAutoMemoUnsafeStructure(stmts) {
4321
4495
  return found;
4322
4496
  }
4323
4497
 
4498
+ /**
4499
+ * Can this props parameter stand in for autoMemo's "one ordinary props
4500
+ * snapshot"? A bare Identifier always can. A destructuring ObjectPattern can
4501
+ * when destructuring it evaluates NO expressions and reads NOTHING mutable:
4502
+ * every binding it introduces is already a component local
4503
+ * (collectComponentLocals walks patterns), so the body's free-identifier and
4504
+ * capture analysis is complete for it — the pattern itself is the only part
4505
+ * those walks never see, because they take the body root, not the params.
4506
+ *
4507
+ * Fail closed on exactly the shapes whose evaluation escapes that analysis or
4508
+ * reads outside the snapshot:
4509
+ * - defaults (`{ a = expr }`): `expr` evaluates at destructure time, and its
4510
+ * reads are invisible to the body walks;
4511
+ * - computed keys (`{ [k]: v }`): same, for the key expression;
4512
+ * - a `current` binding at any depth: the same mutable-ref hazard the body
4513
+ * walk rejects for ObjectPattern declarations (a ref's identity is not a
4514
+ * complete witness for its contents);
4515
+ * - array patterns: destructuring runs the iterator protocol, which is
4516
+ * arbitrary user code on a non-Array prop;
4517
+ * - rest (`...rest`) with anything but a plain Identifier target.
4518
+ * Rest itself is admitted: object rest is an engine-level own-property copy of
4519
+ * the same snapshot, and spread-bearing call sites never receive a region
4520
+ * anyway (callSiteOk excludes them), so a getter-bearing props object cannot
4521
+ * reach a cached region.
4522
+ */
4523
+ function isAutoMemoPropsParam(param) {
4524
+ if (param == null) return false;
4525
+ if (param.type === 'Identifier') return true;
4526
+ if (param.type !== 'ObjectPattern') return false;
4527
+ const admissible = (pattern) => {
4528
+ if (pattern == null || pattern.type !== 'ObjectPattern') return false;
4529
+ for (const p of pattern.properties || []) {
4530
+ if (p.type === 'RestElement') {
4531
+ if (p.argument?.type !== 'Identifier') return false;
4532
+ continue;
4533
+ }
4534
+ if (p.type !== 'Property' || p.computed) return false;
4535
+ const key = p.key?.name ?? p.key?.value;
4536
+ if (key === 'current') return false;
4537
+ const value = p.value;
4538
+ if (value?.type === 'Identifier') continue;
4539
+ if (!admissible(value)) return false;
4540
+ }
4541
+ return true;
4542
+ };
4543
+ return admissible(param);
4544
+ }
4545
+
4324
4546
  /**
4325
4547
  * Bundling MOVES an argument's evaluation out of the arrow body and into the
4326
4548
  * component body, so it now runs on mount and on every update instead of once
@@ -6953,6 +7175,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6953
7175
  eligible: false,
6954
7176
  autoMemoSafe: false,
6955
7177
  autoMemoCallsitesSafe: true,
7178
+ autoMemoLocalHazards: null,
6956
7179
  autoMemoCaptures: [],
6957
7180
  autoMemoComponentDeps: [],
6958
7181
  autoMemoImportedComponents: [],
@@ -6985,16 +7208,26 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6985
7208
  if (compNode.body.render) stmts.push(compNode.body.render);
6986
7209
  const root = b.block(stmts);
6987
7210
  const free = collectFreeIdentifiers(root, locals);
6988
- const { importedComponents: autoMemoImportedComponents, readsDeferredRef } = scanComponentBody(
7211
+ const { importedComponents: autoMemoImportedComponents } = scanComponentBody(
6989
7212
  root,
6990
7213
  ctx.importedNames,
6991
7214
  );
6992
- // Both proofs below ask this of the same root. Kept lazy because the
6993
- // short-circuits ahead of each use skip it entirely for some components.
7215
+ // The callee-side proof below asks this of the same root. Kept lazy because
7216
+ // the short-circuits ahead of its use skip it entirely for some components.
6994
7217
  let importedMemberRead = null;
6995
7218
  const readsImportedMember = () =>
6996
7219
  (importedMemberRead ??= containsImportedMemberRead(root, ctx.importedNames));
6997
- let autoMemoCallsitesSafe = !readsDeferredRef && !readsImportedMember();
7220
+ // Call-site regions inside this body are gated per site: the site's own
7221
+ // props are walked directly (containsAutoMemoUnsafeStructure /
7222
+ // containsImportedMemberRead over the JSX node), and ref/imported-member
7223
+ // reads laundered through a body local are carried by this hazard set —
7224
+ // a site whose free identifiers touch a tainted local falls back. The
7225
+ // free-name loop below still vetoes the whole body for names no dep can
7226
+ // witness at all (boundary wrappers, namespaces, ambient module state).
7227
+ const autoMemoLocalHazards = ctx.autoMemo
7228
+ ? collectAutoMemoLocalHazards(stmts, ctx.importedNames)
7229
+ : null;
7230
+ let autoMemoCallsitesSafe = true;
6998
7231
  for (const name of free) {
6999
7232
  if (
7000
7233
  ctx._octaneBoundaryNames.has(name) ||
@@ -7008,13 +7241,14 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
7008
7241
  break;
7009
7242
  }
7010
7243
  }
7011
- // autoMemo's first proof is intentionally narrower than lite eligibility.
7012
- // It models a pure component as a function of one ordinary props snapshot;
7013
- // destructuring/default/rest parameters can be admitted once their evaluation
7014
- // is included in the shared dependency/purity analysis.
7244
+ // autoMemo models a pure component as a function of one ordinary props
7245
+ // snapshot. A destructuring param is that same snapshot read once at
7246
+ // entry, admitted only while the pattern evaluates no expressions of its
7247
+ // own — see isAutoMemoPropsParam for the exact fail-closed shapes
7248
+ // (defaults, computed keys, `current`, array patterns).
7015
7249
  const ordinaryPropsParam =
7016
7250
  (compNode.params?.length ?? 0) <= 1 &&
7017
- (compNode.params?.length !== 1 || compNode.params[0]?.type === 'Identifier');
7251
+ (compNode.params?.length !== 1 || isAutoMemoPropsParam(compNode.params[0]));
7018
7252
  let autoMemoSafe =
7019
7253
  ordinaryPropsParam &&
7020
7254
  !containsRenderCall(stmts, ctx) &&
@@ -7052,6 +7286,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
7052
7286
  }
7053
7287
  info.autoMemoSafe = autoMemoSafe;
7054
7288
  info.autoMemoCallsitesSafe = autoMemoCallsitesSafe;
7289
+ info.autoMemoLocalHazards = autoMemoLocalHazards;
7055
7290
  info.autoMemoCaptures = autoMemoSafe ? autoMemoCaptures.sort() : [];
7056
7291
  info.autoMemoComponentDeps = autoMemoSafe ? autoMemoComponentDeps.sort() : [];
7057
7292
  info.autoMemoImportedComponents = autoMemoSafe ? [...autoMemoImportedComponents].sort() : [];
@@ -10854,11 +11089,13 @@ function compileComponent(node, ctx, options) {
10854
11089
  // locals.
10855
11090
  const prevLocals = ctx.currentComponentLocals;
10856
11091
  const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
11092
+ const prevAutoMemoLocalHazards = ctx.currentAutoMemoLocalHazards;
10857
11093
  const prevKnownStr = ctx.knownStringLocals;
10858
11094
  const prevProfileComponentId = ctx.currentProfileComponentId;
10859
11095
  const previousComponentOwner = ctx.currentComponentOwner;
10860
11096
  ctx.currentComponentLocals = collectComponentLocals(node);
10861
11097
  ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
11098
+ ctx.currentAutoMemoLocalHazards = ctx.componentInfo.get(name)?.autoMemoLocalHazards ?? null;
10862
11099
  ctx.knownStringLocals = collectKnownStringLocals(node);
10863
11100
  ctx.currentComponentOwner = owner;
10864
11101
  if (ctx.profile) ctx.currentProfileComponentId = profileComponentId(ctx, name, node);
@@ -10876,6 +11113,7 @@ function compileComponent(node, ctx, options) {
10876
11113
  } finally {
10877
11114
  ctx.currentComponentLocals = prevLocals;
10878
11115
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
11116
+ ctx.currentAutoMemoLocalHazards = prevAutoMemoLocalHazards;
10879
11117
  ctx.knownStringLocals = prevKnownStr;
10880
11118
  ctx.currentProfileComponentId = prevProfileComponentId;
10881
11119
  ctx.currentComponentOwner = previousComponentOwner;
@@ -14243,11 +14481,13 @@ function compileReturnJsxFunction(node, ctx, options) {
14243
14481
  const prevValueDirectiveLowering = ctx._valueDirectiveLowering;
14244
14482
  const prevLocals = ctx.currentComponentLocals;
14245
14483
  const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
14484
+ const prevAutoMemoLocalHazards = ctx.currentAutoMemoLocalHazards;
14246
14485
  const prevMapTemps = ctx.currentMapTemps;
14247
14486
  const mapTemps = [];
14248
14487
  ctx._valueDirectiveLowering = lowerBodyValueDirective;
14249
14488
  ctx.currentComponentLocals = collectComponentLocals(node);
14250
14489
  ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
14490
+ ctx.currentAutoMemoLocalHazards = ctx.componentInfo.get(name)?.autoMemoLocalHazards ?? null;
14251
14491
  ctx.currentMapTemps = mapTemps;
14252
14492
  let newStatements;
14253
14493
  try {
@@ -14295,6 +14535,7 @@ function compileReturnJsxFunction(node, ctx, options) {
14295
14535
  ctx._valueDirectiveLowering = prevValueDirectiveLowering;
14296
14536
  ctx.currentComponentLocals = prevLocals;
14297
14537
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
14538
+ ctx.currentAutoMemoLocalHazards = prevAutoMemoLocalHazards;
14298
14539
  ctx.currentMapTemps = prevMapTemps;
14299
14540
  }
14300
14541
  if (
@@ -21898,7 +22139,13 @@ function makeCompCall(
21898
22139
  }
21899
22140
  for (const name of free) {
21900
22141
  if (name === compName) continue;
21901
- if (ctx.importNamespaceNames.has(name)) {
22142
+ if (
22143
+ ctx.importNamespaceNames.has(name) ||
22144
+ // A local laundering a ref or live-import member read (see
22145
+ // collectAutoMemoLocalHazards) — its per-render value is not a
22146
+ // complete witness, so the site keeps ordinary entry semantics.
22147
+ ctx.currentAutoMemoLocalHazards?.has(name) === true
22148
+ ) {
21902
22149
  depsSafe = false;
21903
22150
  } else if (ctx.currentComponentLocals.has(name) || ctx.importedNames.has(name)) {
21904
22151
  if (!callsiteDeps.coveredRoots.has(name)) deps.add(name);
@@ -22570,7 +22817,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
22570
22817
  // survivor shortcut has no compiler-cache epoch cell to consult).
22571
22818
  if (itemMemoContextAware && autoMemoDeps === null) itemMemo = false;
22572
22819
  const hostPure = !hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
22573
- const conditionalHostDepEligible =
22820
+ const structuredHostDepEligible =
22574
22821
  ctx.autoMemo === true &&
22575
22822
  hasNestedComp &&
22576
22823
  hasParentClosure &&
@@ -22578,14 +22825,24 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
22578
22825
  !hasRenderCall &&
22579
22826
  node.nativeArrayMap === undefined &&
22580
22827
  autoMemoDeps !== null &&
22581
- autoMemoDeps.every((name) => seenDeps.has(name)) &&
22828
+ autoMemoDeps.every((name) => seenDeps.has(name) || ctx.importedNames.has(name)) &&
22582
22829
  !containsAutoMemoContextRead(bodyAst, ctx) &&
22583
22830
  hasOnlyHostConditionalItemBodies(subStmts);
22831
+ if (structuredHostDepEligible) {
22832
+ // The whole-list cache already witnesses imported projections. A row
22833
+ // survivor now makes the same decision independently, so its existing
22834
+ // dependency tuple must witness those live bindings as well.
22835
+ for (const name of autoMemoDeps) {
22836
+ if (seenDeps.has(name)) continue;
22837
+ seenDeps.add(name);
22838
+ depNames.push(name);
22839
+ }
22840
+ }
22584
22841
  const hostDepEligible =
22585
22842
  !hostPure &&
22586
22843
  !hasHook &&
22587
22844
  hasParentClosure &&
22588
- (!hasNestedComp || conditionalHostDepEligible) &&
22845
+ (!hasNestedComp || structuredHostDepEligible) &&
22589
22846
  !hasRenderCall;
22590
22847
  if (itemMemo && itemMemoWitnesses.length > 0) {
22591
22848
  pure = hostPure;
@@ -22595,7 +22852,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
22595
22852
  pure = hostPure || (itemMemo && depNames.length === 0);
22596
22853
  depEligible = !pure && !hasHook && (hostDepEligible || (itemMemo && depNames.length > 0));
22597
22854
  }
22598
- requiresScope = depEligible && conditionalHostDepEligible;
22855
+ requiresScope = depEligible && structuredHostDepEligible;
22599
22856
  depNames.sort();
22600
22857
  }
22601
22858
 
package/dist/runtime.js CHANGED
@@ -2155,6 +2155,7 @@ function renderBlockInner(block) {
2155
2155
  if (typeof __OCTANE_PROFILE_ENABLED__ !== "undefined" && __OCTANE_PROFILE_ENABLED__ && profileFrame !== null)
2156
2156
  __profileEndRender(profileFrame, profileDidThrow, profileThrown);
2157
2157
  if (!renderCompleted) {
2158
+ if (block.forSlot !== null) block.forSlot.cachedDeps = null;
2158
2159
  effectEventTarget.length = effectEventCheckpoint;
2159
2160
  effectEventActionTarget.length = effectEventActionCheckpoint;
2160
2161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octane",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": [