deopt to ensure state is ready

pull/17038/head
Rich Harris 4 weeks ago
parent df31c40324
commit 2e571e3dbf

@ -6,7 +6,12 @@ import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js'; import { parse } from '../1-parse/acorn.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; import {
extract_identifiers,
has_await_expression,
object,
unwrap_pattern
} from '../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
@ -689,6 +694,37 @@ export function analyze_component(root, source, options) {
} }
if (instance.has_await) { if (instance.has_await) {
/**
* @param {ESTree.Expression} expression
* @param {Scope} scope
* @param {Set<Binding>} touched
* @param {Set<ESTree.Expression>} seen
*/
const touch = (expression, scope, touched, seen = new Set()) => {
if (seen.has(expression)) return;
seen.add(expression);
walk(
expression,
{ scope },
{
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 {ESTree.Node} node
* @param {Set<ESTree.Node>} seen * @param {Set<ESTree.Node>} seen
@ -699,6 +735,22 @@ export function analyze_component(root, source, options) {
if (seen.has(node)) return; if (seen.has(node)) return;
seen.add(node); 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);
}
}
walk( walk(
node, node,
{ scope: instance.scope }, { scope: instance.scope },
@ -712,12 +764,34 @@ export function analyze_component(root, source, options) {
} }
}, },
AssignmentExpression(node, context) { AssignmentExpression(node, context) {
// TODO mark writes update(node.left, context.state.scope);
},
UpdateExpression(node, context) {
update(
/** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument),
context.state.scope
);
}, },
CallExpression(node, context) { CallExpression(node, context) {
// TODO deopt arguments, assume they are mutated // for now, assume everything touched by the callee ends up mutating the object
// TODO recurse into function definitions // 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) { Identifier(node, context) {
const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); const parent = /** @type {ESTree.Node} */ (context.path.at(-1));
if (is_reference(node, parent)) { if (is_reference(node, parent)) {

@ -334,24 +334,14 @@ function transform_body(program, context) {
for (const binding of s.declarations) { for (const binding of s.declarations) {
binding.blocker = blocker; binding.blocker = blocker;
} }
}
// TODO we likely need to account for updates that happen after the declaration,
// e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise
// a synchronous `{obj.foo}` will fail
for (const binding of context.state.scope.declarations.values()) { for (const binding of s.writes) {
// if the binding is updated (TODO or passed to a function, in which case it // if a statement writes to a binding, any reads of that
// could be mutated), play it safe and block until the end. In future we // binding must wait for the statement
// could develop more sophisticated static analysis to optimise further binding.blocker = blocker;
if (binding.updated) {
binding.blocker = b.member(promises, b.literal(statements.length - 1), true);
} }
} }
} }
// console.log('statements', statements);
// console.log('deriveds', deriveds);
return out; return out;
} }

@ -204,24 +204,14 @@ function transform_body(program, context) {
for (const binding of s.declarations) { for (const binding of s.declarations) {
binding.blocker = blocker; binding.blocker = blocker;
} }
}
// TODO we likely need to account for updates that happen after the declaration,
// e.g. `let obj = $state()` followed by a later `obj = {...}`, otherwise
// a synchronous `{obj.foo}` will fail
for (const binding of context.state.scope.declarations.values()) { for (const binding of s.writes) {
// if the binding is updated (TODO or passed to a function, in which case it // if a statement writes to a binding, any reads of that
// could be mutated), play it safe and block until the end. In future we // binding must wait for the statement
// could develop more sophisticated static analysis to optimise further binding.blocker = blocker;
if (binding.updated) {
binding.blocker = b.member(promises, b.literal(statements.length - 1), true);
} }
} }
} }
// console.log('statements', statements);
// console.log('deriveds', deriveds);
return out; return out;
} }

@ -108,6 +108,9 @@ export class Binding {
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = []; references = [];
/** @type {Array<{ value: Expression; scope: Scope }>} */
assignments = [];
/** /**
* For `legacy_reactive`: its reactive dependencies * For `legacy_reactive`: its reactive dependencies
* @type {Binding[]} * @type {Binding[]}
@ -152,6 +155,10 @@ export class Binding {
this.initial = initial; this.initial = initial;
this.kind = kind; this.kind = kind;
this.declaration_kind = declaration_kind; this.declaration_kind = declaration_kind;
if (initial) {
this.assignments.push({ value: /** @type {Expression} */ (initial), scope });
}
} }
get updated() { get updated() {
@ -868,7 +875,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */ /** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
const references = []; const references = [];
/** @type {[Scope, Pattern | MemberExpression][]} */ /** @type {[Scope, Pattern | MemberExpression, Expression][]} */
const updates = []; const updates = [];
/** /**
@ -1056,12 +1063,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// updates // updates
AssignmentExpression(node, { state, next }) { AssignmentExpression(node, { state, next }) {
updates.push([state.scope, node.left]); updates.push([state.scope, node.left, node.right]);
next(); next();
}, },
UpdateExpression(node, { state, next }) { UpdateExpression(node, { state, next }) {
updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]); const expression = /** @type {Identifier | MemberExpression} */ (node.argument);
updates.push([state.scope, expression, expression]);
next(); next();
}, },
@ -1282,10 +1290,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}, },
BindDirective(node, context) { BindDirective(node, context) {
updates.push([ if (node.expression.type !== 'SequenceExpression') {
context.state.scope, const expression = /** @type {Identifier | MemberExpression} */ (node.expression);
/** @type {Identifier | MemberExpression} */ (node.expression) updates.push([context.state.scope, expression, expression]);
]); }
context.next(); context.next();
}, },
@ -1320,7 +1329,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scope.reference(node, path); scope.reference(node, path);
} }
for (const [scope, node] of updates) { for (const [scope, node, value] of updates) {
for (const expression of unwrap_pattern(node)) { for (const expression of unwrap_pattern(node)) {
const left = object(expression); const left = object(expression);
const binding = left && scope.get(left.name); const binding = left && scope.get(left.name);
@ -1328,6 +1337,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (binding !== null && left !== binding.node) { if (binding !== null && left !== binding.node) {
if (left === expression) { if (left === expression) {
binding.reassigned = true; binding.reassigned = true;
binding.assignments.push({ value, scope });
} else { } else {
binding.mutated = true; binding.mutated = true;
} }

@ -67,15 +67,15 @@ export default test({
assert.deepEqual(logs, [ assert.deepEqual(logs, [
'outside boundary 1', 'outside boundary 1',
'template 42 1',
'$effect.pre 42 1', '$effect.pre 42 1',
'$effect 42 1', '$effect 42 1',
'template 84 2', 'template 42 1',
'$effect.pre 84 2', '$effect.pre 84 2',
'template 84 2',
'outside boundary 2', 'outside boundary 2',
'$effect 84 2', '$effect 84 2',
'template 86 2',
'$effect.pre 86 2', '$effect.pre 86 2',
'template 86 2',
'$effect 86 2' '$effect 86 2'
]); ]);
} }

@ -33,15 +33,15 @@ export default test({
assert.deepEqual(logs, [ assert.deepEqual(logs, [
'outside boundary 1', 'outside boundary 1',
'template 1a 1',
'$effect.pre 1a 1', '$effect.pre 1a 1',
'$effect 1a 1', '$effect 1a 1',
'template 2a 2', 'template 1a 1',
'$effect.pre 2a 2', '$effect.pre 2a 2',
'template 2a 2',
'outside boundary 2', 'outside boundary 2',
'$effect 2a 2', '$effect 2a 2',
'template 2b 2',
'$effect.pre 2b 2', '$effect.pre 2b 2',
'template 2b 2',
'$effect 2b 2' '$effect 2b 2'
]); ]);
} }

Loading…
Cancel
Save