Skip to content

Commit d0dcc0b

Browse files
fix: improve partial evaluation (#15781)
* init * remove console log * Update packages/svelte/src/compiler/phases/scope.js * fix each indices * dedupe * Update packages/svelte/src/compiler/phases/scope.js * always break * fix formatting * Apply suggestions from code review * compactify * compactify * reuse values * add more globals, template literals, try functions and (some) member expressions * remove console logs * remove function handling, tweak failing test * changeset * try putting static stuff in the template * nevermind * unused * simplify * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js * YAGNI * simplify and fix (should use cooked, not raw) * unused * changeset --------- Co-authored-by: Rich Harris <hello@rich-harris.dev> Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent 5b7af5e commit d0dcc0b

File tree

9 files changed

+243
-24
lines changed

9 files changed

+243
-24
lines changed

.changeset/serious-adults-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: partially evaluate more expressions

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,17 @@ export function build_template_chunk(
6969
node.metadata.expression
7070
);
7171

72-
has_state ||= node.metadata.expression.has_state;
72+
const evaluated = state.scope.evaluate(value);
73+
74+
has_state ||= node.metadata.expression.has_state && !evaluated.is_known;
7375

7476
if (values.length === 1) {
7577
// If we have a single expression, then pass that in directly to possibly avoid doing
7678
// extra work in the template_effect (instead we do the work in set_text).
79+
if (evaluated.is_known) {
80+
value = b.literal(evaluated.value);
81+
}
82+
7783
return { value, has_state };
7884
}
7985

@@ -89,8 +95,6 @@ export function build_template_chunk(
8995
}
9096
}
9197

92-
const evaluated = state.scope.evaluate(value);
93-
9498
if (evaluated.is_known) {
9599
quasi.value.cooked += evaluated.value + '';
96100
} else {

packages/svelte/src/compiler/phases/scope.js

Lines changed: 191 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */
1+
/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */
22
/** @import { Context, Visitor } from 'zimmerframe' */
33
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
44
import is_reference from 'is-reference';
@@ -18,8 +18,71 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
1818

1919
const UNKNOWN = Symbol('unknown');
2020
/** Includes `BigInt` */
21-
const NUMBER = Symbol('number');
22-
const STRING = Symbol('string');
21+
export const NUMBER = Symbol('number');
22+
export const STRING = Symbol('string');
23+
24+
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
25+
const globals = {
26+
BigInt: [NUMBER, BigInt],
27+
'Math.min': [NUMBER, Math.min],
28+
'Math.max': [NUMBER, Math.max],
29+
'Math.random': [NUMBER],
30+
'Math.floor': [NUMBER, Math.floor],
31+
// @ts-expect-error
32+
'Math.f16round': [NUMBER, Math.f16round],
33+
'Math.round': [NUMBER, Math.round],
34+
'Math.abs': [NUMBER, Math.abs],
35+
'Math.acos': [NUMBER, Math.acos],
36+
'Math.asin': [NUMBER, Math.asin],
37+
'Math.atan': [NUMBER, Math.atan],
38+
'Math.atan2': [NUMBER, Math.atan2],
39+
'Math.ceil': [NUMBER, Math.ceil],
40+
'Math.cos': [NUMBER, Math.cos],
41+
'Math.sin': [NUMBER, Math.sin],
42+
'Math.tan': [NUMBER, Math.tan],
43+
'Math.exp': [NUMBER, Math.exp],
44+
'Math.log': [NUMBER, Math.log],
45+
'Math.pow': [NUMBER, Math.pow],
46+
'Math.sqrt': [NUMBER, Math.sqrt],
47+
'Math.clz32': [NUMBER, Math.clz32],
48+
'Math.imul': [NUMBER, Math.imul],
49+
'Math.sign': [NUMBER, Math.sign],
50+
'Math.log10': [NUMBER, Math.log10],
51+
'Math.log2': [NUMBER, Math.log2],
52+
'Math.log1p': [NUMBER, Math.log1p],
53+
'Math.expm1': [NUMBER, Math.expm1],
54+
'Math.cosh': [NUMBER, Math.cosh],
55+
'Math.sinh': [NUMBER, Math.sinh],
56+
'Math.tanh': [NUMBER, Math.tanh],
57+
'Math.acosh': [NUMBER, Math.acosh],
58+
'Math.asinh': [NUMBER, Math.asinh],
59+
'Math.atanh': [NUMBER, Math.atanh],
60+
'Math.trunc': [NUMBER, Math.trunc],
61+
'Math.fround': [NUMBER, Math.fround],
62+
'Math.cbrt': [NUMBER, Math.cbrt],
63+
Number: [NUMBER, Number],
64+
'Number.isInteger': [NUMBER, Number.isInteger],
65+
'Number.isFinite': [NUMBER, Number.isFinite],
66+
'Number.isNaN': [NUMBER, Number.isNaN],
67+
'Number.isSafeInteger': [NUMBER, Number.isSafeInteger],
68+
'Number.parseFloat': [NUMBER, Number.parseFloat],
69+
'Number.parseInt': [NUMBER, Number.parseInt],
70+
String: [STRING, String],
71+
'String.fromCharCode': [STRING, String.fromCharCode],
72+
'String.fromCodePoint': [STRING, String.fromCodePoint]
73+
};
74+
75+
/** @type {Record<string, any>} */
76+
const global_constants = {
77+
'Math.PI': Math.PI,
78+
'Math.E': Math.E,
79+
'Math.LN10': Math.LN10,
80+
'Math.LN2': Math.LN2,
81+
'Math.LOG10E': Math.LOG10E,
82+
'Math.LOG2E': Math.LOG2E,
83+
'Math.SQRT2': Math.SQRT2,
84+
'Math.SQRT1_2': Math.SQRT1_2
85+
};
2386

2487
export class Binding {
2588
/** @type {Scope} */
@@ -107,7 +170,7 @@ export class Binding {
107170

108171
class Evaluation {
109172
/** @type {Set<any>} */
110-
values = new Set();
173+
values;
111174

112175
/**
113176
* True if there is exactly one possible value
@@ -147,8 +210,11 @@ class Evaluation {
147210
*
148211
* @param {Scope} scope
149212
* @param {Expression} expression
213+
* @param {Set<any>} values
150214
*/
151-
constructor(scope, expression) {
215+
constructor(scope, expression, values) {
216+
this.values = values;
217+
152218
switch (expression.type) {
153219
case 'Literal': {
154220
this.values.add(expression.value);
@@ -172,15 +238,18 @@ class Evaluation {
172238
binding.kind === 'rest_prop' ||
173239
binding.kind === 'bindable_prop';
174240

175-
if (!binding.updated && binding.initial !== null && !is_prop) {
176-
const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial));
177-
for (const value of evaluation.values) {
178-
this.values.add(value);
179-
}
241+
if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) {
242+
this.values.add(NUMBER);
180243
break;
181244
}
182245

183-
// TODO each index is always defined
246+
if (!binding.updated && binding.initial !== null && !is_prop) {
247+
binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values);
248+
break;
249+
}
250+
} else if (expression.name === 'undefined') {
251+
this.values.add(undefined);
252+
break;
184253
}
185254

186255
// TODO glean what we can from reassignments
@@ -336,6 +405,101 @@ class Evaluation {
336405
break;
337406
}
338407

408+
case 'CallExpression': {
409+
const keypath = get_global_keypath(expression.callee, scope);
410+
411+
if (keypath) {
412+
if (is_rune(keypath)) {
413+
const arg = /** @type {Expression | undefined} */ (expression.arguments[0]);
414+
415+
switch (keypath) {
416+
case '$state':
417+
case '$state.raw':
418+
case '$derived':
419+
if (arg) {
420+
scope.evaluate(arg, this.values);
421+
} else {
422+
this.values.add(undefined);
423+
}
424+
break;
425+
426+
case '$props.id':
427+
this.values.add(STRING);
428+
break;
429+
430+
case '$effect.tracking':
431+
this.values.add(false);
432+
this.values.add(true);
433+
break;
434+
435+
case '$derived.by':
436+
if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') {
437+
scope.evaluate(arg.body, this.values);
438+
break;
439+
}
440+
441+
this.values.add(UNKNOWN);
442+
break;
443+
444+
default: {
445+
this.values.add(UNKNOWN);
446+
}
447+
}
448+
449+
break;
450+
}
451+
452+
if (
453+
Object.hasOwn(globals, keypath) &&
454+
expression.arguments.every((arg) => arg.type !== 'SpreadElement')
455+
) {
456+
const [type, fn] = globals[keypath];
457+
const values = expression.arguments.map((arg) => scope.evaluate(arg));
458+
459+
if (fn && values.every((e) => e.is_known)) {
460+
this.values.add(fn(...values.map((e) => e.value)));
461+
} else {
462+
this.values.add(type);
463+
}
464+
465+
break;
466+
}
467+
}
468+
469+
this.values.add(UNKNOWN);
470+
break;
471+
}
472+
473+
case 'TemplateLiteral': {
474+
let result = expression.quasis[0].value.cooked;
475+
476+
for (let i = 0; i < expression.expressions.length; i += 1) {
477+
const e = scope.evaluate(expression.expressions[i]);
478+
479+
if (e.is_known) {
480+
result += e.value + expression.quasis[i + 1].value.cooked;
481+
} else {
482+
this.values.add(STRING);
483+
break;
484+
}
485+
}
486+
487+
this.values.add(result);
488+
break;
489+
}
490+
491+
case 'MemberExpression': {
492+
const keypath = get_global_keypath(expression, scope);
493+
494+
if (keypath && Object.hasOwn(global_constants, keypath)) {
495+
this.values.add(global_constants[keypath]);
496+
break;
497+
}
498+
499+
this.values.add(UNKNOWN);
500+
break;
501+
}
502+
339503
default: {
340504
this.values.add(UNKNOWN);
341505
}
@@ -548,10 +712,10 @@ export class Scope {
548712
* Only call this once scope has been fully generated in a first pass,
549713
* else this evaluates on incomplete data and may yield wrong results.
550714
* @param {Expression} expression
551-
* @param {Set<any>} values
715+
* @param {Set<any>} [values]
552716
*/
553717
evaluate(expression, values = new Set()) {
554-
return new Evaluation(this, expression);
718+
return new Evaluation(this, expression, values);
555719
}
556720
}
557721

@@ -1115,7 +1279,19 @@ export function get_rune(node, scope) {
11151279
if (!node) return null;
11161280
if (node.type !== 'CallExpression') return null;
11171281

1118-
let n = node.callee;
1282+
const keypath = get_global_keypath(node.callee, scope);
1283+
1284+
if (!keypath || !is_rune(keypath)) return null;
1285+
return keypath;
1286+
}
1287+
1288+
/**
1289+
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
1290+
* @param {Expression | Super} node
1291+
* @param {Scope} scope
1292+
*/
1293+
function get_global_keypath(node, scope) {
1294+
let n = node;
11191295

11201296
let joined = '';
11211297

@@ -1133,12 +1309,8 @@ export function get_rune(node, scope) {
11331309

11341310
if (n.type !== 'Identifier') return null;
11351311

1136-
joined = n.name + joined;
1137-
1138-
if (!is_rune(joined)) return null;
1139-
11401312
const binding = scope.get(n.name);
11411313
if (binding !== null) return null; // rune name, but references a variable or store
11421314

1143-
return joined;
1315+
return n.name + joined;
11441316
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from '../../test';
2+
3+
export default test({});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'svelte/internal/disclose-version';
2+
import 'svelte/internal/flags/legacy';
3+
import * as $ from 'svelte/internal/client';
4+
5+
var root_1 = $.template(`<p></p>`);
6+
7+
export default function Each_index_non_null($$anchor) {
8+
var fragment = $.comment();
9+
var node = $.first_child(fragment);
10+
11+
$.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => {
12+
var p = root_1();
13+
14+
p.textContent = `index: ${i}`;
15+
$.append($$anchor, p);
16+
});
17+
18+
$.append($$anchor, fragment);
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as $ from 'svelte/internal/server';
2+
3+
export default function Each_index_non_null($$payload) {
4+
const each_array = $.ensure_array_like(Array(10));
5+
6+
$$payload.out += `<!--[-->`;
7+
8+
for (let i = 0, $$length = each_array.length; i < $$length; i++) {
9+
$$payload.out += `<p>index: ${$.escape(i)}</p>`;
10+
}
11+
12+
$$payload.out += `<!--]-->`;
13+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{#each Array(10), i}
2+
<p>index: {i}</p>
3+
{/each}

packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function Purity($$anchor) {
88
var fragment = root();
99
var p = $.first_child(fragment);
1010

11-
p.textContent = Math.max(0, Math.min(0, 100));
11+
p.textContent = 0;
1212

1313
var p_1 = $.sibling(p, 2);
1414

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as $ from 'svelte/internal/server';
22

33
export default function Purity($$payload) {
4-
$$payload.out += `<p>${$.escape(Math.max(0, Math.min(0, 100)))}</p> <p>${$.escape(location.href)}</p> `;
4+
$$payload.out += `<p>0</p> <p>${$.escape(location.href)}</p> `;
55
Child($$payload, { prop: encodeURIComponent('hello') });
66
$$payload.out += `<!---->`;
77
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy