Skip to content

Commit 5e4fbea

Browse files
committed
fix: share guarded values across control blocks
1 parent 30fb52e commit 5e4fbea

File tree

4 files changed

+120
-12
lines changed

4 files changed

+120
-12
lines changed

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface ClientTransformState extends TransformState {
3737
update?: (node: UpdateExpression) => Expression;
3838
}
3939
>;
40+
41+
readonly guard_snapshots?: Map<string, { readonly id: Identifier }>;
42+
43+
readonly collect_guard_snapshots?: Map<string, { readonly id: Identifier }>;
4044
}
4145

4246
export interface ComponentClientTransformState extends ClientTransformState {

packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ export function ConstTag(node, context) {
2626

2727
context.state.consts.push(b.const(declaration.id, expression));
2828

29-
context.state.transform[declaration.id.name] = { read: get_value };
29+
const snapshot = context.state.guard_snapshots?.get(declaration.id.name);
30+
context.state.transform[declaration.id.name] = snapshot
31+
? {
32+
read() {
33+
return snapshot.id;
34+
}
35+
}
36+
: { read: get_value };
3037

3138
// we need to eagerly evaluate the expression in order to hit any
3239
// 'Cannot access x before initialization' errors
@@ -78,9 +85,16 @@ export function ConstTag(node, context) {
7885
}
7986

8087
for (const node of identifiers) {
81-
context.state.transform[node.name] = {
82-
read: (node) => b.member(b.call('$.get', tmp), node)
83-
};
88+
const snapshot = context.state.guard_snapshots?.get(node.name);
89+
context.state.transform[node.name] = snapshot
90+
? {
91+
read() {
92+
return snapshot.id;
93+
}
94+
}
95+
: {
96+
read: (node) => b.member(b.call('$.get', tmp), node)
97+
};
8498
}
8599
}
86100
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,59 @@ export function IfBlock(node, context) {
1313
const statements = [];
1414

1515
const { has_await } = node.metadata.expression;
16-
const expression = build_expression(context, node.test, node.metadata.expression);
16+
const guard_snapshots = new Map();
17+
const expression = build_expression(context, node.test, node.metadata.expression, {
18+
...context.state,
19+
collect_guard_snapshots: guard_snapshots
20+
});
1721
const test = has_await ? b.call('$.get', b.id('$$condition')) : expression;
1822

19-
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
23+
let branch_state = context.state;
24+
25+
if (guard_snapshots.size > 0) {
26+
const snapshots = new Map(context.state.guard_snapshots ?? undefined);
27+
const transform = { ...context.state.transform };
28+
29+
for (const [name, snapshot] of guard_snapshots) {
30+
snapshots.set(name, snapshot);
31+
32+
const base = transform[name] ?? context.state.transform[name];
33+
transform[name] = {
34+
...base,
35+
read() {
36+
return snapshot.id;
37+
},
38+
assign: base?.assign,
39+
mutate: base?.mutate,
40+
update: base?.update
41+
};
42+
}
43+
44+
branch_state = {
45+
...context.state,
46+
collect_guard_snapshots: undefined,
47+
guard_snapshots: snapshots,
48+
transform
49+
};
50+
}
51+
52+
const consequent = /** @type {BlockStatement} */ (
53+
branch_state === context.state
54+
? context.visit(node.consequent)
55+
: context.visit(node.consequent, branch_state)
56+
);
2057
const consequent_id = b.id(context.state.scope.generate('consequent'));
2158

2259
statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent)));
2360

2461
let alternate_id;
2562

2663
if (node.alternate) {
27-
const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate));
64+
const alternate = /** @type {BlockStatement} */ (
65+
branch_state === context.state
66+
? context.visit(node.alternate)
67+
: context.visit(node.alternate, branch_state)
68+
);
2869
alternate_id = b.id(context.state.scope.generate('alternate'));
2970
statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate)));
3071
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ function should_memoize_binding(binding) {
4343
return false;
4444
}
4545
}
46+
47+
/**
48+
* @param {Binding | null} binding
49+
* @returns {boolean}
50+
*/
51+
function should_snapshot_guard(binding) {
52+
if (binding === null) return false;
53+
54+
switch (binding.kind) {
55+
case 'derived':
56+
case 'legacy_reactive':
57+
case 'template':
58+
return true;
59+
default:
60+
return false;
61+
}
62+
}
4663
/** @type {WeakMap<any, Map<string, { id: Identifier; getter: (node: Identifier) => Expression }>>} */
4764
const memoized_reads_by_scope = new WeakMap();
4865

@@ -459,14 +476,18 @@ export function build_expression(context, expression, metadata, state = context.
459476

460477
memoized_ids = new Set();
461478

479+
const guard_snapshots = state.collect_guard_snapshots;
480+
462481
for (const [binding, count] of counts) {
463-
if (count <= 1) continue;
464482
const name = binding.node?.name;
465483
if (!name) continue;
466484

467485
const original = state.transform[name];
468486
if (!original?.read) continue;
469487

488+
const capture_for_guard = Boolean(guard_snapshots && should_snapshot_guard(binding));
489+
if (count <= 1 && !capture_for_guard) continue;
490+
470491
let scope_records = memoized_reads_by_scope.get(binding.scope);
471492
if (!scope_records) {
472493
scope_records = new Map();
@@ -505,17 +526,45 @@ export function build_expression(context, expression, metadata, state = context.
505526
};
506527

507528
assignments.push(b.stmt(b.assignment('=', record.id, record.getter(b.id(name)))));
529+
530+
if (guard_snapshots && capture_for_guard) {
531+
guard_snapshots.set(name, { id: record.id });
532+
}
508533
}
509534
}
510535

511536
let value = /** @type {Expression} */ (context.visit(expression, child_state));
512537

513-
if (memoized_ids !== null && memoized_ids.size > 0) {
514-
const memoized = memoized_ids;
538+
const optional_sources = new Set();
539+
540+
if (memoized_ids !== null) {
541+
for (const name of memoized_ids) optional_sources.add(name);
542+
}
543+
544+
if (state.guard_snapshots) {
545+
for (const snapshot of state.guard_snapshots.values()) {
546+
optional_sources.add(snapshot.id.name);
547+
}
548+
}
549+
550+
if (optional_sources.size > 0) {
515551
walk(value, null, {
516552
MemberExpression(node) {
517-
if (node.object.type === 'Identifier' && memoized.has(node.object.name)) {
518-
node.optional = true;
553+
let root = node.object;
554+
while (root && root.type === 'MemberExpression') {
555+
root = root.object;
556+
}
557+
558+
if (root?.type === 'Identifier' && optional_sources.has(root.name)) {
559+
/** @type {import('estree').MemberExpression | null} */
560+
let current = node;
561+
while (current) {
562+
current.optional = true;
563+
const next = /** @type {import('estree').Expression | import('estree').Super} */ (
564+
current.object
565+
);
566+
current = next.type === 'MemberExpression' ? next : null;
567+
}
519568
}
520569
}
521570
});

0 commit comments

Comments
 (0)