From 72c66398cd3e8d52e6812ff94b721a1a56ad7cdd Mon Sep 17 00:00:00 2001 From: Adam Hines Date: Fri, 13 Oct 2023 23:19:54 -0600 Subject: [PATCH] perf: reducing ast node memory overhead (#5133) * perf: pack booleans in bitfield Booleans are represented by numbers and take up 4 bytes each at the time of this commit in v8. This commit packs all the boolean fields of the AST nodes into a single 32-bit integer which ammortizes the cost of the boolean fields. The end result is smaller AST nodes. * chore: use bit shifts instead of binary literals * chore: remove context wrapper accessor from node We now just access through `this.scope.context` directly. * chore: cleaning up keys assignment --------- Co-authored-by: Lukas Taegert-Atkinson --- src/ast/keys.ts | 5 +- src/ast/nodes/ArrayExpression.ts | 2 +- src/ast/nodes/ArrowFunctionExpression.ts | 3 +- src/ast/nodes/AssignmentExpression.ts | 2 +- src/ast/nodes/AssignmentPattern.ts | 2 +- src/ast/nodes/AwaitExpression.ts | 4 +- src/ast/nodes/BlockStatement.ts | 21 ++++-- src/ast/nodes/CallExpression.ts | 15 ++-- src/ast/nodes/CatchClause.ts | 8 +-- src/ast/nodes/ClassBody.ts | 11 +-- src/ast/nodes/ConditionalExpression.ts | 9 ++- src/ast/nodes/ExportAllDeclaration.ts | 2 +- src/ast/nodes/ExportDefaultDeclaration.ts | 8 +-- src/ast/nodes/ExportNamedDeclaration.ts | 2 +- src/ast/nodes/ExpressionStatement.ts | 4 +- src/ast/nodes/ForInStatement.ts | 8 +-- src/ast/nodes/ForOfStatement.ts | 17 +++-- src/ast/nodes/ForStatement.ts | 6 +- src/ast/nodes/Identifier.ts | 35 +++++++--- src/ast/nodes/IfStatement.ts | 10 +-- src/ast/nodes/ImportDeclaration.ts | 2 +- src/ast/nodes/ImportExpression.ts | 6 +- src/ast/nodes/Literal.ts | 4 +- src/ast/nodes/LogicalExpression.ts | 15 +++- src/ast/nodes/MemberExpression.ts | 61 +++++++++++++---- src/ast/nodes/MetaProperty.ts | 6 +- src/ast/nodes/NewExpression.ts | 2 +- src/ast/nodes/Program.ts | 4 +- src/ast/nodes/Property.ts | 26 +++++-- src/ast/nodes/PropertyDefinition.ts | 9 ++- src/ast/nodes/RestElement.ts | 2 +- src/ast/nodes/SpreadElement.ts | 4 +- src/ast/nodes/StaticBlock.ts | 6 +- src/ast/nodes/Super.ts | 2 +- src/ast/nodes/SwitchStatement.ts | 9 ++- src/ast/nodes/TaggedTemplateExpression.ts | 4 +- src/ast/nodes/TemplateElement.ts | 9 ++- src/ast/nodes/ThisExpression.ts | 8 ++- src/ast/nodes/TryStatement.ts | 7 +- src/ast/nodes/UnaryExpression.ts | 11 ++- src/ast/nodes/UpdateExpression.ts | 2 +- src/ast/nodes/shared/BitFlags.ts | 32 +++++++++ src/ast/nodes/shared/CallExpressionBase.ts | 2 +- src/ast/nodes/shared/ClassNode.ts | 7 +- src/ast/nodes/shared/Expression.ts | 10 ++- src/ast/nodes/shared/FunctionBase.ts | 22 ++++-- src/ast/nodes/shared/FunctionNode.ts | 3 +- src/ast/nodes/shared/MethodBase.ts | 9 ++- src/ast/nodes/shared/MultiExpression.ts | 2 - src/ast/nodes/shared/Node.ts | 80 ++++++++++++++-------- src/ast/nodes/shared/ObjectEntity.ts | 26 ++++++- src/ast/scopes/ChildScope.ts | 5 +- src/ast/scopes/ClassBodyScope.ts | 4 +- src/ast/scopes/ModuleScope.ts | 4 +- src/ast/scopes/ParameterScope.ts | 6 +- src/ast/variables/NamespaceVariable.ts | 3 +- src/utils/parseImportAttributes.ts | 6 +- 57 files changed, 411 insertions(+), 183 deletions(-) create mode 100644 src/ast/nodes/shared/BitFlags.ts diff --git a/src/ast/keys.ts b/src/ast/keys.ts index e9f9d437952..74e79f35a25 100644 --- a/src/ast/keys.ts +++ b/src/ast/keys.ts @@ -7,9 +7,8 @@ export const keys: { Program: ['body'] }; -export function getAndCreateKeys(esTreeNode: GenericEsTreeNode): string[] { - keys[esTreeNode.type] = Object.keys(esTreeNode).filter( +export function createKeysForNode(esTreeNode: GenericEsTreeNode): string[] { + return Object.keys(esTreeNode).filter( key => typeof esTreeNode[key] === 'object' && key.charCodeAt(0) !== 95 /* _ */ ); - return keys[esTreeNode.type]; } diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index e82652eb92e..1d731e3642f 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -76,7 +76,7 @@ export default class ArrayExpression extends NodeBase { element.deoptimizePath(UNKNOWN_PATH); } } - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } private getObjectEntity(): ObjectEntity { diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index cb685e4a35c..66135d14456 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -14,7 +14,6 @@ import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; import type { PatternNode } from './shared/Pattern'; export default class ArrowFunctionExpression extends FunctionBase { - declare async: boolean; declare body: BlockStatement | ExpressionNode; declare params: readonly PatternNode[]; declare preventChildBlockScope: true; @@ -23,7 +22,7 @@ export default class ArrowFunctionExpression extends FunctionBase { protected objectEntity: ObjectEntity | null = null; createScope(parentScope: Scope): void { - this.scope = new ReturnValueScope(parentScope, this.context); + this.scope = new ReturnValueScope(parentScope, this.scope.context); } hasEffects(): boolean { diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index 184ff7c3207..ee5386e0e23 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -155,6 +155,6 @@ export default class AssignmentExpression extends NodeBase { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 1e138a11730..e20bdb07710 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -58,6 +58,6 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/AwaitExpression.ts b/src/ast/nodes/AwaitExpression.ts index aafe5995625..25f262c0e64 100644 --- a/src/ast/nodes/AwaitExpression.ts +++ b/src/ast/nodes/AwaitExpression.ts @@ -17,13 +17,13 @@ export default class AwaitExpression extends NodeBase { if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; - checkTopLevelAwait: if (!this.context.usesTopLevelAwait) { + checkTopLevelAwait: if (!this.scope.context.usesTopLevelAwait) { let parent = this.parent; do { if (parent instanceof FunctionNode || parent instanceof ArrowFunctionExpression) break checkTopLevelAwait; } while ((parent = (parent as Node).parent as Node)); - this.context.usesTopLevelAwait = true; + this.scope.context.usesTopLevelAwait = true; } } this.argument.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/BlockStatement.ts b/src/ast/nodes/BlockStatement.ts index b2745f94a5e..bb7343fe261 100644 --- a/src/ast/nodes/BlockStatement.ts +++ b/src/ast/nodes/BlockStatement.ts @@ -3,9 +3,9 @@ import { type RenderOptions, renderStatementList } from '../../utils/renderHelpe import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import type ChildScope from '../scopes/ChildScope'; -import type Scope from '../scopes/Scope'; import ExpressionStatement from './ExpressionStatement'; import * as NodeType from './NodeType'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { type IncludeChildren, type Node, StatementBase, type StatementNode } from './shared/Node'; @@ -13,8 +13,19 @@ export default class BlockStatement extends StatementBase { declare body: readonly StatementNode[]; declare type: NodeType.tBlockStatement; - private declare deoptimizeBody: boolean; - private directlyIncluded = false; + private get deoptimizeBody(): boolean { + return isFlagSet(this.flags, Flag.deoptimizeBody); + } + private set deoptimizeBody(value: boolean) { + this.flags = setFlag(this.flags, Flag.deoptimizeBody, value); + } + + private get directlyIncluded(): boolean { + return isFlagSet(this.flags, Flag.directlyIncluded); + } + private set directlyIncluded(value: boolean) { + this.flags = setFlag(this.flags, Flag.directlyIncluded, value); + } addImplicitReturnExpressionToScope(): void { const lastStatement = this.body[this.body.length - 1]; @@ -23,10 +34,10 @@ export default class BlockStatement extends StatementBase { } } - createScope(parentScope: Scope): void { + createScope(parentScope: ChildScope): void { this.scope = (this.parent as Node).preventChildBlockScope ? (parentScope as ChildScope) - : new BlockScope(parentScope); + : new BlockScope(parentScope, this.scope.context); } hasEffects(context: HasEffectsContext): boolean { diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 157859a0209..3d6639151dc 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -13,6 +13,7 @@ import MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; import type SpreadElement from './SpreadElement'; import type Super from './Super'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import CallExpressionBase from './shared/CallExpressionBase'; import { type ExpressionEntity, UNKNOWN_RETURN_EXPRESSION } from './shared/Expression'; import type { ChainElement, ExpressionNode, IncludeChildren } from './shared/Node'; @@ -24,20 +25,26 @@ export default class CallExpression { declare arguments: (ExpressionNode | SpreadElement)[]; declare callee: ExpressionNode | Super; - declare optional: boolean; declare type: NodeType.tCallExpression; + get optional(): boolean { + return isFlagSet(this.flags, Flag.optional); + } + set optional(value: boolean) { + this.flags = setFlag(this.flags, Flag.optional, value); + } + bind(): void { super.bind(); if (this.callee instanceof Identifier) { const variable = this.scope.findVariable(this.callee.name); if (variable.isNamespace) { - this.context.log(LOGLEVEL_WARN, logCannotCallNamespace(this.callee.name), this.start); + this.scope.context.log(LOGLEVEL_WARN, logCannotCallNamespace(this.callee.name), this.start); } if (this.callee.name === 'eval') { - this.context.log(LOGLEVEL_WARN, logEval(this.context.module.id), this.start); + this.scope.context.log(LOGLEVEL_WARN, logEval(this.scope.context.module.id), this.start); } } this.interaction = { @@ -114,7 +121,7 @@ export default class CallExpression EMPTY_PATH, SHARED_RECURSION_TRACKER ); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } protected getReturnExpression( diff --git a/src/ast/nodes/CatchClause.ts b/src/ast/nodes/CatchClause.ts index 5a85f4ad12a..135bacf258b 100644 --- a/src/ast/nodes/CatchClause.ts +++ b/src/ast/nodes/CatchClause.ts @@ -1,5 +1,5 @@ import CatchScope from '../scopes/CatchScope'; -import type Scope from '../scopes/Scope'; +import type ChildScope from '../scopes/ChildScope'; import type BlockStatement from './BlockStatement'; import type * as NodeType from './NodeType'; import { UNKNOWN_EXPRESSION } from './shared/Expression'; @@ -13,8 +13,8 @@ export default class CatchClause extends NodeBase { declare scope: CatchScope; declare type: NodeType.tCatchClause; - createScope(parentScope: Scope): void { - this.scope = new CatchScope(parentScope, this.context); + createScope(parentScope: ChildScope): void { + this.scope = new CatchScope(parentScope, this.scope.context); } parseNode(esTreeNode: GenericEsTreeNode): void { @@ -23,7 +23,7 @@ export default class CatchClause extends NodeBase { // name instead of the variable const { param } = esTreeNode; if (param) { - (this.param as GenericEsTreeNode) = new (this.context.getNodeConstructor(param.type))( + (this.param as GenericEsTreeNode) = new (this.scope.context.getNodeConstructor(param.type))( param, this, this.scope diff --git a/src/ast/nodes/ClassBody.ts b/src/ast/nodes/ClassBody.ts index 21da58a4420..325b2f32704 100644 --- a/src/ast/nodes/ClassBody.ts +++ b/src/ast/nodes/ClassBody.ts @@ -1,6 +1,7 @@ import type { InclusionContext } from '../ExecutionContext'; +import type ChildScope from '../scopes/ChildScope'; import ClassBodyScope from '../scopes/ClassBodyScope'; -import type Scope from '../scopes/Scope'; + import type MethodDefinition from './MethodDefinition'; import type * as NodeType from './NodeType'; import type PropertyDefinition from './PropertyDefinition'; @@ -12,13 +13,13 @@ export default class ClassBody extends NodeBase { declare scope: ClassBodyScope; declare type: NodeType.tClassBody; - createScope(parentScope: Scope): void { - this.scope = new ClassBodyScope(parentScope, this.parent as ClassNode, this.context); + createScope(parentScope: ChildScope): void { + this.scope = new ClassBodyScope(parentScope, this.parent as ClassNode, this.scope.context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { this.included = true; - this.context.includeVariableInModule(this.scope.thisVariable); + this.scope.context.includeVariableInModule(this.scope.thisVariable); for (const definition of this.body) { definition.include(context, includeChildrenRecursively); } @@ -28,7 +29,7 @@ export default class ClassBody extends NodeBase { const body: NodeBase[] = (this.body = []); for (const definition of esTreeNode.body) { body.push( - new (this.context.getNodeConstructor(definition.type))( + new (this.scope.context.getNodeConstructor(definition.type))( definition, this, definition.static ? this.scope : this.scope.instanceScope diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 94800c20e63..5645a23f910 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -13,6 +13,7 @@ import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import { EMPTY_PATH, SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; import type SpreadElement from './SpreadElement'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import type { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; @@ -25,8 +26,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz declare test: ExpressionNode; declare type: NodeType.tConditionalExpression; + get isBranchResolutionAnalysed(): boolean { + return isFlagSet(this.flags, Flag.isBranchResolutionAnalysed); + } + set isBranchResolutionAnalysed(value: boolean) { + this.flags = setFlag(this.flags, Flag.isBranchResolutionAnalysed, value); + } + private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; - private isBranchResolutionAnalysed = false; private usedBranch: ExpressionNode | null = null; deoptimizeArgumentsOnInteractionAtPath( diff --git a/src/ast/nodes/ExportAllDeclaration.ts b/src/ast/nodes/ExportAllDeclaration.ts index 271e36d7a00..e4bb9f8936a 100644 --- a/src/ast/nodes/ExportAllDeclaration.ts +++ b/src/ast/nodes/ExportAllDeclaration.ts @@ -18,7 +18,7 @@ export default class ExportAllDeclaration extends NodeBase { } initialise(): void { - this.context.addExport(this); + this.scope.context.addExport(this); } render(code: MagicString, _options: RenderOptions, nodeRenderOptions?: NodeRenderOptions): void { diff --git a/src/ast/nodes/ExportDefaultDeclaration.ts b/src/ast/nodes/ExportDefaultDeclaration.ts index b6bf1d32bbe..3101bd8cedc 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.ts +++ b/src/ast/nodes/ExportDefaultDeclaration.ts @@ -44,7 +44,7 @@ export default class ExportDefaultDeclaration extends NodeBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { super.include(context, includeChildrenRecursively); if (includeChildrenRecursively) { - this.context.includeVariableInModule(this.variable); + this.scope.context.includeVariableInModule(this.variable); } } @@ -53,11 +53,11 @@ export default class ExportDefaultDeclaration extends NodeBase { this.declarationName = (declaration.id && declaration.id.name) || (this.declaration as Identifier).name; this.variable = this.scope.addExportDefaultDeclaration( - this.declarationName || this.context.getModuleName(), + this.declarationName || this.scope.context.getModuleName(), this, - this.context + this.scope.context ); - this.context.addExport(this); + this.scope.context.addExport(this); } removeAnnotations(code: MagicString) { diff --git a/src/ast/nodes/ExportNamedDeclaration.ts b/src/ast/nodes/ExportNamedDeclaration.ts index a63ea59e27d..08d457e22b2 100644 --- a/src/ast/nodes/ExportNamedDeclaration.ts +++ b/src/ast/nodes/ExportNamedDeclaration.ts @@ -28,7 +28,7 @@ export default class ExportNamedDeclaration extends NodeBase { } initialise(): void { - this.context.addExport(this); + this.scope.context.addExport(this); } removeAnnotations(code: MagicString) { diff --git a/src/ast/nodes/ExpressionStatement.ts b/src/ast/nodes/ExpressionStatement.ts index e1bb6847edb..5f78477c227 100644 --- a/src/ast/nodes/ExpressionStatement.ts +++ b/src/ast/nodes/ExpressionStatement.ts @@ -16,10 +16,10 @@ export default class ExpressionStatement extends StatementBase { this.directive !== 'use strict' && this.parent.type === NodeType.Program ) { - this.context.log( + this.scope.context.log( LOGLEVEL_WARN, // This is necessary, because either way (deleting or not) can lead to errors. - logModuleLevelDirective(this.directive, this.context.module.id), + logModuleLevelDirective(this.directive, this.scope.context.module.id), this.start ); } diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index c18361512a3..1a32c8dd4a5 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -2,7 +2,7 @@ import type MagicString from 'magic-string'; import { NO_SEMICOLON, type RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; -import type Scope from '../scopes/Scope'; +import type ChildScope from '../scopes/ChildScope'; import { EMPTY_PATH } from '../utils/PathTracker'; import type MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; @@ -23,8 +23,8 @@ export default class ForInStatement extends StatementBase { declare right: ExpressionNode; declare type: NodeType.tForInStatement; - createScope(parentScope: Scope): void { - this.scope = new BlockScope(parentScope); + createScope(parentScope: ChildScope): void { + this.scope = new BlockScope(parentScope, this.scope.context); } hasEffects(context: HasEffectsContext): boolean { @@ -60,6 +60,6 @@ export default class ForInStatement extends StatementBase { protected applyDeoptimizations(): void { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index ea4f40795c1..434ca9355ec 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -2,11 +2,12 @@ import type MagicString from 'magic-string'; import { NO_SEMICOLON, type RenderOptions } from '../../utils/renderHelpers'; import type { InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; -import type Scope from '../scopes/Scope'; +import type ChildScope from '../scopes/ChildScope'; import { EMPTY_PATH, UNKNOWN_PATH } from '../utils/PathTracker'; import type MemberExpression from './MemberExpression'; import type * as NodeType from './NodeType'; import type VariableDeclaration from './VariableDeclaration'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { type ExpressionNode, @@ -18,14 +19,20 @@ import type { PatternNode } from './shared/Pattern'; import { includeLoopBody } from './shared/loops'; export default class ForOfStatement extends StatementBase { - declare await: boolean; declare body: StatementNode; declare left: VariableDeclaration | PatternNode | MemberExpression; declare right: ExpressionNode; declare type: NodeType.tForOfStatement; - createScope(parentScope: Scope): void { - this.scope = new BlockScope(parentScope); + get await(): boolean { + return isFlagSet(this.flags, Flag.await); + } + set await(value: boolean) { + this.flags = setFlag(this.flags, Flag.await, value); + } + + createScope(parentScope: ChildScope): void { + this.scope = new BlockScope(parentScope, this.scope.context); } hasEffects(): boolean { @@ -61,6 +68,6 @@ export default class ForOfStatement extends StatementBase { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/ForStatement.ts b/src/ast/nodes/ForStatement.ts index 4a88250c203..d3c134f35d3 100644 --- a/src/ast/nodes/ForStatement.ts +++ b/src/ast/nodes/ForStatement.ts @@ -2,7 +2,7 @@ import type MagicString from 'magic-string'; import { NO_SEMICOLON, type RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; -import type Scope from '../scopes/Scope'; +import type ChildScope from '../scopes/ChildScope'; import type * as NodeType from './NodeType'; import type VariableDeclaration from './VariableDeclaration'; import { @@ -20,8 +20,8 @@ export default class ForStatement extends StatementBase { declare type: NodeType.tForStatement; declare update: ExpressionNode | null; - createScope(parentScope: Scope): void { - this.scope = new BlockScope(parentScope); + createScope(parentScope: ChildScope): void { + this.scope = new BlockScope(parentScope, this.scope.context); } hasEffects(context: HasEffectsContext): boolean { diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 0ab953826e0..22d439f75b7 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -20,6 +20,7 @@ import LocalVariable from '../variables/LocalVariable'; import type Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import type SpreadElement from './SpreadElement'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { type ExpressionEntity, type LiteralValueOrUnknown, @@ -42,7 +43,17 @@ export default class Identifier extends NodeBase implements PatternNode { declare name: string; declare type: NodeType.tIdentifier; variable: Variable | null = null; - private isTDZAccess: boolean | null = null; + + private get isTDZAccess(): boolean | null { + if (!isFlagSet(this.flags, Flag.tdzAccessDefined)) { + return null; + } + return isFlagSet(this.flags, Flag.tdzAccess); + } + private set isTDZAccess(value: boolean) { + this.flags = setFlag(this.flags, Flag.tdzAccessDefined, true); + this.flags = setFlag(this.flags, Flag.tdzAccess, value); + } addExportedVariables( variables: Variable[], @@ -62,10 +73,10 @@ export default class Identifier extends NodeBase implements PatternNode { declare(kind: string, init: ExpressionEntity): LocalVariable[] { let variable: LocalVariable; - const { treeshake } = this.context.options; + const { treeshake } = this.scope.context.options; switch (kind) { case 'var': { - variable = this.scope.addDeclaration(this, this.context, init, true); + variable = this.scope.addDeclaration(this, this.scope.context, init, true); if (treeshake && treeshake.correctVarValueBeforeDeclaration) { // Necessary to make sure the init is deoptimized. We cannot call deoptimizePath here. variable.markInitializersForDeoptimization(); @@ -74,13 +85,13 @@ export default class Identifier extends NodeBase implements PatternNode { } case 'function': { // in strict mode, functions are only hoisted within a scope but not across block scopes - variable = this.scope.addDeclaration(this, this.context, init, false); + variable = this.scope.addDeclaration(this, this.scope.context, init, false); break; } case 'let': case 'const': case 'class': { - variable = this.scope.addDeclaration(this, this.context, init, false); + variable = this.scope.addDeclaration(this, this.scope.context, init, false); break; } case 'parameter': { @@ -141,7 +152,8 @@ export default class Identifier extends NodeBase implements PatternNode { return true; } return ( - (this.context.options.treeshake as NormalizedTreeshakingOptions).unknownGlobalSideEffects && + (this.scope.context.options.treeshake as NormalizedTreeshakingOptions) + .unknownGlobalSideEffects && this.variable instanceof GlobalVariable && !this.isPureFunction(EMPTY_PATH) && this.variable.hasEffectsOnInteractionAtPath( @@ -184,7 +196,7 @@ export default class Identifier extends NodeBase implements PatternNode { if (!this.included) { this.included = true; if (this.variable !== null) { - this.context.includeVariableInModule(this.variable); + this.scope.context.includeVariableInModule(this.variable); } } } @@ -198,7 +210,8 @@ export default class Identifier extends NodeBase implements PatternNode { isPossibleTDZ(): boolean { // return cached value to avoid issues with the next tree-shaking pass - if (this.isTDZAccess !== null) return this.isTDZAccess; + const cachedTdzAccess = this.isTDZAccess; + if (cachedTdzAccess !== null) return cachedTdzAccess; if ( !( @@ -207,7 +220,7 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable.kind in tdzVariableKinds && // we ignore possible TDZs due to circular module dependencies as // otherwise we get many false positives - this.variable.module === this.context.module + this.variable.module === this.scope.context.module ) ) { return (this.isTDZAccess = false); @@ -271,7 +284,7 @@ export default class Identifier extends NodeBase implements PatternNode { this.deoptimized = true; if (this.variable instanceof LocalVariable) { this.variable.consolidateInitializers(); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } @@ -283,7 +296,7 @@ export default class Identifier extends NodeBase implements PatternNode { } private isPureFunction(path: ObjectPath) { - let currentPureFunction = this.context.manualPureFunctions[this.name]; + let currentPureFunction = this.scope.context.manualPureFunctions[this.name]; for (const segment of path) { if (currentPureFunction) { if (currentPureFunction[PureFunctionKey]) { diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 47c43aa198e..845cc413063 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -67,15 +67,15 @@ export default class IfStatement extends StatementBase implements DeoptimizableE } parseNode(esTreeNode: GenericEsTreeNode): void { - this.consequentScope = new TrackingScope(this.scope); - this.consequent = new (this.context.getNodeConstructor(esTreeNode.consequent.type))( + this.consequentScope = new TrackingScope(this.scope, this.scope.context); + this.consequent = new (this.scope.context.getNodeConstructor(esTreeNode.consequent.type))( esTreeNode.consequent, this, this.consequentScope ); if (esTreeNode.alternate) { - this.alternateScope = new TrackingScope(this.scope); - this.alternate = new (this.context.getNodeConstructor(esTreeNode.alternate.type))( + this.alternateScope = new TrackingScope(this.scope, this.scope.context); + this.alternate = new (this.scope.context.getNodeConstructor(esTreeNode.alternate.type))( esTreeNode.alternate, this, this.alternateScope @@ -92,7 +92,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE const testValue = this.getTestValue(); const hoistedDeclarations: Identifier[] = []; const includesIfElse = this.test.included; - const noTreeshake = !this.context.options.treeshake; + const noTreeshake = !this.scope.context.options.treeshake; if (includesIfElse) { this.test.render(code, options); } else { diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index 41fc90fc50c..28e9b89bd57 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -23,7 +23,7 @@ export default class ImportDeclaration extends NodeBase { } initialise(): void { - this.context.addImport(this); + this.scope.context.addImport(this); } render(code: MagicString, _options: RenderOptions, nodeRenderOptions?: NodeRenderOptions): void { diff --git a/src/ast/nodes/ImportExpression.ts b/src/ast/nodes/ImportExpression.ts index 783092e02a9..8ce6a9693ba 100644 --- a/src/ast/nodes/ImportExpression.ts +++ b/src/ast/nodes/ImportExpression.ts @@ -153,14 +153,14 @@ export default class ImportExpression extends NodeBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { if (!this.included) { this.included = true; - this.context.includeDynamicImport(this); + this.scope.context.includeDynamicImport(this); this.scope.addAccessedDynamicImport(this); } this.source.include(context, includeChildrenRecursively); } initialise(): void { - this.context.addDynamicImport(this); + this.scope.context.addDynamicImport(this); } parseNode(esTreeNode: GenericEsTreeNode): void { @@ -278,7 +278,7 @@ export default class ImportExpression extends NodeBase { { customResolution: typeof this.resolution === 'string' ? this.resolution : null, format, - moduleId: this.context.module.id, + moduleId: this.scope.context.module.id, targetModuleId: this.resolution && typeof this.resolution !== 'string' ? this.resolution.id : null } diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index bc2b41ed1a2..9a66c7ac48f 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -40,10 +40,10 @@ export default class Literal extends Node if ( path.length > 0 || // unknown literals can also be null but do not start with an "n" - (this.value === null && this.context.code.charCodeAt(this.start) !== 110) || + (this.value === null && this.scope.context.code.charCodeAt(this.start) !== 110) || typeof this.value === 'bigint' || // to support shims for regular expressions - this.context.code.charCodeAt(this.start) === 47 + this.scope.context.code.charCodeAt(this.start) === 47 ) { return UnknownValue; } diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 25c5fa7eac2..55de1486181 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -18,6 +18,7 @@ import { UNKNOWN_PATH } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { type ExpressionEntity, type LiteralValueOrUnknown, @@ -34,9 +35,16 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable declare right: ExpressionNode; declare type: NodeType.tLogicalExpression; + //private isBranchResolutionAnalysed = false; + private get isBranchResolutionAnalysed(): boolean { + return isFlagSet(this.flags, Flag.isBranchResolutionAnalysed); + } + private set isBranchResolutionAnalysed(value: boolean) { + this.flags = setFlag(this.flags, Flag.isBranchResolutionAnalysed, value); + } + // We collect deoptimization information if usedBranch !== null private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; - private isBranchResolutionAnalysed = false; private usedBranch: ExpressionNode | null = null; deoptimizeArgumentsOnInteractionAtPath( @@ -53,7 +61,10 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable const unusedBranch = this.usedBranch === this.left ? this.right : this.left; this.usedBranch = null; unusedBranch.deoptimizePath(UNKNOWN_PATH); - const { context, expressionsToBeDeoptimized } = this; + const { + scope: { context }, + expressionsToBeDeoptimized + } = this; this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index ffb2dd48777..045a0e921a8 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -35,6 +35,7 @@ import type * as NodeType from './NodeType'; import type PrivateIdentifier from './PrivateIdentifier'; import type SpreadElement from './SpreadElement'; import type Super from './Super'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { deoptimizeInteraction, type ExpressionEntity, @@ -95,19 +96,49 @@ export default class MemberExpression extends NodeBase implements DeoptimizableEntity, ChainElement { - declare computed: boolean; declare object: ExpressionNode | Super; - declare optional: boolean; declare property: ExpressionNode | PrivateIdentifier; declare propertyKey: ObjectPathKey | null; declare type: NodeType.tMemberExpression; variable: Variable | null = null; protected declare assignmentInteraction: NodeInteractionAssigned; private declare accessInteraction: NodeInteractionAccessed; - private assignmentDeoptimized = false; - private bound = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; - private isUndefined = false; + + get computed(): boolean { + return isFlagSet(this.flags, Flag.computed); + } + set computed(value: boolean) { + this.flags = setFlag(this.flags, Flag.computed, value); + } + + get optional(): boolean { + return isFlagSet(this.flags, Flag.optional); + } + set optional(value: boolean) { + this.flags = setFlag(this.flags, Flag.optional, value); + } + + private get assignmentDeoptimized(): boolean { + return isFlagSet(this.flags, Flag.assignmentDeoptimized); + } + private set assignmentDeoptimized(value: boolean) { + this.flags = setFlag(this.flags, Flag.assignmentDeoptimized, value); + } + + private get bound(): boolean { + return isFlagSet(this.flags, Flag.bound); + } + private set bound(value: boolean) { + this.flags = setFlag(this.flags, Flag.bound, value); + } + + private get isUndefined(): boolean { + return isFlagSet(this.flags, Flag.isUndefined); + } + private set isUndefined(value: boolean) { + this.flags = setFlag(this.flags, Flag.isUndefined, value); + } bind(): void { this.bound = true; @@ -117,7 +148,7 @@ export default class MemberExpression const resolvedVariable = resolveNamespaceVariables( baseVariable, path!.slice(1), - this.context + this.scope.context ); if (!resolvedVariable) { super.bind(); @@ -348,7 +379,7 @@ export default class MemberExpression protected applyDeoptimizations(): void { this.deoptimized = true; - const { propertyReadSideEffects } = this.context.options + const { propertyReadSideEffects } = this.scope.context.options .treeshake as NormalizedTreeshakingOptions; if ( // Namespaces are not bound and should not be deoptimized @@ -362,13 +393,13 @@ export default class MemberExpression [propertyKey], SHARED_RECURSION_TRACKER ); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } private applyAssignmentDeoptimization(): void { this.assignmentDeoptimized = true; - const { propertyReadSideEffects } = this.context.options + const { propertyReadSideEffects } = this.scope.context.options .treeshake as NormalizedTreeshakingOptions; if ( // Namespaces are not bound and should not be deoptimized @@ -381,7 +412,7 @@ export default class MemberExpression [this.getPropertyKey()], SHARED_RECURSION_TRACKER ); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } @@ -390,11 +421,11 @@ export default class MemberExpression const variable = this.scope.findVariable(this.object.name); if (variable.isNamespace) { if (this.variable) { - this.context.includeVariableInModule(this.variable); + this.scope.context.includeVariableInModule(this.variable); } - this.context.log( + this.scope.context.log( LOGLEVEL_WARN, - logIllegalImportReassignment(this.object.name, this.context.module.id), + logIllegalImportReassignment(this.object.name, this.scope.context.module.id), this.start ); } @@ -416,7 +447,7 @@ export default class MemberExpression } private hasAccessEffect(context: HasEffectsContext) { - const { propertyReadSideEffects } = this.context.options + const { propertyReadSideEffects } = this.scope.context.options .treeshake as NormalizedTreeshakingOptions; return ( !(this.variable || this.isUndefined) && @@ -437,7 +468,7 @@ export default class MemberExpression if (!this.included) { this.included = true; if (this.variable) { - this.context.includeVariableInModule(this.variable); + this.scope.context.includeVariableInModule(this.variable); } } this.object.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index e58de55d61f..8d747643355 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -50,7 +50,7 @@ export default class MetaProperty extends NodeBase { if (!this.included) { this.included = true; if (this.meta.name === IMPORT) { - this.context.addImportMeta(this); + this.scope.context.addImportMeta(this); const parent = this.parent; const metaProperty = (this.metaProperty = parent instanceof MemberExpression && typeof parent.propertyKey === 'string' @@ -66,7 +66,9 @@ export default class MetaProperty extends NodeBase { render(code: MagicString, renderOptions: RenderOptions): void { const { format, pluginDriver, snippets } = renderOptions; const { - context: { module }, + scope: { + context: { module } + }, meta: { name }, metaProperty, parent, diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 6c91ba90a6a..98e847d6fcd 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -67,6 +67,6 @@ export default class NewExpression extends NodeBase { EMPTY_PATH, SHARED_RECURSION_TRACKER ); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/Program.ts b/src/ast/nodes/Program.ts index a2a256c70c0..e170353be02 100644 --- a/src/ast/nodes/Program.ts +++ b/src/ast/nodes/Program.ts @@ -32,9 +32,9 @@ export default class Program extends NodeBase { hasEffects(context: HasEffectsContext): boolean { for (const node of this.body) { if (node.hasEffects(context)) { - if (this.context.options.experimentalLogSideEffects && !this.hasLoggedEffect) { + if (this.scope.context.options.experimentalLogSideEffects && !this.hasLoggedEffect) { this.hasLoggedEffect = true; - const { code, log, module } = this.context; + const { code, log, module } = this.scope.context; log( LOGLEVEL_INFO, logFirstSideEffect(code, module.id, locate(code, node.start, { offsetLine: 1 })!), diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index e8d5b7d6027..bc7f42706ad 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -5,6 +5,7 @@ import type { HasEffectsContext } from '../ExecutionContext'; import { UnknownKey } from '../utils/PathTracker'; import type LocalVariable from '../variables/LocalVariable'; import type * as NodeType from './NodeType'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import MethodBase from './shared/MethodBase'; import type { ExpressionNode } from './shared/Node'; @@ -13,11 +14,25 @@ import type { PatternNode } from './shared/Pattern'; export default class Property extends MethodBase implements PatternNode { declare key: ExpressionNode; declare kind: 'init' | 'get' | 'set'; - declare method: boolean; - declare shorthand: boolean; declare type: NodeType.tProperty; private declarationInit: ExpressionEntity | null = null; + //declare method: boolean; + get method(): boolean { + return isFlagSet(this.flags, Flag.method); + } + set method(value: boolean) { + this.flags = setFlag(this.flags, Flag.method, value); + } + + //declare shorthand: boolean; + get shorthand(): boolean { + return isFlagSet(this.flags, Flag.shorthand); + } + set shorthand(value: boolean) { + this.flags = setFlag(this.flags, Flag.shorthand, value); + } + declare(kind: string, init: ExpressionEntity): LocalVariable[] { this.declarationInit = init; return (this.value as PatternNode).declare(kind, UNKNOWN_EXPRESSION); @@ -25,8 +40,9 @@ export default class Property extends MethodBase implements PatternNode { hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) - .propertyReadSideEffects; + const propertyReadSideEffects = ( + this.scope.context.options.treeshake as NormalizedTreeshakingOptions + ).propertyReadSideEffects; return ( (this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always') || this.key.hasEffects(context) || @@ -49,7 +65,7 @@ export default class Property extends MethodBase implements PatternNode { this.deoptimized = true; if (this.declarationInit !== null) { this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } } diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index e7068843649..6f15cc7c112 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -4,6 +4,7 @@ import type { NodeInteraction, NodeInteractionCalled } from '../NodeInteractions import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import type * as NodeType from './NodeType'; import type PrivateIdentifier from './PrivateIdentifier'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { type ExpressionEntity, type LiteralValueOrUnknown, @@ -13,12 +14,18 @@ import { import { type ExpressionNode, NodeBase } from './shared/Node'; export default class PropertyDefinition extends NodeBase { - declare computed: boolean; declare key: ExpressionNode | PrivateIdentifier; declare static: boolean; declare type: NodeType.tPropertyDefinition; declare value: ExpressionNode | null; + get computed(): boolean { + return isFlagSet(this.flags, Flag.computed); + } + set computed(value: boolean) { + this.flags = setFlag(this.flags, Flag.computed, value); + } + deoptimizeArgumentsOnInteractionAtPath( interaction: NodeInteraction, path: ObjectPath, diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index 5493078dbe6..17ac00da806 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -48,7 +48,7 @@ export default class RestElement extends NodeBase implements PatternNode { this.deoptimized = true; if (this.declarationInit !== null) { this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } } diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index 88c3920ecf0..4837815725c 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -26,7 +26,7 @@ export default class SpreadElement extends NodeBase { hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - const { propertyReadSideEffects } = this.context.options + const { propertyReadSideEffects } = this.scope.context.options .treeshake as NormalizedTreeshakingOptions; return ( this.argument.hasEffects(context) || @@ -45,6 +45,6 @@ export default class SpreadElement extends NodeBase { // Only properties of properties of the argument could become subject to reassignment // This will also reassign the return values of iterators this.argument.deoptimizePath([UnknownKey, UnknownKey]); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/StaticBlock.ts b/src/ast/nodes/StaticBlock.ts index 563cda267ba..93400915785 100644 --- a/src/ast/nodes/StaticBlock.ts +++ b/src/ast/nodes/StaticBlock.ts @@ -6,7 +6,7 @@ import { } from '../../utils/renderHelpers'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; -import type Scope from '../scopes/Scope'; +import type ChildScope from '../scopes/ChildScope'; import type * as NodeType from './NodeType'; import { type IncludeChildren, StatementBase, type StatementNode } from './shared/Node'; @@ -14,8 +14,8 @@ export default class StaticBlock extends StatementBase { declare body: readonly StatementNode[]; declare type: NodeType.tStaticBlock; - createScope(parentScope: Scope): void { - this.scope = new BlockScope(parentScope); + createScope(parentScope: ChildScope): void { + this.scope = new BlockScope(parentScope, this.scope.context); } hasEffects(context: HasEffectsContext): boolean { diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index afbed02347e..5db71aed448 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -27,7 +27,7 @@ export default class Super extends NodeBase { include(): void { if (!this.included) { this.included = true; - this.context.includeVariableInModule(this.variable); + this.scope.context.includeVariableInModule(this.variable); } } } diff --git a/src/ast/nodes/SwitchStatement.ts b/src/ast/nodes/SwitchStatement.ts index 9dfb0154a4c..6646ba52d17 100644 --- a/src/ast/nodes/SwitchStatement.ts +++ b/src/ast/nodes/SwitchStatement.ts @@ -7,7 +7,6 @@ import { } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import type ChildScope from '../scopes/ChildScope'; -import type Scope from '../scopes/Scope'; import type * as NodeType from './NodeType'; import type SwitchCase from './SwitchCase'; import type { ExpressionNode, GenericEsTreeNode, IncludeChildren } from './shared/Node'; @@ -21,9 +20,9 @@ export default class SwitchStatement extends StatementBase { private declare defaultCase: number | null; private declare parentScope: ChildScope; - createScope(parentScope: Scope): void { - this.parentScope = parentScope as ChildScope; - this.scope = new BlockScope(parentScope); + createScope(parentScope: ChildScope): void { + this.parentScope = parentScope; + this.scope = new BlockScope(parentScope, this.scope.context); } hasEffects(context: HasEffectsContext): boolean { @@ -94,7 +93,7 @@ export default class SwitchStatement extends StatementBase { } parseNode(esTreeNode: GenericEsTreeNode) { - this.discriminant = new (this.context.getNodeConstructor(esTreeNode.discriminant.type))( + this.discriminant = new (this.scope.context.getNodeConstructor(esTreeNode.discriminant.type))( esTreeNode.discriminant, this, this.parentScope diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index c47bc8cd706..0af7367a958 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -28,7 +28,7 @@ export default class TaggedTemplateExpression extends CallExpressionBase { const variable = this.scope.findVariable(name); if (variable.isNamespace) { - this.context.log(LOGLEVEL_WARN, logCannotCallNamespace(name), this.start); + this.scope.context.log(LOGLEVEL_WARN, logCannotCallNamespace(name), this.start); } } } @@ -87,7 +87,7 @@ export default class TaggedTemplateExpression extends CallExpressionBase { EMPTY_PATH, SHARED_RECURSION_TRACKER ); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } protected getReturnExpression( diff --git a/src/ast/nodes/TemplateElement.ts b/src/ast/nodes/TemplateElement.ts index 96dcd267aa3..0e45ccf5317 100644 --- a/src/ast/nodes/TemplateElement.ts +++ b/src/ast/nodes/TemplateElement.ts @@ -1,14 +1,21 @@ import type * as NodeType from './NodeType'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { type GenericEsTreeNode, NodeBase } from './shared/Node'; export default class TemplateElement extends NodeBase { - declare tail: boolean; declare type: NodeType.tTemplateElement; declare value: { cooked: string | null; raw: string; }; + get tail(): boolean { + return isFlagSet(this.flags, Flag.tail); + } + set tail(value: boolean) { + this.flags = setFlag(this.flags, Flag.tail, value); + } + // Do not try to bind value bind(): void {} diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index 490835e5819..9841fde28be 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -45,15 +45,17 @@ export default class ThisExpression extends NodeBase { include(): void { if (!this.included) { this.included = true; - this.context.includeVariableInModule(this.variable); + this.scope.context.includeVariableInModule(this.variable); } } initialise(): void { this.alias = - this.scope.findLexicalBoundary() instanceof ModuleScope ? this.context.moduleContext : null; + this.scope.findLexicalBoundary() instanceof ModuleScope + ? this.scope.context.moduleContext + : null; if (this.alias === 'undefined') { - this.context.log(LOGLEVEL_WARN, logThisIsUndefined(), this.start); + this.scope.context.log(LOGLEVEL_WARN, logThisIsUndefined(), this.start); } } diff --git a/src/ast/nodes/TryStatement.ts b/src/ast/nodes/TryStatement.ts index 8147d47b6e9..851d2cdd0a4 100644 --- a/src/ast/nodes/TryStatement.ts +++ b/src/ast/nodes/TryStatement.ts @@ -16,15 +16,16 @@ export default class TryStatement extends StatementBase { hasEffects(context: HasEffectsContext): boolean { return ( - ((this.context.options.treeshake as NormalizedTreeshakingOptions).tryCatchDeoptimization + ((this.scope.context.options.treeshake as NormalizedTreeshakingOptions).tryCatchDeoptimization ? this.block.body.length > 0 : this.block.hasEffects(context)) || !!this.finalizer?.hasEffects(context) ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - const tryCatchDeoptimization = (this.context.options.treeshake as NormalizedTreeshakingOptions) - ?.tryCatchDeoptimization; + const tryCatchDeoptimization = ( + this.scope.context.options.treeshake as NormalizedTreeshakingOptions + )?.tryCatchDeoptimization; const { brokenFlow, includedLabels } = context; if (!this.directlyIncluded || !tryCatchDeoptimization) { this.included = true; diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index 18443dd1224..980b29cc270 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -6,6 +6,7 @@ import { EMPTY_PATH, type ObjectPath, type PathTracker } from '../utils/PathTrac import Identifier from './Identifier'; import type { LiteralValue } from './Literal'; import type * as NodeType from './NodeType'; +import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import { type LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { type ExpressionNode, NodeBase } from './shared/Node'; @@ -24,9 +25,15 @@ const unaryOperators: { export default class UnaryExpression extends NodeBase { declare argument: ExpressionNode; declare operator: '!' | '+' | '-' | 'delete' | 'typeof' | 'void' | '~'; - declare prefix: boolean; declare type: NodeType.tUnaryExpression; + get prefix(): boolean { + return isFlagSet(this.flags, Flag.prefix); + } + set prefix(value: boolean) { + this.flags = setFlag(this.flags, Flag.prefix, value); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -61,7 +68,7 @@ export default class UnaryExpression extends NodeBase { this.deoptimized = true; if (this.operator === 'delete') { this.argument.deoptimizePath(EMPTY_PATH); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } } diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 3015dd14d46..21d034a6a2d 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -88,6 +88,6 @@ export default class UpdateExpression extends NodeBase { const variable = this.scope.findVariable(this.argument.name); variable.isReassigned = true; } - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } diff --git a/src/ast/nodes/shared/BitFlags.ts b/src/ast/nodes/shared/BitFlags.ts new file mode 100644 index 00000000000..f32934306b9 --- /dev/null +++ b/src/ast/nodes/shared/BitFlags.ts @@ -0,0 +1,32 @@ +export const enum Flag { + included = 1 << 0, + deoptimized = 1 << 1, + tdzAccessDefined = 1 << 2, + tdzAccess = 1 << 3, + assignmentDeoptimized = 1 << 4, + bound = 1 << 5, + isUndefined = 1 << 6, + optional = 1 << 7, + async = 1 << 8, + deoptimizedReturn = 1 << 9, + computed = 1 << 10, + hasLostTrack = 1 << 11, + hasUnknownDeoptimizedInteger = 1 << 12, + hasUnknownDeoptimizedProperty = 1 << 13, + directlyIncluded = 1 << 14, + deoptimizeBody = 1 << 15, + isBranchResolutionAnalysed = 1 << 16, + await = 1 << 17, + method = 1 << 18, + shorthand = 1 << 19, + tail = 1 << 20, + prefix = 1 << 21 +} + +export function isFlagSet(flags: number, flag: Flag): boolean { + return (flags & flag) !== 0; +} + +export function setFlag(flags: number, flag: Flag, value: boolean): number { + return (flags & ~flag) | (-value & flag); +} diff --git a/src/ast/nodes/shared/CallExpressionBase.ts b/src/ast/nodes/shared/CallExpressionBase.ts index be04e290029..04c045b5d93 100644 --- a/src/ast/nodes/shared/CallExpressionBase.ts +++ b/src/ast/nodes/shared/CallExpressionBase.ts @@ -72,7 +72,7 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo deoptimizePath(path: ObjectPath): void { if ( path.length === 0 || - this.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + this.scope.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) ) { return; } diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index b6c99e298fb..c9e6c9c031d 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -3,7 +3,6 @@ import type { HasEffectsContext, InclusionContext } from '../../ExecutionContext import type { NodeInteraction, NodeInteractionCalled } from '../../NodeInteractions'; import { INTERACTION_CALLED } from '../../NodeInteractions'; import ChildScope from '../../scopes/ChildScope'; -import type Scope from '../../scopes/Scope'; import { EMPTY_PATH, type ObjectPath, @@ -29,8 +28,8 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { private declare classConstructor: MethodDefinition | null; private objectEntity: ObjectEntity | null = null; - createScope(parentScope: Scope): void { - this.scope = new ChildScope(parentScope); + createScope(parentScope: ChildScope): void { + this.scope = new ChildScope(parentScope, this.scope.context); } deoptimizeArgumentsOnInteractionAtPath( @@ -131,7 +130,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { definition.deoptimizePath(UNKNOWN_PATH); } } - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } private getObjectEntity(): ObjectEntity { diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 6728ca26e02..b1cf9ca68fa 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -6,6 +6,7 @@ import type { ObjectPath, PathTracker, SymbolToStringTag } from '../../utils/Pat import { UNKNOWN_PATH } from '../../utils/PathTracker'; import type { LiteralValue } from '../Literal'; import type SpreadElement from '../SpreadElement'; +import { Flag, isFlagSet, setFlag } from './BitFlags'; import type { IncludeChildren } from './Node'; export const UnknownValue = Symbol('Unknown Value'); @@ -26,7 +27,14 @@ export interface InclusionOptions { } export class ExpressionEntity implements WritableEntity { - included = false; + protected flags: number = 0; + + get included(): boolean { + return isFlagSet(this.flags, Flag.included); + } + set included(value: boolean) { + this.flags = setFlag(this.flags, Flag.included, value); + } deoptimizeArgumentsOnInteractionAtPath( interaction: NodeInteraction, diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index ec028d0734c..7b6124c8440 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -16,6 +16,7 @@ import Identifier from '../Identifier'; import * as NodeType from '../NodeType'; import RestElement from '../RestElement'; import type SpreadElement from '../SpreadElement'; +import { Flag, isFlagSet, setFlag } from './BitFlags'; import type { ExpressionEntity, LiteralValueOrUnknown } from './Expression'; import { UNKNOWN_EXPRESSION, UNKNOWN_RETURN_EXPRESSION } from './Expression'; import { @@ -28,13 +29,26 @@ import type { ObjectEntity } from './ObjectEntity'; import type { PatternNode } from './Pattern'; export default abstract class FunctionBase extends NodeBase { - declare async: boolean; declare body: BlockStatement | ExpressionNode; declare params: readonly PatternNode[]; declare preventChildBlockScope: true; declare scope: ReturnValueScope; + + get async(): boolean { + return isFlagSet(this.flags, Flag.async); + } + set async(value: boolean) { + this.flags = setFlag(this.flags, Flag.async, value); + } + + get deoptimizedReturn(): boolean { + return isFlagSet(this.flags, Flag.deoptimizedReturn); + } + set deoptimizedReturn(value: boolean) { + this.flags = setFlag(this.flags, Flag.deoptimizedReturn, value); + } + protected objectEntity: ObjectEntity | null = null; - private deoptimizedReturn = false; deoptimizeArgumentsOnInteractionAtPath( interaction: NodeInteraction, @@ -110,7 +124,7 @@ export default abstract class FunctionBase extends NodeBase { if (!this.deoptimizedReturn) { this.deoptimizedReturn = true; this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH); - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } return UNKNOWN_RETURN_EXPRESSION; } @@ -131,7 +145,7 @@ export default abstract class FunctionBase extends NodeBase { } if (this.async) { - const { propertyReadSideEffects } = this.context.options + const { propertyReadSideEffects } = this.scope.context.options .treeshake as NormalizedTreeshakingOptions; const returnExpression = this.scope.getReturnExpression(); if ( diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 2d456407025..c328bd8ee63 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -14,7 +14,6 @@ import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import type { PatternNode } from './Pattern'; export default class FunctionNode extends FunctionBase { - declare async: boolean; declare body: BlockStatement; declare id: IdentifierWithVariable | null; declare params: readonly PatternNode[]; @@ -24,7 +23,7 @@ export default class FunctionNode extends FunctionBase { private declare constructedEntity: ObjectEntity; createScope(parentScope: FunctionScope): void { - this.scope = new FunctionScope(parentScope, this.context); + this.scope = new FunctionScope(parentScope, this.scope.context); this.constructedEntity = new ObjectEntity(Object.create(null), OBJECT_PROTOTYPE); // This makes sure that all deoptimizations of "this" are applied to the // constructed entity. diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index 1cb887c9d1c..b893dbc61be 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -14,6 +14,7 @@ import { SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; import type PrivateIdentifier from '../PrivateIdentifier'; +import { Flag, isFlagSet, setFlag } from './BitFlags'; import { type ExpressionEntity, type LiteralValueOrUnknown, @@ -23,11 +24,17 @@ import { type ExpressionNode, NodeBase } from './Node'; import type { PatternNode } from './Pattern'; export default class MethodBase extends NodeBase implements DeoptimizableEntity { - declare computed: boolean; declare key: ExpressionNode | PrivateIdentifier; declare kind: 'constructor' | 'method' | 'init' | 'get' | 'set'; declare value: ExpressionNode | (ExpressionNode & PatternNode); + get computed(): boolean { + return isFlagSet(this.flags, Flag.computed); + } + set computed(value: boolean) { + this.flags = setFlag(this.flags, Flag.computed, value); + } + private accessedValue: [expression: ExpressionEntity, isPure: boolean] | null = null; deoptimizeArgumentsOnInteractionAtPath( diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index e5442452f10..5981af20b8c 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -5,8 +5,6 @@ import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; import { ExpressionEntity } from './Expression'; export class MultiExpression extends ExpressionEntity { - included = false; - constructor(private expressions: readonly ExpressionEntity[]) { super(); } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 4b3b583159e..a9bbecd9b1a 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -16,11 +16,12 @@ import { } from '../../ExecutionContext'; import type { NodeInteractionAssigned } from '../../NodeInteractions'; import { INTERACTION_ASSIGNED } from '../../NodeInteractions'; -import { getAndCreateKeys, keys } from '../../keys'; +import { createKeysForNode, keys } from '../../keys'; import type ChildScope from '../../scopes/ChildScope'; import { EMPTY_PATH, UNKNOWN_PATH } from '../../utils/PathTracker'; import type Variable from '../../variables/Variable'; import type * as NodeType from '../NodeType'; +import { Flag, isFlagSet, setFlag } from './BitFlags'; import type { InclusionOptions } from './Expression'; import { ExpressionEntity } from './Expression'; @@ -33,13 +34,12 @@ export type IncludeChildren = boolean | typeof INCLUDE_PARAMETERS; export interface Node extends Entity { annotations?: RollupAnnotation[]; - context: AstContext; end: number; - esTreeNode: GenericEsTreeNode | null; + esTreeNode?: GenericEsTreeNode; included: boolean; - keys: string[]; needsBoundaries?: boolean; parent: Node | { type?: string }; + scope: ChildScope; preventChildBlockScope?: boolean; start: number; type: string; @@ -135,10 +135,8 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { declare annotationPure?: boolean; declare annotations?: RollupAnnotation[]; - context: AstContext; declare end: number; - esTreeNode: AstNode | null; - keys: string[]; + esTreeNode?: AstNode; parent: Node | { context: AstContext; type: string }; declare scope: ChildScope; declare start: number; @@ -147,13 +145,19 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { * This will be populated during initialise if setAssignedValue is called. */ protected declare assignmentInteraction: NodeInteractionAssigned; + /** * Nodes can apply custom deoptimizations once they become part of the * executed code. To do this, they must initialize this as false, implement * applyDeoptimizations and call this from include and hasEffects if they have * custom handlers */ - protected deoptimized = false; + protected get deoptimized(): boolean { + return isFlagSet(this.flags, Flag.deoptimized); + } + protected set deoptimized(value: boolean) { + this.flags = setFlag(this.flags, Flag.deoptimized, value); + } constructor( esTreeNode: GenericEsTreeNode, @@ -164,15 +168,21 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { super(); // Nodes can opt-in to keep the AST if needed during the build pipeline. // Avoid true when possible as large AST takes up memory. - this.esTreeNode = keepEsTreeNode ? esTreeNode : null; - this.keys = keys[esTreeNode.type] || getAndCreateKeys(esTreeNode); + + if (keepEsTreeNode) { + this.esTreeNode = esTreeNode; + } + + const { type } = esTreeNode; + keys[type] ||= createKeysForNode(esTreeNode); + this.parent = parent; - this.context = parent.context; + this.scope = parentScope; this.createScope(parentScope); this.parseNode(esTreeNode); this.initialise(); - this.context.magicString.addSourcemapLocation(this.start); - this.context.magicString.addSourcemapLocation(this.end); + this.scope.context.magicString.addSourcemapLocation(this.start); + this.scope.context.magicString.addSourcemapLocation(this.end); } addExportedVariables( @@ -185,7 +195,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { * that require the scopes to be populated with variables. */ bind(): void { - for (const key of this.keys) { + for (const key of keys[this.type]) { const value = (this as GenericEsTreeNode)[key]; if (Array.isArray(value)) { for (const child of value) { @@ -207,7 +217,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); - for (const key of this.keys) { + for (const key of keys[this.type]) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; if (Array.isArray(value)) { @@ -233,7 +243,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { ): void { if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; - for (const key of this.keys) { + for (const key of keys[this.type]) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; if (Array.isArray(value)) { @@ -262,13 +272,17 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { parseNode(esTreeNode: GenericEsTreeNode, keepEsTreeNodeKeys?: string[]): void { for (const [key, value] of Object.entries(esTreeNode)) { - // That way, we can override this function to add custom initialisation and then call super.parseNode + // Skip properties defined on the class already. + // This way, we can override this function to add custom initialisation and then call super.parseNode + // Note: this doesn't skip properties with defined getters/setters which we use to pack wrap booleans + // in bitfields. Those are still assigned from the value in the esTreeNode. if (this.hasOwnProperty(key)) continue; + if (key.charCodeAt(0) === 95 /* _ */) { if (key === ANNOTATION_KEY) { const annotations = value as RollupAnnotation[]; this.annotations = annotations; - if ((this.context.options.treeshake as NormalizedTreeshakingOptions).annotations) { + if ((this.scope.context.options.treeshake as NormalizedTreeshakingOptions).annotations) { this.annotationNoSideEffects = annotations.some( comment => comment.type === 'noSideEffects' ); @@ -276,13 +290,13 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } } else if (key === INVALID_ANNOTATION_KEY) { for (const { start, end, type } of value as RollupAnnotation[]) { - this.context.magicString.remove(start, end); + this.scope.context.magicString.remove(start, end); if (type === 'pure' || type === 'noSideEffects') { - this.context.log( + this.scope.context.log( LOGLEVEL_WARN, logInvalidAnnotation( - this.context.code.slice(start, end), - this.context.module.id, + this.scope.context.code.slice(start, end), + this.scope.context.module.id, type ), start @@ -298,7 +312,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { (this as GenericEsTreeNode)[key].push( child === null ? null - : new (this.context.getNodeConstructor(child.type))( + : new (this.scope.context.getNodeConstructor(child.type))( child, this, this.scope, @@ -307,7 +321,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { ); } } else { - (this as GenericEsTreeNode)[key] = new (this.context.getNodeConstructor(value.type))( + (this as GenericEsTreeNode)[key] = new (this.scope.context.getNodeConstructor(value.type))( value, this, this.scope, @@ -326,7 +340,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } render(code: MagicString, options: RenderOptions): void { - for (const key of this.keys) { + for (const key of keys[this.type]) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; if (Array.isArray(value)) { @@ -354,7 +368,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { */ protected applyDeoptimizations(): void { this.deoptimized = true; - for (const key of this.keys) { + for (const key of keys[this.type]) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; if (Array.isArray(value)) { @@ -365,22 +379,28 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { value.deoptimizePath(UNKNOWN_PATH); } } - this.context.requestTreeshakingPass(); + this.scope.context.requestTreeshakingPass(); } } export { NodeBase as StatementBase }; export function locateNode(node: Node): Location & { file: string } { - const location = locate(node.context.code, node.start, { offsetLine: 1 }) as Location & { + const { + start, + scope: { + context: { code, fileName } + } + } = node; + const location = locate(code, start, { offsetLine: 1 }) as Location & { file: string; }; - location.file = node.context.fileName; + location.file = fileName; location.toString = () => JSON.stringify(location); return location; } export function logNode(node: Node): string { - return node.context.code.slice(node.start, node.end); + return node.scope.context.code.slice(node.start, node.end); } diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index ac981bc3ef0..bc28f4d6611 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -10,6 +10,7 @@ import { UnknownKey, UnknownNonAccessorKey } from '../../utils/PathTracker'; +import { Flag, isFlagSet, setFlag } from './BitFlags'; import type { LiteralValueOrUnknown } from './Expression'; import { deoptimizeInteraction, @@ -32,15 +33,34 @@ export interface PropertyMap { const INTEGER_REG_EXP = /^\d+$/; export class ObjectEntity extends ExpressionEntity { + private get hasLostTrack(): boolean { + return isFlagSet(this.flags, Flag.hasLostTrack); + } + private set hasLostTrack(value: boolean) { + this.flags = setFlag(this.flags, Flag.hasLostTrack, value); + } + + private get hasUnknownDeoptimizedInteger(): boolean { + return isFlagSet(this.flags, Flag.hasUnknownDeoptimizedInteger); + } + private set hasUnknownDeoptimizedInteger(value: boolean) { + this.flags = setFlag(this.flags, Flag.hasUnknownDeoptimizedInteger, value); + } + + private get hasUnknownDeoptimizedProperty(): boolean { + return isFlagSet(this.flags, Flag.hasUnknownDeoptimizedProperty); + } + private set hasUnknownDeoptimizedProperty(value: boolean) { + this.flags = setFlag(this.flags, Flag.hasUnknownDeoptimizedProperty, value); + } + private readonly additionalExpressionsToBeDeoptimized = new Set(); private readonly allProperties: ExpressionEntity[] = []; private readonly deoptimizedPaths: Record = Object.create(null); private readonly expressionsToBeDeoptimizedByKey: Record = Object.create(null); private readonly gettersByKey: PropertyMap = Object.create(null); - private hasLostTrack = false; - private hasUnknownDeoptimizedInteger = false; - private hasUnknownDeoptimizedProperty = false; + private readonly propertiesAndGettersByKey: PropertyMap = Object.create(null); private readonly propertiesAndSettersByKey: PropertyMap = Object.create(null); private readonly settersByKey: PropertyMap = Object.create(null); diff --git a/src/ast/scopes/ChildScope.ts b/src/ast/scopes/ChildScope.ts index ff653f93071..11b177688ca 100644 --- a/src/ast/scopes/ChildScope.ts +++ b/src/ast/scopes/ChildScope.ts @@ -1,3 +1,4 @@ +import type { AstContext } from '../../Module'; import type { InternalModuleFormat } from '../../rollup/types'; import { getSafeName } from '../../utils/safeName'; import type ImportExpression from '../nodes/ImportExpression'; @@ -8,11 +9,13 @@ import Scope from './Scope'; export default class ChildScope extends Scope { readonly accessedOutsideVariables = new Map(); parent: Scope; + readonly context: AstContext; private declare accessedDynamicImports?: Set; - constructor(parent: Scope) { + constructor(parent: Scope, context: AstContext) { super(); this.parent = parent; + this.context = context; parent.children.push(this); } diff --git a/src/ast/scopes/ClassBodyScope.ts b/src/ast/scopes/ClassBodyScope.ts index d92e63c4b08..662ade5d6a3 100644 --- a/src/ast/scopes/ClassBodyScope.ts +++ b/src/ast/scopes/ClassBodyScope.ts @@ -10,12 +10,12 @@ export default class ClassBodyScope extends ChildScope { readonly thisVariable: LocalVariable; constructor(parent: Scope, classNode: ClassNode, context: AstContext) { - super(parent); + super(parent, context); this.variables.set( 'this', (this.thisVariable = new LocalVariable('this', null, classNode, context)) ); - this.instanceScope = new ChildScope(this); + this.instanceScope = new ChildScope(this, context); this.instanceScope.variables.set('this', new ThisVariable(context)); } diff --git a/src/ast/scopes/ModuleScope.ts b/src/ast/scopes/ModuleScope.ts index c976e3ff260..51239bca0d5 100644 --- a/src/ast/scopes/ModuleScope.ts +++ b/src/ast/scopes/ModuleScope.ts @@ -10,12 +10,10 @@ import ChildScope from './ChildScope'; import type GlobalScope from './GlobalScope'; export default class ModuleScope extends ChildScope { - readonly context: AstContext; declare parent: GlobalScope; constructor(parent: GlobalScope, context: AstContext) { - super(parent); - this.context = context; + super(parent, context); this.variables.set('this', new LocalVariable('this', null, UNDEFINED_EXPRESSION, context)); } diff --git a/src/ast/scopes/ParameterScope.ts b/src/ast/scopes/ParameterScope.ts index 38c9a3ed56e..b128610187b 100644 --- a/src/ast/scopes/ParameterScope.ts +++ b/src/ast/scopes/ParameterScope.ts @@ -12,13 +12,11 @@ export default class ParameterScope extends ChildScope { readonly hoistedBodyVarScope: ChildScope; parameters: readonly ParameterVariable[][] = []; - private readonly context: AstContext; private hasRest = false; constructor(parent: Scope, context: AstContext) { - super(parent); - this.context = context; - this.hoistedBodyVarScope = new ChildScope(this); + super(parent, context); + this.hoistedBodyVarScope = new ChildScope(this, context); } /** diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 94509b94aaa..22c8004c75e 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -185,7 +185,8 @@ export default class NamespaceVariable extends Variable { this.mergedNamespaces = mergedNamespaces; const moduleExecIndex = this.context.getModuleExecIndex(); for (const identifier of this.references) { - if (identifier.context.getModuleExecIndex() <= moduleExecIndex) { + const { context } = identifier.scope; + if (context.getModuleExecIndex() <= moduleExecIndex) { this.referencedEarly = true; break; } diff --git a/src/utils/parseImportAttributes.ts b/src/utils/parseImportAttributes.ts index f2c094062b2..18346089a33 100644 --- a/src/utils/parseImportAttributes.ts +++ b/src/utils/parseImportAttributes.ts @@ -12,7 +12,11 @@ import { logImportAttributeIsInvalid, logImportOptionsAreInvalid } from './logs' const ATTRIBUTE_KEYWORDS = new Set(['assert', 'with']); export function getAttributesFromImportExpression(node: ImportExpression): Record { - const { context, options, start } = node; + const { + scope: { context }, + options, + start + } = node; if (!(options instanceof ObjectExpression)) { if (options) { context.module.log(LOGLEVEL_WARN, logImportAttributeIsInvalid(context.module.id), start);