fix: handle derived destructured iterators (#16015)

* revert #15813

* failing test

* tweak

* tweak

* WIP

* WIP

* WIP

* WIP

* WIP

* working

* tweak

* changeset

* tweak description

* Update packages/svelte/src/compiler/utils/ast.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/16020/head
Rich Harris 6 months ago committed by GitHub
parent 2e27c5d8aa
commit 81a34aa134
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle derived destructured iterators

@ -603,15 +603,15 @@ const instance_script = {
); );
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// const tmp = state.scope.generate('tmp'); // const tmp = b.id(state.scope.generate('tmp'));
// const paths = extract_paths(declarator.id); // const paths = extract_paths(declarator.id, tmp);
// state.props_pre.push( // state.props_pre.push(
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression) // b.declaration('const', tmp, visit(declarator.init!) as Expression)
// ); // );
// for (const path of paths) { // for (const path of paths) {
// const name = (path.node as Identifier).name; // const name = (path.node as Identifier).name;
// const binding = state.scope.get(name)!; // const binding = state.scope.get(name)!;
// const value = path.expression!(b.id(tmp)); // const value = path.expression;
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') { // if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
// state.props.push({ // state.props.push({
// local: name, // local: name,

@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js'; import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js'; import { equal } from '../../../utils/assert.js';
import * as b from '#compiler/builders';
/** /**
* @param {VariableDeclarator} node * @param {VariableDeclarator} node
@ -18,7 +19,7 @@ export function VariableDeclarator(node, context) {
if (context.state.analysis.runes) { if (context.state.analysis.runes) {
const init = node.init; const init = node.init;
const rune = get_rune(init, context.state.scope); const rune = get_rune(init, context.state.scope);
const paths = extract_paths(node.id); const { paths } = extract_paths(node.id, b.id('dummy'));
for (const path of paths) { for (const path of paths) {
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name)); validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));

@ -234,13 +234,21 @@ export function EachBlock(node, context) {
} else if (node.context) { } else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item; const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) { const { inserts, paths } = extract_paths(node.context, unwrapped);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
child_state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value), child_state));
declarations.push(b.var(id, b.call('$.derived', expression)));
}
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk( const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
);
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
@ -249,7 +257,7 @@ export function EachBlock(node, context) {
child_state.transform[name] = { child_state.transform[name] = {
read, read,
assign: (_, value) => { assign: (_, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped)); const left = /** @type {Pattern} */ (path.update_expression);
return b.sequence([b.assignment('=', left, value), ...sequence]); return b.sequence([b.assignment('=', left, value), ...sequence]);
}, },
mutate: (_, mutation) => { mutate: (_, mutation) => {

@ -43,14 +43,21 @@ export function SnippetBlock(node, context) {
let arg_alias = `$$arg${i}`; let arg_alias = `$$arg${i}`;
args.push(b.id(arg_alias)); args.push(b.id(arg_alias));
const paths = extract_paths(argument); const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias)));
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
transform[id.name] = { read: get_value };
declarations.push(
b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))))
);
}
for (const path of paths) { for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk( const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression)));
/** @type {Expression} */ (context.visit(path.expression?.(b.maybe_call(b.id(arg_alias)))))
);
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));

@ -2,12 +2,13 @@
/** @import { Binding } from '#compiler' */ /** @import { Binding } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js'; import { dev } from '../../../../state.js';
import { build_pattern, extract_paths } from '../../../../utils/ast.js'; import { extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js'; import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/** /**
* @param {VariableDeclaration} node * @param {VariableDeclaration} node
@ -116,7 +117,7 @@ export function VariableDeclaration(node, context) {
} }
const args = /** @type {CallExpression} */ (init).arguments; const args = /** @type {CallExpression} */ (init).arguments;
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
if (rune === '$state' || rune === '$state.raw') { if (rune === '$state' || rune === '$state.raw') {
/** /**
@ -137,24 +138,34 @@ export function VariableDeclaration(node, context) {
}; };
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
const expression = /** @type {Expression} */ (context.visit(value));
declarations.push( declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value)) b.declarator(declarator.id, create_state_declarator(declarator.id, expression))
); );
} else { } else {
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope); const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push( declarations.push(
b.declarator(pattern, value), b.declarator(tmp, value),
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map( ...inserts.map(({ id, value }) => {
([original, replacement]) => { id.name = context.state.scope.generate('$$array');
const binding = context.state.scope.get(original.name); context.state.transform[id.name] = { read: get_value };
return b.declarator(
original, const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
binding?.kind === 'state' || binding?.kind === 'raw_state' return b.declarator(id, b.call('$.derived', expression));
? create_state_declarator(binding.node, replacement) }),
: replacement ...paths.map((path) => {
); const value = /** @type {Expression} */ (context.visit(path.expression));
} const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
) return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, value)
: value
);
})
); );
} }
@ -163,44 +174,41 @@ export function VariableDeclaration(node, context) {
if (rune === '$derived' || rune === '$derived.by') { if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
declarations.push( let expression = /** @type {Expression} */ (context.visit(value));
b.declarator( if (rune === '$derived') expression = b.thunk(expression);
declarator.id,
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)) declarations.push(b.declarator(declarator.id, b.call('$.derived', expression)));
)
);
} else { } else {
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
const init = /** @type {CallExpression} */ (declarator.init); const init = /** @type {CallExpression} */ (declarator.init);
/** @type {Identifier} */
let id;
let rhs = value; let rhs = value;
if (rune === '$derived' && init.arguments[0].type === 'Identifier') { if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
id = init.arguments[0]; const id = b.id(context.state.scope.generate('$$d'));
} else {
id = b.id(context.state.scope.generate('$$d'));
rhs = b.call('$.get', id); rhs = b.call('$.get', id);
declarations.push( let expression = /** @type {Expression} */ (context.visit(value));
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))) if (rune === '$derived') expression = b.thunk(expression);
);
declarations.push(b.declarator(id, b.call('$.derived', expression)));
} }
for (let i = 0; i < replacements.size; i++) { const { inserts, paths } = extract_paths(declarator.id, rhs);
const [original, replacement] = [...replacements][i];
declarations.push( for (const { id, value } of inserts) {
b.declarator( id.name = context.state.scope.generate('$$array');
original, context.state.transform[id.name] = { read: get_value };
b.call(
'$.derived', const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
b.arrow([], b.block([b.let(pattern, rhs), b.return(replacement)])) declarations.push(b.declarator(id, b.call('$.derived', expression)));
) }
)
); for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression))));
} }
} }
continue; continue;
} }
} }
@ -229,20 +237,29 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type !== 'Identifier') { if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = context.state.scope.generate('tmp'); const tmp = b.id(context.state.scope.generate('tmp'));
const paths = extract_paths(declarator.id); const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push( declarations.push(
b.declarator( b.declarator(
b.id(tmp), tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init))) /** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
) )
); );
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}
for (const path of paths) { for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name)); const binding = /** @type {Binding} */ (context.state.scope.get(name));
const value = path.expression?.(b.id(tmp)); const value = /** @type {Expression} */ (context.visit(path.expression));
declarations.push( declarations.push(
b.declarator( b.declarator(
path.node, path.node,
@ -276,7 +293,7 @@ export function VariableDeclaration(node, context) {
declarations.push( declarations.push(
...create_state_declarators( ...create_state_declarators(
declarator, declarator,
context.state, context,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init)) /** @type {Expression} */ (declarator.init && context.visit(declarator.init))
) )
); );
@ -296,32 +313,41 @@ export function VariableDeclaration(node, context) {
/** /**
* Creates the output for a state declaration in legacy mode. * Creates the output for a state declaration in legacy mode.
* @param {VariableDeclarator} declarator * @param {VariableDeclarator} declarator
* @param {ComponentClientTransformState} scope * @param {ComponentContext} context
* @param {Expression} value * @param {Expression} value
*/ */
function create_state_declarators(declarator, { scope, analysis }, value) { function create_state_declarators(declarator, context, value) {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
return [ return [
b.declarator( b.declarator(
declarator.id, declarator.id,
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
) )
]; ];
} }
const [pattern, replacements] = build_pattern(declarator.id, scope); const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
return [ return [
b.declarator(pattern, value), b.declarator(tmp, value),
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map( ...inserts.map(({ id, value }) => {
([original, replacement]) => { id.name = context.state.scope.generate('$$array');
const binding = scope.get(original.name); context.state.transform[id.name] = { read: get_value };
return b.declarator(
original, const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
binding?.kind === 'state' return b.declarator(id, b.call('$.derived', expression));
? b.call('$.mutable_source', replacement, analysis.immutable ? b.true : undefined) }),
: replacement ...paths.map((path) => {
); const value = /** @type {Expression} */ (context.visit(path.expression));
} const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
)
return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
: value
);
})
]; ];
} }

@ -3,7 +3,7 @@
/** @import { Context } from '../types.js' */ /** @import { Context } from '../types.js' */
/** @import { ComponentAnalysis } from '../../../types.js' */ /** @import { ComponentAnalysis } from '../../../types.js' */
/** @import { Scope } from '../../../scope.js' */ /** @import { Scope } from '../../../scope.js' */
import { build_pattern, build_fallback, extract_paths } from '../../../../utils/ast.js'; import { build_fallback, extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -120,21 +120,29 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type !== 'Identifier') { if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = .. // Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z. // means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = context.state.scope.generate('tmp'); const tmp = b.id(context.state.scope.generate('tmp'));
const paths = extract_paths(declarator.id); const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push( declarations.push(
b.declarator( b.declarator(
b.id(tmp), tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init))) /** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
) )
); );
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
declarations.push(b.declarator(id, value));
}
for (const path of paths) { for (const path of paths) {
const value = path.expression?.(b.id(tmp)); const value = path.expression;
const name = /** @type {Identifier} */ (path.node).name; const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name)); const binding = /** @type {Binding} */ (context.state.scope.get(name));
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true); const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
declarations.push(b.declarator(path.node, build_fallback(prop, value))); declarations.push(b.declarator(path.node, build_fallback(prop, value)));
} }
continue; continue;
} }
@ -188,10 +196,13 @@ function create_state_declarators(declarator, scope, value) {
return [b.declarator(declarator.id, value)]; return [b.declarator(declarator.id, value)];
} }
const [pattern, replacements] = build_pattern(declarator.id, scope); const tmp = b.id(scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp);
return [ return [
b.declarator(pattern, value), b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below
// TODO inject declarator for opts, so we can use it below ...paths.map((path) => {
...[...replacements].map(([original, replacement]) => b.declarator(original, replacement)) const value = path.expression;
return b.declarator(path.node, value);
})
]; ];
} }

@ -1,8 +1,9 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Node, Pattern } from 'estree' */ /** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern, Statement } from 'estree' */
/** @import { Context as ClientContext } from '../client/types.js' */ /** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */ /** @import { Context as ServerContext } from '../server/types.js' */
import { build_pattern, is_expression_async } from '../../../utils/ast.js'; import { extract_paths, is_expression_async } from '../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_value } from '../client/visitors/shared/declarations.js';
/** /**
* @template {ClientContext | ServerContext} Context * @template {ClientContext | ServerContext} Context
@ -23,23 +24,27 @@ export function visit_assignment_expression(node, context, build_assignment) {
let changed = false; let changed = false;
const [pattern, replacements] = build_pattern(node.left, context.state.scope); const { inserts, paths } = extract_paths(node.left, rhs);
const assignments = [ for (const { id } of inserts) {
b.let(pattern, rhs), id.name = context.state.scope.generate('$$array');
...[...replacements].map(([original, replacement]) => { }
let assignment = build_assignment(node.operator, original, replacement, context);
if (assignment !== null) changed = true; const assignments = paths.map((path) => {
return b.stmt( const value = path.expression;
assignment ??
b.assignment( let assignment = build_assignment('=', path.node, value, context);
node.operator, if (assignment !== null) changed = true;
/** @type {Identifier} */ (context.visit(original)),
/** @type {Expression} */ (context.visit(replacement)) return (
) assignment ??
); b.assignment(
}) '=',
]; /** @type {Pattern} */ (context.visit(path.node)),
/** @type {Expression} */ (context.visit(value))
)
);
});
if (!changed) { if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment // No change to output -> nothing to transform -> we can keep the original assignment
@ -47,36 +52,36 @@ export function visit_assignment_expression(node, context, build_assignment) {
} }
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
const block = b.block(assignments);
if (!is_standalone) { if (inserts.length > 0 || should_cache) {
// this is part of an expression, we need the sequence to end with the value /** @type {Statement[]} */
block.body.push(b.return(rhs)); const statements = [
} ...inserts.map(({ id, value }) => b.var(id, value)),
...assignments.map(b.stmt)
];
if (is_standalone && !should_cache) { if (!is_standalone) {
return block; // this is part of an expression, we need the sequence to end with the value
statements.push(b.return(rhs));
}
const iife = b.arrow([rhs], b.block(statements));
const iife_is_async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
} }
const iife = b.arrow(should_cache ? [rhs] : [], block); const sequence = b.sequence(assignments);
const iife_is_async = if (!is_standalone) {
is_expression_async(value) || // this is part of an expression, we need the sequence to end with the value
assignments.some( sequence.expressions.push(rhs);
(assignment) => }
(assignment.type === 'ExpressionStatement' &&
is_expression_async(assignment.expression)) ||
(assignment.type === 'VariableDeclaration' &&
assignment.declarations.some(
(declaration) =>
is_expression_async(declaration.id) ||
(declaration.init && is_expression_async(declaration.init))
))
);
return iife_is_async return sequence;
? b.await(b.call(b.async(iife), should_cache ? value : undefined))
: b.call(iife, should_cache ? value : undefined);
} }
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {

@ -630,10 +630,9 @@ export class Scope {
/** /**
* @param {string} preferred_name * @param {string} preferred_name
* @param {(name: string, counter: number) => string} [generator]
* @returns {string} * @returns {string}
*/ */
generate(preferred_name, generator = (name, counter) => `${name}_${counter}`) { generate(preferred_name) {
if (this.#porous) { if (this.#porous) {
return /** @type {Scope} */ (this.parent).generate(preferred_name); return /** @type {Scope} */ (this.parent).generate(preferred_name);
} }
@ -648,7 +647,7 @@ export class Scope {
this.root.conflicts.has(name) || this.root.conflicts.has(name) ||
is_reserved(name) is_reserved(name)
) { ) {
name = generator(preferred_name, n++); name = `${preferred_name}_${n++}`;
} }
this.references.set(name, []); this.references.set(name, []);

@ -1,8 +1,7 @@
/** @import { AST, Scope } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import * as ESTree from 'estree' */ /** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import is_reference from 'is-reference';
/** /**
* Gets the left-most identifier of a member expression or identifier. * Gets the left-most identifier of a member expression or identifier.
@ -129,49 +128,6 @@ export function unwrap_pattern(pattern, nodes = []) {
return nodes; return nodes;
} }
/**
* @param {ESTree.Pattern} id
* @param {Scope} scope
* @returns {[ESTree.Pattern, Map<ESTree.Identifier | ESTree.MemberExpression, ESTree.Identifier>]}
*/
export function build_pattern(id, scope) {
/** @type {Map<ESTree.Identifier | ESTree.MemberExpression, ESTree.Identifier>} */
const map = new Map();
/** @type {Map<string, string>} */
const names = new Map();
let counter = 0;
for (const node of unwrap_pattern(id)) {
const name = scope.generate(`$$${++counter}`, (_, counter) => `$$${counter}`);
map.set(node, b.id(name));
if (node.type === 'Identifier') {
names.set(node.name, name);
}
}
const pattern = walk(id, null, {
Identifier(node, context) {
if (is_reference(node, /** @type {ESTree.Pattern} */ (context.path.at(-1)))) {
const name = names.get(node.name);
if (name) return b.id(name);
}
},
MemberExpression(node, context) {
const n = map.get(node);
if (n) return n;
context.next();
}
});
return [pattern, map];
}
/** /**
* Extracts all identifiers from a pattern. * Extracts all identifiers from a pattern.
* @param {ESTree.Pattern} pattern * @param {ESTree.Pattern} pattern
@ -271,40 +227,50 @@ export function extract_identifiers_from_destructuring(node, nodes = []) {
* @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions * @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions
* @property {boolean} is_rest `true` if this is a `...rest` destructuring * @property {boolean} is_rest `true` if this is a `...rest` destructuring
* @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..` * @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..`
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} expression Returns an expression which walks the path starting at the given expression. * @property {ESTree.Expression} expression The value of the current path
* This will be a call expression if a rest element or default is involved e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` since we can't represent `baz` or `rest` purely as a path * This will be a call expression if a rest element or default is involved e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` since we can't represent `baz` or `rest` purely as a path
* Will be an await expression in case of an async default value (`const { foo = await bar } = ...`) * Will be an await expression in case of an async default value (`const { foo = await bar } = ...`)
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} update_expression Like `expression` but without default values. * @property {ESTree.Expression} update_expression Like `expression` but without default values.
*/ */
/** /**
* Extracts all destructured assignments from a pattern. * Extracts all destructured assignments from a pattern.
* For each `id` in the returned `inserts`, make sure to adjust the `name`.
* @param {ESTree.Node} param * @param {ESTree.Node} param
* @returns {DestructuredAssignment[]} * @param {ESTree.Expression} initial
* @returns {{ inserts: Array<{ id: ESTree.Identifier, value: ESTree.Expression }>, paths: DestructuredAssignment[] }}
*/ */
export function extract_paths(param) { export function extract_paths(param, initial) {
return _extract_paths( /**
[], * When dealing with array destructuring patterns (`let [a, b, c] = $derived(blah())`)
param, * we need an intermediate declaration that creates an array, since `blah()` could
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node), * return a non-array-like iterator
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node), * @type {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>}
false */
); const inserts = [];
/** @type {DestructuredAssignment[]} */
const paths = [];
_extract_paths(paths, inserts, param, initial, initial, false);
return { inserts, paths };
} }
/** /**
* @param {DestructuredAssignment[]} assignments * @param {DestructuredAssignment[]} paths
* @param {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>} inserts
* @param {ESTree.Node} param * @param {ESTree.Node} param
* @param {DestructuredAssignment['expression']} expression * @param {ESTree.Expression} expression
* @param {DestructuredAssignment['update_expression']} update_expression * @param {ESTree.Expression} update_expression
* @param {boolean} has_default_value * @param {boolean} has_default_value
* @returns {DestructuredAssignment[]} * @returns {DestructuredAssignment[]}
*/ */
function _extract_paths(assignments = [], param, expression, update_expression, has_default_value) { function _extract_paths(paths, inserts, param, expression, update_expression, has_default_value) {
switch (param.type) { switch (param.type) {
case 'Identifier': case 'Identifier':
case 'MemberExpression': case 'MemberExpression':
assignments.push({ paths.push({
node: param, node: param,
is_rest: false, is_rest: false,
has_default_value, has_default_value,
@ -316,28 +282,25 @@ function _extract_paths(assignments = [], param, expression, update_expression,
case 'ObjectPattern': case 'ObjectPattern':
for (const prop of param.properties) { for (const prop of param.properties) {
if (prop.type === 'RestElement') { if (prop.type === 'RestElement') {
/** @type {DestructuredAssignment['expression']} */ /** @type {ESTree.Expression[]} */
const rest_expression = (object) => { const props = [];
/** @type {ESTree.Expression[]} */
const props = []; for (const p of param.properties) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
for (const p of param.properties) { if (p.key.type === 'Identifier' && !p.computed) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') { props.push(b.literal(p.key.name));
if (p.key.type === 'Identifier' && !p.computed) { } else if (p.key.type === 'Literal') {
props.push(b.literal(p.key.name)); props.push(b.literal(String(p.key.value)));
} else if (p.key.type === 'Literal') { } else {
props.push(b.literal(String(p.key.value))); props.push(b.call('String', p.key));
} else {
props.push(b.call('String', p.key));
}
} }
} }
}
return b.call('$.exclude_from_object', expression(object), b.array(props)); const rest_expression = b.call('$.exclude_from_object', expression, b.array(props));
};
if (prop.argument.type === 'Identifier') { if (prop.argument.type === 'Identifier') {
assignments.push({ paths.push({
node: prop.argument, node: prop.argument,
is_rest: true, is_rest: true,
has_default_value, has_default_value,
@ -346,7 +309,8 @@ function _extract_paths(assignments = [], param, expression, update_expression,
}); });
} else { } else {
_extract_paths( _extract_paths(
assignments, paths,
inserts,
prop.argument, prop.argument,
rest_expression, rest_expression,
rest_expression, rest_expression,
@ -354,11 +318,15 @@ function _extract_paths(assignments = [], param, expression, update_expression,
); );
} }
} else { } else {
/** @type {DestructuredAssignment['expression']} */ const object_expression = b.member(
const object_expression = (object) => expression,
b.member(expression(object), prop.key, prop.computed || prop.key.type !== 'Identifier'); prop.key,
prop.computed || prop.key.type !== 'Identifier'
);
_extract_paths( _extract_paths(
assignments, paths,
inserts,
prop.value, prop.value,
object_expression, object_expression,
object_expression, object_expression,
@ -369,16 +337,27 @@ function _extract_paths(assignments = [], param, expression, update_expression,
break; break;
case 'ArrayPattern': case 'ArrayPattern': {
// we create an intermediate declaration to convert iterables to arrays if necessary.
// the consumer is responsible for setting the name of the identifier
const id = b.id('#');
const value = b.call(
'$.to_array',
expression,
param.elements.at(-1)?.type === 'RestElement' ? undefined : b.literal(param.elements.length)
);
inserts.push({ id, value });
for (let i = 0; i < param.elements.length; i += 1) { for (let i = 0; i < param.elements.length; i += 1) {
const element = param.elements[i]; const element = param.elements[i];
if (element) { if (element) {
if (element.type === 'RestElement') { if (element.type === 'RestElement') {
/** @type {DestructuredAssignment['expression']} */ const rest_expression = b.call(b.member(id, 'slice'), b.literal(i));
const rest_expression = (object) =>
b.call(b.member(expression(object), 'slice'), b.literal(i));
if (element.argument.type === 'Identifier') { if (element.argument.type === 'Identifier') {
assignments.push({ paths.push({
node: element.argument, node: element.argument,
is_rest: true, is_rest: true,
has_default_value, has_default_value,
@ -387,7 +366,8 @@ function _extract_paths(assignments = [], param, expression, update_expression,
}); });
} else { } else {
_extract_paths( _extract_paths(
assignments, paths,
inserts,
element.argument, element.argument,
rest_expression, rest_expression,
rest_expression, rest_expression,
@ -395,10 +375,11 @@ function _extract_paths(assignments = [], param, expression, update_expression,
); );
} }
} else { } else {
/** @type {DestructuredAssignment['expression']} */ const array_expression = b.member(id, b.literal(i), true);
const array_expression = (object) => b.member(expression(object), b.literal(i), true);
_extract_paths( _extract_paths(
assignments, paths,
inserts,
element, element,
array_expression, array_expression,
array_expression, array_expression,
@ -409,13 +390,13 @@ function _extract_paths(assignments = [], param, expression, update_expression,
} }
break; break;
}
case 'AssignmentPattern': { case 'AssignmentPattern': {
/** @type {DestructuredAssignment['expression']} */ const fallback_expression = build_fallback(expression, param.right);
const fallback_expression = (object) => build_fallback(expression(object), param.right);
if (param.left.type === 'Identifier') { if (param.left.type === 'Identifier') {
assignments.push({ paths.push({
node: param.left, node: param.left,
is_rest: false, is_rest: false,
has_default_value: true, has_default_value: true,
@ -423,14 +404,14 @@ function _extract_paths(assignments = [], param, expression, update_expression,
update_expression update_expression
}); });
} else { } else {
_extract_paths(assignments, param.left, fallback_expression, update_expression, true); _extract_paths(paths, inserts, param.left, fallback_expression, update_expression, true);
} }
break; break;
} }
} }
return assignments; return paths;
} }
/** /**

@ -154,7 +154,7 @@ export {
} from './dom/operations.js'; } from './dom/operations.js';
export { attr, clsx } from '../shared/attributes.js'; export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js'; export { noop, fallback, to_array } from '../shared/utils.js';
export { export {
invalid_default_snippet, invalid_default_snippet,
validate_dynamic_element_tag, validate_dynamic_element_tag,

@ -504,7 +504,7 @@ export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';
export { fallback } from '../shared/utils.js'; export { fallback, to_array } from '../shared/utils.js';
export { export {
invalid_default_snippet, invalid_default_snippet,

@ -81,3 +81,38 @@ export function fallback(value, fallback, lazy = false) {
: /** @type {V} */ (fallback) : /** @type {V} */ (fallback)
: value; : value;
} }
/**
* When encountering a situation like `let [a, b, c] = $derived(blah())`,
* we need to stash an intermediate value that `a`, `b`, and `c` derive
* from, in case it's an iterable
* @template T
* @param {ArrayLike<T> | Iterable<T>} value
* @param {number} [n]
* @returns {Array<T>}
*/
export function to_array(value, n) {
// return arrays unchanged
if (Array.isArray(value)) {
return value;
}
// if value is not iterable, or `n` is unspecified (indicates a rest
// element, which means we're not concerned about unbounded iterables)
// convert to an array with `Array.from`
if (n === undefined || !(Symbol.iterator in value)) {
return Array.from(value);
}
// otherwise, populate an array with `n` values
/** @type {T[]} */
const array = [];
for (const element of value) {
array.push(element);
if (array.length === n) break;
}
return array;
}

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>increment</button><p>a: 1</p><p>b: 2</p><p>c: 3</p>`,
test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.htmlEqual(
target.innerHTML,
`<button>increment</button><p>a: 2</p><p>b: 3</p><p>c: 4</p>`
);
}
});

@ -0,0 +1,16 @@
<script>
let offset = $state(1);
function* count(offset) {
let i = offset;
while (true) yield i++;
}
let [a, b, c] = $derived(count(offset));
</script>
<button onclick={() => offset += 1}>increment</button>
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>

@ -7,12 +7,12 @@ let c = 3;
let d = 4; let d = 4;
export function update(array) { export function update(array) {
{ ((array) => {
let [$$1, $$2] = array; var $$array = $.to_array(array, 2);
$.set(a, $$1, true); $.set(a, $$array[0], true);
$.set(b, $$2, true); $.set(b, $$array[1], true);
}; })(array);
[c, d] = array; [c, d] = array;
} }
Loading…
Cancel
Save