From 050cb5be6f1294becd0f2cd5289a0aeca6a39631 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:19:55 -0700 Subject: [PATCH 01/19] init --- packages/svelte/src/compiler/phases/scope.js | 96 +++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 8297f174d3de..18fd19a8515a 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 { regex_is_valid_identifier } from './patterns.js'; const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ @@ -24,11 +25,12 @@ export const STRING = Symbol('string'); /** @type {Record} */ const globals = { BigInt: [NUMBER, BigInt], + 'Date.now': [NUMBER], 'Math.min': [NUMBER, Math.min], 'Math.max': [NUMBER, Math.max], 'Math.random': [NUMBER], 'Math.floor': [NUMBER, Math.floor], - // @ts-expect-error + // @ts-ignore 'Math.f16round': [NUMBER, Math.f16round], 'Math.round': [NUMBER, Math.round], 'Math.abs': [NUMBER, Math.abs], @@ -84,6 +86,39 @@ const global_constants = { 'Math.SQRT1_2': Math.SQRT1_2 }; +/** + * @template T + * @param {(...args: any) => T} fn + * @returns {(this: unknown, ...args: any) => T} + */ +function call_bind(fn) { + return /** @type {(this: unknown, ...args: any) => T} */ (fn.call.bind(fn)); +} + +const string_proto = String.prototype; +const number_proto = Number.prototype; + +/** @type {Record>} */ +const prototype_methods = { + string: { + //@ts-ignore + toString: [STRING, call_bind(string_proto.toString)], + toLowerCase: [STRING, call_bind(string_proto.toLowerCase)], + toUpperCase: [STRING, call_bind(string_proto.toUpperCase)], + slice: [STRING, call_bind(string_proto.slice)], + at: [STRING, call_bind(string_proto.at)], + charAt: [STRING, call_bind(string_proto.charAt)], + trim: [STRING, call_bind(string_proto.trim)], + indexOf: [STRING, call_bind(string_proto.indexOf)] + }, + number: { + //@ts-ignore + toString: [STRING, call_bind(number_proto.toString)], + toFixed: [NUMBER, call_bind(number_proto.toFixed)], + toExponential: [NUMBER, call_bind(number_proto.toExponential)], + toPrecision: [NUMBER, call_bind(number_proto.toPrecision)] + } +}; export class Binding { /** @type {Scope} */ scope; @@ -462,6 +497,53 @@ class Evaluation { this.values.add(type); } + break; + } + } else if ( + expression.callee.type === 'MemberExpression' && + expression.callee.object.type !== 'Super' && + expression.arguments.every((arg) => arg.type !== 'SpreadElement') + ) { + const object = scope.evaluate(expression.callee.object); + if (!object.is_known) { + this.values.add(UNKNOWN); + break; + } + let property; + if ( + expression.callee.computed && + expression.callee.property.type !== 'PrivateIdentifier' + ) { + property = scope.evaluate(expression.callee.property); + if (property.is_known) { + property = property.value; + } else { + this.values.add(UNKNOWN); + break; + } + } else if (expression.callee.property.type === 'Identifier') { + property = expression.callee.property.name; + } + if (property === undefined) { + this.values.add(UNKNOWN); + break; + } + if (typeof object.value !== 'string' && typeof object.value !== 'number') { + this.values.add(UNKNOWN); + break; + } + const available_methods = + prototype_methods[/** @type {'string' | 'number'} */ (typeof object.value)]; + if (Object.hasOwn(available_methods, property)) { + const [type, fn] = available_methods[property]; + console.log([type, fn]); + const values = expression.arguments.map((arg) => scope.evaluate(arg)); + + if (fn && values.every((e) => e.is_known)) { + this.values.add(fn(object.value, ...values.map((e) => e.value))); + } else { + this.values.add(type); + } break; } } @@ -1296,7 +1378,17 @@ function get_global_keypath(node, scope) { let joined = ''; while (n.type === 'MemberExpression') { - if (n.computed) return null; + if (n.computed && n.property.type !== 'PrivateIdentifier') { + const property = scope.evaluate(n.property); + if (property.is_known) { + if (!regex_is_valid_identifier.test(property.value)) { + return null; + } + joined = '.' + property.value + joined; + n = n.object; + continue; + } + } if (n.property.type !== 'Identifier') return null; joined = '.' + n.property.name + joined; n = n.object; From f155d6530f1635ed29abb1501f7da37e4dea1d44 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 12:51:00 -0700 Subject: [PATCH 02/19] more string methods --- packages/svelte/src/compiler/phases/scope.js | 44 ++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 18fd19a8515a..75e24bc01d83 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -21,8 +21,9 @@ const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ export const NUMBER = Symbol('number'); export const STRING = Symbol('string'); +/** @typedef {NUMBER | STRING | UNKNOWN | undefined | boolean} TYPE */ -/** @type {Record} */ +/** @type {Record} */ const globals = { BigInt: [NUMBER, BigInt], 'Date.now': [NUMBER], @@ -98,7 +99,7 @@ function call_bind(fn) { const string_proto = String.prototype; const number_proto = Number.prototype; -/** @type {Record>} */ +/** @type {Record>} */ const prototype_methods = { string: { //@ts-ignore @@ -109,14 +110,32 @@ const prototype_methods = { at: [STRING, call_bind(string_proto.at)], charAt: [STRING, call_bind(string_proto.charAt)], trim: [STRING, call_bind(string_proto.trim)], - indexOf: [STRING, call_bind(string_proto.indexOf)] + indexOf: [NUMBER, call_bind(string_proto.indexOf)], + charCodeAt: [NUMBER, call_bind(string_proto.charCodeAt)], + codePointAt: [[NUMBER, undefined], call_bind(string_proto.codePointAt)], + startsWith: [[true, false], call_bind(string_proto.startsWith)], + endsWith: [[true, false], call_bind(string_proto.endsWith)], + isWellFormed: [[true, false], call_bind(string_proto.isWellFormed)], + lastIndexOf: [NUMBER, call_bind(string_proto.lastIndexOf)], + normalize: [STRING, call_bind(string_proto.normalize)], + padEnd: [STRING, call_bind(string_proto.padEnd)], + padStart: [STRING, call_bind(string_proto.padStart)], + repeat: [STRING, call_bind(string_proto.repeat)], + substring: [STRING, call_bind(string_proto.substring)], + trimEnd: [STRING, call_bind(string_proto.trimEnd)], + trimStart: [STRING, call_bind(string_proto.trimStart)], + toWellFormed: [STRING, call_bind(string_proto.toWellFormed)], + //@ts-ignore + valueOf: [STRING, call_bind(string_proto.valueOf)] }, number: { //@ts-ignore toString: [STRING, call_bind(number_proto.toString)], toFixed: [NUMBER, call_bind(number_proto.toFixed)], toExponential: [NUMBER, call_bind(number_proto.toExponential)], - toPrecision: [NUMBER, call_bind(number_proto.toPrecision)] + toPrecision: [NUMBER, call_bind(number_proto.toPrecision)], + //@ts-ignore + valueOf: [NUMBER, call_bind(number_proto.valueOf)] } }; export class Binding { @@ -494,7 +513,13 @@ class Evaluation { if (fn && values.every((e) => e.is_known)) { this.values.add(fn(...values.map((e) => e.value))); } else { - this.values.add(type); + if (Array.isArray(type)) { + for (const t of type) { + this.values.add(t); + } + } else { + this.values.add(type); + } } break; @@ -536,13 +561,18 @@ class Evaluation { prototype_methods[/** @type {'string' | 'number'} */ (typeof object.value)]; if (Object.hasOwn(available_methods, property)) { const [type, fn] = available_methods[property]; - console.log([type, fn]); const values = expression.arguments.map((arg) => scope.evaluate(arg)); if (fn && values.every((e) => e.is_known)) { this.values.add(fn(object.value, ...values.map((e) => e.value))); } else { - this.values.add(type); + if (Array.isArray(type)) { + for (const t of type) { + this.values.add(t); + } + } else { + this.values.add(type); + } } break; } From 034aab970c9bedcda206200dd9540cac5e4854d0 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:05:54 -0700 Subject: [PATCH 03/19] add some object globals, Function.prototype.name --- packages/svelte/src/compiler/phases/scope.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 75e24bc01d83..2e28cdfb6d17 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -22,7 +22,6 @@ const UNKNOWN = Symbol('unknown'); export const NUMBER = Symbol('number'); export const STRING = Symbol('string'); /** @typedef {NUMBER | STRING | UNKNOWN | undefined | boolean} TYPE */ - /** @type {Record} */ const globals = { BigInt: [NUMBER, BigInt], @@ -70,6 +69,8 @@ const globals = { 'Number.isSafeInteger': [NUMBER, Number.isSafeInteger], 'Number.parseFloat': [NUMBER, Number.parseFloat], 'Number.parseInt': [NUMBER, Number.parseInt], + 'Object.is': [[true, false], Object.is], + 'Object.hasOwn': [[true, false], Object.hasOwn], String: [STRING, String], 'String.fromCharCode': [STRING, String.fromCharCode], 'String.fromCodePoint': [STRING, String.fromCodePoint] @@ -606,6 +607,9 @@ class Evaluation { if (keypath && Object.hasOwn(global_constants, keypath)) { this.values.add(global_constants[keypath]); break; + } else if (keypath?.match(/\.name$/) && Object.hasOwn(globals, keypath.slice(0, -5))) { + this.values.add(globals[keypath.slice(0, -5)]?.[1]?.name ?? STRING); + break; } this.values.add(UNKNOWN); From 3c0601223ee1d25dee15bcccf373e533534a4e09 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:08:28 -0700 Subject: [PATCH 04/19] remove some methods --- 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 2e28cdfb6d17..47eaf5d4ba6f 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -70,7 +70,6 @@ const globals = { 'Number.parseFloat': [NUMBER, Number.parseFloat], 'Number.parseInt': [NUMBER, Number.parseInt], 'Object.is': [[true, false], Object.is], - 'Object.hasOwn': [[true, false], Object.hasOwn], String: [STRING, String], 'String.fromCharCode': [STRING, String.fromCharCode], 'String.fromCodePoint': [STRING, String.fromCodePoint] @@ -117,7 +116,6 @@ const prototype_methods = { startsWith: [[true, false], call_bind(string_proto.startsWith)], endsWith: [[true, false], call_bind(string_proto.endsWith)], isWellFormed: [[true, false], call_bind(string_proto.isWellFormed)], - lastIndexOf: [NUMBER, call_bind(string_proto.lastIndexOf)], normalize: [STRING, call_bind(string_proto.normalize)], padEnd: [STRING, call_bind(string_proto.padEnd)], padStart: [STRING, call_bind(string_proto.padStart)], From 95cdafd39daa02cef579ce29c52d844f3752cf96 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:10:48 -0700 Subject: [PATCH 05/19] try this --- 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 47eaf5d4ba6f..59e99c2c8b18 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -115,7 +115,6 @@ const prototype_methods = { codePointAt: [[NUMBER, undefined], call_bind(string_proto.codePointAt)], startsWith: [[true, false], call_bind(string_proto.startsWith)], endsWith: [[true, false], call_bind(string_proto.endsWith)], - isWellFormed: [[true, false], call_bind(string_proto.isWellFormed)], normalize: [STRING, call_bind(string_proto.normalize)], padEnd: [STRING, call_bind(string_proto.padEnd)], padStart: [STRING, call_bind(string_proto.padStart)], From 3adf47d795012556d29e9fdbbe76cf8a54a19204 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:12:05 -0700 Subject: [PATCH 06/19] try this --- 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 59e99c2c8b18..394493942b9a 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -122,7 +122,6 @@ const prototype_methods = { substring: [STRING, call_bind(string_proto.substring)], trimEnd: [STRING, call_bind(string_proto.trimEnd)], trimStart: [STRING, call_bind(string_proto.trimStart)], - toWellFormed: [STRING, call_bind(string_proto.toWellFormed)], //@ts-ignore valueOf: [STRING, call_bind(string_proto.valueOf)] }, From 784d07b0c1b3c8d7c1b69901beaffd30ad6a751a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:20:22 -0700 Subject: [PATCH 07/19] only memoize function calls that can't be evaluated --- .../phases/3-transform/client/visitors/shared/utils.js | 9 +++++---- 1 file changed, 5 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 bc79b760431c..7d2b2179d893 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,13 +64,14 @@ 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); + if (!evaluated.is_known) { + value = memoize(value, node.metadata.expression); + } + has_state ||= node.metadata.expression.has_state && !evaluated.is_known; if (values.length === 1) { From 4ec133498eefaf7d0f96823c223b9deceeef8a41 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:23:06 -0700 Subject: [PATCH 08/19] fix --- .../phases/3-transform/client/visitors/shared/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 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 7d2b2179d893..67fe9af903b8 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 @@ -78,7 +78,7 @@ export function build_template_chunk( // 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). if (evaluated.is_known) { - value = b.literal(evaluated.value); + value = b.literal(evaluated.value ?? ''); } return { value, has_state }; @@ -97,7 +97,7 @@ export function build_template_chunk( } if (evaluated.is_known) { - quasi.value.cooked += evaluated.value + ''; + quasi.value.cooked += (evaluated.value ?? '') + ''; } else { if (!evaluated.is_defined) { // add `?? ''` where necessary From ee69a4f4f57478f9c257c52a6e5fb391ec21e19b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 13:58:00 -0700 Subject: [PATCH 09/19] start work on typescript type annotations --- packages/svelte/src/compiler/index.js | 1 + .../phases/1-parse/remove_typescript_nodes.js | 21 ++++- packages/svelte/src/compiler/phases/scope.js | 78 +++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 42427dd9c407..041632739316 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -122,6 +122,7 @@ function to_public_ast(source, ast, modern) { if (modern) { const clean = (/** @type {any} */ node) => { delete node.metadata; + delete node.type_information; }; ast.options?.attributes.forEach((attribute) => { diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index aba94ee20db4..0defc8ba12ff 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -20,8 +20,25 @@ const visitors = { _(node, context) { const n = context.next() ?? node; - // TODO there may come a time when we decide to preserve type annotations. - // until that day comes, we just delete them so they don't confuse esrap + const type_information = {}; + if (Object.hasOwn(n, 'typeAnnotation')) { + type_information.annotation = n.typeAnnotation; + } + if (Object.hasOwn(n, 'typeParameters')) { + type_information.parameters = n.typeParameters; + } + if (Object.hasOwn(n, 'typeArguments')) { + type_information.arguments = n.typeArguments; + } + if (Object.hasOwn(n, 'returnType')) { + type_information.return = n.returnType; + } + Object.defineProperty(n, 'type_information', { + value: type_information, + writable: true, + configurable: true, + enumerable: false + }); delete n.typeAnnotation; delete n.typeParameters; delete n.typeArguments; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 394493942b9a..b1d49ac89c18 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -573,6 +573,43 @@ 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) { + if (Object.hasOwn(fn, 'type_information')) { + // @ts-ignore + const { type_information } = fn; + if (Object.hasOwn(type_information, 'return')) { + const return_types = get_type_of_ts_node( + type_information.return?.type_information?.annotation, + scope + ); + if (Array.isArray(return_types)) { + for (let type of return_types) { + this.values.add(type); + } + } else { + this.values.add(return_types); + } + break; + } + } + } + } + } } this.values.add(UNKNOWN); @@ -1436,3 +1473,44 @@ function get_global_keypath(node, scope) { return n.name + joined; } + +/** + * @param {{type: string} & Record} node + * @param {Scope} scope + * @returns {any} + */ +function get_type_of_ts_node(node, scope) { + switch (node.type) { + case 'TypeAnnotation': + return get_type_of_ts_node(node.annotation, scope); + case 'TSCheckType': + return [ + get_type_of_ts_node(node.trueType, scope), + get_type_of_ts_node(node.falseType, scope) + ].flat(); + case 'TSBigIntKeyword': + case 'TSNumberKeyword': + return NUMBER; + case 'TSStringKeyword': + return STRING; + case 'TSLiteralType': + return node.literal.type === 'Literal' + ? typeof node.literal.value === 'string' + ? STRING + : ['number', 'bigint'].includes(typeof node.literal.value) + ? NUMBER + : typeof node.literal.value === 'boolean' + ? [true, false] + : UNKNOWN + : node.literal.type === 'TemplateLiteral' + ? STRING + : UNKNOWN; + case 'TSBooleanKeyword': + return [true, false]; + case 'TSNeverKeyword': + case 'TSVoidKeyword': + return undefined; + default: + return UNKNOWN; + } +} From f7227b1acae9c9bd496b8df5cefab190b194cc26 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:05:18 -0700 Subject: [PATCH 10/19] improve type annotation analysis --- packages/svelte/src/compiler/phases/scope.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index b1d49ac89c18..9e4e4907882d 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1495,13 +1495,7 @@ function get_type_of_ts_node(node, scope) { return STRING; case 'TSLiteralType': return node.literal.type === 'Literal' - ? typeof node.literal.value === 'string' - ? STRING - : ['number', 'bigint'].includes(typeof node.literal.value) - ? NUMBER - : typeof node.literal.value === 'boolean' - ? [true, false] - : UNKNOWN + ? node.literal.value : node.literal.type === 'TemplateLiteral' ? STRING : UNKNOWN; @@ -1510,6 +1504,8 @@ function get_type_of_ts_node(node, scope) { case 'TSNeverKeyword': case 'TSVoidKeyword': return undefined; + case 'TSNullKeyword': + return null; default: return UNKNOWN; } From ffec7680a348546f2e107d05227f78549ff2e10d Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:47:36 -0700 Subject: [PATCH 11/19] union and intersection types --- packages/svelte/src/compiler/phases/scope.js | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9e4e4907882d..738c9a8c41e4 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1480,6 +1480,43 @@ function get_global_keypath(node, scope) { * @returns {any} */ function get_type_of_ts_node(node, scope) { + /** + * @param {any[]} types + * @returns {any[]} + */ + function intersect_types(types) { + if (types.includes(UNKNOWN)) return [UNKNOWN]; + /** @type {any[]} */ + let res = []; + if ( + types.filter((type) => typeof type === 'number' || typeof type === 'bigint').length > 1 || + (!types.some((type) => typeof type === 'number' || typeof type === 'bigint') && + types.includes(NUMBER)) + ) { + res.push(NUMBER); + } else { + res.push(...types.filter((type) => typeof type === 'number' || typeof type === 'bigint')); + } + if ( + types.filter((type) => typeof type === 'string').length > 1 || + (!types.some((type) => typeof type === 'string') && types.includes(STRING)) + ) { + res.push(STRING); + } else { + res.push(...types.filter((type) => typeof type === 'string')); + } + if ( + types.filter((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type)) + .length > 1 + ) { + res.push(UNKNOWN); + } else { + types.push( + ...types.filter((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type)) + ); + } + return res; + } switch (node.type) { case 'TypeAnnotation': return get_type_of_ts_node(node.annotation, scope); @@ -1488,6 +1525,12 @@ function get_type_of_ts_node(node, scope) { get_type_of_ts_node(node.trueType, scope), get_type_of_ts_node(node.falseType, scope) ].flat(); + case 'TSUnionType': + //@ts-ignore + return node.types.map((type) => get_type_of_ts_node(type, scope)).flat(); + case 'TSIntersectionType': + //@ts-ignore + return intersect_types(node.types.map((type) => get_type_of_ts_node(type, scope)).flat()); case 'TSBigIntKeyword': case 'TSNumberKeyword': return NUMBER; From 4f1f4cc265e90e1156abbdce7c9f676d7a121c26 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:06:01 -0700 Subject: [PATCH 12/19] `$props` will never be null --- packages/svelte/src/compiler/phases/scope.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 738c9a8c41e4..67b7a6fe16f7 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -21,6 +21,7 @@ const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ export const NUMBER = Symbol('number'); export const STRING = Symbol('string'); +const NOT_NULL = Symbol('not null'); /** @typedef {NUMBER | STRING | UNKNOWN | undefined | boolean} TYPE */ /** @type {Record} */ const globals = { @@ -298,6 +299,11 @@ class Evaluation { binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values); break; } + + if (binding.kind === 'rest_prop' && !binding.updated) { + this.values.add(NOT_NULL); + break; + } } else if (expression.name === 'undefined') { this.values.add(undefined); break; From 7f609aba363c0373b496297471e9fde10f2a670f Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Apr 2025 02:09:38 -0700 Subject: [PATCH 13/19] start function analysis, only `$.escape` when necessary --- .../server/visitors/shared/utils.js | 15 +- packages/svelte/src/compiler/phases/scope.js | 436 ++++++++++++++++-- .../_expected/server/index.svelte.js | 2 +- 3 files changed, 413 insertions(+), 40 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 8fcf8efa68b6..b7fabacc2c55 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -11,6 +11,7 @@ import { import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; +import { NUMBER } from '../../../../scope.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); @@ -45,13 +46,19 @@ 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 expression = /** @type {Expression} */ (visit(node.expression)); + const evaluated = state.scope.evaluate(expression); if (evaluated.is_known) { quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); - + if ( + (evaluated.values.size === 1 && [...evaluated.values][0] === NUMBER) || + [...evaluated.values].every((value) => typeof value === 'string' && !/[&<]/.test(value)) + ) { + expressions.push(expression); + } else { + expressions.push(b.call('$.escape', expression)); + } quasi = b.quasi('', i + 1 === sequence.length); quasis.push(quasi); } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 67b7a6fe16f7..d7bfa52eb545 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 } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, CallExpression, NewExpression } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -17,12 +17,14 @@ import { determine_slot } from '../utils/slot.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; import { regex_is_valid_identifier } from './patterns.js'; +/** Highest precedence, could be any type, including `undefined` */ const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ export const NUMBER = Symbol('number'); export const STRING = Symbol('string'); const NOT_NULL = Symbol('not null'); /** @typedef {NUMBER | STRING | UNKNOWN | undefined | boolean} TYPE */ +const TYPES = [NUMBER, STRING, UNKNOWN, NOT_NULL, undefined, true, false]; /** @type {Record} */ const globals = { BigInt: [NUMBER, BigInt], @@ -263,8 +265,9 @@ class Evaluation { * @param {Scope} scope * @param {Expression} expression * @param {Set} values + * @param {Binding[]} seen_bindings */ - constructor(scope, expression, values) { + constructor(scope, expression, values, seen_bindings) { this.values = values; switch (expression.type) { @@ -276,6 +279,7 @@ class Evaluation { case 'Identifier': { const binding = scope.get(expression.name); + if (binding && seen_bindings.includes(binding)) break; if (binding) { if ( binding.initial?.type === 'CallExpression' && @@ -296,7 +300,10 @@ 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, [ + ...seen_bindings, + binding + ]); break; } @@ -317,8 +324,12 @@ 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(), + seen_bindings + ); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = scope.evaluate(expression.right, new Set(), seen_bindings); if (a.is_known && b.is_known) { this.values.add(binary[expression.operator](a.value, b.value)); @@ -372,9 +383,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(), seen_bindings); + const consequent = scope.evaluate(expression.consequent, new Set(), seen_bindings); + const alternate = scope.evaluate(expression.alternate, new Set(), seen_bindings); if (test.is_known) { for (const value of (test.value ? consequent : alternate).values) { @@ -393,8 +404,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(), seen_bindings); + const b = scope.evaluate(expression.right, new Set(), seen_bindings); if (a.is_known) { if (b.is_known) { @@ -428,7 +439,7 @@ class Evaluation { } case 'UnaryExpression': { - const argument = scope.evaluate(expression.argument); + const argument = scope.evaluate(expression.argument, new Set(), seen_bindings); if (argument.is_known) { this.values.add(unary[expression.operator](argument.value)); @@ -462,10 +473,23 @@ class Evaluation { break; } + case 'SequenceExpression': { + const { expressions } = expression; + const evaluated = expressions.map((expression) => + scope.evaluate(expression, new Set(), seen_bindings) + ); + if (evaluated.every((ev) => ev.is_known)) { + this.values.add(evaluated.at(-1)?.value); + } else { + this.values.add(UNKNOWN); + } + break; + } + case 'CallExpression': { const keypath = get_global_keypath(expression.callee, scope); - if (keypath) { + if (keypath !== null) { if (is_rune(keypath)) { const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); @@ -490,12 +514,11 @@ class Evaluation { break; case '$derived.by': - if (arg?.type === 'ArrowFunctionExpression' && arg.body.type !== 'BlockStatement') { - scope.evaluate(arg.body, this.values); - break; - } + scope.evaluate(b.call(/** @type {Expression} */ (arg)), this.values, seen_bindings); + break; - this.values.add(UNKNOWN); + case '$effect.root': + this.values.add(NOT_NULL); break; default: { @@ -511,7 +534,9 @@ class Evaluation { expression.arguments.every((arg) => arg.type !== 'SpreadElement') ) { const [type, fn] = globals[keypath]; - const values = expression.arguments.map((arg) => scope.evaluate(arg)); + const values = expression.arguments.map((arg) => + scope.evaluate(arg, new Set(), seen_bindings) + ); if (fn && values.every((e) => e.is_known)) { this.values.add(fn(...values.map((e) => e.value))); @@ -532,7 +557,7 @@ class Evaluation { expression.callee.object.type !== 'Super' && expression.arguments.every((arg) => arg.type !== 'SpreadElement') ) { - const object = scope.evaluate(expression.callee.object); + const object = scope.evaluate(expression.callee.object, new Set(), seen_bindings); if (!object.is_known) { this.values.add(UNKNOWN); break; @@ -542,7 +567,7 @@ class Evaluation { expression.callee.computed && expression.callee.property.type !== 'PrivateIdentifier' ) { - property = scope.evaluate(expression.callee.property); + property = scope.evaluate(expression.callee.property, new Set(), seen_bindings); if (property.is_known) { property = property.value; } else { @@ -564,7 +589,9 @@ class Evaluation { prototype_methods[/** @type {'string' | 'number'} */ (typeof object.value)]; if (Object.hasOwn(available_methods, property)) { const [type, fn] = available_methods[property]; - const values = expression.arguments.map((arg) => scope.evaluate(arg)); + const values = expression.arguments.map((arg) => + scope.evaluate(arg, new Set(), seen_bindings) + ); if (fn && values.every((e) => e.is_known)) { this.values.add(fn(object.value, ...values.map((e) => e.value))); @@ -582,19 +609,18 @@ class Evaluation { } 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') - ) { + if (is_valid_function_binding(binding)) { const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ ( binding.initial ); if (fn && fn.async === false && !fn?.generator) { + const analysis = evaluate_function(fn, binding); // typescript won't tell you if a function is pure or if it could throw, so we have to do this regardless of type annotations + console.log({ fn, binding, analysis }); + if (!analysis.pure || !analysis.never_throws) { + // if its not pure, or we don't know if it could throw, we can't use any constant return values from the evaluation, but we can check if its nullish + this.values.add(NOT_NULL); // `NOT_NULL` doesn't have precedence over `UNKNOWN`, so if the value is nullish, this won't have precedence + } if (Object.hasOwn(fn, 'type_information')) { // @ts-ignore const { type_information } = fn; @@ -610,12 +636,70 @@ class Evaluation { } else { this.values.add(return_types); } + } else if (analysis.is_known) { + this.values.add(analysis.value); break; + } else { + for (let value of analysis.values) { + this.values.add(value); + } + } + } else if (analysis.is_known) { + this.values.add(analysis.value); + break; + } else { + for (let value of analysis.values) { + this.values.add(value); } } + break; } } } + } else if ( + expression.callee.type === 'ArrowFunctionExpression' || + expression.callee.type === 'FunctionExpression' + ) { + const fn = expression.callee; + const binding = /** @type {Binding} */ ({ scope }); + if (fn && fn.async === false && !fn?.generator) { + const analysis = evaluate_function(fn, binding); + if (!analysis.pure || !analysis.never_throws) { + this.values.add(NOT_NULL); + } + if (Object.hasOwn(fn, 'type_information')) { + // @ts-ignore + const { type_information } = fn; + if (Object.hasOwn(type_information, 'return')) { + const return_types = get_type_of_ts_node( + type_information.return?.type_information?.annotation, + scope + ); + if (Array.isArray(return_types)) { + for (let type of return_types) { + this.values.add(type); + } + } else { + this.values.add(return_types); + } + } else if (analysis.is_known) { + this.values.add(analysis.value); + break; + } else { + for (let value of analysis.values) { + this.values.add(value); + } + } + } else if (analysis.is_known) { + this.values.add(analysis.value); + break; + } else { + for (let value of analysis.values) { + this.values.add(value); + } + } + break; + } } this.values.add(UNKNOWN); @@ -626,7 +710,7 @@ class Evaluation { let result = expression.quasis[0].value.cooked; for (let i = 0; i < expression.expressions.length; i += 1) { - const e = scope.evaluate(expression.expressions[i]); + const e = scope.evaluate(expression.expressions[i], new Set(), seen_bindings); if (e.is_known) { result += e.value + expression.quasis[i + 1].value.cooked; @@ -643,10 +727,10 @@ class Evaluation { case 'MemberExpression': { const keypath = get_global_keypath(expression, scope); - if (keypath && Object.hasOwn(global_constants, keypath)) { + if (keypath !== null && Object.hasOwn(global_constants, keypath)) { this.values.add(global_constants[keypath]); break; - } else if (keypath?.match(/\.name$/) && Object.hasOwn(globals, keypath.slice(0, -5))) { + } else if (keypath?.match?.(/\.name$/) && Object.hasOwn(globals, keypath.slice(0, -5))) { this.values.add(globals[keypath.slice(0, -5)]?.[1]?.name ?? STRING); break; } @@ -868,9 +952,10 @@ export class Scope { * else this evaluates on incomplete data and may yield wrong results. * @param {Expression} expression * @param {Set} [values] + * @param {Binding[]} [seen_bindings] */ - evaluate(expression, values = new Set()) { - return new Evaluation(this, expression, values); + evaluate(expression, values = new Set(), seen_bindings = []) { + return new Evaluation(this, expression, values, seen_bindings); } } @@ -921,6 +1006,8 @@ const logical = { export class ScopeRoot { /** @type {Set} */ conflicts = new Set(); + /** @type {Map} */ + scopes = new Map(); /** * @param {string} preferred_name @@ -957,6 +1044,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const scope = new Scope(root, parent, false); scopes.set(ast, scope); + root.scopes = scopes; /** @type {State} */ const state = { scope }; @@ -1436,7 +1524,7 @@ export function get_rune(node, scope) { const keypath = get_global_keypath(node.callee, scope); - if (!keypath || !is_rune(keypath)) return null; + if (keypath === null || !is_rune(keypath)) return null; return keypath; } @@ -1559,3 +1647,281 @@ function get_type_of_ts_node(node, scope) { return UNKNOWN; } } + +// 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', + 'globalThis' +]; + +let fn_cache = new Map(); + +/** + * Analyzes and partially evaluates the provided function. + * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} fn + * @param {Binding} binding + * @param {Set} [stack] + * @param {Binding[]} [seen_bindings] + */ +function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = []) { + if (fn_cache.has(fn)) { + return fn_cache.get(fn); + } + /** + * This big blob of comments is for my (https://github.com/Ocean-OS) sanity and for that of anyone who tries working with this function. Feel free to modify this as the function evolves. + * So, when evaluating functions at compile-time, there are a few things you have to avoid evaluating: + * + * - Side effects + * A function that modifies state from outside of its scope should not be evaluated. + * Additionally, since `$effect`s and `$derived`s exist, any reference to an external value could lead to a missed dependency if the function is evaluated by the compiler. + * - Errors + * A function that could throw an error should not be evaluated. Additionally, `$derived`s could be reevaluated upon reading, which could throw an error. + * The purpose of a compile-time evaluator is to replicate the behavior the function would have at runtime, but in compile time. + * If an error is/could be thrown, that can not be replicated. + * + * So, how do we figure out if either of these things (could) happen in a function? + * Well, for errors, it's relatively simple. If a `throw` statement is used in the function, then we assume that the error could be thrown at any time. + * For side effects, it gets a bit tricky. External `Identifier`s that change their value are definitely side effects, but also any `MemberExpression` that isn't a known global constant could have a side effect, due to getters and `Proxy`s. + * Additionally, since a function can call other functions, we check each individual function call: if it's a known global, we know its pure, and if we can find its definition, the parent function inherits its throwability and purity. If we cannot find its definition, we assume it is impure and could throw. + * + * A few other things to note/remember: + * - Not all functions rely on return statements to determine the return value. + * Arrow functions without a `BlockStatement` for a body use their expression body as an implicit `ReturnStatement`. + * - While currently all the globals we have are pure and error-free, that could change, so we shouldn't be too dependent on that in the future. + * Things like `JSON.stringify` and a *lot* of array methods are prime examples. + */ + 1; + const analysis = { + pure: true, + is_known: false, + is_defined: true, + values: new Set(), + /** @type {any} */ + value: undefined, + never_throws: true + }; + const fn_binding = binding; + const fn_scope = fn.metadata.scope; + const CALL_EXPRESSION = 1 << 1; + const NEW_EXPRESSION = 1 << 2; + const state = { + scope: fn_scope, + scope_path: [fn_scope], + current_call: 0 + }; + const uses_implicit_return = + fn.type === 'ArrowFunctionExpression' && fn.body.type !== 'BlockStatement'; + /** + * @param {CallExpression | NewExpression} node + * @param {import('zimmerframe').Context} context + */ + function handle_call_expression(node, context) { + const { callee: call, arguments: args } = node; + const callee = context.visit(call, { + ...context.state, + current_call: (node.type === 'CallExpression' ? CALL_EXPRESSION : NEW_EXPRESSION) | 0 + }); + for (let arg of args) { + context.visit(arg); + } + if (analysis.pure || analysis.never_throws) { + // don't check unless we think the function is pure or error-free + if (callee.type === 'Identifier') { + const binding = context.state.scope.get(callee.name); + if ( + binding && + binding !== fn_binding && + !stack.has(binding) && + is_valid_function_binding(binding) && + node.type === 'CallExpression' + ) { + const child_analysis = evaluate_function( + /** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ ( + binding.initial + ), + binding, + new Set([...stack, fn_binding]) + ); + analysis.pure &&= child_analysis.pure; + analysis.never_throws &&= child_analysis.never_throws; + } + } else if ( + node.type === 'CallExpression' && + callee !== fn && + (callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') && + [...stack].every(({ scope }) => scope !== callee.metadata.scope) + ) { + const child_analysis = evaluate_function( + callee, + /** @type {Binding} */ ({ scope: callee.metadata.scope }), + new Set([...stack, fn_binding]) + ); + analysis.pure &&= child_analysis.pure; + analysis.never_throws &&= child_analysis.never_throws; + } + } + } + walk(/** @type {AST.SvelteNode} */ (fn), state, { + MemberExpression(node, context) { + const keypath = get_global_keypath(node, context.state.scope); + const evaluated = context.state.scope.evaluate(node); + if (keypath === null && !evaluated.is_known) { + analysis.pure = false; + analysis.never_throws = false; + } + 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)) { + analysis.pure = false; + } + return; + } + if ( + binding.scope !== fn_scope && + !binding.updated && + context.state.current_call === 0 && + !seen_bindings.includes(binding) + ) { + let has_fn_scope = false; + /** @type {null | Scope} */ + let curr = binding.scope; + while (curr !== null) { + curr = curr?.parent ?? null; + if (fn_scope === curr) { + has_fn_scope = true; + break; + } + } + if (!has_fn_scope) { + analysis.pure = false; + } + seen_bindings.push(binding); + } + if (binding.kind === 'derived') { + analysis.never_throws = false; //derived evaluation could throw + } + } + } + context.next(); + }, + CallExpression: handle_call_expression, + NewExpression: handle_call_expression, + ThrowStatement(node, context) { + if ( + fn.type !== 'FunctionDeclaration' || + context.path.findLast((parent) => parent.type === 'FunctionDeclaration') === fn // FunctionDeclarations are separately declared functions; we treat other types of functions as functions that could be evaluated by the parent + ) { + analysis.never_throws = false; + } + context.next(); + }, + ReturnStatement(node, context) { + if ( + !uses_implicit_return && + context.path.findLast((parent) => + ['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression'].includes( + parent.type + ) + ) === fn + ) { + if (node.argument) { + const argument = /** @type {Expression} */ (context.visit(node.argument)); + context.state.scope.evaluate(argument, analysis.values, seen_bindings); + } else { + analysis.values.add(undefined); + } + } + }, + _(node, context) { + const new_scope = + node.type === 'FunctionDeclaration' || + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' + ? node.metadata.scope + : binding.scope.root.scopes.get(node); + if ( + new_scope && + context.state.scope !== new_scope && + (node.type !== 'FunctionDeclaration' || node === fn) + ) { + context.next({ + scope: new_scope, + scope_path: [...context.state.scope_path, new_scope], + current_call: context.state.current_call + }); + } else { + context.next(); + } + } + }); + if (uses_implicit_return) { + fn_scope.evaluate(/** @type {Expression} */ (fn.body), analysis.values, seen_bindings); + } + 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.values.size === 0 + ) { + analysis.is_known = true; + } + fn_cache.set(fn, analysis); + return analysis; +} + +/** + * @param {Binding} binding + * @returns {boolean} + */ +function is_valid_function_binding(binding) { + return ( + (binding.kind === 'normal' && + !binding.reassigned && + binding.initial?.type === 'ArrowFunctionExpression') || + binding.initial?.type === 'FunctionDeclaration' || + (binding.initial?.type === 'FunctionExpression' && + (binding.declaration_kind === 'function' || + binding.declaration_kind === 'const' || + binding.declaration_kind === 'let' || + binding.declaration_kind === 'var')) + ); +} 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 index 3431e36833b5..90418e626797 100644 --- 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 @@ -6,7 +6,7 @@ export default function Each_index_non_null($$payload) { $$payload.out += ``; for (let i = 0, $$length = each_array.length; i < $$length; i++) { - $$payload.out += `

index: ${$.escape(i)}

`; + $$payload.out += `

index: ${i}

`; } $$payload.out += ``; From bb95f5a2fb30b208f9f53fde8b052b5365e60dbf Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Apr 2025 02:16:01 -0700 Subject: [PATCH 14/19] lint fixes --- packages/svelte/src/compiler/phases/scope.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index d7bfa52eb545..534ab9d55dbb 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -616,7 +616,7 @@ class Evaluation { ); if (fn && fn.async === false && !fn?.generator) { const analysis = evaluate_function(fn, binding); // typescript won't tell you if a function is pure or if it could throw, so we have to do this regardless of type annotations - console.log({ fn, binding, analysis }); + // console.log({ fn, binding, analysis }); if (!analysis.pure || !analysis.never_throws) { // if its not pure, or we don't know if it could throw, we can't use any constant return values from the evaluation, but we can check if its nullish this.values.add(NOT_NULL); // `NOT_NULL` doesn't have precedence over `UNKNOWN`, so if the value is nullish, this won't have precedence @@ -1718,7 +1718,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = * - While currently all the globals we have are pure and error-free, that could change, so we shouldn't be too dependent on that in the future. * Things like `JSON.stringify` and a *lot* of array methods are prime examples. */ - 1; + let thing; const analysis = { pure: true, is_known: false, From a58d2df453e30ffa64cde53b09088bb963fc35d5 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Apr 2025 02:19:36 -0700 Subject: [PATCH 15/19] lint --- packages/svelte/src/compiler/phases/scope.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 534ab9d55dbb..3e3d6d1ab2a0 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1698,7 +1698,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = /** * This big blob of comments is for my (https://github.com/Ocean-OS) sanity and for that of anyone who tries working with this function. Feel free to modify this as the function evolves. * So, when evaluating functions at compile-time, there are a few things you have to avoid evaluating: - * + * * - Side effects * A function that modifies state from outside of its scope should not be evaluated. * Additionally, since `$effect`s and `$derived`s exist, any reference to an external value could lead to a missed dependency if the function is evaluated by the compiler. @@ -1706,17 +1706,17 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = * A function that could throw an error should not be evaluated. Additionally, `$derived`s could be reevaluated upon reading, which could throw an error. * The purpose of a compile-time evaluator is to replicate the behavior the function would have at runtime, but in compile time. * If an error is/could be thrown, that can not be replicated. - * + * * So, how do we figure out if either of these things (could) happen in a function? * Well, for errors, it's relatively simple. If a `throw` statement is used in the function, then we assume that the error could be thrown at any time. * For side effects, it gets a bit tricky. External `Identifier`s that change their value are definitely side effects, but also any `MemberExpression` that isn't a known global constant could have a side effect, due to getters and `Proxy`s. * Additionally, since a function can call other functions, we check each individual function call: if it's a known global, we know its pure, and if we can find its definition, the parent function inherits its throwability and purity. If we cannot find its definition, we assume it is impure and could throw. - * + * * A few other things to note/remember: * - Not all functions rely on return statements to determine the return value. * Arrow functions without a `BlockStatement` for a body use their expression body as an implicit `ReturnStatement`. * - While currently all the globals we have are pure and error-free, that could change, so we shouldn't be too dependent on that in the future. - * Things like `JSON.stringify` and a *lot* of array methods are prime examples. + * Things like `JSON.stringify` and a *lot* of array methods are prime examples. */ let thing; const analysis = { @@ -1811,7 +1811,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = } if ( binding.scope !== fn_scope && - !binding.updated && + !binding.updated && context.state.current_call === 0 && !seen_bindings.includes(binding) ) { From 9d934f4d73e07dfa7cabb9cb2eb4c5d23848a274 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Apr 2025 13:24:37 -0700 Subject: [PATCH 16/19] try fixing ident logic, fix failing tests, `String.prototype.length` --- packages/svelte/src/compiler/phases/scope.js | 151 ++++++++++++------ .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- 3 files changed, 101 insertions(+), 54 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 3e3d6d1ab2a0..90ef12a28e24 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -27,7 +27,7 @@ const NOT_NULL = Symbol('not null'); const TYPES = [NUMBER, STRING, UNKNOWN, NOT_NULL, undefined, true, false]; /** @type {Record} */ const globals = { - BigInt: [NUMBER, BigInt], + BigInt: [NUMBER], // `BigInt` throws when a decimal is passed to it 'Date.now': [NUMBER], 'Math.min': [NUMBER, Math.min], 'Math.max': [NUMBER, Math.max], @@ -267,7 +267,7 @@ class Evaluation { * @param {Set} values * @param {Binding[]} seen_bindings */ - constructor(scope, expression, values, seen_bindings) { + constructor(scope, expression, values, seen_bindings, from_fn_call = false) { this.values = values; switch (expression.type) { @@ -300,10 +300,12 @@ class Evaluation { } if (!binding.updated && binding.initial !== null && !is_prop) { - binding.scope.evaluate(/** @type {Expression} */ (binding.initial), this.values, [ - ...seen_bindings, - binding - ]); + binding.scope.evaluate( + /** @type {Expression} */ (binding.initial), + this.values, + [...seen_bindings, binding], + from_fn_call + ); break; } @@ -327,9 +329,10 @@ class Evaluation { const a = scope.evaluate( /** @type {Expression} */ (expression.left), new Set(), - seen_bindings + seen_bindings, + from_fn_call ); // `left` cannot be `PrivateIdentifier` unless operator is `in` - const b = scope.evaluate(expression.right, new Set(), seen_bindings); + const b = scope.evaluate(expression.right, new Set(), seen_bindings, from_fn_call); if (a.is_known && b.is_known) { this.values.add(binary[expression.operator](a.value, b.value)); @@ -383,9 +386,19 @@ class Evaluation { } case 'ConditionalExpression': { - const test = scope.evaluate(expression.test, new Set(), seen_bindings); - const consequent = scope.evaluate(expression.consequent, new Set(), seen_bindings); - const alternate = scope.evaluate(expression.alternate, new Set(), seen_bindings); + const test = scope.evaluate(expression.test, new Set(), seen_bindings, from_fn_call); + const consequent = scope.evaluate( + expression.consequent, + new Set(), + seen_bindings, + from_fn_call + ); + const alternate = scope.evaluate( + expression.alternate, + new Set(), + seen_bindings, + from_fn_call + ); if (test.is_known) { for (const value of (test.value ? consequent : alternate).values) { @@ -404,8 +417,8 @@ class Evaluation { } case 'LogicalExpression': { - const a = scope.evaluate(expression.left, new Set(), seen_bindings); - const b = scope.evaluate(expression.right, new Set(), seen_bindings); + const a = scope.evaluate(expression.left, new Set(), seen_bindings, from_fn_call); + const b = scope.evaluate(expression.right, new Set(), seen_bindings, from_fn_call); if (a.is_known) { if (b.is_known) { @@ -439,7 +452,12 @@ class Evaluation { } case 'UnaryExpression': { - const argument = scope.evaluate(expression.argument, new Set(), seen_bindings); + const argument = scope.evaluate( + expression.argument, + new Set(), + seen_bindings, + from_fn_call + ); if (argument.is_known) { this.values.add(unary[expression.operator](argument.value)); @@ -476,7 +494,7 @@ class Evaluation { case 'SequenceExpression': { const { expressions } = expression; const evaluated = expressions.map((expression) => - scope.evaluate(expression, new Set(), seen_bindings) + scope.evaluate(expression, new Set(), seen_bindings, from_fn_call) ); if (evaluated.every((ev) => ev.is_known)) { this.values.add(evaluated.at(-1)?.value); @@ -498,7 +516,7 @@ class Evaluation { case '$state.raw': case '$derived': if (arg) { - scope.evaluate(arg, this.values); + scope.evaluate(arg, this.values, seen_bindings, from_fn_call); } else { this.values.add(undefined); } @@ -514,7 +532,12 @@ class Evaluation { break; case '$derived.by': - scope.evaluate(b.call(/** @type {Expression} */ (arg)), this.values, seen_bindings); + scope.evaluate( + b.call(/** @type {Expression} */ (arg)), + this.values, + seen_bindings, + from_fn_call + ); break; case '$effect.root': @@ -535,7 +558,7 @@ class Evaluation { ) { const [type, fn] = globals[keypath]; const values = expression.arguments.map((arg) => - scope.evaluate(arg, new Set(), seen_bindings) + scope.evaluate(arg, new Set(), seen_bindings, from_fn_call) ); if (fn && values.every((e) => e.is_known)) { @@ -557,7 +580,12 @@ class Evaluation { expression.callee.object.type !== 'Super' && expression.arguments.every((arg) => arg.type !== 'SpreadElement') ) { - const object = scope.evaluate(expression.callee.object, new Set(), seen_bindings); + const object = scope.evaluate( + expression.callee.object, + new Set(), + seen_bindings, + from_fn_call + ); if (!object.is_known) { this.values.add(UNKNOWN); break; @@ -567,7 +595,12 @@ class Evaluation { expression.callee.computed && expression.callee.property.type !== 'PrivateIdentifier' ) { - property = scope.evaluate(expression.callee.property, new Set(), seen_bindings); + property = scope.evaluate( + expression.callee.property, + new Set(), + seen_bindings, + from_fn_call + ); if (property.is_known) { property = property.value; } else { @@ -590,7 +623,7 @@ class Evaluation { if (Object.hasOwn(available_methods, property)) { const [type, fn] = available_methods[property]; const values = expression.arguments.map((arg) => - scope.evaluate(arg, new Set(), seen_bindings) + scope.evaluate(arg, new Set(), seen_bindings, from_fn_call) ); if (fn && values.every((e) => e.is_known)) { @@ -609,7 +642,7 @@ class Evaluation { } else if (expression.callee.type === 'Identifier' && !expression.arguments.length) { const binding = scope.get(expression.callee.name); if (binding) { - if (is_valid_function_binding(binding)) { + if (binding.is_function()) { const fn = /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ ( binding.initial @@ -663,7 +696,12 @@ class Evaluation { const fn = expression.callee; const binding = /** @type {Binding} */ ({ scope }); if (fn && fn.async === false && !fn?.generator) { - const analysis = evaluate_function(fn, binding); + const analysis = evaluate_function( + fn, + binding, + new Set(), + from_fn_call ? seen_bindings : [] + ); if (!analysis.pure || !analysis.never_throws) { this.values.add(NOT_NULL); } @@ -733,6 +771,35 @@ class Evaluation { } else if (keypath?.match?.(/\.name$/) && Object.hasOwn(globals, keypath.slice(0, -5))) { this.values.add(globals[keypath.slice(0, -5)]?.[1]?.name ?? STRING); break; + } else if ( + expression.object.type !== 'Super' && + expression.property.type !== 'PrivateIdentifier' + ) { + const object = scope.evaluate(expression.object, new Set(), seen_bindings, from_fn_call); + if (object.is_string) { + let property; + if (expression.computed) { + let prop = scope.evaluate( + expression.property, + new Set(), + seen_bindings, + from_fn_call + ); + if (prop.is_known && prop.value === 'length') { + property = 'length'; + } + } else if (expression.property.type === 'Identifier') { + property = expression.property.name; + } + if (property === 'length') { + if (object.is_known) { + this.values.add(object.value.length); + } else { + this.values.add(NUMBER); + } + break; + } + } } this.values.add(UNKNOWN); @@ -954,8 +1021,8 @@ export class Scope { * @param {Set} [values] * @param {Binding[]} [seen_bindings] */ - evaluate(expression, values = new Set(), seen_bindings = []) { - return new Evaluation(this, expression, values, seen_bindings); + evaluate(expression, values = new Set(), seen_bindings = [], from_fn_call = false) { + return new Evaluation(this, expression, values, seen_bindings, from_fn_call); } } @@ -1704,7 +1771,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = * Additionally, since `$effect`s and `$derived`s exist, any reference to an external value could lead to a missed dependency if the function is evaluated by the compiler. * - Errors * A function that could throw an error should not be evaluated. Additionally, `$derived`s could be reevaluated upon reading, which could throw an error. - * The purpose of a compile-time evaluator is to replicate the behavior the function would have at runtime, but in compile time. + * The purpose of a compile-time evaluator is to replicate the behavior the function would have at runtime, without actually running the function. * If an error is/could be thrown, that can not be replicated. * * So, how do we figure out if either of these things (could) happen in a function? @@ -1760,13 +1827,11 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = binding && binding !== fn_binding && !stack.has(binding) && - is_valid_function_binding(binding) && + binding.is_function() && node.type === 'CallExpression' ) { const child_analysis = evaluate_function( - /** @type {ArrowFunctionExpression | FunctionDeclaration | FunctionExpression} */ ( - binding.initial - ), + binding.initial, binding, new Set([...stack, fn_binding]) ); @@ -1792,7 +1857,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = walk(/** @type {AST.SvelteNode} */ (fn), state, { MemberExpression(node, context) { const keypath = get_global_keypath(node, context.state.scope); - const evaluated = context.state.scope.evaluate(node); + const evaluated = context.state.scope.evaluate(node, new Set(), seen_bindings, true); if (keypath === null && !evaluated.is_known) { analysis.pure = false; analysis.never_throws = false; @@ -1811,7 +1876,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = } if ( binding.scope !== fn_scope && - !binding.updated && + binding.updated && context.state.current_call === 0 && !seen_bindings.includes(binding) ) { @@ -1859,7 +1924,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = ) { if (node.argument) { const argument = /** @type {Expression} */ (context.visit(node.argument)); - context.state.scope.evaluate(argument, analysis.values, seen_bindings); + context.state.scope.evaluate(argument, analysis.values, seen_bindings, true); } else { analysis.values.add(undefined); } @@ -1888,7 +1953,7 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = } }); if (uses_implicit_return) { - fn_scope.evaluate(/** @type {Expression} */ (fn.body), analysis.values, seen_bindings); + fn_scope.evaluate(/** @type {Expression} */ (fn.body), analysis.values, seen_bindings, true); } for (const value of analysis.values) { analysis.value = value; // saves having special logic for `size === 1` @@ -1907,21 +1972,3 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = fn_cache.set(fn, analysis); return analysis; } - -/** - * @param {Binding} binding - * @returns {boolean} - */ -function is_valid_function_binding(binding) { - return ( - (binding.kind === 'normal' && - !binding.reassigned && - binding.initial?.type === 'ArrowFunctionExpression') || - binding.initial?.type === 'FunctionDeclaration' || - (binding.initial?.type === 'FunctionExpression' && - (binding.declaration_kind === 'function' || - binding.declaration_kind === 'const' || - binding.declaration_kind === 'let' || - binding.declaration_kind === 'var')) - ); -} diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js index d520d1ef2488..b42be84c4da0 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client/index.svelte.js @@ -18,7 +18,7 @@ export default function Text_nodes_deriveds($$anchor) { var p = root(); var text = $.child(p); + text.nodeValue = '00'; $.reset(p); - $.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]); $.append($$anchor, p); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js index 6f019647f58b..f53007638d92 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js @@ -12,5 +12,5 @@ export default function Text_nodes_deriveds($$payload) { return count2; } - $$payload.out += `

${$.escape(text1())}${$.escape(text2())}

`; + $$payload.out += `

00

`; } \ No newline at end of file From c7f21e1f93021f599f0a9790305ae4f6610e00f9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Apr 2025 19:45:21 -0700 Subject: [PATCH 17/19] maybe fix purity issues, cleanup code --- packages/svelte/src/compiler/phases/scope.js | 95 +++++++++++++------- 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 90ef12a28e24..35b0d876bd5c 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1024,6 +1024,23 @@ export class Scope { evaluate(expression, values = new Set(), seen_bindings = [], from_fn_call = false) { return new Evaluation(this, expression, values, seen_bindings, from_fn_call); } + + /** + * @param {Scope} child + */ + contains(child) { + let contains = false; + /** @type {Scope | null} */ + let curr = child; + while (curr?.parent != null) { + curr = curr?.parent; + if (curr === this) { + contains = true; + break; + } + } + return contains; + } } /** @type {Record any>} */ @@ -1646,7 +1663,12 @@ function get_type_of_ts_node(node, scope) { * @returns {any[]} */ function intersect_types(types) { - if (types.includes(UNKNOWN)) return [UNKNOWN]; + if ( + types.includes(UNKNOWN) || + types.filter((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type)) + .length > 1 + ) + return [UNKNOWN]; /** @type {any[]} */ let res = []; if ( @@ -1666,14 +1688,9 @@ function get_type_of_ts_node(node, scope) { } else { res.push(...types.filter((type) => typeof type === 'string')); } - if ( - types.filter((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type)) - .length > 1 - ) { - res.push(UNKNOWN); - } else { + if (types.some((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type))) { types.push( - ...types.filter((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type)) + types.find((type) => !['symbol', 'string', 'number', 'bigint'].includes(typeof type)) ); } return res; @@ -1751,6 +1768,19 @@ const known_globals = [ let fn_cache = new Map(); +/** + * @param {Expression} callee + * @param {Scope} scope + */ +function is_global_class(callee, scope) { + let keypath = get_global_keypath(callee, scope); + if (keypath === null) return false; + if (keypath.match(/^(globalThis\.)+/)) { + keypath = keypath.replace(/^(globalThis\.)+/, ''); + } + return global_classes.includes(keypath); +} + /** * Analyzes and partially evaluates the provided function. * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} fn @@ -1806,21 +1836,31 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = }; const uses_implicit_return = fn.type === 'ArrowFunctionExpression' && fn.body.type !== 'BlockStatement'; + function needs_check(purity = true, throwability = true) { + if (!throwability) { + return analysis.pure; + } + if (!purity) { + return analysis.never_throws; + } + return analysis.pure || analysis.never_throws; + } /** * @param {CallExpression | NewExpression} node * @param {import('zimmerframe').Context} context */ function handle_call_expression(node, context) { const { callee: call, arguments: args } = node; - const callee = context.visit(call, { - ...context.state, - current_call: (node.type === 'CallExpression' ? CALL_EXPRESSION : NEW_EXPRESSION) | 0 - }); + const callee = /** @type {Expression} */ ( + context.visit(call, { + ...context.state, + current_call: (node.type === 'CallExpression' ? CALL_EXPRESSION : NEW_EXPRESSION) | 0 + }) + ); for (let arg of args) { context.visit(arg); } - if (analysis.pure || analysis.never_throws) { - // don't check unless we think the function is pure or error-free + if (needs_check()) { if (callee.type === 'Identifier') { const binding = context.state.scope.get(callee.name); if ( @@ -1851,6 +1891,9 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = ); analysis.pure &&= child_analysis.pure; analysis.never_throws &&= child_analysis.never_throws; + } else if (node.type === 'NewExpression' && !is_global_class(callee, context.state.scope)) { + analysis.pure = false; + analysis.never_throws = false; } } } @@ -1858,14 +1901,14 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = MemberExpression(node, context) { const keypath = get_global_keypath(node, context.state.scope); const evaluated = context.state.scope.evaluate(node, new Set(), seen_bindings, true); - if (keypath === null && !evaluated.is_known) { + if (!(keypath !== null && Object.hasOwn(globals, keypath)) && !evaluated.is_known) { analysis.pure = false; analysis.never_throws = false; } context.next(); }, Identifier(node, context) { - if (is_reference(node, /** @type {Node} */ (context.path.at(-1)))) { + if (is_reference(node, /** @type {Node} */ (context.path.at(-1))) && needs_check()) { const binding = context.state.scope.get(node.name); if (binding !== fn_binding) { if (binding === null) { @@ -1878,21 +1921,10 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = binding.scope !== fn_scope && binding.updated && context.state.current_call === 0 && - !seen_bindings.includes(binding) + !seen_bindings.includes(binding) && + needs_check(true, false) ) { - let has_fn_scope = false; - /** @type {null | Scope} */ - let curr = binding.scope; - while (curr !== null) { - curr = curr?.parent ?? null; - if (fn_scope === curr) { - has_fn_scope = true; - break; - } - } - if (!has_fn_scope) { - analysis.pure = false; - } + analysis.pure &&= fn_scope.contains(binding.scope); seen_bindings.push(binding); } if (binding.kind === 'derived') { @@ -1904,6 +1936,9 @@ function evaluate_function(fn, binding, stack = new Set(), [...seen_bindings] = }, CallExpression: handle_call_expression, NewExpression: handle_call_expression, + TaggedTemplateExpression(node, context) { + return handle_call_expression(b.call(node.tag, node.quasi), context); + }, ThrowStatement(node, context) { if ( fn.type !== 'FunctionDeclaration' || From 44c9c89bf33763812352fdc393958409f8b3b332 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 20 Apr 2025 19:15:55 -0700 Subject: [PATCH 18/19] start reassignments and if blocks, more ts annotations --- .../phases/1-parse/remove_typescript_nodes.js | 1 + .../client/visitors/IfBlock_unfinished.js | 82 ++ .../server/visitors/IfBlock_unfinished.js | 54 + .../src/compiler/phases/3-transform/utils.js | 13 +- packages/svelte/src/compiler/phases/scope.js | 1052 ++++++++++------- 5 files changed, 749 insertions(+), 453 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock_unfinished.js diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index 0defc8ba12ff..cac0f1de3880 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -43,6 +43,7 @@ const visitors = { delete n.typeParameters; delete n.typeArguments; delete n.returnType; + // TODO figure out what this is exactly, and if it should be added to `type_information` delete n.accessibility; }, Decorator(node) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js new file mode 100644 index 000000000000..ee98f8b1a97c --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js @@ -0,0 +1,82 @@ +/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '#compiler/builders'; + +/** + * @param {AST.IfBlock} node + * @param {ComponentContext} context + */ +export function IfBlock(node, context) { + const test = /** @type {Expression} */ (context.visit(node.test)); + const evaluated = context.state.scope.evaluate(test); + + const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + context.state.template.push(''); + if (evaluated.is_truthy) { + context.state.init.push(b.stmt(b.call(b.arrow([b.id('$$anchor')], consequent), context.state.node))); + } else { + const statements = []; + const consequent_id = context.state.scope.generate('consequent'); + statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent))); + + let alternate_id; + + if (node.alternate) { + alternate_id = context.state.scope.generate('alternate'); + const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); + const nodes = node.alternate.nodes; + + let alternate_args = [b.id('$$anchor')]; + if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) { + alternate_args.push(b.id('$$elseif')); + } + + statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); + } + + /** @type {Expression[]} */ + const args = [ + node.elseif ? b.id('$$anchor') : context.state.node, + b.arrow( + [b.id('$$render')], + b.block([ + b.if( + test, + b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), + alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined + ) + ]) + ) + ]; + + if (node.elseif) { + // We treat this... + // + // {#if x} + // ... + // {:else} + // {#if y} + //
...
+ // {/if} + // {/if} + // + // ...slightly differently to this... + // + // {#if x} + // ... + // {:else if y} + //
...
+ // {/if} + // + // ...even though they're logically equivalent. In the first case, the + // transition will only play when `y` changes, but in the second it + // should play when `x` or `y` change — both are considered 'local' + args.push(b.id('$$elseif')); + } + + statements.push(b.stmt(b.call('$.if', ...args))); + + context.state.init.push(b.block(statements)); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock_unfinished.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock_unfinished.js new file mode 100644 index 000000000000..9a9aefd85759 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock_unfinished.js @@ -0,0 +1,54 @@ +/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { AST } from '#compiler' */ +/** @import { ComponentContext } from '../types.js' */ +import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js'; +import * as b from '#compiler/builders'; +import { block_close, block_open } from './shared/utils.js'; +import { needs_new_scope } from '../../utils.js'; + +/** + * @param {AST.IfBlock} node + * @param {ComponentContext} context + */ +export function IfBlock(node, context) { + const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + const test = /** @type {Expression} */ (context.visit(node.test)); + const evaluated = context.state.scope.evaluate(test); + if (evaluated.is_truthy) { + if (needs_new_scope(consequent)) { + context.state.template.push(consequent); + } else { + context.state.template.push(...consequent.body); + } + } else { + consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let if_statement = b.if(test, consequent); + + context.state.template.push(if_statement, block_close); + + let index = 1; + let alt = node.alternate; + while ( + alt && + alt.nodes.length === 1 && + alt.nodes[0].type === 'IfBlock' && + alt.nodes[0].elseif + ) { + const elseif = alt.nodes[0]; + const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent)); + alternate.body.unshift( + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(``))) + ); + if_statement = if_statement.alternate = b.if( + /** @type {Expression} */ (context.visit(elseif.test)), + alternate + ); + alt = elseif.alternate; + } + + if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]); + if_statement.alternate.body.unshift( + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE))) + ); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 5aa40c8abb5c..43ec4558462e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -1,7 +1,7 @@ /** @import { Context } from 'zimmerframe' */ /** @import { TransformState } from './types.js' */ /** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */ -/** @import { Node, Expression, CallExpression } from 'estree' */ +/** @import { Node, Expression, CallExpression, BlockStatement } from 'estree' */ import { regex_ends_with_whitespaces, regex_not_whitespace, @@ -486,3 +486,14 @@ export function transform_inspect_rune(node, context) { return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg)); } } + +/** + * Whether a `BlockStatement` needs to be a block statement as opposed to just inlining all of its statements. + * @param {BlockStatement} block + */ +export function needs_new_scope(block) { + const has_vars = block.body.some(child => child.type === 'VariableDeclaration'); + const has_fns = block.body.some(child => child.type === 'FunctionDeclaration'); + const has_class = block.body.some(child => child.type === 'ClassDeclaration'); + return has_vars || has_fns || has_class; +} \ No newline at end of file diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 35b0d876bd5c..f4771c2259de 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, CallExpression, NewExpression } from 'estree' */ +/** @import { ArrowFunctionExpression, BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, CallExpression, NewExpression, AssignmentExpression, UpdateExpression } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -151,6 +151,12 @@ export class Binding { /** @type {DeclarationKind} */ declaration_kind; + /** + * Any nodes that may have updated the value of the binding + * @type {AST.SvelteNode[]} + */ + updates = []; + /** * What the value was initialized with. * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` @@ -158,7 +164,7 @@ export class Binding { */ initial = null; - /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ + /** @type {Array<{ node: Identifier; path: AST.SvelteNode[], scope: Scope }>} */ references = []; /** @@ -254,6 +260,20 @@ class Evaluation { */ is_number = true; + /** + * True if the value is known to be truthy + * @readonly + * @type {boolean} + */ + is_truthy = true; + + /** + * True if the value is known to be falsy + * @readonly + * @type {boolean} + */ + is_falsy = true; + /** * @readonly * @type {any} @@ -269,405 +289,517 @@ class Evaluation { */ constructor(scope, expression, values, seen_bindings, from_fn_call = false) { this.values = values; + try { + switch (expression.type) { + case 'Literal': { + this.values.add(expression.value); + break; + } - switch (expression.type) { - case 'Literal': { - this.values.add(expression.value); - break; - } + case 'Identifier': { + const binding = scope.get(expression.name); - case 'Identifier': { - const binding = scope.get(expression.name); + if (binding && seen_bindings.includes(binding)) break; + if (binding) { + if ( + binding.initial?.type === 'CallExpression' && + get_rune(binding.initial, scope) === '$props.id' + ) { + this.values.add(STRING); + break; + } - if (binding && seen_bindings.includes(binding)) break; - if (binding) { - if ( - binding.initial?.type === 'CallExpression' && - get_rune(binding.initial, scope) === '$props.id' - ) { - this.values.add(STRING); - break; - } + const is_prop = + binding.kind === 'prop' || + binding.kind === 'rest_prop' || + binding.kind === 'bindable_prop'; + + if ( + binding.initial?.type === 'EachBlock' && + binding.initial.index === expression.name + ) { + this.values.add(NUMBER); + break; + } - const is_prop = - binding.kind === 'prop' || - binding.kind === 'rest_prop' || - binding.kind === 'bindable_prop'; + if (!binding.updated && binding.initial !== null && !is_prop) { + binding.scope.evaluate( + /** @type {Expression} */ (binding.initial), + this.values, + [...seen_bindings, binding], + from_fn_call + ); + break; + } - if (binding.initial?.type === 'EachBlock' && binding.initial.index === expression.name) { - this.values.add(NUMBER); + if (binding.kind === 'rest_prop' && !binding.updated) { + this.values.add(NOT_NULL); + break; + } + + if ( + Object.hasOwn(binding.node, 'type_information') && + //@ts-expect-error + Object.keys(binding.node?.type_information).includes('annotation') + ) { + //@ts-ignore todo add this to types + const { type_information } = binding.node; + if (type_information.annotation?.type_information?.annotation) { + const type_annotation = get_type_of_ts_node( + type_information?.annotation?.type_information?.annotation + ); + if (Array.isArray(type_annotation)) { + for (let type of type_annotation) { + this.values.add(type); + } + } else { + this.values.add(type_annotation); + } + } + if ( + !( + binding.updated && + !is_prop && + binding.kind !== 'snippet' && + binding.kind !== 'template' && + binding.declaration_kind !== 'param' && + binding.declaration_kind !== 'rest_param' + ) + ) + break; + } + if ( + binding.updated && + !is_prop && + binding.kind !== 'snippet' && + binding.kind !== 'template' && + binding.declaration_kind !== 'param' && + binding.declaration_kind !== 'rest_param' + ) { + if (binding.initial !== null) { + binding.scope.evaluate( + /** @type {Expression} */ (binding.initial), + this.values, + [...seen_bindings, binding], + from_fn_call + ); + } + for (const update of binding.updates) { + switch (update.type) { + case 'AssignmentExpression': + if (binding.references.find(({ node }) => update.left === node)) { + const { scope } = /** @type {Binding['references'][number]} */ ( + binding.references.find(({ node }) => update.left === node) + ); + switch (update.operator) { + case '=': + case '??=': + case '||=': + case '&&=': + scope.evaluate(update.right, this.values, seen_bindings, from_fn_call); + break; + case '+=': { + this.values.add(NUMBER); + this.values.add(STRING); + break; + } + case '-=': + case '*=': + case '/=': + case '%=': + case '**=': + case '<<=': + case '>>=': + case '>>>=': + case '|=': + case '^=': + case '&=': + this.values.add(NUMBER); + break; + default: { + this.values.add(UNKNOWN); + } + } + } else { + this.values.add(UNKNOWN); + } + break; + case 'UpdateExpression': { + if (binding.references.find(({ node }) => update.argument === node)) { + this.values.add(NUMBER); + } else { + this.values.add(UNKNOWN); + } + break; + } + default: { + this.values.add(UNKNOWN); + } + } + } + break; + } + } else if (expression.name === 'undefined') { + this.values.add(undefined); break; } - if (!binding.updated && binding.initial !== null && !is_prop) { - binding.scope.evaluate( - /** @type {Expression} */ (binding.initial), - this.values, - [...seen_bindings, binding], - from_fn_call - ); + // TODO one day, expose props and imports somehow + + this.values.add(UNKNOWN); + break; + } + + case 'BinaryExpression': { + const a = scope.evaluate( + /** @type {Expression} */ (expression.left), + new Set(), + seen_bindings, + from_fn_call + ); // `left` cannot be `PrivateIdentifier` unless operator is `in` + const b = scope.evaluate(expression.right, new Set(), seen_bindings, from_fn_call); + + if (a.is_known && b.is_known) { + this.values.add(binary[expression.operator](a.value, b.value)); break; } - if (binding.kind === 'rest_prop' && !binding.updated) { - this.values.add(NOT_NULL); - break; + switch (expression.operator) { + case '!=': + case '!==': + case '<': + case '<=': + case '>': + case '>=': + case '==': + case '===': + case 'in': + case 'instanceof': + this.values.add(true); + this.values.add(false); + break; + + case '%': + case '&': + case '*': + case '**': + case '-': + case '/': + case '<<': + case '>>': + case '>>>': + case '^': + case '|': + this.values.add(NUMBER); + break; + + case '+': + if (a.is_string || b.is_string) { + this.values.add(STRING); + } else if (a.is_number && b.is_number) { + this.values.add(NUMBER); + } else { + this.values.add(STRING); + this.values.add(NUMBER); + } + break; + + default: + this.values.add(UNKNOWN); } - } else if (expression.name === 'undefined') { - this.values.add(undefined); break; } - // TODO glean what we can from reassignments - // TODO one day, expose props and imports somehow + case 'ConditionalExpression': { + const test = scope.evaluate(expression.test, new Set(), seen_bindings, from_fn_call); + const consequent = scope.evaluate( + expression.consequent, + new Set(), + seen_bindings, + from_fn_call + ); + const alternate = scope.evaluate( + expression.alternate, + new Set(), + seen_bindings, + from_fn_call + ); - this.values.add(UNKNOWN); - break; - } + if (test.is_known) { + for (const value of (test.value ? consequent : alternate).values) { + this.values.add(value); + } + } else { + for (const value of consequent.values) { + this.values.add(value); + } - case 'BinaryExpression': { - const a = scope.evaluate( - /** @type {Expression} */ (expression.left), - new Set(), - seen_bindings, - from_fn_call - ); // `left` cannot be `PrivateIdentifier` unless operator is `in` - const b = scope.evaluate(expression.right, new Set(), seen_bindings, from_fn_call); - - if (a.is_known && b.is_known) { - this.values.add(binary[expression.operator](a.value, b.value)); + for (const value of alternate.values) { + this.values.add(value); + } + } break; } - switch (expression.operator) { - case '!=': - case '!==': - case '<': - case '<=': - case '>': - case '>=': - case '==': - case '===': - case 'in': - case 'instanceof': - this.values.add(true); - this.values.add(false); - break; + case 'LogicalExpression': { + const a = scope.evaluate(expression.left, new Set(), seen_bindings, from_fn_call); + const b = scope.evaluate(expression.right, new Set(), seen_bindings, from_fn_call); - case '%': - case '&': - case '*': - case '**': - case '-': - case '/': - case '<<': - case '>>': - case '>>>': - case '^': - case '|': - this.values.add(NUMBER); - break; + if (a.is_known) { + if (b.is_known) { + this.values.add(logical[expression.operator](a.value, b.value)); + break; + } - case '+': - if (a.is_string || b.is_string) { - this.values.add(STRING); - } else if (a.is_number && b.is_number) { - this.values.add(NUMBER); + if ( + (expression.operator === '&&' && !a.value) || + (expression.operator === '||' && a.value) || + (expression.operator === '??' && a.value != null) + ) { + this.values.add(a.value); } else { - this.values.add(STRING); - this.values.add(NUMBER); + for (const value of b.values) { + this.values.add(value); + } } - break; - - default: - this.values.add(UNKNOWN); - } - break; - } - - case 'ConditionalExpression': { - const test = scope.evaluate(expression.test, new Set(), seen_bindings, from_fn_call); - const consequent = scope.evaluate( - expression.consequent, - new Set(), - seen_bindings, - from_fn_call - ); - const alternate = scope.evaluate( - expression.alternate, - new Set(), - seen_bindings, - from_fn_call - ); - if (test.is_known) { - for (const value of (test.value ? consequent : alternate).values) { - this.values.add(value); + break; } - } else { - for (const value of consequent.values) { + + for (const value of a.values) { this.values.add(value); } - for (const value of alternate.values) { + for (const value of b.values) { this.values.add(value); } + break; } - break; - } - case 'LogicalExpression': { - const a = scope.evaluate(expression.left, new Set(), seen_bindings, from_fn_call); - const b = scope.evaluate(expression.right, new Set(), seen_bindings, from_fn_call); + case 'UnaryExpression': { + const argument = scope.evaluate( + expression.argument, + new Set(), + seen_bindings, + from_fn_call + ); - if (a.is_known) { - if (b.is_known) { - this.values.add(logical[expression.operator](a.value, b.value)); + if (argument.is_known) { + this.values.add(unary[expression.operator](argument.value)); break; } - if ( - (expression.operator === '&&' && !a.value) || - (expression.operator === '||' && a.value) || - (expression.operator === '??' && a.value != null) - ) { - this.values.add(a.value); - } else { - for (const value of b.values) { - this.values.add(value); - } - } - - break; - } + switch (expression.operator) { + case '!': + case 'delete': + this.values.add(false); + this.values.add(true); + break; - for (const value of a.values) { - this.values.add(value); - } + case '+': + case '-': + case '~': + this.values.add(NUMBER); + break; - for (const value of b.values) { - this.values.add(value); - } - break; - } + case 'typeof': + this.values.add(STRING); + break; - case 'UnaryExpression': { - const argument = scope.evaluate( - expression.argument, - new Set(), - seen_bindings, - from_fn_call - ); + case 'void': + this.values.add(undefined); + break; - if (argument.is_known) { - this.values.add(unary[expression.operator](argument.value)); + default: + this.values.add(UNKNOWN); + } break; } - switch (expression.operator) { - case '!': - case 'delete': - this.values.add(false); - this.values.add(true); - break; - - case '+': - case '-': - case '~': - this.values.add(NUMBER); - break; - - case 'typeof': - this.values.add(STRING); - break; - - case 'void': - this.values.add(undefined); - break; - - default: + case 'SequenceExpression': { + const { expressions } = expression; + const evaluated = expressions.map((expression) => + scope.evaluate(expression, new Set(), seen_bindings, from_fn_call) + ); + if (evaluated.every((ev) => ev.is_known)) { + this.values.add(evaluated.at(-1)?.value); + } else { this.values.add(UNKNOWN); + } + break; } - break; - } - - case 'SequenceExpression': { - const { expressions } = expression; - const evaluated = expressions.map((expression) => - scope.evaluate(expression, new Set(), seen_bindings, from_fn_call) - ); - if (evaluated.every((ev) => ev.is_known)) { - this.values.add(evaluated.at(-1)?.value); - } else { - this.values.add(UNKNOWN); - } - break; - } - case 'CallExpression': { - const keypath = get_global_keypath(expression.callee, scope); + case 'CallExpression': { + const keypath = get_global_keypath(expression.callee, scope); - if (keypath !== null) { - if (is_rune(keypath)) { - const arg = /** @type {Expression | undefined} */ (expression.arguments[0]); + if (keypath !== null) { + 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, seen_bindings, from_fn_call); - } else { - this.values.add(undefined); - } - break; + switch (keypath) { + case '$state': + case '$state.raw': + case '$derived': + if (arg) { + scope.evaluate(arg, this.values, seen_bindings, from_fn_call); + } else { + this.values.add(undefined); + } + break; - case '$props.id': - this.values.add(STRING); - break; + case '$props.id': + this.values.add(STRING); + break; - case '$effect.tracking': - this.values.add(false); - this.values.add(true); - break; + case '$effect.tracking': + this.values.add(false); + this.values.add(true); + break; - case '$derived.by': - scope.evaluate( - b.call(/** @type {Expression} */ (arg)), - this.values, - seen_bindings, - from_fn_call - ); - break; + case '$derived.by': + scope.evaluate( + b.call(/** @type {Expression} */ (arg)), + this.values, + seen_bindings, + from_fn_call + ); + break; - case '$effect.root': - this.values.add(NOT_NULL); - break; + case '$effect.root': + this.values.add(NOT_NULL); + break; - default: { - this.values.add(UNKNOWN); + default: { + this.values.add(UNKNOWN); + } } - } - break; - } + 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, new Set(), seen_bindings, from_fn_call) - ); + 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, new Set(), seen_bindings, from_fn_call) + ); - if (fn && values.every((e) => e.is_known)) { - this.values.add(fn(...values.map((e) => e.value))); - } else { - if (Array.isArray(type)) { - for (const t of type) { - this.values.add(t); - } + if (fn && values.every((e) => e.is_known)) { + this.values.add(fn(...values.map((e) => e.value))); } else { - this.values.add(type); + if (Array.isArray(type)) { + for (const t of type) { + this.values.add(t); + } + } else { + this.values.add(type); + } } - } - break; - } - } else if ( - expression.callee.type === 'MemberExpression' && - expression.callee.object.type !== 'Super' && - expression.arguments.every((arg) => arg.type !== 'SpreadElement') - ) { - const object = scope.evaluate( - expression.callee.object, - new Set(), - seen_bindings, - from_fn_call - ); - if (!object.is_known) { - this.values.add(UNKNOWN); - break; - } - let property; - if ( - expression.callee.computed && - expression.callee.property.type !== 'PrivateIdentifier' + break; + } + } else if ( + expression.callee.type === 'MemberExpression' && + expression.callee.object.type !== 'Super' && + expression.arguments.every((arg) => arg.type !== 'SpreadElement') ) { - property = scope.evaluate( - expression.callee.property, + const object = scope.evaluate( + expression.callee.object, new Set(), seen_bindings, from_fn_call ); - if (property.is_known) { - property = property.value; - } else { + if (!object.is_known) { this.values.add(UNKNOWN); break; } - } else if (expression.callee.property.type === 'Identifier') { - property = expression.callee.property.name; - } - if (property === undefined) { - this.values.add(UNKNOWN); - break; - } - if (typeof object.value !== 'string' && typeof object.value !== 'number') { - this.values.add(UNKNOWN); - break; - } - const available_methods = - prototype_methods[/** @type {'string' | 'number'} */ (typeof object.value)]; - if (Object.hasOwn(available_methods, property)) { - const [type, fn] = available_methods[property]; - const values = expression.arguments.map((arg) => - scope.evaluate(arg, new Set(), seen_bindings, from_fn_call) - ); - - if (fn && values.every((e) => e.is_known)) { - this.values.add(fn(object.value, ...values.map((e) => e.value))); - } else { - if (Array.isArray(type)) { - for (const t of type) { - this.values.add(t); - } + let property; + if ( + expression.callee.computed && + expression.callee.property.type !== 'PrivateIdentifier' + ) { + property = scope.evaluate( + expression.callee.property, + new Set(), + seen_bindings, + from_fn_call + ); + if (property.is_known) { + property = property.value; } else { - this.values.add(type); + this.values.add(UNKNOWN); + break; } + } else if (expression.callee.property.type === 'Identifier') { + property = expression.callee.property.name; } - break; - } - } else if (expression.callee.type === 'Identifier' && !expression.arguments.length) { - const binding = scope.get(expression.callee.name); - if (binding) { - if (binding.is_function()) { - const fn = - /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ ( - binding.initial - ); - if (fn && fn.async === false && !fn?.generator) { - const analysis = evaluate_function(fn, binding); // typescript won't tell you if a function is pure or if it could throw, so we have to do this regardless of type annotations - // console.log({ fn, binding, analysis }); - if (!analysis.pure || !analysis.never_throws) { - // if its not pure, or we don't know if it could throw, we can't use any constant return values from the evaluation, but we can check if its nullish - this.values.add(NOT_NULL); // `NOT_NULL` doesn't have precedence over `UNKNOWN`, so if the value is nullish, this won't have precedence + if (property === undefined) { + this.values.add(UNKNOWN); + break; + } + if (typeof object.value !== 'string' && typeof object.value !== 'number') { + this.values.add(UNKNOWN); + break; + } + const available_methods = + prototype_methods[/** @type {'string' | 'number'} */ (typeof object.value)]; + if (Object.hasOwn(available_methods, property)) { + const [type, fn] = available_methods[property]; + const values = expression.arguments.map((arg) => + scope.evaluate(arg, new Set(), seen_bindings, from_fn_call) + ); + + if (fn && values.every((e) => e.is_known)) { + this.values.add(fn(object.value, ...values.map((e) => e.value))); + } else { + if (Array.isArray(type)) { + for (const t of type) { + this.values.add(t); + } + } else { + this.values.add(type); } - if (Object.hasOwn(fn, 'type_information')) { - // @ts-ignore - const { type_information } = fn; - if (Object.hasOwn(type_information, 'return')) { - const return_types = get_type_of_ts_node( - type_information.return?.type_information?.annotation, - scope - ); - if (Array.isArray(return_types)) { - for (let type of return_types) { - this.values.add(type); + } + break; + } + } else if (expression.callee.type === 'Identifier' && !expression.arguments.length) { + const binding = scope.get(expression.callee.name); + if (binding) { + if (binding.is_function()) { + const fn = + /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} */ ( + binding.initial + ); + if (fn && fn.async === false && !fn?.generator) { + const analysis = evaluate_function(fn, binding); // typescript won't tell you if a function is pure or if it could throw, so we have to do this regardless of type annotations + // console.log({ fn, binding, analysis }); + if (!analysis.pure || !analysis.never_throws) { + // if its not pure, or we don't know if it could throw, we can't use any constant return values from the evaluation, but we can check if its nullish + this.values.add(NOT_NULL); // `NOT_NULL` doesn't have precedence over `UNKNOWN`, so if the value is nullish, this won't have precedence + } + if (Object.hasOwn(fn, 'type_information')) { + // @ts-ignore + const { type_information } = fn; + if (Object.hasOwn(type_information, 'return')) { + const return_types = get_type_of_ts_node( + type_information.return?.type_information?.annotation + ); + if (Array.isArray(return_types)) { + for (let type of return_types) { + this.values.add(type); + } + } else { + this.values.add(return_types); } + } else if (analysis.is_known) { + this.values.add(analysis.value); + break; } else { - this.values.add(return_types); + for (let value of analysis.values) { + this.values.add(value); + } } } else if (analysis.is_known) { this.values.add(analysis.value); @@ -677,6 +809,40 @@ class Evaluation { this.values.add(value); } } + break; + } + } + } + } else if ( + expression.callee.type === 'ArrowFunctionExpression' || + expression.callee.type === 'FunctionExpression' + ) { + const fn = expression.callee; + const binding = /** @type {Binding} */ ({ scope }); + if (fn && fn.async === false && !fn?.generator) { + const analysis = evaluate_function( + fn, + binding, + new Set(), + from_fn_call ? seen_bindings : [] + ); + if (!analysis.pure || !analysis.never_throws) { + this.values.add(NOT_NULL); + } + if (Object.hasOwn(fn, 'type_information')) { + // @ts-ignore + const { type_information } = fn; + if (Object.hasOwn(type_information, 'return')) { + const return_types = get_type_of_ts_node( + type_information.return?.type_information?.annotation + ); + if (Array.isArray(return_types)) { + for (let type of return_types) { + this.values.add(type); + } + } else { + this.values.add(return_types); + } } else if (analysis.is_known) { this.values.add(analysis.value); break; @@ -685,41 +851,6 @@ class Evaluation { this.values.add(value); } } - break; - } - } - } - } else if ( - expression.callee.type === 'ArrowFunctionExpression' || - expression.callee.type === 'FunctionExpression' - ) { - const fn = expression.callee; - const binding = /** @type {Binding} */ ({ scope }); - if (fn && fn.async === false && !fn?.generator) { - const analysis = evaluate_function( - fn, - binding, - new Set(), - from_fn_call ? seen_bindings : [] - ); - if (!analysis.pure || !analysis.never_throws) { - this.values.add(NOT_NULL); - } - if (Object.hasOwn(fn, 'type_information')) { - // @ts-ignore - const { type_information } = fn; - if (Object.hasOwn(type_information, 'return')) { - const return_types = get_type_of_ts_node( - type_information.return?.type_information?.annotation, - scope - ); - if (Array.isArray(return_types)) { - for (let type of return_types) { - this.values.add(type); - } - } else { - this.values.add(return_types); - } } else if (analysis.is_known) { this.values.add(analysis.value); break; @@ -728,107 +859,122 @@ class Evaluation { this.values.add(value); } } - } else if (analysis.is_known) { - this.values.add(analysis.value); break; - } else { - for (let value of analysis.values) { - this.values.add(value); - } } - break; } - } - this.values.add(UNKNOWN); - break; - } + this.values.add(UNKNOWN); + break; + } - case 'TemplateLiteral': { - let result = expression.quasis[0].value.cooked; + case 'TemplateLiteral': { + let result = expression.quasis[0].value.cooked; - for (let i = 0; i < expression.expressions.length; i += 1) { - const e = scope.evaluate(expression.expressions[i], new Set(), seen_bindings); + for (let i = 0; i < expression.expressions.length; i += 1) { + const e = scope.evaluate(expression.expressions[i], new Set(), seen_bindings); - if (e.is_known) { - result += e.value + expression.quasis[i + 1].value.cooked; - } else { - this.values.add(STRING); - break; + if (e.is_known) { + result += e.value + expression.quasis[i + 1].value.cooked; + } else { + this.values.add(STRING); + break; + } } - } - this.values.add(result); - break; - } + this.values.add(result); + break; + } - case 'MemberExpression': { - const keypath = get_global_keypath(expression, scope); + case 'MemberExpression': { + const keypath = get_global_keypath(expression, scope); - if (keypath !== null && Object.hasOwn(global_constants, keypath)) { - this.values.add(global_constants[keypath]); - break; - } else if (keypath?.match?.(/\.name$/) && Object.hasOwn(globals, keypath.slice(0, -5))) { - this.values.add(globals[keypath.slice(0, -5)]?.[1]?.name ?? STRING); - break; - } else if ( - expression.object.type !== 'Super' && - expression.property.type !== 'PrivateIdentifier' - ) { - const object = scope.evaluate(expression.object, new Set(), seen_bindings, from_fn_call); - if (object.is_string) { - let property; - if (expression.computed) { - let prop = scope.evaluate( - expression.property, - new Set(), - seen_bindings, - from_fn_call - ); - if (prop.is_known && prop.value === 'length') { - property = 'length'; + if (keypath !== null && Object.hasOwn(global_constants, keypath)) { + this.values.add(global_constants[keypath]); + break; + } else if (keypath?.match?.(/\.name$/) && Object.hasOwn(globals, keypath.slice(0, -5))) { + this.values.add(globals[keypath.slice(0, -5)]?.[1]?.name ?? STRING); + break; + } else if ( + expression.object.type !== 'Super' && + expression.property.type !== 'PrivateIdentifier' + ) { + const object = scope.evaluate( + expression.object, + new Set(), + seen_bindings, + from_fn_call + ); + if (object.is_string) { + let property; + if (expression.computed) { + let prop = scope.evaluate( + expression.property, + new Set(), + seen_bindings, + from_fn_call + ); + if (prop.is_known && prop.value === 'length') { + property = 'length'; + } + } else if (expression.property.type === 'Identifier') { + property = expression.property.name; } - } else if (expression.property.type === 'Identifier') { - property = expression.property.name; - } - if (property === 'length') { - if (object.is_known) { - this.values.add(object.value.length); - } else { - this.values.add(NUMBER); + if (property === 'length') { + if (object.is_known) { + this.values.add(object.value.length); + } else { + this.values.add(NUMBER); + } + break; } - break; } } + + this.values.add(UNKNOWN); + break; } - this.values.add(UNKNOWN); - break; + default: { + this.values.add(UNKNOWN); + } } - default: { - this.values.add(UNKNOWN); - } - } + for (const value of this.values) { + this.value = value; // saves having special logic for `size === 1` - for (const value of this.values) { - this.value = value; // saves having special logic for `size === 1` + if (value !== STRING && typeof value !== 'string') { + this.is_string = false; + } - if (value !== STRING && typeof value !== 'string') { - this.is_string = false; - } + if (value !== NUMBER && typeof value !== 'number') { + this.is_number = false; + } - if (value !== NUMBER && typeof value !== 'number') { - this.is_number = false; + if (value === NUMBER || value === STRING || value === NOT_NULL || !value) { + this.is_truthy = false; + this.is_falsy = !value; + } + + if (value == null || value === UNKNOWN) { + this.is_defined = false; + this.is_truthy = false; + } } - if (value == null || value === UNKNOWN) { - this.is_defined = false; + if (this.values.size > 1 || typeof this.value === 'symbol') { + this.is_known = false; } - } - if (this.values.size > 1 || typeof this.value === 'symbol') { - this.is_known = false; + if (!this.value) { + this.is_truthy = false; + } + } catch (err) { + if ( + /** @type {Error} */ (err).message !== 'Maximum call stack size exceeded' && + /** @type {Error} */ (err).message !== 'too much recursion' + ) { + throw err; + } } } } @@ -1003,7 +1149,7 @@ export class Scope { const binding = this.declarations.get(node.name); if (binding) { - binding.references.push({ node, path }); + binding.references.push({ node, path, scope: this }); } else if (this.parent) { this.parent.reference(node, path); } else { @@ -1135,7 +1281,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { /** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */ const references = []; - /** @type {[Scope, Pattern | MemberExpression][]} */ + /** @type {[Scope, Pattern | MemberExpression, AssignmentExpression | UpdateExpression | AST.BindDirective][]} */ const updates = []; /** @@ -1305,12 +1451,16 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { // updates AssignmentExpression(node, { state, next }) { - updates.push([state.scope, node.left]); + updates.push([state.scope, node.left, node]); next(); }, UpdateExpression(node, { state, next }) { - updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]); + updates.push([ + state.scope, + /** @type {Identifier | MemberExpression} */ (node.argument), + node + ]); next(); }, @@ -1530,7 +1680,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { BindDirective(node, context) { updates.push([ context.state.scope, - /** @type {Identifier | MemberExpression} */ (node.expression) + /** @type {Identifier | MemberExpression} */ (node.expression), + node ]); context.next(); }, @@ -1566,7 +1717,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scope.reference(node, path); } - for (const [scope, node] of updates) { + for (const [scope, node, update] of updates) { for (const expression of unwrap_pattern(node)) { const left = object(expression); const binding = left && scope.get(left.name); @@ -1577,6 +1728,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } else { binding.mutated = true; } + binding.updates.push(update); } } } @@ -1654,10 +1806,9 @@ function get_global_keypath(node, scope) { /** * @param {{type: string} & Record} node - * @param {Scope} scope * @returns {any} */ -function get_type_of_ts_node(node, scope) { +function get_type_of_ts_node(node) { /** * @param {any[]} types * @returns {any[]} @@ -1695,20 +1846,17 @@ function get_type_of_ts_node(node, scope) { } return res; } - switch (node.type) { + switch (node?.type) { case 'TypeAnnotation': - return get_type_of_ts_node(node.annotation, scope); + return get_type_of_ts_node(node.annotation); case 'TSCheckType': - return [ - get_type_of_ts_node(node.trueType, scope), - get_type_of_ts_node(node.falseType, scope) - ].flat(); + return [get_type_of_ts_node(node.trueType), get_type_of_ts_node(node.falseType)].flat(); case 'TSUnionType': //@ts-ignore - return node.types.map((type) => get_type_of_ts_node(type, scope)).flat(); + return node.types.map((type) => get_type_of_ts_node(type)).flat(); case 'TSIntersectionType': //@ts-ignore - return intersect_types(node.types.map((type) => get_type_of_ts_node(type, scope)).flat()); + return intersect_types(node.types.map((type) => get_type_of_ts_node(type)).flat()); case 'TSBigIntKeyword': case 'TSNumberKeyword': return NUMBER; From 9a1eb6e99a6003c45d192ae9fb6c2db34376bdbf Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 20 Apr 2025 19:26:37 -0700 Subject: [PATCH 19/19] lint --- .../3-transform/client/visitors/IfBlock_unfinished.js | 4 +++- .../svelte/src/compiler/phases/3-transform/utils.js | 10 +++++----- packages/svelte/src/compiler/phases/scope.js | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js index ee98f8b1a97c..0057c02514df 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock_unfinished.js @@ -14,7 +14,9 @@ export function IfBlock(node, context) { const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); context.state.template.push(''); if (evaluated.is_truthy) { - context.state.init.push(b.stmt(b.call(b.arrow([b.id('$$anchor')], consequent), context.state.node))); + context.state.init.push( + b.stmt(b.call(b.arrow([b.id('$$anchor')], consequent), context.state.node)) + ); } else { const statements = []; const consequent_id = context.state.scope.generate('consequent'); diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 43ec4558462e..6e7e2f550d5b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -488,12 +488,12 @@ export function transform_inspect_rune(node, context) { } /** - * Whether a `BlockStatement` needs to be a block statement as opposed to just inlining all of its statements. + * Whether a `BlockStatement` needs to be a block statement as opposed to just inlining all of its statements. * @param {BlockStatement} block */ export function needs_new_scope(block) { - const has_vars = block.body.some(child => child.type === 'VariableDeclaration'); - const has_fns = block.body.some(child => child.type === 'FunctionDeclaration'); - const has_class = block.body.some(child => child.type === 'ClassDeclaration'); + const has_vars = block.body.some((child) => child.type === 'VariableDeclaration'); + const has_fns = block.body.some((child) => child.type === 'FunctionDeclaration'); + const has_class = block.body.some((child) => child.type === 'ClassDeclaration'); return has_vars || has_fns || has_class; -} \ No newline at end of file +} diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f4771c2259de..048f6ccef5ef 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1011,7 +1011,7 @@ export class Scope { /** * A set of all the names referenced with this scope * — useful for generating unique names - * @type {Map} + * @type {Map} */ references = new Map(); @@ -1145,7 +1145,7 @@ export class Scope { if (!references) this.references.set(node.name, (references = [])); - references.push({ node, path }); + references.push({ node, path, scope: this }); const binding = this.declarations.get(node.name); if (binding) {