scopes come from the parser's tables: a script is no longer walked, a template's javascript neither, and a reference finds its ancestors through the parser's parent links

goodbye-acorn
Nic 2 days ago
parent b7451891c2
commit 69b0bcd0c8

@ -746,11 +746,12 @@ const instance_script = {
// Analyze declaration bindings to see if they're exclusively updated within a single reactive statement
const possible_derived = bindings.every((binding) =>
binding.references.every((reference) => {
const declaration = reference.path.find((el) => el.type === 'VariableDeclaration');
const assignment = reference.path.find((el) => el.type === 'AssignmentExpression');
const update = reference.path.find((el) => el.type === 'UpdateExpression');
const path = reference.path;
const declaration = path.find((el) => el.type === 'VariableDeclaration');
const assignment = path.find((el) => el.type === 'AssignmentExpression');
const update = path.find((el) => el.type === 'UpdateExpression');
const labeled = /** @type {LabeledStatement | undefined} */ (
reference.path.find((el) => el.type === 'LabeledStatement' && el.label.name === '$')
path.find((el) => el.type === 'LabeledStatement' && el.label.name === '$')
);
if (

@ -4,6 +4,7 @@
import * as teasel from '@teasel/parser';
import * as e from '../../errors.js';
import { find_matching_bracket } from './utils/bracket.js';
import { parsed } from '../../utils/ast.js';
/**
* A standalone module, as `analyze_module` reads one.
@ -28,12 +29,23 @@ export function parse(source, comments, typescript) {
add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments));
delete ast.comments;
delete ast.scopes;
delete ast.bindings;
parsed.set(ast, tables(ast));
return ast;
}
/**
* Takes the tables off an answer, so they reach the scope analysis without showing in the tree.
* @param {{ scopes?: any; bindings?: any; references?: any }} answer
*/
function tables(answer) {
const { scopes, bindings, references } = answer;
delete answer.scopes;
delete answer.bindings;
delete answer.references;
return { scopes, bindings, references };
}
/**
* The program inside a `<script>`, with the positions of the whole template.
* @param {Parser} parser
@ -55,8 +67,7 @@ export function parse_script(parser, start, end) {
/** @type {teasel.Comment[]} */ (ast.comments)
);
delete ast.comments;
delete ast.scopes;
delete ast.bindings;
parsed.set(ast, tables(ast));
unsupported(ast.typescript);
delete ast.typescript;
@ -101,7 +112,9 @@ export function read_expression(parser, until, opening_token = '{') {
const start = parser.index;
try {
return read(parser, (js) => js.parseExpressionAt(start, until)).node;
const answer = read(parser, (js) => js.parseExpressionAt(start, until));
parsed.set(answer.node, tables(answer));
return answer.node;
} catch (err) {
if (parser.loose) {
// Find the next } and treat it as the end of the expression
@ -143,7 +156,9 @@ export function read_pattern(parser) {
}
}
return read(parser, (js) => js.parsePatternAt(start)).node;
const answer = read(parser, (js) => js.parsePatternAt(start));
parsed.set(answer.node, tables(answer));
return answer.node;
}
/**
@ -152,7 +167,9 @@ export function read_pattern(parser) {
*/
export function read_params(parser) {
const start = parser.index;
return read(parser, (js) => js.parseParamsAt(start)).params;
const answer = read(parser, (js) => js.parseParamsAt(start));
parsed.set(answer.params, tables(answer));
return answer.params;
}
/**
@ -162,7 +179,7 @@ export function read_params(parser) {
export function read_statement(parser) {
const start = parser.index;
return read(parser, (js) => {
const answer = read(parser, (js) => {
try {
return js.parseStatementAt(start);
} catch (err) {
@ -170,7 +187,9 @@ export function read_statement(parser) {
e.unexpected_eof(parser.template.length);
throw err;
}
}).node;
});
parsed.set(answer.node, tables(answer));
return answer.node;
}
/** What erasure leaves in place needs a compiler, not this one */

@ -379,22 +379,13 @@ export function analyze_component(root, source, options) {
))
) {
let is_nested_store_subscription_node = undefined;
search: for (const reference of references) {
for (let i = reference.path.length - 1; i >= 0; i--) {
const scope =
scopes.get(reference.path[i]) ||
module.scopes.get(reference.path[i]) ||
instance.scopes.get(reference.path[i]);
if (scope) {
const owner = scope?.owner(store_name);
for (const reference of references) {
const owner = reference.scope.owner(store_name);
if (!!owner && owner !== module.scope && owner !== instance.scope) {
is_nested_store_subscription_node = reference.node;
break search;
}
break;
}
}
}
if (is_nested_store_subscription_node) {
e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
@ -635,7 +626,7 @@ export function analyze_component(root, source, options) {
if (
path[path.length - 1].type === 'StyleDirective' ||
path.some((node) => node.type === 'Fragment') ||
(path[1].type === 'LabeledStatement' && path[1].label.name === '$')
(path[1]?.type === 'LabeledStatement' && path[1].label.name === '$')
) {
binding.kind = 'state';
}

@ -1,6 +1,7 @@
/** @import { Expression, LabeledStatement } from 'estree' */
/** @import { AST, ReactiveStatement } from '#compiler' */
/** @import { Context } from '../types' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { extract_identifiers, object } from '../../../utils/ast.js';
import * as w from '../../../warnings.js';
@ -36,28 +37,13 @@ export function LabeledStatement(node, context) {
// Every referenced binding becomes a dependency, unless it's on
// the left-hand side of an `=` assignment
const assigned = assigned_in(node.body);
for (const [name, nodes] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding === null) continue;
for (const { node, path } of nodes) {
/** @type {Expression} */
let left = node;
let i = path.length - 1;
let parent = /** @type {Expression} */ (path.at(i));
while (parent.type === 'MemberExpression') {
left = parent;
parent = /** @type {Expression} */ (path.at(--i));
}
if (
parent.type === 'AssignmentExpression' &&
parent.operator === '=' &&
parent.left === left
) {
continue;
}
for (const { node } of nodes) {
if (assigned.has(node)) continue;
reactive_statement.dependencies.push(binding);
break;
@ -93,3 +79,29 @@ export function LabeledStatement(node, context) {
context.next();
}
/**
* The identifiers on the left-hand side of the `=` assignments in a statement, the members of
* the target included: `a[b].c = 1` assigns through `a` and `b`.
* @param {import('estree').Node} node
* @returns {Set<import('estree').Node>}
*/
function assigned_in(node) {
/** @type {Set<import('estree').Node>} */
const assigned = new Set();
/** @param {import('estree').Node} target */
const collect = (target) => {
if (target.type === 'Identifier') assigned.add(target);
else if (target.type === 'MemberExpression') {
collect(target.object);
collect(target.property);
}
};
walk(node, null, {
AssignmentExpression(node, { next }) {
if (node.operator === '=') collect(node.left);
next();
}
});
return assigned;
}

@ -1,13 +1,14 @@
/** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression } from 'estree' */
/** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression, Program } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import { walk } from 'zimmerframe';
import { ExpressionMetadata } from './nodes.js';
import * as b from '#compiler/builders';
import * as e from '../errors.js';
import { bindingOf, referenceOf, scopeOf } from '@teasel/parser';
import { bindingOf, parentOf } from '@teasel/parser';
import {
extract_identifiers,
parsed,
extract_identifiers_from_destructuring,
is_reference,
object,
@ -86,6 +87,44 @@ const global_constants = {
'Math.SQRT1_2': Math.SQRT1_2
};
/** A reference an identifier makes: its ancestors, and the scope it is made from. */
export class Reference {
/** @type {Identifier} */
node;
/** @type {Scope} */
scope;
/** @type {AST.SvelteNode[] | undefined} */
#path;
/** @type {AST.SvelteNode[]} the template's ancestors, above the JavaScript the identifier sits in */
#template;
/**
* @param {Identifier} node
* @param {Scope} scope
* @param {AST.SvelteNode[]} path the ancestors, or the template's alone when the identifier
* comes from the parser, whose parent links supply the rest
* @param {boolean} [complete]
*/
constructor(node, scope, path, complete = true) {
this.node = node;
this.scope = scope;
this.#template = path;
if (complete) this.#path = path;
}
/** The ancestors of the identifier, outermost first: the template's, then the JavaScript's. */
get path() {
if (this.#path === undefined) {
const inside = [];
for (let parent = parentOf(this.node); parent !== undefined; parent = parentOf(parent)) {
inside.push(/** @type {AST.SvelteNode} */ (parent));
}
this.#path = this.#template.concat(inside.reverse());
}
return this.#path;
}
}
export class Binding {
/** @type {Scope} */
scope;
@ -106,7 +145,7 @@ export class Binding {
*/
initial = null;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
/** @type {Reference[]} */
references = [];
/**
@ -635,7 +674,7 @@ export class Scope {
/**
* A set of all the names referenced with this scope
* useful for generating unique names
* @type {Map<string, { node: Identifier; path: AST.SvelteNode[] }[]>}
* @type {Map<string, Reference[]>}
*/
references = new Map();
@ -772,25 +811,26 @@ export class Scope {
/**
* @param {Identifier} node
* @param {AST.SvelteNode[]} path
* @param {AST.SvelteNode[]} path the ancestors, or the template's alone for an identifier the
* parser knows the parents of
* @param {Binding | null | undefined} [binding] what the parser resolved the reference to
* @param {Reference} [reference] the record, once the walk up made it
* @returns {Binding | null} what the reference resolved to; null for a global
*/
reference(node, path, binding = undefined) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference
reference(node, path, binding = undefined, reference = new Reference(node, this, path)) {
let references = this.references.get(node.name);
if (!references) this.references.set(node.name, (references = []));
references.push({ node, path });
references.push(reference);
// the parser resolved the reference already, or the name walks up
binding ??= this.declarations.get(node.name);
if (binding !== undefined && binding !== null && binding.scope === this) {
binding.references.push({ node, path });
binding.references.push(reference);
return binding;
}
if (this.parent) return this.parent.reference(node, path, binding);
if (this.parent) return this.parent.reference(node, path, binding, reference);
// no binding was found, and this is the top level scope,
// which means this is a global
this.root.conflicts.add(node.name);
@ -936,7 +976,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {State} */
const state = { scope };
/** @type {[Scope, { node: Identifier; path: AST.SvelteNode[] }][]} */
/**
* Every reference with the scope it is made from, and what the parser knows of it.
* @type {[Scope, Reference, Binding | undefined, import('@teasel/parser').Reference | undefined][]}
*/
const references = [];
/** @type {[Scope, Pattern | MemberExpression, Expression][]} */
@ -956,8 +999,9 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
* @param {Scope} scope
* @param {import('@teasel/parser').Scope} parsed
* @param {boolean} params
* @param {boolean} [template] the declarations are a const tag's
*/
function declare_parsed(scope, parsed, params) {
function declare_parsed(scope, parsed, params, template = false) {
for (const binding of parsed.bindings) {
if ((binding.kind === 'param') !== params) continue;
// no node: `arguments`, or a declaration erased with the TypeScript it belonged to
@ -989,7 +1033,162 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (declaration?.type === 'FunctionDeclaration' || declaration?.type === 'ClassDeclaration') {
initial = declaration;
}
from_parser.set(binding, scope.declare(binding.node, 'normal', kind, initial));
const ours = scope.declare(binding.node, template ? 'template' : 'normal', kind, initial);
from_parser.set(binding, ours);
if (declaration?.type === 'VariableDeclarator') {
ours.metadata = { is_template_declaration: true };
declarators.push([declaration, ours]);
}
}
}
/**
* Each declarator with what it declares, for the scope at its own position: a `var` hoists
* past the block it sits in, and the block is the scope a visitor of the declaration sees.
* @type {[VariableDeclarator, Binding][]}
*/
const declarators = [];
/**
* The `$:` statements of an instance script, each with the scope it opens, for what sits in them.
* @type {[number, number, Scope][]}
*/
const labeled = [];
/**
* The scopes, declarations and references of one parsed answer, from the parser's tables: a
* script's program, or a template's expression, pattern, parameter list or declaration.
* @param {Scope} scope what the answer's outermost scope is here
* @param {NonNullable<ReturnType<typeof parsed.get>>} answer
* @param {AST.SvelteNode[]} path the answer's ancestors
* @param {boolean} template the answer's declarations are a const tag's
*/
function from_tables(scope, answer, path, template) {
const outermost = answer.scopes[0];
/** @type {Map<import('@teasel/parser').Scope, Scope>} the parser's scopes and ours; a function's holds its parameters */
const ours = new Map([[outermost, scope]]);
/** @type {Map<import('@teasel/parser').Scope, Scope>} a function's body, which holds the rest */
const bodies = new Map();
/**
* Our scope at a position inside the parser's: a function's body from where it starts, a
* `$:` statement's inside one.
* @param {import('@teasel/parser').Scope} scope
* @param {number} position
*/
function at(scope, position) {
const body = bodies.get(scope);
if (body !== undefined && position >= /** @type {any} */ (scope.node).body.start) return body;
if (scope === outermost) {
for (const [start, end, inside] of labeled) {
if (position >= start && position < end) return inside;
}
}
return /** @type {Scope} */ (ours.get(scope));
}
declare_parsed(scope, outermost, false, template);
for (let i = 1; i < answer.scopes.length; i++) {
const parsed = answer.scopes[i];
const node = /** @type {any} */ (parsed.node);
const parent = at(
/** @type {import('@teasel/parser').Scope} */ (parsed.parent),
node?.start ?? 0
);
switch (parsed.kind) {
case 'function': {
// the parameters live one above the body, which holds the non-porous function scope
const params = parent.child(true);
scopes.set(node, params);
ours.set(parsed, params);
if (parsed.parent?.kind === 'function-name') ours.set(parsed.parent, params);
declare_parsed(params, parsed, true);
if (
node.body.type !== 'BlockStatement' ||
(node.type === 'FunctionExpression' && node.id)
) {
declare_parsed(params, parsed, false);
}
if (node.body.type === 'BlockStatement') {
const body = params.child();
scopes.set(node.body, body);
bodies.set(parsed, body);
declare_parsed(body, parsed, false);
}
break;
}
case 'block':
case 'for':
case 'switch':
case 'catch':
case 'static-block': {
const inside = parent.child(true);
scopes.set(node, inside);
ours.set(parsed, inside);
declare_parsed(inside, parsed, false);
break;
}
default:
ours.set(parsed, parent);
}
}
/** @type {[Identifier, Scope, Binding | undefined, import('@teasel/parser').Reference | undefined][]} */
const entries = [];
// a declaring identifier counts as a reference to what it declares, as the walk had it;
// an import's did not, since the walk never entered an import declaration
for (const binding of answer.bindings) {
if (binding.node === null || binding.kind === 'import') continue;
const declared = from_parser.get(binding);
entries.push([
binding.node,
declared?.scope ?? at(binding.scope, /** @type {number} */ (binding.node.start)),
declared,
undefined
]);
}
for (const reference of answer.references) {
const binding = reference.binding === null ? undefined : from_parser.get(reference.binding);
entries.push([
reference.node,
at(reference.scope, /** @type {number} */ (reference.node.start)),
binding,
reference
]);
}
/**
* The innermost of the parser's scopes around a position, by the ranges of the nodes that open them.
* @param {number} position
*/
const innermost = (position) => {
let found = outermost;
for (const parsed of answer.scopes) {
const node = /** @type {any} */ (parsed.node);
if (
node &&
node.start <= position &&
position < node.end &&
parsed !== outermost &&
ours.has(parsed)
) {
const current = /** @type {any} */ (found.node);
if (!current || node.start >= current.start) found = parsed;
}
}
return found;
};
for (const [declarator, binding] of declarators.splice(0)) {
const position = /** @type {number} */ (declarator.start);
const here = at(innermost(position), position);
let declared = here.declarators.get(declarator);
if (!declared) here.declarators.set(declarator, (declared = []));
declared.push(binding);
}
entries.sort((a, b) => /** @type {number} */ (a[0].start) - /** @type {number} */ (b[0].start));
for (const [node, scope, binding, reference] of entries) {
references.push([scope, new Reference(node, scope, path, false), binding, reference]);
}
}
@ -1010,11 +1209,45 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
let has_await = false;
if (ast.type === 'Program') {
const parsed = scopeOf(ast);
if (parsed) {
declare_parsed(scope, parsed, false);
has_await = parsed.topLevelAwait;
// a script comes entirely from the parser's tables: nothing in it is walked
const program = ast.type === 'Program' ? parsed.get(ast) : undefined;
if (program !== undefined) {
has_await = program.scopes[0].topLevelAwait;
for (const node of /** @type {Program} */ (ast).body) {
if (
allow_reactive_declarations &&
node.type === 'LabeledStatement' &&
node.label.name === '$'
) {
// create a scope for the $: block
const inside = scope.child();
scopes.set(node, inside);
labeled.push([
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
inside
]);
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
for (const id of extract_identifiers(node.body.expression.left)) {
if (!id.name.startsWith('$')) {
possible_implicit_declarations.push(id);
}
}
}
}
}
from_tables(scope, program, [], false);
for (const node of /** @type {Program} */ (ast).body) {
if (node.type !== 'ImportDeclaration') continue;
// a type-only import binds nothing to the parser; the declaration is the initial value
for (const specifier of node.specifiers) {
const binding =
declared(specifier.local) ?? scope.declare(specifier.local, 'normal', 'import', node);
binding.initial = node;
}
}
}
@ -1079,82 +1312,47 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
};
if (program === undefined)
walk(ast, state, {
// the scopes JavaScript opens, and what they declare, as the parser found them
// the JavaScript of a template, parsed on its own: its scopes come from the parser's tables
_(node, context) {
const parsed = scopeOf(/** @type {any} */ (node));
const parent = context.path.at(-1);
if (parsed === undefined) {
const of_function =
node.type === 'BlockStatement' && parent && scopeOf(/** @type {any} */ (parent));
if (
of_function &&
(parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression')
) {
// the body holds the non-porous function scope; the parameters live one above
const scope = context.state.scope.child();
scopes.set(node, scope);
declare_parsed(scope, of_function, false);
return context.next({ scope });
}
return context.next();
}
switch (parsed.kind) {
case 'fragment':
// a template expression parsed on its own; an await in it runs when the template does
has_await ||= parsed.topLevelAwait;
return context.next();
case 'function': {
const scope = context.state.scope.child(true);
scopes.set(node, scope);
declare_parsed(scope, parsed, true);
if (node.type === 'FunctionExpression' && node.id) declare_parsed(scope, parsed, false);
else if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement')
declare_parsed(scope, parsed, false);
return context.next({ scope });
}
case 'block':
case 'for':
case 'switch':
case 'catch':
case 'static-block': {
const scope = context.state.scope.child(true);
scopes.set(node, scope);
declare_parsed(scope, parsed, false);
return context.next({ scope });
}
default:
return context.next();
}
const answer = parsed.get(node);
if (answer === undefined) return context.next();
has_await ||= answer.scopes[0].topLevelAwait;
from_tables(
context.state.scope,
answer,
context.path.slice(),
context.path.at(-1)?.type === 'ConstTag'
);
},
// an identifier the template built itself, a shorthand attribute's say, is no parser's
Identifier(node, { path, state }) {
if (is_reference(node, path.at(-1))) {
references.push([state.scope, { node, path: path.slice() }]);
references.push([
state.scope,
new Reference(node, state.scope, path.slice()),
undefined,
undefined
]);
}
},
LabeledStatement(node, { path, next }) {
if (path.length > 1 || !allow_reactive_declarations) return next();
if (node.label.name !== '$') return next();
// create a scope for the $: block
const scope = state.scope.child();
scopes.set(node, scope);
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
for (const id of extract_identifiers(node.body.expression.left)) {
if (!id.name.startsWith('$')) {
possible_implicit_declarations.push(id);
}
// a const tag's declaration is built around its parsed pattern and initializer, so the
// parser has no tables for it; its pattern and initializer are visited as usual
VariableDeclaration(node, { state, next }) {
for (const declarator of node.declarations) {
/** @type {Binding[]} */
const bindings = [];
state.scope.declarators.set(declarator, bindings);
for (const id of extract_identifiers(declarator.id)) {
const binding = state.scope.declare(id, 'template', node.kind, declarator.init);
binding.metadata = { is_template_declaration: true };
bindings.push(binding);
}
}
next({ scope });
next();
},
SvelteFragment,
@ -1196,40 +1394,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
SvelteSelf: Component,
SvelteComponent: Component,
ImportDeclaration(node, { state }) {
for (const specifier of node.specifiers) {
const binding =
declared(specifier.local) ??
state.scope.declare(specifier.local, 'normal', 'import', node);
binding.initial = node;
}
},
VariableDeclaration(node, { state, path, next }) {
const is_parent_const_tag = path.at(-1)?.type === 'ConstTag';
for (const declarator of node.declarations) {
/** @type {Binding[]} */
const bindings = [];
state.scope.declarators.set(declarator, bindings);
for (const id of extract_identifiers(declarator.id)) {
const binding =
declared(id) ??
state.scope.declare(
id,
is_parent_const_tag ? 'template' : 'normal',
node.kind,
declarator.init
);
binding.metadata = { is_template_declaration: true };
bindings.push(binding);
}
}
next();
},
EachBlock(node, { state, visit }) {
visit(node.expression);
@ -1342,7 +1506,11 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
}
context.next({ scope: child_scope });
const params = parsed.get(node.parameters);
if (params !== undefined) from_tables(child_scope, params, [...context.path, node], false);
for (const child of node.body.nodes) {
context.visit(child, { scope: child_scope });
}
},
Fragment: (node, context) => {
@ -1387,14 +1555,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// we do this after the fact, so that we don't need to worry
// about encountering references before their declarations
for (const [scope, { node, path }] of references) {
const binding = scope.reference(node, path, declared(node));
for (const [scope, reference, resolved, parsed] of references) {
const binding = scope.reference(reference.node, reference.path, resolved, reference);
// what the parser saw the identifier do; a declaring identifier is no reference to it
const parsed = referenceOf(node);
if (binding === null || parsed === undefined) continue;
if (parsed.write) {
binding.reassigned = true;
binding.assignments.push({ value: parsed.writeExpr ?? node, scope });
binding.assignments.push({ value: parsed.writeExpr ?? reference.node, scope });
}
if (parsed.mutate) binding.mutated = true;
}

@ -2,6 +2,13 @@
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import { bindingOf } from '@teasel/parser';
/**
* The parser's answer for each JavaScript root it parsed, a script's program or a template's
* expression, pattern, parameter list or declaration: its scope, binding and reference tables.
* @type {WeakMap<object, { scopes: import('@teasel/parser').Scope[]; bindings: import('@teasel/parser').Binding[]; references: import('@teasel/parser').Reference[] }>}
*/
export const parsed = new WeakMap();
import * as b from '#compiler/builders';
/**

Loading…
Cancel
Save