From cbb08ad25f81cfde9cc41074659ed3e8f7904138 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:07:23 -0700 Subject: [PATCH 01/25] init --- .../client/visitors/shared/utils.js | 9 +- packages/svelte/src/compiler/phases/scope.js | 133 +++++++++++++++++- 2 files changed, 138 insertions(+), 4 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 55362d75afd1..3c6d04539b93 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 @@ -69,11 +69,14 @@ export function build_template_chunk( node.metadata.expression ); - has_state ||= node.metadata.expression.has_state; - if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). + const evaluated = state.scope.evaluate(value); + has_state ||= node.metadata.expression.has_state && !evaluated.is_known; + if (evaluated.is_known) { + value = b.literal(evaluated.value); + } return { value, has_state }; } @@ -91,6 +94,8 @@ export function build_template_chunk( const evaluated = state.scope.evaluate(value); + has_state ||= node.metadata.expression.has_state && !evaluated.is_known; + if (evaluated.is_known) { quasi.value.cooked += evaluated.value + ''; } else { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 73dfeea1d9b0..e1e1466786ea 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -15,6 +15,7 @@ import { 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'; +import { bind_window_scroll } from 'svelte/internal/client'; export const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ @@ -173,14 +174,24 @@ class Evaluation { binding.kind === 'bindable_prop'; if (!binding.updated && binding.initial !== null && !is_prop) { + console.log(binding.initial); const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); for (const value of evaluation.values) { this.values.add(value); } break; } - - // TODO each index is always defined + if ( + binding.kind === 'each' && + binding.initial?.type === 'EachBlock' && + binding.initial.index === expression.name + ) { + this.values.add(NUMBER); + break; + } + } else if (expression.name === 'undefined') { + this.values.add(undefined); + break; } // TODO glean what we can from reassignments @@ -336,6 +347,124 @@ class Evaluation { break; } + case 'CallExpression': + { + const rune = get_rune(expression, scope); + if ( + rune && + scope.get( + object(/** @type {Identifier | MemberExpression} */ (expression.callee))?.name ?? '' + ) === null + ) { + switch (rune) { + case '$state': + case '$state.raw': + if (expression.arguments.length) { + const evaluated = scope.evaluate( + /** @type {Expression} */ (expression.arguments[0]) + ); + for (let value of evaluated.values) { + this.values.add(value); + } + } else { + this.values.add(undefined); + } + break; + case '$props.id': + this.values.add(STRING); + break; + case '$effect.tracking': + this.values.add(false); + this.values.add(true); + break; + case '$derived': { + const evaluated = scope.evaluate( + /** @type {Expression} */ (expression.arguments[0]) + ); + for (let value of evaluated.values) { + this.values.add(value); + } + break; + } + case '$derived.by': + if (expression.arguments[0]?.type === 'ArrowFunctionExpression') { + if (expression.arguments[0].body?.type !== 'BlockStatement') { + const evaluated = scope.evaluate( + /** @type {Expression} */ (expression.arguments[0].body) + ); + for (let value of evaluated.values) { + this.values.add(value); + } + break; + } + } + default: { + this.values.add(UNKNOWN); + } + } + } else if ( + expression.callee.type === 'Identifier' && + scope.get(expression.callee.name) === null + ) { + switch (expression.callee.name) { + case 'Number': { + if (expression.arguments.length) { + const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); + if (arg.is_known) { + this.values.add(Number(arg.value)); + } else if (arg.is_number) { + this.values.add(NUMBER); + } else { + for (let value of arg.values) { + switch (value) { + case STRING: + case NUMBER: + case UNKNOWN: + this.values.add(NUMBER); + break; + default: + this.values.add(Number(value)); + } + } + } + } else { + this.values.add(0); + } + break; + } + case 'String': + if (expression.arguments.length) { + const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); + if (arg.is_known) { + this.values.add(String(arg.value)); + } else if (arg.is_number || arg.is_string) { + this.values.add(STRING); + } else { + for (let value of arg.values) { + switch (value) { + case STRING: + case NUMBER: + case UNKNOWN: + this.values.add(STRING); + break; + default: + this.values.add(String(value)); + } + } + } + } else { + this.values.add(''); + } + break; + default: + this.values.add(UNKNOWN); + } + } else { + this.values.add(UNKNOWN); + } + } + break; + default: { this.values.add(UNKNOWN); } From e6c9e3498c6a7421aae2839ef1f0da5d307854a0 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:19:04 -0700 Subject: [PATCH 02/25] remove console log --- packages/svelte/src/compiler/phases/scope.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index e1e1466786ea..f1eba50c1cb7 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -174,7 +174,6 @@ class Evaluation { binding.kind === 'bindable_prop'; if (!binding.updated && binding.initial !== null && !is_prop) { - console.log(binding.initial); const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); for (const value of evaluation.values) { this.values.add(value); From 6bb97575504402817faefb58449865678248d1b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:13:54 -0400 Subject: [PATCH 03/25] Update packages/svelte/src/compiler/phases/scope.js --- packages/svelte/src/compiler/phases/scope.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f1eba50c1cb7..f4ef1a428e5b 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -15,7 +15,6 @@ import { 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'; -import { bind_window_scroll } from 'svelte/internal/client'; export const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ From 3a9aa796a3bfed3163670160757a9334446d3abe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:25:33 -0400 Subject: [PATCH 04/25] fix each indices --- packages/svelte/src/compiler/phases/scope.js | 13 +++++-------- .../samples/each-index-non-null/_config.js | 3 +++ .../_expected/client/index.svelte.js | 19 +++++++++++++++++++ .../_expected/server/index.svelte.js | 13 +++++++++++++ .../samples/each-index-non-null/index.svelte | 3 +++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f4ef1a428e5b..c22369e3a206 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -172,6 +172,11 @@ class Evaluation { binding.kind === 'rest_prop' || binding.kind === 'bindable_prop'; + if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) { + this.values.add(NUMBER); + break; + } + if (!binding.updated && binding.initial !== null && !is_prop) { const evaluation = binding.scope.evaluate(/** @type {Expression} */ (binding.initial)); for (const value of evaluation.values) { @@ -179,14 +184,6 @@ class Evaluation { } break; } - if ( - binding.kind === 'each' && - binding.initial?.type === 'EachBlock' && - binding.initial.index === expression.name - ) { - this.values.add(NUMBER); - break; - } } else if (expression.name === 'undefined') { this.values.add(undefined); break; diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js new file mode 100644 index 000000000000..3d46a679b865 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -0,0 +1,19 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var root_1 = $.template(`

`); + +export default function Each_index_non_null($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { + var p = root_1(); + + p.textContent = `index: ${i}`; + $.append($$anchor, p); + }); + + $.append($$anchor, fragment); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js new file mode 100644 index 000000000000..3431e36833b5 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -0,0 +1,13 @@ +import * as $ from 'svelte/internal/server'; + +export default function Each_index_non_null($$payload) { + const each_array = $.ensure_array_like(Array(10)); + + $$payload.out += ``; + + for (let i = 0, $$length = each_array.length; i < $$length; i++) { + $$payload.out += `

index: ${$.escape(i)}

`; + } + + $$payload.out += ``; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte new file mode 100644 index 000000000000..03bfc9e37299 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/index.svelte @@ -0,0 +1,3 @@ +{#each Array(10), i} +

index: {i}

+{/each} From db4538df5e1816784404f01e6654e6f29a7ce34a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:29:21 -0400 Subject: [PATCH 05/25] dedupe --- .../3-transform/client/visitors/shared/utils.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 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 3c6d04539b93..2aae1d0cb337 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 @@ -69,14 +69,17 @@ export function build_template_chunk( node.metadata.expression ); + const evaluated = state.scope.evaluate(value); + + has_state ||= node.metadata.expression.has_state && !evaluated.is_known; + if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - const evaluated = state.scope.evaluate(value); - has_state ||= node.metadata.expression.has_state && !evaluated.is_known; if (evaluated.is_known) { value = b.literal(evaluated.value); } + return { value, has_state }; } @@ -92,10 +95,6 @@ export function build_template_chunk( } } - const evaluated = state.scope.evaluate(value); - - has_state ||= node.metadata.expression.has_state && !evaluated.is_known; - if (evaluated.is_known) { quasi.value.cooked += evaluated.value + ''; } else { From c63cf040f851dd4f76fdafd532e6e7385f104c40 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:30:37 -0400 Subject: [PATCH 06/25] Update packages/svelte/src/compiler/phases/scope.js --- packages/svelte/src/compiler/phases/scope.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index c22369e3a206..6067dcee3544 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -345,12 +345,7 @@ class Evaluation { case 'CallExpression': { const rune = get_rune(expression, scope); - if ( - rune && - scope.get( - object(/** @type {Identifier | MemberExpression} */ (expression.callee))?.name ?? '' - ) === null - ) { + if (rune) { switch (rune) { case '$state': case '$state.raw': From 6844bc4962b8c8b37484983c7c89ff2c25cc67b2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:45:45 -0400 Subject: [PATCH 07/25] always break --- packages/svelte/src/compiler/phases/scope.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 6067dcee3544..4dddfd705f82 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -360,13 +360,16 @@ class Evaluation { this.values.add(undefined); } break; + case '$props.id': this.values.add(STRING); break; + case '$effect.tracking': this.values.add(false); this.values.add(true); break; + case '$derived': { const evaluated = scope.evaluate( /** @type {Expression} */ (expression.arguments[0]) @@ -376,6 +379,7 @@ class Evaluation { } break; } + case '$derived.by': if (expression.arguments[0]?.type === 'ArrowFunctionExpression') { if (expression.arguments[0].body?.type !== 'BlockStatement') { @@ -388,6 +392,10 @@ class Evaluation { break; } } + + this.values.add(UNKNOWN); + break; + default: { this.values.add(UNKNOWN); } From bc997c1fdb1d5fb23022c6e3fcb449af8d1e4e04 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:46:55 -0400 Subject: [PATCH 08/25] fix formatting --- packages/svelte/src/compiler/phases/scope.js | 192 +++++++++---------- 1 file changed, 95 insertions(+), 97 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 4dddfd705f82..f1a4681b5668 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -342,126 +342,124 @@ class Evaluation { break; } - case 'CallExpression': - { - const rune = get_rune(expression, scope); - if (rune) { - switch (rune) { - case '$state': - case '$state.raw': - if (expression.arguments.length) { - const evaluated = scope.evaluate( - /** @type {Expression} */ (expression.arguments[0]) - ); - for (let value of evaluated.values) { - this.values.add(value); - } - } else { - this.values.add(undefined); - } - break; - - case '$props.id': - this.values.add(STRING); - break; - - case '$effect.tracking': - this.values.add(false); - this.values.add(true); - break; - - case '$derived': { + case 'CallExpression': { + const rune = get_rune(expression, scope); + if (rune) { + switch (rune) { + case '$state': + case '$state.raw': + if (expression.arguments.length) { const evaluated = scope.evaluate( /** @type {Expression} */ (expression.arguments[0]) ); for (let value of evaluated.values) { this.values.add(value); } - break; + } else { + this.values.add(undefined); } + break; - case '$derived.by': - if (expression.arguments[0]?.type === 'ArrowFunctionExpression') { - if (expression.arguments[0].body?.type !== 'BlockStatement') { - const evaluated = scope.evaluate( - /** @type {Expression} */ (expression.arguments[0].body) - ); - for (let value of evaluated.values) { - this.values.add(value); - } - break; + case '$props.id': + this.values.add(STRING); + break; + + case '$effect.tracking': + this.values.add(false); + this.values.add(true); + break; + + case '$derived': { + const evaluated = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); + for (let value of evaluated.values) { + this.values.add(value); + } + break; + } + + case '$derived.by': + if (expression.arguments[0]?.type === 'ArrowFunctionExpression') { + if (expression.arguments[0].body?.type !== 'BlockStatement') { + const evaluated = scope.evaluate( + /** @type {Expression} */ (expression.arguments[0].body) + ); + for (let value of evaluated.values) { + this.values.add(value); } + break; } + } - this.values.add(UNKNOWN); - break; + this.values.add(UNKNOWN); + break; - default: { - this.values.add(UNKNOWN); - } + default: { + this.values.add(UNKNOWN); } - } else if ( - expression.callee.type === 'Identifier' && - scope.get(expression.callee.name) === null - ) { - switch (expression.callee.name) { - case 'Number': { - if (expression.arguments.length) { - const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); - if (arg.is_known) { - this.values.add(Number(arg.value)); - } else if (arg.is_number) { - this.values.add(NUMBER); - } else { - for (let value of arg.values) { - switch (value) { - case STRING: - case NUMBER: - case UNKNOWN: - this.values.add(NUMBER); - break; - default: - this.values.add(Number(value)); - } + } + } else if ( + expression.callee.type === 'Identifier' && + scope.get(expression.callee.name) === null + ) { + switch (expression.callee.name) { + case 'Number': { + if (expression.arguments.length) { + const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); + if (arg.is_known) { + this.values.add(Number(arg.value)); + } else if (arg.is_number) { + this.values.add(NUMBER); + } else { + for (let value of arg.values) { + switch (value) { + case STRING: + case NUMBER: + case UNKNOWN: + this.values.add(NUMBER); + break; + default: + this.values.add(Number(value)); } } - } else { - this.values.add(0); } - break; + } else { + this.values.add(0); } - case 'String': - if (expression.arguments.length) { - const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); - if (arg.is_known) { - this.values.add(String(arg.value)); - } else if (arg.is_number || arg.is_string) { - this.values.add(STRING); - } else { - for (let value of arg.values) { - switch (value) { - case STRING: - case NUMBER: - case UNKNOWN: - this.values.add(STRING); - break; - default: - this.values.add(String(value)); - } + break; + } + case 'String': + if (expression.arguments.length) { + const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); + if (arg.is_known) { + this.values.add(String(arg.value)); + } else if (arg.is_number || arg.is_string) { + this.values.add(STRING); + } else { + for (let value of arg.values) { + switch (value) { + case STRING: + case NUMBER: + case UNKNOWN: + this.values.add(STRING); + break; + default: + this.values.add(String(value)); } } - } else { - this.values.add(''); } - break; - default: - this.values.add(UNKNOWN); - } - } else { - this.values.add(UNKNOWN); + } else { + this.values.add(''); + } + break; + default: + this.values.add(UNKNOWN); } + } else { + this.values.add(UNKNOWN); } + break; + } default: { this.values.add(UNKNOWN); From 9325dc117dfa80c9965ebb9b2027e5b433fd3877 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 16:56:18 -0400 Subject: [PATCH 09/25] Apply suggestions from code review --- packages/svelte/src/compiler/phases/scope.js | 28 ++------------------ 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f1a4681b5668..0746a1443362 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -407,20 +407,8 @@ class Evaluation { const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); if (arg.is_known) { this.values.add(Number(arg.value)); - } else if (arg.is_number) { - this.values.add(NUMBER); } else { - for (let value of arg.values) { - switch (value) { - case STRING: - case NUMBER: - case UNKNOWN: - this.values.add(NUMBER); - break; - default: - this.values.add(Number(value)); - } - } + this.values.add(NUMBER); } } else { this.values.add(0); @@ -432,20 +420,8 @@ class Evaluation { const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); if (arg.is_known) { this.values.add(String(arg.value)); - } else if (arg.is_number || arg.is_string) { - this.values.add(STRING); } else { - for (let value of arg.values) { - switch (value) { - case STRING: - case NUMBER: - case UNKNOWN: - this.values.add(STRING); - break; - default: - this.values.add(String(value)); - } - } + this.values.add(STRING); } } else { this.values.add(''); From e3f2642bc25db31d132bf69ce22cc4d576699a06 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 17:03:14 -0400 Subject: [PATCH 10/25] compactify --- packages/svelte/src/compiler/phases/scope.js | 41 +++++++------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 0746a1443362..a83d34683bf7 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -344,15 +344,16 @@ class Evaluation { case 'CallExpression': { const rune = get_rune(expression, scope); + if (rune) { + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + switch (rune) { case '$state': case '$state.raw': - if (expression.arguments.length) { - const evaluated = scope.evaluate( - /** @type {Expression} */ (expression.arguments[0]) - ); - for (let value of evaluated.values) { + case '$derived': + if (arg) { + for (let value of scope.evaluate(arg).values) { this.values.add(value); } } else { @@ -369,25 +370,12 @@ class Evaluation { this.values.add(true); break; - case '$derived': { - const evaluated = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); - for (let value of evaluated.values) { - this.values.add(value); - } - break; - } - case '$derived.by': - if (expression.arguments[0]?.type === 'ArrowFunctionExpression') { - if (expression.arguments[0].body?.type !== 'BlockStatement') { - const evaluated = scope.evaluate( - /** @type {Expression} */ (expression.arguments[0].body) - ); - for (let value of evaluated.values) { - this.values.add(value); - } - break; + if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { + for (let value of scope.evaluate(arg.body).values) { + this.values.add(value); } + break; } this.values.add(UNKNOWN); @@ -397,10 +385,11 @@ class Evaluation { this.values.add(UNKNOWN); } } - } else if ( - expression.callee.type === 'Identifier' && - scope.get(expression.callee.name) === null - ) { + + break; + } + + if (expression.callee.type === 'Identifier' && scope.get(expression.callee.name) === null) { switch (expression.callee.name) { case 'Number': { if (expression.arguments.length) { From 79099ce16cc10c5deb336bd6892c43f4e482166b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 17:14:57 -0400 Subject: [PATCH 11/25] compactify --- packages/svelte/src/compiler/phases/scope.js | 33 +++++++------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index a83d34683bf7..ca937f7754e1 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -392,30 +392,21 @@ class Evaluation { if (expression.callee.type === 'Identifier' && scope.get(expression.callee.name) === null) { switch (expression.callee.name) { case 'Number': { - if (expression.arguments.length) { - const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); - if (arg.is_known) { - this.values.add(Number(arg.value)); - } else { - this.values.add(NUMBER); - } - } else { - this.values.add(0); - } + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + const e = arg && scope.evaluate(arg); + + this.values.add(e ? (e.is_known ? Number(e.value) : NUMBER) : 0); break; } - case 'String': - if (expression.arguments.length) { - const arg = scope.evaluate(/** @type {Expression} */ (expression.arguments[0])); - if (arg.is_known) { - this.values.add(String(arg.value)); - } else { - this.values.add(STRING); - } - } else { - this.values.add(''); - } + + case 'String': { + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + const e = arg && scope.evaluate(arg); + + this.values.add(e ? (e.is_known ? String(e.value) : STRING) : 0); break; + } + default: this.values.add(UNKNOWN); } From ed6c208405a4e19c5b70f6b92328d8f05a650c86 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 17:20:29 -0400 Subject: [PATCH 12/25] reuse values --- packages/svelte/src/compiler/phases/scope.js | 26 +++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index ca937f7754e1..316179bd2a15 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -107,7 +107,7 @@ export class Binding { class Evaluation { /** @type {Set} */ - values = new Set(); + values; /** * True if there is exactly one possible value @@ -147,8 +147,11 @@ class Evaluation { * * @param {Scope} scope * @param {Expression} expression + * @param {Set} values */ - constructor(scope, expression) { + constructor(scope, expression, values = new Set()) { + this.values = values; + switch (expression.type) { case 'Literal': { this.values.add(expression.value); @@ -178,10 +181,7 @@ class Evaluation { } 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); - } + binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); break; } } else if (expression.name === 'undefined') { @@ -353,9 +353,7 @@ class Evaluation { case '$state.raw': case '$derived': if (arg) { - for (let value of scope.evaluate(arg).values) { - this.values.add(value); - } + scope.evaluate(arg, this.values); } else { this.values.add(undefined); } @@ -372,9 +370,7 @@ class Evaluation { case '$derived.by': if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { - for (let value of scope.evaluate(arg.body).values) { - this.values.add(value); - } + scope.evaluate(arg.body, this.values); break; } @@ -629,10 +625,10 @@ export class Scope { * Only call this once scope has been fully generated in a first pass, * else this evaluates on incomplete data and may yield wrong results. * @param {Expression} expression - * @param {Set} values + * @param {Set} [values] */ - evaluate(expression, values = new Set()) { - return new Evaluation(this, expression); + evaluate(expression, values) { + return new Evaluation(this, expression, values); } } From ed7f90ca268d061c6ebbd53d8a0c9dd5fdfda190 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:29:04 -0700 Subject: [PATCH 13/25] add more globals, template literals, try functions and (some) member expressions --- .../client/visitors/RegularElement.js | 2 +- .../client/visitors/shared/utils.js | 8 +- .../server/visitors/shared/utils.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 569 +++++++++++++++++- 4 files changed, 552 insertions(+), 29 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 fa4ee9867f8a..e90a367927c5 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 @@ -685,7 +685,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : value ); - const evaluated = context.state.scope.evaluate(value); + const evaluated = context.state.scope.evaluate(value, new Set(), context.state.scopes); const assignment = b.assignment('=', b.member(node_id, '__value'), value); const inner_assignment = b.assignment( 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 2aae1d0cb337..5a5daca89d0c 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 @@ -64,12 +64,9 @@ export function build_template_chunk( node.expression.name !== 'undefined' || state.scope.get('undefined') ) { - let value = memoize( - /** @type {Expression} */ (visit(node.expression, state)), - node.metadata.expression - ); + let value = /** @type {Expression} */ (visit(node.expression, state)); - const evaluated = state.scope.evaluate(value); + const evaluated = state.scope.evaluate(value, new Set(), state.scopes); has_state ||= node.metadata.expression.has_state && !evaluated.is_known; @@ -98,6 +95,7 @@ export function build_template_chunk( if (evaluated.is_known) { quasi.value.cooked += evaluated.value + ''; } else { + value = memoize(value, node.metadata.expression); if (!evaluated.is_defined) { // add `?? ''` where necessary value = b.logical('??', value, b.literal('')); 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 807e12a8fa92..3cfb5abdd9bc 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 @@ -45,7 +45,7 @@ export function process_children(nodes, { visit, state }) { quasi.value.cooked += node.type === 'Comment' ? `` : escape_html(node.data); } else { - const evaluated = state.scope.evaluate(node.expression); + const evaluated = state.scope.evaluate(node.expression, new Set(), state.scopes); if (evaluated.is_known) { quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 316179bd2a15..a2a28a2ebbff 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, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, Function, Program, PrivateIdentifier } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -20,7 +20,124 @@ export const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ export const NUMBER = Symbol('number'); export const STRING = Symbol('string'); +/** Used for when you need to add `true` and `false` to the values, but can't do it for whatever reason */ +const BOOLEAN = Symbol('boolean'); + +const TYPES = [UNKNOWN, NUMBER, STRING]; + +const global_calls = Object.freeze({ + String: ['fromCharCode', 'fromCodepoint'], + Math: [ + 'min', + 'max', + 'random', + 'floor', + 'f16round', + 'round', + 'abs', + 'acos', + 'asin', + 'atan', + 'atan2', + 'ceil', + 'cos', + 'sin', + 'tan', + 'exp', + 'log', + 'pow', + 'sqrt', + 'clz32', + 'imul', + 'sign', + 'log10', + 'log2', + 'log1p', + 'expm1', + 'cosh', + 'sinh', + 'tanh', + 'acosh', + 'asinh', + 'atanh', + 'trunc', + 'fround', + 'cbrt' + ], + Number: ['isInteger', 'isFinite', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt'] +}); + +const math_constants = Object.freeze([ + 'PI', + 'E', + 'LN10', + 'LN2', + 'LOG10E', + 'LOG2E', + 'SQRT_2', + 'SQRT1_2' +]); +/** + * @param {Expression | Super} callee + * @param {Scope} scope + * @returns {null | [keyof typeof global_calls, (typeof global_calls)[keyof typeof global_calls][number]] | [string]} + */ +function is_global_call(callee, scope) { + if ( + callee.type === 'MemberExpression' && + callee.property?.type !== 'PrivateIdentifier' && + !callee.computed + ) { + const root = object(callee); + if (root?.type !== 'Identifier') { + return null; + } + if (scope.get(root.name)) { + return null; + } + if (callee.object === root && root.name === 'globalThis') { + return is_global_call(callee.property, scope); + } + /** @type {keyof typeof global_calls | undefined} */ + let root_name; + + if ( + callee.object.type === 'MemberExpression' && + callee.object.property.type === 'Identifier' && + !callee.object.computed + ) { + if ( + callee.object.object.type === 'Identifier' && + callee.object.object.name === 'globalThis' + ) { + root_name = /** @type {keyof typeof global_calls} */ (callee.object.property?.name); + } else if (callee.object.object === root) { + root_name = /** @type {keyof typeof global_calls} */ (root.name); + } + } else { + //@ts-ignore + root_name = /** @type {Identifier} */ (callee.object).name; + } + console.log(root_name); + if (root_name === undefined) return null; + if (!(root_name in global_calls)) { + return null; + } + const valid_members = global_calls[root_name]; + const property = /** @type {Identifier} */ (callee.property).name; + console.log(property, valid_members, valid_members.includes(property)); + if (!valid_members.includes(property)) { + return null; + } + return [root_name, property]; + } else if (callee.type === 'Identifier') { + return callee.name === 'Number' || callee.name === 'BigInt' || callee.name === 'String' + ? [callee.name] + : null; + } + return null; +} export class Binding { /** @type {Scope} */ scope; @@ -148,8 +265,9 @@ class Evaluation { * @param {Scope} scope * @param {Expression} expression * @param {Set} values + * @param {Map} scopes */ - constructor(scope, expression, values = new Set()) { + constructor(scope, expression, values, scopes) { this.values = values; switch (expression.type) { @@ -181,7 +299,11 @@ class Evaluation { } if (!binding.updated && binding.initial !== null && !is_prop) { - binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); + binding.scope.evaluate( + /** @type {Expression} */ (binding.initial), + this.values, + scopes + ); break; } } else if (expression.name === 'undefined') { @@ -197,8 +319,8 @@ class Evaluation { } case 'BinaryExpression': { - const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in` - const b = scope.evaluate(expression.right); + const a = scope.evaluate(/** @type {Expression} */ (expression.left), new Set(), scopes); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = scope.evaluate(expression.right, new Set(), scopes); if (a.is_known && b.is_known) { this.values.add(binary[expression.operator](a.value, b.value)); @@ -252,9 +374,9 @@ class Evaluation { } case 'ConditionalExpression': { - const test = scope.evaluate(expression.test); - const consequent = scope.evaluate(expression.consequent); - const alternate = scope.evaluate(expression.alternate); + const test = scope.evaluate(expression.test, new Set(), scopes); + const consequent = scope.evaluate(expression.consequent, new Set(), scopes); + const alternate = scope.evaluate(expression.alternate, new Set(), scopes); if (test.is_known) { for (const value of (test.value ? consequent : alternate).values) { @@ -273,8 +395,8 @@ class Evaluation { } case 'LogicalExpression': { - const a = scope.evaluate(expression.left); - const b = scope.evaluate(expression.right); + const a = scope.evaluate(expression.left, new Set(), scopes); + const b = scope.evaluate(expression.right, new Set(), scopes); if (a.is_known) { if (b.is_known) { @@ -308,7 +430,7 @@ class Evaluation { } case 'UnaryExpression': { - const argument = scope.evaluate(expression.argument); + const argument = scope.evaluate(expression.argument, new Set(), scopes); if (argument.is_known) { this.values.add(unary[expression.operator](argument.value)); @@ -353,7 +475,7 @@ class Evaluation { case '$state.raw': case '$derived': if (arg) { - scope.evaluate(arg, this.values); + scope.evaluate(arg, this.values, scopes); } else { this.values.add(undefined); } @@ -370,7 +492,7 @@ class Evaluation { case '$derived.by': if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { - scope.evaluate(arg.body, this.values); + scope.evaluate(arg.body, this.values, scopes); break; } @@ -385,11 +507,12 @@ class Evaluation { break; } - if (expression.callee.type === 'Identifier' && scope.get(expression.callee.name) === null) { - switch (expression.callee.name) { + if (is_global_call(expression.callee, scope)?.length === 1) { + const [call] = /** @type {[string]} */ (is_global_call(expression.callee, scope)); + switch (call) { case 'Number': { const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg); + const e = arg && scope.evaluate(arg, new Set(), scopes); this.values.add(e ? (e.is_known ? Number(e.value) : NUMBER) : 0); break; @@ -397,19 +520,203 @@ class Evaluation { case 'String': { const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg); + const e = arg && scope.evaluate(arg, new Set(), scopes); - this.values.add(e ? (e.is_known ? String(e.value) : STRING) : 0); + this.values.add(e ? (e.is_known ? String(e.value) : STRING) : ''); + break; + } + + case 'BigInt': { + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + const e = arg && scope.evaluate(arg, new Set(), scopes); + this.values.add(e ? (e.is_known ? BigInt(e.value) : NUMBER) : 0n); break; } default: this.values.add(UNKNOWN); } + break; + } else if ( + expression.callee.type === 'MemberExpression' && + is_global_call(expression.callee, scope)?.length === 2 + ) { + const [object, property] = /** @type {[keyof typeof global_calls, string]} */ ( + is_global_call(expression.callee, scope) + ); + switch (object) { + case 'Math': { + const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => + scope.evaluate(arg, new Set(), scopes) + ); + const all_are_known = args.every((evaluated) => evaluated.is_known); + const vals = all_are_known + ? //@ts-expect-error + /** @type {() => unknown} */ (Math[property])(...args.map((arg) => arg.value)) + : NUMBER; + this.values.add(vals); + break; + } + case 'Number': { + const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => + scope.evaluate(arg, new Set(), scopes) + ); + const all_are_known = args.every((evaluated) => evaluated.is_known); + const vals = all_are_known + ? Number[ + /** @type {'isSafeInteger' | 'isFinite' | 'isNaN' | 'parseFloat' | 'parseInt' | 'isInteger'} */ ( + property + ) + //@ts-expect-error types are a monster here + ](...args.map((arg) => arg.value)) + : property.startsWith('is') + ? BOOLEAN + : NUMBER; + if (vals === BOOLEAN) { + this.values.add(true); + this.values.add(false); + } else { + this.values.add(vals); + } + break; + } + case 'String': { + const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => + scope.evaluate(arg, new Set(), scopes) + ); + const all_are_known = args.every((evaluated) => evaluated.is_known); + this.values.add( + all_are_known + ? String[/** @type {'fromCharCode' | 'fromCodePoint'} */ (property)]( + ...args.map((arg) => arg.value) + ) + : STRING + ); + break; + } + } + break; + } else if (expression.callee.type === 'Identifier' && !expression.arguments.length) { + const binding = scope.get(expression.callee.name); + if (binding) { + if ( + binding.kind === 'normal' && + !binding.reassigned && + (binding.declaration_kind === 'function' || + binding.declaration_kind === 'const' || + binding.declaration_kind === 'let' || + binding.declaration_kind === 'var') + ) { + const fn = + /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ ( + binding.initial + ); + if (fn && fn.async === false && !fn?.generator) { + const fn_analysis = evaluate_function(fn, binding, scopes, new Set()); + if (fn_analysis.pure && fn_analysis.never_throws) { + for (let value of fn_analysis.values) { + this.values.add(value); + } + break; + } + } + } + } + } + + this.values.add(UNKNOWN); + break; + } + + case 'TemplateLiteral': { + const expressions = expression.expressions.map((expr) => + scope.evaluate(expr, new Set(), scopes) + ); + const all_are_known = expressions.every((evaluated) => evaluated.is_known); + if (all_are_known) { + let res = ''; + let quasi_index = 0; + let expr_index = 0; + let last_quasi = false; + for (let i = 0; i < expressions.length + expression.quasis.length; i++) { + if (last_quasi) { + const expression = expressions[expr_index++]; + res += expression.value; + } else { + res += expression.quasis[quasi_index++].value.raw; + } + last_quasi = !last_quasi; + } + this.values.add(res); } else { - this.values.add(UNKNOWN); + this.values.add(STRING); } + break; + } + case 'MemberExpression': { + if ( + expression.object.type !== 'Identifier' && + expression.object.type !== 'MemberExpression' + ) { + this.values.add(UNKNOWN); + break; + } + const { object, property, computed } = expression; + if ( + object.type === 'MemberExpression' && + object.object.type === 'Identifier' && + object.object.name === 'globalThis' && + !scope.get('globalThis') + ) { + if (object.computed && object.property.type !== 'PrivateIdentifier') { + const key = scope.evaluate(object.property, new Set(), scopes); + if (key.is_known && key.value === 'Math') { + if (computed && property.type !== 'PrivateIdentifier') { + const math_prop = scope.evaluate(property, new Set(), scopes); + if (math_prop.is_known && math_constants.includes(math_prop.value)) { + this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); + break; + } + } else if (property.type === 'Identifier') { + if (math_constants.includes(property.name)) { + this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]); + break; + } + } + } + } else if (object.property.type === 'Identifier' && object.property.name === 'Math') { + if (computed && property.type !== 'PrivateIdentifier') { + const math_prop = scope.evaluate(property, new Set(), scopes); + if (math_prop.is_known && math_constants.includes(math_prop.value)) { + this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); + break; + } + } else if (property.type === 'Identifier') { + if (math_constants.includes(property.name)) { + this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]); + break; + } + } + } else { + this.values.add(UNKNOWN); + break; + } + } else if (object.type === 'Identifier' && object.name === 'Math' && !scope.get('Math')) { + if (computed && property.type !== 'PrivateIdentifier') { + const math_prop = scope.evaluate(property, new Set(), scopes); + if (math_prop.is_known && math_constants.includes(math_prop.value)) { + this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); + break; + } + } else if (property.type === 'Identifier') { + if (math_constants.includes(property.name)) { + this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]); + break; + } + } + } + this.values.add(UNKNOWN); break; } @@ -440,6 +747,223 @@ class Evaluation { } } +// TODO add more +const global_classes = [ + 'String', + 'BigInt', + 'Object', + 'Set', + 'Array', + 'Proxy', + 'Map', + 'Boolean', + 'WeakMap', + 'WeakRef', + 'WeakSet', + 'Number', + 'RegExp', + 'Error', + 'Date' +]; + +// TODO ditto +const known_globals = [ + ...global_classes, + 'Symbol', + 'console', + 'Math', + 'isNaN', + 'isFinite', + 'setTimeout', + 'setInterval', + 'NaN', + 'undefined' +]; + +/** + * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} fn + * @param {Binding} binding + * @param {Map} scopes + * @param {Set} [stack] + */ +function evaluate_function(fn, binding, scopes, stack = new Set()) { + const analysis = { + pure: true, + is_known: false, + is_defined: false, + values: new Set(), + /** @type {any} */ + value: undefined, + never_throws: true + }; + /** @type {Set} */ + const walked_scopes = new Set(/** @type {Scope[]} */ ([binding.scope])); + const fn_binding = binding; + walk( + /** @type {AST.SvelteNode} */ (fn), + { scope: binding.scope }, + { + MemberExpression(node, context) { + if ( + !context.state.scope.evaluate(node, new Set(), scopes).is_known && + !is_global_call(node, context.state.scope) + ) { + analysis.pure = false; + analysis.never_throws = false; // getters/proxies could throw + } + context.next(); + }, + Identifier(node, context) { + if (is_reference(node, /** @type {Node} */ (context.path.at(-1)))) { + const binding = context.state.scope.get(node.name); + if (binding !== fn_binding) { + if (binding === null) { + if (known_globals.includes(node.name)) { + context.next(); + return; + } + analysis.pure = false; + } else if (!walked_scopes.has(binding?.scope)) { + const evaluated = context.state.scope.evaluate(node, new Set(), scopes); + if (!evaluated.is_known) { + analysis.pure = false; + } + } + } + } + context.next(); + }, + CallExpression(node, context) { + if (node.callee.type === 'Identifier' && !node.arguments.length) { + const binding = context.state.scope.get(node.callee.name); + if ( + binding && + fn_binding !== binding && + binding.kind === 'normal' && + !binding.reassigned && + (binding.declaration_kind === 'function' || + binding.declaration_kind === 'const' || + binding.declaration_kind === 'let' || + binding.declaration_kind === 'var') + ) { + if (!stack.has(binding)) { + const fn_analysis = evaluate_function( + /** @type {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} */ ( + binding.initial + ), + binding, + scopes, + new Set([...stack]) + ); + analysis.pure &&= fn_analysis.pure; + analysis.never_throws &&= fn_analysis.never_throws; + } + } else if (!(node.callee.name in global_calls)) { + analysis.pure = false; + } + } else if (!is_global_call(node.callee, context.state.scope)) { + analysis.pure = false; + console.log(node.callee); + } + context.next(); + }, + NewExpression(node, context) { + const { callee } = node; + if (callee.type === 'Identifier') { + if (context.state.scope.get(callee.name) || !global_classes.includes(callee.name)) { + analysis.pure = false; + } + } + context.next(); + }, + AssignmentExpression(node, context) { + const { left } = node; + const assigned_to = unwrap_pattern(/** @type {Pattern} */ (context.visit(left))); + if (assigned_to.some((assigned) => assigned.type === 'MemberExpression')) { + analysis.pure = false; + analysis.never_throws = false; + } else if ( + assigned_to.some((assigned) => { + const binding = context.state.scope.get(/** @type {Identifier} */ (assigned).name); + if (binding) { + return !walked_scopes.has(binding.scope); + } else { + return true; + } + }) + ) { + analysis.pure = false; + } + context.next(); + }, + UpdateExpression(node, context) { + analysis.pure = false; + analysis.never_throws = false; + context.next(); + }, + ReturnStatement(node, context) { + if ( + context.path.findLast( + (parent) => + parent.type === 'FunctionDeclaration' || + parent.type === 'FunctionExpression' || + parent.type === 'ArrowFunctionExpression' + ) === fn + ) { + const return_values = node.argument + ? context.state.scope.evaluate( + /** @type {Expression} */ (context.visit(node.argument)), + new Set(), + scopes + ) + : undefined; + if (return_values === undefined) { + analysis.values.add(undefined); + } else { + for (let value of return_values.values) { + analysis.values.add(value); + } + } + } + }, + ThrowStatement(node, context) { + if ( + context.path.findLast( + (parent) => + parent.type === 'FunctionDeclaration' || + parent.type === 'FunctionExpression' || + parent.type === 'ArrowFunctionExpression' + ) === fn + ) { + analysis.never_throws = false; + } + context.next(); + }, + _(node, context) { + const scope = scopes.get(node); + if (scope && node.type !== 'FunctionDeclaration') { + walked_scopes.add(scope); + context.next({ scope }); + } else if (node.type !== 'FunctionDeclaration' || node === fn) { + context.next(context.state); + } + } + } + ); + for (const value of analysis.values) { + analysis.value = value; // saves having special logic for `size === 1` + + if (value == null || value === UNKNOWN) { + analysis.is_defined = false; + } + } + + if (analysis.values.size <= 1 && !TYPES.includes(analysis.value)) { + analysis.is_known = true; + } + console.log(analysis); + return analysis; +} export class Scope { /** @type {ScopeRoot} */ root; @@ -625,10 +1149,11 @@ export class Scope { * Only call this once scope has been fully generated in a first pass, * else this evaluates on incomplete data and may yield wrong results. * @param {Expression} expression - * @param {Set} [values] + * @param {Set} values + * @param {Map} scopes */ - evaluate(expression, values) { - return new Evaluation(this, expression, values); + evaluate(expression, values, scopes) { + return new Evaluation(this, expression, values, scopes); } } From b2388962ea6b73f8a6679972b0199f5e3073c151 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:30:52 -0700 Subject: [PATCH 14/25] remove console logs --- packages/svelte/src/compiler/phases/scope.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index a2a28a2ebbff..2b493e5e7757 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -119,14 +119,12 @@ function is_global_call(callee, scope) { //@ts-ignore root_name = /** @type {Identifier} */ (callee.object).name; } - console.log(root_name); if (root_name === undefined) return null; if (!(root_name in global_calls)) { return null; } const valid_members = global_calls[root_name]; const property = /** @type {Identifier} */ (callee.property).name; - console.log(property, valid_members, valid_members.includes(property)); if (!valid_members.includes(property)) { return null; } @@ -863,7 +861,6 @@ function evaluate_function(fn, binding, scopes, stack = new Set()) { } } else if (!is_global_call(node.callee, context.state.scope)) { analysis.pure = false; - console.log(node.callee); } context.next(); }, @@ -961,7 +958,6 @@ function evaluate_function(fn, binding, scopes, stack = new Set()) { if (analysis.values.size <= 1 && !TYPES.includes(analysis.value)) { analysis.is_known = true; } - console.log(analysis); return analysis; } export class Scope { From 966f3cc07caf01a7b2f1bcf37922eb3ecfcc98ce Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:16:53 -0700 Subject: [PATCH 15/25] remove function handling, tweak failing test --- .../client/visitors/RegularElement.js | 2 +- .../client/visitors/shared/utils.js | 10 +- .../server/visitors/shared/utils.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 301 ++---------------- .../purity/_expected/client/index.svelte.js | 2 +- .../purity/_expected/server/index.svelte.js | 2 +- 6 files changed, 36 insertions(+), 283 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 508f7f7fd22b..7468fcbbc72e 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 @@ -685,7 +685,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : value ); - const evaluated = context.state.scope.evaluate(value, new Set(), context.state.scopes); + const evaluated = context.state.scope.evaluate(value); const assignment = b.assignment('=', b.member(node_id, '__value'), value); const inner_assignment = b.assignment( 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 e3f8f9d09e5b..2aae1d0cb337 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 @@ -3,7 +3,7 @@ /** @import { ComponentClientTransformState, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; -import * as b from '#compiler/builders'; +import * as b from '../../../../../utils/builders.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; @@ -64,9 +64,12 @@ export function build_template_chunk( node.expression.name !== 'undefined' || state.scope.get('undefined') ) { - let value = /** @type {Expression} */ (visit(node.expression, state)); + let value = memoize( + /** @type {Expression} */ (visit(node.expression, state)), + node.metadata.expression + ); - const evaluated = state.scope.evaluate(value, new Set(), state.scopes); + const evaluated = state.scope.evaluate(value); has_state ||= node.metadata.expression.has_state && !evaluated.is_known; @@ -95,7 +98,6 @@ export function build_template_chunk( if (evaluated.is_known) { quasi.value.cooked += evaluated.value + ''; } else { - value = memoize(value, node.metadata.expression); if (!evaluated.is_defined) { // add `?? ''` where necessary value = b.logical('??', value, b.literal('')); 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 ed4c26b114dd..8fcf8efa68b6 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 @@ -45,7 +45,7 @@ export function process_children(nodes, { visit, state }) { quasi.value.cooked += node.type === 'Comment' ? `` : escape_html(node.data); } else { - const evaluated = state.scope.evaluate(node.expression, new Set(), state.scopes); + const evaluated = state.scope.evaluate(node.expression); if (evaluated.is_known) { quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index d0fcb6fbe059..3a4c71b97231 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -263,9 +263,8 @@ class Evaluation { * @param {Scope} scope * @param {Expression} expression * @param {Set} values - * @param {Map} scopes */ - constructor(scope, expression, values, scopes) { + constructor(scope, expression, values) { this.values = values; switch (expression.type) { @@ -297,11 +296,7 @@ class Evaluation { } if (!binding.updated && binding.initial !== null && !is_prop) { - binding.scope.evaluate( - /** @type {Expression} */ (binding.initial), - this.values, - scopes - ); + binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); break; } } else if (expression.name === 'undefined') { @@ -317,8 +312,8 @@ class Evaluation { } case 'BinaryExpression': { - const a = scope.evaluate(/** @type {Expression} */ (expression.left), new Set(), scopes); // `left` cannot be `PrivateIdentifier` unless operator is `in` - const b = scope.evaluate(expression.right, new Set(), scopes); + const a = scope.evaluate(/** @type {Expression} */ (expression.left)); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = scope.evaluate(expression.right); if (a.is_known && b.is_known) { this.values.add(binary[expression.operator](a.value, b.value)); @@ -372,9 +367,9 @@ class Evaluation { } case 'ConditionalExpression': { - const test = scope.evaluate(expression.test, new Set(), scopes); - const consequent = scope.evaluate(expression.consequent, new Set(), scopes); - const alternate = scope.evaluate(expression.alternate, new Set(), scopes); + const test = scope.evaluate(expression.test); + const consequent = scope.evaluate(expression.consequent); + const alternate = scope.evaluate(expression.alternate); if (test.is_known) { for (const value of (test.value ? consequent : alternate).values) { @@ -393,8 +388,8 @@ class Evaluation { } case 'LogicalExpression': { - const a = scope.evaluate(expression.left, new Set(), scopes); - const b = scope.evaluate(expression.right, new Set(), scopes); + const a = scope.evaluate(expression.left); + const b = scope.evaluate(expression.right); if (a.is_known) { if (b.is_known) { @@ -428,7 +423,7 @@ class Evaluation { } case 'UnaryExpression': { - const argument = scope.evaluate(expression.argument, new Set(), scopes); + const argument = scope.evaluate(expression.argument); if (argument.is_known) { this.values.add(unary[expression.operator](argument.value)); @@ -473,7 +468,7 @@ class Evaluation { case '$state.raw': case '$derived': if (arg) { - scope.evaluate(arg, this.values, scopes); + scope.evaluate(arg, this.values); } else { this.values.add(undefined); } @@ -490,7 +485,7 @@ class Evaluation { case '$derived.by': if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { - scope.evaluate(arg.body, this.values, scopes); + scope.evaluate(arg.body, this.values); break; } @@ -510,7 +505,7 @@ class Evaluation { switch (call) { case 'Number': { const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg, new Set(), scopes); + const e = arg && scope.evaluate(arg); this.values.add(e ? (e.is_known ? Number(e.value) : NUMBER) : 0); break; @@ -518,7 +513,7 @@ class Evaluation { case 'String': { const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg, new Set(), scopes); + const e = arg && scope.evaluate(arg); this.values.add(e ? (e.is_known ? String(e.value) : STRING) : ''); break; @@ -526,7 +521,7 @@ class Evaluation { case 'BigInt': { const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg, new Set(), scopes); + const e = arg && scope.evaluate(arg); this.values.add(e ? (e.is_known ? BigInt(e.value) : NUMBER) : 0n); break; } @@ -545,7 +540,7 @@ class Evaluation { switch (object) { case 'Math': { const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => - scope.evaluate(arg, new Set(), scopes) + scope.evaluate(arg) ); const all_are_known = args.every((evaluated) => evaluated.is_known); const vals = all_are_known @@ -557,7 +552,7 @@ class Evaluation { } case 'Number': { const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => - scope.evaluate(arg, new Set(), scopes) + scope.evaluate(arg) ); const all_are_known = args.every((evaluated) => evaluated.is_known); const vals = all_are_known @@ -580,7 +575,7 @@ class Evaluation { } case 'String': { const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => - scope.evaluate(arg, new Set(), scopes) + scope.evaluate(arg) ); const all_are_known = args.every((evaluated) => evaluated.is_known); this.values.add( @@ -594,32 +589,6 @@ class Evaluation { } } break; - } else if (expression.callee.type === 'Identifier' && !expression.arguments.length) { - const binding = scope.get(expression.callee.name); - if (binding) { - if ( - binding.kind === 'normal' && - !binding.reassigned && - (binding.declaration_kind === 'function' || - binding.declaration_kind === 'const' || - binding.declaration_kind === 'let' || - binding.declaration_kind === 'var') - ) { - const fn = - /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ ( - binding.initial - ); - if (fn && fn.async === false && !fn?.generator) { - const fn_analysis = evaluate_function(fn, binding, scopes, new Set()); - if (fn_analysis.pure && fn_analysis.never_throws) { - for (let value of fn_analysis.values) { - this.values.add(value); - } - break; - } - } - } - } } this.values.add(UNKNOWN); @@ -627,9 +596,7 @@ class Evaluation { } case 'TemplateLiteral': { - const expressions = expression.expressions.map((expr) => - scope.evaluate(expr, new Set(), scopes) - ); + const expressions = expression.expressions.map((expr) => scope.evaluate(expr)); const all_are_known = expressions.every((evaluated) => evaluated.is_known); if (all_are_known) { let res = ''; @@ -668,10 +635,10 @@ class Evaluation { !scope.get('globalThis') ) { if (object.computed && object.property.type !== 'PrivateIdentifier') { - const key = scope.evaluate(object.property, new Set(), scopes); + const key = scope.evaluate(object.property); if (key.is_known && key.value === 'Math') { if (computed && property.type !== 'PrivateIdentifier') { - const math_prop = scope.evaluate(property, new Set(), scopes); + const math_prop = scope.evaluate(property); if (math_prop.is_known && math_constants.includes(math_prop.value)) { this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); break; @@ -685,7 +652,7 @@ class Evaluation { } } else if (object.property.type === 'Identifier' && object.property.name === 'Math') { if (computed && property.type !== 'PrivateIdentifier') { - const math_prop = scope.evaluate(property, new Set(), scopes); + const math_prop = scope.evaluate(property); if (math_prop.is_known && math_constants.includes(math_prop.value)) { this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); break; @@ -702,7 +669,7 @@ class Evaluation { } } else if (object.type === 'Identifier' && object.name === 'Math' && !scope.get('Math')) { if (computed && property.type !== 'PrivateIdentifier') { - const math_prop = scope.evaluate(property, new Set(), scopes); + const math_prop = scope.evaluate(property); if (math_prop.is_known && math_constants.includes(math_prop.value)) { this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); break; @@ -745,221 +712,6 @@ class Evaluation { } } -// TODO add more -const global_classes = [ - 'String', - 'BigInt', - 'Object', - 'Set', - 'Array', - 'Proxy', - 'Map', - 'Boolean', - 'WeakMap', - 'WeakRef', - 'WeakSet', - 'Number', - 'RegExp', - 'Error', - 'Date' -]; - -// TODO ditto -const known_globals = [ - ...global_classes, - 'Symbol', - 'console', - 'Math', - 'isNaN', - 'isFinite', - 'setTimeout', - 'setInterval', - 'NaN', - 'undefined' -]; - -/** - * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} fn - * @param {Binding} binding - * @param {Map} scopes - * @param {Set} [stack] - */ -function evaluate_function(fn, binding, scopes, stack = new Set()) { - const analysis = { - pure: true, - is_known: false, - is_defined: false, - values: new Set(), - /** @type {any} */ - value: undefined, - never_throws: true - }; - /** @type {Set} */ - const walked_scopes = new Set(/** @type {Scope[]} */ ([binding.scope])); - const fn_binding = binding; - walk( - /** @type {AST.SvelteNode} */ (fn), - { scope: binding.scope }, - { - MemberExpression(node, context) { - if ( - !context.state.scope.evaluate(node, new Set(), scopes).is_known && - !is_global_call(node, context.state.scope) - ) { - analysis.pure = false; - analysis.never_throws = false; // getters/proxies could throw - } - context.next(); - }, - Identifier(node, context) { - if (is_reference(node, /** @type {Node} */ (context.path.at(-1)))) { - const binding = context.state.scope.get(node.name); - if (binding !== fn_binding) { - if (binding === null) { - if (known_globals.includes(node.name)) { - context.next(); - return; - } - analysis.pure = false; - } else if (!walked_scopes.has(binding?.scope)) { - const evaluated = context.state.scope.evaluate(node, new Set(), scopes); - if (!evaluated.is_known) { - analysis.pure = false; - } - } - } - } - context.next(); - }, - CallExpression(node, context) { - if (node.callee.type === 'Identifier' && !node.arguments.length) { - const binding = context.state.scope.get(node.callee.name); - if ( - binding && - fn_binding !== binding && - binding.kind === 'normal' && - !binding.reassigned && - (binding.declaration_kind === 'function' || - binding.declaration_kind === 'const' || - binding.declaration_kind === 'let' || - binding.declaration_kind === 'var') - ) { - if (!stack.has(binding)) { - const fn_analysis = evaluate_function( - /** @type {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} */ ( - binding.initial - ), - binding, - scopes, - new Set([...stack]) - ); - analysis.pure &&= fn_analysis.pure; - analysis.never_throws &&= fn_analysis.never_throws; - } - } else if (!(node.callee.name in global_calls)) { - analysis.pure = false; - } - } else if (!is_global_call(node.callee, context.state.scope)) { - analysis.pure = false; - } - context.next(); - }, - NewExpression(node, context) { - const { callee } = node; - if (callee.type === 'Identifier') { - if (context.state.scope.get(callee.name) || !global_classes.includes(callee.name)) { - analysis.pure = false; - } - } - context.next(); - }, - AssignmentExpression(node, context) { - const { left } = node; - const assigned_to = unwrap_pattern(/** @type {Pattern} */ (context.visit(left))); - if (assigned_to.some((assigned) => assigned.type === 'MemberExpression')) { - analysis.pure = false; - analysis.never_throws = false; - } else if ( - assigned_to.some((assigned) => { - const binding = context.state.scope.get(/** @type {Identifier} */ (assigned).name); - if (binding) { - return !walked_scopes.has(binding.scope); - } else { - return true; - } - }) - ) { - analysis.pure = false; - } - context.next(); - }, - UpdateExpression(node, context) { - analysis.pure = false; - analysis.never_throws = false; - context.next(); - }, - ReturnStatement(node, context) { - if ( - context.path.findLast( - (parent) => - parent.type === 'FunctionDeclaration' || - parent.type === 'FunctionExpression' || - parent.type === 'ArrowFunctionExpression' - ) === fn - ) { - const return_values = node.argument - ? context.state.scope.evaluate( - /** @type {Expression} */ (context.visit(node.argument)), - new Set(), - scopes - ) - : undefined; - if (return_values === undefined) { - analysis.values.add(undefined); - } else { - for (let value of return_values.values) { - analysis.values.add(value); - } - } - } - }, - ThrowStatement(node, context) { - if ( - context.path.findLast( - (parent) => - parent.type === 'FunctionDeclaration' || - parent.type === 'FunctionExpression' || - parent.type === 'ArrowFunctionExpression' - ) === fn - ) { - analysis.never_throws = false; - } - context.next(); - }, - _(node, context) { - const scope = scopes.get(node); - if (scope && node.type !== 'FunctionDeclaration') { - walked_scopes.add(scope); - context.next({ scope }); - } else if (node.type !== 'FunctionDeclaration' || node === fn) { - context.next(context.state); - } - } - } - ); - for (const value of analysis.values) { - analysis.value = value; // saves having special logic for `size === 1` - - if (value == null || value === UNKNOWN) { - analysis.is_defined = false; - } - } - - if (analysis.values.size <= 1 && !TYPES.includes(analysis.value)) { - analysis.is_known = true; - } - return analysis; -} export class Scope { /** @type {ScopeRoot} */ root; @@ -1145,11 +897,10 @@ export class Scope { * Only call this once scope has been fully generated in a first pass, * else this evaluates on incomplete data and may yield wrong results. * @param {Expression} expression - * @param {Set} values - * @param {Map} scopes + * @param {Set} [values] */ - evaluate(expression, values, scopes) { - return new Evaluation(this, expression, values, scopes); + evaluate(expression, values = new Set()) { + return new Evaluation(this, expression, values); } } diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index 940ed8f9e8fa..5bc9766acfd4 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,7 +8,7 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = Math.max(0, Math.min(0, 100)); + p.textContent = 0; var p_1 = $.sibling(p, 2); diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js index 588332407a63..9457378c0db4 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Purity($$payload) { - $$payload.out += `

${$.escape(Math.max(0, Math.min(0, 100)))}

${$.escape(location.href)}

`; + $$payload.out += `

0

${$.escape(location.href)}

`; Child($$payload, { prop: encodeURIComponent('hello') }); $$payload.out += ``; } \ No newline at end of file From bba8242db39c209bc084d3a725def7e1685d6582 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:21:00 -0700 Subject: [PATCH 16/25] changeset --- .changeset/serious-adults-sit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/serious-adults-sit.md diff --git a/.changeset/serious-adults-sit.md b/.changeset/serious-adults-sit.md new file mode 100644 index 000000000000..8111008b1ef0 --- /dev/null +++ b/.changeset/serious-adults-sit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: partially evaluate more expressions From 4d920c657d5e09c6f69dee9758ebfec389d4e8c7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:05:52 -0700 Subject: [PATCH 17/25] try putting static stuff in the template --- .../phases/3-transform/client/visitors/shared/fragment.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index c91e2b3b4497..361286a79b59 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -68,8 +68,6 @@ export function process_children(nodes, initial, is_element, { visit, state }) { return; } - state.template.push(' '); - const { has_state, value } = build_template_chunk(sequence, visit, state); // if this is a standalone `{expression}`, make sure we handle the case where @@ -81,8 +79,12 @@ export function process_children(nodes, initial, is_element, { visit, state }) { if (has_state && !within_bound_contenteditable) { state.update.push(update); - } else { + state.template.push(' '); + } else if (value.type !== 'Literal') { state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); + state.template.push(' '); + } else if (value.type === 'Literal') { + state.template.push((value.value ?? '') + ''); } } From 968045f428df579ef3d333262eb40240858a043b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:22:16 -0700 Subject: [PATCH 18/25] nevermind --- .../phases/3-transform/client/visitors/shared/fragment.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 361286a79b59..c91e2b3b4497 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -68,6 +68,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) { return; } + state.template.push(' '); + const { has_state, value } = build_template_chunk(sequence, visit, state); // if this is a standalone `{expression}`, make sure we handle the case where @@ -79,12 +81,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) { if (has_state && !within_bound_contenteditable) { state.update.push(update); - state.template.push(' '); - } else if (value.type !== 'Literal') { + } else { state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); - state.template.push(' '); - } else if (value.type === 'Literal') { - state.template.push((value.value ?? '') + ''); } } From ec04063e42614c6f83eb4c9ac4363b2cc88f1745 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 09:36:44 -0400 Subject: [PATCH 19/25] unused --- packages/svelte/src/compiler/phases/scope.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 3a4c71b97231..023529f9cfc4 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -23,8 +23,6 @@ export const STRING = Symbol('string'); /** Used for when you need to add `true` and `false` to the values, but can't do it for whatever reason */ const BOOLEAN = Symbol('boolean'); -const TYPES = [UNKNOWN, NUMBER, STRING]; - const global_calls = Object.freeze({ String: ['fromCharCode', 'fromCodepoint'], Math: [ From fbf5512882a178636e7d6721edc84abdbc78698d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 10:34:28 -0400 Subject: [PATCH 20/25] simplify --- packages/svelte/src/compiler/phases/scope.js | 417 ++++++------------- 1 file changed, 125 insertions(+), 292 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 023529f9cfc4..b454f2e6c91c 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -23,117 +23,69 @@ export const STRING = Symbol('string'); /** Used for when you need to add `true` and `false` to the values, but can't do it for whatever reason */ const BOOLEAN = Symbol('boolean'); -const global_calls = Object.freeze({ - String: ['fromCharCode', 'fromCodepoint'], - Math: [ - 'min', - 'max', - 'random', - 'floor', - 'f16round', - 'round', - 'abs', - 'acos', - 'asin', - 'atan', - 'atan2', - 'ceil', - 'cos', - 'sin', - 'tan', - 'exp', - 'log', - 'pow', - 'sqrt', - 'clz32', - 'imul', - 'sign', - 'log10', - 'log2', - 'log1p', - 'expm1', - 'cosh', - 'sinh', - 'tanh', - 'acosh', - 'asinh', - 'atanh', - 'trunc', - 'fround', - 'cbrt' - ], - Number: ['isInteger', 'isFinite', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt'] -}); - -const math_constants = Object.freeze([ - 'PI', - 'E', - 'LN10', - 'LN2', - 'LOG10E', - 'LOG2E', - 'SQRT_2', - 'SQRT1_2' -]); +/** @type {Record} */ +const global_constants = { + 'Math.PI': Math.PI, + 'Math.E': Math.E, + 'Math.LN10': Math.LN10, + 'Math.LN2': Math.LN2, + 'Math.LOG10E': Math.LOG10E, + 'Math.LOG2E': Math.LOG2E, + 'Math.SQRT2': Math.SQRT2, + 'Math.SQRT1_2': Math.SQRT1_2 +}; - if ( - callee.object.type === 'MemberExpression' && - callee.object.property.type === 'Identifier' && - !callee.object.computed - ) { - if ( - callee.object.object.type === 'Identifier' && - callee.object.object.name === 'globalThis' - ) { - root_name = /** @type {keyof typeof global_calls} */ (callee.object.property?.name); - } else if (callee.object.object === root) { - root_name = /** @type {keyof typeof global_calls} */ (root.name); - } - } else { - //@ts-ignore - root_name = /** @type {Identifier} */ (callee.object).name; - } - if (root_name === undefined) return null; - if (!(root_name in global_calls)) { - return null; - } - const valid_members = global_calls[root_name]; - const property = /** @type {Identifier} */ (callee.property).name; - if (!valid_members.includes(property)) { - return null; - } - return [root_name, property]; - } else if (callee.type === 'Identifier') { - return callee.name === 'Number' || callee.name === 'BigInt' || callee.name === 'String' - ? [callee.name] - : null; - } - return null; -} export class Binding { /** @type {Scope} */ scope; @@ -456,137 +408,64 @@ class Evaluation { } case 'CallExpression': { - const rune = get_rune(expression, scope); - - if (rune) { - const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - - switch (rune) { - case '$state': - case '$state.raw': - case '$derived': - if (arg) { - scope.evaluate(arg, this.values); - } else { - this.values.add(undefined); - } - break; - - case '$props.id': - this.values.add(STRING); - break; - - case '$effect.tracking': - this.values.add(false); - this.values.add(true); - break; - - case '$derived.by': - if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { - scope.evaluate(arg.body, this.values); + const keypath = get_global_keypath(expression.callee, scope); + + if (keypath) { + if (is_rune(keypath)) { + const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + + switch (keypath) { + case '$state': + case '$state.raw': + case '$derived': + if (arg) { + scope.evaluate(arg, this.values); + } else { + this.values.add(undefined); + } break; - } - this.values.add(UNKNOWN); - break; + case '$props.id': + this.values.add(STRING); + break; - default: { - this.values.add(UNKNOWN); - } - } + case '$effect.tracking': + this.values.add(false); + this.values.add(true); + break; - break; - } + case '$derived.by': + if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { + scope.evaluate(arg.body, this.values); + break; + } - if (is_global_call(expression.callee, scope)?.length === 1) { - const [call] = /** @type {[string]} */ (is_global_call(expression.callee, scope)); - switch (call) { - case 'Number': { - const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg); + this.values.add(UNKNOWN); + break; - this.values.add(e ? (e.is_known ? Number(e.value) : NUMBER) : 0); - break; + default: { + this.values.add(UNKNOWN); + } } - case 'String': { - const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg); + break; + } - this.values.add(e ? (e.is_known ? String(e.value) : STRING) : ''); - break; - } + if ( + Object.hasOwn(globals, keypath) && + expression.arguments.every((arg) => arg.type !== 'SpreadElement') + ) { + const [type, fn] = globals[keypath]; + const values = expression.arguments.map((arg) => scope.evaluate(arg)); - case 'BigInt': { - const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); - const e = arg && scope.evaluate(arg); - this.values.add(e ? (e.is_known ? BigInt(e.value) : NUMBER) : 0n); - break; + if (fn && values.every((e) => e.is_known)) { + this.values.add(fn(...values.map((e) => e.value))); + } else { + this.values.add(type); } - default: - this.values.add(UNKNOWN); - } - break; - } else if ( - expression.callee.type === 'MemberExpression' && - is_global_call(expression.callee, scope)?.length === 2 - ) { - const [object, property] = /** @type {[keyof typeof global_calls, string]} */ ( - is_global_call(expression.callee, scope) - ); - switch (object) { - case 'Math': { - const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => - scope.evaluate(arg) - ); - const all_are_known = args.every((evaluated) => evaluated.is_known); - const vals = all_are_known - ? //@ts-expect-error - /** @type {() => unknown} */ (Math[property])(...args.map((arg) => arg.value)) - : NUMBER; - this.values.add(vals); - break; - } - case 'Number': { - const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => - scope.evaluate(arg) - ); - const all_are_known = args.every((evaluated) => evaluated.is_known); - const vals = all_are_known - ? Number[ - /** @type {'isSafeInteger' | 'isFinite' | 'isNaN' | 'parseFloat' | 'parseInt' | 'isInteger'} */ ( - property - ) - //@ts-expect-error types are a monster here - ](...args.map((arg) => arg.value)) - : property.startsWith('is') - ? BOOLEAN - : NUMBER; - if (vals === BOOLEAN) { - this.values.add(true); - this.values.add(false); - } else { - this.values.add(vals); - } - break; - } - case 'String': { - const args = /** @type {Expression[]} */ (expression.arguments).map((arg) => - scope.evaluate(arg) - ); - const all_are_known = args.every((evaluated) => evaluated.is_known); - this.values.add( - all_are_known - ? String[/** @type {'fromCharCode' | 'fromCodePoint'} */ (property)]( - ...args.map((arg) => arg.value) - ) - : STRING - ); - break; - } + break; } - break; } this.values.add(UNKNOWN); @@ -618,67 +497,13 @@ class Evaluation { } case 'MemberExpression': { - if ( - expression.object.type !== 'Identifier' && - expression.object.type !== 'MemberExpression' - ) { - this.values.add(UNKNOWN); + const keypath = get_global_keypath(expression, scope); + + if (keypath && Object.hasOwn(global_constants, keypath)) { + this.values.add(global_constants[keypath]); break; } - const { object, property, computed } = expression; - if ( - object.type === 'MemberExpression' && - object.object.type === 'Identifier' && - object.object.name === 'globalThis' && - !scope.get('globalThis') - ) { - if (object.computed && object.property.type !== 'PrivateIdentifier') { - const key = scope.evaluate(object.property); - if (key.is_known && key.value === 'Math') { - if (computed && property.type !== 'PrivateIdentifier') { - const math_prop = scope.evaluate(property); - if (math_prop.is_known && math_constants.includes(math_prop.value)) { - this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); - break; - } - } else if (property.type === 'Identifier') { - if (math_constants.includes(property.name)) { - this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]); - break; - } - } - } - } else if (object.property.type === 'Identifier' && object.property.name === 'Math') { - if (computed && property.type !== 'PrivateIdentifier') { - const math_prop = scope.evaluate(property); - if (math_prop.is_known && math_constants.includes(math_prop.value)) { - this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); - break; - } - } else if (property.type === 'Identifier') { - if (math_constants.includes(property.name)) { - this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]); - break; - } - } - } else { - this.values.add(UNKNOWN); - break; - } - } else if (object.type === 'Identifier' && object.name === 'Math' && !scope.get('Math')) { - if (computed && property.type !== 'PrivateIdentifier') { - const math_prop = scope.evaluate(property); - if (math_prop.is_known && math_constants.includes(math_prop.value)) { - this.values.add(Math[/** @type {keyof typeof Math} */ (math_prop.value)]); - break; - } - } else if (property.type === 'Identifier') { - if (math_constants.includes(property.name)) { - this.values.add(Math[/** @type {keyof typeof Math} */ (property.name)]); - break; - } - } - } + this.values.add(UNKNOWN); break; } @@ -1462,7 +1287,19 @@ export function get_rune(node, scope) { if (!node) return null; if (node.type !== 'CallExpression') return null; - let n = node.callee; + const keypath = get_global_keypath(node.callee, scope); + + if (!keypath || !is_rune(keypath)) return null; + return keypath; +} + +/** + * Returns the name of the rune if the given expression is a `CallExpression` using a rune. + * @param {Expression | Super} node + * @param {Scope} scope + */ +function get_global_keypath(node, scope) { + let n = node; let joined = ''; @@ -1480,12 +1317,8 @@ export function get_rune(node, scope) { if (n.type !== 'Identifier') return null; - joined = n.name + joined; - - if (!is_rune(joined)) return null; - const binding = scope.get(n.name); if (binding !== null) return null; // rune name, but references a variable or store - return joined; + return n.name + joined; } From 8f6f0778c0de2dc4673c3b9e6cea0d8eebbf7711 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 10:35:02 -0400 Subject: [PATCH 21/25] Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js --- .../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 2aae1d0cb337..bc79b760431c 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 @@ -3,7 +3,7 @@ /** @import { ComponentClientTransformState, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; -import * as b from '../../../../../utils/builders.js'; +import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; From 47851cacbc2f24de6bd7331cc52def35665181dd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 10:36:36 -0400 Subject: [PATCH 22/25] YAGNI --- packages/svelte/src/compiler/phases/scope.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index b454f2e6c91c..bba45e086088 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -20,10 +20,8 @@ const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ export const NUMBER = Symbol('number'); export const STRING = Symbol('string'); -/** Used for when you need to add `true` and `false` to the values, but can't do it for whatever reason */ -const BOOLEAN = Symbol('boolean'); -/** @type {Record} */ const globals = { BigInt: [NUMBER, BigInt], 'Math.min': [NUMBER, Math.min], From 2aec7b7222e919a835ac12bd6123b7541e484994 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 10:46:13 -0400 Subject: [PATCH 23/25] simplify and fix (should use cooked, not raw) --- packages/svelte/src/compiler/phases/scope.js | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index bba45e086088..a27fe2b5cd81 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -471,26 +471,20 @@ class Evaluation { } case 'TemplateLiteral': { - const expressions = expression.expressions.map((expr) => scope.evaluate(expr)); - const all_are_known = expressions.every((evaluated) => evaluated.is_known); - if (all_are_known) { - let res = ''; - let quasi_index = 0; - let expr_index = 0; - let last_quasi = false; - for (let i = 0; i < expressions.length + expression.quasis.length; i++) { - if (last_quasi) { - const expression = expressions[expr_index++]; - res += expression.value; - } else { - res += expression.quasis[quasi_index++].value.raw; - } - last_quasi = !last_quasi; + let result = expression.quasis[0].value.cooked; + + for (let i = 0; i < expression.expressions.length; i += 1) { + const e = scope.evaluate(expression.expressions[i]); + + if (e.is_known) { + result += e.value + expression.quasis[i + 1].value.cooked; + } else { + this.values.add(STRING); + break; } - this.values.add(res); - } else { - this.values.add(STRING); } + + this.values.add(result); break; } From 040edc31f5d02b914cfe4751053e7818abab85cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 11:15:10 -0400 Subject: [PATCH 24/25] unused --- 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 a27fe2b5cd81..8297f174d3de 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, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, Function, Program, PrivateIdentifier } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; From 0ef68b90a7d9223f2759a1672bb46e1830463115 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 11:16:20 -0400 Subject: [PATCH 25/25] changeset --- .changeset/serious-adults-sit.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/serious-adults-sit.md b/.changeset/serious-adults-sit.md index 8111008b1ef0..8c98a7c3666a 100644 --- a/.changeset/serious-adults-sit.md +++ b/.changeset/serious-adults-sit.md @@ -1,5 +1,5 @@ --- -'svelte': patch +'svelte': minor --- -fix: partially evaluate more expressions +feat: partially evaluate more expressions