From c787d593d5e2c05b9a6a40fecf6199cc66cc991f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Mar 2025 16:11:39 -0400 Subject: [PATCH 01/11] feat: add partial evaluation --- .../client/visitors/RegularElement.js | 13 +- packages/svelte/src/compiler/phases/scope.js | 114 +++++++++++++++++- .../_expected/client/index.svelte.js | 2 +- 3 files changed, 122 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 9b3ecc922d89..f4836fa14318 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -33,6 +33,7 @@ import { memoize_expression } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; +import { UNKNOWN } from '../../../scope.js'; /** * @param {AST.RegularElement} node @@ -685,14 +686,16 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : value ); + const values = context.state.scope.evaluate(value); + + const assignment = b.assignment('=', b.member(node_id, '__value'), value); + const inner_assignment = b.assignment( '=', b.member(node_id, 'value'), - b.conditional( - b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)), - b.literal(''), // render null/undefined values as empty string to support placeholder options - value - ) + values.has(UNKNOWN) || values.has(null) || values.has(undefined) + ? b.logical('??', assignment, b.literal('')) + : assignment ); const update = b.stmt( diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index b6063c32343f..7a0af04e5c1b 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -16,6 +16,10 @@ import { is_reserved, is_rune } from '../../utils.js'; import { determine_slot } from '../utils/slot.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; +export const UNKNOWN = Symbol(); +export const NUMBER = Symbol(); +export const STRING = Symbol(); + export class Binding { /** @type {Scope} */ scope; @@ -34,7 +38,7 @@ export class Binding { * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` * @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock} */ - initial; + initial = null; /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ references = []; @@ -279,6 +283,114 @@ export class Scope { this.root.conflicts.add(node.name); } } + + /** + * + * @param {Expression} expression + * @param {Set} values + */ + evaluate(expression, values = new Set()) { + switch (expression.type) { + case 'Literal': + values.add(expression.value); + break; + + case 'Identifier': + const binding = this.get(expression.name); + if (binding && !binding.updated && binding.initial !== null) { + this.evaluate(/** @type {Expression} */ (binding.initial), values); + break; + } + + values.add(UNKNOWN); + break; + + case 'BinaryExpression': + switch (expression.operator) { + case '!=': + case '!==': + case '<': + case '<=': + case '>': + case '>=': + case '==': + case '===': + case 'in': + case 'instanceof': + values.add(true); + values.add(false); + break; + + case '%': + case '&': + case '*': + case '**': + case '-': + case '/': + case '<<': + case '>>': + case '>>>': + case '^': + case '|': + values.add(NUMBER); + break; + + case '+': + const a = Array.from(this.evaluate(/** @type {Expression} */ (expression.left))); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = Array.from(this.evaluate(expression.right)); + + if ( + a.every((v) => v === STRING || typeof v === 'string') || + b.every((v) => v === STRING || typeof v === 'string') + ) { + // concatenating strings + if ( + a.includes(STRING) || + b.includes(STRING) || + a.length > 1 || + b.length > 1 || + typeof a[0] === 'symbol' || + typeof b[0] === 'symbol' + ) { + values.add(STRING); + break; + } + + values.add(a[0] + b[0]); + break; + } + + if ( + a.every((v) => v === NUMBER || typeof v === 'number') || + b.every((v) => v === NUMBER || typeof v === 'number') + ) { + // adding numbers + if (a.includes(NUMBER) || b.includes(NUMBER) || a.length > 1 || b.length > 1) { + values.add(NUMBER); + break; + } + + values.add(a[0] + b[0]); + break; + } + + values.add(STRING); + values.add(NUMBER); + break; + + default: + values.add(UNKNOWN); + } + break; + + // TODO others (LogicalExpression, ConditionalExpression, Identifier when we know something about the binding, etc) + + default: + values.add(UNKNOWN); + } + + return values; + } } export class ScopeRoot { diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 46d376aca2f9..b341d39f28fb 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -38,7 +38,7 @@ export default function Skip_static_subtree($$anchor, $$props) { var select = $.sibling(div_1, 2); var option = $.child(select); - option.value = null == (option.__value = 'a') ? '' : 'a'; + option.value = option.__value = 'a'; $.reset(select); var img = $.sibling(select, 2); From 5934c17b590c8ebae4c2089de0f21a6e438cf509 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Mar 2025 16:19:09 -0400 Subject: [PATCH 02/11] fix --- packages/svelte/src/compiler/phases/scope.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 7a0af04e5c1b..9f63f23156ac 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -298,7 +298,7 @@ export class Scope { case 'Identifier': const binding = this.get(expression.name); if (binding && !binding.updated && binding.initial !== null) { - this.evaluate(/** @type {Expression} */ (binding.initial), values); + binding.scope.evaluate(/** @type {Expression} */ (binding.initial), values); break; } From e244e75a931fd51404fe2542f4cf02ee1a48a577 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 16:47:50 -0400 Subject: [PATCH 03/11] tweak --- .../client/visitors/RegularElement.js | 6 +- packages/svelte/src/compiler/phases/scope.js | 268 +++++++++++------- 2 files changed, 169 insertions(+), 105 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index f4836fa14318..a3ec780af0ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -686,16 +686,14 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : value ); - const values = context.state.scope.evaluate(value); + const evaluated = context.state.scope.evaluate(value); const assignment = b.assignment('=', b.member(node_id, '__value'), value); const inner_assignment = b.assignment( '=', b.member(node_id, 'value'), - values.has(UNKNOWN) || values.has(null) || values.has(undefined) - ? b.logical('??', assignment, b.literal('')) - : assignment + evaluated.is_defined ? assignment : b.logical('??', assignment, b.literal('')) ); const update = b.stmt( diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9f63f23156ac..4867c59c35c8 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -104,6 +104,145 @@ export class Binding { } } +class Evaluation { + /** @type {Set} */ + values = new Set(); + + /** + * True if there is exactly one possible value + * @readonly + * @type {boolean} + */ + is_definite = false; + + /** + * True if the value is known to not be null/undefined + * @readonly + * @type {boolean} + */ + is_defined = true; + + /** + * True if the value is known to be a string + * @readonly + * @type {boolean} + */ + is_string = true; + + /** + * True if the value is known to be a number + * @readonly + * @type {boolean} + */ + is_number = true; + + /** + * @readonly + * @type {any} + */ + value = undefined; + + /** + * + * @param {Scope} scope + * @param {Expression} expression + */ + constructor(scope, expression) { + switch (expression.type) { + case 'Literal': + this.values.add(expression.value); + break; + + case 'Identifier': + var binding = scope.get(expression.name); + if (binding && !binding.updated && binding.initial !== null) { + const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); + for (const value of evaluation.values) { + this.values.add(value); + } + break; + } + + this.values.add(UNKNOWN); + break; + + case 'BinaryExpression': + var a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in` + var b = scope.evaluate(expression.right); + + if (a.is_definite && b.is_definite) { + this.values.add(binary[expression.operator](a.value, b.value)); + break; + } + + switch (expression.operator) { + case '!=': + case '!==': + case '<': + case '<=': + case '>': + case '>=': + case '==': + case '===': + case 'in': + case 'instanceof': + this.values.add(true); + this.values.add(false); + break; + + case '%': + case '&': + case '*': + case '**': + case '-': + case '/': + case '<<': + case '>>': + case '>>>': + case '^': + case '|': + this.values.add(NUMBER); + break; + + case '+': + if (a.is_string || b.is_string) { + this.values.add(STRING); + } else if (a.is_number && b.is_number) { + this.values.add(NUMBER); + } else { + this.values.add(STRING); + this.values.add(NUMBER); + } + break; + } + break; + + // TODO others (LogicalExpression, ConditionalExpression, Identifier when we know something about the binding, etc) + + default: + this.values.add(UNKNOWN); + } + + for (const value of this.values) { + this.value = value; // saves having special logic for `size === 1` + + if (value !== STRING && typeof value !== 'string') { + this.is_string = false; + } + + if (value !== NUMBER && typeof value !== 'number') { + this.is_number = false; + } + + if (value == null || typeof value === 'symbol') { + this.is_defined = false; + } + } + + this.is_definite = this.values.size === 1 && typeof this.value !== 'symbol'; + } +} + export class Scope { /** @type {ScopeRoot} */ root; @@ -290,109 +429,36 @@ export class Scope { * @param {Set} values */ evaluate(expression, values = new Set()) { - switch (expression.type) { - case 'Literal': - values.add(expression.value); - break; - - case 'Identifier': - const binding = this.get(expression.name); - if (binding && !binding.updated && binding.initial !== null) { - binding.scope.evaluate(/** @type {Expression} */ (binding.initial), values); - break; - } - - values.add(UNKNOWN); - break; - - case 'BinaryExpression': - switch (expression.operator) { - case '!=': - case '!==': - case '<': - case '<=': - case '>': - case '>=': - case '==': - case '===': - case 'in': - case 'instanceof': - values.add(true); - values.add(false); - break; - - case '%': - case '&': - case '*': - case '**': - case '-': - case '/': - case '<<': - case '>>': - case '>>>': - case '^': - case '|': - values.add(NUMBER); - break; - - case '+': - const a = Array.from(this.evaluate(/** @type {Expression} */ (expression.left))); // `left` cannot be `PrivateIdentifier` unless operator is `in` - const b = Array.from(this.evaluate(expression.right)); - - if ( - a.every((v) => v === STRING || typeof v === 'string') || - b.every((v) => v === STRING || typeof v === 'string') - ) { - // concatenating strings - if ( - a.includes(STRING) || - b.includes(STRING) || - a.length > 1 || - b.length > 1 || - typeof a[0] === 'symbol' || - typeof b[0] === 'symbol' - ) { - values.add(STRING); - break; - } - - values.add(a[0] + b[0]); - break; - } - - if ( - a.every((v) => v === NUMBER || typeof v === 'number') || - b.every((v) => v === NUMBER || typeof v === 'number') - ) { - // adding numbers - if (a.includes(NUMBER) || b.includes(NUMBER) || a.length > 1 || b.length > 1) { - values.add(NUMBER); - break; - } - - values.add(a[0] + b[0]); - break; - } - - values.add(STRING); - values.add(NUMBER); - break; - - default: - values.add(UNKNOWN); - } - break; - - // TODO others (LogicalExpression, ConditionalExpression, Identifier when we know something about the binding, etc) - - default: - values.add(UNKNOWN); - } - - return values; + return new Evaluation(this, expression); } } +/** @type {Record any>} */ +const binary = { + '!=': (left, right) => left != right, + '!==': (left, right) => left !== right, + '<': (left, right) => left < right, + '<=': (left, right) => left <= right, + '>': (left, right) => left > right, + '>=': (left, right) => left >= right, + '==': (left, right) => left == right, + '===': (left, right) => left === right, + in: (left, right) => left in right, + instanceof: (left, right) => left instanceof right, + '%': (left, right) => left % right, + '&': (left, right) => left & right, + '*': (left, right) => left * right, + '**': (left, right) => left ** right, + '+': (left, right) => left + right, + '-': (left, right) => left - right, + '/': (left, right) => left / right, + '<<': (left, right) => left << right, + '>>': (left, right) => left >> right, + '>>>': (left, right) => left >>> right, + '^': (left, right) => left ^ right, + '|': (left, right) => left | right +}; + export class ScopeRoot { /** @type {Set} */ conflicts = new Set(); From 177111b1b9657664bb4bca8767d92bc493dc4b56 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 17:46:36 -0400 Subject: [PATCH 04/11] more --- .../client/visitors/shared/utils.js | 8 +- packages/svelte/src/compiler/phases/scope.js | 152 ++++++++++++++++-- .../_expected/client/index.svelte.js | 2 +- 3 files changed, 141 insertions(+), 21 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index df6308d6316a..84a48b89bee2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -89,13 +89,9 @@ export function build_template_chunk( } } - const is_defined = - value.type === 'BinaryExpression' || - (value.type === 'UnaryExpression' && value.operator !== 'void') || - (value.type === 'LogicalExpression' && value.right.type === 'Literal') || - (value.type === 'Identifier' && value.name === state.analysis.props_id?.name); + const evaluated = state.scope.evaluate(value); - if (!is_defined) { + if (!evaluated.is_defined) { // add `?? ''` where necessary (TODO optimise more cases) value = b.logical('??', value, b.literal('')); } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 4867c59c35c8..c952e06e2883 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -16,9 +16,9 @@ import { is_reserved, is_rune } from '../../utils.js'; import { determine_slot } from '../utils/slot.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; -export const UNKNOWN = Symbol(); -export const NUMBER = Symbol(); -export const STRING = Symbol(); +export const UNKNOWN = Symbol('unknown'); +export const NUMBER = Symbol('number'); // includes BigInt +export const STRING = Symbol('string'); export class Binding { /** @type {Scope} */ @@ -113,7 +113,7 @@ class Evaluation { * @readonly * @type {boolean} */ - is_definite = false; + is_known = false; /** * True if the value is known to not be null/undefined @@ -155,14 +155,28 @@ class Evaluation { case 'Identifier': var binding = scope.get(expression.name); - if (binding && !binding.updated && binding.initial !== null) { - const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); - for (const value of evaluation.values) { - this.values.add(value); + + if (binding) { + if ( + binding.initial?.type === 'CallExpression' && + get_rune(binding.initial, scope) === '$props.id' + ) { + this.values.add(STRING); + break; + } + + if (!binding.updated && binding.initial !== null) { + const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); + for (const value of evaluation.values) { + this.values.add(value); + } + break; } - break; } + // TODO glean what we can from reassignments + // TODO one day, expose props and imports somehow + this.values.add(UNKNOWN); break; @@ -170,7 +184,7 @@ class Evaluation { var a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in` var b = scope.evaluate(expression.right); - if (a.is_definite && b.is_definite) { + if (a.is_known && b.is_known) { this.values.add(binary[expression.operator](a.value, b.value)); break; } @@ -214,10 +228,102 @@ class Evaluation { this.values.add(NUMBER); } break; + + default: + // @ts-expect-error we can't guard against future operators without + // TypeScript getting confused + throw new Error(`Unknown operator ${expression.operator}`); + } + break; + + case 'ConditionalExpression': + var test = scope.evaluate(expression.test); + var consequent = scope.evaluate(expression.consequent); + var alternate = scope.evaluate(expression.alternate); + + if (test.is_known) { + for (const value of (test.value ? consequent : alternate).values) { + this.values.add(value); + } + } else { + for (const value of consequent.values) { + this.values.add(value); + } + + for (const value of alternate.values) { + this.values.add(value); + } + } + break; + + case 'LogicalExpression': + a = scope.evaluate(expression.left); + b = scope.evaluate(expression.right); + + if (a.is_known) { + if (b.is_known) { + this.values.add(logical[expression.operator](a.value, b.value)); + break; + } + + if ( + (expression.operator === '&&' && !a.value) || + (expression.operator === '||' && a.value) || + (expression.operator === '??' && a.value != null) + ) { + this.values.add(a.value); + } else { + for (const value of b.values) { + this.values.add(value); + } + } + + break; + } + + for (const value of a.values) { + this.values.add(value); + } + + for (const value of b.values) { + this.values.add(value); } break; - // TODO others (LogicalExpression, ConditionalExpression, Identifier when we know something about the binding, etc) + case 'UnaryExpression': + const argument = scope.evaluate(expression.argument); + + if (argument.is_known) { + this.values.add(unary[expression.operator](argument.value)); + break; + } + + switch (expression.operator) { + case '!': + case 'delete': + this.values.add(false); + this.values.add(true); + break; + + case '+': + case '-': + case '~': + this.values.add(NUMBER); + break; + + case 'typeof': + this.values.add(STRING); + break; + + case 'void': + this.values.add(undefined); + break; + + default: + // @ts-expect-error we can't guard against future operators without + // TypeScript getting confused + throw new Error(`Unknown operator ${expression.operator}`); + } default: this.values.add(UNKNOWN); @@ -234,12 +340,12 @@ class Evaluation { this.is_number = false; } - if (value == null || typeof value === 'symbol') { + if (value == null || value === UNKNOWN) { this.is_defined = false; } } - this.is_definite = this.values.size === 1 && typeof this.value !== 'symbol'; + this.is_known = this.values.size === 1 && typeof this.value !== 'symbol'; } } @@ -459,6 +565,24 @@ const binary = { '|': (left, right) => left | right }; +/** @type {Record any>} */ +const unary = { + '-': (argument) => -argument, + '+': (argument) => +argument, + '!': (argument) => !argument, + '~': (argument) => ~argument, + typeof: (argument) => typeof argument, + void: () => undefined, + delete: () => true +}; + +/** @type {Record any>} */ +const logical = { + '||': (left, right) => left || right, + '&&': (left, right) => left && right, + '??': (left, right) => left ?? right +}; + export class ScopeRoot { /** @type {Set} */ conflicts = new Set(); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 332c909ebed9..eab5a1526259 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -10,7 +10,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var fragment = root(); var h1 = $.first_child(fragment); - h1.textContent = `Hello, ${name ?? ''}!`; + h1.textContent = `Hello, ${name}!`; var b = $.sibling(h1, 2); From 66b8b2a005c90b7ddb526386cded85a7489eb987 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 17:49:29 -0400 Subject: [PATCH 05/11] more --- packages/svelte/src/compiler/phases/scope.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index c952e06e2883..c0c9e9c9daab 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -172,6 +172,8 @@ class Evaluation { } break; } + + // TODO each index is always defined } // TODO glean what we can from reassignments From 8acf96719bb8056286e3628acb42f91a8359a4c8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 17:54:30 -0400 Subject: [PATCH 06/11] evaluate stuff in template --- .../client/visitors/shared/utils.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 84a48b89bee2..98beada2845e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -91,15 +91,19 @@ export function build_template_chunk( const evaluated = state.scope.evaluate(value); - if (!evaluated.is_defined) { - // add `?? ''` where necessary (TODO optimise more cases) - value = b.logical('??', value, b.literal('')); - } + if (evaluated.is_known) { + quasi.value.cooked += evaluated.value + ''; + } else { + if (!evaluated.is_defined) { + // add `?? ''` where necessary (TODO optimise more cases) + value = b.logical('??', value, b.literal('')); + } - expressions.push(value); + expressions.push(value); - quasi = b.quasi('', i + 1 === values.length); - quasis.push(quasi); + quasi = b.quasi('', i + 1 === values.length); + quasis.push(quasi); + } } } From ea1940343bda63339f7b1b15c8bae1bd091eac69 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 17:56:33 -0400 Subject: [PATCH 07/11] update test --- .../_expected/client/index.svelte.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index eab5a1526259..21f6ed9680a9 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -10,11 +10,11 @@ export default function Nullish_coallescence_omittance($$anchor) { var fragment = root(); var h1 = $.first_child(fragment); - h1.textContent = `Hello, ${name}!`; + h1.textContent = 'Hello, world!'; var b = $.sibling(h1, 2); - b.textContent = `${1 ?? 'stuff'}${2 ?? 'more stuff'}${3 ?? 'even more stuff'}`; + b.textContent = '123'; var button = $.sibling(b, 2); @@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); - h1_1.textContent = `Hello, ${name ?? 'earth' ?? ''}`; + h1_1.textContent = 'Hello, world'; $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); } From 49b5d7f5e0ccc002810851162dea5863580d0086 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 20:28:23 -0400 Subject: [PATCH 08/11] SSR --- .../3-transform/server/visitors/shared/utils.js | 16 +++++++++------- packages/svelte/src/compiler/phases/scope.js | 16 ++++++++++++---- .../_expected/server/index.svelte.js | 2 +- .../samples/attached-sourcemap/input.svelte | 2 +- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 2c6aa2f316aa..807e12a8fa92 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -44,15 +44,17 @@ export function process_children(nodes, { visit, state }) { if (node.type === 'Text' || node.type === 'Comment') { quasi.value.cooked += node.type === 'Comment' ? `` : escape_html(node.data); - } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') { - if (node.expression.value != null) { - quasi.value.cooked += escape_html(node.expression.value + ''); - } } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + const evaluated = state.scope.evaluate(node.expression); + + if (evaluated.is_known) { + quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); + } else { + expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); - quasi = b.quasi('', i + 1 === sequence.length); - quasis.push(quasi); + quasi = b.quasi('', i + 1 === sequence.length); + quasis.push(quasi); + } } } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index c0c9e9c9daab..89668462f39a 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -113,7 +113,7 @@ class Evaluation { * @readonly * @type {boolean} */ - is_known = false; + is_known = true; /** * True if the value is known to not be null/undefined @@ -165,7 +165,12 @@ class Evaluation { break; } - if (!binding.updated && binding.initial !== null) { + const is_prop = + binding.kind === 'prop' || + binding.kind === 'rest_prop' || + binding.kind === 'bindable_prop'; + + if (!binding.updated && binding.initial !== null && !is_prop) { const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); for (const value of evaluation.values) { this.values.add(value); @@ -293,7 +298,7 @@ class Evaluation { break; case 'UnaryExpression': - const argument = scope.evaluate(expression.argument); + var argument = scope.evaluate(expression.argument); if (argument.is_known) { this.values.add(unary[expression.operator](argument.value)); @@ -326,6 +331,7 @@ class Evaluation { // TypeScript getting confused throw new Error(`Unknown operator ${expression.operator}`); } + break; default: this.values.add(UNKNOWN); @@ -347,7 +353,9 @@ class Evaluation { } } - this.is_known = this.values.size === 1 && typeof this.value !== 'symbol'; + if (this.values.size > 1 || typeof this.value === 'symbol') { + this.is_known = false; + } } } diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 8181bfd98eeb..3b23befcd44e 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) { let name = 'world'; let count = 0; - $$payload.out += `

Hello, ${$.escape(name)}!

${$.escape(1 ?? 'stuff')}${$.escape(2 ?? 'more stuff')}${$.escape(3 ?? 'even more stuff')}

Hello, ${$.escape(name ?? 'earth' ?? null)}

`; + $$payload.out += `

Hello, world!

123

Hello, world

`; } \ No newline at end of file diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte index 21a47a72a9c9..715bbda8d92e 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/input.svelte @@ -8,4 +8,4 @@ replace_me_script = 'hello' ; -

{done_replace_script_2}

+

{Math.random() < 1 && done_replace_script_2}

From ad9069314b0d697b83e3f4df31099fe935463ffb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 20:28:53 -0400 Subject: [PATCH 09/11] unused --- .../phases/3-transform/client/visitors/RegularElement.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index a3ec780af0ee..0292fecdf1f0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -33,7 +33,6 @@ import { memoize_expression } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; -import { UNKNOWN } from '../../../scope.js'; /** * @param {AST.RegularElement} node From 655e7004bfbb33504f42ed4ca0fcfc0ea84ea249 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 20:30:13 -0400 Subject: [PATCH 10/11] changeset --- .changeset/selfish-onions-begin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/selfish-onions-begin.md diff --git a/.changeset/selfish-onions-begin.md b/.changeset/selfish-onions-begin.md new file mode 100644 index 000000000000..7ecc1a2ebaa0 --- /dev/null +++ b/.changeset/selfish-onions-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: partial evaluation From 57f1886a8eab978e854accea6ffd531ca6304560 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Mar 2025 20:39:28 -0400 Subject: [PATCH 11/11] remove TODO --- .../compiler/phases/3-transform/client/visitors/shared/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 98beada2845e..4fbc51f84a48 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -95,7 +95,7 @@ export function build_template_chunk( quasi.value.cooked += evaluated.value + ''; } else { if (!evaluated.is_defined) { - // add `?? ''` where necessary (TODO optimise more cases) + // add `?? ''` where necessary value = b.logical('??', value, b.literal('')); }