Skip to content

Commit c787d59

Browse files
committed
feat: add partial evaluation
1 parent a1257c1 commit c787d59

File tree

3 files changed

+122
-7
lines changed

3 files changed

+122
-7
lines changed

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
memoize_expression
3434
} from './shared/utils.js';
3535
import { visit_event_attribute } from './shared/events.js';
36+
import { UNKNOWN } from '../../../scope.js';
3637

3738
/**
3839
* @param {AST.RegularElement} node
@@ -685,14 +686,16 @@ function build_element_special_value_attribute(element, node_id, attribute, cont
685686
: value
686687
);
687688

689+
const values = context.state.scope.evaluate(value);
690+
691+
const assignment = b.assignment('=', b.member(node_id, '__value'), value);
692+
688693
const inner_assignment = b.assignment(
689694
'=',
690695
b.member(node_id, 'value'),
691-
b.conditional(
692-
b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)),
693-
b.literal(''), // render null/undefined values as empty string to support placeholder options
694-
value
695-
)
696+
values.has(UNKNOWN) || values.has(null) || values.has(undefined)
697+
? b.logical('??', assignment, b.literal(''))
698+
: assignment
696699
);
697700

698701
const update = b.stmt(

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

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { is_reserved, is_rune } from '../../utils.js';
1616
import { determine_slot } from '../utils/slot.js';
1717
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
1818

19+
export const UNKNOWN = Symbol();
20+
export const NUMBER = Symbol();
21+
export const STRING = Symbol();
22+
1923
export class Binding {
2024
/** @type {Scope} */
2125
scope;
@@ -34,7 +38,7 @@ export class Binding {
3438
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
3539
* @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock}
3640
*/
37-
initial;
41+
initial = null;
3842

3943
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
4044
references = [];
@@ -279,6 +283,114 @@ export class Scope {
279283
this.root.conflicts.add(node.name);
280284
}
281285
}
286+
287+
/**
288+
*
289+
* @param {Expression} expression
290+
* @param {Set<any>} values
291+
*/
292+
evaluate(expression, values = new Set()) {
293+
switch (expression.type) {
294+
case 'Literal':
295+
values.add(expression.value);
296+
break;
297+
298+
case 'Identifier':
299+
const binding = this.get(expression.name);
300+
if (binding && !binding.updated && binding.initial !== null) {
301+
this.evaluate(/** @type {Expression} */ (binding.initial), values);
302+
break;
303+
}
304+
305+
values.add(UNKNOWN);
306+
break;
307+
308+
case 'BinaryExpression':
309+
switch (expression.operator) {
310+
case '!=':
311+
case '!==':
312+
case '<':
313+
case '<=':
314+
case '>':
315+
case '>=':
316+
case '==':
317+
case '===':
318+
case 'in':
319+
case 'instanceof':
320+
values.add(true);
321+
values.add(false);
322+
break;
323+
324+
case '%':
325+
case '&':
326+
case '*':
327+
case '**':
328+
case '-':
329+
case '/':
330+
case '<<':
331+
case '>>':
332+
case '>>>':
333+
case '^':
334+
case '|':
335+
values.add(NUMBER);
336+
break;
337+
338+
case '+':
339+
const a = Array.from(this.evaluate(/** @type {Expression} */ (expression.left))); // `left` cannot be `PrivateIdentifier` unless operator is `in`
340+
const b = Array.from(this.evaluate(expression.right));
341+
342+
if (
343+
a.every((v) => v === STRING || typeof v === 'string') ||
344+
b.every((v) => v === STRING || typeof v === 'string')
345+
) {
346+
// concatenating strings
347+
if (
348+
a.includes(STRING) ||
349+
b.includes(STRING) ||
350+
a.length > 1 ||
351+
b.length > 1 ||
352+
typeof a[0] === 'symbol' ||
353+
typeof b[0] === 'symbol'
354+
) {
355+
values.add(STRING);
356+
break;
357+
}
358+
359+
values.add(a[0] + b[0]);
360+
break;
361+
}
362+
363+
if (
364+
a.every((v) => v === NUMBER || typeof v === 'number') ||
365+
b.every((v) => v === NUMBER || typeof v === 'number')
366+
) {
367+
// adding numbers
368+
if (a.includes(NUMBER) || b.includes(NUMBER) || a.length > 1 || b.length > 1) {
369+
values.add(NUMBER);
370+
break;
371+
}
372+
373+
values.add(a[0] + b[0]);
374+
break;
375+
}
376+
377+
values.add(STRING);
378+
values.add(NUMBER);
379+
break;
380+
381+
default:
382+
values.add(UNKNOWN);
383+
}
384+
break;
385+
386+
// TODO others (LogicalExpression, ConditionalExpression, Identifier when we know something about the binding, etc)
387+
388+
default:
389+
values.add(UNKNOWN);
390+
}
391+
392+
return values;
393+
}
282394
}
283395

284396
export class ScopeRoot {

packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
3838
var select = $.sibling(div_1, 2);
3939
var option = $.child(select);
4040

41-
option.value = null == (option.__value = 'a') ? '' : 'a';
41+
option.value = option.__value = 'a';
4242
$.reset(select);
4343

4444
var img = $.sibling(select, 2);

0 commit comments

Comments
 (0)