out-of-order-rendering
Rich Harris 1 day ago
parent 30b04a1e8c
commit a77fab9d17

@ -1,11 +1,10 @@
/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } 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';
import { runes } from '../../../../state.js';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} node
@ -143,205 +142,15 @@ export function Program(node, context) {
if (context.state.is_instance && runes) {
return {
...node,
body: transform_body(node, context)
body: transform_body(
node,
context.state.analysis.awaited_statements,
b.id('$.run'),
(node) => /** @type {Node} */ (context.visit(node)),
(statement) => context.state.hoisted.push(statement)
)
};
}
context.next();
}
// TODO find a way to DRY out this and the corresponding server visitor
/**
* @param {Program} program
* @param {ComponentContext} context
*/
function transform_body(program, context) {
/** @type {Statement[]} */
const out = [];
/** @type {AwaitedStatement[]} */
const statements = [];
/** @type {AwaitedStatement[]} */
const deriveds = [];
const { awaited_statements } = context.state.analysis;
let awaited = false;
/**
* @param {Statement | VariableDeclarator | ClassDeclaration | 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 {VariableDeclaration} */ (context.visit(b.var(node.id, node.init))));
} else {
out.push(/** @type {Statement} */ (context.visit(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
context.state.hoisted.push(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') {
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 {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);
}
}
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 (s.node.type === 'VariableDeclarator') {
const visited = /** @type {VariableDeclaration} */ (
context.visit(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.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') {
const expression = /** @type {Expression} */ (context.visit(s.node.expression));
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
}
return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await);
});
out.push(b.var(promises, b.call('$.run', 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;
}
}
}
return out;
}

@ -1,8 +1,8 @@
/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Node, Program } from 'estree' */
/** @import { Context, ComponentContext } from '../types' */
/** @import { AwaitedStatement } from '../../../types' */
import * as b from '#compiler/builders';
import { runes } from '../../../../state.js';
import { transform_body } from '../../shared/transform-async.js';
/**
* @param {Program} node
@ -10,208 +10,20 @@ import { runes } from '../../../../state.js';
*/
export function Program(node, context) {
if (context.state.is_instance && runes) {
// @ts-ignore wtf
const c = /** @type {ComponentContext} */ (context);
return {
...node,
// @ts-ignore wtf
body: transform_body(node, /** @type {ComponentContext} */ (context))
body: transform_body(
node,
c.state.analysis.awaited_statements,
b.id('$$renderer.run'),
(node) => /** @type {Node} */ (context.visit(node)),
(statement) => c.state.hoisted.push(statement)
)
};
}
context.next();
}
// TODO find a way to DRY out this and the corresponding server visitor
/**
* @param {Program} program
* @param {ComponentContext} context
*/
function transform_body(program, context) {
/** @type {Statement[]} */
const out = [];
/** @type {AwaitedStatement[]} */
const statements = [];
/** @type {AwaitedStatement[]} */
const deriveds = [];
const { awaited_statements } = context.state.analysis;
let awaited = false;
/**
* @param {Statement | VariableDeclarator | ClassDeclaration | 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 {VariableDeclaration} */ (context.visit(b.var(node.id, node.init))));
} else {
out.push(/** @type {Statement} */ (context.visit(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
context.state.hoisted.push(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') {
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 {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);
}
}
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 (s.node.type === 'VariableDeclarator') {
const visited = /** @type {VariableDeclaration} */ (
context.visit(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.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') {
const expression = /** @type {Expression} */ (context.visit(s.node.expression));
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
}
return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await);
});
out.push(b.var(promises, b.call('$$renderer.run', 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;
}
}
}
return out;
}

@ -0,0 +1,200 @@
/** @import * as ESTree from 'estree' */
/** @import { AwaitedStatement } 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 {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') {
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);
}
}
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 (s.node.type === 'VariableDeclarator') {
const visited = /** @type {ESTree.VariableDeclaration} */ (
transform(b.var(s.node.id, s.node.init))
);
if (visited.declarations.length === 1) {
return b.thunk(
b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0),
s.has_await
);
}
// if we have multiple declarations, it indicates destructuring
return b.thunk(
b.block([
b.var(visited.declarations[0].id, visited.declarations[0].init),
...visited.declarations
.slice(1)
.map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0)))
]),
s.has_await
);
}
if (s.node.type === 'ClassDeclaration') {
return b.thunk(
b.assignment(
'=',
s.node.id,
/** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' })
),
s.has_await
);
}
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));
return expression.type === 'AwaitExpression'
? b.thunk(expression, true)
: b.thunk(b.unary('void', expression), s.has_await);
}
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;
}
}
}
return out;
}
Loading…
Cancel
Save