compiler work

pull/16781/head
S. Elliott Johnson 4 weeks ago
parent 4356b649cd
commit b63a56a67e

@ -278,7 +278,8 @@ export function analyze_module(source, options) {
tracing: false,
async_deriveds: new Set(),
comments,
classes: new Map()
classes: new Map(),
pickled_awaits: new Set()
};
state.adjust({
@ -304,7 +305,8 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
parent_element: null,
reactive_statement: null
reactive_statement: null,
in_derived: false
},
visitors
);
@ -540,7 +542,8 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set()
async_deriveds: new Set(),
pickled_awaits: new Set()
};
if (!runes) {
@ -699,7 +702,8 @@ export function analyze_component(root, source, options) {
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth,
reactive_statement: null
reactive_statement: null,
in_derived: false
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -766,7 +770,8 @@ export function analyze_component(root, source, options) {
component_slots: new Set(),
expression: null,
state_fields: new Map(),
function_depth: scope.function_depth
function_depth: scope.function_depth,
in_derived: false
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);

@ -27,6 +27,11 @@ export interface AnalysisState {
// legacy stuff
reactive_statement: null | ReactiveStatement;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
in_derived: boolean;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -1,14 +1,27 @@
/** @import { AwaitExpression } from 'estree' */
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
import * as e from '../../../errors.js';
import * as b from '#compiler/builders';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1;
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
if (
tla ||
(is_reactive_expression(context.path, context.state.in_derived) &&
!is_last_evaluated_expression(context.path, node))
) {
context.state.analysis.pickled_awaits.add(node);
}
let suspend = tla;
if (context.state.expression) {
context.state.expression.has_await = true;
@ -34,3 +47,101 @@ export function AwaitExpression(node, context) {
context.next();
}
/**
* @param {AST.SvelteNode[]} path
* @param {boolean} in_derived
*/
export function is_reactive_expression(path, in_derived) {
if (in_derived) {
return true;
}
let i = path.length;
while (i--) {
const parent = path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {AST.SvelteNode[]} path
* @param {Expression | SpreadElement | Property} node
*/
export function is_last_evaluated_expression(path, node) {
let i = path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -241,6 +241,7 @@ export function CallExpression(node, context) {
context.next({
...context.state,
function_depth: context.state.function_depth + 1,
in_derived: true,
expression
});

@ -35,5 +35,9 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
context.visit(declaration.init, {
...context.state,
expression: node.metadata.expression,
in_derived: true
});
}

@ -49,6 +49,12 @@ export function VariableDeclarator(node, context) {
}
}
if (rune === '$derived') {
context.visit(node.id);
context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true });
return;
}
if (rune === '$props') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);

@ -166,7 +166,6 @@ export function client_component(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
instance_level_snippets: [],
module_level_snippets: [],
@ -712,7 +711,6 @@ export function client_module(analysis, options) {
state_fields: new Map(),
transform: {},
in_constructor: false,
in_derived: false,
is_instance: false
};

@ -6,8 +6,7 @@ import type {
Expression,
AssignmentExpression,
UpdateExpression,
VariableDeclaration,
Declaration
VariableDeclaration
} from 'estree';
import type { AST, Namespace, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -22,11 +21,6 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/**
* True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`)
*/
readonly in_derived: boolean;
/** `true` if we're transforming the contents of `<script>` */
readonly is_instance: boolean;

@ -1,4 +1,4 @@
/** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
@ -10,12 +10,7 @@ import * as b from '../../../../utils/builders.js';
export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument));
const tla = context.state.is_instance && context.state.scope.function_depth === 1;
// preserve context for
// a) top-level await and
// b) awaits that precede other expressions in template or `$derived(...)`
if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) {
if (context.state.analysis.pickled_awaits.has(node)) {
return b.call(b.await(b.call('$.save', argument)));
}
@ -27,100 +22,3 @@ export function AwaitExpression(node, context) {
return argument === node.argument ? node : { ...node, argument };
}
/**
* @param {Context} context
*/
function is_reactive_expression(context) {
if (context.state.in_derived) {
return true;
}
let i = context.path.length;
while (i--) {
const parent = context.path[i];
if (
parent.type === 'ArrowFunctionExpression' ||
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration'
) {
return false;
}
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
}
return false;
}
/**
* @param {Context} context
* @param {Expression | SpreadElement | Property} node
*/
function is_last_evaluated_expression(context, node) {
let i = context.path.length;
while (i--) {
const parent = /** @type {Expression | Property | SpreadElement} */ (context.path[i]);
// @ts-expect-error we could probably use a neater/more robust mechanism
if (parent.metadata) {
return true;
}
switch (parent.type) {
case 'ArrayExpression':
if (node !== parent.elements.at(-1)) return false;
break;
case 'AssignmentExpression':
case 'BinaryExpression':
case 'LogicalExpression':
if (node === parent.left) return false;
break;
case 'CallExpression':
case 'NewExpression':
if (node !== parent.arguments.at(-1)) return false;
break;
case 'ConditionalExpression':
if (node === parent.test) return false;
break;
case 'MemberExpression':
if (parent.computed && node === parent.object) return false;
break;
case 'ObjectExpression':
if (node !== parent.properties.at(-1)) return false;
break;
case 'Property':
if (node === parent.key) return false;
break;
case 'SequenceExpression':
if (node !== parent.expressions.at(-1)) return false;
break;
case 'TaggedTemplateExpression':
if (node !== parent.quasi.expressions.at(-1)) return false;
break;
case 'TemplateLiteral':
if (node !== parent.expressions.at(-1)) return false;
break;
default:
return false;
}
node = parent;
}
}

@ -44,9 +44,7 @@ export function CallExpression(node, context) {
case '$derived':
case '$derived.by': {
let fn = /** @type {Expression} */ (
context.visit(node.arguments[0], { ...context.state, in_derived: rune === '$derived' })
);
let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));
return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
}

@ -16,11 +16,7 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = build_expression(
{ ...context, state: { ...context.state, in_derived: true } },
declaration.init,
node.metadata.expression
);
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, init, node.metadata.expression.has_await);
@ -51,8 +47,7 @@ export function ConstTag(node, context) {
const child_state = /** @type {ComponentContext['state']} */ ({
...context.state,
transform,
in_derived: true
transform
});
// TODO optimise the simple `{ x } = y` case — we can just return `y`

@ -202,12 +202,7 @@ export function VariableDeclaration(node, context) {
);
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let expression = /** @type {Expression} */ (context.visit(value));
if (is_async) {
const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init);
@ -231,12 +226,7 @@ export function VariableDeclaration(node, context) {
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);
let expression = /** @type {Expression} */ (
context.visit(value, {
...context.state,
in_derived: rune === '$derived'
})
);
let expression = /** @type {Expression} */ (context.visit(value));
let rhs = value;

@ -10,6 +10,7 @@ import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { AwaitExpression } from './visitors/AwaitExpression.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
@ -45,6 +46,7 @@ import { call_child_payload } from './visitors/shared/utils.js';
const global_visitors = {
_: set_scope,
AssignmentExpression,
AwaitExpression,
CallExpression,
ClassBody,
ExpressionStatement,

@ -0,0 +1,17 @@
/** @import { AwaitExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {AwaitExpression} node
* @param {Context} context
*/
export function AwaitExpression(node, context) {
const argument = /** @type {Expression} */ (context.visit(node.argument));
if (context.state.analysis.pickled_awaits.has(node)) {
return b.call(b.await(b.call('$.save', argument)));
}
return argument === node.argument ? node : { ...node, argument };
}

@ -1,5 +1,12 @@
import type { AST, Binding, StateField } from '#compiler';
import type { CallExpression, ClassBody, Identifier, LabeledStatement, Program } from 'estree';
import type {
AwaitExpression,
CallExpression,
ClassBody,
Identifier,
LabeledStatement,
Program
} from 'estree';
import type { Scope, ScopeRoot } from './scope.js';
export interface Js {
@ -40,6 +47,8 @@ export interface Analysis {
/** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>;
/** Awaits needing context preservation */
pickled_awaits: Set<AwaitExpression>;
}
export interface ComponentAnalysis extends Analysis {

@ -96,3 +96,33 @@ function get_parent_context(component_context) {
return null;
}
/**
* Capture the current component context so that we can restore it after an async operation completes.
*/
function capture() {
var previous_component = current_component;
return function restore() {
current_component = previous_component;
};
}
/**
* Wraps an `await` expression in such a way that the component context that was
* active before the expression evaluated can be reapplied afterwards
* `await a + b()` becomes `(await $.save(a))() + b()`, meaning `b()` will have access
* to the context of its component.
* @template T
* @param {Promise<T>} promise
* @returns {Promise<() => T>}
*/
export async function save(promise) {
var restore = capture();
var value = await promise;
return () => {
restore();
return value;
};
}

@ -588,7 +588,7 @@ export { attr, clsx };
export { html } from './blocks/html.js';
export { push, pop } from './context.js';
export { push, pop, save } from './context.js';
export { push_element, pop_element, validate_snippet_args } from './dev.js';

Loading…
Cancel
Save