Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: static template expression evaluation #15374

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/calm-beds-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: static template expression evaluation
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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 { evaluate_static_expression, DYNAMIC } from '../../../shared/static-evaluation.js';

/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
Expand Down Expand Up @@ -186,7 +187,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,6 +111,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) {
let name = 'world';
let count = 0;

$$payload.out += `<h1>Hello, ${$.escape(name)}!</h1> <b>${$.escape(1 ?? 'stuff')}${$.escape(2 ?? 'more stuff')}${$.escape(3 ?? 'even more stuff')}</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, ${$.escape(name ?? 'earth' ?? null)}</h1>`;
$$payload.out += `<h1>Hello, ${$.escape(name)}!</h1> <b>123</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, ${$.escape(name ?? 'earth' ?? null)}</h1>`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from '../../test';

export default test({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';

var on_click = (_, count) => $.update(count);
var root = $.template(`<h1></h1> <p></p> <button> </button> <p></p> <p></p> <p></p> <!>`, 1);

export default function Static_template_expression_evaluation($$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']);
Loading