Skip to content

Commit d427ffd

Browse files
authored
chore: encapsulate expression memoization (#16269)
* chore: encapsulate expression memoization * add comment * tweak * use b.id
1 parent eb530c8 commit d427ffd

File tree

7 files changed

+99
-70
lines changed

7 files changed

+99
-70
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,9 @@ export function client_component(analysis, options) {
168168
// these are set inside the `Fragment` visitor, and cannot be used until then
169169
init: /** @type {any} */ (null),
170170
update: /** @type {any} */ (null),
171-
expressions: /** @type {any} */ (null),
172171
after_update: /** @type {any} */ (null),
173-
template: /** @type {any} */ (null)
172+
template: /** @type {any} */ (null),
173+
memoizer: /** @type {any} */ (null)
174174
};
175175

176176
const module = /** @type {ESTree.Program} */ (

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
1212
import type { TransformState } from '../types.js';
1313
import type { ComponentAnalysis } from '../../types.js';
1414
import type { Template } from './transform-template/template.js';
15+
import type { Memoizer } from './visitors/shared/utils.js';
1516

1617
export interface ClientTransformState extends TransformState {
1718
/**
@@ -49,8 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState {
4950
readonly update: Statement[];
5051
/** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */
5152
readonly after_update: Statement[];
52-
/** Expressions used inside the render effect */
53-
readonly expressions: Expression[];
53+
/** Memoized expressions */
54+
readonly memoizer: Memoizer;
5455
/** The HTML template string */
5556
readonly template: Template;
5657
readonly metadata: {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as b from '#compiler/builders';
66
import { clean_nodes, infer_namespace } from '../../utils.js';
77
import { transform_template } from '../transform-template/index.js';
88
import { process_children } from './shared/fragment.js';
9-
import { build_render_statement } from './shared/utils.js';
9+
import { build_render_statement, Memoizer } from './shared/utils.js';
1010
import { Template } from '../transform-template/template.js';
1111

1212
/**
@@ -64,8 +64,8 @@ export function Fragment(node, context) {
6464
...context.state,
6565
init: [],
6666
update: [],
67-
expressions: [],
6867
after_update: [],
68+
memoizer: new Memoizer(),
6969
template: new Template(),
7070
transform: { ...context.state.transform },
7171
metadata: {

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

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
build_set_style
2323
} from './shared/element.js';
2424
import { process_children } from './shared/fragment.js';
25-
import { build_render_statement, build_template_chunk, get_expression_id } from './shared/utils.js';
25+
import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js';
2626
import { visit_event_attribute } from './shared/events.js';
2727

2828
/**
@@ -253,8 +253,7 @@ export function RegularElement(node, context) {
253253
const { value, has_state } = build_attribute_value(
254254
attribute.value,
255255
context,
256-
(value, metadata) =>
257-
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
256+
(value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value)
258257
);
259258

260259
const update = build_element_attribute_update(node, node_id, name, value, attributes);
@@ -455,11 +454,15 @@ function setup_select_synchronization(value_binding, context) {
455454

456455
/**
457456
* @param {AST.ClassDirective[]} class_directives
458-
* @param {Expression[]} expressions
459457
* @param {ComponentContext} context
458+
* @param {Memoizer} memoizer
460459
* @return {ObjectExpression | Identifier}
461460
*/
462-
export function build_class_directives_object(class_directives, expressions, context) {
461+
export function build_class_directives_object(
462+
class_directives,
463+
context,
464+
memoizer = context.state.memoizer
465+
) {
463466
let properties = [];
464467
let has_call_or_state = false;
465468

@@ -471,38 +474,40 @@ export function build_class_directives_object(class_directives, expressions, con
471474

472475
const directives = b.object(properties);
473476

474-
return has_call_or_state ? get_expression_id(expressions, directives) : directives;
477+
return has_call_or_state ? memoizer.add(directives) : directives;
475478
}
476479

477480
/**
478481
* @param {AST.StyleDirective[]} style_directives
479-
* @param {Expression[]} expressions
480482
* @param {ComponentContext} context
481-
* @return {ObjectExpression | ArrayExpression}}
483+
* @param {Memoizer} memoizer
484+
* @return {ObjectExpression | ArrayExpression | Identifier}}
482485
*/
483-
export function build_style_directives_object(style_directives, expressions, context) {
484-
let normal_properties = [];
485-
let important_properties = [];
486+
export function build_style_directives_object(
487+
style_directives,
488+
context,
489+
memoizer = context.state.memoizer
490+
) {
491+
const normal = b.object([]);
492+
const important = b.object([]);
486493

487-
for (const directive of style_directives) {
494+
let has_call_or_state = false;
495+
496+
for (const d of style_directives) {
488497
const expression =
489-
directive.value === true
490-
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
491-
: build_attribute_value(directive.value, context, (value, metadata) =>
492-
metadata.has_call ? get_expression_id(expressions, value) : value
493-
).value;
494-
const property = b.init(directive.name, expression);
495-
496-
if (directive.modifiers.includes('important')) {
497-
important_properties.push(property);
498-
} else {
499-
normal_properties.push(property);
500-
}
498+
d.value === true
499+
? build_getter(b.id(d.name), context.state)
500+
: build_attribute_value(d.value, context).value;
501+
502+
const object = d.modifiers.includes('important') ? important : normal;
503+
object.properties.push(b.init(d.name, expression));
504+
505+
has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state;
501506
}
502507

503-
return important_properties.length
504-
? b.array([b.object(normal_properties), b.object(important_properties)])
505-
: b.object(normal_properties);
508+
const directives = important.properties.length ? b.array([normal, important]) : normal;
509+
510+
return has_call_or_state ? memoizer.add(directives) : directives;
506511
}
507512

508513
/**
@@ -624,7 +629,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
624629
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
625630

626631
const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
627-
metadata.has_call ? get_expression_id(state.expressions, value) : value
632+
metadata.has_call ? state.memoizer.add(value) : value
628633
);
629634

630635
const evaluated = context.state.scope.evaluate(value);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
build_attribute_effect,
1111
build_set_class
1212
} from './shared/element.js';
13-
import { build_render_statement } from './shared/utils.js';
13+
import { build_render_statement, Memoizer } from './shared/utils.js';
1414

1515
/**
1616
* @param {AST.SvelteElement} node
@@ -46,8 +46,8 @@ export function SvelteElement(node, context) {
4646
node: element_id,
4747
init: [],
4848
update: [],
49-
expressions: [],
50-
after_update: []
49+
after_update: [],
50+
memoizer: new Memoizer()
5151
}
5252
};
5353

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

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js';
77
import { is_event_attribute } from '../../../../../utils/ast.js';
88
import * as b from '#compiler/builders';
99
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
10-
import { build_expression, build_template_chunk, get_expression_id } from './utils.js';
10+
import { build_expression, build_template_chunk, Memoizer } from './utils.js';
1111

1212
/**
1313
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@@ -28,18 +28,12 @@ export function build_attribute_effect(
2828
/** @type {ObjectExpression['properties']} */
2929
const values = [];
3030

31-
/** @type {Expression[]} */
32-
const expressions = [];
33-
34-
/** @param {Expression} value */
35-
function memoize(value) {
36-
return b.id(`$${expressions.push(value) - 1}`);
37-
}
31+
const memoizer = new Memoizer();
3832

3933
for (const attribute of attributes) {
4034
if (attribute.type === 'Attribute') {
4135
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
42-
metadata.has_call ? memoize(value) : value
36+
metadata.has_call ? memoizer.add(value) : value
4337
);
4438

4539
if (
@@ -57,7 +51,7 @@ export function build_attribute_effect(
5751
let value = /** @type {Expression} */ (context.visit(attribute));
5852

5953
if (attribute.metadata.expression.has_call) {
60-
value = memoize(value);
54+
value = memoizer.add(value);
6155
}
6256

6357
values.push(b.spread(value));
@@ -69,7 +63,7 @@ export function build_attribute_effect(
6963
b.prop(
7064
'init',
7165
b.array([b.id('$.CLASS')]),
72-
build_class_directives_object(class_directives, expressions, context)
66+
build_class_directives_object(class_directives, context, memoizer)
7367
)
7468
);
7569
}
@@ -79,21 +73,20 @@ export function build_attribute_effect(
7973
b.prop(
8074
'init',
8175
b.array([b.id('$.STYLE')]),
82-
build_style_directives_object(style_directives, expressions, context)
76+
build_style_directives_object(style_directives, context, memoizer)
8377
)
8478
);
8579
}
8680

81+
const ids = memoizer.apply();
82+
8783
context.state.init.push(
8884
b.stmt(
8985
b.call(
9086
'$.attribute_effect',
9187
element_id,
92-
b.arrow(
93-
expressions.map((_, i) => b.id(`$${i}`)),
94-
b.object(values)
95-
),
96-
expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))),
88+
b.arrow(ids, b.object(values)),
89+
memoizer.sync_values(),
9790
element.metadata.scoped &&
9891
context.state.analysis.css.hash !== '' &&
9992
b.literal(context.state.analysis.css.hash),
@@ -158,7 +151,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
158151
value = b.call('$.clsx', value);
159152
}
160153

161-
return metadata.has_call ? get_expression_id(context.state.expressions, value) : value;
154+
return metadata.has_call ? context.state.memoizer.add(value) : value;
162155
});
163156

164157
/** @type {Identifier | undefined} */
@@ -171,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
171164
let next;
172165

173166
if (class_directives.length) {
174-
next = build_class_directives_object(class_directives, context.state.expressions, context);
167+
next = build_class_directives_object(class_directives, context);
175168
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
176169

177170
if (has_state) {
@@ -226,7 +219,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c
226219
*/
227220
export function build_set_style(node_id, attribute, style_directives, context) {
228221
let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
229-
metadata.has_call ? get_expression_id(context.state.expressions, value) : value
222+
metadata.has_call ? context.state.memoizer.add(value) : value
230223
);
231224

232225
/** @type {Identifier | undefined} */
@@ -235,11 +228,11 @@ export function build_set_style(node_id, attribute, style_directives, context) {
235228
/** @type {ObjectExpression | Identifier | undefined} */
236229
let prev;
237230

238-
/** @type {ArrayExpression | ObjectExpression | undefined} */
231+
/** @type {Expression | undefined} */
239232
let next;
240233

241234
if (style_directives.length) {
242-
next = build_style_directives_object(style_directives, context.state.expressions, context);
235+
next = build_style_directives_object(style_directives, context);
243236
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
244237

245238
if (has_state) {

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,41 @@ export function memoize_expression(state, value) {
2121
}
2222

2323
/**
24-
* Pushes `value` into `expressions` and returns a new id
25-
* @param {Expression[]} expressions
26-
* @param {Expression} value
24+
* A utility for extracting complex expressions (such as call expressions)
25+
* from templates and replacing them with `$0`, `$1` etc
2726
*/
28-
export function get_expression_id(expressions, value) {
29-
return b.id(`$${expressions.push(value) - 1}`);
27+
export class Memoizer {
28+
/** @type {Array<{ id: Identifier, expression: Expression }>} */
29+
#sync = [];
30+
31+
/**
32+
* @param {Expression} expression
33+
*/
34+
add(expression) {
35+
const id = b.id('#'); // filled in later
36+
37+
this.#sync.push({ id, expression });
38+
39+
return id;
40+
}
41+
42+
apply() {
43+
return this.#sync.map((memo, i) => {
44+
memo.id.name = `$${i}`;
45+
return memo.id;
46+
});
47+
}
48+
49+
deriveds(runes = true) {
50+
return this.#sync.map((memo) =>
51+
b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression)))
52+
);
53+
}
54+
55+
sync_values() {
56+
if (this.#sync.length === 0) return;
57+
return b.array(this.#sync.map((memo) => b.thunk(memo.expression)));
58+
}
3059
}
3160

3261
/**
@@ -40,8 +69,7 @@ export function build_template_chunk(
4069
values,
4170
context,
4271
state = context.state,
43-
memoize = (value, metadata) =>
44-
metadata.has_call ? get_expression_id(state.expressions, value) : value
72+
memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value)
4573
) {
4674
/** @type {Expression[]} */
4775
const expressions = [];
@@ -128,18 +156,20 @@ export function build_template_chunk(
128156
* @param {ComponentClientTransformState} state
129157
*/
130158
export function build_render_statement(state) {
159+
const ids = state.memoizer.apply();
160+
const values = state.memoizer.sync_values();
161+
131162
return b.stmt(
132163
b.call(
133164
'$.template_effect',
134165
b.arrow(
135-
state.expressions.map((_, i) => b.id(`$${i}`)),
166+
ids,
136167
state.update.length === 1 && state.update[0].type === 'ExpressionStatement'
137168
? state.update[0].expression
138169
: b.block(state.update)
139170
),
140-
state.expressions.length > 0 &&
141-
b.array(state.expressions.map((expression) => b.thunk(expression))),
142-
state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal')
171+
values,
172+
values && !state.analysis.runes && b.id('$.derived_safe_equal')
143173
)
144174
);
145175
}

0 commit comments

Comments
 (0)