From a36b2cf741ab9237ffd2e86f07774a29fdf6553d Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:23:12 -0800 Subject: [PATCH 01/16] init --- .../client/visitors/shared/element.js | 7 +- .../client/visitors/shared/utils.js | 202 +++++++++++++++++- 2 files changed, 206 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index abffad0ff7a4..ce26cecf8fd3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -6,7 +6,7 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; import { build_getter } from '../../utils.js'; -import { build_template_chunk, get_expression_id } from './utils.js'; +import { build_template_chunk, get_expression_id, DYNAMIC, evaluate_static_expression } from './utils.js'; /** * @param {Array} attributes @@ -186,7 +186,10 @@ export function build_attribute_value(value, context, memoize = (value) => value } let expression = /** @type {Expression} */ (context.visit(chunk.expression)); - + let evaluated = evaluate_static_expression(expression, context.state); + if (evaluated !== DYNAMIC) { + return { value: b.literal(evaluated), has_state: false}; + } return { value: memoize(expression, chunk.metadata.expression), has_state: chunk.metadata.expression.has_state 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 c25ef3ab50e3..70e5b7c611c6 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 @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super, Node, UnaryExpression, TemplateLiteral, BinaryExpression, LogicalExpression, ConditionalExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState } from '../../types' */ import { walk } from 'zimmerframe'; @@ -74,6 +74,199 @@ function compare_expressions(a, b) { return true; } +export const DYNAMIC = Symbol('DYNAMIC'); + +/** + * + * @param {Expression | Node} node + * @param {ComponentClientTransformState} state + * @returns {any} + */ +export function evaluate_static_expression(node, state) { + if (node == undefined) return DYNAMIC; + /** + * + * @param {BinaryExpression | LogicalExpression} node + */ + function handle_left_right_node(node) { + let left = evaluate_static_expression(node?.left, state); + let right = evaluate_static_expression(node?.right, state); + if (left === DYNAMIC || right === DYNAMIC) { + return DYNAMIC; + } + switch (node.operator) { + case '+': + return left + right; + case '-': + return left - right; + case '&': + return left & right; + case '|': + return left | right; + case '<<': + return left << right; + case '>>': + return left >> right; + case '>': + return left > right; + case '<': + return left < right; + case '>=': + return left >= right; + case '<=': + return left <= right; + case '==': + return left == right; + case '===': + return left === right; + case '||': + return left || right; + case '??': + return left ?? right; + case '&&': + return left && right; + case '%': + return left % right; + case '>>>': + return left >>> right; + case '^': + return left ^ right; + case '**': + return left ** right; + case '*': + return left * right; + case '/': + return left / right; + case '!=': + return left != right; + case '!==': + return left !== right; + default: + return DYNAMIC; + } + } + /** + * + * @param {UnaryExpression} node + */ + function handle_unary_node(node) { + let argument = evaluate_static_expression(node?.argument, state); + if (argument === DYNAMIC) return DYNAMIC; + /** + * + * @param {Expression} argument + */ + function handle_void(argument) { + //@ts-ignore + let evaluated = evaluate_static_expression(argument); + if (evaluated !== DYNAMIC) { + return undefined; + } + return DYNAMIC; + } + switch (node.operator) { + case '!': + return !argument; + case '-': + return -argument; + case 'typeof': + return typeof argument; + case '~': + return ~argument; + case '+': + return +argument; + case 'void': + return handle_void(argument); + default: + // `delete` is ignored, since it may have side effects + return DYNAMIC; + } + } + /** + * @param {SequenceExpression} node + */ + function handle_sequence(node) { + let is_static = node.expressions.reduce((a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, true); + if (is_static) { + //@ts-ignore + return evaluate_static_expression(node.expressions.at(-1), state); + } + return DYNAMIC; + } + /** + * @param {string} name + */ + function handle_ident(name) { + let scope = state.scope.get(name); + if (scope?.kind === 'normal') { + if (scope.initial && !scope.mutated && !scope.reassigned && !scope.updated) { + //@ts-ignore + let evaluated = evaluate_static_expression(scope.initial, state); + return evaluated; + } + } + return DYNAMIC; + } + /** + * @param {TemplateLiteral} node + */ + function handle_template(node) { + const expressions = node.expressions; + const is_static = expressions.reduce((a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, true); + if (is_static) { + let res = ''; + let quasis = node.quasis; + let last_was_quasi = false; + let expr_index = 0; + let quasi_index = 0; + for (let index = 0; index < quasis.length + expressions.length; index++) { + if (last_was_quasi) { + res += evaluate_static_expression(expressions[expr_index++], state); + last_was_quasi = false; + } else { + res += quasis[quasi_index++].value.cooked; + last_was_quasi = true; + } + } + return res; + } + return DYNAMIC; + } + /** + * @param {ConditionalExpression} node + */ + function handle_ternary(node) { + let test = evaluate_static_expression(node.test, state); + if (test !== DYNAMIC) { + if (test) { + return evaluate_static_expression(node.consequent, state); + } else { + return evaluate_static_expression(node.alternate, state); + } + } + return DYNAMIC; + } + switch (node.type) { + case 'Literal': + return node.value; + case 'BinaryExpression': + return handle_left_right_node(node); + case 'LogicalExpression': + return handle_left_right_node(node); + case 'UnaryExpression': + return handle_unary_node(node); + case 'Identifier': + return handle_ident(node.name); + case 'SequenceExpression': + return handle_sequence(node); + case 'TemplateLiteral': + return handle_template(node); + case 'ConditionalExpression': + return handle_ternary(node); + default: + return DYNAMIC; + } +} /** * @param {Array} values @@ -110,6 +303,13 @@ export function build_template_chunk( node.expression.name !== 'undefined' || state.scope.get('undefined') ) { + let evaluated = evaluate_static_expression(node.expression, state); + if (evaluated !== DYNAMIC) { + if (evaluated != null) { + quasi.value.cooked += evaluated + ''; + } + continue; + } let value = memoize( /** @type {Expression} */ (visit(node.expression, state)), node.metadata.expression From 7d4dca674c6ab845d7f32a0b3c9ce6d370e9a065 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:38:30 -0800 Subject: [PATCH 02/16] changeset --- .changeset/calm-beds-decide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/calm-beds-decide.md diff --git a/.changeset/calm-beds-decide.md b/.changeset/calm-beds-decide.md new file mode 100644 index 000000000000..fd6d75a1dc6c --- /dev/null +++ b/.changeset/calm-beds-decide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: static template expression evaluation From 4533ecba2a9dcbf8fe030f38bf4c9704abd97fd3 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:57:16 -0800 Subject: [PATCH 03/16] fix 1 failing test, maybe fix another --- .../_expected/client/index.svelte.js | 6 +++--- .../tests/sourcemaps/samples/attached-sourcemap/_config.js | 3 +-- 2 files changed, 4 insertions(+), 5 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 332c909ebed9..df8466f78be1 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); } diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js index ee9a3d92c4fb..38361cf24306 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js @@ -54,8 +54,7 @@ export default test({ get_processor('style', '.done_replace_style_1', '.done_replace_style_2') ], client: [ - { str: 'replace_me_script', strGenerated: 'done_replace_script_2' }, - { str: 'done_replace_script_2', idxGenerated: 1 } + { str: 'replace_me_script', strGenerated: 'done_replace_script_2' } ], css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-o6vre' }], test({ assert, code_preprocessed, code_css }) { From 29500fc7a5a6be348c88419606b3ed22dd7d8560 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 21:59:35 -0800 Subject: [PATCH 04/16] oops --- .../_expected/client/index.svelte.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 df8466f78be1..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 @@ -26,7 +26,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); - h1_1.textContent = 'Hello, world!'; + h1_1.textContent = 'Hello, world'; $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); } From 9f299f7ccb6849a45fc113ed8d1f43457de53de1 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 22:02:53 -0800 Subject: [PATCH 05/16] lint --- .../tests/sourcemaps/samples/attached-sourcemap/_config.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js index 38361cf24306..5fc835d58c65 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js @@ -53,9 +53,7 @@ export default test({ get_processor('style', '.replace_me_style', '.done_replace_style_1'), get_processor('style', '.done_replace_style_1', '.done_replace_style_2') ], - client: [ - { str: 'replace_me_script', strGenerated: 'done_replace_script_2' } - ], + client: [{ str: 'replace_me_script', strGenerated: 'done_replace_script_2' }], css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-o6vre' }], test({ assert, code_preprocessed, code_css }) { assert.equal( From 57dc52031e2d47eb0c15aa8ff56201eb4ebe829e Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 22:07:54 -0800 Subject: [PATCH 06/16] more linting and a few code style changes --- .../client/visitors/shared/element.js | 9 +++++++-- .../client/visitors/shared/utils.js | 20 ++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index ce26cecf8fd3..2b1fbd312fa3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -6,7 +6,12 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; import { build_getter } from '../../utils.js'; -import { build_template_chunk, get_expression_id, DYNAMIC, evaluate_static_expression } from './utils.js'; +import { + build_template_chunk, + get_expression_id, + DYNAMIC, + evaluate_static_expression +} from './utils.js'; /** * @param {Array} attributes @@ -188,7 +193,7 @@ export function build_attribute_value(value, context, memoize = (value) => value let expression = /** @type {Expression} */ (context.visit(chunk.expression)); let evaluated = evaluate_static_expression(expression, context.state); if (evaluated !== DYNAMIC) { - return { value: b.literal(evaluated), has_state: false}; + return { value: b.literal(evaluated), has_state: false }; } return { value: memoize(expression, chunk.metadata.expression), 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 70e5b7c611c6..35066946c37f 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 @@ -77,7 +77,6 @@ function compare_expressions(a, b) { export const DYNAMIC = Symbol('DYNAMIC'); /** - * * @param {Expression | Node} node * @param {ComponentClientTransformState} state * @returns {any} @@ -85,7 +84,6 @@ export const DYNAMIC = Symbol('DYNAMIC'); export function evaluate_static_expression(node, state) { if (node == undefined) return DYNAMIC; /** - * * @param {BinaryExpression | LogicalExpression} node */ function handle_left_right_node(node) { @@ -146,15 +144,13 @@ export function evaluate_static_expression(node, state) { } } /** - * * @param {UnaryExpression} node */ function handle_unary_node(node) { let argument = evaluate_static_expression(node?.argument, state); if (argument === DYNAMIC) return DYNAMIC; /** - * - * @param {Expression} argument + * @param {Expression} argument */ function handle_void(argument) { //@ts-ignore @@ -183,10 +179,13 @@ export function evaluate_static_expression(node, state) { } } /** - * @param {SequenceExpression} node + * @param {SequenceExpression} node */ function handle_sequence(node) { - let is_static = node.expressions.reduce((a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, true); + let is_static = node.expressions.reduce( + (a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, + true + ); if (is_static) { //@ts-ignore return evaluate_static_expression(node.expressions.at(-1), state); @@ -208,11 +207,14 @@ export function evaluate_static_expression(node, state) { return DYNAMIC; } /** - * @param {TemplateLiteral} node + * @param {TemplateLiteral} node */ function handle_template(node) { const expressions = node.expressions; - const is_static = expressions.reduce((a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, true); + const is_static = expressions.reduce( + (a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, + true + ); if (is_static) { let res = ''; let quasis = node.quasis; From 945973b3583b38804214ee1439fb58930a40ad88 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 22 Feb 2025 22:22:11 -0800 Subject: [PATCH 07/16] minor tweaks/fixes --- .../client/visitors/shared/utils.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 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 35066946c37f..5be0be8185c2 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 @@ -77,7 +77,7 @@ function compare_expressions(a, b) { export const DYNAMIC = Symbol('DYNAMIC'); /** - * @param {Expression | Node} node + * @param {Node} node * @param {ComponentClientTransformState} state * @returns {any} */ @@ -86,9 +86,9 @@ export function evaluate_static_expression(node, state) { /** * @param {BinaryExpression | LogicalExpression} node */ - function handle_left_right_node(node) { - let left = evaluate_static_expression(node?.left, state); - let right = evaluate_static_expression(node?.right, state); + function handle_left_right(node) { + const left = evaluate_static_expression(node?.left, state); + const right = evaluate_static_expression(node?.right, state); if (left === DYNAMIC || right === DYNAMIC) { return DYNAMIC; } @@ -146,15 +146,15 @@ export function evaluate_static_expression(node, state) { /** * @param {UnaryExpression} node */ - function handle_unary_node(node) { - let argument = evaluate_static_expression(node?.argument, state); + function handle_unary(node) { + const argument = evaluate_static_expression(node?.argument, state); if (argument === DYNAMIC) return DYNAMIC; /** * @param {Expression} argument */ function handle_void(argument) { //@ts-ignore - let evaluated = evaluate_static_expression(argument); + const evaluated = evaluate_static_expression(argument); if (evaluated !== DYNAMIC) { return undefined; } @@ -182,7 +182,7 @@ export function evaluate_static_expression(node, state) { * @param {SequenceExpression} node */ function handle_sequence(node) { - let is_static = node.expressions.reduce( + const is_static = node.expressions.reduce( (a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, true ); @@ -196,8 +196,8 @@ export function evaluate_static_expression(node, state) { * @param {string} name */ function handle_ident(name) { - let scope = state.scope.get(name); - if (scope?.kind === 'normal') { + const scope = state.scope.get(name); + if (scope?.kind === 'normal' && scope?.declaration_kind !== 'import') { if (scope.initial && !scope.mutated && !scope.reassigned && !scope.updated) { //@ts-ignore let evaluated = evaluate_static_expression(scope.initial, state); @@ -211,13 +211,13 @@ export function evaluate_static_expression(node, state) { */ function handle_template(node) { const expressions = node.expressions; + const quasis = node.quasis; const is_static = expressions.reduce( (a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, true ); if (is_static) { let res = ''; - let quasis = node.quasis; let last_was_quasi = false; let expr_index = 0; let quasi_index = 0; @@ -238,7 +238,7 @@ export function evaluate_static_expression(node, state) { * @param {ConditionalExpression} node */ function handle_ternary(node) { - let test = evaluate_static_expression(node.test, state); + const test = evaluate_static_expression(node.test, state); if (test !== DYNAMIC) { if (test) { return evaluate_static_expression(node.consequent, state); @@ -252,11 +252,11 @@ export function evaluate_static_expression(node, state) { case 'Literal': return node.value; case 'BinaryExpression': - return handle_left_right_node(node); + return handle_left_right(node); case 'LogicalExpression': - return handle_left_right_node(node); + return handle_left_right(node); case 'UnaryExpression': - return handle_unary_node(node); + return handle_unary(node); case 'Identifier': return handle_ident(node.name); case 'SequenceExpression': From bdc2ac5e2c6cfc12a2f2dee26bcd3e9e6ca8c29b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 17:55:08 -0800 Subject: [PATCH 08/16] add test --- .../_expected/_expected/_config.js | 3 + .../_expected/client/index.svelte.js | 58 +++++++++++++++++++ .../_expected/server/index.svelte.js | 13 +++++ .../_expected/_expected/index.svelte | 16 +++++ 4 files changed, 90 insertions(+) create mode 100644 packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/index.svelte diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_config.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js new file mode 100644 index 000000000000..a1f7dd3cad86 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js @@ -0,0 +1,58 @@ +import 'svelte/internal/disclose-version'; +import * as $ from 'svelte/internal/client'; + +var on_click = (_, count) => $.update(count); +var root = $.template(`

`, 1); + +export default function App($$anchor) { + let a = 1; + let b = 2; + let name = 'world'; + let count = $.state(0); + + function Component() {} // placeholder component + + var fragment = root(); + var h1 = $.first_child(fragment); + + h1.textContent = 'Hello, world!'; + + var p = $.sibling(h1, 2); + + p.textContent = '1 + 2 = 3'; + + var button = $.sibling(p, 2); + + button.__click = [on_click, count]; + + var text = $.child(button); + + $.reset(button); + + var p_1 = $.sibling(button, 2); + + p_1.textContent = '1 + 2 = 3'; + + var p_2 = $.sibling(p_1, 2); + + p_2.textContent = 'Sum is 3'; + + var p_3 = $.sibling(p_2, 2); + + p_3.textContent = '1'; + + var node = $.sibling(p_3, 2); + + Component(node, { + a: 1, + get count() { + return $.get(count); + }, + c: 3 + }); + + $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + $.append($$anchor, fragment); +} + +$.delegate(['click']); \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js new file mode 100644 index 000000000000..1476b11e3b60 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js @@ -0,0 +1,13 @@ +import * as $ from 'svelte/internal/server'; + +export default function App($$payload) { + let a = 1; + let b = 2; + let name = 'world'; + let count = 0; + + function Component() {} // placeholder component + $$payload.out += `

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

${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}

1 + 2 = ${$.escape(1 + 2)}

Sum is ${$.escape((a, b, a + b))}

${$.escape(a === 1 ? a : b)}

`; + Component($$payload, { a, count, c: a + b }); + $$payload.out += ``; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/index.svelte b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/index.svelte new file mode 100644 index 000000000000..8fc69ad7873a --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/index.svelte @@ -0,0 +1,16 @@ + +

Hello, {name}!

+

{a} + {b} = {a + b}

+ +

{1} + {2} = {1 + 2}

+

Sum is {(a, b, a + b)}

+

{a === 1 ? a : b}

+ \ No newline at end of file From 5ad8dcb396f957824e6257bd3ae128289707074d Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:01:21 -0800 Subject: [PATCH 09/16] maybe this'll fix it --- .../_expected/_expected/_expected/client/index.svelte.js | 2 +- .../_expected/_expected/_expected/server/index.svelte.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js index a1f7dd3cad86..c343230f5338 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js @@ -4,7 +4,7 @@ import * as $ from 'svelte/internal/client'; var on_click = (_, count) => $.update(count); var root = $.template(`

`, 1); -export default function App($$anchor) { +export default function Static_template_expression_evaluation($$anchor) { let a = 1; let b = 2; let name = 'world'; diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js index 1476b11e3b60..78194b3b5605 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js @@ -1,6 +1,6 @@ import * as $ from 'svelte/internal/server'; -export default function App($$payload) { +export default function Static_template_expression_evaluation($$payload) { let a = 1; let b = 2; let name = 'world'; From 855c72ad5e65bf55a450acdf5dd4f33863b23e34 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:17:49 -0800 Subject: [PATCH 10/16] doh --- .../{_expected/_expected => }/_config.js | 0 .../_expected/{_expected/_expected => }/client/index.svelte.js | 2 +- .../_expected/{_expected/_expected => }/server/index.svelte.js | 0 .../{_expected/_expected => }/index.svelte | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/{_expected/_expected => }/_config.js (100%) rename packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/{_expected/_expected => }/client/index.svelte.js (98%) rename packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/{_expected/_expected => }/server/index.svelte.js (100%) rename packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/{_expected/_expected => }/index.svelte (100%) diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_config.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_config.js similarity index 100% rename from packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_config.js rename to packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_config.js diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js similarity index 98% rename from packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js rename to packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js index c343230f5338..0adfc9e1b315 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js @@ -51,7 +51,7 @@ export default function Static_template_expression_evaluation($$anchor) { c: 3 }); - $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ""}`)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js similarity index 100% rename from packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/_expected/server/index.svelte.js rename to packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/index.svelte b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte similarity index 100% rename from packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/_expected/index.svelte rename to packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte From 1021f0d3bd316c07f83dbc6f71c080d5dc1c29b1 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:18:26 -0800 Subject: [PATCH 11/16] oops --- .../_expected/client/index.svelte.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js index 0adfc9e1b315..c343230f5338 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js @@ -51,7 +51,7 @@ export default function Static_template_expression_evaluation($$anchor) { c: 3 }); - $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ""}`)); + $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); } From 32ed9d52c8575bed6498aaed03d9bf196564e19b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:26:05 -0800 Subject: [PATCH 12/16] add to server, add stack overflow guard --- .../client/visitors/shared/element.js | 8 +- .../client/visitors/shared/utils.js | 198 +-------------- .../server/visitors/shared/utils.js | 11 +- .../3-transform/shared/static-evaluation.js | 230 ++++++++++++++++++ .../_expected/server/index.svelte.js | 2 +- 5 files changed, 245 insertions(+), 204 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 2b1fbd312fa3..2669df6beeb9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -6,12 +6,8 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; import { build_getter } from '../../utils.js'; -import { - build_template_chunk, - get_expression_id, - DYNAMIC, - evaluate_static_expression -} from './utils.js'; +import { build_template_chunk, get_expression_id } from './utils.js'; +import { evaluate_static_expression, DYNAMIC } from '../../../shared/static-evaluation.js'; /** * @param {Array} attributes 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 5be0be8185c2..640dc9832ce2 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 @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super, Node, UnaryExpression, TemplateLiteral, BinaryExpression, LogicalExpression, ConditionalExpression } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState } from '../../types' */ import { walk } from 'zimmerframe'; @@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { locator } from '../../../../../state.js'; import { create_derived } from '../../utils.js'; +import { evaluate_static_expression, DYNAMIC } from '../../../shared/static-evaluation.js'; /** * @param {ComponentClientTransformState} state @@ -74,201 +75,6 @@ function compare_expressions(a, b) { return true; } -export const DYNAMIC = Symbol('DYNAMIC'); - -/** - * @param {Node} node - * @param {ComponentClientTransformState} state - * @returns {any} - */ -export function evaluate_static_expression(node, state) { - if (node == undefined) return DYNAMIC; - /** - * @param {BinaryExpression | LogicalExpression} node - */ - function handle_left_right(node) { - const left = evaluate_static_expression(node?.left, state); - const right = evaluate_static_expression(node?.right, state); - if (left === DYNAMIC || right === DYNAMIC) { - return DYNAMIC; - } - switch (node.operator) { - case '+': - return left + right; - case '-': - return left - right; - case '&': - return left & right; - case '|': - return left | right; - case '<<': - return left << right; - case '>>': - return left >> right; - case '>': - return left > right; - case '<': - return left < right; - case '>=': - return left >= right; - case '<=': - return left <= right; - case '==': - return left == right; - case '===': - return left === right; - case '||': - return left || right; - case '??': - return left ?? right; - case '&&': - return left && right; - case '%': - return left % right; - case '>>>': - return left >>> right; - case '^': - return left ^ right; - case '**': - return left ** right; - case '*': - return left * right; - case '/': - return left / right; - case '!=': - return left != right; - case '!==': - return left !== right; - default: - return DYNAMIC; - } - } - /** - * @param {UnaryExpression} node - */ - function handle_unary(node) { - const argument = evaluate_static_expression(node?.argument, state); - if (argument === DYNAMIC) return DYNAMIC; - /** - * @param {Expression} argument - */ - function handle_void(argument) { - //@ts-ignore - const evaluated = evaluate_static_expression(argument); - if (evaluated !== DYNAMIC) { - return undefined; - } - return DYNAMIC; - } - switch (node.operator) { - case '!': - return !argument; - case '-': - return -argument; - case 'typeof': - return typeof argument; - case '~': - return ~argument; - case '+': - return +argument; - case 'void': - return handle_void(argument); - default: - // `delete` is ignored, since it may have side effects - return DYNAMIC; - } - } - /** - * @param {SequenceExpression} node - */ - function handle_sequence(node) { - const is_static = node.expressions.reduce( - (a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, - true - ); - if (is_static) { - //@ts-ignore - return evaluate_static_expression(node.expressions.at(-1), state); - } - return DYNAMIC; - } - /** - * @param {string} name - */ - function handle_ident(name) { - const scope = state.scope.get(name); - if (scope?.kind === 'normal' && scope?.declaration_kind !== 'import') { - if (scope.initial && !scope.mutated && !scope.reassigned && !scope.updated) { - //@ts-ignore - let evaluated = evaluate_static_expression(scope.initial, state); - return evaluated; - } - } - return DYNAMIC; - } - /** - * @param {TemplateLiteral} node - */ - function handle_template(node) { - const expressions = node.expressions; - const quasis = node.quasis; - const is_static = expressions.reduce( - (a, b) => a && evaluate_static_expression(b, state) !== DYNAMIC, - true - ); - if (is_static) { - let res = ''; - let last_was_quasi = false; - let expr_index = 0; - let quasi_index = 0; - for (let index = 0; index < quasis.length + expressions.length; index++) { - if (last_was_quasi) { - res += evaluate_static_expression(expressions[expr_index++], state); - last_was_quasi = false; - } else { - res += quasis[quasi_index++].value.cooked; - last_was_quasi = true; - } - } - return res; - } - return DYNAMIC; - } - /** - * @param {ConditionalExpression} node - */ - function handle_ternary(node) { - const test = evaluate_static_expression(node.test, state); - if (test !== DYNAMIC) { - if (test) { - return evaluate_static_expression(node.consequent, state); - } else { - return evaluate_static_expression(node.alternate, state); - } - } - return DYNAMIC; - } - switch (node.type) { - case 'Literal': - return node.value; - case 'BinaryExpression': - return handle_left_right(node); - case 'LogicalExpression': - return handle_left_right(node); - case 'UnaryExpression': - return handle_unary(node); - case 'Identifier': - return handle_ident(node.name); - case 'SequenceExpression': - return handle_sequence(node); - case 'TemplateLiteral': - return handle_template(node); - case 'ConditionalExpression': - return handle_ternary(node); - default: - return DYNAMIC; - } -} /** * @param {Array} values 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..439c30286580 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 @@ -11,6 +11,7 @@ import { import * as b from '../../../../../utils/builders.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; +import { evaluate_static_expression, DYNAMIC } from '../../../shared/static-evaluation.js'; /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ export const block_open = b.literal(BLOCK_OPEN); @@ -49,7 +50,15 @@ export function process_children(nodes, { visit, state }) { quasi.value.cooked += escape_html(node.expression.value + ''); } } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + let evaluated = evaluate_static_expression(node.expression, state, true); + if (evaluated === DYNAMIC) { + expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + } else { + if (evaluated != null) { + quasi.value.cooked += escape_html(evaluated + ''); + } + continue; + } quasi = b.quasi('', i + 1 === sequence.length); quasis.push(quasi); diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js new file mode 100644 index 000000000000..d910f07fefa4 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js @@ -0,0 +1,230 @@ +/** @import { Node, BinaryExpression, LogicalExpression, UnaryExpression, Expression, SequenceExpression, TemplateLiteral, ConditionalExpression } from 'estree' */ +/** @import { ComponentClientTransformState } from '../client/types' */ +/** @import { ComponentServerTransformState } from '../server/types' */ +export const DYNAMIC = Symbol('DYNAMIC'); + +/** + * @template {boolean} S + * @param {Node} node + * @param {S extends true ? ComponentServerTransformState : ComponentClientTransformState} state + * @param {S} [server=false] + * @returns {any} + */ +export function evaluate_static_expression(node, state, server) { + /** + * @template {boolean} S + * @param {Node} node + * @param {S extends true ? ComponentServerTransformState : ComponentClientTransformState} state + * @param {S} [server] + * @returns {any} + */ + function internal(node, state, server) { + if (node == undefined) return DYNAMIC; + /** + * @param {BinaryExpression | LogicalExpression} node + */ + function handle_left_right(node) { + const left = internal(node?.left, state, server); + const right = internal(node?.right, state, server); + if (left === DYNAMIC || right === DYNAMIC) { + return DYNAMIC; + } + switch (node.operator) { + case '+': + return left + right; + case '-': + return left - right; + case '&': + return left & right; + case '|': + return left | right; + case '<<': + return left << right; + case '>>': + return left >> right; + case '>': + return left > right; + case '<': + return left < right; + case '>=': + return left >= right; + case '<=': + return left <= right; + case '==': + return left == right; + case '===': + return left === right; + case '||': + return left || right; + case '??': + return left ?? right; + case '&&': + return left && right; + case '%': + return left % right; + case '>>>': + return left >>> right; + case '^': + return left ^ right; + case '**': + return left ** right; + case '*': + return left * right; + case '/': + return left / right; + case '!=': + return left != right; + case '!==': + return left !== right; + default: + return DYNAMIC; + } + } + /** + * @param {UnaryExpression} node + */ + function handle_unary(node) { + const argument = internal(node?.argument, state, server); + if (argument === DYNAMIC) return DYNAMIC; + /** + * @param {Expression} argument + */ + function handle_void(argument) { + //@ts-ignore + const evaluated = internal(argument, state, server); + if (evaluated !== DYNAMIC) { + return undefined; + } + return DYNAMIC; + } + switch (node.operator) { + case '!': + return !argument; + case '-': + return -argument; + case 'typeof': + return typeof argument; + case '~': + return ~argument; + case '+': + return +argument; + case 'void': + return handle_void(argument); + default: + // `delete` is ignored, since it may have side effects + return DYNAMIC; + } + } + /** + * @param {SequenceExpression} node + */ + function handle_sequence(node) { + const is_static = node.expressions.reduce( + (a, b) => a && internal(b, state, server) !== DYNAMIC, + true + ); + if (is_static) { + //@ts-ignore + return internal(node.expressions.at(-1), state, server); + } + return DYNAMIC; + } + /** + * @param {string} name + */ + function handle_ident(name) { + if (server) return DYNAMIC; + const scope = state.scope.get(name); + if (scope?.kind === 'normal' && scope?.declaration_kind !== 'import') { + if (scope.initial && !scope.mutated && !scope.reassigned && !scope.updated) { + //@ts-ignore + let evaluated = internal(scope.initial, state); + return evaluated; + } + } + return DYNAMIC; + } + /** + * @param {TemplateLiteral} node + */ + function handle_template(node) { + const expressions = node.expressions; + const quasis = node.quasis; + const is_static = expressions.reduce( + (a, b) => a && internal(b, state, server) !== DYNAMIC, + true + ); + if (is_static) { + let res = ''; + let last_was_quasi = false; + let expr_index = 0; + let quasi_index = 0; + for (let index = 0; index < quasis.length + expressions.length; index++) { + if (last_was_quasi) { + res += internal(expressions[expr_index++], state, server); + last_was_quasi = false; + } else { + res += quasis[quasi_index++].value.cooked; + last_was_quasi = true; + } + } + return res; + } + return DYNAMIC; + } + /** + * @param {ConditionalExpression} node + */ + function handle_ternary(node) { + const test = internal(node.test, state, server); + if (test !== DYNAMIC) { + if (test) { + return internal(node.consequent, state, server); + } else { + return internal(node.alternate, state, server); + } + } + return DYNAMIC; + } + switch (node.type) { + case 'Literal': + return node.value; + case 'BinaryExpression': + return handle_left_right(node); + case 'LogicalExpression': + return handle_left_right(node); + case 'UnaryExpression': + return handle_unary(node); + case 'Identifier': + return handle_ident(node.name); + case 'SequenceExpression': + return handle_sequence(node); + case 'TemplateLiteral': + return handle_template(node); + case 'ConditionalExpression': + return handle_ternary(node); + default: + return DYNAMIC; + } + } + try { + return internal(node, state, server ?? false); + } catch (err) { + // if the expression is so nested it causes a call stack overflow, then it's probably not static + // this probably won't ever happen, but just in case... + if (err instanceof RangeError && err.message === 'Maximum call stack size exceeded') { + return DYNAMIC; + } else if ( + //@ts-expect-error firefox has a non-standard recursion error + typeof globalThis['InternalError'] === 'function' && + //@ts-expect-error + err instanceof globalThis['InternalError'] && + //@ts-ignore + err.message === 'too much recursion' + ) { + return DYNAMIC; + } else { + throw err; + } + } +} diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js index 78194b3b5605..9449547f7d6a 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js @@ -7,7 +7,7 @@ export default function Static_template_expression_evaluation($$payload) { let count = 0; function Component() {} // placeholder component - $$payload.out += `

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

${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}

1 + 2 = ${$.escape(1 + 2)}

Sum is ${$.escape((a, b, a + b))}

${$.escape(a === 1 ? a : b)}

`; + $$payload.out += `

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

${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}

1 + 2 = 3

Sum is ${$.escape((a, b, a + b))}

${$.escape(a === 1 ? a : b)}

`; Component($$payload, { a, count, c: a + b }); $$payload.out += ``; } \ No newline at end of file From 440b93b7cbc55431ab944fd33526819a1129509e Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:29:12 -0800 Subject: [PATCH 13/16] fix failing test --- .../_expected/server/index.svelte.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..19fed683a772 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, ${$.escape(name)}!

123

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

`; } \ No newline at end of file From 9907cbcaf683383a7c08a8cbf912bd8abe2d88b7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:34:07 -0800 Subject: [PATCH 14/16] YALF (yet another lint fix) --- .../compiler/phases/3-transform/shared/static-evaluation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js index d910f07fefa4..5b2a48d24801 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js @@ -217,9 +217,9 @@ export function evaluate_static_expression(node, state, server) { } else if ( //@ts-expect-error firefox has a non-standard recursion error typeof globalThis['InternalError'] === 'function' && - //@ts-expect-error + //@ts-expect-error err instanceof globalThis['InternalError'] && - //@ts-ignore + //@ts-ignore err.message === 'too much recursion' ) { return DYNAMIC; From 1853da4cbaa3408d08fca3e0d22200bd846060bc Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 11 Mar 2025 21:35:56 -0700 Subject: [PATCH 15/16] add basic `CallExpression` optimizations (with tests) --- .../3-transform/shared/static-evaluation.js | 111 +++++++++++++++++- .../_expected/client/index.svelte.js | 18 ++- .../_expected/server/index.svelte.js | 11 +- .../index.svelte | 10 +- 4 files changed, 143 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js index 5b2a48d24801..91ca5a7d9ee1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js @@ -1,8 +1,27 @@ -/** @import { Node, BinaryExpression, LogicalExpression, UnaryExpression, Expression, SequenceExpression, TemplateLiteral, ConditionalExpression } from 'estree' */ +/** @import { Node, BinaryExpression, LogicalExpression, UnaryExpression, Expression, SequenceExpression, TemplateLiteral, ConditionalExpression, CallExpression } from 'estree' */ /** @import { ComponentClientTransformState } from '../client/types' */ /** @import { ComponentServerTransformState } from '../server/types' */ export const DYNAMIC = Symbol('DYNAMIC'); - +if (!('difference' in Set.prototype)) { + /** + * Quick and dirty polfill for `Set.prototype.difference` + * @template T + * @this {Set} + * @param {Set} other + * @returns {Set} + */ + //@ts-ignore + Set.prototype.difference = function difference(other) { + /** @type {Set} */ + let res = new Set(); + for (let item of this) { + if (!other.has(item)) { + res.add(item); + } + } + return res; + }; +} /** * @template {boolean} S * @param {Node} node @@ -135,6 +154,7 @@ export function evaluate_static_expression(node, state, server) { function handle_ident(name) { if (server) return DYNAMIC; const scope = state.scope.get(name); + // TODO tweak this when implicit top-level reactivity is removed if (scope?.kind === 'normal' && scope?.declaration_kind !== 'import') { if (scope.initial && !scope.mutated && !scope.reassigned && !scope.updated) { //@ts-ignore @@ -186,6 +206,91 @@ export function evaluate_static_expression(node, state, server) { } return DYNAMIC; } + /** + * @param {CallExpression} node + */ + function handle_call(node) { + /** + * There isn't much we can really do here (without having an unreasonable amount of code), + * so we don't optimize for much + * We only optimize for these: + * ``` + * (() => identifier_or_evaluable_value)(); + * (() => { + * return evaluable; + * })(); + * (() => { + * let variable = ident_or_evaluable_value; + * return variable; + * }); + * // it's fine if we don't want to use this for side effect reasons, its just one line (251) + * (() => { + * anything_but_a_return; + * })() + * ``` + * I would like to possibly optimize this: + * ``` + * (() => { + * let variable = ident_or_evaluable_value; + * return variable_combined_with_evaluable; + * })(); + * ``` + * But I don't know how to do that with the `Scope` class. + */ + let { callee } = node; + if ( + callee.type !== 'ArrowFunctionExpression' || + node.arguments.length || + callee.params.length + ) { + return DYNAMIC; + } + let { body } = callee; + if (body.type === 'BlockStatement') { + let children = body.body; + if (!children.find(({ type }) => type === 'ReturnStatement')) return undefined; + if (children.length === 1 && children[0].type === 'ReturnStatement') { + return children[0].argument == null + ? undefined + : internal(children[0].argument, state, server); + } + let valid_body_children = new Set([ + 'VariableDeclaration', + 'EmptyStatement', + 'ReturnStatement' + ]); + let types = new Set(children.map(({ type }) => type)); + if (types.difference(valid_body_children).size) { + return DYNAMIC; + } + if (types.has('EmptyStatement')) { + children = children.filter(({ type }) => type !== 'EmptyStatement'); + } + if (children.length > 2) return DYNAMIC; + if (children[0].type !== 'VariableDeclaration' || children[1].type !== 'ReturnStatement') + return DYNAMIC; + let [declaration, return_statement] = children; + if (declaration.declarations.length > 1) return DYNAMIC; + let [declarator] = declaration.declarations; + if (declarator.id.type !== 'Identifier') return DYNAMIC; + let variable_value; + if (declarator.init != null) { + variable_value = internal(declarator.init, state, server); + if (variable_value === DYNAMIC) { + //might be unpure + return DYNAMIC; + } + } + let { argument } = return_statement; + if (argument == null) return undefined; + if (argument.type === 'Identifier' && argument.name === declarator.id.name) { + return variable_value; + } + return internal(argument, state, server); + } else { + return internal(body, state, server); + } + } switch (node.type) { case 'Literal': return node.value; @@ -203,6 +308,8 @@ export function evaluate_static_expression(node, state, server) { return handle_template(node); case 'ConditionalExpression': return handle_ternary(node); + case 'CallExpression': + return handle_call(node); default: return DYNAMIC; } diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js index c343230f5338..712dabf63cfa 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/client/index.svelte.js @@ -2,9 +2,11 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; var on_click = (_, count) => $.update(count); -var root = $.template(`

`, 1); +var root = $.template(`



`, 1); + +export default function Static_template_expression_evaluation($$anchor, $$props) { + $.push($$props, true); -export default function Static_template_expression_evaluation($$anchor) { let a = 1; let b = 2; let name = 'world'; @@ -51,8 +53,20 @@ export default function Static_template_expression_evaluation($$anchor) { c: 3 }); + var text_1 = $.sibling(node); + + text_1.nodeValue = ' 0'; + + var text_2 = $.sibling(text_1, 2); + + text_2.nodeValue = ' Hello, world!'; + + var text_3 = $.sibling(text_2, 2); + + text_3.nodeValue = ' 3'; $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); $.append($$anchor, fragment); + $.pop(); } $.delegate(['click']); \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js index 9449547f7d6a..f4f450bc34f2 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/_expected/server/index.svelte.js @@ -1,6 +1,8 @@ import * as $ from 'svelte/internal/server'; -export default function Static_template_expression_evaluation($$payload) { +export default function Static_template_expression_evaluation($$payload, $$props) { + $.push(); + let a = 1; let b = 2; let name = 'world'; @@ -9,5 +11,10 @@ export default function Static_template_expression_evaluation($$payload) { function Component() {} // placeholder component $$payload.out += `

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

${$.escape(a)} + ${$.escape(b)} = ${$.escape(a + b)}

1 + 2 = 3

Sum is ${$.escape((a, b, a + b))}

${$.escape(a === 1 ? a : b)}

`; Component($$payload, { a, count, c: a + b }); - $$payload.out += ``; + + $$payload.out += ` 0
Hello, world!
${$.escape((() => { + return a + b; + })())}`; + + $.pop(); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte index 8fc69ad7873a..59649e31fd49 100644 --- a/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte +++ b/packages/svelte/tests/snapshot/samples/static-template-expression-evaluation/index.svelte @@ -13,4 +13,12 @@

{1} + {2} = {1 + 2}

Sum is {(a, b, a + b)}

{a === 1 ? a : b}

- \ No newline at end of file + +{(() => { + let thing = 0; + return thing; +})()}
+{(() => 'Hello, world!')()}
+{(() => { + return a + b; +})()} \ No newline at end of file From 225a01635fc706d2d0d59019922b952a884eb2b4 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 11 Mar 2025 21:41:43 -0700 Subject: [PATCH 16/16] oops, didn't think that through --- .../compiler/phases/3-transform/shared/static-evaluation.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js index 91ca5a7d9ee1..5bac45a5ec45 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/static-evaluation.js @@ -223,10 +223,6 @@ export function evaluate_static_expression(node, state, server) { * let variable = ident_or_evaluable_value; * return variable; * }); - * // it's fine if we don't want to use this for side effect reasons, its just one line (251) - * (() => { - * anything_but_a_return; - * })() * ``` * I would like to possibly optimize this: * ``` @@ -248,7 +244,6 @@ export function evaluate_static_expression(node, state, server) { let { body } = callee; if (body.type === 'BlockStatement') { let children = body.body; - if (!children.find(({ type }) => type === 'ReturnStatement')) return undefined; if (children.length === 1 && children[0].type === 'ReturnStatement') { return children[0].argument == null ? undefined