hoist-unmodified-var
Ben McCann 9 months ago
parent 0bd0c4e77e
commit b0900386f1

@ -1,5 +1,5 @@
import * as b from '../../../utils/builders.js'; import * as b from '../../../utils/builders.js';
import { extract_paths, is_simple_expression } from '../../../utils/ast.js'; import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js';
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
@ -7,6 +7,7 @@ import {
PROPS_IS_RUNES, PROPS_IS_RUNES,
PROPS_IS_UPDATED PROPS_IS_UPDATED
} from '../../../../constants.js'; } from '../../../../constants.js';
import { get_rune } from '../../scope';
/** /**
* @template {import('./types').ClientTransformState} State * @template {import('./types').ClientTransformState} State
@ -535,18 +536,19 @@ export function should_proxy_or_freeze(node) {
/** /**
* @param {import('#compiler').Binding | undefined} binding * @param {import('#compiler').Binding | undefined} binding
* @param {import('#compiler').SvelteNode[]} path * @param {string} name
* @returns {boolean} * @returns {boolean}
*/ */
export function is_hoistable_declaration(binding, path) { export function is_hoistable_declaration(binding, name) {
const is_top_level = path.at(-1)?.type === 'Program';
// TODO: allow object expressions that are not passed to functions or components as props // TODO: allow object expressions that are not passed to functions or components as props
// and expressions as long as they do not reference non-hoistable variables // and expressions as long as they do not reference non-hoistable variables
return ( return (
is_top_level &&
!!binding && !!binding &&
binding.kind === 'normal' &&
binding.scope.is_top_level &&
!binding.mutated && !binding.mutated &&
!binding.reassigned && !binding.reassigned &&
binding?.initial?.type === 'Literal' binding.initial?.type === 'Literal' &&
!binding.scope.declared_in_outer_scope(name)
); );
} }

@ -1,7 +1,12 @@
import { is_hoistable_function } from '../../utils.js'; import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js'; import { extract_paths } from '../../../../utils/ast.js';
import { create_state_declarators, get_prop_source, serialize_get_binding } from '../utils.js'; import {
create_state_declarators,
get_prop_source,
is_hoistable_declaration,
serialize_get_binding
} from '../utils.js';
/** @type {import('../types.js').ComponentVisitors} */ /** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_legacy = { export const javascript_visitors_legacy = {
@ -18,6 +23,19 @@ export const javascript_visitors_legacy = {
if (!has_state && !has_props) { if (!has_state && !has_props) {
const init = declarator.init; const init = declarator.init;
if (init != null && declarator.id.type === 'Identifier') {
const binding = state.scope
.owner(declarator.id.name)
?.declarations.get(declarator.id.name);
if (is_hoistable_declaration(binding, declarator.id.name)) {
state.scope.root.unique;
state.hoisted.push(b.declaration('const', declarator.id, init));
continue;
}
}
if (init != null && is_hoistable_function(init)) { if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init); const hoistable_function = visit(init);
state.hoisted.push( state.hoisted.push(

@ -156,21 +156,22 @@ export const javascript_visitors_runes = {
return { ...node, body }; return { ...node, body };
}, },
VariableDeclaration(node, { path, state, visit }) { VariableDeclaration(node, { state, visit }) {
const declarations = []; const declarations = [];
for (const declarator of node.declarations) { for (const declarator of node.declarations) {
const init = unwrap_ts_expression(declarator.init); const init = unwrap_ts_expression(declarator.init);
const rune = get_rune(init, state.scope);
if (!rune && init != null && declarator.id.type === 'Identifier') { if (init != null && declarator.id.type === 'Identifier') {
const binding = state.scope.owner(declarator.id.name)?.declarations.get(declarator.id.name); const binding = state.scope.owner(declarator.id.name)?.declarations.get(declarator.id.name);
if (is_hoistable_declaration(binding, path)) { if (is_hoistable_declaration(binding, declarator.id.name)) {
state.scope.root.unique;
state.hoisted.push(b.declaration('const', declarator.id, init)); state.hoisted.push(b.declaration('const', declarator.id, init));
continue; continue;
} }
} }
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect') { if (!rune || rune === '$effect.active' || rune === '$effect.root' || rune === '$inspect') {
if (init != null && is_hoistable_function(init)) { if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init); const hoistable_function = visit(init);

@ -501,8 +501,6 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
} }
let grouped_value = value; let grouped_value = value;
/** @type {import('estree').Expression | null} */
let constant_expression = grouped_value;
if (name === 'autofocus') { if (name === 'autofocus') {
state.init.push(b.stmt(b.call('$.auto_focus', node_id, value))); state.init.push(b.stmt(b.call('$.auto_focus', node_id, value)));
@ -511,7 +509,6 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
if (name === 'class') { if (name === 'class') {
grouped_value = b.call('$.to_class', value); grouped_value = b.call('$.to_class', value);
constant_expression = null;
} }
/** /**
@ -567,18 +564,19 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
} }
}; };
let is_in_hoistable = false; let hoistable = false;
if (Array.isArray(attribute.value)) { if (Array.isArray(attribute.value)) {
for (let value of attribute.value) { for (let value of attribute.value) {
if (value.type === 'ExpressionTag' && value.expression.type === 'Identifier') { if (value.type === 'ExpressionTag' && value.expression.type === 'Identifier') {
const binding = context.state.scope const binding = context.state.scope
.owner(value.expression.name) .owner(value.expression.name)
?.declarations.get(value.expression.name); ?.declarations.get(value.expression.name);
is_in_hoistable ||= is_hoistable_declaration(binding, context.path); hoistable ||= is_hoistable_declaration(binding, value.expression.name);
} }
} }
} }
if (attribute.metadata.dynamic && !is_in_hoistable) {
if (attribute.metadata.dynamic && !hoistable) {
const id = state.scope.generate(`${node_id.name}_${name}`); const id = state.scope.generate(`${node_id.name}_${name}`);
serialize_update_assignment( serialize_update_assignment(
state, state,
@ -590,12 +588,12 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
); );
return true; return true;
} else { } else {
if (!is_in_hoistable || DOMBooleanAttributes.includes(name)) { if (hoistable) {
state.init.push(assign(grouped_value).grouped);
} else if (constant_expression) {
push_template_quasi(context.state, ` ${name}="`); push_template_quasi(context.state, ` ${name}="`);
push_template_expression(context.state, grouped_value); push_template_expression(context.state, grouped_value);
push_template_quasi(context.state, `"`); push_template_quasi(context.state, `"`);
} else {
state.init.push(assign(grouped_value).grouped);
} }
return false; return false;
} }

@ -70,6 +70,76 @@ export const ElementBindings = [
'indeterminate' 'indeterminate'
]; ];
export const GlobalBindings = new Set([
'Map',
'Set',
'Array',
'Uint8Array',
'Uint8ClampedArray',
'Int16Array',
'Uint16Array',
'Int32Array',
'Uint32Array',
'BigInt64Array',
'BigUint64Array',
'Float32Array',
'Float64Array',
'Int8Array',
'Function',
'Error',
'AggregateError',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
'InternalError',
'Number',
'Math',
'BigInt',
'String',
'RegEx',
'Date',
'Boolean',
'Symbol',
'Object',
'ArrayBuffer',
'SharedArrayBuffer',
'DataView',
'Atomics',
'JSON',
'WeakRef',
'FinalizationRegistry',
'Iterator',
'AsyncIterator',
'Promise',
'GeneratorFunction',
'AsyncGeneratorFunction',
'Generator',
'AsyncGenerator',
'AsyncFunction',
'Reflect',
'Proxy',
'Intl',
'eval',
'isFinite',
'isNaN',
'parseFloat',
'parseInt',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'console',
'undefined',
'NaN',
'Infinity',
'globalThis',
'window',
'document'
]);
export const Runes = /** @type {const} */ ([ export const Runes = /** @type {const} */ ([
'$state', '$state',
'$state.frozen', '$state.frozen',

@ -4,12 +4,19 @@ import { is_element_node } from './nodes.js';
import * as b from '../utils/builders.js'; import * as b from '../utils/builders.js';
import { error } from '../errors.js'; import { error } from '../errors.js';
import { extract_identifiers, extract_identifiers_from_expression } from '../utils/ast.js'; import { extract_identifiers, extract_identifiers_from_expression } from '../utils/ast.js';
import { JsKeywords, Runes } from './constants.js'; import { GlobalBindings, JsKeywords, Runes } from './constants.js';
export class Scope { export class Scope {
/** @type {ScopeRoot} */ /** @type {ScopeRoot} */
root; root;
/**
* Whether this is a top-level script scope.
* I.e. `<script>`, `<script context=module>`, or `.svelte.js`.
* @type {boolean}
*/
is_top_level;
/** /**
* The immediate parent scope * The immediate parent scope
* @type {Scope | null} * @type {Scope | null}
@ -49,16 +56,17 @@ export class Scope {
function_depth = 0; function_depth = 0;
/** /**
*
* @param {ScopeRoot} root * @param {ScopeRoot} root
* @param {Scope | null} parent * @param {Scope | null} parent
* @param {boolean} porous * @param {boolean} porous
* @param {boolean} is_top_level
*/ */
constructor(root, parent, porous) { constructor(root, parent, porous, is_top_level) {
this.root = root; this.root = root;
this.#parent = parent; this.#parent = parent;
this.#porous = porous; this.#porous = porous;
this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0; this.function_depth = parent ? parent.function_depth + (porous ? 0 : 1) : 0;
this.is_top_level = is_top_level;
} }
/** /**
@ -119,8 +127,23 @@ export class Scope {
return binding; return binding;
} }
/**
* @param {string} name
* @returns {boolean}
*/
declared_in_outer_scope(name) {
let outer = this.#parent;
while (outer !== null) {
if (outer.references.has(name) || outer.declarations.has(name)) {
return true;
}
outer = outer.#parent;
}
return GlobalBindings.has(name) || JsKeywords.includes(name);
}
child(porous = false) { child(porous = false) {
return new Scope(this.root, this, porous); return new Scope(this.root, this, porous, false);
} }
/** /**
@ -238,7 +261,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
* @type {Map<import('#compiler').SvelteNode, Scope>} * @type {Map<import('#compiler').SvelteNode, Scope>}
*/ */
const scopes = new Map(); const scopes = new Map();
const scope = new Scope(root, parent, false); const scope = new Scope(root, parent, false, ast.type === 'Program');
scopes.set(ast, scope); scopes.set(ast, scope);
/** @type {State} */ /** @type {State} */

Loading…
Cancel
Save