pre-transform

pull/17038/head
Rich Harris 3 weeks ago
parent f1715ac2e6
commit 3ad47c324d

@ -1,7 +1,7 @@
/** @import * as ESTree from 'estree' */
/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, AwaitedStatement, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js';
@ -549,7 +549,12 @@ export function analyze_component(root, source, options) {
snippets: new Set(),
async_deriveds: new Set(),
pickled_awaits: new Set(),
awaited_statements: new Map()
instance_body: {
sync: [],
async: [],
declarations: [],
hoisted: []
}
};
if (!runes) {
@ -693,177 +698,191 @@ export function analyze_component(root, source, options) {
e.legacy_rest_props_invalid(rest_props_refs[0].node);
}
if (instance.has_await) {
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
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) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
/**
* @param {ESTree.Node} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Node>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
ImportDeclaration(node) {},
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) {
touched.add(binding);
for (const assignment of binding.assignments) {
touch(assignment.value, assignment.scope, touched, seen);
}
}
}
}
);
};
}
);
};
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Node} node
* @param {Set<ESTree.Node>} seen
* @param {Set<Binding>} reads
* @param {Set<Binding>} writes
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
const trace_references = (node, reads, writes, seen = new Set()) => {
if (seen.has(node)) return;
seen.add(node);
/**
* @param {ESTree.Pattern} node
* @param {Scope} scope
*/
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
function update(node, scope) {
for (const pattern of unwrap_pattern(node)) {
const node = object(pattern);
if (!node) return;
const binding = scope.get(node.name);
if (!binding) return;
writes.add(binding);
}
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
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) {
reads.add(binding);
}
walk(
node,
{ scope: instance.scope },
{
_(node, context) {
const scope = scopes.get(node);
if (scope) {
context.next({ scope });
} else {
context.next();
}
},
AssignmentExpression(node, context) {
update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
},
CallExpression(node, context) {
// for now, assume everything touched by the callee ends up mutating the object
// TODO optimise this better
// special case — no need to peek inside effects
const rune = get_rune(node, context.state.scope);
if (rune === '$effect') return;
/** @type {Set<Binding>} */
const touched = new Set();
touch(node, context.state.scope, touched);
for (const b of touched) {
writes.add(b);
}
},
// don't look inside functions until they are called
ArrowFunctionExpression(_, context) {},
FunctionDeclaration(_, context) {},
FunctionExpression(_, context) {},
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) {
reads.add(binding);
}
}
}
);
};
}
);
};
/**
* @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.FunctionDeclaration | ESTree.ClassDeclaration} node
*/
const push = (node) => {
let awaited = false;
// TODO this should probably be attached to the scope?
var promises = b.id('$$promises');
/**
* @param {ESTree.Identifier} id
* @param {ESTree.Expression} blocker
*/
function push_declaration(id, blocker) {
analysis.instance_body.declarations.push(id);
const binding = /** @type {Binding} */ (instance.scope.get(id.name));
binding.blocker = blocker;
}
for (let node of instance.ast.body) {
if (node.type === 'ImportDeclaration') {
analysis.instance_body.hoisted.push(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// these can't exist inside `<script>` but TypeScript doesn't know that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
}
const has_await = has_await_expression(node);
awaited ||= has_await;
if (awaited && node.type !== 'FunctionDeclaration') {
/** @type {Set<Binding>} */
const reads = new Set();
const reads = new Set(); // TODO we're not actually using this yet
/** @type {Set<Binding>} */
const writes = new Set();
trace_references(node, reads, writes);
/** @type {AwaitedStatement} */
const statement = {
node,
has_await: has_await_expression(node),
declarations: [],
reads,
writes
};
analysis.awaited_statements.set(node, statement);
if (node.type === 'VariableDeclarator') {
for (const identifier of extract_identifiers(node.id)) {
const binding = /** @type {Binding} */ (instance.scope.get(identifier.name));
statement.declarations.push(binding);
}
} else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') {
const binding = /** @type {Binding} */ (instance.scope.get(node.id.name));
statement.declarations.push(binding);
}
};
for (let node of instance.ast.body) {
if (
node.type === 'ImportDeclaration' ||
node.type === 'ExportDefaultDeclaration' ||
node.type === 'ExportAllDeclaration'
) {
continue;
}
const blocker = b.member(promises, b.literal(analysis.instance_body.async.length), true);
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
node = node.declaration;
} else {
continue;
}
for (const binding of writes) {
binding.blocker = blocker;
}
if (node.type === 'VariableDeclaration') {
for (const declarator of node.declarations) {
push(declarator);
for (const id of extract_identifiers(declarator.id)) {
push_declaration(id, blocker);
}
// one declarator per declaration, makes things simpler
analysis.instance_body.async.push({
node: declarator,
has_await
});
}
} else if (node.type === 'ClassDeclaration') {
push_declaration(node.id, blocker);
analysis.instance_body.async.push({ node, has_await });
} else {
push(node);
analysis.instance_body.async.push({ node, has_await });
}
} else {
analysis.instance_body.sync.push(node);
}
}

@ -153,7 +153,7 @@ export function client_component(analysis, options) {
scope: analysis.module.scope,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
hoisted: [b.import_all('$', 'svelte/internal/client'), ...analysis.instance_body.hoisted],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),

@ -8,6 +8,7 @@ import * as b from '#compiler/builders';
*/
export function ImportDeclaration(node, context) {
if ('hoisted' in context.state) {
// TODO we can get rid of this visitor
context.state.hoisted.push(node);
return b.empty;
}

@ -144,7 +144,7 @@ export function Program(node, context) {
...node,
body: transform_body(
node,
context.state.analysis.awaited_statements,
context.state.analysis.instance_body,
b.id('$.run'),
(node) => /** @type {Node} */ (context.visit(node)),
(statement) => context.state.hoisted.push(statement)

@ -97,7 +97,7 @@ export function server_component(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
hoisted: [b.import_all('$', 'svelte/internal/server')],
hoisted: [b.import_all('$', 'svelte/internal/server'), ...analysis.instance_body.hoisted],
legacy_reactive_statements: new Map(),
// these are set inside the `Fragment` visitor, and cannot be used until then
init: /** @type {any} */ (null),

@ -17,7 +17,7 @@ export function Program(node, context) {
...node,
body: transform_body(
node,
c.state.analysis.awaited_statements,
c.state.analysis.instance_body,
b.id('$$renderer.run'),
(node) => /** @type {Node} */ (context.visit(node)),
(statement) => c.state.hoisted.push(statement)

@ -1,132 +1,28 @@
/** @import * as ESTree from 'estree' */
/** @import { AwaitedStatement } from '../../types' */
/** @import { ComponentAnalysis } from '../../types' */
import * as b from '#compiler/builders';
// TODO find a way to DRY out this and the corresponding server visitor
/**
* @param {ESTree.Program} program
* @param {Map<ESTree.Node, AwaitedStatement>} awaited_statements
* @param {ComponentAnalysis['instance_body']} instance_body
* @param {ESTree.Expression} runner
* @param {(node: ESTree.Node) => ESTree.Node} transform
* @param {(node: ESTree.Statement | ESTree.ModuleDeclaration) => void} hoist
*/
export function transform_body(program, awaited_statements, runner, transform, hoist) {
/** @type {ESTree.Statement[]} */
const out = [];
/** @type {AwaitedStatement[]} */
const statements = [];
/** @type {AwaitedStatement[]} */
const deriveds = [];
let awaited = false;
/**
* @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.ClassDeclaration | ESTree.FunctionDeclaration} node
*/
const push = (node) => {
const statement = awaited_statements.get(node);
awaited ||= !!statement?.has_await;
if (!awaited || !statement || node.type === 'FunctionDeclaration') {
if (node.type === 'VariableDeclarator') {
out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init))));
} else {
out.push(/** @type {ESTree.Statement} */ (transform(node)));
}
return;
}
// TODO put deriveds into a separate array, and group them immediately
// after their latest dependency. for now, to avoid having to figure
// out the intricacies of dependency tracking, just let 'em waterfall
// if (node.type === 'VariableDeclarator') {
// const rune = get_rune(node.init, context.state.scope);
// if (rune === '$derived' || rune === '$derived.by') {
// deriveds.push(statement);
// return;
// }
// }
statements.push(statement);
};
for (let node of program.body) {
if (node.type === 'ImportDeclaration') {
// TODO we can get rid of the visitor
hoist(node);
continue;
}
if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') {
// this can't happen, but it's useful for TypeScript to understand that
continue;
}
if (node.type === 'ExportNamedDeclaration') {
if (node.declaration) {
// TODO ditto — no visitor needed
node = node.declaration;
} else {
continue;
}
}
if (node.type === 'VariableDeclaration') {
if (
!awaited &&
node.declarations.every((declarator) => !awaited_statements.get(declarator)?.has_await)
) {
out.push(/** @type {ESTree.VariableDeclaration} */ (transform(node)));
} else {
for (const declarator of node.declarations) {
push(declarator);
}
}
} else {
push(node);
}
}
for (const derived of deriveds) {
// find the earliest point we can insert this derived
let index = -1;
for (const binding of derived.reads) {
index = Math.max(
index,
statements.findIndex((s) => s.declarations.includes(binding))
);
}
if (index === -1 && !derived.has_await) {
const node = /** @type {ESTree.VariableDeclarator} */ (derived.node);
out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init))));
} else {
// TODO combine deriveds with Promise.all where necessary
statements.splice(index + 1, 0, derived);
}
export function transform_body(program, instance_body, runner, transform) {
const statements = instance_body.sync.map(transform);
if (instance_body.declarations.length > 0) {
statements.push(
b.declaration(
'var',
instance_body.declarations.map((id) => b.declarator(id))
)
);
}
var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict
if (statements.length > 0) {
var declarations = statements.map((s) => s.declarations).flat();
if (declarations.length > 0) {
out.push(
b.declaration(
'var',
declarations.map((d) => b.declarator(d.node))
)
);
}
const thunks = statements.map((s) => {
if (instance_body.async.length > 0) {
const thunks = instance_body.async.map((s) => {
if (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration} */ (
transform(b.var(s.node.id, s.node.init))
@ -162,17 +58,6 @@ export function transform_body(program, awaited_statements, runner, transform, h
);
}
if (s.node.type === 'FunctionDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' })
),
s.has_await
);
}
if (s.node.type === 'ExpressionStatement') {
const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression));
@ -184,24 +69,9 @@ export function transform_body(program, awaited_statements, runner, transform, h
return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await);
});
out.push(b.var(promises, b.call(runner, b.array(thunks))));
for (let i = 0; i < statements.length; i += 1) {
const s = statements[i];
var blocker = b.member(promises, b.literal(i), true);
for (const binding of s.declarations) {
binding.blocker = blocker;
}
for (const binding of s.writes) {
// if a statement writes to a binding, any reads of that
// binding must wait for the statement
binding.blocker = blocker;
}
}
// TODO get the `$$promises` ID from scope
statements.push(b.var('$$promises', b.call(runner, b.array(thunks))));
}
return out;
return statements;
}

@ -11,6 +11,7 @@ import type {
Pattern,
Program,
Statement,
VariableDeclaration,
VariableDeclarator
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
@ -42,14 +43,6 @@ export interface AwaitedDeclaration {
updated_by: Set<Identifier>;
}
export interface AwaitedStatement {
node: Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration;
has_await: boolean;
declarations: Binding[];
reads: Set<Binding>;
writes: Set<Binding>;
}
/**
* Analysis common to modules and components
*/
@ -132,8 +125,12 @@ export interface ComponentAnalysis extends Analysis {
*/
snippets: Set<AST.SnippetBlock>;
/**
* Information about top-level instance statements that need to be transformed
* so that we can run the template synchronously
* Pre-transformed `<script>` block
*/
awaited_statements: Map<Statement | ModuleDeclaration | VariableDeclarator, AwaitedStatement>;
instance_body: {
hoisted: Array<Statement | ModuleDeclaration>;
sync: Array<Statement | ModuleDeclaration | VariableDeclaration>;
async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>;
declarations: Array<Identifier>;
};
}

Loading…
Cancel
Save