diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ac1d3c812d..1c92d6f0da 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,7 +1,7 @@ /** @import * as ESTree from 'estree' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ -/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ +/** @import { Analysis, AwaitedStatement, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; @@ -701,44 +701,62 @@ export function analyze_component(root, source, options) { } if (instance.has_await) { - let awaiting = false; - let i = 0; + /** + * @param {ESTree.Node} node + * @param {Set} dependencies + */ + const trace_dependencies = (node, dependencies) => { + walk( + node, + { scope: instance.scope }, + { + _(node, context) { + const scope = scopes.get(node); + if (scope) { + context.next({ scope }); + } else { + context.next(); + } + }, + Identifier(node, context) { + const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); + if (is_reference(node, parent)) { + const binding = context.state.scope.get(node.name); + if (binding) { + dependencies.add(binding); + } + + // TODO recurse into function definitions + } + } + } + ); + + return dependencies; + }; /** * @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.FunctionDeclaration | ESTree.ClassDeclaration} node */ const push = (node) => { - const has_await = has_await_expression(node); - awaiting ||= has_await; + /** @type {AwaitedStatement} */ + const statement = { + node, + has_await: has_await_expression(node), + declarations: [], + dependencies: trace_dependencies(node, new Set()) + }; - if (!awaiting) return; - - const id = b.id(`$$${i++}`); - - analysis.awaited_statements.set(node, { - id, - has_await, - metadata: create_expression_metadata() - }); + analysis.awaited_statements.set(node, statement); if (node.type === 'VariableDeclarator') { for (const identifier of extract_identifiers(node.id)) { - analysis.awaited_declarations.set(identifier.name, { - id, - has_await, - pattern: node.id, - updated_by: new Set(), - metadata: create_expression_metadata() - }); + const binding = /** @type {Binding} */ (instance.scope.get(identifier.name)); + statement.declarations.push(binding); } } else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') { - analysis.awaited_declarations.set(node.id.name, { - id, - has_await, - pattern: node.id, - updated_by: new Set(), - metadata: create_expression_metadata() - }); + const binding = /** @type {Binding} */ (instance.scope.get(node.id.name)); + statement.declarations.push(binding); } }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 58af1a8a1e..269b353523 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,8 +1,10 @@ -/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclarator } from 'estree' */ +/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { ComponentContext } from '../types' */ +/** @import { AwaitedStatement } from '../../../types' */ import { build_getter, is_prop_source } from '../utils.js'; import * as b from '#compiler/builders'; import { add_state_transformers } from './shared/declarations.js'; +import { get_rune } from '../../../scope.js'; /** * @param {Program} node @@ -155,55 +157,47 @@ function transform_body(program, context) { /** @type {Statement[]} */ const out = []; - const { awaited_declarations, awaited_statements } = context.state.analysis; + /** @type {Identifier[]} */ + const ids = []; - /** @type {Identifier | null} */ - let last = null; + /** @type {AwaitedStatement[]} */ + const statements = []; + + /** @type {AwaitedStatement[]} */ + const deriveds = []; + + const { awaited_statements } = context.state.analysis; + + let awaited = false; /** - * @param {Statement | VariableDeclarator | FunctionDeclaration | ClassDeclaration} node + * @param {Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration} node */ const push = (node) => { - const awaited = awaited_statements.get(node); + const statement = awaited_statements.get(node); - if (awaited) { - const ids = new Set(); - const patterns = new Set(); + awaited ||= !!statement?.has_await; - for (const binding of awaited.metadata.dependencies) { - const dep = awaited_declarations.get(binding.node.name); - if (dep && dep.id !== awaited.id && !ids.has(dep.id)) { - ids.add(dep.id); - patterns.add(dep.pattern); - } + if (!awaited || !statement || node.type === 'FunctionDeclaration') { + if (node.type === 'VariableDeclarator') { + out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); + } else { + out.push(/** @type {Statement} */ (context.visit(node))); } - if (last) { - ids.add(last); - } + return; + } - const rhs = - node.type === 'VariableDeclarator' - ? node.init ?? b.block([]) - : node.type === 'ExpressionStatement' - ? node.expression - : node.type === 'FunctionDeclaration' - ? node - : b.block([node]); - - out.push( - b.var( - awaited.id, - b.call('$.run', b.array([...ids]), b.arrow([...patterns], rhs, awaited.has_await)) - ) - ); + if (node.type === 'VariableDeclarator') { + const rune = get_rune(node.init, context.state.scope); - last = awaited.id; - } else if (node.type === 'VariableDeclarator') { - out.push(b.var(node.id, node.init)); - } else { - out.push(node); + if (rune === '$derived' || rune === '$derived.by') { + deriveds.push(statement); + return; + } } + + statements.push(statement); }; for (let node of program.body) { @@ -231,13 +225,86 @@ function transform_body(program, context) { for (const declarator of node.declarations) { push(declarator); } - } else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') { - // TODO - push(node); } else { push(node); } } - return out.map((node) => /** @type {Statement} */ (context.visit(node))); + for (const derived of deriveds) { + // find the earliest point we can insert this derived + let index = -1; + + for (const binding of derived.dependencies) { + index = Math.max( + index, + statements.findIndex((s) => s.declarations.includes(binding)) + ); + } + + if (index === -1 && !derived.has_await) { + const node = /** @type {VariableDeclarator} */ (derived.node); + out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); + } else { + // TODO combine deriveds with Promise.all where necessary + statements.splice(index + 1, 0, derived); + } + } + + if (statements.length > 0) { + var declarations = statements.map((s) => s.declarations).flat(); + out.push( + b.declaration( + 'var', + declarations.map((d) => b.declarator(d.node)) + ) + ); + + const thunks = statements.map((s) => { + if (s.node.type === 'VariableDeclarator') { + const visited = /** @type {VariableDeclaration} */ ( + context.visit(b.var(s.node.id, s.node.init)) + ); + + return b.thunk( + b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), + s.has_await + ); + } + + if (s.node.type === 'ClassDeclaration') { + return b.thunk( + b.assignment( + '=', + s.node.id, + /** @type {ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) + ), + s.has_await + ); + } + + if (s.node.type === 'FunctionDeclaration') { + return b.thunk( + b.assignment( + '=', + s.node.id, + /** @type {FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) + ), + s.has_await + ); + } + + if (s.node.type === 'ExpressionStatement') { + return b.thunk(b.unary('void', /** @type {Expression} */ (s.node.expression)), s.has_await); + } + + return b.thunk(b.block([/** @type {Statement} */ (s.node)]), s.has_await); + }); + + out.push(b.var('$$promises', b.call('$.run', b.array(thunks)))); + } + + // console.log('statements', statements); + // console.log('deriveds', deriveds); + + return out; } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index e62c2da735..8d2d526559 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -3,6 +3,8 @@ import type { AwaitExpression, CallExpression, ClassBody, + ClassDeclaration, + FunctionDeclaration, Identifier, LabeledStatement, ModuleDeclaration, @@ -40,9 +42,10 @@ export interface AwaitedDeclaration { } export interface AwaitedStatement { - id: Identifier; + node: Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration; has_await: boolean; - metadata: ExpressionMetadata; + declarations: Binding[]; + dependencies: Set; } /** diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 8a3f04f4ad..4a44294189 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -82,6 +82,17 @@ export function block(body) { return { type: 'BlockStatement', body }; } +/** + * @param {ESTree.Identifier | null} id + * @param {ESTree.ClassBody} body + * @param {ESTree.Expression | null} [superClass] + * @param {ESTree.Decorator[]} [decorators] + * @returns {ESTree.ClassExpression} + */ +export function class_expression(id, body, superClass, decorators = []) { + return { type: 'ClassExpression', body, superClass, decorators }; +} + /** * @param {string} name * @param {ESTree.Statement} body