Skip to content

Commit 1853da4

Browse files
committed
add basic CallExpression optimizations (with tests)
1 parent bdf200d commit 1853da4

File tree

4 files changed

+143
-7
lines changed

4 files changed

+143
-7
lines changed

packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
1-
/** @import { Node, BinaryExpression, LogicalExpression, UnaryExpression, Expression, SequenceExpression, TemplateLiteral, ConditionalExpression } from 'estree' */
1+
/** @import { Node, BinaryExpression, LogicalExpression, UnaryExpression, Expression, SequenceExpression, TemplateLiteral, ConditionalExpression, CallExpression } from 'estree' */
22
/** @import { ComponentClientTransformState } from '../client/types' */
33
/** @import { ComponentServerTransformState } from '../server/types' */
44
export const DYNAMIC = Symbol('DYNAMIC');
5-
5+
if (!('difference' in Set.prototype)) {
6+
/**
7+
* Quick and dirty polfill for `Set.prototype.difference`
8+
* @template T
9+
* @this {Set<T>}
10+
* @param {Set<T>} other
11+
* @returns {Set<T>}
12+
*/
13+
//@ts-ignore
14+
Set.prototype.difference = function difference(other) {
15+
/** @type {Set<T>} */
16+
let res = new Set();
17+
for (let item of this) {
18+
if (!other.has(item)) {
19+
res.add(item);
20+
}
21+
}
22+
return res;
23+
};
24+
}
625
/**
726
* @template {boolean} S
827
* @param {Node} node
@@ -135,6 +154,7 @@ export function evaluate_static_expression(node, state, server) {
135154
function handle_ident(name) {
136155
if (server) return DYNAMIC;
137156
const scope = state.scope.get(name);
157+
// TODO tweak this when implicit top-level reactivity is removed
138158
if (scope?.kind === 'normal' && scope?.declaration_kind !== 'import') {
139159
if (scope.initial && !scope.mutated && !scope.reassigned && !scope.updated) {
140160
//@ts-ignore
@@ -186,6 +206,91 @@ export function evaluate_static_expression(node, state, server) {
186206
}
187207
return DYNAMIC;
188208
}
209+
/**
210+
* @param {CallExpression} node
211+
*/
212+
function handle_call(node) {
213+
/**
214+
* There isn't much we can really do here (without having an unreasonable amount of code),
215+
* so we don't optimize for much
216+
* We only optimize for these:
217+
* ```
218+
* (() => identifier_or_evaluable_value)();
219+
* (() => {
220+
* return evaluable;
221+
* })();
222+
* (() => {
223+
* let variable = ident_or_evaluable_value;
224+
* return variable;
225+
* });
226+
* // it's fine if we don't want to use this for side effect reasons, its just one line (251)
227+
* (() => {
228+
* anything_but_a_return;
229+
* })()
230+
* ```
231+
* I would like to possibly optimize this:
232+
* ```
233+
* (() => {
234+
* let variable = ident_or_evaluable_value;
235+
* return variable_combined_with_evaluable;
236+
* })();
237+
* ```
238+
* But I don't know how to do that with the `Scope` class.
239+
*/
240+
let { callee } = node;
241+
if (
242+
callee.type !== 'ArrowFunctionExpression' ||
243+
node.arguments.length ||
244+
callee.params.length
245+
) {
246+
return DYNAMIC;
247+
}
248+
let { body } = callee;
249+
if (body.type === 'BlockStatement') {
250+
let children = body.body;
251+
if (!children.find(({ type }) => type === 'ReturnStatement')) return undefined;
252+
if (children.length === 1 && children[0].type === 'ReturnStatement') {
253+
return children[0].argument == null
254+
? undefined
255+
: internal(children[0].argument, state, server);
256+
}
257+
let valid_body_children = new Set([
258+
'VariableDeclaration',
259+
'EmptyStatement',
260+
'ReturnStatement'
261+
]);
262+
let types = new Set(children.map(({ type }) => type));
263+
if (types.difference(valid_body_children).size) {
264+
return DYNAMIC;
265+
}
266+
if (types.has('EmptyStatement')) {
267+
children = children.filter(({ type }) => type !== 'EmptyStatement');
268+
}
269+
if (children.length > 2) return DYNAMIC;
270+
if (children[0].type !== 'VariableDeclaration' || children[1].type !== 'ReturnStatement')
271+
return DYNAMIC;
272+
let [declaration, return_statement] = children;
273+
if (declaration.declarations.length > 1) return DYNAMIC;
274+
let [declarator] = declaration.declarations;
275+
if (declarator.id.type !== 'Identifier') return DYNAMIC;
276+
let variable_value;
277+
if (declarator.init != null) {
278+
variable_value = internal(declarator.init, state, server);
279+
if (variable_value === DYNAMIC) {
280+
//might be unpure
281+
return DYNAMIC;
282+
}
283+
}
284+
let { argument } = return_statement;
285+
if (argument == null) return undefined;
286+
if (argument.type === 'Identifier' && argument.name === declarator.id.name) {
287+
return variable_value;
288+
}
289+
return internal(argument, state, server);
290+
} else {
291+
return internal(body, state, server);
292+
}
293+
}
189294
switch (node.type) {
190295
case 'Literal':
191296
return node.value;
@@ -203,6 +308,8 @@ export function evaluate_static_expression(node, state, server) {
203308
return handle_template(node);
204309
case 'ConditionalExpression':
205310
return handle_ternary(node);
311+
case 'CallExpression':
312+
return handle_call(node);
206313
default:
207314
return DYNAMIC;
208315
}

packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import 'svelte/internal/disclose-version';
22
import * as $ from 'svelte/internal/client';
33

44
var on_click = (_, count) => $.update(count);
5-
var root = $.template(`<h1></h1> <p></p> <button> </button> <p></p> <p></p> <p></p> <!>`, 1);
5+
var root = $.template(`<h1></h1> <p></p> <button> </button> <p></p> <p></p> <p></p> <!> <br> <br> `, 1);
6+
7+
export default function Static_template_expression_evaluation($$anchor, $$props) {
8+
$.push($$props, true);
69

7-
export default function Static_template_expression_evaluation($$anchor) {
810
let a = 1;
911
let b = 2;
1012
let name = 'world';
@@ -51,8 +53,20 @@ export default function Static_template_expression_evaluation($$anchor) {
5153
c: 3
5254
});
5355

56+
var text_1 = $.sibling(node);
57+
58+
text_1.nodeValue = ' 0';
59+
60+
var text_2 = $.sibling(text_1, 2);
61+
62+
text_2.nodeValue = ' Hello, world!';
63+
64+
var text_3 = $.sibling(text_2, 2);
65+
66+
text_3.nodeValue = ' 3';
5467
$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
5568
$.append($$anchor, fragment);
69+
$.pop();
5670
}
5771

5872
$.delegate(['click']);
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as $ from 'svelte/internal/server';
22

3-
export default function Static_template_expression_evaluation($$payload) {
3+
export default function Static_template_expression_evaluation($$payload, $$props) {
4+
$.push();
5+
46
let a = 1;
57
let b = 2;
68
let name = 'world';
@@ -9,5 +11,10 @@ export default function Static_template_expression_evaluation($$payload) {
911
function Component() {} // placeholder component
1012
$$payload.out += `<h1>Hello, ${$.escape(name)}!</h1> <p>${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}</p> <button>Count is ${$.escape(count)}</button> <p>1 + 2 = 3</p> <p>Sum is ${$.escape((a, b, a + b))}</p> <p>${$.escape(a === 1 ? a : b)}</p> `;
1113
Component($$payload, { a, count, c: a + b });
12-
$$payload.out += `<!---->`;
14+
15+
$$payload.out += `<!----> 0<br> Hello, world!<br> ${$.escape((() => {
16+
return a + b;
17+
})())}`;
18+
19+
$.pop();
1320
}

packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,12 @@
1313
<p>{1} + {2} = {1 + 2}</p>
1414
<p>Sum is {(a, b, a + b)}</p>
1515
<p>{a === 1 ? a : b}</p>
16-
<Component {a} {count} c={a+b} />
16+
<Component {a} {count} c={a+b} />
17+
{(() => {
18+
let thing = 0;
19+
return thing;
20+
})()}<br />
21+
{(() => 'Hello, world!')()}<br />
22+
{(() => {
23+
return a + b;
24+
})()}

0 commit comments

Comments
 (0)