UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

19,170 lines • 598 kB
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Alexander Akait @alexander-akait
*/

"use strict";

const {
	ASTRAL_IDENTIFIER_PART_RANGES,
	ASTRAL_IDENTIFIER_START_RANGES,
	Field,
	Flag,
	IDENTIFIER_PART_RANGES,
	IDENTIFIER_START_RANGES,
	NODE_FIELDS,
	NodeType,
	Part
} = require("./data");

/** @typedef {{ label: string, keyword?: string, beforeExpr: boolean, startsExpr: boolean, isLoop: boolean, isAssign: boolean, prefix: boolean, postfix: boolean, binop: number | null, updateContext: ((prevType: TokenType) => void) | null }} TokenTypeShape */

/** @typedef {{ line: number, column: number }} PositionLike a position as any caller may state one */
/** @typedef {{ kind?: string | null, name?: string, statementStart?: number }} LabelLike a label in scope, as any caller may hold one */
/** @typedef {{ type: string, start: number, end: number, loc?: import("estree").SourceLocation | null, sourceFile?: string, range?: [number, number] }} NodeLike a node as this parser builds one, whichever fields the options asked for */
/**
 * @internal
 * @typedef {{ start: number, end: number, loc?: import("estree").SourceLocation | null, range?: [number, number], sourceFile?: string }} NodeOffsets what every node this parser builds carries besides its ESTree fields
 */
/**
 * @internal
 * @typedef {AssignmentProperty | CatchClause | Class | ClassBody | Expression | AnyFunction | Identifier | Literal | MethodDefinition | ModuleDeclaration | ModuleSpecifier | Pattern | PrivateIdentifier | Program | Property | PropertyDefinition | SpreadElement | Statement | Super | SwitchCase | TemplateElement | VariableDeclarator} Node
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ImportAttribute", key: Identifier | Literal, value: Literal }} ImportAttribute
 */
/**
 * @internal
 * @typedef {FunctionDeclaration | VariableDeclaration | ClassDeclaration} Declaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "FunctionDeclaration", id: Identifier | null, body: BlockStatement, params: Pattern[], generator?: boolean, async?: boolean }} MaybeNamedFunctionDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ClassDeclaration", id: Identifier | null, superClass?: Expression | null, body: ClassBody }} MaybeNamedClassDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Property", value: Pattern, kind: "init", method: boolean, key: Expression, shorthand: boolean, computed: boolean }} AssignmentProperty
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ExpressionStatement", expression: Literal, directive: string }} Directive
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Literal", value?: RegExp | null, regex: { pattern: string, flags: string }, raw?: string }} RegExpLiteral
 */
/**
 * @internal
 * @typedef {"-" | "+" | "!" | "~" | "typeof" | "void" | "delete"} UnaryOperator
 */
/**
 * @internal
 * @typedef {"++" | "--"} UpdateOperator
 */
/**
 * @internal
 * @typedef {"||" | "&&" | "??"} LogicalOperator
 */
/**
 * @internal
 * @typedef {"==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "**" | "|" | "^" | "&" | "in" | "instanceof"} BinaryOperator
 */
/**
 * @internal
 * @typedef {"=" | "+=" | "-=" | "*=" | "/=" | "%=" | "**=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" | "||=" | "&&=" | "??="} AssignmentOperator
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "CatchClause", param: Pattern | null, body: BlockStatement }} CatchClause
 */
/**
 * @internal
 * @typedef {ClassDeclaration | ClassExpression} Class
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ClassBody", body: (MethodDefinition | PropertyDefinition | StaticBlock)[] }} ClassBody
 */
/**
 * @internal
 * @typedef {ArrayExpression | ArrowFunctionExpression | AssignmentExpression | AwaitExpression | BinaryExpression | CallExpression | ChainExpression | ClassExpression | ConditionalExpression | FunctionExpression | Identifier | ImportExpression | Literal | LogicalExpression | MemberExpression | MetaProperty | NewExpression | ObjectExpression | SequenceExpression | TaggedTemplateExpression | TemplateLiteral | ThisExpression | UnaryExpression | UpdateExpression | YieldExpression} Expression
 */
/**
 * @internal
 * @typedef {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} AnyFunction ESTree's `Function`, which a JSDoc type cannot be named
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Identifier", name: string }} Identifier
 */
/**
 * @internal
 * @typedef {SimpleLiteral | RegExpLiteral | BigIntLiteral} Literal
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "MethodDefinition", key: Expression | PrivateIdentifier, value: FunctionExpression, kind: "constructor" | "method" | "get" | "set", computed: boolean, static: boolean }} MethodDefinition
 */
/**
 * @internal
 * @typedef {ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration} ModuleDeclaration
 */
/**
 * @internal
 * @typedef {ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier} ModuleSpecifier
 */
/**
 * @internal
 * @typedef {Identifier | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | MemberExpression} Pattern
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "PrivateIdentifier", name: string }} PrivateIdentifier
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Program", sourceType: "script" | "module", body: (Directive | Statement | ModuleDeclaration)[], comments?: import("estree").Comment[] }} Program
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Property", key: Expression, value: Expression | Pattern, kind: "init" | "get" | "set", method: boolean, shorthand: boolean, computed: boolean }} Property
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "PropertyDefinition", key: Expression | PrivateIdentifier, value?: Expression | null, computed: boolean, static: boolean }} PropertyDefinition
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "SpreadElement", argument: Expression }} SpreadElement
 */
/**
 * @internal
 * @typedef {ExpressionStatement | BlockStatement | StaticBlock | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | Declaration} Statement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Super" }} Super
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "SwitchCase", test?: Expression | null, consequent: Statement[] }} SwitchCase
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "TemplateElement", tail: boolean, value: { cooked?: string | null, raw: string } }} TemplateElement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "VariableDeclarator", id: Pattern, init?: Expression | null }} VariableDeclarator
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "FunctionDeclaration", id: Identifier, body: BlockStatement, params: Pattern[], generator?: boolean, async?: boolean }} FunctionDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "VariableDeclaration", declarations: VariableDeclarator[], kind: "var" | "let" | "const" | "using" | "await using" }} VariableDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ClassDeclaration", id: Identifier, superClass?: Expression | null, body: ClassBody }} ClassDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "BlockStatement", body: Statement[], innerComments?: import("estree").Comment[] }} BlockStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ExpressionStatement", expression: Expression }} ExpressionStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ClassExpression", id?: Identifier | null, superClass?: Expression | null, body: ClassBody }} ClassExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "StaticBlock", body: Statement[], innerComments?: import("estree").Comment[] }} StaticBlock
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ArrayExpression", elements: (Expression | SpreadElement | null)[] }} ArrayExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ArrowFunctionExpression", expression: boolean, body: BlockStatement | Expression, params: Pattern[], generator?: boolean, async?: boolean }} ArrowFunctionExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "AssignmentExpression", operator: AssignmentOperator, left: Pattern | MemberExpression, right: Expression }} AssignmentExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "AwaitExpression", argument: Expression }} AwaitExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "BinaryExpression", operator: BinaryOperator, left: Expression | PrivateIdentifier, right: Expression }} BinaryExpression
 */
/**
 * @internal
 * @typedef {SimpleCallExpression | NewExpression} CallExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ChainExpression", expression: ChainElement }} ChainExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ConditionalExpression", test: Expression, alternate: Expression, consequent: Expression }} ConditionalExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "FunctionExpression", id?: Identifier | null, body: BlockStatement, params: Pattern[], generator?: boolean, async?: boolean }} FunctionExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ImportExpression", phase?: ImportPhase, source: Expression, options?: Expression | null }} ImportExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "LogicalExpression", operator: LogicalOperator, left: Expression, right: Expression }} LogicalExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "MemberExpression", object: Expression | Super, property: Expression | PrivateIdentifier, computed: boolean, optional: boolean }} MemberExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "MetaProperty", meta: Identifier, property: Identifier }} MetaProperty
 */
/**
 * @internal
 * @typedef {(Expression | SpreadElement)[]} CallArguments what a call or `new` passes
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "NewExpression", callee: Expression | Super, arguments: CallArguments }} NewExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ObjectExpression", properties: (Property | SpreadElement)[] }} ObjectExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "SequenceExpression", expressions: Expression[] }} SequenceExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "TaggedTemplateExpression", tag: Expression, quasi: TemplateLiteral }} TaggedTemplateExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "TemplateLiteral", quasis: TemplateElement[], expressions: Expression[] }} TemplateLiteral
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ThisExpression" }} ThisExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "UnaryExpression", operator: UnaryOperator, prefix: true, argument: Expression }} UnaryExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "UpdateExpression", operator: UpdateOperator, argument: Expression, prefix: boolean }} UpdateExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "YieldExpression", argument?: Expression | null, delegate: boolean }} YieldExpression
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Literal", value: string | boolean | number | null, raw?: string }} SimpleLiteral
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "Literal", value?: bigint | null, bigint: string, raw?: string }} BigIntLiteral
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ImportDeclaration", phase?: ImportPhase, specifiers: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[], attributes: ImportAttribute[], source: Literal }} ImportDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ExportNamedDeclaration", declaration?: Declaration | null, specifiers: ExportSpecifier[], attributes: ImportAttribute[], source?: Literal | null }} ExportNamedDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ExportDefaultDeclaration", declaration: MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | Expression }} ExportDefaultDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ExportAllDeclaration", exported: Identifier | Literal | null, attributes: ImportAttribute[], source: Literal }} ExportAllDeclaration
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ImportSpecifier", imported: Identifier | Literal, local: Identifier }} ImportSpecifier
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ImportDefaultSpecifier", local: Identifier }} ImportDefaultSpecifier
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ImportNamespaceSpecifier", local: Identifier }} ImportNamespaceSpecifier
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ExportSpecifier", local: Identifier | Literal, exported: Identifier | Literal }} ExportSpecifier
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ObjectPattern", properties: (AssignmentProperty | RestElement)[] }} ObjectPattern
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ArrayPattern", elements: (Pattern | null)[] }} ArrayPattern
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "RestElement", argument: Pattern }} RestElement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "AssignmentPattern", left: Pattern, right: Expression }} AssignmentPattern
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "EmptyStatement" }} EmptyStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "DebuggerStatement" }} DebuggerStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "WithStatement", object: Expression, body: Statement }} WithStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ReturnStatement", argument?: Expression | null }} ReturnStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "LabeledStatement", label: Identifier, body: Statement }} LabeledStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "BreakStatement", label?: Identifier | null }} BreakStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ContinueStatement", label?: Identifier | null }} ContinueStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "IfStatement", test: Expression, consequent: Statement, alternate?: Statement | null }} IfStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "SwitchStatement", discriminant: Expression, cases: SwitchCase[] }} SwitchStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ThrowStatement", argument: Expression }} ThrowStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "TryStatement", block: BlockStatement, handler?: CatchClause | null, finalizer?: BlockStatement | null }} TryStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "WhileStatement", test: Expression, body: Statement }} WhileStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "DoWhileStatement", body: Statement, test: Expression }} DoWhileStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ForStatement", init?: VariableDeclaration | Expression | null, test?: Expression | null, update?: Expression | null, body: Statement }} ForStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ForInStatement", left: VariableDeclaration | Pattern, right: Expression, body: Statement }} ForInStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ForOfStatement", await: boolean, left: VariableDeclaration | Pattern, right: Expression, body: Statement }} ForOfStatement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "CallExpression", optional: boolean, callee: Expression | Super, arguments: CallArguments }} SimpleCallExpression
 */
/**
 * @internal
 * @typedef {SimpleCallExpression | MemberExpression} ChainElement
 */
/**
 * @internal
 * @typedef {NodeOffsets & { type: "ParenthesizedExpression", expression: Expression }} ParenthesizedExpression the node `preserveParens` keeps for each parenthesized expression, which webpack never asks for
 */
/**
 * @internal
 * @typedef {Node | ImportAttribute | ParenthesizedExpression} AnyNode any node this parser builds
 */
/**
 * @internal
 * @typedef {AnyNode["type"]} AnyNodeType the `type` of a node this parser builds
 */
/**
 * @template {AnyNodeType} T
 * @internal
 * @typedef {Extract<AnyNode, { type: T }>} NodeOf the node whose `type` is `T`
 */
/**
 * @template N
 * @template K
 * @internal
 * @typedef {N extends AnyNode ? (K extends keyof N ? N[K] : never) : never} FieldOf what field `K` holds in whichever node of `N` has it
 */
/**
 * @internal
 * @typedef {AnyNode extends infer N ? (N extends AnyNode ? keyof N : never) : never} AnyField a field any node may have
 */
/**
 * @internal
 * @typedef {NodeLike & { [Field in Exclude<AnyField, keyof NodeLike>]?: FieldOf<AnyNode, Field> }} UnfinishedNode a node between `startNode` and `finishNode`: any field of any node, each with its own type
 */
/**
 * @template {AnyNodeType} T
 * @internal
 * @typedef {NodeLike & Partial<Omit<NodeOf<T>, "type" | "start" | "end">>} NodeInProgress a node `finishNode` will finish as `T`, for code that reads back the fields it set
 */
/** @typedef {import("estree").Program & NodeLike} ParsedProgram a program as `SyntaxParser.parse` returns it */
/** @typedef {(import("estree").Expression & NodeLike) | (NodeLike & { type: "ParenthesizedExpression", expression: ParsedExpression })} ParsedExpression an expression as `SyntaxParser.parseExpressionAt` returns it, including the parenthesized one `preserveParens` asks for */
/** @typedef {NodeLike & { token: string }} TokenLike */
/** @typedef {{ token: string, isExpr: boolean, preserveSpace?: boolean, override?: (parser: SyntaxParser) => void, generator?: boolean }} TokContextLike a tokenizer context, as the owned tokenizer reads one */
/** @typedef {3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 | "latest"} EcmaVersion the ECMAScript versions the parser accepts */
/** @typedef {(isBlock: boolean, text: string, start: number, end: number, startLoc?: Position | null, endLoc?: Position | null) => void} OnComment what the tokenizer calls for each comment */
/**
 * @typedef {object} Options what a caller may ask the parser for
 * @property {EcmaVersion} ecmaVersion which edition to parse
 * @property {("script" | "module" | "commonjs")=} sourceType the goal symbol
 * @property {boolean=} strict whether the code is strict whatever it says
 * @property {((lastTokEnd: number, lastTokEndLoc?: Position | null) => void)=} onInsertedSemicolon called where a semicolon was inserted
 * @property {((lastTokEnd: number, lastTokEndLoc?: Position | null) => void)=} onTrailingComma called at a trailing comma
 * @property {(boolean | "never")=} allowReserved whether reserved words may name things
 * @property {boolean=} allowReturnOutsideFunction whether `return` may sit at the top level
 * @property {boolean=} allowImportExportEverywhere whether module syntax may sit anywhere
 * @property {boolean=} allowAwaitOutsideFunction whether `await` may sit outside a function
 * @property {boolean=} allowSuperOutsideMethod whether `super` may sit outside a method
 * @property {boolean=} allowHashBang whether a leading `#!` line is skipped
 * @property {boolean=} checkPrivateFields whether private names must be declared
 * @property {boolean=} locations whether nodes carry `loc`
 * @property {{ line: number, column: number }=} startLocation where the first offset falls
 * @property {(((token: Token) => void) | Token[])=} onToken where tokens are reported
 * @property {((parser: SyntaxParser) => void)=} onTokenRead the parser itself at each token, for a reader that copies what it needs rather than taking a token of its own
 * @property {(OnComment | CollectedComment[])=} onComment where comments are reported
 * @property {boolean=} ranges whether nodes carry `range`
 * @property {NodeLike=} program a program node to append to
 * @property {string=} sourceFile the name errors and locations carry
 * @property {string=} directSourceFile the name every node carries
 * @property {boolean=} preserveParens whether parentheses become nodes
 */
/**
 * @typedef {object} ResolvedOptions the options once every default is filled in and every alternative spelling is normalized
 * @property {number} ecmaVersion the edition, as the number the parser compares against
 * @property {"script" | "module" | "commonjs"} sourceType the goal symbol
 * @property {boolean} strict whether the code is strict whatever it says
 * @property {((lastTokEnd: number, lastTokEndLoc?: Position | null) => void) | null} onInsertedSemicolon called where a semicolon was inserted
 * @property {((lastTokEnd: number, lastTokEndLoc?: Position | null) => void) | null} onTrailingComma called at a trailing comma
 * @property {boolean | "never"} allowReserved whether reserved words may name things
 * @property {boolean} allowReturnOutsideFunction whether `return` may sit at the top level
 * @property {boolean} allowImportExportEverywhere whether module syntax may sit anywhere
 * @property {boolean} allowAwaitOutsideFunction whether `await` may sit outside a function
 * @property {boolean} allowSuperOutsideMethod whether `super` may sit outside a method
 * @property {boolean} allowHashBang whether a leading `#!` line is skipped
 * @property {boolean} checkPrivateFields whether private names must be declared
 * @property {boolean} locations whether nodes carry `loc`
 * @property {{ line: number, column: number } | null} startLocation where the first offset falls
 * @property {((token: Token) => void) | null} onToken where tokens are reported
 * @property {((parser: SyntaxParser) => void) | null} onTokenRead the parser itself at each token
 * @property {OnComment | null} onComment where comments are reported
 * @property {boolean} ranges whether nodes carry `range`
 * @property {NodeLike | null} program a program node to append to
 * @property {string | null} sourceFile the name errors and locations carry
 * @property {string | null} directSourceFile the name every node carries
 * @property {boolean} preserveParens whether parentheses become nodes
 */
/** @typedef {Omit<ResolvedOptions, "ecmaVersion" | "allowReserved" | "allowAwaitOutsideFunction" | "allowSuperOutsideMethod" | "onToken" | "onComment"> & { ecmaVersion: EcmaVersion | number | null, allowReserved: boolean | "never" | null, allowAwaitOutsideFunction: boolean | null, allowSuperOutsideMethod: boolean | null, onToken: ((token: Token) => void) | Token[] | null, onComment: OnComment | CollectedComment[] | null }} DraftOptions the options while `getOptions` fills in defaults and normalizes them */

// The first code point above the Basic Multilingual Plane, where the surrogate
// pairs the astral tables describe begin.
const FIRST_ASTRAL = 0x10000;

/**
 * Decode one run-length table into flat `[start, end)` bounds. Kept until the
 * first non-ASCII classification asks for it, which most builds never do.
 * @param {number[]} ranges gap-and-length pairs
 * @param {number} base the code point the first gap counts from
 * @returns {Uint32Array} the bounds, ascending and non-overlapping
 */
const decodeRanges = (ranges, base) => {
	const bounds = new Uint32Array(ranges.length);
	let position = base;
	for (let i = 0; i < ranges.length; i += 2) {
		position += ranges[i];
		bounds[i] = position;
		position += ranges[i + 1];
		bounds[i + 1] = position;
	}
	return bounds;
};

/** @type {Uint32Array | null} */
let identifierStartBounds = null;
/** @type {Uint32Array | null} */
let identifierPartBounds = null;
/** @type {Uint32Array | null} */
let astralStartBounds = null;
/** @type {Uint32Array | null} */
let astralPartBounds = null;

/**
 * Binary search over flat `[start, end)` bounds. Both tests are written to
 * answer `false` for a `NaN` code, which is what `charCodeAt` past the end is.
 * @param {Uint32Array} bounds the decoded table
 * @param {number} code the code point to classify
 * @returns {boolean} whether a range covers it
 */
const inRanges = (bounds, code) => {
	let low = 0;
	let high = (bounds.length >> 1) - 1;
	while (low <= high) {
		const middle = (low + high) >> 1;
		const index = middle << 1;
		if (code >= bounds[index + 1]) low = middle + 1;
		else if (code >= bounds[index]) return true;
		else high = middle - 1;
	}
	return false;
};

/**
 * Whether a code point can start an identifier, matching acorn's tables.
 * @param {number} code the code point
 * @param {boolean=} astral whether code points above the BMP may match
 * @returns {boolean} whether it can start an identifier
 */
const isIdentifierStart = (code, astral) => {
	if (code < 65) return code === 36;
	if (code < 91) return true;
	if (code < 97) return code === 95;
	if (code < 123) return true;
	if (code <= 0xffff) {
		if (code < 0xaa) return false;
		if (identifierStartBounds === null) {
			identifierStartBounds = decodeRanges(IDENTIFIER_START_RANGES, 0x80);
		}
		return inRanges(identifierStartBounds, code);
	}
	if (astral === false) return false;
	if (astralStartBounds === null) {
		astralStartBounds = decodeRanges(
			ASTRAL_IDENTIFIER_START_RANGES,
			FIRST_ASTRAL
		);
	}
	return inRanges(astralStartBounds, code);
};

/**
 * Whether a code point can continue an identifier, matching acorn's tables.
 * @param {number} code the code point
 * @param {boolean=} astral whether code points above the BMP may match
 * @returns {boolean} whether it can continue an identifier
 */
const isIdentifierChar = (code, astral) => {
	if (code < 48) return code === 36;
	if (code < 58) return true;
	if (code < 65) return false;
	if (code < 91) return true;
	if (code < 97) return code === 95;
	if (code < 123) return true;
	if (code <= 0xffff) {
		if (code < 0xaa) return false;
		if (identifierPartBounds === null) {
			identifierPartBounds = decodeRanges(IDENTIFIER_PART_RANGES, 0x80);
		}
		return inRanges(identifierPartBounds, code);
	}
	if (astral === false) return false;
	if (astralPartBounds === null) {
		astralPartBounds = decodeRanges(
			ASTRAL_IDENTIFIER_PART_RANGES,
			FIRST_ASTRAL
		);
	}
	return inRanges(astralPartBounds, code);
};

// A whole line break, where CRLF counts as one.
const lineBreak = /\r\n?|\n|\u2028|\u2029/;
const lineBreakG = new RegExp(lineBreak.source, "g");
const nonASCIIwhitespace = /[\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]/;
const skipWhiteSpace = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g;

/**
 * @param {number} code the code point
 * @returns {boolean} whether it terminates a line
 */
const isNewLine = (code) =>
	code === 10 || code === 13 || code === 0x2028 || code === 0x2029;

/**
 * The offset just past the next line break at or after `from`.
 * @param {string} code the source text
 * @param {number} from where to start looking
 * @param {number=} end where to stop looking
 * @returns {number} the offset, or `-1` when there is no break
 */
const nextLineBreak = (code, from, end) => {
	const limit = end === undefined ? code.length : end;
	for (let i = from; i < limit; i++) {
		const next = code.charCodeAt(i);
		if (isNewLine(next)) {
			return i < limit - 1 && next === 13 && code.charCodeAt(i + 1) === 10
				? i + 2
				: i + 1;
		}
	}
	return -1;
};

/** @type {Record<string, RegExp>} */
const regexpCache = Object.create(null);

/**
 * A cached `^(?:a|b|…)$` matcher for a space-separated word list.
 * @param {string} words the list
 * @returns {RegExp} the matcher
 */
const wordsRegexp = (words) => {
	const cached = regexpCache[words];
	if (cached !== undefined) return cached;
	const built = new RegExp(`^(?:${words.replace(/ /g, "|")})$`);
	regexpCache[words] = built;
	return built;
};

/**
 * @param {number} code a code point
 * @returns {string} the string for it, encoded as UTF-16
 */
const codePointToString = (code) => {
	if (code <= 0xffff) return String.fromCharCode(code);
	const rest = code - 0x10000;
	return String.fromCharCode((rest >> 10) + 0xd800, (rest & 1023) + 0xdc00);
};

// Reserved word lists for the dialects the parser accepts.
const reservedWords = {
	3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile",
	5: "class enum extends super const export import",
	6: "enum",
	strict:
		"implements interface let package private protected public static yield",
	strictBind: "eval arguments"
};

const ecma5AndLessKeywords =
	"break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this";

const keywordLists = {
	5: ecma5AndLessKeywords,
	"5module": `${ecma5AndLessKeywords} export import`,
	6: `${ecma5AndLessKeywords} const class extends export import super`
};

/**
 * A token's kind, carrying what the parser needs to know about it without
 * re-inspecting the source: whether an expression may follow, its binary
 * precedence, and how it updates the tokenizer's context.
 */
class TokenType {
	/**
	 * @param {string} label how the token prints in errors
	 * @param {{ keyword?: string, beforeExpr?: boolean, startsExpr?: boolean, isLoop?: boolean, isAssign?: boolean, prefix?: boolean, postfix?: boolean, binop?: number }=} conf what the token allows
	 */
	constructor(label, conf) {
		const options = conf || {};
		this.label = label;
		this.keyword = options.keyword;
		this.beforeExpr = Boolean(options.beforeExpr);
		this.startsExpr = Boolean(options.startsExpr);
		this.isLoop = Boolean(options.isLoop);
		this.isAssign = Boolean(options.isAssign);
		this.prefix = Boolean(options.prefix);
		this.postfix = Boolean(options.postfix);
		this.binop = options.binop || null;
		/** @type {((prevType: TokenType) => void) | null} */
		this.updateContext = null;
	}
}

/**
 * @param {string} name how the operator prints
 * @param {number} prec its binary precedence
 * @returns {TokenType} the token type
 */
const binop = (name, prec) =>
	new TokenType(name, { beforeExpr: true, binop: prec });

const beforeExpr = { beforeExpr: true };
const startsExpr = { startsExpr: true };

/** @type {Record<string, TokenType>} */
const keywordTypes = {};

/**
 * Define a keyword's token type and record it under its name.
 * @param {string} name the keyword
 * @param {{ beforeExpr?: boolean, startsExpr?: boolean, isLoop?: boolean, prefix?: boolean, binop?: number }=} options what the keyword allows
 * @returns {TokenType} the token type
 */
const kw = (name, options) => {
	const conf = { ...options, keyword: name };
	const type = new TokenType(name, conf);
	keywordTypes[name] = type;
	return type;
};

const tokTypes = {
	num: new TokenType("num", startsExpr),
	regexp: new TokenType("regexp", startsExpr),
	string: new TokenType("string", startsExpr),
	name: new TokenType("name", startsExpr),
	privateId: new TokenType("privateId", startsExpr),
	eof: new TokenType("eof"),

	bracketL: new TokenType("[", { beforeExpr: true, startsExpr: true }),
	bracketR: new TokenType("]"),
	braceL: new TokenType("{", { beforeExpr: true, startsExpr: true }),
	braceR: new TokenType("}"),
	parenL: new TokenType("(", { beforeExpr: true, startsExpr: true }),
	parenR: new TokenType(")"),
	comma: new TokenType(",", beforeExpr),
	semi: new TokenType(";", beforeExpr),
	colon: new TokenType(":", beforeExpr),
	dot: new TokenType("."),
	question: new TokenType("?", beforeExpr),
	questionDot: new TokenType("?."),
	arrow: new TokenType("=>", beforeExpr),
	template: new TokenType("template"),
	invalidTemplate: new TokenType("invalidTemplate"),
	ellipsis: new TokenType("...", beforeExpr),
	backQuote: new TokenType("`", startsExpr),
	dollarBraceL: new TokenType("${", { beforeExpr: true, startsExpr: true }),

	eq: new TokenType("=", { beforeExpr: true, isAssign: true }),
	assign: new TokenType("_=", { beforeExpr: true, isAssign: true }),
	incDec: new TokenType("++/--", {
		prefix: true,
		postfix: true,
		startsExpr: true
	}),
	prefix: new TokenType("!/~", {
		beforeExpr: true,
		prefix: true,
		startsExpr: true
	}),
	logicalOR: binop("||", 1),
	logicalAND: binop("&&", 2),
	bitwiseOR: binop("|", 3),
	bitwiseXOR: binop("^", 4),
	bitwiseAND: binop("&", 5),
	equality: binop("==/!=/===/!==", 6),
	relational: binop("</>/<=/>=", 7),
	bitShift: binop("<</>>/>>>", 8),
	plusMin: new TokenType("+/-", {
		beforeExpr: true,
		binop: 9,
		prefix: true,
		startsExpr: true
	}),
	modulo: binop("%", 10),
	star: binop("*", 10),
	slash: binop("/", 10),
	starstar: new TokenType("**", { beforeExpr: true }),
	coalesce: binop("??", 1),

	_break: kw("break"),
	_case: kw("case", beforeExpr),
	_catch: kw("catch"),
	_continue: kw("continue"),
	_debugger: kw("debugger"),
	_default: kw("default", beforeExpr),
	_do: kw("do", { isLoop: true, beforeExpr: true }),
	_else: kw("else", beforeExpr),
	_finally: kw("finally"),
	_for: kw("for", { isLoop: true }),
	_function: kw("function", startsExpr),
	_if: kw("if"),
	_return: kw("return", beforeExpr),
	_switch: kw("switch"),
	_throw: kw("throw", beforeExpr),
	_try: kw("try"),
	_var: kw("var"),
	_const: kw("const"),
	_while: kw("while", { isLoop: true }),
	_with: kw("with"),
	_new: kw("new", { beforeExpr: true, startsExpr: true }),
	_this: kw("this", startsExpr),
	_super: kw("super", startsExpr),
	_class: kw("class", startsExpr),
	_extends: kw("extends", beforeExpr),
	_export: kw("export"),
	_import: kw("import", startsExpr),
	_null: kw("null", startsExpr),
	_true: kw("true", startsExpr),
	_false: kw("false", startsExpr),
	_in: kw("in", { beforeExpr: true, binop: 7 }),
	_instanceof: kw("instanceof", { beforeExpr: true, binop: 7 }),
	_typeof: kw("typeof", { beforeExpr: true, prefix: true, startsExpr: true }),
	_void: kw("void", { beforeExpr: true, prefix: true, startsExpr: true }),
	_delete: kw("delete", { beforeExpr: true, prefix: true, startsExpr: true })
};

/**
 * A tokenizer context: what kind of brace, paren or template the tokenizer is
 * inside, which decides how the next `/` or `}` reads.
 */
class TokContext {
	/**
	 * @param {string} token the opening token
	 * @param {boolean} isExpr whether the context holds an expression
	 * @param {boolean=} preserveSpace whether whitespace is significant inside
	 * @param {((parser: ParserInstance) => void)=} override how to read the next token
	 * @param {boolean=} generator whether the enclosing function is a generator
	 */
	constructor(token, isExpr, preserveSpace, override, generator) {
		this.token = token;
		this.isExpr = Boolean(isExpr);
		this.preserveSpace = Boolean(preserveSpace);
		this.override = override;
		this.generator = Boolean(generator);
	}
}

const tokContexts = {
	b_stat: new TokContext("{", false),
	b_expr: new TokContext("{", true),
	b_tmpl: new TokContext("${", false),
	p_stat: new TokContext("(", false),
	p_expr: new TokContext("(", true),
	q_tmpl: new TokContext("`", true, true, (parser) =>
		parser.tryReadTemplateToken()
	),
	f_stat: new TokContext("function", false),
	f_expr: new TokContext("function", true),
	f_expr_gen: new TokContext("function", true, false, undefined, true),
	f_gen: new TokContext("function", false, false, undefined, true)
};

/** A `{ line, column }` pair, as `options.locations` reports one. */
class Position {
	/**
	 * @param {number} line one-based line number
	 * @param {number} col zero-based column
	 */
	constructor(line, col) {
		this.line = line;
		this.column = col;
	}

	/**
	 * @param {number} n how far to move along the line
	 * @returns {Position} the shifted position
	 */
	offset(n) {
		return new Position(this.line, this.column + n);
	}
}

/** The `loc` a node carries when `options.locations` is on. */
class ParserSourceLocation {
	/**
	 * @param {{ sourceFile: string | null }} p the parser, for its `sourceFile`
	 * @param {PositionLike | null | undefined} start where the node starts
	 * @param {PositionLike | null | undefined} end where it ends, or nothing until `finishNode` sets it
	 */
	constructor(p, start, end) {
		this.start = start;
		this.end = end;
		if (p.sourceFile !== null) this.source = p.sourceFile;
	}
}

/**
 * The line and column an offset falls on, for callers that parsed without
 * `options.locations`.
 * @param {string} input the source text
 * @param {number} offset the offset to locate
 * @returns {Position} where it falls
 */
const getLineInfo = (input, offset) => {
	let line = 1;
	let cur = 0;
	for (;;) {
		const nextBreak = nextLineBreak(input, cur, offset);
		if (nextBreak < 0) return new Position(line, offset - cur);
		++line;
		cur = nextBreak;
	}
};

const defaultOptions = {
	ecmaVersion: null,
	sourceType: "script",
	strict: false,
	onInsertedSemicolon: null,
	onTrailingComma: null,
	allowReserved: null,
	allowReturnOutsideFunction: false,
	allowImportExportEverywhere: false,
	allowAwaitOutsideFunction: null,
	allowSuperOutsideMethod: null,
	allowHashBang: false,
	checkPrivateFields: true,
	locations: false,
	startLocation: null,
	onToken: null,
	onTokenRead: null,
	onComment: null,
	ranges: false,
	program: null,
	sourceFile: null,
	directSourceFile: null,
	preserveParens: false
};

/**
 * Turn an `onComment` array into the callback the tokenizer calls.
 * @param {{ locations: boolean, ranges: boolean }} options the normalized options
 * @param {CollectedComment[]} array where to push each comment
 * @returns {OnComment} the callback
 */
const pushComment = (options, array) =>
	/**
	 * @this {{ sourceFile: string | null }}
	 * @param {boolean} block whether it is a block comment
	 * @param {string} text its text
	 * @param {number} start where it starts
	 * @param {number} end where it ends
	 * @param {(Position | null)=} startLoc its start position
	 * @param {(Position | null)=} endLoc its end position
	 * @returns {void}
	 */
	function pushOneComment(block, text, start, end, startLoc, endLoc) {
		/** @type {CollectedComment} */
		const comment = {
			type: block ? "Block" : "Line",
			value: text,
			start,
			end
		};
		if (options.locations) {
			comment.loc = /** @type {SourceLocation} */ (
				new ParserSourceLocation(this, startLoc, endLoc)
			);
		}
		if (options.ranges) comment.range = [start, end];
		array.push(comment);
	};

let warnedAboutEcmaVersion = false;

/**
 * Fill in every option the parser reads, and normalize the two that are stated
 * in more than one way: `ecmaVersion` as a year, and array comment sinks.
 * @param {Partial<Options> | null | undefined} opts what the caller passed
 * @returns {ResolvedOptions} the normalized options
 */
const getOptions = (opts) => {
	const options = /** @type {DraftOptions} */ ({});
	for (const opt of Object.keys(defaultOptions)) {
		/** @type {Record<string, DraftOptions[keyof DraftOptions] | undefined>} */ (
			options
		)[opt] =
			opts && Object.prototype.hasOwnProperty.call(opts, opt)
				? opts[/** @type {keyof Options} */ (opt)]
				: defaultOptions[/** @type {keyof typeof defaultOptions} */ (opt)];
	}

	if (options.ecmaVersion === "latest") {
		options.ecmaVersion = 1e8;
	} else if (
		options.ecmaVersion === null ||
		options.ecmaVersion === undefined
	) {
		if (
			!warnedAboutEcmaVersion &&
			typeof console === "object" &&
			// eslint-disable-next-line no-console
			console.warn
		) {
			warnedAboutEcmaVersion = true;
			// eslint-disable-next-line no-console
			console.warn(
				"Since Acorn 8.0.0, options.ecmaVersion is required.\nDefaulting to 2020, but this will stop working in the future."
			);
		}
		options.ecmaVersion = 11;
	} else if (options.ecmaVersion >= 2015) {
		options.ecmaVersion -= 2009;
	}

	if (options.allowReserved === null || options.allowReserved === undefined) {
		options.allowReserved = options.ecmaVersion < 5;
	}

	if (
		!opts ||
		opts.allowHashBang === null ||
		opts.allowHashBang === undefined
	) {
		options.allowHashBang = options.ecmaVersion >= 14;
	}

	if (Array.isArray(options.onToken)) {
		const tokens = options.onToken;
		options.onToken = (/** @type {Token} */ token) => tokens.push(token);
	}
	if (Array.isArray(options.onComment)) {
		options.onComment = pushComment(options, options.onComment);
	}

	if (options.sourceType === "commonjs" && options.allowAwaitOutsideFunction) {
		throw new Error(
			"Cannot use allowAwaitOutsideFunction with sourceType: commonjs"
		);
	}

	return /** @type {ResolvedOptions} */ (options);
};

// Each scope gets a bitset that may contain these flags.
const SCOPE_TOP = 1;
const SCOPE_FUNCTION = 2;
const SCOPE_ASYNC = 4;
const SCOPE_GENERATOR = 8;
const SCOPE_ARROW = 16;
const SCOPE_SIMPLE_CATCH = 32;
const SCOPE_SUPER = 64;
const SCOPE_DIRECT_SUPER = 128;
const SCOPE_CLASS_STATIC_BLOCK = 256;
const SCOPE_CLASS_FIELD_INIT = 512;
const SCOPE_SWITCH = 1024;
const SCOPE_VAR = SCOPE_TOP | SCOPE_FUNCTION | SCOPE_CLASS_STATIC_BLOCK;

/**
 * @param {boolean} async whether the function is async
 * @param {boolean} generator whether it is a generator
 * @returns {number} the scope flags for its body
 */
const functionFlags = (async, generator) =>
	SCOPE_FUNCTION |
	(async ? SCOPE_ASYNC : 0) |
	(generator ? SCOPE_GENERATOR : 0);

// What kind of binding a name introduces, for `checkLVal*` and `declareName`.
const BIND_NONE = 0;
const BIND_VAR = 1;
const BIND_LEXICAL = 2;
const BIND_FUNCTION = 3;
const BIND_SIMPLE_CATCH = 4;
const BIND_OUTSIDE = 5;

/**
 * One lexical scope, holding the names declared directly in it. Each set is
 * built only once a name goes into it, which most scopes never do.
 */
class ParserScope {
	/**
	 * @param {number} flags what kind of scope it is
	 */
	constructor(flags) {
		this.flags = flags;
		/** @type {Set<string> | undefined} */
		this.var = undefined;
		/** @type {Set<string> | undefined} */
		this.lexical = undefined;
		/** @type {Set<string> | undefined} */
		this.functions = undefined;
		// The scope's first lexical name: a simple catch scope's parameter, and
		// the only element of `lexical` that anything reads by position.
		/** @type {string | undefined} */
		this.firstLexical = undefined;
		// WHY: the declarations that hoist to this scope, in source order, kept
		// for the consumer that would otherwise re-walk every statement to find
		// them again. Only a var scope fills it; `exitScope` hands it to the
		// node that owns the scope and clears the slot before pooling.
		/** @type {(NodeLike | number)[] | undefined} */
		this.hoisted = undefined;
	}
}

/** An ESTree node, as this parser builds one. */
class ParserNode {
	/**
	 * @param {SyntaxParser} parser the parser building it
	 * @param {number} pos where the node starts
	 * @param {Position=} loc the start position, when locations are tracked
	 */
	constructor(parser, pos, loc) {
		this.type = "";
		this.start = pos;
		this.end = 0;
		if (parser.options.locations) {
			this.loc = new ParserSourceLocation(
				parser,
				/** @type {Position} */ (loc),
				undefined
			);
		}
		if (parser.options.directSourceFile) {
			this.sourceFile = parser.options.directSourceFile;
		}
		if (parser.options.ranges) this.range = [pos, 0];
	}
}

/** A token as `options.onToken` receives one. */
class Token {
	/**
	 * @param {SyntaxParser} p the parser, read at its current token
	 */
	constructor(p) {
		this.type = p.type;
		this.value = p.value;
		this.start = p.start;
		this.end = p.end;
		if (p.options.locations) {
			this.loc = new ParserSourceLocation(p, p.startLoc, p.endLoc);
		}
		if (p.options.ranges) this.range = [p.start, p.end];
	}
}

/**
 * Where a shorthand assignment, trailing comma or parenthesized pattern was
 * seen, so the expression parser can raise once it knows whether it is
 * parsing an expression or a binding pattern.
 */
class DestructuringErrors {
	constructor() {
		this.shorthandAssign = -1;
		this.trailingComma = -1;
		this.parenthesizedAssign = -1;
		this.parenthesizedBind = -1;
		this.doubleProto = -1;
	}
}

/** A label in scope, and what may jump to it. */
class Label {
	/**
	 * @param {string | null} kind what the label names
	 * @param {string=} name the label's own name
	 * @param {number=} statementStart where the labelled statement starts
	 */
	constructor(kind, name, statementStart) {
		/** @type {string | null | undefined} */
		this.kind = kind;
		/** @type {string | undefined} */
		this.name = name;
		/** @type {number | undefined} */
		this.statementStart = statementStart;
	}
}

// How a function declaration was reached, which decides whether it may be
// unnamed and whether its name binds in the enclosing scope.

// A directive prologue entry, used to spot `"use strict"` before parsing.
const literalDirective = /^(?:'((?:\\[^]|[^'\\])*?)'|"((?:\\[^]|[^"\\])*?)")/;

// Only a pattern the host engine itself rejected reaches the regexp validator,
// so it and its Unicode property tables load on that path alone.
/** @type {typeof import("./regexp") | null} */
let regexpValidator = null;

const loneSurrogate =
	/(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/;

const keywordRelationalOperator = /^in(stanceof)?$/;

/**
 * Scan for `in` or `instanceof` where the first character is already `i`.
 * @param {SyntaxParser} parser the parser
 * @param {number} pos the operator's start
 * @returns {boolean} whether a keyword operator starts here
 */
const keywordOperatorAt = (parser, pos) => {
	let end = pos + 1;
	const stop = Math.min(parser.input.length, pos + 11);
	while (end < stop) {
		const code = parser.fullCharCodeAt(end);
		if (!isIdentifierChar(code, true)) break;
		end += code <= 0xffff ? 1 : 2;
	}
	return (
		(end === pos + 2 && parser.input.slice(pos, end) === "in") ||
		(end === pos + 10 && parser.input.slice(pos, end) === "instanceof")
	);
};

/**
 * @returns {typeof import("./regexp")} the validator, installed on first use
 */
const loadRegExpValidator = () => {
	if (regexpValidator === null) {
		regexpValidator = require("./regexp");
		regexpValidator.install(SyntaxParser);
	}
	return regexpValidator;
};

/** @type {never[]} */
const empty = [];

const loopLabel = new Label("loop");

const switchLabel = new Label("switch");

/**
 * @internal
 * @typedef {NodeOf<"VariableDeclaration">["kind"]} VariableKind which keyword opened a declaration
 */
/**
 * @template {boolean} P
 * @internal
 * @typedef {P extends true ? NodeOf<"ObjectPattern"> : NodeOf<"ObjectExpression">} ObjectNodeFor the object pattern when `P` says it binds, else the object literal
 */
/**
 * @internal
 * @typedef {Identifier | (NodeOf<"Literal"> & { value: string })} ModuleExportName a name an import or export states
 */
/**
 * @internal
 * @typedef {DestructuringErrors | null} RefDestructuringErrors what the expression parser recorded, when it records
 */
/**
 * @internal
 * @typedef {Record<string, boolean>} ExportedNames the names a module exported so far
 */
/**
 * @internal
 * @typedef {Record<string, boolean> | Set<string> | false | null} ClashCheck where repeated names are tracked, or `false`/`null` for nowhere
 */
/**
 * @internal
 * @typedef {Record<string, string>} PrivateNameMap the private names one class body declares
 */
/**
 * @internal
 * @typedef {FieldOf<AnyNode, "expression"> | undefined} ExpressionField what an `expression` field holds, in whichever node has one
 */
/**
 * @internal
 * @typedef {import("estree").Statement | import("estree").ModuleDeclaration} EstreeStatement a statement as `JavascriptParser` holds it
 */
/**
 * @internal
 * @typedef {string} LabelName the name a labelled statement states
 */
/**
 * @internal
 * @typedef {string | Identifier | NodeOf<"Literal">} ExportedName the name an export states, as text or as the node naming it
 */
/**
 * @internal
 * @typedef {NodeOf<"Property" | "SpreadElement"> & { computed?: boolean, method?: boolean, shorthand?: boolean }} ObjectMember a member of an object literal or pattern
 */
/**
 * @internal
 * @typedef {(this: SyntaxParser, left: Expression, startPos: number, startLoc: Position | undefined) => Expression} AfterLeftParse a hook `parseMaybeAssign` runs on the parsed left side
 */
/**
 * @internal
 * @typedef {string | number | bigint | RegExp | null} LiteralValue what a literal node holds
 */
/**
 * @internal
 * @typedef {boolean} BindsPattern whether an object or array binds names rather than builds a value
 */
/**
 * @internal
 * @typedef {SyntaxParser} ParserInstance the parser, where a public signature leaves its type unstated
 */
/**
 * @internal
 * @typedef {typeof SyntaxParser} SyntaxParserClass the parser class, where a public signature leaves its type unstated
 */
/**
 * @internal
 * @typedef {{ proto?: boolean, [name: string]: Record<string, boolean> | boolean | undefined }} PropHash the property names an object literal stated so far, by kind
 */

/**
 * The ECMAScript parser webpack owns, ported from acorn 8.18.0 so the bundler
 * ships no parser dependency. `WebpackParser` below subclasses it and
 * overrides the hot paths.
 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/state.js
 */
class SyntaxParser {
	/**
	 * @param {Partial<Options> | null | undefined} options parser options
	 * @param {string} input the source text
	 * @param {number=} startPos offset to start at
	 */
	constructor(options, input, startPos) {
		const resolved = getOptions(options);
		this.options = resolved;
		this.sourceFile = resolved.sourceFile;
		this.keywords = wordsRegexp(
			keywordLists[
				resolved.ecmaVersion >= 6
					? 6
					: resolved.sourceType === "module"
						? "5module"
						: 5
			]
		);
		let reserved = "";
		if (resolved.allowReserved !== true) {
			reserved =
				reservedWords[
					resolved.ecmaVersion >= 6 ? 6 : resolved.ecmaVersion === 5 ? 5 : 3
				];
			if (resolved.sourceType === "module") reserved += " await";
		}
		this.reservedWords = wordsRegexp(reserved);
		const reservedStrict = `${reserved ? `${reserved} ` : ""}${reservedWords.strict}`;
		this.reservedWordsStrict = wordsRegexp(reservedStrict);
		this.reservedWordsStrictBind = wordsRegexp(
			`${reservedStrict} ${reservedWords.strictBind}`
		);
		this.input = String(input);

		// Whether the last `readWord1` saw an escape, which stops a word from
		// reading as a keyword.
		this.containsEsc = false;

		this.pos = startPos || 0;
		this.curLine = 1;
		if (resolved.startLocation) {
			this.lineStart = this.pos - resolved.startLocation.column;
			this.curLine = resolved.startLocation.line;
		} else if (startPos) {
			this.lineStart = this.input.lastIndexOf("\n", startPos - 1) + 1;
			if (resolved.locations) {
				this.curLine = this.input
					.slice(0, this.lineStart)
					.split(lineBreak).length;
			}
		} else {
			this.lineStart = 0;
		}

		this.type = tokTypes.eof;
		/** @type {TokenValue} */
		this.value = null;
		// acorn writes each pair through one chained assignment, so the second
		// name of each is the slot created first
		this.end = this.pos;
		this.start = this.pos;
		/** @type {Position | undefined} */
		this.endLoc = this.curPosition();
		/** @type {Position | undefined} */
		this.startLoc = this.endLoc;

		/** @type {Position | null | undefined} */
		this.lastTokStartLoc = null;
		/** @type {Position | null | undefined} */
		this.lastTokEndLoc = null;
		this.lastTokEnd = this.pos;
		this.lastTokStart = this.pos;

		// Enough syntactic context to tell a regexp apart from a division.
		/** @type {TokContextLike[]} */
		this.context = this.initialContext();
		this.exprAllowed = true;

		this.inModule = resolved.sourceType === "module";
		this.strict =
			this.inModule ||
			resolved.strict === true ||
			this.strictDirective(this.pos);

		this.potentialArrowAt = -1;
		this.potentialArrowInForAwait = false;

		// Where a `yield`/`await` was seen, to check once it is known whether the
		// surrounding parameters were defaults.
		this.awaitIdentPos = 0;
		this.awaitPos = 0;
		this.yieldPos = 0;
		/** @type {LabelLike[]} */
		this.labels = [];
		/** @type {Record<string, NodeLike>} */
		this.undefinedExports = Object.create(null);

		if (
			this.pos === 0 &&
			resolved.allowHashBang &&
			this.input.slice(0, 2) === "#!"
		) {
			this.skipLineComment(2);
		}

		/** @type {ParserScope[]} */
		this.scopeStack = [];
		this.enterScope(
			resolved.sourceType === "commonjs" ? SCOPE_FUNCTION : SCOPE_TOP
		);

		/** @type {import("./regexp").RegExpValidationState | null} */
		this.regexpState = null;

		/** @type {PrivateNameStackEntry[]} */
		this.privateNameStack = [];
	}

	/** @returns {boolean} whether the innermost var scope is a function */
	get inFunction() {
		return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0;
	}

	/** @returns {boolean} whether the innermost var scope is a generator */
	get inGenerator() {
		return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0;
	}

	/** @returns {boolean} whether the innermost var scope is async */
	get inAsync() {
		return (this.currentVarScope().flags & SCOPE_ASYNC) > 0;
	}

	/** @returns {boolean} whether `await` is an operator here */
	get canAwait() {
		for (let i = this.scopeStack.length - 1; i >= 0; i--) {
			const { flags } = this.scopeStack[i];
			if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) {
				return false;
			}
			if (flags & SCOPE_FUNCTION) return (flags & SCOPE_ASYNC) > 0;
		}
		return Boolean(
			(this.inModule && this.options.ecmaVersion >= 13) ||
			this.options.allowAwaitOutsideFunction
		);
	}

	/** @returns {boolean} whether `return` is allowed here */
	get allowReturn() {
		if (this.inFunction) return true;
		if (
			this.options.allowReturnOutsideFunction &&
			this.currentVarScope().flags & SCOPE_TOP
		) {
			return true;
		}
		return false;
	}

	/** @returns {boolean} whether `super` may be referenced here */
	get allowSuper() {
		const { flags } = this.currentThisScope();
		return Boolean(
			(flags & SCOPE_SUPER) > 0 || this.options.allowSuperOutsideMethod
		);
	}

	/** @returns {boolean} whether `super()` may be called here */
	get allowDirectSuper() {
		return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0;
	}

	/** @returns {boolean | number} whether function declarations bind like `var` here */
	get treatFunctionsAsVar() {
		return this.treatFunctionsAsVarInScope(this.currentScope());
	}

	/** @returns {boolean} whether `new.target` may be referenced here */
	get allowNewDotTarget() {
		for (let i = this.scopeStack.length - 1; i >= 0; i--) {
			const { flags } = this.scopeStack[i];
			if (
				flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) ||
				(flags & SCOPE_FUNCTION && !(flags & SCOPE_ARROW))
			) {
				return true;
			}
		}
		return false;
	}

	/** @returns {boolean} whether a `using` declaration may appear here */
	get allowUsing() {
		const { flags } = this.currentScope();
		if (flags & SCOPE_SWITCH) return false;
		if (!this.inModule && flags & SCOPE_TOP) return false;
		return true;
	}

	/** @returns {boolean} whether the innermost var scope is a class static block */
	get inClassStaticBlock() {
		return (this.currentVarScope().flags & SCOPE_CLASS_STATIC_BLOCK) > 0;
	}

	/**
	 * @param {...((parser: SyntaxParserClass) => SyntaxParserClass)} plugins the plugins to apply
	 * @returns {SyntaxParserClass} the extended parser class
	 */
	static extend(...plugins) {
		/** @type {ParserClass} */
		let cls = this;
		for (let i = 0; i < plugins.length; i++) cls = plugins[i](cls);
		return cls;
	}

	/**
	 * @param {string} input the source text
	 * @param {Partial<Options>=} options parser options
	 * @returns {ParsedProgram} the parsed program
	 */
	static parse(input, options) {
		return /** @type {ParsedProgram} */ (new this(options, input).parse());
	}

	/**
	 * @param {string} input the source text
	 * @param {number} pos where the expression starts
	 * @param {Partial<Options>=} options parser options
	 * @returns {ParsedExpression} the parsed expression
	 */
	static parseExpressionAt(input, pos, options) {
		const parser = new this(options, input, pos);
		parser.nextToken();
		return /** @type {ParsedExpression} */ (parser.parseExpression());
	}

	/**
	 * @param {string} input the source text
	 * @param {Partial<Options>=} options parser options
	 * @returns {SyntaxParser} a parser positioned before the first token
	 */
	static tokenizer(input, options) {
		return new this(options, input);
	}

	/**
	 * Whether a directive prologue starting at `start` opens with `"use strict"`.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.19.0/acorn/src/parseutil.js
	 * @param {number} start where the prologue may begin
	 * @returns {boolean} whether the code is strict
	 */
	strictDirective(start) {
		if (this.options.ecmaVersion < 5) return false;
		let at = start;
		for (;;) {
			skipWhiteSpace.lastIndex = at;
			at += /** @type {RegExpExecArray} */ (skipWhiteSpace.exec(this.input))[0]
				.length;
			const match = literalDirective.exec(this.input.slice(at));
			if (!match) return false;
			if ((match[1] || match[2]) === "use strict") {
				skipWhiteSpace.lastIndex = at + match[0].length;
				const spaceAfter = /** @type {RegExpExecArray} */ (
					skipWhiteSpace.exec(this.input)
				);
				const end = spaceAfter.index + spaceAfter[0].length;
				const next = this.input.charAt(end);
				return (
					next === ";" ||
					next === "}" ||
					(lineBreak.test(spaceAfter[0]) &&
						!(
							/[(`.[+\-/*%<>=,?^&]/.test(next) ||
							(next === "!" && this.input.charAt(end + 1) === "=") ||
							(next === "i" && keywordOperatorAt(this, end))
						))
				);
			}
			at += match[0].length;

			skipWhiteSpace.lastIndex = at;
			at += /** @type {RegExpExecArray} */ (skipWhiteSpace.exec(this.input))[0]
				.length;
			if (this.input[at] === ";") at++;
		}
	}

	/**
	 * @param {number=} pos where the offending token starts
	 * @returns {never} never returns
	 */
	unexpected(pos) {
		this.raise(
			pos === null || pos === undefined ? this.start : pos,
			"Unexpected token"
		);
	}

	/** @returns {TokContextLike[]} the context stack a fresh parser starts with */
	initialContext() {
		return [tokContexts.b_stat];
	}

	/**
	 * @param {number} flags what kind of scope it is
	 * @returns {void}
	 */
	enterScope(flags) {
		this.scopeStack.push(new ParserScope(flags));
	}

	/**
	 * @param {{ flags: number }} scope the scope to ask about
	 * @returns {boolean | number} whether function names bind like `var` in it
	 */
	treatFunctionsAsVarInScope(scope) {
		return Boolean(
			scope.flags & SCOPE_FUNCTION ||
			(!this.inModule && scope.flags & SCOPE_TOP)
		);
	}

	/** @returns {ParserScope} the innermost scope */
	currentScope() {
		return this.scopeStack[this.scopeStack.length - 1];
	}

	/** @returns {ParserScope} the innermost scope `var` reaches */
	currentVarScope() {
		for (let i = this.scopeStack.length - 1; ; i--) {
			const scope = this.scopeStack[i];
			if (
				scope.flags &
				(SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK)
			) {
				return scope;
			}
		}
	}

	/** @returns {ParserScope} the innermost scope that binds `this` */
	currentThisScope() {
		for (let i = this.scopeStack.length - 1; ; i--) {
			const scope = this.scopeStack[i];
			if (
				scope.flags &
					(SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK) &&
				!(scope.flags & SCOPE_ARROW)
			) {
				return scope;
			}
		}
	}

	/**
	 * Report a parse error, naming where in the source it was found.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/location.js
	 * @param {number} pos where the error is
	 * @param {string} message what is wrong
	 * @returns {never} never returns
	 */
	raise(pos, message) {
		const loc = getLineInfo(this.input, pos);
		let text = `${message} (${loc.line}:${loc.column})`;
		if (this.sourceFile) text += ` in ${this.sourceFile}`;
		const err =
			/** @type {SyntaxError & { pos: number, loc: Position, raisedAt: number }} */ (
				new SyntaxError(text)
			);
		err.pos = pos;
		err.loc = loc;
		err.raisedAt = this.pos;
		throw err;
	}

	/**
	 * @param {number} pos where the error is
	 * @param {string} message what is wrong
	 * @returns {never} never returns
	 */
	raiseRecoverable(pos, message) {
		return this.raise(pos, message);
	}

	/** @returns {Position | undefined} where the tokenizer stands, when tracked */
	curPosition() {
		if (this.options.locations) {
			return new Position(this.curLine, this.pos - this.lineStart);
		}
	}

	/**
	 * @param {number} startSkip how many characters open the comment
	 * @returns {void}
	 */
	skipLineComment(startSkip) {
		const start = this.pos;
		const startLoc = this.options.onComment && this.curPosition();
		this.pos += startSkip;
		let ch = this.input.charCodeAt(this.pos);
		while (this.pos < this.input.length && !isNewLine(ch)) {
			ch = this.input.charCodeAt(++this.pos);
		}
		if (this.options.onComment) {
			this.options.onComment(
				false,
				this.input.slice(start + startSkip, this.pos),
				start,
				this.pos,
				startLoc,
				this.curPosition()
			);
		}
	}

	/**
	 * @returns {Program} the parsed program
	 */
	parse() {
		const node = /** @type {NodeInProgress<"Program">} */ (
			this.options.program || this.startNode()
		);
		this.nextToken();
		return this.catchStackOverflow(() => this.parseTopLevel(node));
	}

	/**
	 * @param {TokenType} type the token type to consume
	 * @returns {boolean} whether it was consumed
	 */
	eat(type) {
		if (this.type === type) {
			this.next();
			return true;
		}
		return false;
	}

	/**
	 * @param {string} name the contextual keyword
	 * @returns {boolean} whether the current token is that keyword
	 */
	isContextual(name) {
		return (
			this.type === tokTypes.name && this.value === name && !this.containsEsc
		);
	}

	/**
	 * @param {string} name the contextual keyword
	 * @returns {boolean} whether it was consumed
	 */
	eatContextual(name) {
		if (!this.isContextual(name)) return false;
		this.next();
		return true;
	}

	/**
	 * Turn the host's stack overflow into a parse error, since a deeply nested
	 * expression is an input the caller can act on rather than a crash.
	 * @template T
	 * @param {() => T} f the work to run
	 * @returns {T} whatever it returned
	 */
	catchStackOverflow(f) {
		try {
			return f();
		} catch (err) {
			if (
				err instanceof Error &&
				(/\bstack\b.*\b(exceeded|overflow)\b/i.test(err.message) ||
					/\btoo much recursion\b/i.test(err.message))
			) {
				this.raise(this.start, "Not enough stack space to parse input");
			}
			throw err;
		}
	}

	/**
	 * @param {string} name the contextual keyword that must follow
	 * @returns {void}
	 */
	expectContextual(name) {
		if (!this.eatContextual(name)) this.unexpected();
	}

	/** @returns {boolean} whether a semicolon may be inserted here */
	canInsertSemicolon() {
		return (
			this.type === tokTypes.eof ||
			this.type === tokTypes.braceR ||
			lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
		);
	}

	/** @returns {boolean | undefined} whether one was inserted */
	insertSemicolon() {
		if (this.canInsertSemicolon()) {
			if (this.options.onInsertedSemicolon) {
				this.options.onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc);
			}
			return true;
		}
	}

	/** @returns {void} */
	semicolon() {
		if (!this.eat(tokTypes.semi) && !this.insertSemicolon()) this.unexpected();
	}

	/**
	 * @param {TokenType} tokType the token that may close the list
	 * @param {boolean=} notNext whether to leave the token unconsumed
	 * @returns {boolean | undefined} whether a trailing comma was accepted
	 */
	afterTrailingComma(tokType, notNext) {
		if (this.type === tokType) {
			if (this.options.onTrailingComma) {
				this.options.onTrailingComma(this.lastTokStart, this.lastTokStartLoc);
			}
			if (!notNext) this.next();
			return true;
		}
	}

	/**
	 * @param {TokenType} type the token type that must follow
	 * @returns {void}
	 */
	expect(type) {
		if (!this.eat(type)) this.unexpected();
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @param {boolean=} isAssign whether the target is an assignment
	 * @returns {void}
	 */
	checkPatternErrors(refDestructuringErrors, isAssign) {
		if (!refDestructuringErrors) return;
		if (refDestructuringErrors.trailingComma > -1) {
			this.raiseRecoverable(
				refDestructuringErrors.trailingComma,
				"Comma is not permitted after the rest element"
			);
		}
		const parens = isAssign
			? refDestructuringErrors.parenthesizedAssign
			: refDestructuringErrors.parenthesizedBind;
		if (parens > -1) {
			this.raiseRecoverable(
				parens,
				isAssign ? "Assigning to rvalue" : "Parenthesized pattern"
			);
		}
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @param {boolean=} andThrow whether to raise rather than report
	 * @returns {boolean | undefined} whether anything was recorded
	 */
	checkExpressionErrors(refDestructuringErrors, andThrow) {
		if (!refDestructuringErrors) return false;
		const { shorthandAssign, doubleProto } = refDestructuringErrors;
		if (!andThrow) return shorthandAssign >= 0 || doubleProto >= 0;
		if (shorthandAssign >= 0) {
			this.raise(
				shorthandAssign,
				"Shorthand property assignments are valid only in destructuring patterns"
			);
		}
		if (doubleProto >= 0) {
			this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property");
		}
	}

	/** @returns {void} */
	checkYieldAwaitInDefaultParams() {
		if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos)) {
			this.raise(this.yieldPos, "Yield expression cannot be a default value");
		}
		if (this.awaitPos) {
			this.raise(this.awaitPos, "Await expression cannot be a default value");
		}
	}

	/**
	 * @param {Expression | ParenthesizedExpression} expr the expression to test
	 * @returns {boolean} whether it may be assigned to without destructuring
	 */
	isSimpleAssignTarget(expr) {
		if (expr.type === "ParenthesizedExpression") {
			return this.isSimpleAssignTarget(expr.expression);
		}
		return expr.type === "Identifier" || expr.type === "MemberExpression";
	}

	/**
	 * Read statements until the end of input and wrap them in a `Program`.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {NodeInProgress<"Program">} node the program node to fill
	 * @returns {Program} the finished program
	 */
	parseTopLevel(node) {
		const exports = Object.create(null);
		if (!node.body) node.body = [];
		while (this.type !== tokTypes.eof) {
			const stmt = this.parseStatement(null, true, exports);
			node.body.push(stmt);
		}
		if (this.inModule) {
			for (const name of Object.keys(this.undefinedExports)) {
				this.raiseRecoverable(
					this.undefinedExports[name].start,
					`Export '${name}' is not defined`
				);
			}
		}
		this.adaptDirectivePrologue(node.body);
		this.next();
		node.sourceType =
			this.options.sourceType === "commonjs"
				? "script"
				: this.options.sourceType;
		return this.finishNode(node, "Program");
	}

	/**
	 * Whether a `let` here opens a lexical declaration rather than naming a
	 * variable, which needs a look-ahead past the keyword.
	 * @param {string | null=} context the statement context
	 * @returns {boolean} whether it declares
	 */
	isLet(context) {
		if (this.options.ecmaVersion < 6 || !this.isContextual("let")) return false;
		skipWhiteSpace.lastIndex = this.pos;
		const skip = /** @type {RegExpExecArray} */ (
			skipWhiteSpace.exec(this.input)
		);
		let next = this.pos + skip[0].length;
		let nextCh = this.fullCharCodeAt(next);
		// `[` and `\` are an explicit negative look-ahead for an expression
		// statement, so they declare even where only a statement is allowed.
		if (nextCh === 91 || nextCh === 92) return true;
		if (context) return false;

		if (nextCh === 123) return true;
		if (isIdentifierStart(nextCh, true)) {
			const start = next;
			do {
				next += nextCh <= 0xffff ? 1 : 2;
				nextCh = this.fullCharCodeAt(next);
			} while (isIdentifierChar(nextCh, true));
			if (nextCh === 92) return true;
			const ident = this.input.slice(start, next);
			if (!keywordRelationalOperator.test(ident)) return true;
		}
		return false;
	}

	/**
	 * Whether `async` here heads a function declaration, which no line break may
	 * separate from the `function` keyword.
	 * @returns {boolean} whether it does
	 */
	isAsyncFunction() {
		if (this.options.ecmaVersion < 8 || !this.isContextual("async")) {
			return false;
		}

		skipWhiteSpace.lastIndex = this.pos;
		const skip = /** @type {RegExpExecArray} */ (
			skipWhiteSpace.exec(this.input)
		);
		const next = this.pos + skip[0].length;
		let after;
		return (
			!lineBreak.test(this.input.slice(this.pos, next)) &&
			this.input.slice(next, next + 8) === "function" &&
			(next + 8 === this.input.length ||
				!(
					isIdentifierChar((after = this.fullCharCodeAt(next + 8)), true) ||
					after === 92
				))
		);
	}

	/**
	 * Whether `using` (or `await using`) here heads a declaration rather than
	 * naming a variable.
	 * @param {boolean} isAwaitUsing whether the `await` form is being tested
	 * @param {boolean=} isFor whether this is the head of a `for` statement
	 * @returns {boolean} whether it declares
	 */
	isUsingKeyword(isAwaitUsing, isFor) {
		if (
			this.options.ecmaVersion < 17 ||
			!this.isContextual(isAwaitUsing ? "await" : "using")
		) {
			return false;
		}

		skipWhiteSpace.lastIndex = this.pos;
		const skip = /** @type {RegExpExecArray} */ (
			skipWhiteSpace.exec(this.input)
		);
		let next = this.pos + skip[0].length;

		if (lineBreak.test(this.input.slice(this.pos, next))) return false;

		if (isAwaitUsing) {
			const usingEndPos = next + 5;
			let after;
			if (
				this.input.slice(next, usingEndPos) !== "using" ||
				usingEndPos === this.input.length ||
				isIdentifierChar((after = this.fullCharCodeAt(usingEndPos)), true) ||
				after === 92
			) {
				return false;
			}

			skipWhiteSpace.lastIndex = usingEndPos;
			const skipAfterUsing = /** @type {RegExpExecArray} */ (
				skipWhiteSpace.exec(this.input)
			);
			next = usingEndPos + skipAfterUsing[0].length;
			if (
				skipAfterUsing &&
				lineBreak.test(this.input.slice(usingEndPos, next))
			) {
				return false;
			}
		}

		let ch = this.fullCharCodeAt(next);
		if (!isIdentifierStart(ch, true) && ch !== 92) return false;
		const idStart = next;
		do {
			next += ch <= 0xffff ? 1 : 2;
			ch = this.fullCharCodeAt(next);
		} while (isIdentifierChar(ch, true));
		if (ch === 92) return true;
		const id = this.input.slice(idStart, next);
		if (keywordRelationalOperator.test(id)) return false;
		if (isFor && !isAwaitUsing && id === "of") {
			// `for (using of = …)` declares; `for (using of x)` iterates, so the
			// `=` that separates them decides, and `==`/`=>` are not it.
			skipWhiteSpace.lastIndex = next;
			const skipAfterOf = /** @type {RegExpExecArray} */ (
				skipWhiteSpace.exec(this.input)
			);
			next += skipAfterOf[0].length;
			if (
				this.input.charCodeAt(next) !== 61 ||
				(ch = this.input.charCodeAt(next + 1)) === 61 ||
				ch === 62
			) {
				return false;
			}
		}
		return true;
	}

	/**
	 * @param {boolean=} isFor whether this is the head of a `for` statement
	 * @returns {boolean} whether `await using` heads a declaration
	 */
	isAwaitUsing(isFor) {
		return this.isUsingKeyword(true, isFor);
	}

	/**
	 * @param {boolean=} isFor whether this is the head of a `for` statement
	 * @returns {boolean} whether `using` heads a declaration
	 */
	isUsing(isFor) {
		return this.isUsingKeyword(false, isFor);
	}

	/**
	 * Read one statement. A statement head that is only a keyword by position —
	 * `let`, `async`, `using` — is settled by the probes above.
	 * @param {string | null=} context what encloses the statement
	 * @param {boolean=} topLevel whether it sits at the top level
	 * @param {ExportedNames=} exports where module exports are tracked
	 * @returns {Statement | ModuleDeclaration} the statement
	 */
	parseStatement(context, topLevel, exports) {
		let startType = this.type;
		const node = this.startNode();
		/** @type {"let" | undefined} */
		let kind;

		if (this.isLet(context)) {
			startType = tokTypes._var;
			kind = "let";
		}

		switch (startType) {
			case tokTypes._break:
			case tokTypes._continue:
				return this.parseBreakContinueStatement(
					node,
					/** @type {string} */ (startType.keyword)
				);
			case tokTypes._debugger:
				return this.parseDebuggerStatement(node);
			case tokTypes._do:
				return this.parseDoStatement(node);
			case tokTypes._for:
				return this.parseForStatement(node);
			case tokTypes._function:
				// A function is the whole body of an `if` or a label, but not of a
				// label that is itself the whole body of an `if`.
				if (
					context &&
					(this.strict || (context !== "if" && context !== "label")) &&
					this.options.ecmaVersion >= 6
				) {
					this.unexpected();
				}
				return this.parseFunctionStatement(node, false, !context);
			case tokTypes._class:
				if (context) this.unexpected();
				return /** @type {NodeOf<"ClassDeclaration">} */ (
					this.parseClass(node, true)
				);
			case tokTypes._if:
				return this.parseIfStatement(node);
			case tokTypes._return:
				return this.parseReturnStatement(node);
			case tokTypes._switch:
				return this.parseSwitchStatement(node);
			case tokTypes._throw:
				return this.parseThrowStatement(node);
			case tokTypes._try:
				return this.parseTryStatement(node);
			case tokTypes._const:
			case tokTypes._var: {
				const varKind = kind || /** @type {VariableKind} */ (this.value);
				if (context && varKind !== "var") this.unexpected();
				return this.parseVarStatement(node, varKind);
			}
			case tokTypes._while:
				return this.parseWhileStatement(node);
			case tokTypes._with:
				return this.parseWithStatement(node);
			case tokTypes.braceL:
				return this.parseBlock(true, node);
			case tokTypes.semi:
				return this.parseEmptyStatement(node);
			case tokTypes._export:
			case tokTypes._import: {
				if (this.options.ecmaVersion > 10 && startType === tokTypes._import) {
					skipWhiteSpace.lastIndex = this.pos;
					const skip = /** @type {RegExpExecArray} */ (
						skipWhiteSpace.exec(this.input)
					);
					const next = this.pos + skip[0].length;
					const nextCh = this.input.charCodeAt(next);
					if (nextCh === 40 || nextCh === 46) {
						return this.parseExpressionStatement(node, this.parseExpression());
					}
				}

				if (!this.options.allowImportExportEverywhere) {
					if (!topLevel) {
						this.raise(
							this.start,
							"'import' and 'export' may only appear at the top level"
						);
					}
					if (!this.inModule) {
						this.raise(
							this.start,
							"'import' and 'export' may appear only with 'sourceType: module'"
						);
					}
				}
				return startType === tokTypes._import
					? this.parseImport(node)
					: this.parseExport(node, exports);
			}

			default: {
				if (this.isAsyncFunction()) {
					if (context) this.unexpected();
					this.next();
					return this.parseFunctionStatement(node, true, !context);
				}

				const usingKind = this.isAwaitUsing(false)
					? "await using"
					: this.isUsing(false)
						? "using"
						: null;
				if (usingKind) {
					if (!this.allowUsing) {
						this.raise(
							this.start,
							"Using declaration cannot appear in the top level when source type is `script` or in the bare case statement"
						);
					}
					if (context) {
						this.raise(
							this.start,
							"Using declaration is not allowed in single-statement positions"
						);
					}
					if (usingKind === "await using") {
						if (!this.canAwait) {
							this.raise(
								this.start,
								"Await using cannot appear outside of async function"
							);
						}
						this.next();
					}
					this.next();
					this.parseVar(node, false, usingKind);
					this.semicolon();
					return this.finishNode(node, "VariableDeclaration");
				}

				const maybeName = /** @type {string} */ (this.value);
				const expr = this.parseExpression();
				if (
					startType === tokTypes.name &&
					expr.type === "Identifier" &&
					this.eat(tokTypes.colon)
				) {
					return this.parseLabeledStatement(node, maybeName, expr, context);
				}
				return this.parseExpressionStatement(node, expr);
			}
		}
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {string} keyword either `break` or `continue`
	 * @returns {NodeOf<"BreakStatement"> | NodeOf<"ContinueStatement">} the finished statement
	 */
	parseBreakContinueStatement(node, keyword) {
		const isBreak = keyword === "break";
		this.next();
		if (this.eat(tokTypes.semi) || this.insertSemicolon()) {
			node.label = null;
		} else if (this.type !== tokTypes.name) {
			this.unexpected();
		} else {
			node.label = this.parseIdent();
			this.semicolon();
		}

		// A jump needs a destination in scope, so walk the labels for one this
		// keyword may reach.
		let i = 0;
		for (; i < this.labels.length; ++i) {
			const lab = this.labels[i];
			if (
				node.label === null ||
				node.label === undefined ||
				lab.name === node.label.name
			) {
				if (
					lab.kind !== null &&
					lab.kind !== undefined &&
					(isBreak || lab.kind === "loop")
				) {
					break;
				}
				if (node.label && isBreak) break;
			}
		}
		if (i === this.labels.length) {
			this.raise(node.start, `Unsyntactic ${keyword}`);
		}
		return this.finishNode(
			node,
			isBreak ? "BreakStatement" : "ContinueStatement"
		);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"DebuggerStatement">} the finished statement
	 */
	parseDebuggerStatement(node) {
		this.next();
		this.semicolon();
		return this.finishNode(node, "DebuggerStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"DoWhileStatement">} the finished statement
	 */
	parseDoStatement(node) {
		this.next();
		this.labels.push(loopLabel);
		node.body = /** @type {Statement} */ (this.parseStatement("do"));
		this.labels.pop();
		this.expect(tokTypes._while);
		node.test = this.parseParenExpression();
		if (this.options.ecmaVersion >= 6) this.eat(tokTypes.semi);
		else this.semicolon();
		return this.finishNode(node, "DoWhileStatement");
	}

	/**
	 * Read a `for` head, which is only known to be plain, `in` or `of` once its
	 * init part has been parsed with `in` held back as an operator.
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"ForStatement"> | NodeOf<"ForInStatement"> | NodeOf<"ForOfStatement">} the finished statement
	 */
	parseForStatement(node) {
		this.next();
		const awaitAt =
			this.options.ecmaVersion >= 9 &&
			this.canAwait &&
			this.eatContextual("await")
				? this.lastTokStart
				: -1;
		this.labels.push(loopLabel);
		this.enterScope(0);
		this.expect(tokTypes.parenL);
		if (this.type === tokTypes.semi) {
			if (awaitAt > -1) this.unexpected(awaitAt);
			return this.parseFor(node, null);
		}
		const isLet = this.isLet();
		if (this.type === tokTypes._var || this.type === tokTypes._const || isLet) {
			const init = this.startNode();
			const kind = isLet ? "let" : /** @type {VariableKind} */ (this.value);
			this.next();
			this.parseVar(init, true, kind);
			this.finishNode(init, "VariableDeclaration");
			return this.parseForAfterInit(
				node,
				/** @type {NodeOf<"VariableDeclaration">} */ (init),
				awaitAt
			);
		}
		const startsWithLet = this.isContextual("let");
		let isForOf = false;

		const usingKind = this.isUsing(true)
			? "using"
			: this.isAwaitUsing(true)
				? "await using"
				: null;
		if (usingKind) {
			const init = this.startNode();
			this.next();
			if (usingKind === "await using") {
				if (!this.canAwait) {
					this.raise(
						this.start,
						"Await using cannot appear outside of async function"
					);
				}
				this.next();
			}
			this.parseVar(init, true, usingKind);
			this.finishNode(init, "VariableDeclaration");
			return this.parseForAfterInit(
				node,
				/** @type {NodeOf<"VariableDeclaration">} */ (init),
				awaitAt
			);
		}
		const { containsEsc } = this;
		const refDestructuringErrors = new DestructuringErrors();
		const initPos = this.start;
		const init =
			awaitAt > -1
				? this.parseExprSubscripts(refDestructuringErrors, "await")
				: this.parseExpression(true, refDestructuringErrors);
		if (
			this.type === tokTypes._in ||
			(isForOf = this.options.ecmaVersion >= 6 && this.isContextual("of"))
		) {
			if (awaitAt > -1) {
				if (this.type === tokTypes._in) this.unexpected(awaitAt);
				node.await = true;
			} else if (isForOf && this.options.ecmaVersion >= 8) {
				if (
					init.start === initPos &&
					!containsEsc &&
					init.type === "Identifier" &&
					init.name === "async"
				) {
					this.unexpected();
				} else if (this.options.ecmaVersion >= 9) {
					node.await = false;
				}
			}
			if (startsWithLet && isForOf) {
				this.raise(
					init.start,
					"The left-hand side of a for-of loop may not start with 'let'."
				);
			}
			this.toAssignable(init, false, refDestructuringErrors);
			this.checkLValPattern(init);
			return this.parseForIn(node, /** @type {Pattern} */ (init));
		}
		this.checkExpressionErrors(refDestructuringErrors, true);
		if (awaitAt > -1) this.unexpected(awaitAt);
		return this.parseFor(node, init);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {NodeOf<"VariableDeclaration">} init the parsed declaration
	 * @param {number} awaitAt where `await` was seen, or `-1`
	 * @returns {NodeOf<"ForStatement"> | NodeOf<"ForInStatement"> | NodeOf<"ForOfStatement">} the finished statement
	 */
	parseForAfterInit(node, init, awaitAt) {
		if (
			(this.type === tokTypes._in ||
				(this.options.ecmaVersion >= 6 && this.isContextual("of"))) &&
			init.declarations.length === 1
		) {
			if (this.type === tokTypes._in) {
				if (
					(init.kind === "using" || init.kind === "await using") &&
					!init.declarations[0].init
				) {
					this.raise(
						this.start,
						"Using declaration is not allowed in for-in loops"
					);
				}
				if (this.options.ecmaVersion >= 9 && awaitAt > -1) {
					this.unexpected(awaitAt);
				}
			} else if (this.options.ecmaVersion >= 9) {
				node.await = awaitAt > -1;
			}
			return this.parseForIn(node, init);
		}
		if (awaitAt > -1) this.unexpected(awaitAt);
		return this.parseFor(node, init);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {boolean} isAsync whether it is an async function
	 * @param {boolean} declarationPosition whether the name binds here
	 * @returns {NodeOf<"FunctionDeclaration">} the finished statement
	 */
	parseFunctionStatement(node, isAsync, declarationPosition) {
		this.next();
		return /** @type {NodeOf<"FunctionDeclaration">} */ (
			this.parseFunction(
				node,
				FUNC_STATEMENT | (declarationPosition ? 0 : FUNC_HANGING_STATEMENT),
				false,
				isAsync
			)
		);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"IfStatement">} the finished statement
	 */
	parseIfStatement(node) {
		this.next();
		node.test = this.parseParenExpression();
		node.consequent = /** @type {Statement} */ (this.parseStatement("if"));
		node.alternate = this.eat(tokTypes._else)
			? /** @type {Statement} */ (this.parseStatement("if"))
			: null;
		return this.finishNode(node, "IfStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"ReturnStatement">} the finished statement
	 */
	parseReturnStatement(node) {
		if (!this.allowReturn) {
			this.raise(this.start, "'return' outside of function");
		}
		this.next();

		if (this.eat(tokTypes.semi) || this.insertSemicolon()) {
			node.argument = null;
		} else {
			node.argument = this.parseExpression();
			this.semicolon();
		}
		return this.finishNode(node, "ReturnStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"SwitchStatement">} the finished statement
	 */
	parseSwitchStatement(node) {
		this.next();
		node.discriminant = this.parseParenExpression();
		node.cases = [];
		this.expect(tokTypes.braceL);
		this.labels.push(switchLabel);
		this.enterScope(SCOPE_SWITCH);

		// Statements belong to the case that precedes them, so `cur` holds the one
		// being filled until the next `case` or `default` starts another.
		/** @type {NodeInProgress<"SwitchCase"> | undefined} */
		let cur;
		for (let sawDefault = false; this.type !== tokTypes.braceR;) {
			if (this.type === tokTypes._case || this.type === tokTypes._default) {
				const isCase = this.type === tokTypes._case;
				if (cur) this.finishNode(cur, "SwitchCase");
				cur = /** @type {NodeInProgress<"SwitchCase">} */ (this.startNode());
				node.cases.push(/** @type {NodeOf<"SwitchCase">} */ (cur));
				cur.consequent = [];
				this.next();
				if (isCase) {
					cur.test = this.parseExpression();
				} else {
					if (sawDefault) {
						this.raiseRecoverable(
							this.lastTokStart,
							"Multiple default clauses"
						);
					}
					sawDefault = true;
					cur.test = null;
				}
				this.expect(tokTypes.colon);
			} else {
				if (!cur) this.unexpected();
				/** @type {Statement[]} */ (cur.consequent).push(
					/** @type {Statement} */ (this.parseStatement(null))
				);
			}
		}
		this.exitScope();
		if (cur) this.finishNode(cur, "SwitchCase");
		this.next();
		this.labels.pop();
		return this.finishNode(node, "SwitchStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"ThrowStatement">} the finished statement
	 */
	parseThrowStatement(node) {
		this.next();
		if (lineBreak.test(this.input.slice(this.lastTokEnd, this.start))) {
			this.raise(this.lastTokEnd, "Illegal newline after throw");
		}
		node.argument = this.parseExpression();
		this.semicolon();
		return this.finishNode(node, "ThrowStatement");
	}

	/**
	 * @returns {Pattern} the catch clause's parameter
	 */
	parseCatchClauseParam() {
		const param = this.parseBindingAtom();
		const simple = param.type === "Identifier";
		this.enterScope(simple ? SCOPE_SIMPLE_CATCH : 0);
		this.checkLValPattern(param, simple ? BIND_SIMPLE_CATCH : BIND_LEXICAL);
		this.expect(tokTypes.parenR);

		return param;
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"TryStatement">} the finished statement
	 */
	parseTryStatement(node) {
		this.next();
		node.block = this.parseBlock();
		node.handler = null;
		if (this.type === tokTypes._catch) {
			const clause = this.startNode();
			this.next();
			if (this.eat(tokTypes.parenL)) {
				clause.param = this.parseCatchClauseParam();
			} else {
				if (this.options.ecmaVersion < 10) this.unexpected();
				clause.param = null;
				this.enterScope(0);
			}
			clause.body = this.parseBlock(false);
			this.exitScope();
			node.handler = this.finishNode(clause, "CatchClause");
		}
		node.finalizer = this.eat(tokTypes._finally) ? this.parseBlock() : null;
		if (!node.handler && !node.finalizer) {
			this.raise(node.start, "Missing catch or finally clause");
		}
		return this.finishNode(node, "TryStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {string} kind which declaration keyword opened it
	 * @param {boolean=} allowMissingInitializer whether a declarator may lack one
	 * @returns {NodeOf<"VariableDeclaration">} the finished statement
	 */
	parseVarStatement(node, kind, allowMissingInitializer) {
		this.next();
		this.parseVar(node, false, kind, allowMissingInitializer);
		this.semicolon();
		return this.finishNode(node, "VariableDeclaration");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"WhileStatement">} the finished statement
	 */
	parseWhileStatement(node) {
		this.next();
		node.test = this.parseParenExpression();
		this.labels.push(loopLabel);
		node.body = /** @type {Statement} */ (this.parseStatement("while"));
		this.labels.pop();
		return this.finishNode(node, "WhileStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"WithStatement">} the finished statement
	 */
	parseWithStatement(node) {
		if (this.strict) this.raise(this.start, "'with' in strict mode");
		this.next();
		node.object = this.parseParenExpression();
		node.body = /** @type {Statement} */ (this.parseStatement("with"));
		return this.finishNode(node, "WithStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"EmptyStatement">} the finished statement
	 */
	parseEmptyStatement(node) {
		this.next();
		return this.finishNode(node, "EmptyStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {LabelName} maybeName the label's name
	 * @param {Identifier} expr the identifier that named it
	 * @param {string | null=} context what encloses the statement
	 * @returns {NodeOf<"LabeledStatement">} the finished statement
	 */
	parseLabeledStatement(node, maybeName, expr, context) {
		for (const label of this.labels) {
			if (label.name === maybeName) {
				this.raise(expr.start, `Label '${maybeName}' is already declared`);
			}
		}
		const kind = this.type.isLoop
			? "loop"
			: this.type === tokTypes._switch
				? "switch"
				: null;
		for (let i = this.labels.length - 1; i >= 0; i--) {
			const label = this.labels[i];
			if (label.statementStart === node.start) {
				label.statementStart = this.start;
				label.kind = kind;
			} else {
				break;
			}
		}
		this.labels.push(new Label(kind, maybeName, this.start));
		node.body = /** @type {Statement} */ (
			this.parseStatement(
				context
					? !context.includes("label")
						? `${context}label`
						: context
					: "label"
			)
		);
		this.labels.pop();
		node.label = expr;
		return this.finishNode(node, "LabeledStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {Expression} expr the expression it holds
	 * @returns {NodeOf<"ExpressionStatement">} the finished statement
	 */
	parseExpressionStatement(node, expr) {
		node.expression = expr;
		this.semicolon();
		return this.finishNode(node, "ExpressionStatement");
	}

	/**
	 * @param {boolean=} createNewLexicalScope whether the block opens a scope
	 * @param {UnfinishedNode=} node the block node
	 * @param {boolean=} exitStrict whether to leave strict mode after it
	 * @returns {NodeOf<"BlockStatement">} the finished block
	 */
	parseBlock(createNewLexicalScope, node, exitStrict) {
		const opensScope =
			createNewLexicalScope === undefined ? true : createNewLexicalScope;
		const block = /** @type {NodeInProgress<"BlockStatement">} */ (
			node === undefined ? this.startNode() : node
		);

		block.body = [];
		this.expect(tokTypes.braceL);
		if (opensScope) this.enterScope(0);
		while (this.type !== tokTypes.braceR) {
			const stmt = /** @type {Statement} */ (this.parseStatement(null));
			block.body.push(stmt);
		}
		if (exitStrict) this.strict = false;
		this.next();
		if (opensScope) this.exitScope();
		return this.finishNode(block, "BlockStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {NodeOf<"VariableDeclaration"> | Expression | null} init the already-parsed init part
	 * @returns {NodeOf<"ForStatement">} the finished statement
	 */
	parseFor(node, init) {
		node.init = init;
		this.expect(tokTypes.semi);
		node.test = this.type === tokTypes.semi ? null : this.parseExpression();
		this.expect(tokTypes.semi);
		node.update = this.type === tokTypes.parenR ? null : this.parseExpression();
		this.expect(tokTypes.parenR);
		node.body = /** @type {Statement} */ (this.parseStatement("for"));
		this.exitScope();
		this.labels.pop();
		return this.finishNode(node, "ForStatement");
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {NodeOf<"VariableDeclaration"> | Pattern} init what the loop iterates over
	 * @returns {NodeOf<"ForInStatement"> | NodeOf<"ForOfStatement">} the finished statement
	 */
	parseForIn(node, init) {
		const isForIn = this.type === tokTypes._in;
		this.next();

		if (
			init.type === "VariableDeclaration" &&
			init.declarations[0].init !== null &&
			init.declarations[0].init !== undefined &&
			(!isForIn ||
				this.options.ecmaVersion < 8 ||
				this.strict ||
				init.kind !== "var" ||
				init.declarations[0].id.type !== "Identifier")
		) {
			this.raise(
				init.start,
				`${isForIn ? "for-in" : "for-of"} loop variable declaration may not have an initializer`
			);
		}
		node.left = init;
		node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
		this.expect(tokTypes.parenR);
		node.body = /** @type {Statement} */ (this.parseStatement("for"));
		this.exitScope();
		this.labels.pop();
		return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement");
	}

	/**
	 * @param {UnfinishedNode} node the declaration node
	 * @param {boolean} isFor whether it heads a `for` statement
	 * @param {string} kind which declaration keyword opened it
	 * @param {boolean=} allowMissingInitializer whether a declarator may lack one
	 * @returns {UnfinishedNode} the filled declaration
	 */
	parseVar(node, isFor, kind, allowMissingInitializer) {
		node.declarations = [];
		node.kind = /** @type {VariableKind} */ (kind);
		for (;;) {
			const decl = this.startNode();
			this.parseVarId(decl, kind);
			if (this.eat(tokTypes.eq)) {
				decl.init = this.parseMaybeAssign(isFor);
			} else if (
				!allowMissingInitializer &&
				kind === "const" &&
				!(
					this.type === tokTypes._in ||
					(this.options.ecmaVersion >= 6 && this.isContextual("of"))
				)
			) {
				this.unexpected();
			} else if (
				!allowMissingInitializer &&
				(kind === "using" || kind === "await using") &&
				this.options.ecmaVersion >= 17 &&
				this.type !== tokTypes._in &&
				!this.isContextual("of")
			) {
				this.raise(
					this.lastTokEnd,
					`Missing initializer in ${kind} declaration`
				);
			} else if (
				!allowMissingInitializer &&
				/** @type {Pattern} */ (decl.id).type !== "Identifier" &&
				!(isFor && (this.type === tokTypes._in || this.isContextual("of")))
			) {
				this.raise(
					this.lastTokEnd,
					"Complex binding patterns require an initialization value"
				);
			} else {
				decl.init = null;
			}
			node.declarations.push(this.finishNode(decl, "VariableDeclarator"));
			if (!this.eat(tokTypes.comma)) break;
		}
		return node;
	}

	/**
	 * @param {UnfinishedNode} decl the declarator node
	 * @param {string} kind which declaration keyword opened it
	 * @returns {void}
	 */
	parseVarId(decl, kind) {
		decl.id =
			kind === "using" || kind === "await using"
				? this.parseIdent()
				: this.parseBindingAtom();

		this.checkLValPattern(
			decl.id,
			kind === "var" ? BIND_VAR : BIND_LEXICAL,
			false
		);
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @param {number} statement how the function was reached
	 * @param {boolean=} allowExpressionBody whether a concise body is allowed
	 * @param {boolean=} isAsync whether it is async
	 * @param {(boolean | string)=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"FunctionDeclaration"> | NodeOf<"FunctionExpression">} the finished function
	 */
	parseFunction(node, statement, allowExpressionBody, isAsync, forInit) {
		this.initFunction(node);
		if (
			this.options.ecmaVersion >= 9 ||
			(this.options.ecmaVersion >= 6 && !isAsync)
		) {
			if (this.type === tokTypes.star && statement & FUNC_HANGING_STATEMENT) {
				this.unexpected();
			}
			node.generator = this.eat(tokTypes.star);
		}
		if (this.options.ecmaVersion >= 8) node.async = Boolean(isAsync);

		if (statement & FUNC_STATEMENT) {
			node.id =
				statement & FUNC_NULLABLE_ID && this.type !== tokTypes.name
					? null
					: this.parseIdent();
			if (node.id && !(statement & FUNC_HANGING_STATEMENT)) {
				// A sloppy-mode declaration binds under Annex B; anything else binds
				// by what the current scope does with function names.
				this.checkLValSimple(
					node.id,
					this.strict || node.generator || node.async
						? this.treatFunctionsAsVar
							? BIND_VAR
							: BIND_LEXICAL
						: BIND_FUNCTION
				);
			}
		}

		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		const oldAwaitIdentPos = this.awaitIdentPos;
		this.yieldPos = 0;
		this.awaitPos = 0;
		this.awaitIdentPos = 0;
		this.enterScope(
			functionFlags(
				/** @type {boolean} */ (node.async),
				/** @type {boolean} */ (node.generator)
			)
		);

		if (!(statement & FUNC_STATEMENT)) {
			node.id = this.type === tokTypes.name ? this.parseIdent() : null;
		}

		this.parseFunctionParams(node);
		this.parseFunctionBody(node, allowExpressionBody, false, forInit);

		this.yieldPos = oldYieldPos;
		this.awaitPos = oldAwaitPos;
		this.awaitIdentPos = oldAwaitIdentPos;
		return this.finishNode(
			node,
			statement & FUNC_STATEMENT ? "FunctionDeclaration" : "FunctionExpression"
		);
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @returns {void}
	 */
	parseFunctionParams(node) {
		this.expect(tokTypes.parenL);
		node.params = /** @type {Pattern[]} */ (
			this.parseBindingList(
				tokTypes.parenR,
				false,
				this.options.ecmaVersion >= 8
			)
		);
		this.checkYieldAwaitInDefaultParams();
	}

	/**
	 * @param {UnfinishedNode} node the class node
	 * @param {(boolean | string)=} isStatement whether it is a declaration
	 * @returns {NodeOf<"ClassDeclaration"> | NodeOf<"ClassExpression">} the finished class
	 */
	parseClass(node, isStatement) {
		this.next();

		// A class body is always strict code, whatever encloses it.
		const oldStrict = this.strict;
		this.strict = true;

		this.parseClassId(node, isStatement);
		this.parseClassSuper(node);
		const privateNameMap = this.enterClassBody();
		const classBody = /** @type {NodeInProgress<"ClassBody">} */ (
			this.startNode()
		);
		let hadConstructor = false;
		classBody.body = [];
		this.expect(tokTypes.braceL);
		while (this.type !== tokTypes.braceR) {
			const element = this.parseClassElement(node.superClass !== null);
			if (element) {
				classBody.body.push(element);
				if (
					element.type === "MethodDefinition" &&
					element.kind === "constructor"
				) {
					if (hadConstructor) {
						this.raiseRecoverable(
							element.start,
							"Duplicate constructor in the same class"
						);
					}
					hadConstructor = true;
				} else if (
					/** @type {NodeOf<"MethodDefinition" | "PropertyDefinition">} */ (
						element
					).key &&
					/** @type {NodeOf<"MethodDefinition" | "PropertyDefinition">} */ (
						element
					).key.type === "PrivateIdentifier" &&
					isPrivateNameConflicted(
						privateNameMap,
						/** @type {NodeOf<"MethodDefinition" | "PropertyDefinition"> & { key: NodeOf<"PrivateIdentifier"> }} */ (
							element
						)
					)
				) {
					this.raiseRecoverable(
						/** @type {NodeOf<"MethodDefinition" | "PropertyDefinition"> & { key: NodeOf<"PrivateIdentifier"> }} */ (
							element
						).key.start,
						`Identifier '#${/** @type {NodeOf<"MethodDefinition" | "PropertyDefinition"> & { key: NodeOf<"PrivateIdentifier"> }} */ (element).key.name}' has already been declared`
					);
				}
			}
		}
		this.strict = oldStrict;
		this.next();
		node.body = this.finishNode(classBody, "ClassBody");
		this.exitClassBody();
		return this.finishNode(
			node,
			isStatement ? "ClassDeclaration" : "ClassExpression"
		);
	}

	/**
	 * @param {boolean} constructorAllowsSuper whether the class has a superclass
	 * @returns {NodeOf<"MethodDefinition"> | NodeOf<"PropertyDefinition"> | NodeOf<"StaticBlock"> | null} the element, or `null` for a stray semicolon
	 */
	parseClassElement(constructorAllowsSuper) {
		if (this.eat(tokTypes.semi)) return null;

		const { ecmaVersion } = this.options;
		const node = this.startNode();
		let keyName = "";
		let isGenerator = false;
		let isAsync = false;
		/** @type {"method" | "get" | "set"} */
		let kind = "method";
		let isStatic = false;

		if (this.eatContextual("static")) {
			if (ecmaVersion >= 13 && this.eat(tokTypes.braceL)) {
				this.parseClassStaticBlock(node);
				return /** @type {NodeOf<"StaticBlock">} */ (node);
			}
			if (this.isClassElementNameStart() || this.type === tokTypes.star) {
				isStatic = true;
			} else {
				keyName = "static";
			}
		}
		node.static = isStatic;
		if (!keyName && ecmaVersion >= 8 && this.eatContextual("async")) {
			if (
				(this.isClassElementNameStart() || this.type === tokTypes.star) &&
				!this.canInsertSemicolon()
			) {
				isAsync = true;
			} else {
				keyName = "async";
			}
		}
		if (!keyName && (ecmaVersion >= 9 || !isAsync) && this.eat(tokTypes.star)) {
			isGenerator = true;
		}
		if (!keyName && !isAsync && !isGenerator) {
			const lastValue = this.value;
			if (this.eatContextual("get") || this.eatContextual("set")) {
				if (this.isClassElementNameStart()) {
					kind = /** @type {"get" | "set"} */ (lastValue);
				} else {
					keyName = /** @type {string} */ (lastValue);
				}
			}
		}

		if (keyName) {
			// `async`, `get`, `set` or `static` did not read as a modifier here, so
			// the token that spelled it is the element's name.
			node.computed = false;
			node.key = /** @type {Identifier} */ (
				this.startNodeAt(this.lastTokStart, this.lastTokStartLoc)
			);
			node.key.name = keyName;
			this.finishNode(node.key, "Identifier");
		} else {
			this.parseClassElementName(node);
		}

		if (
			ecmaVersion < 13 ||
			this.type === tokTypes.parenL ||
			kind !== "method" ||
			isGenerator ||
			isAsync
		) {
			const isConstructor =
				!node.static &&
				checkKeyName(
					/** @type {NodeOf<"MethodDefinition">} */ (node),
					"constructor"
				);
			const allowsDirectSuper = isConstructor && constructorAllowsSuper;
			if (isConstructor && kind !== "method") {
				this.raise(
					/** @type {NodeOf<"MethodDefinition">["key"]} */ (node.key).start,
					"Constructor can't have get/set modifier"
				);
			}
			node.kind = isConstructor ? "constructor" : kind;
			this.parseClassMethod(node, isGenerator, isAsync, allowsDirectSuper);
		} else {
			this.parseClassField(node);
		}

		return /** @type {NodeOf<"MethodDefinition" | "PropertyDefinition">} */ (
			node
		);
	}

	/** @returns {boolean} whether the current token can name a class element */
	isClassElementNameStart() {
		return Boolean(
			this.type === tokTypes.name ||
			this.type === tokTypes.privateId ||
			this.type === tokTypes.num ||
			this.type === tokTypes.string ||
			this.type === tokTypes.bracketL ||
			this.type.keyword
		);
	}

	/**
	 * @param {UnfinishedNode} element the element node
	 * @returns {void}
	 */
	parseClassElementName(element) {
		if (this.type === tokTypes.privateId) {
			if (this.value === "constructor") {
				this.raise(
					this.start,
					"Classes can't have an element named '#constructor'"
				);
			}
			element.computed = false;
			element.key = this.parsePrivateIdent();
		} else {
			this.parsePropertyName(element);
		}
	}

	/**
	 * @param {UnfinishedNode} method the method node
	 * @param {boolean} isGenerator whether it is a generator
	 * @param {boolean} isAsync whether it is async
	 * @param {boolean} allowsDirectSuper whether `super()` may be called in it
	 * @returns {NodeOf<"MethodDefinition">} the finished method
	 */
	parseClassMethod(method, isGenerator, isAsync, allowsDirectSuper) {
		const { key } = /** @type {NodeOf<"MethodDefinition">} */ (method);
		if (method.kind === "constructor") {
			if (isGenerator) {
				this.raise(key.start, "Constructor can't be a generator");
			}
			if (isAsync) {
				this.raise(key.start, "Constructor can't be an async method");
			}
		} else if (
			method.static &&
			checkKeyName(
				/** @type {NodeOf<"MethodDefinition">} */ (method),
				"prototype"
			)
		) {
			this.raise(
				key.start,
				"Classes may not have a static property named prototype"
			);
		}

		const value = this.parseMethod(isGenerator, isAsync, allowsDirectSuper);
		method.value = value;

		if (method.kind === "get" && value.params.length !== 0) {
			this.raiseRecoverable(value.start, "getter should have no params");
		}
		if (method.kind === "set" && value.params.length !== 1) {
			this.raiseRecoverable(
				value.start,
				"setter should have exactly one param"
			);
		}
		if (method.kind === "set" && value.params[0].type === "RestElement") {
			this.raiseRecoverable(
				value.params[0].start,
				"Setter cannot use rest params"
			);
		}

		return this.finishNode(method, "MethodDefinition");
	}

	/**
	 * @param {UnfinishedNode} field the field node
	 * @returns {NodeOf<"PropertyDefinition">} the finished field
	 */
	parseClassField(field) {
		if (
			checkKeyName(
				/** @type {NodeOf<"PropertyDefinition">} */ (field),
				"constructor"
			)
		) {
			this.raise(
				/** @type {NodeOf<"PropertyDefinition">["key"]} */ (field.key).start,
				"Classes can't have a field named 'constructor'"
			);
		} else if (
			field.static &&
			checkKeyName(
				/** @type {NodeOf<"PropertyDefinition">} */ (field),
				"prototype"
			)
		) {
			this.raise(
				/** @type {NodeOf<"PropertyDefinition">["key"]} */ (field.key).start,
				"Classes can't have a static field named 'prototype'"
			);
		}

		if (this.eat(tokTypes.eq)) {
			// The initializer is its own scope so that `arguments` in it raises.
			this.enterScope(SCOPE_CLASS_FIELD_INIT | SCOPE_SUPER);
			field.value = this.parseMaybeAssign();
			this.exitScope();
		} else {
			field.value = null;
		}
		this.semicolon();

		return this.finishNode(field, "PropertyDefinition");
	}

	/**
	 * @param {UnfinishedNode} node the static block node
	 * @returns {NodeOf<"StaticBlock">} the finished block
	 */
	parseClassStaticBlock(node) {
		node.body = [];

		const oldLabels = this.labels;
		this.labels = [];
		this.enterScope(SCOPE_CLASS_STATIC_BLOCK | SCOPE_SUPER);
		while (this.type !== tokTypes.braceR) {
			const stmt = /** @type {Statement} */ (this.parseStatement(null));
			/** @type {Statement[]} */ (node.body).push(stmt);
		}
		this.next();
		this.exitScope();
		this.labels = oldLabels;

		return this.finishNode(node, "StaticBlock");
	}

	/**
	 * @param {UnfinishedNode} node the class node
	 * @param {(boolean | string)=} isStatement whether it is a declaration
	 * @returns {void}
	 */
	parseClassId(node, isStatement) {
		if (this.type === tokTypes.name) {
			node.id = this.parseIdent();
			if (isStatement) {
				this.checkLValSimple(node.id, BIND_LEXICAL, false);
			}
		} else {
			if (isStatement === true) this.unexpected();
			node.id = null;
		}
	}

	/**
	 * @param {UnfinishedNode} node the class node
	 * @returns {void}
	 */
	parseClassSuper(node) {
		node.superClass = this.eat(tokTypes._extends)
			? this.parseExprSubscripts(null, false)
			: null;
	}

	/** @returns {PrivateNameMap} the map of private names this body declares */
	enterClassBody() {
		const element = { declared: Object.create(null), used: [] };
		this.privateNameStack.push(element);
		return element.declared;
	}

	/** @returns {void} */
	exitClassBody() {
		const { declared, used } = /** @type {PrivateNameStackEntry} */ (
			this.privateNameStack.pop()
		);
		if (!this.options.checkPrivateFields) return;
		const len = this.privateNameStack.length;
		const parent = len === 0 ? null : this.privateNameStack[len - 1];
		for (let i = 0; i < used.length; ++i) {
			const id = used[i];
			if (!Object.prototype.hasOwnProperty.call(declared, id.name)) {
				if (parent) {
					parent.used.push(id);
				} else {
					this.raiseRecoverable(
						id.start,
						`Private field '#${id.name}' must be declared in an enclosing class`
					);
				}
			}
		}
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportAllDeclaration">} the finished export
	 */
	parseExportAllDeclaration(node, exports) {
		if (this.options.ecmaVersion >= 11) {
			if (this.eatContextual("as")) {
				node.exported = this.parseModuleExportName();
				this.checkExport(exports, node.exported, this.lastTokStart);
			} else {
				node.exported = null;
			}
		}
		this.expectContextual("from");
		if (this.type !== tokTypes.string) this.unexpected();
		node.source = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
		if (this.options.ecmaVersion >= 16) {
			node.attributes = this.parseWithClause();
		}
		this.semicolon();
		return this.finishNode(node, "ExportAllDeclaration");
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportAllDeclaration"> | NodeOf<"ExportDefaultDeclaration"> | NodeOf<"ExportNamedDeclaration">} the finished export
	 */
	parseExport(node, exports) {
		this.next();
		if (this.eat(tokTypes.star)) {
			return this.parseExportAllDeclaration(node, exports);
		}
		if (this.eat(tokTypes._default)) {
			this.checkExport(exports, "default", this.lastTokStart);
			node.declaration = this.parseExportDefaultDeclaration();
			return this.finishNode(node, "ExportDefaultDeclaration");
		}
		if (this.shouldParseExportStatement()) {
			node.declaration = this.parseExportDeclaration(node);
			if (node.declaration.type === "VariableDeclaration") {
				this.checkVariableExport(exports, node.declaration.declarations);
			} else {
				this.checkExport(
					exports,
					/** @type {Identifier} */ (node.declaration.id),
					/** @type {Identifier} */ (node.declaration.id).start
				);
			}
			node.specifiers = [];
			node.source = null;
			if (this.options.ecmaVersion >= 16) node.attributes = [];
		} else {
			node.declaration = null;
			node.specifiers = this.parseExportSpecifiers(exports);
			if (this.eatContextual("from")) {
				if (this.type !== tokTypes.string) this.unexpected();
				node.source = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
				if (this.options.ecmaVersion >= 16) {
					node.attributes = this.parseWithClause();
				}
			} else {
				for (const spec of node.specifiers) {
					this.checkUnreserved(/** @type {Identifier} */ (spec.local));
					this.checkLocalExport(/** @type {Identifier} */ (spec.local));

					if (spec.local.type === "Literal") {
						this.raise(
							spec.local.start,
							"A string literal cannot be used as an exported binding without `from`."
						);
					}
				}

				node.source = null;
				if (this.options.ecmaVersion >= 16) node.attributes = [];
			}
			this.semicolon();
		}
		return this.finishNode(node, "ExportNamedDeclaration");
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @returns {Declaration} the exported declaration
	 */
	parseExportDeclaration(node) {
		return /** @type {Declaration} */ (this.parseStatement(null));
	}

	/** @returns {NodeOf<"ExportDefaultDeclaration">["declaration"]} what `export default` exports */
	parseExportDefaultDeclaration() {
		let isAsync;
		if (
			this.type === tokTypes._function ||
			(isAsync = this.isAsyncFunction())
		) {
			const fNode = this.startNode();
			this.next();
			if (isAsync) this.next();
			return this.parseFunction(
				fNode,
				FUNC_STATEMENT | FUNC_NULLABLE_ID,
				false,
				isAsync
			);
		}
		if (this.type === tokTypes._class) {
			const cNode = this.startNode();
			return this.parseClass(cNode, "nullableID");
		}
		const declaration = this.parseMaybeAssign();
		this.semicolon();
		return declaration;
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @param {ExportedName} name the exported name
	 * @param {number} pos where it was written
	 * @returns {void}
	 */
	checkExport(exports, name, pos) {
		if (!exports) return;
		const exported =
			typeof name === "string"
				? name
				: name.type === "Identifier"
					? name.name
					: /** @type {string} */ (name.value);
		if (Object.prototype.hasOwnProperty.call(exports, exported)) {
			this.raiseRecoverable(pos, `Duplicate export '${exported}'`);
		}

		exports[exported] = true;
	}

	/**
	 * @param {ExportedNames} exports where module exports are tracked
	 * @param {Pattern | AssignmentProperty} pat the pattern that binds the exported names
	 * @returns {void}
	 */
	checkPatternExport(exports, pat) {
		const { type } = pat;
		if (type === "Identifier") {
			this.checkExport(exports, pat, pat.start);
		} else if (type === "ObjectPattern") {
			for (const prop of pat.properties) this.checkPatternExport(exports, prop);
		} else if (type === "ArrayPattern") {
			for (const elt of pat.elements) {
				if (elt) this.checkPatternExport(exports, elt);
			}
		} else if (type === "Property") {
			this.checkPatternExport(exports, pat.value);
		} else if (type === "AssignmentPattern") {
			this.checkPatternExport(exports, pat.left);
		} else if (type === "RestElement") {
			this.checkPatternExport(exports, pat.argument);
		}
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @param {NodeOf<"VariableDeclarator">[]} declarations the declarators being exported
	 * @returns {void}
	 */
	checkVariableExport(exports, declarations) {
		if (!exports) return;
		for (const declaration of declarations) {
			this.checkPatternExport(exports, declaration.id);
		}
	}

	/** @returns {boolean} whether `export` is followed by a declaration */
	shouldParseExportStatement() {
		return (
			this.type.keyword === "var" ||
			this.type.keyword === "const" ||
			this.type.keyword === "class" ||
			this.type.keyword === "function" ||
			this.isLet() ||
			this.isAsyncFunction()
		);
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportSpecifier">} the finished specifier
	 */
	parseExportSpecifier(exports) {
		const node = this.startNode();
		node.local = this.parseModuleExportName();

		node.exported = this.eatContextual("as")
			? this.parseModuleExportName()
			: node.local;
		this.checkExport(exports, node.exported, node.exported.start);

		return this.finishNode(node, "ExportSpecifier");
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportSpecifier">[]} the specifiers
	 */
	parseExportSpecifiers(exports) {
		const nodes = [];
		let first = true;
		this.expect(tokTypes.braceL);
		while (!this.eat(tokTypes.braceR)) {
			if (first) {
				first = false;
			} else {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) break;
			}

			nodes.push(this.parseExportSpecifier(exports));
		}
		return nodes;
	}

	/**
	 * @param {UnfinishedNode} node the import node
	 * @returns {NodeOf<"ImportDeclaration">} the finished import
	 */
	parseImport(node) {
		this.next();

		if (this.type === tokTypes.string) {
			node.specifiers = empty;
			node.source = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
		} else {
			node.specifiers = this.parseImportSpecifiers();
			this.expectContextual("from");
			node.source =
				this.type === tokTypes.string
					? /** @type {NodeOf<"Literal">} */ (this.parseExprAtom())
					: this.unexpected();
		}
		if (this.options.ecmaVersion >= 16) {
			node.attributes = this.parseWithClause();
		}
		this.semicolon();
		return this.finishNode(node, "ImportDeclaration");
	}

	/** @returns {ImportSpecifier} the finished specifier */
	parseImportSpecifier() {
		const node = this.startNode();
		node.imported = this.parseModuleExportName();

		if (this.eatContextual("as")) {
			node.local = this.parseIdent();
		} else {
			this.checkUnreserved(/** @type {Identifier} */ (node.imported));
			node.local = node.imported;
		}
		this.checkLValSimple(/** @type {Identifier} */ (node.local), BIND_LEXICAL);

		return this.finishNode(node, "ImportSpecifier");
	}

	/** @returns {ImportDefaultSpecifier} the finished specifier */
	parseImportDefaultSpecifier() {
		const node = this.startNode();
		node.local = this.parseIdent();
		this.checkLValSimple(node.local, BIND_LEXICAL);
		return this.finishNode(node, "ImportDefaultSpecifier");
	}

	/** @returns {ImportNamespaceSpecifier} the finished specifier */
	parseImportNamespaceSpecifier() {
		const node = this.startNode();
		this.next();
		this.expectContextual("as");
		node.local = this.parseIdent();
		this.checkLValSimple(node.local, BIND_LEXICAL);
		return this.finishNode(node, "ImportNamespaceSpecifier");
	}

	/** @returns {AnyImportSpecifier[]} the specifiers */
	parseImportSpecifiers() {
		const nodes = [];
		let first = true;
		if (this.type === tokTypes.name) {
			nodes.push(this.parseImportDefaultSpecifier());
			if (!this.eat(tokTypes.comma)) return nodes;
		}
		if (this.type === tokTypes.star) {
			nodes.push(this.parseImportNamespaceSpecifier());
			return nodes;
		}
		this.expect(tokTypes.braceL);
		while (!this.eat(tokTypes.braceR)) {
			if (first) {
				first = false;
			} else {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) break;
			}

			nodes.push(this.parseImportSpecifier());
		}
		return nodes;
	}

	/** @returns {ImportAttribute[]} the attributes a `with` clause states */
	parseWithClause() {
		/** @type {ImportAttribute[]} */
		const nodes = [];
		if (!this.eat(tokTypes._with)) return nodes;
		this.expect(tokTypes.braceL);
		/** @type {Record<string, boolean>} */
		const attributeKeys = {};
		let first = true;
		while (!this.eat(tokTypes.braceR)) {
			if (first) {
				first = false;
			} else {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) break;
			}

			const attr = this.parseImportAttribute();
			const keyName =
				attr.key.type === "Identifier"
					? attr.key.name
					: /** @type {string} */ (attr.key.value);
			if (Object.prototype.hasOwnProperty.call(attributeKeys, keyName)) {
				this.raiseRecoverable(
					attr.key.start,
					`Duplicate attribute key '${keyName}'`
				);
			}
			attributeKeys[keyName] = true;
			nodes.push(attr);
		}
		return nodes;
	}

	/** @returns {ImportAttribute} the finished attribute */
	parseImportAttribute() {
		const node = this.startNode();
		node.key =
			this.type === tokTypes.string
				? /** @type {NodeOf<"Literal">} */ (this.parseExprAtom())
				: this.parseIdent(this.options.allowReserved !== "never");
		this.expect(tokTypes.colon);
		if (this.type !== tokTypes.string) this.unexpected();
		node.value = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
		return this.finishNode(node, "ImportAttribute");
	}

	/** @returns {ModuleExportName} the name, as an identifier or a string literal */
	parseModuleExportName() {
		if (this.options.ecmaVersion >= 13 && this.type === tokTypes.string) {
			const stringLiteral =
				/** @type {NodeOf<"Literal"> & { value: string }} */ (
					this.parseLiteral(/** @type {string} */ (this.value))
				);
			if (loneSurrogate.test(stringLiteral.value)) {
				this.raise(
					stringLiteral.start,
					"An export name cannot include a lone surrogate."
				);
			}
			return stringLiteral;
		}
		return this.parseIdent(true);
	}

	/**
	 * @param {AnyNode[]} statements the statements that open a body
	 * @returns {void}
	 */
	adaptDirectivePrologue(statements) {
		for (
			let i = 0;
			i < statements.length && this.isDirectiveCandidate(statements[i]);
			++i
		) {
			/** @type {Directive} */ (statements[i]).directive =
				/** @type {string} */ (
					/** @type {Directive} */ (statements[i]).expression.raw
				).slice(1, -1);
		}
	}

	/**
	 * @param {NodeLike & { expression?: ExpressionField }} statement the statement to test
	 * @returns {boolean} whether it may be a directive
	 */
	isDirectiveCandidate(statement) {
		const node = /** @type {AnyNode} */ (statement);
		return (
			this.options.ecmaVersion >= 5 &&
			node.type === "ExpressionStatement" &&
			node.expression.type === "Literal" &&
			typeof node.expression.value === "string" &&
			(this.input[node.start] === '"' || this.input[node.start] === "'")
		);
	}

	/**
	 * Rewrite an expression as the binding pattern it turns out to be, which is
	 * only known once the `=` or `of` after it has been read.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/lval.js
	 * @template {AnyNode | null} T
	 * @param {T} node the parsed expression
	 * @param {boolean=} isBinding whether it binds rather than assigns
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @returns {T} the same node, rewritten
	 */
	toAssignable(node, isBinding, refDestructuringErrors) {
		if (this.options.ecmaVersion >= 6 && node) {
			switch (node.type) {
				case "Identifier":
					if (this.inAsync && node.name === "await") {
						this.raise(
							node.start,
							"Cannot use 'await' as identifier inside an async function"
						);
					}
					break;

				case "ObjectPattern":
				case "ArrayPattern":
				case "AssignmentPattern":
				case "RestElement":
					break;

				case "ObjectExpression":
					/** @type {NodeOf<"ObjectPattern">} */ (
						/** @type {AnyNode} */ (node)
					).type = "ObjectPattern";
					if (refDestructuringErrors) {
						this.checkPatternErrors(refDestructuringErrors, true);
					}
					for (const prop of /** @type {NodeOf<"Property" | "SpreadElement" | "RestElement">[]} */ (
						node.properties
					)) {
						this.toAssignable(prop, isBinding);
						// A rest property's target may not itself destructure.
						if (
							prop.type === "RestElement" &&
							(prop.argument.type === "ArrayPattern" ||
								prop.argument.type === "ObjectPattern")
						) {
							this.raise(prop.argument.start, "Unexpected token");
						}
					}
					break;

				case "Property":
					if (node.kind !== "init") {
						this.raise(
							node.key.start,
							"Object pattern can't contain getter or setter"
						);
					}
					this.toAssignable(node.value, isBinding);
					break;

				case "ArrayExpression":
					/** @type {NodeOf<"ArrayPattern">} */ (
						/** @type {AnyNode} */ (node)
					).type = "ArrayPattern";
					if (refDestructuringErrors) {
						this.checkPatternErrors(refDestructuringErrors, true);
					}
					this.toAssignableList(node.elements, isBinding);
					break;

				case "SpreadElement":
					/** @type {NodeOf<"RestElement">} */ (
						/** @type {AnyNode} */ (node)
					).type = "RestElement";
					this.toAssignable(node.argument, isBinding);
					if (
						/** @type {AnyNode} */ (node.argument).type === "AssignmentPattern"
					) {
						this.raise(
							/** @type {AnyNode} */ (node.argument).start,
							"Rest elements cannot have a default value"
						);
					}
					break;

				case "AssignmentExpression":
					if (node.operator !== "=") {
						this.raise(
							node.left.end,
							"Only '=' operator can be used for specifying default value."
						);
					}
					/** @type {NodeOf<"AssignmentPattern">} */ (
						/** @type {AnyNode} */ (node)
					).type = "AssignmentPattern";
					delete (
						/** @type {NodeInProgress<"AssignmentExpression">} */ (node)
							.operator
					);
					this.toAssignable(node.left, isBinding);
					break;

				case "ParenthesizedExpression":
					this.toAssignable(node.expression, isBinding, refDestructuringErrors);
					break;

				case "ChainExpression":
					this.raiseRecoverable(
						node.start,
						"Optional chaining cannot appear in left-hand side"
					);
					break;

				case "MemberExpression":
					if (!isBinding) break;
					this.raise(node.start, "Assigning to rvalue");
					break;

				default:
					this.raise(node.start, "Assigning to rvalue");
			}
		} else if (refDestructuringErrors) {
			this.checkPatternErrors(refDestructuringErrors, true);
		}
		return node;
	}

	/**
	 * @template {AnyNode | null} T
	 * @param {T[]} exprList the parsed expressions
	 * @param {boolean=} isBinding whether they bind rather than assign
	 * @returns {T[]} the same list, rewritten
	 */
	toAssignableList(exprList, isBinding) {
		const end = exprList.length;
		for (let i = 0; i < end; i++) {
			const elt = exprList[i];
			if (elt) this.toAssignable(elt, isBinding);
		}
		if (end) {
			const last = exprList[end - 1];
			if (
				this.options.ecmaVersion === 6 &&
				isBinding &&
				last &&
				last.type === "RestElement" &&
				last.argument.type !== "Identifier"
			) {
				this.unexpected(last.argument.start);
			}
		}
		return exprList;
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @returns {NodeOf<"SpreadElement">} the finished element
	 */
	parseSpread(refDestructuringErrors) {
		const node = this.startNode();
		this.next();
		node.argument = this.parseMaybeAssign(false, refDestructuringErrors);
		return this.finishNode(node, "SpreadElement");
	}

	/** @returns {NodeOf<"RestElement">} the finished element */
	parseRestBinding() {
		const node = this.startNode();
		this.next();

		// Under ES6 a rest parameter may only name an identifier.
		if (this.options.ecmaVersion === 6 && this.type !== tokTypes.name) {
			this.unexpected();
		}

		node.argument = this.parseBindingAtom();

		return this.finishNode(node, "RestElement");
	}

	/** @returns {Pattern} the binding target */
	parseBindingAtom() {
		if (this.options.ecmaVersion >= 6) {
			switch (this.type) {
				case tokTypes.bracketL: {
					const node = this.startNode();
					this.next();
					node.elements = this.parseBindingList(tokTypes.bracketR, true, true);
					return this.finishNode(node, "ArrayPattern");
				}

				case tokTypes.braceL:
					return this.parseObj(true);
			}
		}
		return this.parseIdent();
	}

	/**
	 * @param {TokenType} close the token that ends the list
	 * @param {boolean} allowEmpty whether holes are allowed
	 * @param {boolean} allowTrailingComma whether a trailing comma is allowed
	 * @param {boolean=} allowModifiers whether items may carry modifiers
	 * @returns {(Pattern | null)[]} the bound targets
	 */
	parseBindingList(close, allowEmpty, allowTrailingComma, allowModifiers) {
		const elements = [];
		let first = true;
		while (!this.eat(close)) {
			if (first) first = false;
			else this.expect(tokTypes.comma);
			if (allowEmpty && this.type === tokTypes.comma) {
				elements.push(null);
			} else if (allowTrailingComma && this.afterTrailingComma(close)) {
				break;
			} else if (this.type === tokTypes.ellipsis) {
				const rest = this.parseRestBinding();
				this.parseBindingListItem(rest);
				elements.push(rest);
				if (this.type === tokTypes.comma) {
					this.raiseRecoverable(
						this.start,
						"Comma is not permitted after the rest element"
					);
				}
				this.expect(close);
				break;
			} else {
				elements.push(this.parseAssignableListItem(allowModifiers));
			}
		}
		return elements;
	}

	/**
	 * @param {boolean=} allowModifiers whether the item may carry modifiers
	 * @returns {Pattern} the item
	 */
	parseAssignableListItem(allowModifiers) {
		const elem = this.parseMaybeDefault(this.start, this.startLoc);
		this.parseBindingListItem(elem);
		return elem;
	}

	/**
	 * @template {Pattern} T
	 * @param {T} param the bound target
	 * @returns {T} the same target
	 */
	parseBindingListItem(param) {
		return param;
	}

	/**
	 * @param {number} startPos where the target starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {Pattern=} left the already-parsed target
	 * @returns {Pattern} the target, wrapped when it has a default
	 */
	parseMaybeDefault(startPos, startLoc, left) {
		const target = left || this.parseBindingAtom();
		if (this.options.ecmaVersion < 6 || !this.eat(tokTypes.eq)) return target;
		const node = this.startNodeAt(startPos, startLoc);
		node.left = target;
		node.right = this.parseMaybeAssign();
		return this.finishNode(node, "AssignmentPattern");
	}

	/**
	 * Check a target that may only be an identifier or member expression, and
	 * record the binding it introduces.
	 * @param {AnyNode} expr the target
	 * @param {number=} bindingType what kind of binding it introduces
	 * @param {ClashCheck=} checkClashes where duplicate parameter names are tracked
	 * @returns {void}
	 */
	checkLValSimple(expr, bindingType, checkClashes) {
		const binding = bindingType === undefined ? BIND_NONE : bindingType;
		const isBind = binding !== BIND_NONE;

		switch (expr.type) {
			case "Identifier":
				if (this.strict && this.reservedWordsStrictBind.test(expr.name)) {
					this.raiseRecoverable(
						expr.start,
						`${isBind ? "Binding " : "Assigning to "}${expr.name} in strict mode`
					);
				}
				if (isBind) {
					if (binding === BIND_LEXICAL && expr.name === "let") {
						this.raiseRecoverable(
							expr.start,
							"let is disallowed as a lexically bound name"
						);
					}
					if (checkClashes) {
						if (Object.prototype.hasOwnProperty.call(checkClashes, expr.name)) {
							this.raiseRecoverable(expr.start, "Argument name clash");
						}
						/** @type {Record<string, boolean>} */ (checkClashes)[expr.name] =
							true;
					}
					if (binding !== BIND_OUTSIDE) {
						this.declareName(expr.name, binding, expr.start);
					}
				}
				break;

			case "ChainExpression":
				this.raiseRecoverable(
					expr.start,
					"Optional chaining cannot appear in left-hand side"
				);
				break;

			case "MemberExpression":
				if (isBind) {
					this.raiseRecoverable(expr.start, "Binding member expression");
				}
				break;

			case "ParenthesizedExpression":
				if (isBind) {
					this.raiseRecoverable(expr.start, "Binding parenthesized expression");
				}
				return this.checkLValSimple(expr.expression, binding, checkClashes);

			default:
				this.raise(expr.start, `${isBind ? "Binding" : "Assigning to"} rvalue`);
		}
	}

	/**
	 * @param {AnyNode} expr the target
	 * @param {number=} bindingType what kind of binding it introduces
	 * @param {ClashCheck=} checkClashes where duplicate parameter names are tracked
	 * @returns {void}
	 */
	checkLValPattern(expr, bindingType, checkClashes) {
		const binding = bindingType === undefined ? BIND_NONE : bindingType;
		switch (expr.type) {
			case "ObjectPattern":
				for (const prop of expr.properties) {
					this.checkLValInnerPattern(prop, binding, checkClashes);
				}
				break;

			case "ArrayPattern":
				for (const elem of expr.elements) {
					if (elem) this.checkLValInnerPattern(elem, binding, checkClashes);
				}
				break;

			default:
				this.checkLValSimple(expr, binding, checkClashes);
		}
	}

	/**
	 * @param {AnyNode} expr the target
	 * @param {number=} bindingType what kind of binding it introduces
	 * @param {ClashCheck=} checkClashes where duplicate parameter names are tracked
	 * @returns {void}
	 */
	checkLValInnerPattern(expr, bindingType, checkClashes) {
		const binding = bindingType === undefined ? BIND_NONE : bindingType;
		switch (expr.type) {
			case "Property":
				this.checkLValInnerPattern(expr.value, binding, checkClashes);
				break;

			case "AssignmentPattern":
				this.checkLValPattern(expr.left, binding, checkClashes);
				break;

			case "RestElement":
				this.checkLValPattern(expr.argument, binding, checkClashes);
				break;

			default:
				this.checkLValPattern(expr, binding, checkClashes);
		}
	}

	/** @returns {TokContextLike} the innermost context */
	curContext() {
		return this.context[this.context.length - 1];
	}

	/**
	 * Whether a `{` here opens a block rather than an object literal, which the
	 * token before it decides.
	 * @param {TokenType} prevType the token before the brace
	 * @returns {boolean} whether it opens a block
	 */
	braceIsBlock(prevType) {
		const parent = this.curContext();
		if (parent === tokContexts.f_expr || parent === tokContexts.f_stat) {
			return true;
		}
		if (
			prevType === tokTypes.colon &&
			(parent === tokContexts.b_stat || parent === tokContexts.b_expr)
		) {
			return !parent.isExpr;
		}

		// `name` with an expression allowed means a `yield` or `of` was just read,
		// which `tokTypes.name.updateContext` is what records.
		if (
			prevType === tokTypes._return ||
			(prevType === tokTypes.name && this.exprAllowed)
		) {
			return lineBreak.test(this.input.slice(this.lastTokEnd, this.start));
		}
		if (
			prevType === tokTypes._else ||
			prevType === tokTypes.semi ||
			prevType === tokTypes.eof ||
			prevType === tokTypes.parenR ||
			prevType === tokTypes.arrow
		) {
			return true;
		}
		if (prevType === tokTypes.braceL) return parent === tokContexts.b_stat;
		if (
			prevType === tokTypes._var ||
			prevType === tokTypes._const ||
			prevType === tokTypes.name
		) {
			return false;
		}
		return !this.exprAllowed;
	}

	/**
	 * Whether the enclosing function is a generator. A parse reads its scope: a
	 * method's `*` pushes no `function` context, which only the tokenizer reads.
	 * @returns {boolean} true where `yield` is an operator
	 */
	inGeneratorContext() {
		if (this.scopeStack.length > 0) return this.inGenerator;
		for (let i = this.context.length - 1; i >= 1; i--) {
			const context = this.context[i];
			if (context.token === "function") return Boolean(context.generator);
		}
		return false;
	}

	/**
	 * @param {TokenType} prevType the token before the current one
	 * @returns {void}
	 */
	updateContext(prevType) {
		const type = this.type;
		if (type.keyword && prevType === tokTypes.dot) {
			this.exprAllowed = false;
			return;
		}
		const update = type.updateContext;
		if (update) update.call(this, prevType);
		else this.exprAllowed = type.beforeExpr;
	}

	/**
	 * @param {TokContextLike} tokenCtx the context the token really opened
	 * @returns {void}
	 */
	overrideContext(tokenCtx) {
		if (this.curContext() !== tokenCtx) {
			this.context[this.context.length - 1] = tokenCtx;
		}
	}

	/** @returns {void} */
	exitScope() {
		this.scopeStack.pop();
	}

	/**
	 * Record a name in the scope its binding belongs to, and report a name that
	 * was already declared there.
	 * @param {string} name the bound name
	 * @param {number} bindingType what kind of binding it is
	 * @param {number} pos where it was written
	 * @returns {void}
	 */
	declareName(name, bindingType, pos) {
		let redeclared = false;
		if (bindingType === BIND_LEXICAL) {
			const scope = this.currentScope();
			redeclared =
				(scope.lexical !== undefined && scope.lexical.has(name)) ||
				(scope.functions !== undefined && scope.functions.has(name)) ||
				(scope.var !== undefined && scope.var.has(name));
			if (scope.lexical === undefined) {
				scope.firstLexical = name;
				scope.lexical = new Set();
			}
			scope.lexical.add(name);
			if (this.inModule && scope.flags & SCOPE_TOP) {
				delete this.undefinedExports[name];
			}
		} else if (bindingType === BIND_SIMPLE_CATCH) {
			const scope = this.currentScope();
			if (scope.lexical === undefined) {
				scope.firstLexical = name;
				scope.lexical = new Set();
			}
			scope.lexical.add(name);
		} else if (bindingType === BIND_FUNCTION) {
			const scope = this.currentScope();
			redeclared = this.treatFunctionsAsVar
				? scope.lexical !== undefined && scope.lexical.has(name)
				: (scope.lexical !== undefined && scope.lexical.has(name)) ||
					(scope.var !== undefined && scope.var.has(name));
			if (scope.functions === undefined) scope.functions = new Set();
			scope.functions.add(name);
		} else {
			for (let i = this.scopeStack.length - 1; i >= 0; --i) {
				const scope = this.scopeStack[i];
				if (
					(scope.lexical !== undefined &&
						scope.lexical.has(name) &&
						!(
							scope.flags & SCOPE_SIMPLE_CATCH && scope.firstLexical === name
						)) ||
					(scope.functions !== undefined &&
						!this.treatFunctionsAsVarInScope(scope) &&
						scope.functions.has(name))
				) {
					redeclared = true;
					break;
				}
				if (scope.var === undefined) scope.var = new Set();
				scope.var.add(name);
				if (this.inModule && scope.flags & SCOPE_TOP) {
					delete this.undefinedExports[name];
				}
				if (scope.flags & SCOPE_VAR) break;
			}
		}
		if (redeclared) {
			this.raiseRecoverable(
				pos,
				`Identifier '${name}' has already been declared`
			);
		}
	}

	/**
	 * @param {Identifier} id the exported identifier
	 * @returns {void}
	 */
	checkLocalExport(id) {
		// Module code is always strict, so `functions` is empty here.
		const top = this.scopeStack[0];
		if (
			(top.lexical === undefined || !top.lexical.has(id.name)) &&
			(top.var === undefined || !top.var.has(id.name))
		) {
			this.undefinedExports[id.name] = id;
		}
	}

	/** @returns {UnfinishedNode} a node starting at the current token */
	startNode() {
		return /** @type {UnfinishedNode} */ (
			new ParserNode(this, this.start, this.startLoc)
		);
	}

	/**
	 * @param {number} pos where the node starts
	 * @param {PositionLike | null=} loc its start position
	 * @returns {UnfinishedNode} the node
	 */
	startNodeAt(pos, loc) {
		return /** @type {UnfinishedNode} */ (
			new ParserNode(this, pos, /** @type {Position} */ (loc))
		);
	}

	/**
	 * @template {AnyNodeType} T
	 * @param {UnfinishedNode} node the node to finish
	 * @param {T} type its ESTree type
	 * @returns {NodeOf<T>} the same node
	 */
	finishNode(node, type) {
		return this.finishNodeAt(node, type, this.lastTokEnd, this.lastTokEndLoc);
	}

	/**
	 * @template {AnyNodeType} T
	 * @param {UnfinishedNode} node the node to finish
	 * @param {T} type its ESTree type
	 * @param {number} pos where it ends
	 * @param {PositionLike | null=} loc its end position
	 * @returns {NodeOf<T>} the same node
	 */
	finishNodeAt(node, type, pos, loc) {
		node.type = type;
		node.end = pos;
		if (this.options.locations) {
			/** @type {import("estree").SourceLocation} */ (node.loc).end =
				/** @type {PositionLike} */ (loc);
		}
		if (this.options.ranges) /** @type {Range} */ (node.range)[1] = pos;
		return /** @type {NodeOf<T>} */ (node);
	}

	/**
	 * @template {AnyNode} T
	 * @param {T} node the node to copy
	 * @returns {T} the copy
	 */
	copyNode(node) {
		const newNode = /** @type {T} */ (
			/** @type {UnfinishedNode} */ (
				new ParserNode(this, node.start, this.startLoc)
			)
		);
		for (const prop of /** @type {(keyof T)[]} */ (Object.keys(node))) {
			newNode[prop] = node[prop];
		}
		return newNode;
	}

	/**
	 * Report a property name that may not be repeated: a getter or setter that
	 * clashes, and under ES5 a repeated `init` in strict mode.
	 * @param {ObjectMember} prop the property
	 * @param {PropHash} propHash where names seen so far are tracked
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @returns {void}
	 */
	checkPropClash(prop, propHash, refDestructuringErrors) {
		if (this.options.ecmaVersion >= 9 && prop.type === "SpreadElement") return;
		if (
			this.options.ecmaVersion >= 6 &&
			(prop.computed || prop.method || prop.shorthand)
		) {
			return;
		}
		const { key } = /** @type {NodeOf<"Property">} */ (prop);
		let name;
		switch (key.type) {
			case "Identifier":
				name = key.name;
				break;
			case "Literal":
				name = String(key.value);
				break;
			default:
				return;
		}
		const { kind } = /** @type {NodeOf<"Property">} */ (prop);
		if (this.options.ecmaVersion >= 6) {
			if (name === "__proto__" && kind === "init") {
				if (propHash.proto) {
					if (refDestructuringErrors) {
						if (refDestructuringErrors.doubleProto < 0) {
							refDestructuringErrors.doubleProto = key.start;
						}
					} else {
						this.raiseRecoverable(
							key.start,
							"Redefinition of __proto__ property"
						);
					}
				}
				propHash.proto = true;
			}
			return;
		}
		name = `$${name}`;
		let other = /** @type {Record<string, boolean> | undefined} */ (
			propHash[name]
		);
		if (other) {
			const redefinition =
				kind === "init"
					? (this.strict && other.init) || other.get || other.set
					: other.init || other[kind];
			if (redefinition) {
				this.raiseRecoverable(key.start, "Redefinition of property");
			}
		} else {
			other = { init: false, get: false, set: false };
			propHash[name] = other;
		}
		other[kind] = true;
	}

	/**
	 * Read a full expression, including the comma operator.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/expression.js
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {Expression} the expression
	 */
	parseExpression(forInit, refDestructuringErrors) {
		return this.catchStackOverflow(() => {
			const startPos = this.start;
			const startLoc = this.startLoc;
			const expr = this.parseMaybeAssign(forInit, refDestructuringErrors);
			if (this.type === tokTypes.comma) {
				const node = this.startNodeAt(startPos, startLoc);
				node.expressions = [expr];
				while (this.eat(tokTypes.comma)) {
					node.expressions.push(
						this.parseMaybeAssign(forInit, refDestructuringErrors)
					);
				}
				return this.finishNode(node, "SequenceExpression");
			}
			return expr;
		});
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @param {AfterLeftParse=} afterLeftParse a hook run on the parsed left side
	 * @returns {Expression} the expression
	 */
	parseMaybeAssign(forInit, refDestructuringErrors, afterLeftParse) {
		if (this.isContextual("yield")) {
			if (this.inGenerator) return this.parseYield(forInit);
			// The tokenizer assumes an expression follows `yield`, which is wrong
			// where `yield` is only a name.
			this.exprAllowed = false;
		}

		let errors = refDestructuringErrors;
		let ownDestructuringErrors = false;
		let oldParenAssign = -1;
		let oldTrailingComma = -1;
		let oldDoubleProto = -1;
		if (errors) {
			oldParenAssign = errors.parenthesizedAssign;
			oldTrailingComma = errors.trailingComma;
			oldDoubleProto = errors.doubleProto;
			errors.parenthesizedAssign = -1;
			errors.trailingComma = -1;
		} else {
			errors = new DestructuringErrors();
			ownDestructuringErrors = true;
		}

		const startPos = this.start;
		const startLoc = this.startLoc;
		if (this.type === tokTypes.parenL || this.type === tokTypes.name) {
			this.potentialArrowAt = this.start;
			this.potentialArrowInForAwait = forInit === "await";
		}
		let left = this.parseMaybeConditional(forInit, errors);
		if (afterLeftParse) {
			left = afterLeftParse.call(this, left, startPos, startLoc);
		}
		if (this.type.isAssign) {
			const node = this.startNodeAt(startPos, startLoc);
			node.operator = /** @type {AssignmentOperator} */ (this.value);
			if (this.type === tokTypes.eq) {
				left = this.toAssignable(left, false, errors);
			}
			if (!ownDestructuringErrors) {
				errors.parenthesizedAssign = -1;
				errors.trailingComma = -1;
				if (errors.shorthandAssign >= left.start) errors.shorthandAssign = -1;
				if (errors.doubleProto >= left.start) errors.doubleProto = -1;
			}
			if (this.type === tokTypes.eq) this.checkLValPattern(left);
			else this.checkLValSimple(left);
			node.left = /** @type {Pattern} */ (left);
			this.next();
			node.right = this.parseMaybeAssign(forInit);
			if (oldDoubleProto > -1) errors.doubleProto = oldDoubleProto;
			return this.finishNode(node, "AssignmentExpression");
		}
		if (ownDestructuringErrors) this.checkExpressionErrors(errors, true);
		if (oldParenAssign > -1) errors.parenthesizedAssign = oldParenAssign;
		if (oldTrailingComma > -1) errors.trailingComma = oldTrailingComma;
		return left;
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {Expression} the expression
	 */
	parseMaybeConditional(forInit, refDestructuringErrors) {
		const startPos = this.start;
		const startLoc = this.startLoc;
		const expr = this.parseExprOps(forInit, refDestructuringErrors);
		if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
		if (
			!(expr.type === "ArrowFunctionExpression" && expr.start === startPos) &&
			this.eat(tokTypes.question)
		) {
			const node = this.startNodeAt(startPos, startLoc);
			node.test = expr;
			node.consequent = this.parseMaybeAssign();
			this.expect(tokTypes.colon);
			node.alternate = this.parseMaybeAssign(forInit);
			return this.finishNode(node, "ConditionalExpression");
		}
		return expr;
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {Expression} the expression
	 */
	parseExprOps(forInit, refDestructuringErrors) {
		const startPos = this.start;
		const startLoc = this.startLoc;
		const expr = this.parseMaybeUnary(
			refDestructuringErrors,
			false,
			false,
			forInit
		);
		if (this.checkExpressionErrors(refDestructuringErrors)) {
			return /** @type {Expression} */ (expr);
		}
		return expr.start === startPos && expr.type === "ArrowFunctionExpression"
			? expr
			: this.parseExprOp(expr, startPos, startLoc, -1, forInit);
	}

	/**
	 * Read binary operators by precedence climbing, stopping where an operator
	 * binds less tightly than the caller is parsing.
	 * @param {Expression | NodeOf<"PrivateIdentifier">} left the left operand
	 * @param {number} leftStartPos where the left operand starts
	 * @param {PositionLike | null | undefined} leftStartLoc its start position
	 * @param {number} minPrec the precedence to stop below
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseExprOp(left, leftStartPos, leftStartLoc, minPrec, forInit) {
		let prec = this.type.binop;
		if (
			prec !== null &&
			prec !== undefined &&
			(!forInit || this.type !== tokTypes._in) &&
			prec > minPrec
		) {
			const logical =
				this.type === tokTypes.logicalOR || this.type === tokTypes.logicalAND;
			const coalesce = this.type === tokTypes.coalesce;
			if (coalesce) {
				// `??` binds like the logical operators so that mixing the two
				// without parentheses is caught rather than silently nested.
				prec = /** @type {number} */ (tokTypes.logicalAND.binop);
			}
			const op = /** @type {BinaryOperator | LogicalOperator} */ (this.value);
			this.next();
			const startPos = this.start;
			const startLoc = this.startLoc;
			const right = this.parseExprOp(
				this.parseMaybeUnary(null, false, false, forInit),
				startPos,
				startLoc,
				prec,
				forInit
			);
			const node = this.buildBinary(
				leftStartPos,
				leftStartLoc,
				left,
				right,
				op,
				logical || coalesce
			);
			if (
				(logical && this.type === tokTypes.coalesce) ||
				(coalesce &&
					(this.type === tokTypes.logicalOR ||
						this.type === tokTypes.logicalAND))
			) {
				this.raiseRecoverable(
					this.start,
					"Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses"
				);
			}
			return this.parseExprOp(
				node,
				leftStartPos,
				leftStartLoc,
				minPrec,
				forInit
			);
		}
		return /** @type {Expression} */ (left);
	}

	/**
	 * @param {number} startPos where the expression starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {Expression | NodeOf<"PrivateIdentifier">} left the left operand
	 * @param {Expression | NodeOf<"PrivateIdentifier">} right the right operand
	 * @param {BinaryOperator | LogicalOperator} op the operator
	 * @param {boolean} logical whether it is a logical operator
	 * @returns {NodeOf<"BinaryExpression"> | NodeOf<"LogicalExpression">} the expression
	 */
	buildBinary(startPos, startLoc, left, right, op, logical) {
		if (right.type === "PrivateIdentifier") {
			this.raise(
				right.start,
				"Private identifier can only be left side of binary expression"
			);
		}
		const node = this.startNodeAt(startPos, startLoc);
		node.left = left;
		node.operator = op;
		node.right = right;
		return this.finishNode(
			node,
			logical ? "LogicalExpression" : "BinaryExpression"
		);
	}

	/**
	 * @param {RefDestructuringErrors | undefined} refDestructuringErrors where to record deferred errors
	 * @param {boolean=} sawUnary whether a unary operator was already read
	 * @param {boolean=} incDec whether the operand belongs to `++`/`--`
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression | NodeOf<"PrivateIdentifier">} the expression
	 */
	parseMaybeUnary(refDestructuringErrors, sawUnary, incDec, forInit) {
		const startPos = this.start;
		const startLoc = this.startLoc;
		let expr;
		let unary = sawUnary;
		if (this.isContextual("await") && this.canAwait) {
			expr = this.parseAwait(forInit);
			unary = true;
		} else if (this.type.prefix) {
			const node = this.startNode();
			const update = this.type === tokTypes.incDec;
			node.operator = /** @type {UnaryOperator | UpdateOperator} */ (
				this.value
			);
			node.prefix = true;
			this.next();
			node.argument = /** @type {Expression} */ (
				this.parseMaybeUnary(null, true, update, forInit)
			);
			this.checkExpressionErrors(refDestructuringErrors, true);
			if (update) {
				this.checkLValSimple(node.argument);
			} else if (
				this.strict &&
				node.operator === "delete" &&
				isLocalVariableAccess(node.argument)
			) {
				this.raiseRecoverable(
					node.start,
					"Deleting local variable in strict mode"
				);
			} else if (
				node.operator === "delete" &&
				isPrivateFieldAccess(node.argument)
			) {
				this.raiseRecoverable(node.start, "Private fields can not be deleted");
			} else {
				unary = true;
			}
			expr = this.finishNode(
				node,
				update ? "UpdateExpression" : "UnaryExpression"
			);
		} else if (!unary && this.type === tokTypes.privateId) {
			if (
				(forInit || this.privateNameStack.length === 0) &&
				this.options.checkPrivateFields
			) {
				this.unexpected();
			}
			expr = this.parsePrivateIdent();
			// A private name is only an operand of `in`.
			if (this.type !== tokTypes._in) this.unexpected();
		} else {
			expr = this.parseExprSubscripts(refDestructuringErrors, forInit);
			if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
			while (this.type.postfix && !this.canInsertSemicolon()) {
				const node = this.startNodeAt(startPos, startLoc);
				node.operator = /** @type {UpdateOperator} */ (this.value);
				node.prefix = false;
				node.argument = expr;
				this.checkLValSimple(expr);
				this.next();
				expr = this.finishNode(node, "UpdateExpression");
			}
		}

		if (
			!incDec &&
			!(expr.type === "ArrowFunctionExpression" && expr.start === startPos) &&
			this.eat(tokTypes.starstar)
		) {
			if (unary) this.unexpected(this.lastTokStart);
			return this.buildBinary(
				startPos,
				startLoc,
				expr,
				this.parseMaybeUnary(null, false, false, forInit),
				"**",
				false
			);
		}
		return expr;
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseExprSubscripts(refDestructuringErrors, forInit) {
		const startPos = this.start;
		const startLoc = this.startLoc;
		let oldDoubleProto = -1;
		let oldShorthandAssign = -1;
		if (refDestructuringErrors) {
			oldDoubleProto = refDestructuringErrors.doubleProto;
			oldShorthandAssign = refDestructuringErrors.shorthandAssign;
			refDestructuringErrors.doubleProto =
				refDestructuringErrors.shorthandAssign = -1;
		}
		const expr = this.parseExprAtom(refDestructuringErrors, forInit);
		if (
			expr.type === "ArrowFunctionExpression" &&
			this.input.slice(this.lastTokStart, this.lastTokEnd) !== ")"
		) {
			return expr;
		}
		const result = this.parseSubscripts(
			expr,
			startPos,
			startLoc,
			false,
			forInit
		);
		if (refDestructuringErrors) {
			if (result.end > expr.end) {
				this.checkExpressionErrors(refDestructuringErrors, true);
				if (refDestructuringErrors.parenthesizedAssign >= result.start) {
					refDestructuringErrors.parenthesizedAssign = -1;
				}
				if (refDestructuringErrors.parenthesizedBind >= result.start) {
					refDestructuringErrors.parenthesizedBind = -1;
				}
				if (refDestructuringErrors.trailingComma >= result.start) {
					refDestructuringErrors.trailingComma = -1;
				}
			}
			if (oldDoubleProto > -1) {
				refDestructuringErrors.doubleProto = oldDoubleProto;
			}
			if (oldShorthandAssign > -1) {
				refDestructuringErrors.shorthandAssign = oldShorthandAssign;
			}
		}
		return result;
	}

	/**
	 * @param {Expression | NodeOf<"Super">} baseExpr what the subscripts apply to
	 * @param {number} startPos where the whole expression starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {boolean} noCalls whether a call would belong to an enclosing `new`
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseSubscripts(baseExpr, startPos, startLoc, noCalls, forInit) {
		let maybeAsyncArrow =
			this.options.ecmaVersion >= 8 &&
			baseExpr.type === "Identifier" &&
			baseExpr.name === "async" &&
			this.lastTokEnd === baseExpr.end &&
			!this.canInsertSemicolon() &&
			baseExpr.end - baseExpr.start === 5 &&
			this.potentialArrowAt === baseExpr.start;
		let optionalChained = false;
		let current = baseExpr;

		for (;;) {
			const element = this.parseSubscript(
				current,
				startPos,
				startLoc,
				noCalls,
				maybeAsyncArrow,
				optionalChained,
				forInit
			);

			if (
				/** @type {Expression & { optional?: boolean }} */ (element).optional
			) {
				optionalChained = true;
			}
			if (
				element.end === current.end ||
				element.type === "ArrowFunctionExpression"
			) {
				if (optionalChained) {
					const chainNode = this.startNodeAt(startPos, startLoc);
					chainNode.expression = /** @type {Expression} */ (element);
					return this.finishNode(chainNode, "ChainExpression");
				}
				return /** @type {Expression} */ (element);
			}

			current = element;
			maybeAsyncArrow = false;
		}
	}

	/** @returns {boolean} whether an async arrow's body follows */
	shouldParseAsyncArrow() {
		return !this.canInsertSemicolon() && this.eat(tokTypes.arrow);
	}

	/**
	 * @param {number} startPos where the arrow starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {(Expression | SpreadElement)[]} exprList its parameters
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"ArrowFunctionExpression">} the arrow function
	 */
	parseSubscriptAsyncArrow(startPos, startLoc, exprList, forInit) {
		return this.parseArrowExpression(
			this.startNodeAt(startPos, startLoc),
			exprList,
			true,
			forInit
		);
	}

	/**
	 * @param {Expression | NodeOf<"Super">} baseExpr what the subscript applies to
	 * @param {number} startPos where the whole expression starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {boolean} noCalls whether a call would belong to an enclosing `new`
	 * @param {boolean} maybeAsyncArrow whether the base may head an async arrow
	 * @param {boolean} optionalChained whether an earlier link was optional
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression | NodeOf<"Super">} the expression, or the base when nothing applied
	 */
	parseSubscript(
		baseExpr,
		startPos,
		startLoc,
		noCalls,
		maybeAsyncArrow,
		optionalChained,
		forInit
	) {
		const optionalSupported = this.options.ecmaVersion >= 11;
		const optional = optionalSupported && this.eat(tokTypes.questionDot);
		if (noCalls && optional) {
			this.raise(
				this.lastTokStart,
				"Optional chaining cannot appear in the callee of new expressions"
			);
		}

		const computed = this.eat(tokTypes.bracketL);
		if (
			computed ||
			(optional &&
				this.type !== tokTypes.parenL &&
				this.type !== tokTypes.backQuote) ||
			this.eat(tokTypes.dot)
		) {
			const node = this.startNodeAt(startPos, startLoc);
			node.object = baseExpr;
			if (computed) {
				node.property = this.parseExpression();
				this.expect(tokTypes.bracketR);
			} else if (
				this.type === tokTypes.privateId &&
				baseExpr.type !== "Super"
			) {
				node.property = this.parsePrivateIdent();
			} else {
				node.property = this.parseIdent(this.options.allowReserved !== "never");
			}
			node.computed = Boolean(computed);
			if (optionalSupported) node.optional = optional;
			return this.finishNode(node, "MemberExpression");
		}
		if (!noCalls && this.eat(tokTypes.parenL)) {
			const refDestructuringErrors = new DestructuringErrors();
			const oldYieldPos = this.yieldPos;
			const oldAwaitPos = this.awaitPos;
			const oldAwaitIdentPos = this.awaitIdentPos;
			this.yieldPos = 0;
			this.awaitPos = 0;
			this.awaitIdentPos = 0;
			const exprList = /** @type {CallArguments} */ (
				this.parseExprList(
					tokTypes.parenR,
					this.options.ecmaVersion >= 8,
					false,
					refDestructuringErrors
				)
			);
			if (maybeAsyncArrow && !optional && this.shouldParseAsyncArrow()) {
				this.checkPatternErrors(refDestructuringErrors, false);
				this.checkYieldAwaitInDefaultParams();
				if (this.awaitIdentPos > 0) {
					this.raise(
						this.awaitIdentPos,
						"Cannot use 'await' as identifier inside an async function"
					);
				}
				this.yieldPos = oldYieldPos;
				this.awaitPos = oldAwaitPos;
				this.awaitIdentPos = oldAwaitIdentPos;
				return this.parseSubscriptAsyncArrow(
					startPos,
					startLoc,
					exprList,
					forInit
				);
			}
			this.checkExpressionErrors(refDestructuringErrors, true);
			this.yieldPos = oldYieldPos || this.yieldPos;
			this.awaitPos = oldAwaitPos || this.awaitPos;
			this.awaitIdentPos = oldAwaitIdentPos || this.awaitIdentPos;
			const node = this.startNodeAt(startPos, startLoc);
			node.callee = baseExpr;
			node.arguments = exprList;
			if (optionalSupported) node.optional = optional;
			return this.finishNode(node, "CallExpression");
		}
		if (this.type === tokTypes.backQuote) {
			if (optional || optionalChained) {
				this.raise(
					this.start,
					"Optional chaining cannot appear in the tag of tagged template expressions"
				);
			}
			const node = this.startNodeAt(startPos, startLoc);
			node.tag = /** @type {Expression} */ (baseExpr);
			node.quasi = this.parseTemplate({ isTagged: true });
			return this.finishNode(node, "TaggedTemplateExpression");
		}
		return baseExpr;
	}

	/**
	 * Read an expression that no operator binds into: a token that is an
	 * expression on its own, or one that punctuation encloses.
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {boolean=} forNew whether an enclosing `new` reads the callee
	 * @returns {Expression | NodeOf<"Super">} the expression
	 */
	parseExprAtom(refDestructuringErrors, forInit, forNew) {
		// A `/` where an expression belongs means the tokenizer guessed division,
		// so make it read the regexp it really is.
		if (this.type === tokTypes.slash) this.readRegexp();

		let node;
		const canBeArrow = this.potentialArrowAt === this.start;
		switch (this.type) {
			case tokTypes._super:
				if (!this.allowSuper) {
					this.raise(this.start, "'super' keyword outside a method");
				}
				node = this.startNode();
				this.next();
				if (this.type === tokTypes.parenL && !this.allowDirectSuper) {
					this.raise(
						node.start,
						"super() call outside constructor of a subclass"
					);
				}
				if (
					this.type !== tokTypes.dot &&
					this.type !== tokTypes.bracketL &&
					this.type !== tokTypes.parenL
				) {
					this.unexpected();
				}
				return this.finishNode(node, "Super");

			case tokTypes._this:
				node = this.startNode();
				this.next();
				return this.finishNode(node, "ThisExpression");

			case tokTypes.name: {
				const startPos = this.start;
				const startLoc = this.startLoc;
				const { containsEsc } = this;
				let id = this.parseIdent(false);
				if (
					this.options.ecmaVersion >= 8 &&
					!containsEsc &&
					id.name === "async" &&
					!this.canInsertSemicolon() &&
					this.eat(tokTypes._function)
				) {
					this.overrideContext(tokContexts.f_expr);
					return /** @type {NodeOf<"FunctionExpression">} */ (
						this.parseFunction(
							this.startNodeAt(startPos, startLoc),
							0,
							false,
							true,
							forInit
						)
					);
				}
				if (canBeArrow && !this.canInsertSemicolon()) {
					if (this.eat(tokTypes.arrow)) {
						return this.parseArrowExpression(
							this.startNodeAt(startPos, startLoc),
							[id],
							false,
							forInit
						);
					}
					if (
						this.options.ecmaVersion >= 8 &&
						id.name === "async" &&
						this.type === tokTypes.name &&
						!containsEsc &&
						(!this.potentialArrowInForAwait ||
							this.value !== "of" ||
							this.containsEsc)
					) {
						id = this.parseIdent(false);
						if (this.canInsertSemicolon() || !this.eat(tokTypes.arrow)) {
							this.unexpected();
						}
						return this.parseArrowExpression(
							this.startNodeAt(startPos, startLoc),
							[id],
							true,
							forInit
						);
					}
				}
				return id;
			}

			case tokTypes.regexp: {
				const value = /** @type {RegExpTokenValue} */ (this.value);
				node = this.parseLiteral(value.value);
				/** @type {RegExpLiteral} */ (node).regex = {
					pattern: value.pattern,
					flags: value.flags
				};
				return node;
			}

			case tokTypes.num:
			case tokTypes.string:
				return this.parseLiteral(
					/** @type {string | number | bigint} */ (this.value)
				);

			case tokTypes._null:
			case tokTypes._true:
			case tokTypes._false:
				node = this.startNode();
				node.value =
					this.type === tokTypes._null ? null : this.type === tokTypes._true;
				node.raw = this.type.keyword;
				this.next();
				return this.finishNode(node, "Literal");

			case tokTypes.parenL: {
				const start = this.start;
				const expr = this.parseParenAndDistinguishExpression(
					canBeArrow,
					forInit
				);
				if (refDestructuringErrors) {
					if (
						refDestructuringErrors.parenthesizedAssign < 0 &&
						!this.isSimpleAssignTarget(expr)
					) {
						refDestructuringErrors.parenthesizedAssign = start;
					}
					if (refDestructuringErrors.parenthesizedBind < 0) {
						refDestructuringErrors.parenthesizedBind = start;
					}
				}
				return expr;
			}

			case tokTypes.bracketL:
				node = this.startNode();
				this.next();
				node.elements = this.parseExprList(
					tokTypes.bracketR,
					true,
					true,
					refDestructuringErrors
				);
				return this.finishNode(node, "ArrayExpression");

			case tokTypes.braceL:
				this.overrideContext(tokContexts.b_expr);
				return this.parseObj(false, refDestructuringErrors);

			case tokTypes._function:
				node = this.startNode();
				this.next();
				return /** @type {NodeOf<"FunctionExpression">} */ (
					this.parseFunction(node, 0)
				);

			case tokTypes._class:
				return /** @type {NodeOf<"ClassExpression">} */ (
					this.parseClass(this.startNode(), false)
				);

			case tokTypes._new:
				return this.parseNew();

			case tokTypes.backQuote:
				return this.parseTemplate();

			case tokTypes._import:
				if (this.options.ecmaVersion >= 11) return this.parseExprImport(forNew);
				return this.unexpected();

			default:
				return this.parseExprAtomDefault();
		}
	}

	/** @returns {never} never returns */
	parseExprAtomDefault() {
		return this.unexpected();
	}

	/**
	 * @param {boolean=} forNew whether an enclosing `new` reads the callee
	 * @returns {ImportExpression | MetaProperty} the import expression or meta property
	 */
	parseExprImport(forNew) {
		const node = this.startNode();

		// `import` reads as an identifier for `import.meta`, and `parseIdent(true)`
		// does not check escapes, so the keyword's own escape check is here.
		if (this.containsEsc) {
			this.raiseRecoverable(this.start, "Escape sequence in keyword import");
		}
		this.next();

		if (this.type === tokTypes.parenL && !forNew) {
			return this.parseDynamicImport(node);
		}
		if (this.type === tokTypes.dot) {
			const meta = this.startNodeAt(node.start, node.loc && node.loc.start);
			meta.name = "import";
			node.meta = this.finishNode(meta, "Identifier");
			return this.parseImportMeta(node);
		}
		return this.unexpected();
	}

	/**
	 * @param {UnfinishedNode} node the import expression node
	 * @returns {ImportExpression} the finished expression
	 */
	parseDynamicImport(node) {
		this.next();

		node.source = this.parseMaybeAssign();

		if (this.options.ecmaVersion >= 16) {
			if (this.eat(tokTypes.parenR)) {
				node.options = null;
			} else {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.parenR)) {
					node.options = null;
				} else {
					node.options = this.parseMaybeAssign();
					if (!this.eat(tokTypes.parenR)) {
						this.expect(tokTypes.comma);
						if (!this.afterTrailingComma(tokTypes.parenR)) this.unexpected();
					}
				}
			}
		} else if (!this.eat(tokTypes.parenR)) {
			const errorPos = this.start;
			if (this.eat(tokTypes.comma) && this.eat(tokTypes.parenR)) {
				this.raiseRecoverable(
					errorPos,
					"Trailing comma is not allowed in import()"
				);
			} else {
				this.unexpected(errorPos);
			}
		}

		return this.finishNode(node, "ImportExpression");
	}

	/**
	 * @param {UnfinishedNode} node the meta property node
	 * @returns {MetaProperty} the finished expression
	 */
	parseImportMeta(node) {
		this.next();

		const { containsEsc } = this;
		node.property = this.parseIdent(true);

		if (node.property.name !== "meta") {
			this.raiseRecoverable(
				node.property.start,
				"The only valid meta property for import is 'import.meta'"
			);
		}
		if (containsEsc) {
			this.raiseRecoverable(
				node.start,
				"'import.meta' must not contain escaped characters"
			);
		}
		if (
			this.options.sourceType !== "module" &&
			!this.options.allowImportExportEverywhere
		) {
			this.raiseRecoverable(
				node.start,
				"Cannot use 'import.meta' outside a module"
			);
		}

		return this.finishNode(node, "MetaProperty");
	}

	/**
	 * @param {LiteralValue} value the literal's value
	 * @returns {NodeOf<"Literal">} the finished literal
	 */
	parseLiteral(value) {
		const node = this.startNode();
		node.value = value;
		node.raw = this.input.slice(this.start, this.end);
		if (node.raw.charCodeAt(node.raw.length - 1) === 110) {
			node.bigint =
				node.value === null || node.value === undefined
					? node.raw.slice(0, -1).replace(/_/g, "")
					: node.value.toString();
		}
		this.next();
		return this.finishNode(node, "Literal");
	}

	/** @returns {Expression} the parenthesized expression */
	parseParenExpression() {
		this.expect(tokTypes.parenL);
		const val = this.parseExpression();
		this.expect(tokTypes.parenR);
		return val;
	}

	/**
	 * @param {(Expression | Pattern)[]} exprList what the parentheses held
	 * @returns {boolean} whether an arrow body may follow
	 */
	shouldParseArrow(exprList) {
		return !this.canInsertSemicolon();
	}

	/**
	 * Read a parenthesized expression, which is only known to be an arrow's
	 * parameter list once the `=>` after the closing paren is seen.
	 * @param {boolean} canBeArrow whether an arrow may start here
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseParenAndDistinguishExpression(canBeArrow, forInit) {
		const startPos = this.start;
		const startLoc = this.startLoc;
		/** @type {Expression} */
		let val;
		const allowTrailingComma = this.options.ecmaVersion >= 8;
		if (this.options.ecmaVersion >= 6) {
			this.next();

			const innerStartPos = this.start;
			const innerStartLoc = this.startLoc;
			/** @type {(Expression | Pattern)[]} */
			const exprList = [];
			let first = true;
			let lastIsComma = false;
			const refDestructuringErrors = new DestructuringErrors();
			const oldYieldPos = this.yieldPos;
			const oldAwaitPos = this.awaitPos;
			let spreadStart;
			this.yieldPos = 0;
			this.awaitPos = 0;
			// `awaitIdentPos` is deliberately kept, so an `await` nested in these
			// parameters is still checked once the arrow is known.
			while (this.type !== tokTypes.parenR) {
				if (first) first = false;
				else this.expect(tokTypes.comma);
				if (
					allowTrailingComma &&
					this.afterTrailingComma(tokTypes.parenR, true)
				) {
					lastIsComma = true;
					break;
				} else if (this.type === tokTypes.ellipsis) {
					spreadStart = this.start;
					exprList.push(this.parseParenItem(this.parseRestBinding()));
					if (this.type === tokTypes.comma) {
						this.raiseRecoverable(
							this.start,
							"Comma is not permitted after the rest element"
						);
					}
					break;
				} else {
					exprList.push(
						this.parseMaybeAssign(
							false,
							refDestructuringErrors,
							this.parseParenItem
						)
					);
				}
			}
			const innerEndPos = this.lastTokEnd;
			const innerEndLoc = this.lastTokEndLoc;
			this.expect(tokTypes.parenR);

			if (
				canBeArrow &&
				this.shouldParseArrow(exprList) &&
				this.eat(tokTypes.arrow)
			) {
				this.checkPatternErrors(refDestructuringErrors, false);
				this.checkYieldAwaitInDefaultParams();
				this.yieldPos = oldYieldPos;
				this.awaitPos = oldAwaitPos;
				return this.parseParenArrowList(startPos, startLoc, exprList, forInit);
			}

			if (!exprList.length || lastIsComma) this.unexpected(this.lastTokStart);
			if (spreadStart) this.unexpected(spreadStart);
			this.checkExpressionErrors(refDestructuringErrors, true);
			this.yieldPos = oldYieldPos || this.yieldPos;
			this.awaitPos = oldAwaitPos || this.awaitPos;

			if (exprList.length > 1) {
				val = /** @type {NodeOf<"SequenceExpression">} */ (
					this.startNodeAt(innerStartPos, innerStartLoc)
				);
				val.expressions = /** @type {Expression[]} */ (exprList);
				this.finishNodeAt(val, "SequenceExpression", innerEndPos, innerEndLoc);
			} else {
				val = /** @type {Expression} */ (exprList[0]);
			}
		} else {
			val = this.parseParenExpression();
		}

		if (this.options.preserveParens) {
			const par = this.startNodeAt(startPos, startLoc);
			par.expression = val;
			return /** @type {Expression} */ (
				/** @type {AnyNode} */ (this.finishNode(par, "ParenthesizedExpression"))
			);
		}
		return val;
	}

	/**
	 * @template {AnyNode} T
	 * @param {T} item what the parentheses held
	 * @returns {T} the same item
	 */
	parseParenItem(item) {
		return item;
	}

	/**
	 * @param {number} startPos where the arrow starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {(Expression | Pattern)[]} exprList its parameters
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"ArrowFunctionExpression">} the arrow function
	 */
	parseParenArrowList(startPos, startLoc, exprList, forInit) {
		return this.parseArrowExpression(
			this.startNodeAt(startPos, startLoc),
			exprList,
			false,
			forInit
		);
	}

	/**
	 * Read a `new` expression, whose callee takes subscripts but not a call —
	 * the argument list belongs to the `new` itself.
	 * @returns {NodeOf<"NewExpression"> | MetaProperty} the expression
	 */
	parseNew() {
		if (this.containsEsc) {
			this.raiseRecoverable(this.start, "Escape sequence in keyword new");
		}
		const node = this.startNode();
		this.next();
		if (this.options.ecmaVersion >= 6 && this.type === tokTypes.dot) {
			const meta = this.startNodeAt(node.start, node.loc && node.loc.start);
			meta.name = "new";
			node.meta = this.finishNode(meta, "Identifier");
			this.next();
			const { containsEsc } = this;
			node.property = this.parseIdent(true);
			if (node.property.name !== "target") {
				this.raiseRecoverable(
					node.property.start,
					"The only valid meta property for new is 'new.target'"
				);
			}
			if (containsEsc) {
				this.raiseRecoverable(
					node.start,
					"'new.target' must not contain escaped characters"
				);
			}
			if (!this.allowNewDotTarget) {
				this.raiseRecoverable(
					node.start,
					"'new.target' can only be used in functions and class static block"
				);
			}
			return this.finishNode(node, "MetaProperty");
		}
		const startPos = this.start;
		const startLoc = this.startLoc;
		node.callee = this.parseSubscripts(
			this.parseExprAtom(null, false, true),
			startPos,
			startLoc,
			true,
			false
		);
		if (
			/** @type {NodeOf<"NewExpression">["callee"]} */ (node.callee).type ===
			"Super"
		) {
			this.raiseRecoverable(startPos, "Invalid use of 'super'");
		}
		if (this.eat(tokTypes.parenL)) {
			node.arguments = /** @type {CallArguments} */ (
				this.parseExprList(
					tokTypes.parenR,
					this.options.ecmaVersion >= 8,
					false
				)
			);
		} else {
			node.arguments = empty;
		}
		return this.finishNode(node, "NewExpression");
	}

	/**
	 * @param {{ isTagged: boolean }} opts whether the template is tagged
	 * @returns {NodeOf<"TemplateElement">} the finished element
	 */
	parseTemplateElement(opts) {
		const { isTagged } = opts;

		const elem = this.startNode();
		if (this.type === tokTypes.invalidTemplate) {
			if (!isTagged) {
				this.raiseRecoverable(
					this.start,
					"Bad escape sequence in untagged template literal"
				);
			}
			elem.value = {
				raw: /** @type {string} */ (this.value).replace(/\r\n?/g, "\n"),
				cooked: null
			};
		} else {
			elem.value = {
				raw: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"),
				cooked: /** @type {string | null} */ (this.value)
			};
		}
		this.next();
		elem.tail = this.type === tokTypes.backQuote;
		return this.finishNode(elem, "TemplateElement");
	}

	/**
	 * @param {{ isTagged?: boolean }=} opts whether the template is tagged
	 * @returns {NodeOf<"TemplateLiteral">} the finished template
	 */
	parseTemplate(opts) {
		const isTagged = Boolean(opts && opts.isTagged);

		const node = /** @type {NodeInProgress<"TemplateLiteral">} */ (
			this.startNode()
		);
		this.next();
		node.expressions = [];
		let curElt = this.parseTemplateElement({ isTagged });
		node.quasis = [curElt];
		while (!curElt.tail) {
			if (this.type === tokTypes.eof) {
				this.raise(this.pos, "Unterminated template literal");
			}
			this.expect(tokTypes.dollarBraceL);
			node.expressions.push(this.parseExpression());
			this.expect(tokTypes.braceR);
			curElt = this.parseTemplateElement({ isTagged });
			node.quasis.push(curElt);
		}
		this.next();
		return this.finishNode(node, "TemplateLiteral");
	}

	/**
	 * @param {KeyedNode} prop the property whose key was just read
	 * @returns {boolean} whether `async` there was a modifier rather than the name
	 */
	isAsyncProp(prop) {
		return (
			!prop.computed &&
			prop.key.type === "Identifier" &&
			prop.key.name === "async" &&
			(this.type === tokTypes.name ||
				this.type === tokTypes.num ||
				this.type === tokTypes.string ||
				this.type === tokTypes.bracketL ||
				Boolean(this.type.keyword) ||
				(this.options.ecmaVersion >= 9 && this.type === tokTypes.star)) &&
			!lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
		);
	}

	/**
	 * @template {BindsPattern} P
	 * @param {P} isPattern whether it binds rather than builds a value
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {ObjectNodeFor<P>} the object literal or pattern
	 */
	parseObj(isPattern, refDestructuringErrors) {
		const node = this.startNode();
		let first = true;
		/** @type {PropHash} */
		const propHash = {};
		node.properties = [];
		this.next();
		while (!this.eat(tokTypes.braceR)) {
			if (first) {
				first = false;
			} else {
				this.expect(tokTypes.comma);
				if (
					this.options.ecmaVersion >= 5 &&
					this.afterTrailingComma(tokTypes.braceR)
				) {
					break;
				}
			}

			const prop = this.parseProperty(isPattern, refDestructuringErrors);
			if (!isPattern) {
				this.checkPropClash(
					/** @type {NodeOf<"Property" | "SpreadElement">} */ (prop),
					propHash,
					refDestructuringErrors
				);
			}
			/** @type {(NodeOf<"Property"> | NodeOf<"SpreadElement"> | NodeOf<"RestElement">)[]} */ (
				node.properties
			).push(prop);
		}
		return /** @type {ObjectNodeFor<P>} */ (
			this.finishNode(node, isPattern ? "ObjectPattern" : "ObjectExpression")
		);
	}

	/**
	 * @param {boolean} isPattern whether it binds rather than builds a value
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {NodeOf<"Property"> | NodeOf<"SpreadElement"> | NodeOf<"RestElement">} the property
	 */
	parseProperty(isPattern, refDestructuringErrors) {
		const prop = this.startNode();
		let isGenerator;
		let isAsync;
		let startPos;
		let startLoc;
		if (this.options.ecmaVersion >= 9 && this.eat(tokTypes.ellipsis)) {
			if (isPattern) {
				prop.argument = this.parseIdent(false);
				if (this.type === tokTypes.comma) {
					this.raiseRecoverable(
						this.start,
						"Comma is not permitted after the rest element"
					);
				}
				return this.finishNode(prop, "RestElement");
			}
			prop.argument = this.parseMaybeAssign(false, refDestructuringErrors);
			// Recorded so that `toAssignable` can refuse a trailing comma here.
			if (
				this.type === tokTypes.comma &&
				refDestructuringErrors &&
				refDestructuringErrors.trailingComma < 0
			) {
				refDestructuringErrors.trailingComma = this.start;
			}
			return this.finishNode(prop, "SpreadElement");
		}
		if (this.options.ecmaVersion >= 6) {
			prop.method = false;
			prop.shorthand = false;
			if (isPattern || refDestructuringErrors) {
				startPos = this.start;
				startLoc = this.startLoc;
			}
			if (!isPattern) isGenerator = this.eat(tokTypes.star);
		}
		const { containsEsc } = this;
		this.parsePropertyName(prop);
		if (
			!isPattern &&
			!containsEsc &&
			this.options.ecmaVersion >= 8 &&
			!isGenerator &&
			this.isAsyncProp(/** @type {KeyedNode} */ (prop))
		) {
			isAsync = true;
			isGenerator = this.options.ecmaVersion >= 9 && this.eat(tokTypes.star);
			this.parsePropertyName(prop);
		} else {
			isAsync = false;
		}
		this.parsePropertyValue(
			/** @type {KeyedNode} */ (prop),
			isPattern,
			isGenerator,
			isAsync,
			startPos,
			startLoc,
			refDestructuringErrors,
			containsEsc
		);
		return this.finishNode(prop, "Property");
	}

	/**
	 * @param {KeyedNode} prop the property whose `get`/`set` was just read
	 * @returns {void}
	 */
	parseGetterSetter(prop) {
		const kind = /** @type {"get" | "set"} */ (
			/** @type {Identifier} */ (prop.key).name
		);
		this.parsePropertyName(prop);
		prop.value = this.parseMethod(false);
		prop.kind = kind;
		const paramCount = prop.kind === "get" ? 0 : 1;
		if (prop.value.params.length === paramCount) {
			if (prop.kind === "set" && prop.value.params[0].type === "RestElement") {
				this.raiseRecoverable(
					prop.value.params[0].start,
					"Setter cannot use rest params"
				);
			}
		} else {
			const start = prop.value.start;
			if (prop.kind === "get") {
				this.raiseRecoverable(start, "getter should have no params");
			} else {
				this.raiseRecoverable(start, "setter should have exactly one param");
			}
		}
	}

	/**
	 * @param {KeyedNode} prop the property whose key was read
	 * @param {boolean} isPattern whether it binds rather than builds a value
	 * @param {boolean | undefined} isGenerator whether it is a generator method
	 * @param {boolean | undefined} isAsync whether it is an async method
	 * @param {number | undefined} startPos where the property starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {RefDestructuringErrors | undefined} refDestructuringErrors where to record deferred errors
	 * @param {boolean} containsEsc whether the key was written with an escape
	 * @returns {void}
	 */
	parsePropertyValue(
		prop,
		isPattern,
		isGenerator,
		isAsync,
		startPos,
		startLoc,
		refDestructuringErrors,
		containsEsc
	) {
		if ((isGenerator || isAsync) && this.type === tokTypes.colon) {
			this.unexpected();
		}

		if (this.eat(tokTypes.colon)) {
			prop.value = isPattern
				? this.parseMaybeDefault(this.start, this.startLoc)
				: this.parseMaybeAssign(false, refDestructuringErrors);
			prop.kind = "init";
		} else if (this.options.ecmaVersion >= 6 && this.type === tokTypes.parenL) {
			if (isPattern) this.unexpected();
			prop.method = true;
			prop.value = this.parseMethod(isGenerator, isAsync);
			prop.kind = "init";
		} else if (
			!isPattern &&
			!containsEsc &&
			this.options.ecmaVersion >= 5 &&
			!prop.computed &&
			prop.key.type === "Identifier" &&
			(prop.key.name === "get" || prop.key.name === "set") &&
			this.type !== tokTypes.comma &&
			this.type !== tokTypes.braceR &&
			this.type !== tokTypes.eq
		) {
			if (isGenerator || isAsync) this.unexpected();
			this.parseGetterSetter(prop);
		} else if (
			this.options.ecmaVersion >= 6 &&
			!prop.computed &&
			prop.key.type === "Identifier"
		) {
			if (isGenerator || isAsync) this.unexpected();
			this.checkUnreserved(prop.key);
			if (prop.key.name === "await" && !this.awaitIdentPos) {
				this.awaitIdentPos = /** @type {number} */ (startPos);
			}
			if (isPattern) {
				prop.value = this.parseMaybeDefault(
					/** @type {number} */ (startPos),
					/** @type {Position | null} */ (startLoc),
					this.copyNode(prop.key)
				);
			} else if (this.type === tokTypes.eq && refDestructuringErrors) {
				if (refDestructuringErrors.shorthandAssign < 0) {
					refDestructuringErrors.shorthandAssign = this.start;
				}
				prop.value = this.parseMaybeDefault(
					/** @type {number} */ (startPos),
					/** @type {Position | null} */ (startLoc),
					this.copyNode(prop.key)
				);
			} else {
				prop.value = this.copyNode(prop.key);
			}
			prop.kind = "init";
			prop.shorthand = true;
		} else {
			this.unexpected();
		}
	}

	/**
	 * @param {UnfinishedNode} prop the property to name
	 * @returns {NodeOf<"Property">["key"]} the key
	 */
	parsePropertyName(prop) {
		if (this.options.ecmaVersion >= 6) {
			if (this.eat(tokTypes.bracketL)) {
				prop.computed = true;
				prop.key = this.parseMaybeAssign();
				this.expect(tokTypes.bracketR);
				return /** @type {NodeOf<"Property">["key"]} */ (prop.key);
			}
			prop.computed = false;
		}
		prop.key =
			this.type === tokTypes.num || this.type === tokTypes.string
				? /** @type {NodeOf<"Literal">} */ (this.parseExprAtom())
				: this.parseIdent(this.options.allowReserved !== "never");
		return /** @type {NodeOf<"Property">["key"]} */ (prop.key);
	}

	/**
	 * @param {UnfinishedNode} node the function node to blank out
	 * @returns {void}
	 */
	initFunction(node) {
		node.id = null;
		if (this.options.ecmaVersion >= 6) {
			// acorn writes `node.generator = node.expression = false`, so the
			// `expression` slot is the one created first
			node.expression = false;
			node.generator = false;
		}
		if (this.options.ecmaVersion >= 8) node.async = false;
	}

	/**
	 * @param {boolean=} isGenerator whether it is a generator
	 * @param {boolean=} isAsync whether it is async
	 * @param {boolean=} allowDirectSuper whether `super()` may be called in it
	 * @returns {NodeOf<"FunctionExpression">} the method's function expression
	 */
	parseMethod(isGenerator, isAsync, allowDirectSuper) {
		const node = this.startNode();
		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		const oldAwaitIdentPos = this.awaitIdentPos;

		this.initFunction(node);
		if (this.options.ecmaVersion >= 6) node.generator = isGenerator;
		if (this.options.ecmaVersion >= 8) node.async = Boolean(isAsync);

		this.yieldPos = 0;
		this.awaitPos = 0;
		this.awaitIdentPos = 0;
		this.enterScope(
			functionFlags(Boolean(isAsync), /** @type {boolean} */ (node.generator)) |
				SCOPE_SUPER |
				(allowDirectSuper ? SCOPE_DIRECT_SUPER : 0)
		);

		this.expect(tokTypes.parenL);
		node.params = /** @type {Pattern[]} */ (
			this.parseBindingList(
				tokTypes.parenR,
				false,
				this.options.ecmaVersion >= 8
			)
		);
		this.checkYieldAwaitInDefaultParams();
		this.parseFunctionBody(node, false, true, false);

		this.yieldPos = oldYieldPos;
		this.awaitPos = oldAwaitPos;
		this.awaitIdentPos = oldAwaitIdentPos;
		return this.finishNode(node, "FunctionExpression");
	}

	/**
	 * @param {UnfinishedNode} node the arrow node
	 * @param {(Expression | Pattern | NodeOf<"SpreadElement">)[]} params what stood before the arrow
	 * @param {boolean} isAsync whether it is async
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"ArrowFunctionExpression">} the arrow function
	 */
	parseArrowExpression(node, params, isAsync, forInit) {
		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		const oldAwaitIdentPos = this.awaitIdentPos;

		this.enterScope(functionFlags(isAsync, false) | SCOPE_ARROW);
		this.initFunction(node);
		if (this.options.ecmaVersion >= 8) node.async = Boolean(isAsync);

		this.yieldPos = 0;
		this.awaitPos = 0;
		this.awaitIdentPos = 0;

		node.params = /** @type {Pattern[]} */ (
			this.toAssignableList(params, true)
		);
		this.parseFunctionBody(node, true, false, forInit);

		this.yieldPos = oldYieldPos;
		this.awaitPos = oldAwaitPos;
		this.awaitIdentPos = oldAwaitIdentPos;
		return this.finishNode(node, "ArrowFunctionExpression");
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @param {boolean=} isArrowFunction whether it is an arrow
	 * @param {boolean=} isMethod whether it is a method
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {void}
	 */
	parseFunctionBody(node, isArrowFunction, isMethod, forInit) {
		const isExpression = isArrowFunction && this.type !== tokTypes.braceL;
		const oldStrict = this.strict;
		let useStrict = false;

		if (isExpression) {
			node.body = this.parseMaybeAssign(forInit);
			node.expression = true;
			this.checkParams(node, false);
		} else {
			const nonSimple =
				this.options.ecmaVersion >= 7 &&
				!this.isSimpleParamList(/** @type {Pattern[]} */ (node.params));
			if (!oldStrict || nonSimple) {
				useStrict = this.strictDirective(this.end);
				if (useStrict && nonSimple) {
					this.raiseRecoverable(
						node.start,
						"Illegal 'use strict' directive in function with non-simple parameter list"
					);
				}
			}
			// Labels and the enclosing function flag are per-function, so hold the
			// outer ones aside until the body is read.
			const oldLabels = this.labels;
			this.labels = [];
			if (useStrict) this.strict = true;

			this.checkParams(
				node,
				!oldStrict &&
					!useStrict &&
					!isArrowFunction &&
					!isMethod &&
					this.isSimpleParamList(/** @type {Pattern[]} */ (node.params))
			);
			if (this.strict && node.id) this.checkLValSimple(node.id, BIND_OUTSIDE);
			node.body = this.parseBlock(false, undefined, useStrict && !oldStrict);
			node.expression = false;
			this.adaptDirectivePrologue(node.body.body);
			this.labels = oldLabels;
		}
		this.exitScope();
		this.rereadYieldContext();
	}

	/**
	 * The token after a function body is read in the body's scope, so a `yield`
	 * there is classified again in the enclosing one.
	 * @returns {void}
	 */
	rereadYieldContext() {
		if (this.type === tokTypes.name && this.value === "yield") {
			this.exprAllowed = this.inGeneratorContext();
		}
	}

	/**
	 * @param {Pattern[]} params the parameter list
	 * @returns {boolean} whether every parameter is a plain identifier
	 */
	isSimpleParamList(params) {
		for (const param of params) {
			if (param.type !== "Identifier") return false;
		}
		return true;
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @param {boolean} allowDuplicates whether repeated parameter names are allowed
	 * @returns {void}
	 */
	checkParams(node, allowDuplicates) {
		const nameHash = Object.create(null);
		for (const param of /** @type {Pattern[]} */ (node.params)) {
			this.checkLValInnerPattern(
				param,
				BIND_VAR,
				allowDuplicates ? null : nameHash
			);
		}
	}

	/**
	 * @param {TokenType} close the token that ends the list
	 * @param {boolean} allowTrailingComma whether a trailing comma is allowed
	 * @param {boolean} allowEmpty whether holes are allowed
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {NodeOf<"ArrayExpression">["elements"]} the expressions
	 */
	parseExprList(close, allowTrailingComma, allowEmpty, refDestructuringErrors) {
		const elements = [];
		let first = true;
		while (!this.eat(close)) {
			if (first) {
				first = false;
			} else {
				this.expect(tokTypes.comma);
				if (allowTrailingComma && this.afterTrailingComma(close)) break;
			}

			let elt;
			if (allowEmpty && this.type === tokTypes.comma) {
				elt = null;
			} else if (this.type === tokTypes.ellipsis) {
				elt = this.parseSpread(refDestructuringErrors);
				if (
					refDestructuringErrors &&
					this.type === tokTypes.comma &&
					refDestructuringErrors.trailingComma < 0
				) {
					refDestructuringErrors.trailingComma = this.start;
				}
			} else {
				elt = this.parseMaybeAssign(false, refDestructuringErrors);
			}
			elements.push(elt);
		}
		return elements;
	}

	/**
	 * Report a name that the surrounding code may not use as an identifier.
	 * @param {Identifier} ref the identifier node
	 * @returns {void}
	 */
	checkUnreserved(ref) {
		const { start, end, name } = ref;

		if (this.inGenerator && name === "yield") {
			this.raiseRecoverable(
				start,
				"Cannot use 'yield' as identifier inside a generator"
			);
		}
		if (this.inAsync && name === "await") {
			this.raiseRecoverable(
				start,
				"Cannot use 'await' as identifier inside an async function"
			);
		}
		if (!(this.currentThisScope().flags & SCOPE_VAR) && name === "arguments") {
			this.raiseRecoverable(
				start,
				"Cannot use 'arguments' in class field initializer"
			);
		}
		if (this.inClassStaticBlock && (name === "arguments" || name === "await")) {
			this.raise(
				start,
				`Cannot use ${name} in class static initialization block`
			);
		}
		if (this.keywords.test(name)) {
			this.raise(start, `Unexpected keyword '${name}'`);
		}
		if (
			this.options.ecmaVersion < 6 &&
			this.input.slice(start, end).includes("\\")
		) {
			return;
		}
		const re = this.strict ? this.reservedWordsStrict : this.reservedWords;
		if (re.test(name)) {
			if (!this.inAsync && name === "await") {
				this.raiseRecoverable(
					start,
					"Cannot use keyword 'await' outside an async function"
				);
			}
			this.raiseRecoverable(start, `The keyword '${name}' is reserved`);
		}
	}

	/**
	 * @param {boolean=} liberal whether a keyword may be read as a name
	 * @returns {Identifier} the identifier
	 */
	parseIdent(liberal) {
		const node = /** @type {Identifier} */ (this.parseIdentNode());
		this.next(Boolean(liberal));
		this.finishNode(node, "Identifier");
		if (!liberal) {
			this.checkUnreserved(node);
			if (node.name === "await" && !this.awaitIdentPos) {
				this.awaitIdentPos = node.start;
			}
		}
		return node;
	}

	/** @returns {NodeInProgress<"Identifier">} the identifier, before the token is consumed */
	parseIdentNode() {
		const node = this.startNode();
		if (this.type === tokTypes.name) {
			node.name = /** @type {string} */ (this.value);
		} else if (this.type.keyword) {
			node.name = this.type.keyword;

			// `class` and `function` push a context that nothing would pop once the
			// keyword is read as a property name; a preceding dot already skips that.
			if (
				(node.name === "class" || node.name === "function") &&
				(this.lastTokEnd !== this.lastTokStart + 1 ||
					this.input.charCodeAt(this.lastTokStart) !== 46)
			) {
				this.context.pop();
			}
			this.type = tokTypes.name;
		} else {
			this.unexpected();
		}
		return node;
	}

	/** @returns {NodeOf<"PrivateIdentifier">} the private identifier */
	parsePrivateIdent() {
		const node = /** @type {NodeOf<"PrivateIdentifier">} */ (this.startNode());
		if (this.type === tokTypes.privateId) {
			node.name = /** @type {string} */ (this.value);
		} else {
			this.unexpected();
		}
		this.next();
		this.finishNode(node, "PrivateIdentifier");

		if (this.options.checkPrivateFields) {
			if (this.privateNameStack.length === 0) {
				this.raise(
					node.start,
					`Private field '#${node.name}' must be declared in an enclosing class`
				);
			} else {
				this.privateNameStack[this.privateNameStack.length - 1].used.push(node);
			}
		}

		return node;
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"YieldExpression">} the yield expression
	 */
	parseYield(forInit) {
		if (!this.yieldPos) this.yieldPos = this.start;

		const node = this.startNode();
		this.next();
		if (
			this.type === tokTypes.semi ||
			this.canInsertSemicolon() ||
			(this.type !== tokTypes.star && !this.type.startsExpr)
		) {
			node.delegate = false;
			node.argument = null;
		} else {
			node.delegate = this.eat(tokTypes.star);
			node.argument = this.parseMaybeAssign(forInit);
		}
		return this.finishNode(node, "YieldExpression");
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"AwaitExpression">} the await expression
	 */
	parseAwait(forInit) {
		if (!this.awaitPos) this.awaitPos = this.start;

		const node = this.startNode();
		this.next();
		node.argument = /** @type {Expression} */ (
			this.parseMaybeUnary(null, true, false, forInit)
		);
		return this.finishNode(node, "AwaitExpression");
	}

	/**
	 * Move past the current token.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {boolean=} ignoreEscapeSequenceInKeyword whether an escaped keyword is allowed
	 * @returns {void}
	 */
	next(ignoreEscapeSequenceInKeyword) {
		if (
			!ignoreEscapeSequenceInKeyword &&
			this.type.keyword &&
			this.containsEsc
		) {
			this.raiseRecoverable(
				this.start,
				`Escape sequence in keyword ${this.type.keyword}`
			);
		}
		if (this.options.onToken) this.options.onToken(new Token(this));
		if (this.options.onTokenRead) this.options.onTokenRead(this);

		this.lastTokEnd = this.end;
		this.lastTokStart = this.start;
		this.lastTokEndLoc = this.endLoc;
		this.lastTokStartLoc = this.startLoc;
		this.nextToken();
	}

	/** @returns {Token} the token after the current one */
	getToken() {
		this.next();
		return new Token(this);
	}

	/** @returns {Iterator<Token>} the remaining tokens */
	[Symbol.iterator]() {
		return {
			next: () => {
				const token = this.getToken();
				return { done: token.type === tokTypes.eof, value: token };
			}
		};
	}

	/** @returns {void} */
	nextToken() {
		const curContext = this.curContext();
		if (!curContext || !curContext.preserveSpace) this.skipSpace();

		this.start = this.pos;
		if (this.options.locations) {
			this.startLoc = this.curPosition();
		}
		if (this.pos >= this.input.length) {
			this.finishToken(tokTypes.eof);
			return;
		}

		if (curContext.override) {
			curContext.override(this);
			return;
		}
		this.readToken(this.fullCharCodeAtPos());
	}

	/**
	 * @param {number} code the code point at the tokenizer's position
	 * @returns {void}
	 */
	readToken(code) {
		// A `\` starts an identifier too, since a name may be written with escapes.
		if (isIdentifierStart(code, this.options.ecmaVersion >= 6) || code === 92) {
			this.readWord();
			return;
		}

		this.getTokenFromCode(code);
	}

	/**
	 * @param {number} pos where to read
	 * @returns {number} the code point there, joining a surrogate pair
	 */
	fullCharCodeAt(pos) {
		const code = this.input.charCodeAt(pos);
		if (code <= 0xd7ff || code >= 0xdc00) return code;
		const next = this.input.charCodeAt(pos + 1);
		return next <= 0xdbff || next >= 0xe000
			? code
			: (code << 10) + next - 0x35fdc00;
	}

	/** @returns {number} the code point at the tokenizer's position */
	fullCharCodeAtPos() {
		return this.fullCharCodeAt(this.pos);
	}

	/** @returns {void} */
	skipBlockComment() {
		const startLoc = this.options.onComment && this.curPosition();
		const start = this.pos;
		this.pos += 2;
		const end = this.input.indexOf("*/", this.pos);
		if (end === -1) this.raise(this.pos - 2, "Unterminated comment");
		this.pos = end + 2;
		if (this.options.locations) {
			let nextBreak;
			let pos = start;
			while ((nextBreak = nextLineBreak(this.input, pos, this.pos)) > -1) {
				++this.curLine;
				this.lineStart = nextBreak;
				pos = nextBreak;
			}
		}
		if (this.options.onComment) {
			this.options.onComment(
				true,
				this.input.slice(start + 2, end),
				start,
				this.pos,
				startLoc,
				this.curPosition()
			);
		}
	}

	/** @returns {void} */
	skipSpace() {
		loop: while (this.pos < this.input.length) {
			const ch = this.input.charCodeAt(this.pos);
			switch (ch) {
				case 32:
				case 160:
					++this.pos;
					break;
				case 13:
					if (this.input.charCodeAt(this.pos + 1) === 10) ++this.pos;
				// falls through
				case 10:
				case 8232:
				case 8233:
					++this.pos;
					if (this.options.locations) {
						++this.curLine;
						this.lineStart = this.pos;
					}
					break;
				case 47:
					switch (this.input.charCodeAt(this.pos + 1)) {
						case 42:
							this.skipBlockComment();
							break;
						case 47:
							this.skipLineComment(2);
							break;
						default:
							break loop;
					}
					break;
				default:
					if (
						(ch > 8 && ch < 14) ||
						(ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch)))
					) {
						++this.pos;
					} else {
						break loop;
					}
			}
		}
	}

	/**
	 * @param {TokenType} type the token's kind
	 * @param {TokenValue=} value what it carries
	 * @returns {void}
	 */
	finishToken(type, value) {
		this.end = this.pos;
		if (this.options.locations) {
			this.endLoc = this.curPosition();
		}
		const prevType = this.type;
		this.type = type;
		this.value = value;

		this.updateContext(prevType);
	}

	/** @returns {void} */
	readToken_dot() {
		const next = this.input.charCodeAt(this.pos + 1);
		if (next >= 48 && next <= 57) {
			this.readNumber(true);
			return;
		}
		const next2 = this.input.charCodeAt(this.pos + 2);
		if (this.options.ecmaVersion >= 6 && next === 46 && next2 === 46) {
			this.pos += 3;
			this.finishToken(tokTypes.ellipsis);
			return;
		}
		++this.pos;
		this.finishToken(tokTypes.dot);
	}

	/** @returns {void} */
	readToken_slash() {
		const next = this.input.charCodeAt(this.pos + 1);
		if (this.exprAllowed) {
			++this.pos;
			this.readRegexp();
			return;
		}
		if (next === 61) {
			this.finishOp(tokTypes.assign, 2);
			return;
		}
		this.finishOp(tokTypes.slash, 1);
	}

	/**
	 * @param {number} code the operator's first character
	 * @returns {void}
	 */
	readToken_mult_modulo_exp(code) {
		let next = this.input.charCodeAt(this.pos + 1);
		let size = 1;
		let tokenType = code === 42 ? tokTypes.star : tokTypes.modulo;

		if (this.options.ecmaVersion >= 7 && code === 42 && next === 42) {
			++size;
			tokenType = tokTypes.starstar;
			next = this.input.charCodeAt(this.pos + 2);
		}

		if (next === 61) {
			this.finishOp(tokTypes.assign, size + 1);
			return;
		}
		this.finishOp(tokenType, size);
	}

	/**
	 * @param {number} code the operator's first character
	 * @returns {void}
	 */
	readToken_pipe_amp(code) {
		const next = this.input.charCodeAt(this.pos + 1);
		if (next === code) {
			if (this.options.ecmaVersion >= 12) {
				const next2 = this.input.charCodeAt(this.pos + 2);
				if (next2 === 61) {
					this.finishOp(tokTypes.assign, 3);
					return;
				}
			}
			this.finishOp(code === 124 ? tokTypes.logicalOR : tokTypes.logicalAND, 2);
			return;
		}
		if (next === 61) {
			this.finishOp(tokTypes.assign, 2);
			return;
		}
		this.finishOp(code === 124 ? tokTypes.bitwiseOR : tokTypes.bitwiseAND, 1);
	}

	/** @returns {void} */
	readToken_caret() {
		const next = this.input.charCodeAt(this.pos + 1);
		if (next === 61) {
			this.finishOp(tokTypes.assign, 2);
			return;
		}
		this.finishOp(tokTypes.bitwiseXOR, 1);
	}

	/**
	 * @param {number} code the operator's first character
	 * @returns {void}
	 */
	readToken_plus_min(code) {
		const next = this.input.charCodeAt(this.pos + 1);
		if (next === code) {
			if (
				next === 45 &&
				!this.inModule &&
				this.input.charCodeAt(this.pos + 2) === 62 &&
				(this.lastTokEnd === 0 ||
					lineBreak.test(this.input.slice(this.lastTokEnd, this.pos)))
			) {
				// `-->` closes an HTML-style comment, which annex B reads as a line one.
				this.skipLineComment(3);
				this.skipSpace();
				this.nextToken();
				return;
			}
			this.finishOp(tokTypes.incDec, 2);
			return;
		}
		if (next === 61) {
			this.finishOp(tokTypes.assign, 2);
			return;
		}
		this.finishOp(tokTypes.plusMin, 1);
	}

	/**
	 * @param {number} code the operator's first character
	 * @returns {void}
	 */
	readToken_lt_gt(code) {
		const next = this.input.charCodeAt(this.pos + 1);
		let size = 1;
		if (next === code) {
			size = code === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2;
			if (this.input.charCodeAt(this.pos + size) === 61) {
				this.finishOp(tokTypes.assign, size + 1);
				return;
			}
			this.finishOp(tokTypes.bitShift, size);
			return;
		}
		if (
			next === 33 &&
			code === 60 &&
			!this.inModule &&
			this.input.charCodeAt(this.pos + 2) === 45 &&
			this.input.charCodeAt(this.pos + 3) === 45
		) {
			// `<!--` opens an HTML-style comment, which annex B reads as a line one.
			this.skipLineComment(4);
			this.skipSpace();
			this.nextToken();
			return;
		}
		if (next === 61) size = 2;
		this.finishOp(tokTypes.relational, size);
	}

	/**
	 * @param {number} code the operator's first character
	 * @returns {void}
	 */
	readToken_eq_excl(code) {
		const next = this.input.charCodeAt(this.pos + 1);
		if (next === 61) {
			this.finishOp(
				tokTypes.equality,
				this.input.charCodeAt(this.pos + 2) === 61 ? 3 : 2
			);
			return;
		}
		if (code === 61 && next === 62 && this.options.ecmaVersion >= 6) {
			this.pos += 2;
			this.finishToken(tokTypes.arrow);
			return;
		}
		this.finishOp(code === 61 ? tokTypes.eq : tokTypes.prefix, 1);
	}

	/** @returns {void} */
	readToken_question() {
		const { ecmaVersion } = this.options;
		if (ecmaVersion >= 11) {
			const next = this.input.charCodeAt(this.pos + 1);
			if (next === 46) {
				const next2 = this.input.charCodeAt(this.pos + 2);
				if (next2 < 48 || next2 > 57) {
					this.finishOp(tokTypes.questionDot, 2);
					return;
				}
			}
			if (next === 63) {
				if (ecmaVersion >= 12) {
					const next2 = this.input.charCodeAt(this.pos + 2);
					if (next2 === 61) {
						this.finishOp(tokTypes.assign, 3);
						return;
					}
				}
				this.finishOp(tokTypes.coalesce, 2);
				return;
			}
		}
		this.finishOp(tokTypes.question, 1);
	}

	/** @returns {void} */
	readToken_numberSign() {
		const { ecmaVersion } = this.options;
		let code = 35;
		if (ecmaVersion >= 13) {
			++this.pos;
			code = this.fullCharCodeAtPos();
			if (isIdentifierStart(code, true) || code === 92) {
				this.finishToken(tokTypes.privateId, this.readWord1());
				return;
			}
		}

		this.raise(this.pos, `Unexpected character '${codePointToString(code)}'`);
	}

	/**
	 * @param {number} code the code point at the tokenizer's position
	 * @returns {void}
	 */
	getTokenFromCode(code) {
		switch (code) {
			case 46:
				this.readToken_dot();
				return;

			case 40:
				++this.pos;
				this.finishToken(tokTypes.parenL);
				return;
			case 41:
				++this.pos;
				this.finishToken(tokTypes.parenR);
				return;
			case 59:
				++this.pos;
				this.finishToken(tokTypes.semi);
				return;
			case 44:
				++this.pos;
				this.finishToken(tokTypes.comma);
				return;
			case 91:
				++this.pos;
				this.finishToken(tokTypes.bracketL);
				return;
			case 93:
				++this.pos;
				this.finishToken(tokTypes.bracketR);
				return;
			case 123:
				++this.pos;
				this.finishToken(tokTypes.braceL);
				return;
			case 125:
				++this.pos;
				this.finishToken(tokTypes.braceR);
				return;
			case 58:
				++this.pos;
				this.finishToken(tokTypes.colon);
				return;

			case 96:
				if (this.options.ecmaVersion < 6) break;
				++this.pos;
				this.finishToken(tokTypes.backQuote);
				return;

			case 48: {
				const next = this.input.charCodeAt(this.pos + 1);
				if (next === 120 || next === 88) {
					this.readRadixNumber(16);
					return;
				}
				if (this.options.ecmaVersion >= 6) {
					if (next === 111 || next === 79) {
						this.readRadixNumber(8);
						return;
					}
					if (next === 98 || next === 66) {
						this.readRadixNumber(2);
						return;
					}
				}
				this.readNumber(false);
				return;
			}

			case 49:
			case 50:
			case 51:
			case 52:
			case 53:
			case 54:
			case 55:
			case 56:
			case 57:
				this.readNumber(false);
				return;

			case 34:
			case 39:
				this.readString(code);
				return;

			case 47:
				this.readToken_slash();
				return;

			case 37:
			case 42:
				this.readToken_mult_modulo_exp(code);
				return;

			case 124:
			case 38:
				this.readToken_pipe_amp(code);
				return;

			case 94:
				this.readToken_caret();
				return;

			case 43:
			case 45:
				this.readToken_plus_min(code);
				return;

			case 60:
			case 62:
				this.readToken_lt_gt(code);
				return;

			case 61:
			case 33:
				this.readToken_eq_excl(code);
				return;

			case 63:
				this.readToken_question();
				return;

			case 126:
				this.finishOp(tokTypes.prefix, 1);
				return;

			case 35:
				this.readToken_numberSign();
				return;
		}

		this.raise(this.pos, `Unexpected character '${codePointToString(code)}'`);
	}

	/**
	 * @param {TokenType} type the token's kind
	 * @param {number} size how many characters it spans
	 * @returns {void}
	 */
	finishOp(type, size) {
		const str = this.input.slice(this.pos, this.pos + size);
		this.pos += size;
		this.finishToken(type, str);
	}

	/**
	 * Check a regexp literal's flags.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/regexp.js
	 * @param {import("./regexp").RegExpValidationState} state the validation state
	 * @returns {void}
	 */
	validateRegExpFlags(state) {
		loadRegExpValidator().RegExpValidator.prototype.validateRegExpFlags.call(
			this,
			state
		);
	}

	/**
	 * Check a regexp literal's pattern, re-reading it once a group name shows
	 * that the named-capture goal symbol was the right one.
	 * @param {import("./regexp").RegExpValidationState} state the validation state
	 * @returns {void}
	 */
	validateRegExpPattern(state) {
		loadRegExpValidator().RegExpValidator.prototype.validateRegExpPattern.call(
			this,
			state
		);
	}

	/** @returns {void} */
	readRegexp() {
		let escaped;
		let inClass;
		const start = this.pos;
		for (;;) {
			if (this.pos >= this.input.length) {
				this.raise(start, "Unterminated regular expression");
			}
			const ch = this.input.charAt(this.pos);
			if (lineBreak.test(ch)) {
				this.raise(start, "Unterminated regular expression");
			}
			if (escaped) {
				escaped = false;
			} else {
				if (ch === "[") inClass = true;
				else if (ch === "]" && inClass) inClass = false;
				else if (ch === "/" && !inClass) break;
				escaped = ch === "\\";
			}
			++this.pos;
		}
		const pattern = this.input.slice(start, this.pos);
		++this.pos;
		const flagsStart = this.pos;
		const flags = this.readWord1();
		if (this.containsEsc) this.unexpected(flagsStart);

		const state =
			this.regexpState ||
			(this.regexpState = new (loadRegExpValidator().RegExpValidationState)(
				this
			));
		state.reset(start, pattern, flags);
		this.validateRegExpFlags(state);
		this.validateRegExpPattern(state);

		// ESTree asks for a null value where the host cannot build the pattern.
		let value = null;
		try {
			value = new RegExp(pattern, flags);
		} catch (_err) {
			// Left null, as ESTree requires.
		}

		this.finishToken(tokTypes.regexp, { pattern, flags, value });
	}

	/**
	 * Read digits in the given radix, or `null` where none were there or the
	 * count did not match what an escape asked for.
	 * @param {number} radix the base to read in
	 * @param {number=} len exactly how many digits to read
	 * @param {boolean=} maybeLegacyOctalNumericLiteral whether a leading zero makes it octal
	 * @returns {number | null} the value
	 */
	readInt(radix, len, maybeLegacyOctalNumericLiteral) {
		// Separators are a numeric-literal feature, so an escape sequence, which
		// states its own length, never allows them.
		const allowSeparators = this.options.ecmaVersion >= 12 && len === undefined;
		const isLegacyOctalNumericLiteral =
			maybeLegacyOctalNumericLiteral && this.input.charCodeAt(this.pos) === 48;

		const start = this.pos;
		let total = 0;
		let lastCode = 0;
		for (
			let i = 0, e = len === null || len === undefined ? Infinity : len;
			i < e;
			++i, ++this.pos
		) {
			const code = this.input.charCodeAt(this.pos);
			let val;

			if (allowSeparators && code === 95) {
				if (isLegacyOctalNumericLiteral) {
					this.raiseRecoverable(
						this.pos,
						"Numeric separator is not allowed in legacy octal numeric literals"
					);
				}
				if (lastCode === 95) {
					this.raiseRecoverable(
						this.pos,
						"Numeric separator must be exactly one underscore"
					);
				}
				if (i === 0) {
					this.raiseRecoverable(
						this.pos,
						"Numeric separator is not allowed at the first of digits"
					);
				}
				lastCode = code;
				continue;
			}

			if (code >= 97) val = code - 97 + 10;
			else if (code >= 65) val = code - 65 + 10;
			else if (code >= 48 && code <= 57) val = code - 48;
			else val = Infinity;
			if (val >= radix) break;
			lastCode = code;
			total = total * radix + val;
		}

		if (allowSeparators && lastCode === 95) {
			this.raiseRecoverable(
				this.pos - 1,
				"Numeric separator is not allowed at the last of digits"
			);
		}
		if (
			this.pos === start ||
			(len !== null && len !== undefined && this.pos - start !== len)
		) {
			return null;
		}

		return total;
	}

	/**
	 * @param {number} radix the base the prefix names
	 * @returns {void}
	 */
	readRadixNumber(radix) {
		const start = this.pos;
		this.pos += 2;
		/** @type {number | bigint | null} */
		let val = this.readInt(radix);
		if (val === null || val === undefined) {
			this.raise(this.start + 2, `Expected number in radix ${radix}`);
		}
		if (
			this.options.ecmaVersion >= 11 &&
			this.input.charCodeAt(this.pos) === 110
		) {
			val = stringToBigInt(this.input.slice(start, this.pos));
			++this.pos;
		} else if (isIdentifierStart(this.fullCharCodeAtPos(), true)) {
			this.raise(this.pos, "Identifier directly after number");
		}
		this.finishToken(tokTypes.num, val);
	}

	/**
	 * @param {boolean} startsWithDot whether the literal opened with `.`
	 * @returns {void}
	 */
	readNumber(startsWithDot) {
		const start = this.pos;
		if (!startsWithDot && this.readInt(10, undefined, true) === null) {
			this.raise(start, "Invalid number");
		}
		let octal = this.pos - start >= 2 && this.input.charCodeAt(start) === 48;
		if (octal && this.strict) this.raise(start, "Invalid number");
		let next = this.input.charCodeAt(this.pos);
		if (
			!octal &&
			!startsWithDot &&
			this.options.ecmaVersion >= 11 &&
			next === 110
		) {
			const bigVal = stringToBigInt(this.input.slice(start, this.pos));
			++this.pos;
			if (isIdentifierStart(this.fullCharCodeAtPos(), true)) {
				this.raise(this.pos, "Identifier directly after number");
			}
			this.finishToken(tokTypes.num, bigVal);
			return;
		}
		if (octal && /[89]/.test(this.input.slice(start, this.pos))) octal = false;
		if (next === 46 && !octal) {
			++this.pos;
			this.readInt(10);
			next = this.input.charCodeAt(this.pos);
		}
		if ((next === 69 || next === 101) && !octal) {
			next = this.input.charCodeAt(++this.pos);
			if (next === 43 || next === 45) ++this.pos;
			if (this.readInt(10) === null) this.raise(start, "Invalid number");
		}
		if (isIdentifierStart(this.fullCharCodeAtPos(), true)) {
			this.raise(this.pos, "Identifier directly after number");
		}

		const val = stringToNumber(this.input.slice(start, this.pos), octal);
		this.finishToken(tokTypes.num, val);
	}

	/** @returns {number} the code point an escape names */
	readCodePoint() {
		const ch = this.input.charCodeAt(this.pos);
		let code;

		if (ch === 123) {
			if (this.options.ecmaVersion < 6) this.unexpected();
			const codePos = ++this.pos;
			code = this.readHexChar(this.input.indexOf("}", this.pos) - this.pos);
			++this.pos;
			if (code > 0x10ffff) {
				this.invalidStringToken(codePos, "Code point out of bounds");
			}
		} else {
			code = this.readHexChar(4);
		}
		return code;
	}

	/**
	 * @param {number} quote which quote opened the string
	 * @returns {void}
	 */
	readString(quote) {
		let out = "";
		let chunkStart = ++this.pos;
		for (;;) {
			if (this.pos >= this.input.length) {
				this.raise(this.start, "Unterminated string constant");
			}
			const ch = this.input.charCodeAt(this.pos);
			if (ch === quote) break;
			if (ch === 92) {
				out += this.input.slice(chunkStart, this.pos);
				out += this.readEscapedChar(false);
				chunkStart = this.pos;
			} else if (ch === 0x2028 || ch === 0x2029) {
				if (this.options.ecmaVersion < 10) {
					this.raise(this.start, "Unterminated string constant");
				}
				++this.pos;
				if (this.options.locations) {
					this.curLine++;
					this.lineStart = this.pos;
				}
			} else {
				if (isNewLine(ch)) {
					this.raise(this.start, "Unterminated string constant");
				}
				++this.pos;
			}
		}
		out += this.input.slice(chunkStart, this.pos++);
		this.finishToken(tokTypes.string, out);
	}

	/** @returns {void} */
	tryReadTemplateToken() {
		this.inTemplateElement = true;
		try {
			this.readTmplToken();
		} catch (err) {
			if (err === INVALID_TEMPLATE_ESCAPE_ERROR) {
				this.readInvalidTemplateToken();
			} else {
				throw err;
			}
		}

		this.inTemplateElement = false;
	}

	/**
	 * Report a bad escape, unless it sits in a tagged template, where the raw
	 * text is still well-formed and only the cooked value is lost.
	 * @param {number} position where the escape is
	 * @param {string} message what is wrong
	 * @returns {void}
	 */
	invalidStringToken(position, message) {
		if (this.inTemplateElement && this.options.ecmaVersion >= 9) {
			throw INVALID_TEMPLATE_ESCAPE_ERROR;
		}
		this.raise(position, message);
	}

	/** @returns {void} */
	readTmplToken() {
		let out = "";
		let chunkStart = this.pos;
		for (;;) {
			if (this.pos >= this.input.length) {
				this.raise(this.start, "Unterminated template");
			}
			const ch = this.input.charCodeAt(this.pos);
			if (
				ch === 96 ||
				(ch === 36 && this.input.charCodeAt(this.pos + 1) === 123)
			) {
				if (
					this.pos === this.start &&
					(this.type === tokTypes.template ||
						this.type === tokTypes.invalidTemplate)
				) {
					if (ch === 36) {
						this.pos += 2;
						this.finishToken(tokTypes.dollarBraceL);
						return;
					}
					++this.pos;
					this.finishToken(tokTypes.backQuote);
					return;
				}
				out += this.input.slice(chunkStart, this.pos);
				this.finishToken(tokTypes.template, out);
				return;
			}
			if (ch === 92) {
				out += this.input.slice(chunkStart, this.pos);
				out += this.readEscapedChar(true);
				chunkStart = this.pos;
			} else if (isNewLine(ch)) {
				out += this.input.slice(chunkStart, this.pos);
				++this.pos;
				switch (ch) {
					case 13:
						if (this.input.charCodeAt(this.pos) === 10) ++this.pos;
					// falls through
					case 10:
						out += "\n";
						break;
					default:
						out += String.fromCharCode(ch);
						break;
				}
				if (this.options.locations) {
					++this.curLine;
					this.lineStart = this.pos;
				}
				chunkStart = this.pos;
			} else {
				++this.pos;
			}
		}
	}

	/** @returns {void} */
	readInvalidTemplateToken() {
		for (; this.pos < this.input.length; this.pos++) {
			switch (this.input[this.pos]) {
				case "\\":
					++this.pos;
					break;

				case "$":
					if (this.input[this.pos + 1] !== "{") break;
				// falls through
				case "`":
					this.finishToken(
						tokTypes.invalidTemplate,
						this.input.slice(this.start, this.pos)
					);
					return;

				case "\r":
					if (this.input[this.pos + 1] === "\n") ++this.pos;
				// falls through
				case "\n":
				case "\u2028":
				case "\u2029":
					++this.curLine;
					this.lineStart = this.pos + 1;
					break;
			}
		}
		this.raise(this.start, "Unterminated template");
	}

	/**
	 * @param {boolean} inTemplate whether the escape sits in a template
	 * @returns {string} what the escape stands for
	 */
	readEscapedChar(inTemplate) {
		let ch = this.input.charCodeAt(++this.pos);
		++this.pos;
		switch (ch) {
			case 110:
				return "\n";
			case 114:
				return "\r";
			case 120:
				return String.fromCharCode(this.readHexChar(2));
			case 117:
				return codePointToString(this.readCodePoint());
			case 116:
				return "\t";
			case 98:
				return "\b";
			case 118:
				return "\u000B";
			case 102:
				return "\f";
			case 13:
				if (this.input.charCodeAt(this.pos) === 10) ++this.pos;
			// falls through
			case 10:
				if (this.options.locations) {
					this.lineStart = this.pos;
					++this.curLine;
				}
				return "";
			case 56:
			case 57:
				if (this.strict) {
					this.invalidStringToken(this.pos - 1, "Invalid escape sequence");
				}
				if (inTemplate) {
					this.invalidStringToken(
						this.pos - 1,
						"Invalid escape sequence in template string"
					);
				}
			// falls through
			default:
				if (ch >= 48 && ch <= 55) {
					let octalStr = /** @type {RegExpMatchArray} */ (
						this.input.slice(this.pos - 1, this.pos + 2).match(/^[0-7]+/)
					)[0];
					let octal = Number.parseInt(octalStr, 8);
					if (octal > 255) {
						octalStr = octalStr.slice(0, -1);
						octal = Number.parseInt(octalStr, 8);
					}
					this.pos += octalStr.length - 1;
					ch = this.input.charCodeAt(this.pos);
					if (
						(octalStr !== "0" || ch === 56 || ch === 57) &&
						(this.strict || inTemplate)
					) {
						this.invalidStringToken(
							this.pos - 1 - octalStr.length,
							inTemplate
								? "Octal literal in template string"
								: "Octal literal in strict mode"
						);
					}
					return String.fromCharCode(octal);
				}
				if (isNewLine(ch)) {
					// A line terminator after a backslash is dropped, in both strings
					// and templates.
					if (this.options.locations) {
						this.lineStart = this.pos;
						++this.curLine;
					}
					return "";
				}
				return String.fromCharCode(ch);
		}
	}

	/**
	 * @param {number} len how many hex digits the escape has
	 * @returns {number} the value they name
	 */
	readHexChar(len) {
		const codePos = this.pos;
		const n = this.readInt(16, len);
		if (n === null) {
			this.invalidStringToken(codePos, "Bad character escape sequence");
		}
		return /** @type {number} */ (n);
	}

	/**
	 * Read an identifier's text, recording in `containsEsc` whether any of it
	 * was written as an escape — which stops it reading as a keyword.
	 * @returns {string} the identifier
	 */
	readWord1() {
		this.containsEsc = false;
		let word = "";
		let first = true;
		let chunkStart = this.pos;
		const astral = this.options.ecmaVersion >= 6;
		while (this.pos < this.input.length) {
			const ch = this.fullCharCodeAtPos();
			if (isIdentifierChar(ch, astral)) {
				this.pos += ch <= 0xffff ? 1 : 2;
			} else if (ch === 92) {
				this.containsEsc = true;
				word += this.input.slice(chunkStart, this.pos);
				const escStart = this.pos;
				if (this.input.charCodeAt(++this.pos) !== 117) {
					this.invalidStringToken(
						this.pos,
						"Expecting Unicode escape sequence \\uXXXX"
					);
				}
				++this.pos;
				const esc = this.readCodePoint();
				if (!(first ? isIdentifierStart : isIdentifierChar)(esc, astral)) {
					this.invalidStringToken(escStart, "Invalid Unicode escape");
				}
				word += codePointToString(esc);
				chunkStart = this.pos;
			} else {
				break;
			}
			first = false;
		}
		return word + this.input.slice(chunkStart, this.pos);
	}

	/** @returns {void} */
	readWord() {
		const word = this.readWord1();
		let type = tokTypes.name;
		if (this.keywords.test(word)) type = keywordTypes[word];
		this.finishToken(type, word);
	}
}

/**
 * @param {Expression | NodeOf<"PrivateIdentifier"> | ParenthesizedExpression} node the operand of `delete`
 * @returns {boolean} whether it names a variable rather than a property
 */
const isLocalVariableAccess = (node) =>
	node.type === "Identifier" ||
	(node.type === "ParenthesizedExpression" &&
		isLocalVariableAccess(node.expression));

/**
 * @param {Expression | NodeOf<"PrivateIdentifier"> | ParenthesizedExpression} node the operand of `delete`
 * @returns {boolean} whether it reads a private field
 */
const isPrivateFieldAccess = (node) =>
	(node.type === "MemberExpression" &&
		node.property.type === "PrivateIdentifier") ||
	(node.type === "ChainExpression" && isPrivateFieldAccess(node.expression)) ||
	(node.type === "ParenthesizedExpression" &&
		isPrivateFieldAccess(node.expression));

/**
 * @param {string} str the literal's text
 * @param {boolean} isLegacyOctalNumericLiteral whether a leading zero makes it octal
 * @returns {number} its value
 */
const stringToNumber = (str, isLegacyOctalNumericLiteral) => {
	if (isLegacyOctalNumericLiteral) return Number.parseInt(str, 8);

	// `parseFloat` stops at the first separator, so they come out first.
	return Number.parseFloat(str.replace(/_/g, ""));
};

// Thrown to unwind out of a template element whose escape is bad, so the raw
// text can be re-read without cooking it.

// Token-specific context updates. A closing brace or paren pops what its
// opener pushed, and a `{` is only a block where `braceIsBlock` says so.
/**
 * @this {SyntaxParser}
 * @returns {void}
 */
function closeBraceOrParenContext() {
	if (this.context.length === 1) {
		this.exprAllowed = true;
		return;
	}
	let out = this.context.pop();
	if (out === tokContexts.b_stat && this.curContext().token === "function") {
		out = this.context.pop();
	}
	this.exprAllowed = !(/** @type {TokContext} */ (out).isExpr);
}

/**
 * @this {SyntaxParser}
 * @param {TokenType} prevType the token before this one
 * @returns {void}
 */
function braceLContext(prevType) {
	this.context.push(
		this.braceIsBlock(prevType) ? tokContexts.b_stat : tokContexts.b_expr
	);
	this.exprAllowed = true;
}

/**
 * @this {SyntaxParser}
 * @returns {void}
 */
function dollarBraceLContext() {
	this.context.push(tokContexts.b_tmpl);
	this.exprAllowed = true;
}

/**
 * @this {SyntaxParser}
 * @param {TokenType} prevType the token before this one
 * @returns {void}
 */
function parenLContext(prevType) {
	const statementParens =
		prevType === tokTypes._if ||
		prevType === tokTypes._for ||
		prevType === tokTypes._with ||
		prevType === tokTypes._while;
	this.context.push(statementParens ? tokContexts.p_stat : tokContexts.p_expr);
	this.exprAllowed = true;
}

/**
 * `++`/`--` leaves `exprAllowed` as it stands, since it reads as either a
 * prefix or a postfix operator.
 * @returns {void}
 */
function incDecContext() {}

/**
 * @this {SyntaxParser}
 * @param {TokenType} prevType the token before this one
 * @returns {void}
 */
function functionOrClassContext(prevType) {
	if (
		prevType.beforeExpr &&
		prevType !== tokTypes._else &&
		!(prevType === tokTypes.semi && this.curContext() !== tokContexts.p_stat) &&
		!(
			prevType === tokTypes._return &&
			lineBreak.test(this.input.slice(this.lastTokEnd, this.start))
		) &&
		!(
			(prevType === tokTypes.colon || prevType === tokTypes.braceL) &&
			this.curContext() === tokContexts.b_stat
		)
	) {
		this.context.push(tokContexts.f_expr);
	} else {
		this.context.push(tokContexts.f_stat);
	}
	this.exprAllowed = false;
}

/**
 * @this {SyntaxParser}
 * @returns {void}
 */
function colonContext() {
	if (this.curContext().token === "function") this.context.pop();
	this.exprAllowed = true;
}

/**
 * @this {SyntaxParser}
 * @returns {void}
 */
function backQuoteContext() {
	if (this.curContext() === tokContexts.q_tmpl) this.context.pop();
	else this.context.push(tokContexts.q_tmpl);
	this.exprAllowed = false;
}

/**
 * @this {SyntaxParser}
 * @param {TokenType} prevType the token before this one
 * @returns {void}
 */
function starContext(prevType) {
	if (prevType === tokTypes._function) {
		const index = this.context.length - 1;
		this.context[index] =
			this.context[index] === tokContexts.f_expr
				? tokContexts.f_expr_gen
				: tokContexts.f_gen;
	}
	this.exprAllowed = true;
}

/**
 * A name allows an expression after it only where it was `of` or a `yield`
 * that the enclosing generator makes an operator.
 * @this {SyntaxParser}
 * @param {TokenType} prevType the token before this one
 * @returns {void}
 */
function nameContext(prevType) {
	let allowed = false;
	if (
		this.options.ecmaVersion >= 6 &&
		prevType !== tokTypes.dot &&
		((this.value === "of" && !this.exprAllowed) ||
			(this.value === "yield" && this.inGeneratorContext()))
	) {
		allowed = true;
	}
	this.exprAllowed = allowed;
}

tokTypes.parenR.updateContext = closeBraceOrParenContext;
tokTypes.braceR.updateContext = closeBraceOrParenContext;
tokTypes.braceL.updateContext = braceLContext;
tokTypes.dollarBraceL.updateContext = dollarBraceLContext;
tokTypes.parenL.updateContext = parenLContext;
tokTypes.incDec.updateContext = incDecContext;
tokTypes._function.updateContext = functionOrClassContext;
tokTypes._class.updateContext = functionOrClassContext;
tokTypes.colon.updateContext = colonContext;
tokTypes.backQuote.updateContext = backQuoteContext;
tokTypes.star.updateContext = starContext;
tokTypes.name.updateContext = nameContext;

// cspell:ignore yuku binop prec Prec stanceof iget iset sget sset

/** @type {typeof SyntaxParser} */
const BaseParser = SyntaxParser;

/**
 * @internal
 * @typedef {(NodeOf<"VariableDeclaration"> | NodeOf<"FunctionDeclaration"> | NodeOf<"ClassDeclaration">)[]} HoistedDeclarations the declarations one scope hoists, in source order
 */
/**
 * @internal
 * @typedef {(ModuleDeclaration | Statement | undefined)[]} ModuleDeclarations the program's import and re-export declarations in source order, each followed by the statement written before it
 */
/** @typedef {number | number[]} BlockDeclarations which statements of one list declare into its block scope: a mask of their indices, or the indices themselves once one passes `MAX_MASKED_INDEX` */
/** @typedef {{ token: string, isExpr: boolean, preserveSpace?: boolean, generator?: boolean, override?: (parser: ParserInstance) => void }} TokContextShim the TokContext fields the owned tokenizer reads */

// the token contexts used by the inlined finishToken context updates
const CTX_B_STAT = /** @type {TokContextShim} */ (tokContexts.b_stat);
const CTX_B_EXPR = /** @type {TokContextShim} */ (tokContexts.b_expr);
const CTX_P_STAT = /** @type {TokContextShim} */ (tokContexts.p_stat);
const CTX_P_EXPR = /** @type {TokContextShim} */ (tokContexts.p_expr);
const CTX_F_STAT = /** @type {TokContextShim} */ (tokContexts.f_stat);
const CTX_F_EXPR = /** @type {TokContextShim} */ (tokContexts.f_expr);

/**
 * acorn's `stringToBigInt`, minus the pre-BigInt fallback (every supported
 * Node has `BigInt`).
 * @param {string} str numeric literal text (may contain `_` separators)
 * @returns {bigint} bigint value
 */
const stringToBigInt = (str) => BigInt(str.replace(/_/g, ""));

/**
 * @internal
 * @typedef {UnfinishedNode & { key: NodeOf<"Property">["key"] }} KeyedNode a property or class element whose key `parsePropertyName` has read
 */
/**
 * @internal
 * @typedef {{ pattern: string, flags: string, value: RegExp | null }} RegExpTokenValue what a regular expression token carries
 */
/**
 * @internal
 * @typedef {string | number | bigint | RegExpTokenValue | null | undefined} TokenValue what a token carries: its name, string, number or regexp, or nothing
 */
/**
 * @internal
 * @typedef {ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier} AnyImportSpecifier
 */
/** @typedef {TokenType & { beforeExpr: boolean, isAssign?: boolean, prefix?: boolean, postfix?: boolean, startsExpr?: boolean, isLoop?: boolean, binop: number | null, updateContext: ((prevType: TokenType) => void) | null }} TokenTypeInternal the internal TokenType fields the owned tokenizer reads */
/** @typedef {import("estree").SourceLocation} SourceLocation */
/** @typedef {[number, number]} Range */
/** @typedef {"defer" | "source"} ImportPhase */
/** @typedef {import("estree").Comment & { start: number, end: number }} CollectedComment comment as JavascriptParser exposes it */
/** @typedef {typeof SyntaxParser} ParserClass the parser class itself, as a plugin receives and returns it */
/** @typedef {(BaseParser: ParserClass) => ParserClass} ParserPlugin a plugin: receives the parser class, returns an extended one */
/** @typedef {CollectedComment[] | ((isBlock: boolean, text: string, start: number, end: number) => void)} CommentSink where comments are collected, as an array or a callback */
/** @typedef {Omit<Options, "onComment"> & { onComment?: CommentSink, lazyNodes?: boolean, lazyComments?: CollectedComment[], importPhases?: boolean, moduleFallback?: boolean }} ParserOptions the parser's options, carrying webpack's own comment type plus its lazy-node and import-phase switches */

// Symbol-keyed so they stay out of for-in, Object.keys and JSON.stringify over
// AST nodes.
const kSource = Symbol("source");
const kRange = Symbol("range");
const kText = Symbol("text");
const kTextStart = Symbol("text start");

// Marks import attributes parsed from the legacy `assert {...}` syntax.
const LEGACY_ASSERT_ATTRIBUTES = Symbol("assert");

// acorn's parseFunction statement bit flags
const FUNC_STATEMENT = 1;
const FUNC_HANGING_STATEMENT = 2;
const FUNC_NULLABLE_ID = 4;

// ASCII identifier-continuation chars ($ 0-9 A-Z _ a-z); css/html-style
// Uint8Array table so the tokenizer fast path is one load per char
const IDENT_CHAR = new Uint8Array(128);
IDENT_CHAR[36] = 1;
IDENT_CHAR[95] = 1;
for (let i = 48; i <= 57; i++) IDENT_CHAR[i] = 1;
for (let i = 65; i <= 90; i++) IDENT_CHAR[i] = 1;
for (let i = 97; i <= 122; i++) IDENT_CHAR[i] = 1;

// ASCII identifier-start chars (IDENT_CHAR minus 0-9), for token dispatch in
// the owned `nextToken` loop.
const IDENT_START = new Uint8Array(128);
IDENT_START[36] = 1;
IDENT_START[95] = 1;
for (let i = 65; i <= 90; i++) IDENT_START[i] = 1;
for (let i = 97; i <= 122; i++) IDENT_START[i] = 1;

// Single-char punctuators acorn reads as just `++pos; finishToken(type)`, with
// no value and no operator state machine. Dispatching them from `nextToken`'s
// char table skips a call and a switch for the commonest tokens in JS.
const SIMPLE_PUNCT = Array.from({ length: 128 });
SIMPLE_PUNCT[40] = tokTypes.parenL;
SIMPLE_PUNCT[41] = tokTypes.parenR;
SIMPLE_PUNCT[59] = tokTypes.semi;
SIMPLE_PUNCT[44] = tokTypes.comma;
SIMPLE_PUNCT[91] = tokTypes.bracketL;
SIMPLE_PUNCT[93] = tokTypes.bracketR;
SIMPLE_PUNCT[123] = tokTypes.braceL;
SIMPLE_PUNCT[125] = tokTypes.braceR;
SIMPLE_PUNCT[58] = tokTypes.colon;

// Characters starting only a token that cannot continue an expression, so an
// atom directly followed by one is the whole expression. `}` is included: it
// only ever closes a block, an object or a template substitution.
const EXPRESSION_END_CHAR = new Uint8Array(128);
EXPRESSION_END_CHAR[41] = 1; // )
EXPRESSION_END_CHAR[44] = 1; // ,
EXPRESSION_END_CHAR[58] = 1; // :
EXPRESSION_END_CHAR[59] = 1; // ;
EXPRESSION_END_CHAR[93] = 1; // ]
EXPRESSION_END_CHAR[125] = 1; // }

// Char classification for the owned `nextToken` (yuku's ws_class): one table
// load steers the skip loop, the token dispatch and `readWord`'s scan. Both
// loops leave on one compare, which is what the ordering below is for.

// an identifier continues with these two, so `readWord` scans while `cls <= 1`
const CLS_DIGIT = 0;
const CLS_IDENT = 1;
const CLS_PUNCT = 2;
const CLS_DOT = 3;
const CLS_EQ = 4;
const CLS_OTHER = 5;
const CLS_UNICODE = 6;

// no token opens with these, so the skip loop stops while `cls < CLS_SPACE`
const CLS_SPACE = 7;
const CLS_NEWLINE = 8;
const CLS_SLASH = 9;
// Covers the whole `charCodeAt` range, so the scan loops need no `code > 127`
// branch: non-ASCII classifies as CLS_UNICODE, which sorts below CLS_SPACE and
// so exits on the same compare, then delegates to acorn's unicode paths.
const CHAR_CLASS = new Uint8Array(0x10000).fill(CLS_UNICODE, 128);
CHAR_CLASS.fill(CLS_OTHER, 0, 128);
for (let i = 0; i < 128; i++) {
	if (IDENT_START[i] === 1) CHAR_CLASS[i] = CLS_IDENT;
	// IDENT_CHAR minus IDENT_START is exactly 0-9
	else if (IDENT_CHAR[i] === 1) CHAR_CLASS[i] = CLS_DIGIT;
	else if (SIMPLE_PUNCT[i] !== undefined) CHAR_CLASS[i] = CLS_PUNCT;
}
CHAR_CLASS[46] = CLS_DOT;
CHAR_CLASS[61] = CLS_EQ;
// WHY: a `\` opens an identifier written with a `\u` escape, which only the
// cold readers handle — the same answer CLS_UNICODE already carries, so it
// shares that class rather than adding one the two scan loops must test for.
// `_skipSpaceCold` consumes nothing here and `readToken` sends 92 to `readWord`.
CHAR_CLASS[92] = CLS_UNICODE;
CHAR_CLASS[32] = CLS_SPACE;
CHAR_CLASS[9] = CLS_SPACE;
CHAR_CLASS[11] = CLS_SPACE;
CHAR_CLASS[12] = CLS_SPACE;
CHAR_CLASS[10] = CLS_NEWLINE;
CHAR_CLASS[13] = CLS_NEWLINE;
CHAR_CLASS[47] = CLS_SLASH;

/**
 * Drop-in replacement for acorn's `Node` that materializes `loc` and `range`
 * on first access instead of allocating them during parsing. Most nodes never
 * get either read, which saves three objects and an array per node.
 */
class LazyLocNode {
	/**
	 * @param {number} pos start offset
	 */
	constructor(pos) {
		this.type = "";
		this.start = pos;
		this.end = 0;
	}

	/**
	 * Memoized in a symbol slot — a plain store is far cheaper than making the
	 * property own via defineProperty, and the slot stays invisible to for-in,
	 * Object.keys and JSON.stringify. No `loc` is served at all — locations
	 * are derived from offsets via `JavascriptParser#getLocation`.
	 * @returns {Range} source range
	 */
	get range() {
		const cached = this[kRange];
		if (cached !== undefined) return cached;
		/** @type {Range} */
		const range = [this.start, this.end];
		if (this.end > 0) this[kRange] = range;
		return range;
	}

	/**
	 * @param {Range} value source range
	 */
	set range(value) {
		this[kRange] = value;
	}
}

/**
 * Single-shape `Identifier`, the most common node: all fields are assigned in
 * one constructor, so every instance is born on its final hidden class instead
 * of transitioning through acorn's start-empty-then-mutate construction.
 */
class IdentifierNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {string} name identifier name
	 */
	constructor(start, end, name) {
		/** @type {"Identifier"} */
		this.type = "Identifier";
		this.start = start;
		this.end = end;
		this.name = name;
	}
}

/**
 * Single-shape `Literal`; `bigint` and `regex` stay post-construction
 * additions since both are rare.
 */
class LiteralNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {string | number | boolean | null | RegExp | bigint} value literal value
	 * @param {string} raw literal source text
	 */
	constructor(start, end, value, raw) {
		/** @type {"Literal"} */
		this.type = "Literal";
		this.start = start;
		this.end = end;
		this.value = value;
		this.raw = raw;
	}
}

/**
 * Single-shape `MemberExpression`. `optional` is a real field on every
 * instance since webpack always parses with `ecmaVersion >= 11`.
 */
class MemberExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"MemberExpression">["object"]} object object expression
	 * @param {NodeOf<"MemberExpression">["property"]} property property node
	 * @param {boolean} computed whether the access is computed (`a[b]`)
	 * @param {boolean} optional whether the access is optional (`a?.b`)
	 */
	constructor(start, end, object, property, computed, optional) {
		/** @type {"MemberExpression"} */
		this.type = "MemberExpression";
		this.start = start;
		this.end = end;
		this.object = object;
		this.property = property;
		this.computed = computed;
		this.optional = optional;
	}
}

/**
 * Single-shape `CallExpression`; `optional` as in `MemberExpressionNode`.
 */
class CallExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"CallExpression">["callee"]} callee callee expression
	 * @param {CallArguments} args call arguments
	 * @param {boolean} optional whether the call is optional (`a?.()`)
	 */
	constructor(start, end, callee, args, optional) {
		/** @type {"CallExpression"} */
		this.type = "CallExpression";
		this.start = start;
		this.end = end;
		this.callee = callee;
		this.arguments = args;
		this.optional = optional;
	}
}

/**
 * Single-shape `ThisExpression`.
 */
class ThisNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 */
	constructor(start, end) {
		/** @type {"ThisExpression"} */
		this.type = "ThisExpression";
		this.start = start;
		this.end = end;
	}
}

/**
 * Single-shape `BinaryExpression`/`LogicalExpression` — identical field sets,
 * so both node types share one hidden class.
 * @template {"BinaryExpression" | "LogicalExpression"} T
 */
class BinaryNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {T} type node type
	 * @param {NodeOf<T>["left"]} left left operand
	 * @param {NodeOf<T>["operator"]} operator operator text
	 * @param {NodeOf<T>["right"]} right right operand
	 */
	constructor(start, end, type, left, operator, right) {
		this.type = type;
		this.start = start;
		this.end = end;
		this.left = left;
		this.operator = operator;
		this.right = right;
	}
}

/**
 * Single-shape `AssignmentExpression`.
 */
class AssignmentNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"AssignmentExpression">["operator"]} operator assignment operator text
	 * @param {NodeOf<"AssignmentExpression">["left"]} left assignment target
	 * @param {NodeOf<"AssignmentExpression">["right"]} right assigned value
	 */
	constructor(start, end, operator, left, right) {
		/** @type {"AssignmentExpression"} */
		this.type = "AssignmentExpression";
		this.start = start;
		this.end = end;
		this.operator = operator;
		this.left = left;
		this.right = right;
	}
}

/**
 * Single-shape `UnaryExpression`/`UpdateExpression` — identical field sets,
 * so both node types share one hidden class.
 * @template {"UnaryExpression" | "UpdateExpression"} T
 */
class UnaryNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {T} type node type
	 * @param {NodeOf<T>["operator"]} operator operator text
	 * @param {NodeOf<T>["prefix"]} prefix whether the operator is prefixed
	 * @param {NodeOf<T>["argument"]} argument operand
	 */
	constructor(start, end, type, operator, prefix, argument) {
		this.type = type;
		this.start = start;
		this.end = end;
		this.operator = operator;
		this.prefix = prefix;
		this.argument = argument;
	}
}

// WHY: a sparse symbol slot, not a field: the list is webpack's own record of
// the scope, and a named key would land in every consumer's view of the AST —
// `JSON.stringify` included, where it duplicates each declaration's subtree.
const HOISTED_DECLARATIONS = Symbol("hoistedDeclarations");

// The slot of a scope that declared nothing. It has to differ from the empty
// slot of a node some other parser built, which has no record to read.
/** @type {HoistedDeclarations} */
const NO_HOISTED_DECLARATIONS = [];

// The program's own import and re-export declarations, read before anything
// else — same sparse-slot reasoning as the hoisted list above.
const MODULE_DECLARATIONS = Symbol("moduleDeclarations");

/** @type {ModuleDeclarations} */
const NO_MODULE_DECLARATIONS = [];

// Which statements of one list declare into its own block scope, read before
// any of it is walked — same sparse-slot reasoning as the two lists above. It
// holds their indices, so `undefined` alone means no record was kept.
const BLOCK_DECLARATIONS = Symbol("blockDeclarations");

// WHY: the indices live in one number rather than an array because 59.4% of the
// lists that declare anything declare exactly once, and a 4-element array costs
// ~200 bytes of V8 overhead — 2.33 MB of an 84 MB AST measured over the
// `js-parser-unit` sources. Only an index past this one needs the array.
const MAX_MASKED_INDEX = 30;

/**
 * Turn a mask that can no longer hold every index into the list it falls back
 * to, holding what the mask held plus the index that overflowed it.
 * @param {number} mask the indices recorded so far
 * @param {number} index the index that does not fit
 * @returns {number[]} every index, in source order
 */
const overflowFrom = (mask, index) => {
	/** @type {number[]} */
	const indices = [];
	for (let bits = mask; bits !== 0; bits &= bits - 1) {
		indices.push(31 - Math.clz32(bits & -bits));
	}
	indices.push(index);
	return indices;
};

/**
 * Whether a statement declares anything into the block scope holding it, which
 * is what the consumer reads the list for.
 * @param {EstreeStatement} statement the finished statement, as `JavascriptParser` also holds it
 * @returns {boolean} whether it belongs in the list
 */
const declaresIntoBlock = (statement) => {
	const type = statement.type;
	if (
		type === "VariableDeclaration" ||
		type === "FunctionDeclaration" ||
		type === "ClassDeclaration" ||
		type === "ExportNamedDeclaration" ||
		type === "ExportDefaultDeclaration"
	) {
		return true;
	}
	// WHY: only an assignment to an object pattern is read here, for the
	// properties it names. Keeping every expression statement would carry the
	// most common statement there is: 10390 of three-long's 22758, against 1.
	if (type !== "ExpressionStatement") return false;
	const expression = statement.expression;
	return (
		expression.type === "AssignmentExpression" &&
		expression.left.type === "ObjectPattern"
	);
};

/**
 * Single-shape `VariableDeclaration` (statement position; `for` heads keep the
 * generic node since their caller finishes them).
 */
class VariableDeclarationNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"VariableDeclaration">["declarations"]} declarations declarators
	 * @param {NodeOf<"VariableDeclaration">["kind"]} kind declaration kind (`var`/`let`/`const`/`using`)
	 */
	constructor(start, end, declarations, kind) {
		/** @type {"VariableDeclaration"} */
		this.type = "VariableDeclaration";
		this.start = start;
		this.end = end;
		this.declarations = declarations;
		this.kind = kind;
	}
}

/**
 * Single-shape `VariableDeclarator`.
 */
class VariableDeclaratorNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"VariableDeclarator">["id"]} id binding target
	 * @param {NodeOf<"VariableDeclarator">["init"]} init initializer
	 */
	constructor(start, end, id, init) {
		/** @type {"VariableDeclarator"} */
		this.type = "VariableDeclarator";
		this.start = start;
		this.end = end;
		this.id = id;
		this.init = init;
	}
}

/**
 * Single-shape `ExpressionStatement`.
 */
class ExpressionStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ExpressionStatement">["expression"]} expression the statement's expression
	 */
	constructor(start, end, expression) {
		/** @type {"ExpressionStatement"} */
		this.type = "ExpressionStatement";
		this.start = start;
		this.end = end;
		this.expression = expression;
	}
}

/**
 * Single-shape `BlockStatement`.
 */
class BlockStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"BlockStatement">["body"]} body statements
	 */
	constructor(start, end, body) {
		/** @type {"BlockStatement"} */
		this.type = "BlockStatement";
		this.start = start;
		this.end = end;
		this.body = body;
		// filled for a function body only: what hoists to the scope it opens
		/** @type {HoistedDeclarations | undefined} */
		this[HOISTED_DECLARATIONS] = undefined;
		/** @type {BlockDeclarations | undefined} */
		this[BLOCK_DECLARATIONS] = undefined;
	}
}

/**
 * Single-shape `IfStatement`.
 */
class IfStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"IfStatement">["test"]} test condition
	 * @param {NodeOf<"IfStatement">["consequent"]} consequent then-branch
	 * @param {NodeOf<"IfStatement">["alternate"]} alternate else-branch
	 */
	constructor(start, end, test, consequent, alternate) {
		/** @type {"IfStatement"} */
		this.type = "IfStatement";
		this.start = start;
		this.end = end;
		this.test = test;
		this.consequent = consequent;
		this.alternate = alternate;
	}
}

/**
 * Single-shape `ReturnStatement`.
 */
class ReturnStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ReturnStatement">["argument"]} argument returned expression
	 */
	constructor(start, end, argument) {
		/** @type {"ReturnStatement"} */
		this.type = "ReturnStatement";
		this.start = start;
		this.end = end;
		this.argument = argument;
	}
}

/**
 * Single-shape `ConditionalExpression`.
 */
class ConditionalExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ConditionalExpression">["test"]} test condition
	 * @param {NodeOf<"ConditionalExpression">["consequent"]} consequent then-value
	 * @param {NodeOf<"ConditionalExpression">["alternate"]} alternate else-value
	 */
	constructor(start, end, test, consequent, alternate) {
		/** @type {"ConditionalExpression"} */
		this.type = "ConditionalExpression";
		this.start = start;
		this.end = end;
		this.test = test;
		this.consequent = consequent;
		this.alternate = alternate;
	}
}

/**
 * Single-shape `NewExpression`.
 */
class NewExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"NewExpression">["callee"]} callee constructed expression
	 * @param {CallArguments} args constructor arguments
	 */
	constructor(start, end, callee, args) {
		/** @type {"NewExpression"} */
		this.type = "NewExpression";
		this.start = start;
		this.end = end;
		this.callee = callee;
		this.arguments = args;
	}
}

/**
 * Single-shape `ArrayExpression`.
 */
class ArrayExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ArrayExpression">["elements"]} elements array elements (`null` for holes)
	 */
	constructor(start, end, elements) {
		/** @type {"ArrayExpression"} */
		this.type = "ArrayExpression";
		this.start = start;
		this.end = end;
		this.elements = elements;
	}
}

/**
 * Single-shape `TemplateLiteral`.
 */
class TemplateLiteralNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"TemplateLiteral">["expressions"]} expressions substitution expressions
	 * @param {NodeOf<"TemplateLiteral">["quasis"]} quasis template chunks
	 */
	constructor(start, end, expressions, quasis) {
		/** @type {"TemplateLiteral"} */
		this.type = "TemplateLiteral";
		this.start = start;
		this.end = end;
		this.expressions = expressions;
		this.quasis = quasis;
	}
}

/**
 * Single-shape `TemplateElement`.
 */
class TemplateElementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {{ raw: string, cooked: string | null }} value chunk text
	 * @param {boolean} tail whether this is the closing chunk
	 */
	constructor(start, end, value, tail) {
		/** @type {"TemplateElement"} */
		this.type = "TemplateElement";
		this.start = start;
		this.end = end;
		this.value = value;
		this.tail = tail;
	}
}

/**
 * Single-shape `ObjectExpression`/`ObjectPattern` — identical field sets, so
 * both node types share one hidden class.
 * @template {"ObjectExpression" | "ObjectPattern"} T
 */
class ObjectNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {T} type node type
	 * @param {NodeOf<T>["properties"]} properties properties
	 */
	constructor(start, end, type, properties) {
		this.type = type;
		this.start = start;
		this.end = end;
		this.properties = properties;
	}
}

/**
 * Pre-shaped `Property`: acorn fills property nodes through shared
 * subroutines (`parsePropertyName`/`parsePropertyValue`), so instead of
 * rebuilding that flow the fields are all declared up-front and acorn's
 * writes land in existing slots — one hidden class, no transitions (yuku's
 * decoder emits `Property` with this fixed shape). Every non-throwing acorn
 * branch assigns `computed`, `key`, `value` and `kind`; `finishNode` sets
 * `type` and `end`.
 */
class PropertyNode {
	/**
	 * @param {number} start start offset
	 */
	constructor(start) {
		this.type = "";
		this.start = start;
		this.end = 0;
		this.method = false;
		this.shorthand = false;
		this.computed = false;
		/** @type {Node | null} */
		this.key = null;
		/** @type {Node | null} */
		this.value = null;
		this.kind = "";
	}
}

/**
 * Single-shape `ForStatement`.
 */
class ForStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ForStatement">["init"]} init init statement or expression
	 * @param {NodeOf<"ForStatement">["test"]} test loop condition
	 * @param {NodeOf<"ForStatement">["update"]} update update expression
	 * @param {NodeOf<"ForStatement">["body"]} body loop body
	 */
	constructor(start, end, init, test, update, body) {
		/** @type {"ForStatement"} */
		this.type = "ForStatement";
		this.start = start;
		this.end = end;
		this.init = init;
		this.test = test;
		this.update = update;
		this.body = body;
	}
}

/**
 * Single-shape `ForInStatement` (no `await` slot, matching acorn).
 */
class ForInStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ForInStatement">["left"]} left loop target
	 * @param {NodeOf<"ForInStatement">["right"]} right iterated expression
	 * @param {NodeOf<"ForInStatement">["body"]} body loop body
	 */
	constructor(start, end, left, right, body) {
		/** @type {"ForInStatement"} */
		this.type = "ForInStatement";
		this.start = start;
		this.end = end;
		this.left = left;
		this.right = right;
		this.body = body;
	}
}

/**
 * Single-shape `ForOfStatement`; `await` leads, matching acorn's write order.
 */
class ForOfStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {boolean} isAwait whether this is `for await`
	 * @param {NodeOf<"ForOfStatement">["left"]} left loop target
	 * @param {NodeOf<"ForOfStatement">["right"]} right iterated expression
	 * @param {NodeOf<"ForOfStatement">["body"]} body loop body
	 */
	constructor(start, end, isAwait, left, right, body) {
		/** @type {"ForOfStatement"} */
		this.type = "ForOfStatement";
		this.start = start;
		this.end = end;
		this.await = isAwait;
		this.left = left;
		this.right = right;
		this.body = body;
	}
}

/**
 * Single-shape `WhileStatement`.
 */
class WhileStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"WhileStatement">["test"]} test loop condition
	 * @param {NodeOf<"WhileStatement">["body"]} body loop body
	 */
	constructor(start, end, test, body) {
		/** @type {"WhileStatement"} */
		this.type = "WhileStatement";
		this.start = start;
		this.end = end;
		this.test = test;
		this.body = body;
	}
}

/**
 * Single-shape `SwitchStatement`.
 */
class SwitchStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"SwitchStatement">["discriminant"]} discriminant switched expression
	 * @param {NodeOf<"SwitchStatement">["cases"]} cases case clauses
	 */
	constructor(start, end, discriminant, cases) {
		/** @type {"SwitchStatement"} */
		this.type = "SwitchStatement";
		this.start = start;
		this.end = end;
		this.discriminant = discriminant;
		this.cases = cases;
	}
}

/**
 * Pre-shaped `SwitchCase`: filled in acorn's write order (`consequent` before
 * `test`), finished via `finishNode` like acorn's.
 */
class SwitchCaseNode {
	/**
	 * @param {number} start start offset
	 */
	constructor(start) {
		this.type = "";
		this.start = start;
		this.end = 0;
		/** @type {Node[] | null} */
		this.consequent = null;
		/** @type {Expression | null} */
		this.test = null;
		/** @type {BlockDeclarations | undefined} */
		this[BLOCK_DECLARATIONS] = undefined;
	}
}

/**
 * Single-shape `ThrowStatement`.
 */
class ThrowStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ThrowStatement">["argument"]} argument thrown expression
	 */
	constructor(start, end, argument) {
		/** @type {"ThrowStatement"} */
		this.type = "ThrowStatement";
		this.start = start;
		this.end = end;
		this.argument = argument;
	}
}

/**
 * Single-shape `BreakStatement`/`ContinueStatement` — identical field sets, so
 * both node types share one hidden class.
 * @template {"BreakStatement" | "ContinueStatement"} T
 */
class BreakContinueNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {T} type node type
	 * @param {NodeOf<T>["label"]} label target label
	 */
	constructor(start, end, type, label) {
		this.type = type;
		this.start = start;
		this.end = end;
		this.label = label;
	}
}

/**
 * Single-shape `TryStatement`.
 */
class TryStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"TryStatement">["block"]} block try block
	 * @param {NodeOf<"TryStatement">["handler"]} handler catch clause
	 * @param {NodeOf<"TryStatement">["finalizer"]} finalizer finally block
	 */
	constructor(start, end, block, handler, finalizer) {
		/** @type {"TryStatement"} */
		this.type = "TryStatement";
		this.start = start;
		this.end = end;
		this.block = block;
		this.handler = handler;
		this.finalizer = finalizer;
	}
}

/**
 * Single-shape `CatchClause`.
 */
class CatchClauseNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"CatchClause">["param"]} param catch parameter
	 * @param {NodeOf<"CatchClause">["body"]} body catch block
	 */
	constructor(start, end, param, body) {
		/** @type {"CatchClause"} */
		this.type = "CatchClause";
		this.start = start;
		this.end = end;
		this.param = param;
		this.body = body;
	}
}

/**
 * Pre-shaped `FunctionDeclaration`/`FunctionExpression`/
 * `ArrowFunctionExpression`: acorn fills function nodes through shared
 * subroutines (`parseFunctionParams`/`parseFunctionBody`), so like
 * `PropertyNode` the fields are all declared up-front — in acorn's exact write
 * order (`initFunction` assigns `expression` before `generator`) so JSON key
 * order is unchanged — and acorn's writes land in existing slots: one hidden
 * class for all three function node types, no transitions.
 */
class FunctionNode {
	/**
	 * @param {number} start start offset
	 */
	constructor(start) {
		this.type = "";
		this.start = start;
		this.end = 0;
		/** @type {Identifier | null} */
		this.id = null;
		this.expression = false;
		this.generator = false;
		this.async = false;
		/** @type {Node[] | null} */
		this.params = null;
		/** @type {Node | null} */
		this.body = null;
	}
}

/**
 * Pre-shaped `ClassDeclaration`/`ClassExpression`: acorn fills class nodes
 * through shared subroutines (`parseClassId`/`parseClassSuper`), so like
 * `FunctionNode` the fields are declared up-front in acorn's write order —
 * one hidden class for both node types, no transitions.
 */
class ClassNode {
	/**
	 * @param {number} start start offset
	 */
	constructor(start) {
		this.type = "";
		this.start = start;
		this.end = 0;
		/** @type {Identifier | null} */
		this.id = null;
		/** @type {Expression | null} */
		this.superClass = null;
		/** @type {Node | null} */
		this.body = null;
	}
}

/**
 * Single-shape `ClassBody`.
 */
class ClassBodyNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ClassBody">["body"]} body class elements
	 */
	constructor(start, end, body) {
		/** @type {"ClassBody"} */
		this.type = "ClassBody";
		this.start = start;
		this.end = end;
		this.body = body;
	}
}

/**
 * Pre-shaped `MethodDefinition`, filled in acorn's write order (`static`,
 * `computed`, `key`, `kind`, `value`) and finished via `finishNode`. Also the
 * scratch an ambiguous class element parses its name into before the
 * method/field split (see `parseClassElement`) — it carries the superset of
 * both shapes' fields, so no write transitions it.
 */
class MethodDefinitionNode {
	/**
	 * @param {number} start start offset
	 */
	constructor(start) {
		this.type = "";
		this.start = start;
		this.end = 0;
		this.static = false;
		this.computed = false;
		/** @type {Node | null} */
		this.key = null;
		this.kind = "";
		/** @type {Node | null} */
		this.value = null;
	}
}

/**
 * Pre-shaped `PropertyDefinition` — `kind`-less, matching acorn's key set, so
 * it cannot share `MethodDefinitionNode`'s shape: `value` lands in
 * `parseClassField`, `finishNode` sets `type` and `end`.
 */
class PropertyDefinitionNode {
	/**
	 * @param {number} start start offset
	 * @param {boolean} isStatic whether the field is static
	 * @param {boolean} computed whether the key is computed
	 * @param {NodeOf<"PropertyDefinition">["key"]} key field key
	 */
	constructor(start, isStatic, computed, key) {
		this.type = "";
		this.start = start;
		this.end = 0;
		this.static = isStatic;
		this.computed = computed;
		this.key = key;
		/** @type {Node | null} */
		this.value = null;
	}
}

/**
 * Pre-shaped `StaticBlock` (no `static` slot — acorn never writes one):
 * `parseClassStaticBlock` fills `body`, `finishNode` sets `type` and `end`.
 */
class StaticBlockNode {
	/**
	 * @param {number} start start offset
	 */
	constructor(start) {
		this.type = "";
		this.start = start;
		this.end = 0;
		/** @type {Node[] | null} */
		this.body = null;
		/** @type {BlockDeclarations | undefined} */
		this[BLOCK_DECLARATIONS] = undefined;
	}
}

/**
 * Single-shape `ImportDeclaration`; `phase` (`import defer/source`) stays a
 * post-construction addition since phase imports are rare.
 */
class ImportDeclarationNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ImportDeclaration">["specifiers"]} specifiers import specifiers
	 * @param {NodeOf<"ImportDeclaration">["source"]} source module source string literal
	 * @param {NodeOf<"ImportDeclaration">["attributes"]} attributes import attributes (`with { ... }`)
	 */
	constructor(start, end, specifiers, source, attributes) {
		/** @type {"ImportDeclaration"} */
		this.type = "ImportDeclaration";
		this.start = start;
		this.end = end;
		this.specifiers = specifiers;
		this.source = source;
		this.attributes = attributes;
	}
}

/**
 * Single-shape `ImportSpecifier`.
 */
class ImportSpecifierNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ImportSpecifier">["imported"]} imported imported name (identifier or string literal)
	 * @param {NodeOf<"ImportSpecifier">["local"]} local local binding identifier
	 */
	constructor(start, end, imported, local) {
		/** @type {"ImportSpecifier"} */
		this.type = "ImportSpecifier";
		this.start = start;
		this.end = end;
		this.imported = imported;
		this.local = local;
	}
}

/**
 * Single-shape `ImportDefaultSpecifier`/`ImportNamespaceSpecifier` —
 * identical field sets, so both node types share one hidden class.
 * @template {"ImportDefaultSpecifier" | "ImportNamespaceSpecifier"} T
 */
class ImportSimpleSpecifierNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {T} type node type
	 * @param {NodeOf<T>["local"]} local local binding identifier
	 */
	constructor(start, end, type, local) {
		this.type = type;
		this.start = start;
		this.end = end;
		this.local = local;
	}
}

/**
 * Single-shape `ImportAttribute`.
 */
class ImportAttributeNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ImportAttribute">["key"]} key attribute key (identifier or string literal)
	 * @param {NodeOf<"ImportAttribute">["value"]} value attribute value string literal
	 */
	constructor(start, end, key, value) {
		/** @type {"ImportAttribute"} */
		this.type = "ImportAttribute";
		this.start = start;
		this.end = end;
		this.key = key;
		this.value = value;
	}
}

/**
 * Single-shape `ExportNamedDeclaration`.
 */
class ExportNamedDeclarationNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ExportNamedDeclaration">["declaration"]} declaration exported declaration statement
	 * @param {NodeOf<"ExportNamedDeclaration">["specifiers"]} specifiers export specifiers
	 * @param {NodeOf<"ExportNamedDeclaration">["source"]} source re-export source string literal
	 * @param {NodeOf<"ExportNamedDeclaration">["attributes"]} attributes import attributes (`with { ... }`)
	 */
	constructor(start, end, declaration, specifiers, source, attributes) {
		/** @type {"ExportNamedDeclaration"} */
		this.type = "ExportNamedDeclaration";
		this.start = start;
		this.end = end;
		this.declaration = declaration;
		this.specifiers = specifiers;
		this.source = source;
		this.attributes = attributes;
	}
}

/**
 * Single-shape `ExportDefaultDeclaration`.
 */
class ExportDefaultDeclarationNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ExportDefaultDeclaration">["declaration"]} declaration default-exported declaration or expression
	 */
	constructor(start, end, declaration) {
		/** @type {"ExportDefaultDeclaration"} */
		this.type = "ExportDefaultDeclaration";
		this.start = start;
		this.end = end;
		this.declaration = declaration;
	}
}

/**
 * Single-shape `ExportAllDeclaration`.
 */
class ExportAllDeclarationNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ExportAllDeclaration">["exported"]} exported `export * as name` export name
	 * @param {NodeOf<"ExportAllDeclaration">["source"]} source re-export source string literal
	 * @param {NodeOf<"ExportAllDeclaration">["attributes"]} attributes import attributes (`with { ... }`)
	 */
	constructor(start, end, exported, source, attributes) {
		/** @type {"ExportAllDeclaration"} */
		this.type = "ExportAllDeclaration";
		this.start = start;
		this.end = end;
		this.exported = exported;
		this.source = source;
		this.attributes = attributes;
	}
}

/**
 * Single-shape `ExportSpecifier`.
 */
class ExportSpecifierNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"ExportSpecifier">["local"]} local local name (identifier or string literal)
	 * @param {NodeOf<"ExportSpecifier">["exported"]} exported exported name (identifier or string literal)
	 */
	constructor(start, end, local, exported) {
		/** @type {"ExportSpecifier"} */
		this.type = "ExportSpecifier";
		this.start = start;
		this.end = end;
		this.local = local;
		this.exported = exported;
	}
}

/**
 * Single-shape `AssignmentPattern` (also the shape retyped
 * `AssignmentExpression`s collapse to, minus their deleted `operator`).
 */
class AssignmentPatternNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"AssignmentPattern">["left"]} left binding target
	 * @param {NodeOf<"AssignmentPattern">["right"]} right default value
	 */
	constructor(start, end, left, right) {
		/** @type {"AssignmentPattern"} */
		this.type = "AssignmentPattern";
		this.start = start;
		this.end = end;
		this.left = left;
		this.right = right;
	}
}

/**
 * Single-shape `DoWhileStatement`.
 */
class DoWhileStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"DoWhileStatement">["body"]} body loop body
	 * @param {NodeOf<"DoWhileStatement">["test"]} test loop condition
	 */
	constructor(start, end, body, test) {
		/** @type {"DoWhileStatement"} */
		this.type = "DoWhileStatement";
		this.start = start;
		this.end = end;
		this.body = body;
		this.test = test;
	}
}

/**
 * Single-shape `WithStatement`.
 */
class WithStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"WithStatement">["object"]} object scope object expression
	 * @param {NodeOf<"WithStatement">["body"]} body statement body
	 */
	constructor(start, end, object, body) {
		/** @type {"WithStatement"} */
		this.type = "WithStatement";
		this.start = start;
		this.end = end;
		this.object = object;
		this.body = body;
	}
}

/**
 * Single-shape `LabeledStatement`; `body` leads `label`, matching acorn's
 * write order.
 */
class LabeledStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"LabeledStatement">["body"]} body labeled statement
	 * @param {Identifier} label label identifier
	 */
	constructor(start, end, body, label) {
		/** @type {"LabeledStatement"} */
		this.type = "LabeledStatement";
		this.start = start;
		this.end = end;
		this.body = body;
		this.label = label;
	}
}

/**
 * Single-shape `EmptyStatement`.
 */
class EmptyStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 */
	constructor(start, end) {
		/** @type {"EmptyStatement"} */
		this.type = "EmptyStatement";
		this.start = start;
		this.end = end;
	}
}

/**
 * Single-shape `DebuggerStatement`.
 */
class DebuggerStatementNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 */
	constructor(start, end) {
		/** @type {"DebuggerStatement"} */
		this.type = "DebuggerStatement";
		this.start = start;
		this.end = end;
	}
}

/**
 * Single-shape `AwaitExpression`.
 */
class AwaitExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"AwaitExpression">["argument"]} argument awaited expression
	 */
	constructor(start, end, argument) {
		/** @type {"AwaitExpression"} */
		this.type = "AwaitExpression";
		this.start = start;
		this.end = end;
		this.argument = argument;
	}
}

/**
 * Single-shape `YieldExpression`; `delegate` leads, matching acorn's write
 * order.
 */
class YieldExpressionNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {boolean} delegate whether this is `yield*`
	 * @param {NodeOf<"YieldExpression">["argument"]} argument yielded expression
	 */
	constructor(start, end, delegate, argument) {
		/** @type {"YieldExpression"} */
		this.type = "YieldExpression";
		this.start = start;
		this.end = end;
		this.delegate = delegate;
		this.argument = argument;
	}
}

/**
 * Single-shape `PrivateIdentifier` (`IdentifierNode`'s fields, distinct
 * type).
 */
class PrivateIdentifierNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {string} name private name without the `#`
	 */
	constructor(start, end, name) {
		/** @type {"PrivateIdentifier"} */
		this.type = "PrivateIdentifier";
		this.start = start;
		this.end = end;
		this.name = name;
	}
}

/**
 * Single-shape `Program`; `sourceType` trails `body`, matching acorn's
 * write order.
 */
class ProgramNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {NodeOf<"Program">["body"]} body top-level statements
	 * @param {NodeOf<"Program">["sourceType"]} sourceType effective source type
	 */
	constructor(start, end, body, sourceType) {
		/** @type {"Program"} */
		this.type = "Program";
		this.start = start;
		this.end = end;
		this.body = body;
		this.sourceType = sourceType;
		/** @type {HoistedDeclarations | undefined} */
		this[HOISTED_DECLARATIONS] = undefined;
		/** @type {ModuleDeclarations | undefined} */
		this[MODULE_DECLARATIONS] = undefined;
		/** @type {BlockDeclarations | undefined} */
		this[BLOCK_DECLARATIONS] = undefined;
	}
}

/**
 * Single-shape `SpreadElement`/`RestElement` — identical field sets, so both
 * node types share one hidden class.
 * @template {"SpreadElement" | "RestElement"} T
 */
class RestSpreadNode {
	/**
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {T} type node type
	 * @param {NodeOf<T>["argument"]} argument spread/rest argument
	 */
	constructor(start, end, type, argument) {
		this.type = type;
		this.start = start;
		this.end = end;
		this.argument = argument;
	}
}

// Mirrors of acorn's module-level `loopLabel`/`switchLabel`.
const LOOP_LABEL = { kind: "loop" };
const SWITCH_LABEL = { kind: "switch" };

// Mirror of acorn's `INVALID_TEMPLATE_ESCAPE_ERROR`: thrown by the owned
// `invalidStringToken`, caught by identity in the owned `tryReadTemplateToken`.
const INVALID_TEMPLATE_ESCAPE_ERROR = {};

/**
 * Mirror of acorn's `Position` (line/column pair with its `offset` helper),
 * served by the owned `raise`/`curPosition`.
 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/locutil.js
 */
class ParserPosition {
	/**
	 * @param {number} line 1-based line
	 * @param {number} col 0-based column
	 */
	constructor(line, col) {
		this.line = line;
		this.column = col;
	}

	/**
	 * @param {number} n column offset to add
	 * @returns {ParserPosition} shifted position
	 */
	offset(n) {
		return new ParserPosition(this.line, this.column + n);
	}
}

// Shared zero-length arguments array for `new X` without parens, mirroring
// acorn's module-level `empty`.
/** @type {Expression[]} */
const EMPTY_NEW_ARGS = [];

// Shared zero-length specifiers array for `import "x"`, mirroring acorn's
// module-level `empty$1`.
/** @type {AnyImportSpecifier[]} */
const EMPTY_IMPORT_SPECIFIERS = [];

// Mirror of acorn's module-level `loneSurrogate` (not exported), for the owned
// `parseModuleExportName`'s string-name check.
const LONE_SURROGATE_REGEXP =
	/(?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/;

/**
 * Character-level mirror of acorn's `keywordRelationalOperator` (`in` /
 * `instanceof`) over a source span, so lookahead probes need no identifier
 * slice.
 * @param {string} input source text
 * @param {number} start span start offset
 * @param {number} end span end offset
 * @returns {boolean} whether the span spells `in` or `instanceof`
 */
const isRelationalKeyword = (input, start, end) => {
	const len = end - start;
	if (len === 2) {
		return (
			input.charCodeAt(start) === 105 && input.charCodeAt(start + 1) === 110
		);
	}
	return len === 10 && input.startsWith("instanceof", start);
};

/**
 * Mirror of acorn's module-level `checkKeyName`.
 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
 * @param {Node & { computed?: boolean, key?: Node | null }} node class element
 * @param {string} name checked name
 * @returns {boolean} whether the element's non-computed key spells `name`
 */
const checkKeyName = (node, name) => {
	const computed = node.computed;
	const key = /** @type {Node & { name?: string, value?: LiteralValue }} */ (
		node.key
	);
	return (
		!computed &&
		((key.type === "Identifier" && key.name === name) ||
			(key.type === "Literal" && key.value === name))
	);
};

/**
 * Mirror of acorn's module-level `isPrivateNameConflicted`.
 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
 * @param {Record<string, string>} privateNameMap declared private names of the enclosing class body
 * @param {Node & { kind?: string, static?: boolean, key?: Node & { name: string } }} element class element with a `PrivateIdentifier` key
 * @returns {boolean} whether the name conflicts with an earlier declaration
 */
const isPrivateNameConflicted = (privateNameMap, element) => {
	const name = /** @type {Node & { name: string }} */ (element.key).name;
	const curr = privateNameMap[name];
	let next = "true";
	if (
		element.type === "MethodDefinition" &&
		(element.kind === "get" || element.kind === "set")
	) {
		next = (element.static ? "s" : "i") + element.kind;
	}
	// `class { get #a(){}; static set #a(_){} }` is also a conflict
	if (
		(curr === "iget" && next === "iset") ||
		(curr === "iset" && next === "iget") ||
		(curr === "sget" && next === "sset") ||
		(curr === "sset" && next === "sget")
	) {
		privateNameMap[name] = "true";
		return false;
	} else if (!curr) {
		privateNameMap[name] = next;
		return false;
	}
	return true;
};

/**
 * Acorn's per-class private-name record: `declared` maps each private name to
 * "true" or an accessor tag (`iget`/`iset`/`sget`/`sset`); `used` collects the
 * `PrivateIdentifier` nodes `parsePrivateIdent` saw, resolved against
 * `declared` when the class body closes (`exitClassBody`).
 * @internal
 * @typedef {{ declared: Record<string, string>, used: (Node & { name: string })[] }} PrivateNameStackEntry
 */

// the dedicated node classes serve `range` exactly like LazyLocNode
for (const NodeClass of [
	IdentifierNode,
	LiteralNode,
	MemberExpressionNode,
	CallExpressionNode,
	ThisNode,
	BinaryNode,
	AssignmentNode,
	UnaryNode,
	VariableDeclarationNode,
	VariableDeclaratorNode,
	ExpressionStatementNode,
	BlockStatementNode,
	IfStatementNode,
	ReturnStatementNode,
	ConditionalExpressionNode,
	NewExpressionNode,
	ArrayExpressionNode,
	TemplateLiteralNode,
	TemplateElementNode,
	ObjectNode,
	PropertyNode,
	RestSpreadNode,
	FunctionNode,
	ProgramNode,
	AwaitExpressionNode,
	YieldExpressionNode,
	PrivateIdentifierNode,
	AssignmentPatternNode,
	DoWhileStatementNode,
	WithStatementNode,
	LabeledStatementNode,
	EmptyStatementNode,
	DebuggerStatementNode,
	ImportDeclarationNode,
	ImportSpecifierNode,
	ImportSimpleSpecifierNode,
	ImportAttributeNode,
	ExportNamedDeclarationNode,
	ExportDefaultDeclarationNode,
	ExportAllDeclarationNode,
	ExportSpecifierNode,
	ClassNode,
	ClassBodyNode,
	MethodDefinitionNode,
	PropertyDefinitionNode,
	StaticBlockNode,
	ForStatementNode,
	ForInStatementNode,
	ForOfStatementNode,
	WhileStatementNode,
	SwitchStatementNode,
	SwitchCaseNode,
	ThrowStatementNode,
	BreakContinueNode,
	TryStatementNode,
	CatchClauseNode
]) {
	for (const key of ["range"]) {
		Object.defineProperty(
			NodeClass.prototype,
			key,
			/** @type {PropertyDescriptor} */
			(Object.getOwnPropertyDescriptor(LazyLocNode.prototype, key))
		);
	}
}

/**
 * Comment collected without slicing its text out of the source: only magic
 * comments and pure annotations ever get their text read, and only binary
 * searches around magic-comment sites read `range`, so the text slice and the
 * range array are both deferred to first access and memoized like `loc`.
 */
class LazyComment {
	/**
	 * @param {boolean} block whether this is a block comment
	 * @param {number} textStart offset right after the comment opener
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {string} source full source text for the lazy `value` slice
	 */
	constructor(block, textStart, start, end, source) {
		/** @type {"Block" | "Line"} */
		this.type = block ? "Block" : "Line";
		this.start = start;
		this.end = end;
		this[kSource] = source;
		this[kTextStart] = textStart;
	}

	/**
	 * Memoized like `LazyLocNode#range`; offsets are final at construction so
	 * the slot is cached unconditionally.
	 * @returns {Range} source range
	 */
	get range() {
		const cached = this[kRange];
		if (cached !== undefined) return cached;
		/** @type {Range} */
		const range = [this.start, this.end];
		return (this[kRange] = range);
	}

	/**
	 * @param {Range} value source range
	 */
	set range(value) {
		this[kRange] = value;
	}

	/**
	 * @returns {string} comment text without the delimiters
	 */
	get value() {
		const cached = this[kText];
		if (cached !== undefined) return cached;
		return (this[kText] = this[kSource].slice(
			this[kTextStart],
			this.type === "Block" ? this.end - 2 : this.end
		));
	}

	/**
	 * @param {string} value comment text
	 */
	set value(value) {
		this[kText] = value;
	}
}

/**
 * The base parser plus `WebpackParser`'s own fields, so the constructor can
 * write them before the instance is one.
 * @typedef {SyntaxParser & {
 * _gapHasNewline: () => boolean,
 * _parseTrivialAtom: (forInit?: boolean | string) => Expression | null,
 * _classFastPath: boolean,
 * _deStack: DestructuringErrorsShim[],
 * _deDepth: number,
 * _ecmaVersion: number,
 * _noLocations: boolean,
 * _validRegexpFlags: string,
 * _propHashFastPath: boolean,
 * _propHashStack: { proto: boolean }[],
 * _propHashDepth: number,
 * _acquireDestructuringErrors: () => DestructuringErrorsShim,
 * _releaseDestructuringErrors: () => void,
 * _arrStack: (Node | null)[][],
 * _arrDepth: number,
 * _acquireScratch: <T = Node>() => T[],
 * _releaseScratch: <T>(scratch: T[], count: number) => T[],
 * _subscriptFastPath: boolean,
 * _readInt: (radix: number, len?: number, maybeLegacyOctal?: boolean) => number | null,
 * _readCodePoint: () => number,
 * _readHexChar: (len: number) => number,
 * _readEscapedChar: (inTemplate: boolean) => string,
 * _readStringCold: (quote: number) => void,
 * _readTmplTokenCold: () => void,
 * _readNumberCold: (startsWithDot: boolean) => void,
 * _readRadixNumber: (radix: number) => void,
 * _readWord1Cold: () => string,
 * _finishWordSlow: (word: string) => void,
 * _readWordIntoCache: (start: number, end: number, wordLen: number, slot: number) => void,
 * _readWordUncacheable: (start: number, end: number) => void,
 * _updateContext: (internal: TokenTypeInternal, prevType: TokenType, value?: TokenValue) => void,
 * _skipSpaceCold: () => void,
 * _getUnknownOrPrivate: (code: number) => void,
 * lineStart: number,
 * _scopePool: ParserScope[],
 * _setPool: Set<string>[],
 * _exportHeadDepth: number,
 * _exitedHoisted: HoistedDeclarations | undefined,
 * _moduleDeclarations: ModuleDeclarations | undefined,
 * _previousTopLevelStatement: Statement | ModuleDeclaration | undefined,
 * _takeSet: () => Set<string>,
 * _labelsStack: { kind?: string | null, name?: string, statementStart?: number }[][],
 * _labelsDepth: number,
 * _wordLookups: WordLookups,
 * _parseImportSpecifiersBase: () => AnyImportSpecifier[],
 * _moduleDeclFastPath: boolean,
 * _tokenFastPath: boolean,
 * _inlineFinish: boolean,
 * _lazy: boolean,
 * _importPhase: ImportPhase | null,
 * _importPhasesEnabled: boolean,
 * _lazyComments: CollectedComment[] | undefined,
 * _newlineBefore: 0 | 1 | 2,
 * _wordKind: number,
 * _fullTokenFastPath: boolean,
 * _stmtFastPath: boolean,
 * _lastArrow: Expression | null,
 * _arrowFastPath: boolean,
 * _funcFastPath: boolean,
 * _funcStmtOwn: boolean,
 * _parseFunctionAt: (start: number, statement: number, allowExpressionBody: boolean, isAsync: boolean, forInit?: boolean | string) => NodeOf<"FunctionDeclaration" | "FunctionExpression">,
 * _scanGap: (pos: number) => number,
 * _scanGapNewline: boolean,
 * _varIdOwn: boolean,
 * _parseVarInto: (declarations: NodeOf<"VariableDeclarator">[], isFor: boolean, kind: VariableKind, allowMissingInitializer?: boolean) => number,
 * _parseVarStatementAt: (start: number, kind: VariableKind, allowMissingInitializer?: boolean) => NodeOf<"VariableDeclaration">,
 * _parseIfStatementAt: (start: number) => NodeOf<"IfStatement">,
 * _parseReturnStatementAt: (start: number) => NodeOf<"ReturnStatement">,
 * _parseExpressionStatementAt: (start: number, expr: Expression) => NodeOf<"ExpressionStatement">,
 * _stmt2FastPath: boolean,
 * _exprFastPath: boolean,
 * _parseForStatementAt: (start: number) => NodeOf<"ForStatement" | "ForInStatement" | "ForOfStatement">,
 * _parseForAt: (start: number, init: NodeOf<"VariableDeclaration"> | Expression | null) => NodeOf<"ForStatement">,
 * _parseForInAt: (start: number, isAwait: boolean, init: NodeOf<"VariableDeclaration"> | Pattern) => NodeOf<"ForInStatement" | "ForOfStatement">,
 * _parseForAfterInitAt: (start: number, init: NodeOf<"VariableDeclaration">, awaitAt: number) => NodeOf<"ForStatement" | "ForInStatement" | "ForOfStatement">,
 * _parseWhileStatementAt: (start: number) => NodeOf<"WhileStatement">,
 * _parseSwitchStatementAt: (start: number) => NodeOf<"SwitchStatement">,
 * _parseThrowStatementAt: (start: number) => NodeOf<"ThrowStatement">,
 * _parseTryStatementAt: (start: number) => NodeOf<"TryStatement">,
 * _parseBreakContinueStatementAt: (start: number, keyword: string) => NodeOf<"BreakStatement" | "ContinueStatement">,
 * _moduleFallback: boolean,
 * _moduleSyntaxSeen: boolean,
 * _tryModuleFallback: () => boolean,
 * }} ParserInternals
 */

// internal methods are absent from acorn's types, so super calls do not
// type-check; call through a typed view of the base prototype instead
const base = BaseParser.prototype;

/**
 * The parser's destructuring-errors record. Owned methods must create records
 * with the same hidden class the rest of the expression parser reads, or every
 * record field access there turns polymorphic.
 * @typedef {{ shorthandAssign: number, trailingComma: number, parenthesizedAssign: number, parenthesizedBind: number, doubleProto: number }} DestructuringErrorsShim
 */

/**
 * @returns {DestructuringErrorsShim} fresh destructuring-errors record
 */
const createDestructuringErrors = () => new DestructuringErrors();

/**
 * Reserved-word classification for `checkUnreserved`'s single lookup:
 * `1` keyword, `2` reserved in sloppy and strict mode, `3` reserved in strict
 * mode only.
 * @typedef {1 | 2 | 3} ReservedKind
 */

/**
 * @typedef {object} WordLookups
 * @property {Map<string, TokenType>} keywords keyword name → token type
 * @property {Map<string, ReservedKind>} reservedKinds identifier name → reserved kind
 * @property {number} reservedMaxLen longest key in `reservedKinds`
 * @property {{ test: (name: string) => boolean }} reservedBindTest strict-mode binding check, a Set-backed stand-in for acorn's `reservedWordsStrictBind` regexp
 * @property {Set<string>} reservedBindSet strict-mode binding-reserved names
 * @property {number} reservedBindMinLen shortest key in `reservedBindSet`
 * @property {number} reservedBindMaxLen longest key in `reservedBindSet`
 * @property {number} id owner tag for the shared `WORD_TYPES` classification memo
 */

// One entry per distinct keyword/reserved-word set; webpack parses with a
// single option set, making this effectively a one-time build shared across
// every parse.
/** @type {Map<string, WordLookups>} */
const wordLookupsCache = new Map();

// Sentinel for the released cache state; `ensureParserCaches` runs in the
// parser constructor, so the hot paths never observe the nulls behind it.
const NO_CACHE = /** @type {never} */ (null);

// Direct-mapped identifier cache for `readWord1`: one slot per hash, verified by
// char compare, overwritten on collision. Shared across parses, since a hit is
// content-checked. Only lengths V8 stores flat, never source-retaining slices.
const WORD_CACHE_MASK = 0xfff;
const WORD_CACHE_MAX_LEN = 12;

// Four entries per slot (word, token type, id of the keyword set that classified
// it, padding), so a probe touches one line; another set's id retypes the word.
// An empty slot holds "", which no word matches, so a probe needs no null check.
/** @type {(string | TokenType | number | null)[]} */
let WORD_CACHE = NO_CACHE;
const WORD_CACHE_STRIDE = 4;
let nextWordLookupsId = 1;

// WHY: a direct-mapped cache for short non-word slices — WORD_CACHE's regime
// applied to literal text. Number raws repeat heavily, a typical large input
// holding some ten times more number literals than distinct raws, and short
// string values and raws repeat too, so serving them from one shared string cuts
// both allocation churn and retained AST strings. Same rules as WORD_CACHE:
// content-checked hits, and only the lengths V8 stores flat, never a slice
// retaining the source.
const SLICE_CACHE_MASK = 0xfff;
/** @type {string[]} */
let SLICE_CACHE = NO_CACHE;
const SLICE_CACHE_MAX_LEN = 12;

/**
 * Allocates the shared tokenizer caches; runs in the parser constructor so
 * they exist exactly while something is parsing.
 * @returns {void}
 */
const ensureParserCaches = () => {
	if (WORD_CACHE !== null) return;
	WORD_CACHE = Array.from(
		{ length: (WORD_CACHE_MASK + 1) * WORD_CACHE_STRIDE },
		(_, i) =>
			i % WORD_CACHE_STRIDE === 0 ? "" : i % WORD_CACHE_STRIDE === 2 ? 0 : null
	);
	SLICE_CACHE = Array.from({ length: SLICE_CACHE_MASK + 1 }, () => "");
};

/**
 * Drops every shared parser cache — the string/type caches above plus the
 * word-lookup, operator, regexp-flag and construction-shape memos — so an
 * idle process retains no parser state. The next parse rebuilds them.
 * @returns {void}
 */
const releaseParserCaches = () => {
	WORD_CACHE = NO_CACHE;
	SLICE_CACHE = NO_CACHE;
	wordLookupsCache.clear();
	OP_CACHE.clear();
	VALID_REGEXP_FLAGS.clear();
	fastConstructionShapes.clear();
};

/**
 * Cached `input.slice(start, end)` for spans of 2-12 chars (callers gate the
 * length; 1-char slices come from V8's single-character table already).
 * @param {string} input source text
 * @param {number} start span start offset
 * @param {number} end span end offset
 * @returns {string} the span's text, shared across equal occurrences
 */
const getShortSlice = (input, start, end) => {
	let hash = 0;
	for (let i = start; i < end; i++) {
		hash = (Math.imul(hash, 33) + input.charCodeAt(i)) | 0;
	}
	const slot = hash & SLICE_CACHE_MASK;
	const cached = SLICE_CACHE[slot];
	if (cached.length === end - start && input.startsWith(cached, start)) {
		return cached;
	}
	return (SLICE_CACHE[slot] = input.slice(start, end));
};

/**
 * Whether the word at `start` is `cached`, whose hash already matched: a
 * char loop beats a builtin call for words this short.
 * @param {string} input source code
 * @param {number} start where the word starts
 * @param {string} cached the cached word
 * @param {number} wordLen the word's length
 * @returns {boolean} true when they are the same word
 */
const wordMatches = (input, start, cached, wordLen) => {
	if (cached.length !== wordLen) return false;
	for (let i = 0; i < wordLen; i++) {
		if (cached.charCodeAt(i) !== input.charCodeAt(start + i)) return false;
	}
	return true;
};

/**
 * The reserved kind `checkUnreserved` acts on for a word, 0 when it has nothing
 * to check: pure in (word, lookups), so it is memoized beside the token type.
 * @param {string} word word
 * @param {WordLookups} lookups the parser's keyword sets
 * @returns {number} the reserved kind, 4 for the names with their own checks
 */
const wordKind = (word, lookups) => {
	const kind = lookups.reservedKinds.get(word);
	if (kind !== undefined) return kind;
	return word === "arguments" || word === "await" || word === "yield" ? 4 : 0;
};

/**
 * Keyword-or-name classification: pure in (word, keywords), so it can be
 * memoized per `WORD_CACHE` slot.
 * @param {string} word word
 * @param {Map<string, TokenType>} keywords keyword name → token type
 * @returns {TokenType} token type for `word`
 */
const classifyWord = (word, keywords) => {
	const len = word.length;
	// every acorn keyword is 2-10 lowercase ASCII chars (`do`…`instanceof`)
	if (len >= 2 && len <= 10) {
		const first = word.charCodeAt(0);
		if (first >= 97 && first <= 122) return keywords.get(word) || tokTypes.name;
	}
	return tokTypes.name;
};

// Multi-char operator strings for `finishOp` (`=>`, `===`, `&&=`, …), keyed by
// their char codes packed 7 bits apart — collision-free for ASCII operators up
// to acorn's maximum of 4 chars (`>>>=`), so the set stays ~40 entries.
/** @type {Map<number, string>} */
const OP_CACHE = new Map();

// The regexp flag whitelist depends only on the ecmaVersion, so build one
// string per version instead of a fresh one in every parser constructor.
/** @type {Map<number, string>} */
const VALID_REGEXP_FLAGS = new Map();

/**
 * @param {number} ecmaVersion normalized acorn ecmaVersion
 * @returns {string} the regexp flags allowed at `ecmaVersion`
 */
const getValidRegexpFlags = (ecmaVersion) => {
	let flags = VALID_REGEXP_FLAGS.get(ecmaVersion);
	if (flags === undefined) {
		flags = `gim${ecmaVersion >= 6 ? "uy" : ""}${ecmaVersion >= 9 ? "s" : ""}${
			ecmaVersion >= 13 ? "d" : ""
		}${ecmaVersion >= 15 ? "v" : ""}`;
		VALID_REGEXP_FLAGS.set(ecmaVersion, flags);
	}
	return flags;
};

// Sticky mirrors of acorn's `skipWhiteSpace` / string-literal / `lineBreak`
// regexes for the owned `strictDirective` (they scan at an offset, no slice).
const STRICT_SKIP_WS = /(?:\s|\/\/.*|\/\*[^]*?\*\/)*/g;
const STRICT_LITERAL = /(?:'((?:\\[^]|[^'\\])*?)'|"((?:\\[^]|[^"\\])*?)")/y;
const STRICT_LINE_BREAK = /\r\n?|\n|\u2028|\u2029/;

/**
 * @param {RegExp} re acorn `wordsRegexp` output (`^(?:a|b|c)$`)
 * @returns {Set<string>} the alternation's words
 */
const wordsRegexpToSet = (re) => {
	const match = /^\^\(\?:(.*)\)\$$/.exec(re.source);
	const body = match ? match[1] : "";
	return new Set(body ? body.split("|") : []);
};

// One-entry identity memo in front of the string-keyed cache. `wordsRegexp`
// interns its regexps, so identity captures the whole word set, and a build
// constructs thousands of parsers from one option set.
/** @type {RegExp | undefined} */
let lastKeywordsRe;
/** @type {RegExp | undefined} */
let lastReservedRe;
/** @type {RegExp | undefined} */
let lastReservedStrictRe;
/** @type {WordLookups | undefined} */
let lastWordLookups;

/**
 * Mirrors acorn's `keywords` / `reservedWords` / `reservedWordsStrict` regexps
 * as Map/Set lookups. Membership is the hot per-word test in `readWord` and
 * `checkUnreserved`, and a hash lookup beats an anchored alternation regexp.
 * @param {ParserInternals} parser parser instance
 * @returns {WordLookups} lookups for this parser's keyword set
 */
const getWordLookups = (parser) => {
	if (
		parser.keywords === lastKeywordsRe &&
		parser.reservedWords === lastReservedRe &&
		parser.reservedWordsStrict === lastReservedStrictRe
	) {
		return /** @type {WordLookups} */ (lastWordLookups);
	}
	// module vs script share a keyword set but differ in reserved words, so the
	// key must cover all three regexps
	const key = `${parser.keywords.source}\n${parser.reservedWords.source}\n${parser.reservedWordsStrict.source}`;
	lastKeywordsRe = parser.keywords;
	lastReservedRe = parser.reservedWords;
	lastReservedStrictRe = parser.reservedWordsStrict;
	const cached = wordLookupsCache.get(key);
	if (cached !== undefined) {
		lastWordLookups = cached;
		return cached;
	}
	/** @type {Map<string, TokenType>} */
	const keywords = new Map();
	// acorn's keyword regexp is a subset of keywordTypes for the ecmaVersion
	for (const name of Object.keys(keywordTypes)) {
		if (parser.keywords.test(name)) keywords.set(name, keywordTypes[name]);
	}
	const reserved = wordsRegexpToSet(parser.reservedWords);
	/** @type {Map<string, ReservedKind>} */
	const reservedKinds = new Map();
	for (const name of reserved) reservedKinds.set(name, 2);
	for (const name of wordsRegexpToSet(parser.reservedWordsStrict)) {
		if (!reserved.has(name)) reservedKinds.set(name, 3);
	}
	// keyword classification wins, matching acorn's keyword-first check
	for (const name of keywords.keys()) reservedKinds.set(name, 1);
	const reservedBind = wordsRegexpToSet(parser.reservedWordsStrictBind);
	// length bounds so the owned checkLValSimple can skip the Set probe for
	// most identifiers
	let reservedBindMinLen = 0x7fffffff;
	let reservedBindMaxLen = 0;
	for (const name of reservedBind) {
		if (name.length < reservedBindMinLen) reservedBindMinLen = name.length;
		if (name.length > reservedBindMaxLen) reservedBindMaxLen = name.length;
	}
	let reservedMaxLen = 0;
	for (const name of reservedKinds.keys()) {
		if (name.length > reservedMaxLen) reservedMaxLen = name.length;
	}
	/** @type {WordLookups} */
	const lookups = {
		keywords,
		reservedKinds,
		reservedMaxLen,
		reservedBindTest: { test: (name) => reservedBind.has(name) },
		reservedBindSet: reservedBind,
		reservedBindMinLen,
		reservedBindMaxLen,
		id: nextWordLookupsId++
	};
	wordLookupsCache.set(key, lookups);
	lastWordLookups = lookups;
	return lookups;
};

/**
 * Per-option-shape data for the fast construction path: everything acorn's
 * constructor derives from options alone.
 * @typedef {object} FastConstructionShape
 * @property {ResolvedOptions} options normalized options template — never handed to an instance, see `cloneNormalizedOptions`
 * @property {RegExp} keywords acorn's interned keywords regexp
 * @property {RegExp} reservedWords acorn's interned reserved-words regexp
 * @property {RegExp} reservedWordsStrict acorn's interned strict reserved-words regexp
 * @property {RegExp} reservedWordsStrictBind acorn's interned strict-bind reserved-words regexp
 * @property {WordLookups} wordLookups word lookups for this keyword set
 * @property {number} ecmaVersion normalized numeric ecmaVersion
 * @property {boolean} inModule whether sourceType is module
 * @property {boolean} baseStrict strictness before the directive probe (module or explicit strict)
 * @property {boolean} allowHashBang normalized allowHashBang
 * @property {string} validRegexpFlags regexp flag whitelist for the ecmaVersion
 */

// One record per option shape passing the fast-construction gate; webpack
// parses with one or two shapes, so this is effectively a one-time build.
/** @type {Map<string, FastConstructionShape>} */
const fastConstructionShapes = new Map();

/**
 * Fresh per-instance copy of a shape's normalized options: `_tryModuleFallback`
 * writes `sourceType`/`allowReturnOutsideFunction` into `this.options`
 * mid-parse, so a shared template would leak one parse's downgrade into every
 * later parse. Sequential stores in `defaultOptions` key order land on the
 * hidden class acorn's `getOptions` builds, keeping `this.options` reads
 * monomorphic across fast- and slow-constructed parsers.
 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/options.js
 * @param {ResolvedOptions} template normalized options template of the shape
 * @returns {ResolvedOptions} per-instance options object
 */
const cloneNormalizedOptions = (template) => {
	const from = template;
	/** @type {Partial<ResolvedOptions>} */
	const options = {};
	options.ecmaVersion = from.ecmaVersion;
	options.sourceType = from.sourceType;
	options.strict = from.strict;
	options.onInsertedSemicolon = from.onInsertedSemicolon;
	options.onTrailingComma = from.onTrailingComma;
	options.allowReserved = from.allowReserved;
	options.allowReturnOutsideFunction = from.allowReturnOutsideFunction;
	options.allowImportExportEverywhere = from.allowImportExportEverywhere;
	options.allowAwaitOutsideFunction = from.allowAwaitOutsideFunction;
	options.allowSuperOutsideMethod = from.allowSuperOutsideMethod;
	options.allowHashBang = from.allowHashBang;
	options.checkPrivateFields = from.checkPrivateFields;
	options.locations = from.locations;
	options.startLocation = from.startLocation;
	options.onToken = from.onToken;
	options.onTokenRead = from.onTokenRead;
	options.onComment = from.onComment;
	options.ranges = from.ranges;
	options.program = from.program;
	options.sourceFile = from.sourceFile;
	options.directSourceFile = from.directSourceFile;
	options.preserveParens = from.preserveParens;
	return /** @type {ResolvedOptions} */ (options);
};

/**
 * Builds a shape via one sacrificial base construction: acorn's `getOptions`
 * and `wordsRegexp` run once here, and the captured regexps are the interned
 * instances acorn's module-level cache would serve every equal construction —
 * so `getWordLookups`' identity memo keeps matching for slow-path parsers too.
 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/state.js
 * @param {Options} options raw options that passed the fast-construction gate
 * @returns {FastConstructionShape} shape record
 */
const buildFastConstructionShape = (options) => {
	// canonicalized copy of the gate-variable options; acorn reads `strict` only
	// as `=== true` and the allow* flags only by truthiness, so booleans match
	/** @type {Options} */
	const raw = {
		ecmaVersion: options.ecmaVersion,
		sourceType: options.sourceType,
		strict: options.strict === true,
		allowReturnOutsideFunction: Boolean(options.allowReturnOutsideFunction),
		allowImportExportEverywhere: Boolean(options.allowImportExportEverywhere),
		allowAwaitOutsideFunction: Boolean(options.allowAwaitOutsideFunction),
		allowSuperOutsideMethod: Boolean(options.allowSuperOutsideMethod)
	};
	// null/undefined must stay absent so getOptions applies its own default
	if (options.allowHashBang !== null && options.allowHashBang !== undefined) {
		raw.allowHashBang = Boolean(options.allowHashBang);
	}
	const ProbeParser =
		/** @type {new (options: Options, input: string) => SyntaxParser} */ (
			BaseParser
		);
	const probe = /** @type {ParserInternals} */ (new ProbeParser(raw, ""));
	const normalized = probe.options;
	const ecmaVersion = normalized.ecmaVersion;
	return {
		options: normalized,
		keywords: probe.keywords,
		reservedWords: probe.reservedWords,
		reservedWordsStrict: probe.reservedWordsStrict,
		reservedWordsStrictBind: probe.reservedWordsStrictBind,
		wordLookups: getWordLookups(probe),
		ecmaVersion,
		inModule: normalized.sourceType === "module",
		baseStrict:
			normalized.sourceType === "module" || normalized.strict === true,
		allowHashBang: normalized.allowHashBang === true,
		validRegexpFlags: getValidRegexpFlags(ecmaVersion)
	};
};

/**
 * Gate for the fast construction path: the exact webpack hot-path option
 * shape, with every option whose normalization is not replicated absent or
 * default. Returns the cached per-shape record, or null for the slow path.
 * @param {Options & { lazyNodes?: boolean }} options raw options
 * @param {string} input source code
 * @param {number | undefined} startPos start position
 * @returns {FastConstructionShape | null} shape record, or null for the slow path
 */
const getFastConstructionShape = (options, input, startPos) => {
	if (
		startPos !== undefined ||
		options.lazyNodes !== true ||
		typeof input !== "string" ||
		options.ecmaVersion === null ||
		options.ecmaVersion === undefined ||
		(options.sourceType !== "module" && options.sourceType !== "script") ||
		(options.allowReserved !== null && options.allowReserved !== undefined) ||
		options.onInsertedSemicolon ||
		options.onTrailingComma ||
		options.locations ||
		options.startLocation ||
		options.onToken ||
		options.onTokenRead ||
		options.onComment ||
		options.ranges ||
		options.program ||
		options.sourceFile ||
		options.directSourceFile ||
		options.preserveParens ||
		// getOptions copies an explicitly-present key even when undefined, so
		// presence with any value but true must take the slow path
		(options.checkPrivateFields !== true && "checkPrivateFields" in options)
	) {
		return null;
	}
	const key = `${options.ecmaVersion}|${options.sourceType}|${
		options.strict === true ? "s" : ""
	}${options.allowReturnOutsideFunction ? "r" : ""}${
		options.allowImportExportEverywhere ? "e" : ""
	}${options.allowAwaitOutsideFunction ? "a" : ""}${
		options.allowSuperOutsideMethod ? "m" : ""
	}${
		options.allowHashBang === null || options.allowHashBang === undefined
			? ""
			: options.allowHashBang
				? "|h"
				: "|n"
	}`;
	let shape = fastConstructionShapes.get(key);
	if (shape === undefined) {
		shape = buildFastConstructionShape(options);
		fastConstructionShapes.set(key, shape);
	}
	return shape;
};

/**
 * webpack's parser: acorn plus lazy `range` (no `loc` at all), Set-based scopes,
 * tokenizer fast paths, import attributes and import phases (with acorn's
 * `!forNew` guard, unlike the former `acorn-import-phases` package).
 */
class WebpackParser extends BaseParser {
	/**
	 * Applies parser plugins, keeping the result typed as this parser.
	 * @param {...ParserPlugin} plugins parser plugins
	 * @returns {typeof WebpackParser} the extended parser class
	 */
	static extend(...plugins) {
		return /** @type {typeof WebpackParser} */ (super.extend(...plugins));
	}

	/**
	 * @param {Options & { lazyNodes?: boolean, lazyComments?: CollectedComment[], importPhases?: boolean, moduleFallback?: boolean }} options options
	 * @param {string} input source code
	 * @param {number=} startPos start position
	 */
	constructor(options, input, startPos) {
		ensureParserCaches();
		// fast construction for the gated hot-path shape, skipping acorn's per-parse
		// getOptions/wordsRegexp work. A derived constructor may return an object
		// without super(), and a plugin's body then runs on it just the same.
		const shape = getFastConstructionShape(options, input, startPos);
		if (shape !== null) {
			// write order below is load-bearing: acorn's constructor fields first, in
			// dist order, then WebpackParser's in slow-constructor order, so fast- and
			// slow-built instances walk one hidden-class transition chain
			const self = /** @type {ParserInternals} */ (
				Object.create(new.target.prototype)
			);
			self.options = cloneNormalizedOptions(shape.options);
			self.sourceFile = null;
			self.keywords = shape.keywords;
			self.reservedWords = shape.reservedWords;
			self.reservedWordsStrict = shape.reservedWordsStrict;
			self.reservedWordsStrictBind = shape.reservedWordsStrictBind;
			self.input = input;
			self.containsEsc = false;
			self.pos = 0;
			self.curLine = 1;
			self.lineStart = 0;
			self.type = tokTypes.eof;
			self.value = null;
			self.end = 0;
			self.start = 0;
			// locations are off, so this yields undefined — still called so a
			// plugin override of curPosition keeps taking effect
			const curPos = self.curPosition();
			self.endLoc = curPos;
			self.startLoc = curPos;
			self.lastTokStartLoc = null;
			self.lastTokEndLoc = null;
			self.lastTokEnd = 0;
			self.lastTokStart = 0;
			self.context = self.initialContext();
			self.exprAllowed = true;
			self.inModule = shape.inModule;
			// acorn's short-circuit order: the directive probe runs only for
			// sloppy scripts, and before the hashbang skip below moves `pos`
			self.strict = shape.baseStrict || self.strictDirective(self.pos);
			self.potentialArrowAt = -1;
			self.potentialArrowInForAwait = false;
			self.awaitIdentPos = 0;
			self.awaitPos = 0;
			self.yieldPos = 0;
			self.labels = [];
			self.undefinedExports = Object.create(null);
			// the hashbang skip waits for `_lazyComments` (it only moves `pos`), so
			// the owned skipLineComment records the comment itself, not via acorn
			const hashbang = shape.allowHashBang && input.startsWith("#!");
			self.scopeStack = [];
			// sourceType is gated to module/script, never commonjs's SCOPE_FUNCTION
			self.enterScope(SCOPE_TOP);
			self.regexpState = null;
			self.privateNameStack = [];
			// WebpackParser fields, mirroring the slow constructor below with the
			// gate-guaranteed constants (lazy, no locations/onComment) folded in
			self._wordLookups = shape.wordLookups;
			// acorn only calls `.test()` on reservedWordsStrictBind (in
			// checkLValSimple); swap its regexp for the Set-backed check
			/** @type {{ reservedWordsStrictBind: { test: (name: string) => boolean } }} */ (
				self
			).reservedWordsStrictBind = shape.wordLookups.reservedBindTest;
			self._ecmaVersion = shape.ecmaVersion;
			self._noLocations = true;
			self._lazy = true;
			self._lazyComments =
				/** @type {Options & { lazyComments?: CollectedComment[] }} */ (
					options
				).lazyComments;
			if (hashbang) self.skipLineComment(2);
			self._importPhase = null;
			self._importPhasesEnabled =
				/** @type {Options & { importPhases?: boolean }} */ (options)
					.importPhases === true;
			self._moduleFallback =
				/** @type {Options & { moduleFallback?: boolean }} */ (options)
					.moduleFallback === true;
			self._moduleSyntaxSeen = false;
			self._subscriptFastPath = shape.ecmaVersion >= 11;
			const proto = WebpackParser.prototype;
			// lazy is gate-guaranteed, so the slow path's `lazy ||` short-circuits
			// fold to constants; the method-identity gates still run so a plugin
			// subclass constructed through here keeps its overrides respected
			self._tokenFastPath = true;
			self._fullTokenFastPath = shape.ecmaVersion >= 12;
			self._inlineFinish = self.finishToken === proto.finishToken;
			self._newlineBefore = 2;
			self._wordKind = -1;
			self._deStack = [];
			self._deDepth = 0;
			self._propHashFastPath = self.checkPropClash === proto.checkPropClash;
			self._propHashStack = [];
			self._propHashDepth = 0;
			self._arrStack = [];
			self._arrDepth = 0;
			self._scopePool = [];
			self._setPool = [];
			self._exportHeadDepth = -1;
			self._exitedHoisted = undefined;
			self._moduleDeclarations = undefined;
			self._previousTopLevelStatement = undefined;
			self._labelsStack = [];
			self._labelsDepth = 0;
			self._scanGapNewline = false;
			self._validRegexpFlags = shape.validRegexpFlags;
			self._varIdOwn = self.parseVarId === proto.parseVarId;
			self._stmtFastPath =
				self._varIdOwn &&
				self.parseVarStatement === proto.parseVarStatement &&
				self.parseVar === proto.parseVar &&
				self.parseIfStatement === proto.parseIfStatement &&
				self.parseReturnStatement === proto.parseReturnStatement &&
				self.parseExpressionStatement === proto.parseExpressionStatement;
			self._lastArrow = null;
			self._arrowFastPath =
				self.parseArrowExpression === proto.parseArrowExpression;
			self._funcFastPath =
				shape.ecmaVersion >= 9 &&
				self.initFunction === proto.initFunction &&
				self.isSimpleParamList === proto.isSimpleParamList;
			self._funcStmtOwn =
				self._funcFastPath &&
				self.parseFunctionStatement === proto.parseFunctionStatement &&
				self.parseFunction === proto.parseFunction;
			self._stmt2FastPath =
				shape.ecmaVersion >= 9 &&
				self._varIdOwn &&
				self.parseForStatement === proto.parseForStatement &&
				self.parseFor === proto.parseFor &&
				self.parseForIn === proto.parseForIn &&
				self.parseForAfterInit === proto.parseForAfterInit &&
				self.parseVar === proto.parseVar &&
				self.parseWhileStatement === proto.parseWhileStatement &&
				self.parseSwitchStatement === proto.parseSwitchStatement &&
				self.parseThrowStatement === proto.parseThrowStatement &&
				self.parseTryStatement === proto.parseTryStatement &&
				self.parseBreakContinueStatement ===
					proto.parseBreakContinueStatement &&
				self.parseDoStatement === proto.parseDoStatement &&
				self.parseWithStatement === proto.parseWithStatement &&
				self.parseEmptyStatement === proto.parseEmptyStatement &&
				self.parseDebuggerStatement === proto.parseDebuggerStatement;
			self._exprFastPath =
				self.parseMaybeConditional === proto.parseMaybeConditional &&
				self.parseExprOps === proto.parseExprOps &&
				self.parseExprOp === proto.parseExprOp &&
				self.parseMaybeUnary === proto.parseMaybeUnary &&
				self.parseExprSubscripts === proto.parseExprSubscripts &&
				self.parseSubscripts === proto.parseSubscripts &&
				self.parseSubscript === proto.parseSubscript &&
				self.parseExprAtom === proto.parseExprAtom &&
				self.checkExpressionErrors === proto.checkExpressionErrors &&
				self.isContextual === proto.isContextual;
			self._moduleDeclFastPath = shape.ecmaVersion >= 16;
			self._classFastPath =
				shape.ecmaVersion >= 13 &&
				self.parseClassElementName === proto.parseClassElementName &&
				self.parsePropertyName === proto.parsePropertyName;
			// eslint-disable-next-line no-constructor-return -- the explicit object return is the mechanism that skips acorn's constructor
			return /** @type {WebpackParser} */ (self);
		}
		// `locations` are acorn's to track, so asking for them turns the lazy
		// path off rather than dropping them from the nodes
		const lazy = options.lazyNodes === true && options.locations !== true;
		// JavascriptParser._parse pre-disables acorn's tracking, so the
		// defensive copy only runs for direct callers
		if (lazy && options.ranges) {
			options = { ...options, ranges: false };
		}
		super(options, input, startPos);
		// acorn sets this.keywords/reservedWords in its constructor; parsing
		// (and thus readWord) only starts later in parse(), so this is ready
		/** @type {WordLookups} */
		this._wordLookups = getWordLookups(/** @type {ParserInternals} */ (this));
		// acorn only calls `.test()` on reservedWordsStrictBind (in
		// checkLValSimple); swap its regexp for the Set-backed check
		/** @type {{ reservedWordsStrictBind: { test: (name: string) => boolean } }} */
		(this).reservedWordsStrictBind = this._wordLookups.reservedBindTest;
		// per-token option probes cached once: acorn normalizes options in
		// `getOptions` before the constructor body runs and never mutates them
		const normalizedOptions = /** @type {ParserInternals} */ (this).options;
		/** @type {number} */
		this._ecmaVersion = normalizedOptions.ecmaVersion;
		this._noLocations = !normalizedOptions.locations;
		// lazy mode: nodes get only offsets, gating the owned tokenizer and
		// statement fast paths
		/** @type {boolean} */
		this._lazy = lazy;
		// lazy comment collection must not race a user-provided onComment
		/** @type {CollectedComment[] | undefined} */
		this._lazyComments =
			lazy && !options.onComment ? options.lazyComments : undefined;
		// acorn skips a hashbang inside its constructor, before `_lazyComments`
		// above exists — reconstruct the comment the override missed
		if (
			this._lazyComments !== undefined &&
			!startPos &&
			this.options.allowHashBang &&
			input.startsWith("#!")
		) {
			this._lazyComments.push(
				new LazyComment(
					false,
					2,
					0,
					/** @type {ParserInternals} */ (this).pos,
					input
				)
			);
		}
		/** @type {ImportPhase | null} */
		this._importPhase = null;
		/** @type {boolean} */
		this._importPhasesEnabled = options.importPhases === true;
		// auto source type: parse as module first, downgrade to script in place
		// (instead of a second full parse) when script-only syntax is hit
		/** @type {boolean} */
		this._moduleFallback = options.moduleFallback === true;
		// set once a module-only construct is parsed; blocks the downgrade
		/** @type {boolean} */
		this._moduleSyntaxSeen = false;
		// the owned parseSubscript assumes optional chaining exists (it bakes
		// `optional` into the node shape), so gate it on the normalized version
		/** @type {boolean} */
		this._subscriptFastPath = lazy && this._ecmaVersion >= 11;
		const proto = WebpackParser.prototype;
		const self = /** @type {ParserInternals} */ (this);
		// WHY: the owned per-token loop (nextToken/finishToken/next) also serves the
		// public non-lazy tokenizer(): locations off, no onToken, and none of the
		// acorn tokenizer methods it inlines or skips overridden by a plugin.
		// `nextToken`'s punct/dot/eq shortcuts bypass `getTokenFromCode`, `finishOp`,
		// `readToken_dot` and `readToken_eq_excl` even when the full token path below
		// is off, so their overrides must gate this loop too.
		/** @type {boolean} */
		this._tokenFastPath =
			lazy ||
			(this._noLocations &&
				!normalizedOptions.onToken &&
				this.nextToken === proto.nextToken &&
				this.finishToken === proto.finishToken &&
				this.next === proto.next &&
				this.skipSpace === proto.skipSpace &&
				this.getTokenFromCode === proto.getTokenFromCode &&
				this.finishOp === proto.finishOp &&
				self.readToken === proto.readToken &&
				self.updateContext === proto.updateContext &&
				self.readToken_dot === proto.readToken_dot &&
				self.readToken_eq_excl === proto.readToken_eq_excl);
		// the owned getTokenFromCode bakes in every ES2021 operator (?., ??=,
		// &&=, ...), so it needs at least that version; outside lazy mode it also
		// bypasses acorn's readToken_* family, so any override there turns it off
		/** @type {boolean} */
		this._fullTokenFastPath =
			this._ecmaVersion >= 12 &&
			(lazy ||
				(this._tokenFastPath &&
					self.readToken_slash === proto.readToken_slash &&
					self.readToken_mult_modulo_exp === proto.readToken_mult_modulo_exp &&
					self.readToken_pipe_amp === proto.readToken_pipe_amp &&
					self.readToken_caret === proto.readToken_caret &&
					self.readToken_plus_min === proto.readToken_plus_min &&
					self.readToken_lt_gt === proto.readToken_lt_gt &&
					self.readToken_question === proto.readToken_question &&
					self.readToken_numberSign === proto.readToken_numberSign &&
					self.readRadixNumber === proto.readRadixNumber));
		// the owned nextToken/readWord finish the commonest tokens in place,
		// which skips this.finishToken: needs the fast token loop (readWord runs
		// in every mode) and no finishToken override by a plugin
		/** @type {boolean} */
		this._inlineFinish =
			this._tokenFastPath &&
			this.finishToken === WebpackParser.prototype.finishToken;
		// whether the gap before the current token holds a line terminator:
		// 0 no, 1 yes, 2 unknown (canInsertSemicolon then scans the gap)
		/** @type {0 | 1 | 2} */
		this._newlineBefore = 2;
		this._wordKind = -1;
		// LIFO pool for call-scoped destructuring-errors records; depth resets
		// implicitly since a raise aborts the whole parse
		/** @type {DestructuringErrorsShim[]} */
		this._deStack = [];
		/** @type {number} */
		this._deDepth = 0;
		// LIFO pool of `parseObj` prop-clash records: ES9+ `checkPropClash` only
		// touches `.proto`, so one per depth; a subclass override gets a fresh `{}`
		/** @type {boolean} */
		this._propHashFastPath =
			/** @type {ParserInternals} */ (this).checkPropClash ===
			WebpackParser.prototype.checkPropClash;
		/** @type {{ proto: boolean }[]} */
		this._propHashStack = [];
		/** @type {number} */
		this._propHashDepth = 0;
		// LIFO pool of scratch arrays for list productions: elements are written
		// by index, then copied out exactly sized — a push-grown array retains
		// ~17 slots of capacity slack per list otherwise
		/** @type {(Node | null)[][]} */
		this._arrStack = [];
		/** @type {number} */
		this._arrDepth = 0;
		// LIFO pool of exited ParserScope records reused by enterScope; a raise leaves
		// stale entries behind, which the next parse's resets make harmless
		/** @type {ParserScope[]} */
		this._scopePool = [];
		// pool of emptied name Sets harvested by exitScope for declareName
		/** @type {Set<string>[]} */
		this._setPool = [];
		// WHY: a declaration directly under an `export` head is reported by the
		// export statement that holds it, never by the scope, so recording is
		// held back for exactly the one scope depth the head opened at.
		/** @type {number} */
		this._exportHeadDepth = -1;
		// what the scope `exitScope` just closed had recorded, for the caller
		// that owns that scope's node and reads it right after the call
		/** @type {HoistedDeclarations | undefined} */
		this._exitedHoisted = undefined;
		// the program's import and re-export declarations, in source order, each
		// followed by the top-level statement written before it
		/** @type {ModuleDeclarations | undefined} */
		this._moduleDeclarations = undefined;
		// WHY: what precedes a module declaration decides whether removing it
		// joins two statements through ASI, so the consumer needs it and cannot
		// read it back off a list holding declarations alone.
		/** @type {Statement | ModuleDeclaration | undefined} */
		this._previousTopLevelStatement = undefined;
		// per-depth pool of function-body label arrays (see parseFunctionBody)
		/** @type {{ kind?: string | null, name?: string, statementStart?: number }[][]} */
		this._labelsStack = [];
		/** @type {number} */
		this._labelsDepth = 0;
		// whether the last _scanGap saw a line terminator
		/** @type {boolean} */
		this._scanGapNewline = false;
		// `readRegexp`'s flag whitelist depends only on the ecmaVersion
		/** @type {string} */
		this._validRegexpFlags = getValidRegexpFlags(this._ecmaVersion);
		// _parseVarInto inlines acorn's parseVarId, so an override of it turns
		// every owned var-declaration path off
		/** @type {boolean} */
		this._varIdOwn = self.parseVarId === proto.parseVarId;
		// the owned parseStatement inlines these methods, so a parser plugin
		// overriding any of them turns the statement fast path off
		/** @type {boolean} */
		this._stmtFastPath =
			lazy &&
			this._varIdOwn &&
			this.parseVarStatement === proto.parseVarStatement &&
			this.parseVar === proto.parseVar &&
			this.parseIfStatement === proto.parseIfStatement &&
			this.parseReturnStatement === proto.parseReturnStatement &&
			this.parseExpressionStatement === proto.parseExpressionStatement;
		// last arrow finished by parseArrowExpression, so `expr === this._lastArrow`
		// replaces the megamorphic type probes on the expression spine: arrows are
		// created in one place and never backtracked, so identity captures the test
		/** @type {Expression | null} */
		this._lastArrow = null;
		// arrows must flow through parseArrowExpression for the identity probe;
		// a plugin overriding it falls back to the type-based probes
		/** @type {boolean} */
		this._arrowFastPath =
			lazy && this.parseArrowExpression === proto.parseArrowExpression;
		// the owned parseFunction/parseFunctionBody inline initFunction and
		// isSimpleParamList; a plugin overriding either turns the fast path off
		const internals = /** @type {ParserInternals} */ (this);
		/** @type {boolean} */
		this._funcFastPath =
			lazy &&
			this._ecmaVersion >= 9 &&
			internals.initFunction === proto.initFunction &&
			internals.isSimpleParamList === proto.isSimpleParamList;
		// the owned parseStatement inlines parseFunctionStatement's body
		/** @type {boolean} */
		this._funcStmtOwn =
			this._funcFastPath &&
			internals.parseFunctionStatement === proto.parseFunctionStatement &&
			this.parseFunction === proto.parseFunction;
		// the owned parseStatement inlines these statement parsers too; any
		// override falls back to acorn's dispatch for these heads
		/** @type {boolean} */
		this._stmt2FastPath =
			lazy &&
			this._ecmaVersion >= 9 &&
			this._varIdOwn &&
			internals.parseForStatement === proto.parseForStatement &&
			internals.parseFor === proto.parseFor &&
			internals.parseForIn === proto.parseForIn &&
			internals.parseForAfterInit === proto.parseForAfterInit &&
			this.parseVar === proto.parseVar &&
			internals.parseWhileStatement === proto.parseWhileStatement &&
			internals.parseSwitchStatement === proto.parseSwitchStatement &&
			internals.parseThrowStatement === proto.parseThrowStatement &&
			internals.parseTryStatement === proto.parseTryStatement &&
			internals.parseBreakContinueStatement ===
				proto.parseBreakContinueStatement &&
			internals.parseDoStatement === proto.parseDoStatement &&
			internals.parseWithStatement === proto.parseWithStatement &&
			internals.parseEmptyStatement === proto.parseEmptyStatement &&
			internals.parseDebuggerStatement === proto.parseDebuggerStatement;
		// `parseMaybeAssign`'s trivial-atom fast path returns the atom without
		// descending the seven-layer expression chain, so every layer it skips
		// must be the owned one
		/** @type {boolean} */
		this._exprFastPath =
			lazy &&
			this.parseMaybeConditional === proto.parseMaybeConditional &&
			this.parseExprOps === proto.parseExprOps &&
			this.parseExprOp === proto.parseExprOp &&
			this.parseMaybeUnary === proto.parseMaybeUnary &&
			this.parseExprSubscripts === proto.parseExprSubscripts &&
			this.parseSubscripts === proto.parseSubscripts &&
			this.parseSubscript === proto.parseSubscript &&
			this.parseExprAtom === proto.parseExprAtom &&
			this.checkExpressionErrors === proto.checkExpressionErrors &&
			this.isContextual === proto.isContextual;
		// the owned import/export declarations bake in ecmaVersion ≥ 16 (import
		// attributes; with it string export names ≥ 13 and `export * as` ≥ 11)
		/** @type {boolean} */
		this._moduleDeclFastPath = lazy && this._ecmaVersion >= 16;
		// the owned parseClassElement bakes in ES13 and rebuilds field nodes, which
		// would drop what a name-parser override adds: those (and pre-ES13) use acorn
		/** @type {boolean} */
		this._classFastPath =
			lazy &&
			this._ecmaVersion >= 13 &&
			this.parseClassElementName === proto.parseClassElementName &&
			this.parsePropertyName === proto.parsePropertyName;
	}

	/**
	 * Fetches a destructuring-errors record from the pool: acorn allocates one
	 * per expression parse and drops it at the end of the call, so strictly
	 * call-scoped users can reuse records instead. Pair every acquire with a
	 * `_releaseDestructuringErrors` on each non-throwing exit.
	 * @returns {DestructuringErrorsShim} reset record
	 */
	_acquireDestructuringErrors() {
		const stack = this._deStack;
		const depth = this._deDepth++;
		const cached = stack[depth];
		if (cached !== undefined) {
			cached.shorthandAssign =
				cached.trailingComma =
				cached.parenthesizedAssign =
				cached.parenthesizedBind =
				cached.doubleProto =
					-1;
			return cached;
		}
		return (stack[depth] = createDestructuringErrors());
	}

	/**
	 * @returns {void}
	 */
	_releaseDestructuringErrors() {
		this._deDepth--;
	}

	/**
	 * Fetches a scratch array for a list production: write elements by index,
	 * then materialize with `_releaseScratch`. Entries past the caller's write
	 * index are stale and meaningless.
	 * @template [T=Node]
	 * @returns {T[]} scratch array
	 */
	_acquireScratch() {
		const stack = this._arrStack;
		const depth = this._arrDepth++;
		const cached = stack[depth];
		if (cached !== undefined) return /** @type {T[]} */ (cached);
		return (stack[depth] = []);
	}

	/**
	 * @template T
	 * @param {T[]} scratch scratch array from `_acquireScratch`
	 * @param {number} count number of elements written
	 * @returns {T[]} exactly-sized copy of the first `count` entries
	 */
	_releaseScratch(scratch, count) {
		this._arrDepth--;
		return scratch.slice(0, count);
	}

	// ----- owned acorn utility layer: exact copies, so the lazy path runs no acorn
	// code; what remains is the class relationship, tables and cold delegations -----

	/**
	 * @param {TokenType} type the token type to consume
	 * @returns {boolean} whether it was consumed
	 */
	eat(type) {
		if (this.type === type) {
			this.next();
			return true;
		}
		return false;
	}

	/**
	 * @param {TokenType} type the token type that must follow
	 * @returns {void}
	 */
	expect(type) {
		if (!this.eat(type)) this.unexpected();
	}

	/** @returns {void} */
	semicolon() {
		if (!this.eat(tokTypes.semi) && !this.insertSemicolon()) this.unexpected();
	}

	/** @returns {boolean | undefined} whether one was inserted */
	insertSemicolon() {
		if (this.canInsertSemicolon()) {
			const onInsertedSemicolon =
				/** @type {Options & { onInsertedSemicolon?: (pos: number, loc?: Position | null) => void }} */ (
					this.options
				).onInsertedSemicolon;
			if (onInsertedSemicolon) {
				onInsertedSemicolon(this.lastTokEnd, this.lastTokEndLoc);
			}
			return true;
		}
	}

	/**
	 * @param {string} name the contextual keyword
	 * @returns {boolean} whether the current token is that keyword
	 */
	isContextual(name) {
		return (
			this.type === tokTypes.name && this.value === name && !this.containsEsc
		);
	}

	/**
	 * @param {string} name the contextual keyword
	 * @returns {boolean} whether it was consumed
	 */
	eatContextual(name) {
		if (!this.isContextual(name)) return false;
		this.next();
		return true;
	}

	/**
	 * @param {string} name the contextual keyword that must follow
	 * @returns {void}
	 */
	expectContextual(name) {
		if (!this.eatContextual(name)) this.unexpected();
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @param {boolean=} isAssign whether the target is an assignment
	 * @returns {void}
	 */
	checkPatternErrors(refDestructuringErrors, isAssign) {
		if (!refDestructuringErrors) return;
		if (refDestructuringErrors.trailingComma > -1) {
			this.raiseRecoverable(
				refDestructuringErrors.trailingComma,
				"Comma is not permitted after the rest element"
			);
		}
		const parens = isAssign
			? refDestructuringErrors.parenthesizedAssign
			: refDestructuringErrors.parenthesizedBind;
		if (parens > -1) {
			this.raiseRecoverable(
				parens,
				isAssign ? "Assigning to rvalue" : "Parenthesized pattern"
			);
		}
	}

	/** @returns {void} */
	checkYieldAwaitInDefaultParams() {
		if (this.yieldPos && (!this.awaitPos || this.yieldPos < this.awaitPos)) {
			this.raise(this.yieldPos, "Yield expression cannot be a default value");
		}
		if (this.awaitPos) {
			this.raise(this.awaitPos, "Await expression cannot be a default value");
		}
	}

	/**
	 * @param {Expression | ParenthesizedExpression} expr the expression to test
	 * @returns {boolean} whether it may be assigned to without destructuring
	 */
	isSimpleAssignTarget(expr) {
		if (expr.type === "ParenthesizedExpression") {
			return this.isSimpleAssignTarget(expr.expression);
		}
		return expr.type === "Identifier" || expr.type === "MemberExpression";
	}

	/** @returns {Expression} the parenthesized expression */
	parseParenExpression() {
		this.expect(tokTypes.parenL);
		const val = this.parseExpression();
		this.expect(tokTypes.parenR);
		return val;
	}

	/**
	 * @param {boolean=} isFor whether this is the head of a `for` statement
	 * @returns {boolean} whether `using` heads a declaration
	 */
	isUsing(isFor) {
		return this.isUsingKeyword(false, isFor);
	}

	/**
	 * @param {boolean=} isFor whether this is the head of a `for` statement
	 * @returns {boolean} whether `await using` heads a declaration
	 */
	isAwaitUsing(isFor) {
		return this.isUsingKeyword(true, isFor);
	}

	/** @returns {boolean} whether `export` is followed by a declaration */
	shouldParseExportStatement() {
		const keyword = this.type.keyword;
		return (
			keyword === "var" ||
			keyword === "const" ||
			keyword === "class" ||
			keyword === "function" ||
			this.isLet() ||
			this.isAsyncFunction()
		);
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @returns {Declaration} the exported declaration
	 */
	parseExportDeclaration(node) {
		return /** @type {Declaration} */ (
			this._inExportHead(() => this.parseStatement(null))
		);
	}

	/**
	 * @param {KeyedNode} prop the property whose `get`/`set` was just read
	 * @returns {void}
	 */
	parseGetterSetter(prop) {
		const target =
			/** @type {Node & { key: Node & { name: string }, value: Node & { params: Node[] }, kind: string }} */ (
				prop
			);
		const kind = target.key.name;
		this.parsePropertyName(prop);
		target.value = /** @type {Node & { params: Node[] }} */ (
			this.parseMethod(false)
		);
		target.kind = kind;
		const paramCount = target.kind === "get" ? 0 : 1;
		if (target.value.params.length !== paramCount) {
			const start = target.value.start;
			if (target.kind === "get") {
				this.raiseRecoverable(start, "getter should have no params");
			} else {
				this.raiseRecoverable(start, "setter should have exactly one param");
			}
		} else if (
			target.kind === "set" &&
			target.value.params[0].type === "RestElement"
		) {
			this.raiseRecoverable(
				target.value.params[0].start,
				"Setter cannot use rest params"
			);
		}
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @returns {void}
	 */
	parseFunctionParams(node) {
		this.expect(tokTypes.parenL);
		// no holes: the list is read with `allowEmpty` off
		node.params = /** @type {Pattern[]} */ (
			this.parseBindingList(tokTypes.parenR, false, this._ecmaVersion >= 8)
		);
		this.checkYieldAwaitInDefaultParams();
	}

	/** @returns {boolean} whether an async arrow's body follows */
	shouldParseAsyncArrow() {
		return !this.canInsertSemicolon() && this.eat(tokTypes.arrow);
	}

	/**
	 * @param {number} startPos where the arrow starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {(Expression | SpreadElement)[]} exprList its parameters
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"ArrowFunctionExpression">} the arrow function
	 */
	parseSubscriptAsyncArrow(startPos, startLoc, exprList, forInit) {
		return this.parseArrowExpression(
			this.startNodeAt(startPos, startLoc),
			exprList,
			true,
			forInit
		);
	}

	/**
	 * @param {(Expression | Pattern)[]} exprList what the parentheses held
	 * @returns {boolean} whether an arrow body may follow
	 */
	shouldParseArrow(exprList) {
		return !this.canInsertSemicolon();
	}

	/**
	 * @param {number} startPos where the arrow starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {(Expression | Pattern)[]} exprList its parameters
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"ArrowFunctionExpression">} the arrow function
	 */
	parseParenArrowList(startPos, startLoc, exprList, forInit) {
		return this.parseArrowExpression(
			this.startNodeAt(startPos, startLoc),
			exprList,
			false,
			forInit
		);
	}

	/**
	 * @template {AnyNode} T
	 * @param {T} item what the parentheses held
	 * @returns {T} the same item
	 */
	parseParenItem(item) {
		return item;
	}

	/**
	 * @param {boolean=} allowModifiers whether the item may carry modifiers
	 * @returns {Pattern} the item
	 */
	parseAssignableListItem(allowModifiers) {
		const element = this.parseMaybeDefault(this.start, this.startLoc);
		this.parseBindingListItem(element);
		return element;
	}

	/**
	 * @template {Pattern} T
	 * @param {T} param the bound target
	 * @returns {T} the same target
	 */
	parseBindingListItem(param) {
		return param;
	}

	/**
	 * @param {AnyNode[]} statements the statements that open a body
	 * @returns {void}
	 */
	adaptDirectivePrologue(statements) {
		for (
			let i = 0;
			i < statements.length && this.isDirectiveCandidate(statements[i]);
			++i
		) {
			const statement =
				/** @type {Node & { directive?: string, expression: Node & { raw: string } }} */ (
					statements[i]
				);
			statement.directive = statement.expression.raw.slice(1, -1);
		}
	}

	/**
	 * @param {NodeLike & { expression?: ExpressionField }} statement the statement to test
	 * @returns {boolean} whether it may be a directive
	 */
	isDirectiveCandidate(statement) {
		const target =
			/** @type {Node & { expression?: Node & { value?: LiteralValue } }} */ (
				statement
			);
		const quote = this.input.charCodeAt(statement.start);
		return (
			this._ecmaVersion >= 5 &&
			statement.type === "ExpressionStatement" &&
			/** @type {Node} */ (target.expression).type === "Literal" &&
			typeof (
				/** @type {Node & { value?: LiteralValue }} */ (target.expression).value
			) === "string" &&
			// reject parenthesized strings
			(quote === 34 || quote === 39)
		);
	}

	/**
	 * @returns {Pattern} the catch clause's parameter
	 */
	parseCatchClauseParam() {
		const param = this.parseBindingAtom();
		const simple = param.type === "Identifier";
		this.enterScope(simple ? SCOPE_SIMPLE_CATCH : 0);
		this.checkLValPattern(param, simple ? BIND_SIMPLE_CATCH : BIND_LEXICAL);
		this.expect(tokTypes.parenR);
		return param;
	}

	/** @returns {boolean} whether the current token can name a class element */
	isClassElementNameStart() {
		const type = this.type;
		return Boolean(
			type === tokTypes.name ||
			type === tokTypes.privateId ||
			type === tokTypes.num ||
			type === tokTypes.string ||
			type === tokTypes.bracketL ||
			type.keyword
		);
	}

	/**
	 * @template {AnyNodeType} T
	 * @param {UnfinishedNode} node the node to finish
	 * @param {T} type its ESTree type
	 * @param {number} pos where it ends
	 * @param {PositionLike | null=} loc its end position
	 * @returns {NodeOf<T>} the same node
	 */
	finishNodeAt(node, type, pos, loc) {
		if (!this._lazy) {
			return /** @type {NodeOf<T>} */ (
				base.finishNodeAt.call(this, node, type, pos, loc)
			);
		}
		node.type = type;
		node.end = pos;
		return /** @type {NodeOf<T>} */ (node);
	}

	/**
	 * @param {number} pos where to read
	 * @returns {number} the code point there, joining a surrogate pair
	 */
	fullCharCodeAt(pos) {
		const code = this.input.charCodeAt(pos);
		if (code <= 0xd7ff || code >= 0xdc00) return code;
		const next = this.input.charCodeAt(pos + 1);
		return next <= 0xdbff || next >= 0xe000
			? code
			: (code << 10) + next - 0x35fdc00;
	}

	/** @returns {number} the code point at the tokenizer's position */
	fullCharCodeAtPos() {
		return this.fullCharCodeAt(this.pos);
	}

	/**
	 * @param {number} code the code point at the tokenizer's position
	 * @returns {void}
	 */
	readToken(code) {
		// identifiers may start with an escape, so '\' dispatches to readWord too
		if (isIdentifierStart(code, this._ecmaVersion >= 6) || code === 92) {
			return this.readWord();
		}
		return this.getTokenFromCode(code);
	}

	/** @returns {TokContextLike} the innermost context */
	curContext() {
		return this.context[this.context.length - 1];
	}

	/**
	 * @param {TokContextLike} tokenCtx the context the token really opened
	 * @returns {void}
	 */
	overrideContext(tokenCtx) {
		if (this.curContext() !== tokenCtx) {
			this.context[this.context.length - 1] = /** @type {TokContextShim} */ (
				tokenCtx
			);
		}
	}

	/**
	 * Whether the enclosing function is a generator. A parse reads its scope: a
	 * method's `*` pushes no `function` context, which only the tokenizer reads.
	 * @returns {boolean} true where `yield` is an operator
	 */
	inGeneratorContext() {
		if (this.scopeStack.length > 0) return this.inGenerator;
		for (let i = this.context.length - 1; i >= 1; i--) {
			const context = this.context[i];
			if (context.token === "function") {
				return /** @type {boolean} */ (context.generator);
			}
		}
		return false;
	}

	/** @returns {TokContextLike[]} the context stack a fresh parser starts with */
	initialContext() {
		return [CTX_B_STAT];
	}

	/** @returns {ParserScope} the innermost scope */
	currentScope() {
		return this.scopeStack[this.scopeStack.length - 1];
	}

	/** @returns {ParserScope} the innermost scope `var` reaches */
	currentVarScope() {
		for (let i = this.scopeStack.length - 1; ; i--) {
			const scope = this.scopeStack[i];
			if (
				scope.flags &
				(SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK)
			) {
				return scope;
			}
		}
	}

	/** @returns {ParserScope} the innermost scope that binds `this` */
	currentThisScope() {
		for (let i = this.scopeStack.length - 1; ; i--) {
			const scope = this.scopeStack[i];
			if (
				scope.flags &
					(SCOPE_VAR | SCOPE_CLASS_FIELD_INIT | SCOPE_CLASS_STATIC_BLOCK) &&
				!(scope.flags & SCOPE_ARROW)
			) {
				return scope;
			}
		}
	}

	/**
	 * @param {{ flags: number }} scope the scope to ask about
	 * @returns {boolean | number} whether function names bind like `var` in it
	 */
	treatFunctionsAsVarInScope(scope) {
		return Boolean(
			scope.flags & SCOPE_FUNCTION ||
			(!this.inModule && scope.flags & SCOPE_TOP)
		);
	}

	/**
	 * @returns {boolean} whether the current position is inside a function body
	 */
	get inFunction() {
		return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0;
	}

	/**
	 * @returns {boolean} whether the enclosing function is a generator
	 */
	get inGenerator() {
		return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0;
	}

	/**
	 * @returns {boolean} whether the enclosing function is async
	 */
	get inAsync() {
		return (this.currentVarScope().flags & SCOPE_ASYNC) > 0;
	}

	/**
	 * @returns {boolean} whether `await` is usable at the current position
	 */
	get canAwait() {
		for (let i = this.scopeStack.length - 1; i >= 0; i--) {
			const flags = this.scopeStack[i].flags;
			if (flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT)) {
				return false;
			}
			if (flags & SCOPE_FUNCTION) return (flags & SCOPE_ASYNC) > 0;
		}
		return (
			(this.inModule && this._ecmaVersion >= 13) ||
			this.options.allowAwaitOutsideFunction === true
		);
	}

	/**
	 * @returns {boolean} whether a `return` statement is allowed here
	 */
	get allowReturn() {
		if (this.inFunction) return true;
		return Boolean(
			this.options.allowReturnOutsideFunction &&
			this.currentVarScope().flags & SCOPE_TOP
		);
	}

	/**
	 * @returns {boolean} whether `super.x` is allowed here
	 */
	get allowSuper() {
		return (
			(this.currentThisScope().flags & SCOPE_SUPER) > 0 ||
			this.options.allowSuperOutsideMethod === true
		);
	}

	/**
	 * @returns {boolean} whether `super()` is allowed here
	 */
	get allowDirectSuper() {
		return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0;
	}

	/**
	 * @returns {boolean | number} whether function declarations bind like vars here
	 */
	get treatFunctionsAsVar() {
		return this.treatFunctionsAsVarInScope(this.currentScope());
	}

	/**
	 * @returns {boolean} whether `new.target` is allowed here
	 */
	get allowNewDotTarget() {
		for (let i = this.scopeStack.length - 1; i >= 0; i--) {
			const flags = this.scopeStack[i].flags;
			if (
				flags & (SCOPE_CLASS_STATIC_BLOCK | SCOPE_CLASS_FIELD_INIT) ||
				(flags & SCOPE_FUNCTION && !(flags & SCOPE_ARROW))
			) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @returns {boolean} whether a `using` declaration is allowed here
	 */
	get allowUsing() {
		const flags = this.currentScope().flags;
		if (flags & SCOPE_SWITCH) return false;
		if (!this.inModule && flags & SCOPE_TOP) return false;
		return true;
	}

	/**
	 * @returns {boolean} whether the current position is in a static block
	 */
	get inClassStaticBlock() {
		return (this.currentVarScope().flags & SCOPE_CLASS_STATIC_BLOCK) > 0;
	}

	/**
	 * Report a parse error, naming where in the source it was found.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/location.js
	 * @param {number} pos where the error is
	 * @param {string} message what is wrong
	 * @returns {never} never returns
	 */
	raise(pos, message) {
		const loc = getLineInfo(this.input, pos);
		message += ` (${loc.line}:${loc.column})`;
		if (this.sourceFile) message += ` in ${this.sourceFile}`;
		const err =
			/** @type {SyntaxError & { pos: number, loc: ParserPosition, raisedAt: number }} */ (
				new SyntaxError(message)
			);
		err.pos = pos;
		err.loc = loc;
		err.raisedAt = this.pos;
		throw err;
	}

	/**
	 * @param {number} pos where the error is
	 * @param {string} message what is wrong
	 * @returns {never} never returns
	 */
	raiseRecoverable(pos, message) {
		const loc = getLineInfo(this.input, pos);
		message += ` (${loc.line}:${loc.column})`;
		if (this.sourceFile) message += ` in ${this.sourceFile}`;
		const err =
			/** @type {SyntaxError & { pos: number, loc: ParserPosition, raisedAt: number }} */ (
				new SyntaxError(message)
			);
		err.pos = pos;
		err.loc = loc;
		err.raisedAt = this.pos;
		throw err;
	}

	/** @returns {Position | undefined} where the tokenizer stands, when tracked */
	curPosition() {
		if (this.options.locations) {
			return /** @type {Position} */ (
				new ParserPosition(this.curLine, this.pos - this.lineStart)
			);
		}
	}

	/**
	 * Report a bad escape, unless it sits in a tagged template, where the raw
	 * text is still well-formed and only the cooked value is lost.
	 * @param {number} position where the escape is
	 * @param {string} message what is wrong
	 * @returns {void}
	 */
	invalidStringToken(position, message) {
		if (this.inTemplateElement && this._ecmaVersion >= 9) {
			throw INVALID_TEMPLATE_ESCAPE_ERROR;
		} else {
			this.raise(position, message);
		}
	}

	/** @returns {void} */
	tryReadTemplateToken() {
		this.inTemplateElement = true;
		try {
			this.readTmplToken();
		} catch (err) {
			if (err === INVALID_TEMPLATE_ESCAPE_ERROR) {
				this.readInvalidTemplateToken();
			} else {
				throw err;
			}
		}
		this.inTemplateElement = false;
	}

	/** @returns {void} */
	readInvalidTemplateToken() {
		const input = this.input;
		for (; this.pos < input.length; this.pos++) {
			switch (input[this.pos]) {
				case "\\":
					++this.pos;
					break;
				case "$":
					if (input[this.pos + 1] !== "{") break;
				// falls through
				case "`":
					return this.finishToken(
						tokTypes.invalidTemplate,
						input.slice(this.start, this.pos)
					);
				case "\r":
					if (input[this.pos + 1] === "\n") ++this.pos;
				// falls through
				case "\n":
				case "\u2028":
				case "\u2029":
					++this.curLine;
					this.lineStart = this.pos + 1;
					break;
			}
		}
		this.raise(this.start, "Unterminated template");
	}

	// ----- tokenizer fast paths -----

	/** @returns {void} */
	nextToken() {
		const context = this.context;
		const curContext = context[context.length - 1];
		if (!this._tokenFastPath || curContext === undefined) {
			this._newlineBefore = 2;
			return base.nextToken.call(this);
		}
		// WHY: read the two fields rather than memoize their answer onto the
		// context. acorn's TokContext constructor sets both, so two loads off one
		// shape beat a symbol-keyed probe — and the memo added a property to the
		// contexts that reached it, splitting them across shapes and making every
		// other read of a context polymorphic.
		if (curContext.preserveSpace || curContext.override) {
			this._newlineBefore = 2;
			// template contexts: acorn's nextToken minus its dead location bookkeeping
			// (no space skipping, EOF probe, then the context's own virtual reader)
			const override = curContext.override;
			if (override !== undefined && curContext.preserveSpace) {
				this.start = this.pos;
				if (this.pos >= this.input.length) {
					return this.finishToken(tokTypes.eof);
				}
				return /** @type {(parser: SyntaxParser) => void} */ (override)(this);
			}
			return base.nextToken.call(this);
		}
		const input = this.input;
		const len = input.length;
		let pos = this.pos;
		// line terminators are flagged while skipping, so ASI needs no gap re-scan;
		// a delegated path (acorn html comments) may eat part of it, so 2 = unknown
		/** @type {0 | 1 | 2} */
		let newline = pos === this.lastTokEnd ? 0 : 2;
		// one CHAR_CLASS load classifies each char for both the skip loop and
		// the token dispatch below (yuku's ws_class/ident/punct tables in one)
		let code = 0;
		let cls = CLS_OTHER;
		while (pos < len) {
			code = input.charCodeAt(pos);
			cls = CHAR_CLASS[code];
			if (cls < CLS_SPACE) {
				if (cls === CLS_UNICODE) {
					// unicode whitespace / line terminators: the cold reader consumes them
					this.pos = pos;
					this._skipSpaceCold();
					pos = this.pos;
					if (newline === 0) newline = 2;
					code = pos < len ? input.charCodeAt(pos) : 0;
					cls = CHAR_CLASS[code];
				}
				break;
			}
			if (cls === CLS_SPACE) {
				// space, tab, VT, FF (no CRLF/line bookkeeping in lazy mode)
				pos++;
			} else if (cls === CLS_NEWLINE) {
				newline = 1;
				pos++;
			} else {
				const next = input.charCodeAt(pos + 1);
				if (next === 42) {
					this.pos = pos;
					this.skipBlockComment();
					pos = this.pos;
					// the comment body may hold a line terminator
					if (newline === 0) newline = 2;
				} else if (next === 47) {
					this.pos = pos;
					this.skipLineComment(2);
					pos = this.pos;
				} else {
					// a division/regexp token, not a comment
					cls = CLS_OTHER;
					break;
				}
			}
		}
		this._newlineBefore = newline;
		this.pos = pos;
		this.start = pos;
		if (pos >= len) return this.finishToken(tokTypes.eof);
		switch (cls) {
			case CLS_IDENT:
				return this.readWord();
			case CLS_PUNCT: {
				const type = /** @type {TokenType} */ (SIMPLE_PUNCT[code]);
				this.pos = pos + 1;
				if (!this._inlineFinish) return this.finishToken(type);
				// finishToken inlined for the simple punctuators (no value, and
				// their context updates are per-char static)
				this.end = pos + 1;
				const prevType = this.type;
				this.type = type;
				this.value = undefined;
				switch (code) {
					case 41:
					case 125: {
						// parenR/braceR.updateContext, inlined
						if (context.length === 1) {
							this.exprAllowed = true;
							break;
						}
						let out = /** @type {TokContextShim} */ (context.pop());
						if (
							out === CTX_B_STAT &&
							/** @type {TokContextShim} */ (context[context.length - 1])
								.token === "function"
						) {
							out = /** @type {TokContextShim} */ (context.pop());
						}
						this.exprAllowed = !out.isExpr;
						break;
					}
					case 123:
						// braceL.updateContext, inlined
						context.push(this.braceIsBlock(prevType) ? CTX_B_STAT : CTX_B_EXPR);
						this.exprAllowed = true;
						break;
					case 40: {
						// parenL.updateContext, inlined
						const statementParens =
							prevType === tokTypes._if ||
							prevType === tokTypes._for ||
							prevType === tokTypes._with ||
							prevType === tokTypes._while;
						context.push(statementParens ? CTX_P_STAT : CTX_P_EXPR);
						this.exprAllowed = true;
						break;
					}
					case 58:
						// colon.updateContext, inlined
						if (
							/** @type {TokContextShim} */ (context[context.length - 1])
								.token === "function"
						) {
							context.pop();
						}
						this.exprAllowed = true;
						break;
					case 93:
						// bracketR: no context hook, beforeExpr is false
						this.exprAllowed = false;
						break;
					default:
						// semi, comma, bracketL: no context hook, beforeExpr is true
						this.exprAllowed = true;
				}
				return;
			}
			case CLS_DOT: {
				// `.` not starting `.5` or `...`: skip readToken_dot's re-dispatch
				const next = input.charCodeAt(pos + 1);
				if ((next < 48 || next > 57) && next !== 46) {
					this.pos = pos + 1;
					return this.finishToken(tokTypes.dot);
				}
				return this.getTokenFromCode(code);
			}
			case CLS_EQ: {
				// `=` not starting `==` or `=>`: skip readToken_eq_excl + finishOp slice
				const next = input.charCodeAt(pos + 1);
				if (next !== 61 && next !== 62) {
					this.pos = pos + 1;
					return this.finishToken(tokTypes.eq, "=");
				}
				return this.getTokenFromCode(code);
			}
			case CLS_UNICODE:
				return this.readToken(this.fullCharCodeAtPos());
			default:
				return this.getTokenFromCode(code);
		}
	}

	/**
	 * @param {TokenType} type the token's kind
	 * @param {TokenValue=} value what it carries
	 * @returns {void}
	 */
	finishToken(type, value) {
		if (!this._tokenFastPath) {
			return base.finishToken.call(this, type, value);
		}
		this.end = this.pos;
		const prevType = this.type;
		this.type = type;
		this.value = value;
		const internal = /** @type {TokenTypeInternal} */ (type);
		// WHY: the types without a context hook are the majority and are answered
		// here, the rest in `_updateContext`. Keeping the two apart is what holds
		// this method under V8's inlining budget — inlined at its ~30 call sites it
		// costs no call at all, and whole it was over the limit and inlined nowhere.
		if (internal.updateContext === null) {
			// keyword-after-dot forbids an expression, else `beforeExpr` says whether
			// a `/` next opens a regexp or divides. `name` has a hook, so it is not
			// here; acorn gives every keyword-after-dot case one too.
			this.exprAllowed =
				prevType === tokTypes.dot && internal.keyword !== undefined
					? false
					: internal.beforeExpr;
			return;
		}
		this._updateContext(internal, prevType, value);
	}

	/**
	 * `finishToken`'s tail for the token types carrying a context hook: acorn's
	 * `updateContext`, inlined per type.
	 * @param {TokenTypeInternal} internal the token's kind
	 * @param {TokenType} prevType the kind before it
	 * @param {(string | number | bigint | null | { pattern: string, flags: string, value: RegExp | null })=} value what it carries
	 * @returns {void}
	 */
	_updateContext(internal, prevType, value) {
		const type = /** @type {TokenType} */ (internal);
		if (type === tokTypes.name) {
			// name.updateContext, inlined for the commonest token: only `of` /
			// `yield` (outside a `.` access, ES6+) can re-allow an expression
			this.exprAllowed =
				((value === "of" && !this.exprAllowed) ||
					(value === "yield" && this.inGeneratorContext())) &&
				prevType !== tokTypes.dot &&
				this._ecmaVersion >= 6;
		} else {
			const update =
				/** @type {(prevType: TokenType) => void} */
				(internal.updateContext);
			if (type === tokTypes.parenR || type === tokTypes.braceR) {
				// parenR/braceR.updateContext, inlined
				const context = this.context;
				if (context.length === 1) {
					this.exprAllowed = true;
				} else {
					let out = /** @type {TokContextShim} */ (context.pop());
					if (
						out === CTX_B_STAT &&
						/** @type {TokContextShim} */ (context[context.length - 1])
							.token === "function"
					) {
						out = /** @type {TokContextShim} */ (context.pop());
					}
					this.exprAllowed = !out.isExpr;
				}
			} else if (type === tokTypes.braceL) {
				// braceL.updateContext, inlined
				this.context.push(
					this.braceIsBlock(prevType) ? CTX_B_STAT : CTX_B_EXPR
				);
				this.exprAllowed = true;
			} else if (type === tokTypes.parenL) {
				// parenL.updateContext, inlined
				const statementParens =
					prevType === tokTypes._if ||
					prevType === tokTypes._for ||
					prevType === tokTypes._with ||
					prevType === tokTypes._while;
				this.context.push(statementParens ? CTX_P_STAT : CTX_P_EXPR);
				this.exprAllowed = true;
			} else if (internal.keyword !== undefined && prevType === tokTypes.dot) {
				// `.function` etc.: keyword-after-dot wins over the type's own hook
				this.exprAllowed = false;
			} else {
				update.call(this, prevType);
			}
		}
	}

	/**
	 * Whether a `{` here opens a block rather than an object literal, which the
	 * token before it decides.
	 * @param {TokenType} prevType the token before the brace
	 * @returns {boolean} whether it opens a block
	 */
	braceIsBlock(prevType) {
		const context = this.context;
		const parent = /** @type {TokContextShim} */ (context[context.length - 1]);
		if (parent === CTX_F_EXPR || parent === CTX_F_STAT) return true;
		if (
			prevType === tokTypes.colon &&
			(parent === CTX_B_STAT || parent === CTX_B_EXPR)
		) {
			return !parent.isExpr;
		}
		// after `return`, or after `yield`/`of` (name with exprAllowed), a line
		// terminator decides between block and expression
		if (
			prevType === tokTypes._return ||
			(prevType === tokTypes.name && this.exprAllowed)
		) {
			return this._gapHasNewline();
		}
		if (
			prevType === tokTypes._else ||
			prevType === tokTypes.semi ||
			prevType === tokTypes.eof ||
			prevType === tokTypes.parenR ||
			prevType === tokTypes.arrow
		) {
			return true;
		}
		if (prevType === tokTypes.braceL) return parent === CTX_B_STAT;
		if (
			prevType === tokTypes._var ||
			prevType === tokTypes._const ||
			prevType === tokTypes.name
		) {
			return false;
		}
		return !this.exprAllowed;
	}

	/**
	 * Allocation-free mirror of acorn's `skipWhiteSpace` regexp: advances over
	 * whitespace and comments from `pos` without touching tokenizer state,
	 * flagging line terminators (including inside block comments) in
	 * `_scanGapNewline`. An unterminated block comment stops the scan, exactly
	 * like the regexp.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/whitespace.js
	 * @param {number} pos scan start offset
	 * @returns {number} offset of the first char past the gap
	 */
	_scanGap(pos) {
		const input = this.input;
		const len = input.length;
		let newline = false;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			if (ch === 32 || ch === 9 || ch === 11 || ch === 12 || ch === 160) {
				pos++;
			} else if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) {
				newline = true;
				pos++;
			} else if (ch === 47) {
				const next = input.charCodeAt(pos + 1);
				if (next === 47) {
					pos += 2;
					while (pos < len) {
						const commentCh = input.charCodeAt(pos);
						if (
							commentCh === 10 ||
							commentCh === 13 ||
							commentCh === 0x2028 ||
							commentCh === 0x2029
						) {
							break;
						}
						pos++;
					}
				} else if (next === 42) {
					const end = input.indexOf("*/", pos + 2);
					if (end === -1) break;
					if (!newline) {
						for (let i = pos + 2; i < end; i++) {
							const commentCh = input.charCodeAt(i);
							if (
								commentCh === 10 ||
								commentCh === 13 ||
								commentCh === 0x2028 ||
								commentCh === 0x2029
							) {
								newline = true;
								break;
							}
						}
					}
					pos = end + 2;
				} else {
					break;
				}
			} else if (
				ch >= 5760 &&
				nonASCIIwhitespace.test(String.fromCharCode(ch))
			) {
				pos++;
			} else {
				break;
			}
		}
		this._scanGapNewline = newline;
		return pos;
	}

	/**
	 * Whether a `let` here opens a lexical declaration rather than naming a
	 * variable, which needs a look-ahead past the keyword.
	 * @param {string | null=} context the statement context
	 * @returns {boolean} whether it declares
	 */
	isLet(context) {
		if (this._ecmaVersion < 6 || !this.isContextual("let")) return false;
		let next = this._scanGap(this.pos);
		let nextCh = this.fullCharCodeAt(next);
		// `let [` is an explicit negative lookahead for ExpressionStatement
		if (nextCh === 91 || nextCh === 92) return true; // '[', '\'
		if (context) return false;
		if (nextCh === 123) return true; // '{'
		if (isIdentifierStart(nextCh)) {
			const start = next;
			do {
				next += nextCh <= 0xffff ? 1 : 2;
			} while (isIdentifierChar((nextCh = this.fullCharCodeAt(next))));
			if (nextCh === 92) return true;
			if (!isRelationalKeyword(this.input, start, next)) return true;
		}
		return false;
	}

	/**
	 * Whether `async` here heads a function declaration, which no line break may
	 * separate from the `function` keyword.
	 * @returns {boolean} whether it does
	 */
	isAsyncFunction() {
		if (this._ecmaVersion < 8 || !this.isContextual("async")) return false;
		const input = this.input;
		const next = this._scanGap(this.pos);
		if (this._scanGapNewline || !input.startsWith("function", next)) {
			return false;
		}
		if (next + 8 === input.length) return true;
		const after = this.fullCharCodeAt(next + 8);
		return !(isIdentifierChar(after) || after === 92);
	}

	/**
	 * Whether `using` (or `await using`) here heads a declaration rather than
	 * naming a variable.
	 * @param {boolean} isAwaitUsing whether the `await` form is being tested
	 * @param {boolean=} isFor whether this is the head of a `for` statement
	 * @returns {boolean} whether it declares
	 */
	isUsingKeyword(isAwaitUsing, isFor) {
		if (
			this._ecmaVersion < 17 ||
			!this.isContextual(isAwaitUsing ? "await" : "using")
		) {
			return false;
		}
		const input = this.input;
		let next = this._scanGap(this.pos);
		if (this._scanGapNewline) return false;
		if (isAwaitUsing) {
			const usingEndPos = next + 5;
			let after;
			if (
				!input.startsWith("using", next) ||
				usingEndPos === input.length ||
				isIdentifierChar((after = this.fullCharCodeAt(usingEndPos))) ||
				after === 92
			) {
				return false;
			}
			next = this._scanGap(usingEndPos);
			if (this._scanGapNewline) return false;
		}
		let ch = this.fullCharCodeAt(next);
		if (!isIdentifierStart(ch) && ch !== 92) return false;
		const idStart = next;
		do {
			next += ch <= 0xffff ? 1 : 2;
		} while (isIdentifierChar((ch = this.fullCharCodeAt(next))));
		if (ch === 92) return true;
		if (isRelationalKeyword(input, idStart, next)) return false;
		if (
			isFor &&
			!isAwaitUsing &&
			next - idStart === 2 &&
			input.charCodeAt(idStart) === 111 &&
			input.charCodeAt(idStart + 1) === 102
		) {
			// `for (using of …)`: only an initializer keeps it a declaration
			next = this._scanGap(next);
			if (
				input.charCodeAt(next) !== 61 ||
				(ch = input.charCodeAt(next + 1)) === 61 ||
				ch === 62
			) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Whether the gap before the current token holds a line terminator, served
	 * from the owned tokenizer's flag when known.
	 * @returns {boolean} whether a line terminator precedes the current token
	 */
	_gapHasNewline() {
		const newlineBefore = this._newlineBefore;
		if (newlineBefore !== 2) return newlineBefore === 1;
		const input = this.input;
		const end = this.start;
		for (let i = this.lastTokEnd; i < end; i++) {
			const ch = input.charCodeAt(i);
			// LF, CR, LS, PS — acorn's `lineBreak` alternation
			if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) {
				// memoize: the gap is fixed until `nextToken` rewrites the flag, and
				// ASI probes repeat per token (e.g. names behind a /*#__PURE__*/ comment)
				this._newlineBefore = 1;
				return true;
			}
		}
		this._newlineBefore = 0;
		return false;
	}

	/**
	 * Move past the current token.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {boolean=} ignoreEscapeSequenceInKeyword whether an escaped keyword is allowed
	 * @returns {void}
	 */
	next(ignoreEscapeSequenceInKeyword) {
		if (!this._tokenFastPath) {
			return base.next.call(this, ignoreEscapeSequenceInKeyword);
		}
		const type = this.type;
		// `containsEsc` is a parser field and almost always false; testing it first
		// keeps the TokenType load off the common path.
		if (this.containsEsc && !ignoreEscapeSequenceInKeyword && type.keyword) {
			this.raiseRecoverable(
				this.start,
				`Escape sequence in keyword ${type.keyword}`
			);
		}
		if (this.options.onTokenRead) this.options.onTokenRead(this);
		this.lastTokEnd = this.end;
		this.lastTokStart = this.start;
		this.nextToken();
	}

	/**
	 * @param {TokenType} type the token's kind
	 * @param {number} size how many characters it spans
	 * @returns {void}
	 */
	finishOp(type, size) {
		const pos = this.pos;
		const input = this.input;
		if (size === 1) {
			this.pos = pos + 1;
			return this.finishToken(type, input.slice(pos, pos + 1));
		}
		let key = input.charCodeAt(pos) | (input.charCodeAt(pos + 1) << 7);
		if (size > 2) {
			key |= input.charCodeAt(pos + 2) << 14;
			if (size > 3) key |= input.charCodeAt(pos + 3) << 21;
		}
		let str = OP_CACHE.get(key);
		if (str === undefined) {
			str = input.slice(pos, pos + size);
			OP_CACHE.set(key, str);
		}
		this.pos = pos + size;
		return this.finishToken(type, str);
	}

	/**
	 * @param {number} code the code point at the tokenizer's position
	 * @returns {void}
	 */
	getTokenFromCode(code) {
		if (!this._fullTokenFastPath) {
			return base.getTokenFromCode.call(this, code);
		}
		const input = this.input;
		const pos = this.pos;
		switch (code) {
			case 46: {
				// '.': number, ellipsis or plain dot
				const next = input.charCodeAt(pos + 1);
				if (next >= 48 && next <= 57) return this.readNumber(true);
				if (next === 46 && input.charCodeAt(pos + 2) === 46) {
					this.pos = pos + 3;
					return this.finishToken(tokTypes.ellipsis);
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.dot);
			}
			case 47: {
				// '/': regexp in expression position, otherwise /= or /
				if (this.exprAllowed) {
					this.pos = pos + 1;
					return this.readRegexp();
				}
				if (input.charCodeAt(pos + 1) === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "/=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.slash, "/");
			}
			case 37: {
				// '%': %= or %
				if (input.charCodeAt(pos + 1) === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "%=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.modulo, "%");
			}
			case 42: {
				// '*': **=, **, *= or *
				const next = input.charCodeAt(pos + 1);
				if (next === 42) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.assign, "**=");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.starstar, "**");
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "*=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.star, "*");
			}
			case 124: {
				// '|': ||=, ||, |= or |
				const next = input.charCodeAt(pos + 1);
				if (next === 124) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.assign, "||=");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.logicalOR, "||");
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "|=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.bitwiseOR, "|");
			}
			case 38: {
				// '&': &&=, &&, &= or &
				const next = input.charCodeAt(pos + 1);
				if (next === 38) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.assign, "&&=");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.logicalAND, "&&");
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "&=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.bitwiseAND, "&");
			}
			case 94: {
				// '^': ^= or ^
				if (input.charCodeAt(pos + 1) === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "^=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.bitwiseXOR, "^");
			}
			case 43: {
				// '+': ++, += or +
				const next = input.charCodeAt(pos + 1);
				if (next === 43) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.incDec, "++");
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "+=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.plusMin, "+");
			}
			case 45: {
				// '-': --, -= or -; `-->` may open an HTML line comment
				const next = input.charCodeAt(pos + 1);
				if (next === 45) {
					// `-->` opens an HTML line comment only at the start of a line
					// (acorn readToken_plus_min): https://github.com/acornjs/acorn/blob/8.15.0/acorn/src/tokenize.js#L599-L618
					if (
						input.charCodeAt(pos + 2) === 62 &&
						!this.inModule &&
						(this.lastTokEnd === 0 ||
							lineBreak.test(input.slice(this.lastTokEnd, pos)))
					) {
						this.skipLineComment(3);
						this.skipSpace();
						return this.nextToken();
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.incDec, "--");
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.assign, "-=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.plusMin, "-");
			}
			case 60: {
				// '<': <<=, <<, <= or <; `<!--` opens an HTML line comment
				const next = input.charCodeAt(pos + 1);
				if (next === 60) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.assign, "<<=");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.bitShift, "<<");
				}
				if (
					next === 33 &&
					!this.inModule &&
					input.charCodeAt(pos + 2) === 45 &&
					input.charCodeAt(pos + 3) === 45
				) {
					// `<!--` opens an HTML line comment (acorn readToken_lt_gt):
					// https://github.com/acornjs/acorn/blob/8.15.0/acorn/src/tokenize.js#L620-L649
					this.skipLineComment(4);
					this.skipSpace();
					return this.nextToken();
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.relational, "<=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.relational, "<");
			}
			case 62: {
				// '>': >>>=, >>>, >>=, >>, >= or >
				const next = input.charCodeAt(pos + 1);
				if (next === 62) {
					if (input.charCodeAt(pos + 2) === 62) {
						if (input.charCodeAt(pos + 3) === 61) {
							this.pos = pos + 4;
							return this.finishToken(tokTypes.assign, ">>>=");
						}
						this.pos = pos + 3;
						return this.finishToken(tokTypes.bitShift, ">>>");
					}
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.assign, ">>=");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.bitShift, ">>");
				}
				if (next === 61) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.relational, ">=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.relational, ">");
			}
			case 61: {
				// '=': ===, ==, => or =
				const next = input.charCodeAt(pos + 1);
				if (next === 61) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.equality, "===");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.equality, "==");
				}
				if (next === 62) {
					this.pos = pos + 2;
					return this.finishToken(tokTypes.arrow);
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.eq, "=");
			}
			case 33: {
				// '!': !==, != or !
				const next = input.charCodeAt(pos + 1);
				if (next === 61) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.equality, "!==");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.equality, "!=");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.prefix, "!");
			}
			case 63: {
				// '?': ?. (not before a digit), ??=, ?? or ?
				const next = input.charCodeAt(pos + 1);
				if (next === 46) {
					const next2 = input.charCodeAt(pos + 2);
					if (next2 < 48 || next2 > 57) {
						this.pos = pos + 2;
						return this.finishToken(tokTypes.questionDot, "?.");
					}
				}
				if (next === 63) {
					if (input.charCodeAt(pos + 2) === 61) {
						this.pos = pos + 3;
						return this.finishToken(tokTypes.assign, "??=");
					}
					this.pos = pos + 2;
					return this.finishToken(tokTypes.coalesce, "??");
				}
				this.pos = pos + 1;
				return this.finishToken(tokTypes.question, "?");
			}
			case 126: {
				// '~'
				this.pos = pos + 1;
				return this.finishToken(tokTypes.prefix, "~");
			}
			case 96: {
				// '`'
				this.pos = pos + 1;
				return this.finishToken(tokTypes.backQuote);
			}
			case 48: {
				// '0': radix literals split off before the decimal reader
				const next = input.charCodeAt(pos + 1);
				if (next === 120 || next === 88) return this._readRadixNumber(16);
				if (next === 111 || next === 79) return this._readRadixNumber(8);
				if (next === 98 || next === 66) return this._readRadixNumber(2);
				return this.readNumber(false);
			}
			case 49:
			case 50:
			case 51:
			case 52:
			case 53:
			case 54:
			case 55:
			case 56:
			case 57:
				return this.readNumber(false);
			case 34:
			case 39:
				return this.readString(code);
			default:
				return this._getUnknownOrPrivate(code);
		}
	}

	/**
	 * Read an identifier's text, recording in `containsEsc` whether any of it
	 * was written as an escape — which stops it reading as a keyword.
	 * @returns {string} the identifier
	 */
	readWord1() {
		const input = this.input;
		const start = this.pos;
		const len = input.length;
		// djb2-style hash folded into the scan loop (yuku scans each lexeme in
		// one pass); computing it for the rare over-long words is cheaper than
		// re-walking every word in a second pass
		let hash = 0;
		let pos = start;
		let cls = CLS_OTHER;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			cls = CHAR_CLASS[ch];
			// CLS_DIGIT and CLS_IDENT are the two identifier-continue classes
			if (cls > CLS_IDENT) break;
			hash = (Math.imul(hash, 33) + ch) | 0;
			pos++;
		}
		if (cls === CLS_UNICODE) {
			// an escape or a non-ASCII char: restart cold, where those rules live
			return this._readWord1Cold();
		}
		this.containsEsc = false;
		this.pos = pos;
		const wordLen = pos - start;
		// Single-char words skip the cache (V8 serves those slices from its
		// single-character table without allocating); long words skip it too.
		if (wordLen >= 2 && wordLen <= WORD_CACHE_MAX_LEN) {
			const slot = (hash & WORD_CACHE_MASK) * WORD_CACHE_STRIDE;
			const cached = /** @type {string} */ (WORD_CACHE[slot]);
			if (wordMatches(input, start, cached, wordLen)) return cached;
			const word = input.slice(start, pos);
			WORD_CACHE[slot] = word;
			WORD_CACHE[slot + 2] = 0;
			return word;
		}
		return input.slice(start, pos);
	}

	/**
	 * @param {number} quote which quote opened the string
	 * @returns {void}
	 */
	readString(quote) {
		if (!this._noLocations || this._ecmaVersion < 10) {
			return base.readString.call(this, quote);
		}
		const input = this.input;
		const len = input.length;
		const quotePos = this.pos;
		const start = quotePos + 1;
		let pos = start;
		for (;;) {
			if (pos >= len) this.raise(this.start, "Unterminated string constant");
			const ch = input.charCodeAt(pos);
			if (ch === quote) break;
			if (ch === 92) {
				// any escape: restart in the cold reader (this.pos is still on the
				// quote), which cooks them all. Keeping none of that here is what keeps
				// this method small enough to inline, and an escaped string is rare.
				this.pos = quotePos;
				return this._readStringCold(quote);
			}
			if (ch === 10 || ch === 13) {
				// bare LF/CR is unterminated; LS/PS stay valid at ES2019+
				this.raise(this.start, "Unterminated string constant");
			}
			pos++;
		}
		this.pos = pos + 1;
		const valueLen = pos - start;
		this.finishToken(
			tokTypes.string,
			valueLen >= 2 && valueLen <= SLICE_CACHE_MAX_LEN
				? getShortSlice(input, start, pos)
				: input.slice(start, pos)
		);
	}

	/**
	 * @param {boolean} startsWithDot whether the literal opened with `.`
	 * @returns {void}
	 */
	readNumber(startsWithDot) {
		if (startsWithDot) return this._readNumberCold(startsWithDot);
		const input = this.input;
		const start = this.pos;
		const len = input.length;
		const first = input.charCodeAt(start);
		let pos;
		if (first === 48) {
			const c1 = start + 1 < len ? input.charCodeAt(start + 1) : 0;
			if (c1 === 46) {
				// `0.<digits>`
				pos = start + 1;
			} else if (c1 > 127 || IDENT_CHAR[c1] === 1) {
				// 0x/0o/0b, 0e…, 0n, 0_, legacy `0NN`, or `0`+identifier: acorn
				return this._readNumberCold(startsWithDot);
			} else {
				// bare `0` before punctuation/operator/whitespace/EOF
				this.pos = start + 1;
				return this.finishToken(tokTypes.num, 0);
			}
		} else if (first > 48 && first <= 57) {
			// integer digits, accumulated numerically for the integer-only case
			let value = first - 48;
			pos = start + 1;
			while (pos < len) {
				const ch = input.charCodeAt(pos);
				if (ch >= 48 && ch <= 57) {
					value = value * 10 + (ch - 48);
					pos++;
				} else {
					break;
				}
			}
			const after = pos < len ? input.charCodeAt(pos) : 0;
			if (after !== 46) {
				// no fraction: exponent, separator, bigint suffix or a trailing
				// identifier char all need acorn's full handling and exact errors
				if (
					after === 101 ||
					after === 69 ||
					after === 95 ||
					after === 110 ||
					after > 127 ||
					IDENT_CHAR[after] === 1
				) {
					return this._readNumberCold(startsWithDot);
				}
				// 15 digits always fit exactly into a double
				if (pos - start > 15) {
					return this._readNumberCold(startsWithDot);
				}
				this.pos = pos;
				return this.finishToken(tokTypes.num, value);
			}
			// a fraction follows the integer part
		} else {
			return this._readNumberCold(startsWithDot);
		}
		// decimal fraction: `pos` is at the '.'
		pos++;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			if (ch >= 48 && ch <= 57) pos++;
			else break;
		}
		const after = pos < len ? input.charCodeAt(pos) : 0;
		// exponent, a second dot, separator, bigint suffix or trailing identifier
		if (
			after === 46 ||
			after === 101 ||
			after === 69 ||
			after === 95 ||
			after === 110 ||
			after > 127 ||
			IDENT_CHAR[after] === 1
		) {
			return this._readNumberCold(startsWithDot);
		}
		this.pos = pos;
		// the cached slice also pre-warms the slot parseLiteral's raw reuses
		this.finishToken(
			tokTypes.num,
			Number.parseFloat(
				pos - start <= SLICE_CACHE_MAX_LEN
					? getShortSlice(input, start, pos)
					: input.slice(start, pos)
			)
		);
	}

	/**
	 * Whether a directive prologue starting at `start` opens with `"use strict"`.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.19.0/acorn/src/parseutil.js
	 * @param {number} start where the prologue may begin
	 * @returns {boolean} whether the code is strict
	 */
	strictDirective(start) {
		if (this.options.ecmaVersion < 5) return false;
		const input = this.input;
		for (;;) {
			// Skip whitespace and comments (acorn's `skipWhiteSpace`, sticky).
			STRICT_SKIP_WS.lastIndex = start;
			start += /** @type {RegExpExecArray} */ (STRICT_SKIP_WS.exec(input))[0]
				.length;
			STRICT_LITERAL.lastIndex = start;
			const match = STRICT_LITERAL.exec(input);
			if (!match) return false;
			if ((match[1] || match[2]) === "use strict") {
				STRICT_SKIP_WS.lastIndex = start + match[0].length;
				const spaceAfter = /** @type {RegExpExecArray} */ (
					STRICT_SKIP_WS.exec(input)
				);
				const end = spaceAfter.index + spaceAfter[0].length;
				const next = input.charAt(end);
				return (
					next === ";" ||
					next === "}" ||
					(STRICT_LINE_BREAK.test(spaceAfter[0]) &&
						!(
							/[(`.[+\-/*%<>=,?^&]/.test(next) ||
							(next === "!" && input.charAt(end + 1) === "=") ||
							(next === "i" && keywordOperatorAt(this, end))
						))
				);
			}
			start += match[0].length;

			// Skip semicolon, if any.
			STRICT_SKIP_WS.lastIndex = start;
			start += /** @type {RegExpExecArray} */ (STRICT_SKIP_WS.exec(input))[0]
				.length;
			if (input[start] === ";") start++;
		}
	}

	/** @returns {void} */
	readTmplToken() {
		if (!this._noLocations) return base.readTmplToken.call(this);
		const input = this.input;
		const start = this.pos;
		const len = input.length;
		let pos = start;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			if (ch === 96 || (ch === 36 && input.charCodeAt(pos + 1) === 123)) {
				if (
					pos === this.start &&
					(this.type === tokTypes.template ||
						this.type === tokTypes.invalidTemplate)
				) {
					if (ch === 36) {
						this.pos = pos + 2;
						return this.finishToken(tokTypes.dollarBraceL);
					}
					this.pos = pos + 1;
					return this.finishToken(tokTypes.backQuote);
				}
				this.pos = pos;
				return this.finishToken(tokTypes.template, input.slice(start, pos));
			}
			// backslash and CR need the cooked-string building of the cold reader
			if (ch === 92 || ch === 13) {
				return this._readTmplTokenCold();
			}
			pos++;
		}
		this.raise(this.start, "Unterminated template");
	}

	/** @returns {void} */
	skipSpace() {
		if (!this._noLocations) return base.skipSpace.call(this);
		const input = this.input;
		const len = input.length;
		let pos = this.pos;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			// 9-13 and 32 cover tab, LF, VT, FF, CR and space
			if (ch === 32 || (ch > 8 && ch < 14)) {
				pos++;
			} else if (ch === 47 || ch > 127) {
				// comments or unicode whitespace: hand off to the cold reader
				this.pos = pos;
				return this._skipSpaceCold();
			} else {
				break;
			}
		}
		this.pos = pos;
	}

	// ----- cold-path readers (ported from acorn's tokenizer, reached only from
	// the lazy fast paths above; non-lazy mode keeps acorn's own prototype
	// methods so plugins that patch them still see acorn's tokenizer) -----

	/**
	 * acorn `readInt`: reads `radix`-base digits (optionally exactly `len` of
	 * them), honoring ES2021 `_` separators.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {number} radix numeric radix
	 * @param {number=} len fixed digit count (for escapes), else read greedily
	 * @param {boolean=} maybeLegacyOctal whether a leading zero disallows separators
	 * @returns {number | null} the integer value, or null when no digits matched
	 */
	_readInt(radix, len, maybeLegacyOctal) {
		const input = this.input;
		const allowSeparators = this._ecmaVersion >= 12 && len === undefined;
		const isLegacyOctal =
			maybeLegacyOctal === true && input.charCodeAt(this.pos) === 48;
		const start = this.pos;
		let total = 0;
		let lastCode = 0;
		for (
			let i = 0, e = len === undefined ? Infinity : len;
			i < e;
			++i, ++this.pos
		) {
			const code = input.charCodeAt(this.pos);
			let val;
			if (allowSeparators && code === 95) {
				if (isLegacyOctal) {
					this.raiseRecoverable(
						this.pos,
						"Numeric separator is not allowed in legacy octal numeric literals"
					);
				}
				if (lastCode === 95) {
					this.raiseRecoverable(
						this.pos,
						"Numeric separator must be exactly one underscore"
					);
				}
				if (i === 0) {
					this.raiseRecoverable(
						this.pos,
						"Numeric separator is not allowed at the first of digits"
					);
				}
				lastCode = code;
				continue;
			}
			if (code >= 97) val = code - 97 + 10;
			else if (code >= 65) val = code - 65 + 10;
			else if (code >= 48 && code <= 57) val = code - 48;
			else val = Infinity;
			if (val >= radix) break;
			lastCode = code;
			total = total * radix + val;
		}
		if (allowSeparators && lastCode === 95) {
			this.raiseRecoverable(
				this.pos - 1,
				"Numeric separator is not allowed at the last of digits"
			);
		}
		if (this.pos === start || (len !== undefined && this.pos - start !== len)) {
			return null;
		}
		return total;
	}

	/**
	 * acorn `readRadixNumber`: `0x`/`0o`/`0b` integer or bigint literals.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {number} radix numeric radix (2/8/16)
	 * @returns {void}
	 */
	_readRadixNumber(radix) {
		const input = this.input;
		const start = this.pos;
		this.pos += 2;
		/** @type {number | bigint | null} */
		let val = this._readInt(radix);
		if (val === null) {
			this.raise(this.start + 2, `Expected number in radix ${radix}`);
		}
		if (this._ecmaVersion >= 11 && input.charCodeAt(this.pos) === 110) {
			val = stringToBigInt(input.slice(start, this.pos));
			++this.pos;
		} else if (isIdentifierStart(this.fullCharCodeAtPos())) {
			this.raise(this.pos, "Identifier directly after number");
		}
		return this.finishToken(tokTypes.num, val);
	}

	/**
	 * acorn `readNumber`: decimal/float/legacy-octal/bigint literals (the fast
	 * path handles only plain integers and simple fractions).
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {boolean} startsWithDot whether the literal started with `.`
	 * @returns {void}
	 */
	_readNumberCold(startsWithDot) {
		const input = this.input;
		const start = this.pos;
		if (!startsWithDot && this._readInt(10, undefined, true) === null) {
			this.raise(start, "Invalid number");
		}
		let octal = this.pos - start >= 2 && input.charCodeAt(start) === 48;
		if (octal && this.strict) this.raise(start, "Invalid number");
		let next = input.charCodeAt(this.pos);
		if (!octal && !startsWithDot && this._ecmaVersion >= 11 && next === 110) {
			const val = stringToBigInt(input.slice(start, this.pos));
			++this.pos;
			if (isIdentifierStart(this.fullCharCodeAtPos())) {
				this.raise(this.pos, "Identifier directly after number");
			}
			return this.finishToken(tokTypes.num, val);
		}
		if (octal && /[89]/.test(input.slice(start, this.pos))) octal = false;
		if (next === 46 && !octal) {
			++this.pos;
			this._readInt(10);
			next = input.charCodeAt(this.pos);
		}
		if ((next === 69 || next === 101) && !octal) {
			next = input.charCodeAt(++this.pos);
			if (next === 43 || next === 45) ++this.pos;
			if (this._readInt(10) === null) this.raise(start, "Invalid number");
		}
		if (isIdentifierStart(this.fullCharCodeAtPos())) {
			this.raise(this.pos, "Identifier directly after number");
		}
		const val = stringToNumber(input.slice(start, this.pos), octal);
		return this.finishToken(tokTypes.num, val);
	}

	/**
	 * acorn `readCodePoint`: a `\u{...}` or `\uXXXX` escape.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @returns {number} the escaped code point
	 */
	_readCodePoint() {
		const input = this.input;
		const ch = input.charCodeAt(this.pos);
		let code;
		if (ch === 123) {
			if (this._ecmaVersion < 6) this.unexpected();
			const codePos = ++this.pos;
			code = this._readHexChar(input.indexOf("}", this.pos) - this.pos);
			++this.pos;
			if (code > 0x10ffff) {
				this.invalidStringToken(codePos, "Code point out of bounds");
			}
		} else {
			code = this._readHexChar(4);
		}
		return code;
	}

	/**
	 * acorn `readHexChar`: exactly `len` hex digits.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {number} len number of hex digits
	 * @returns {number} the parsed code
	 */
	_readHexChar(len) {
		const codePos = this.pos;
		const n = this._readInt(16, len);
		if (n === null) {
			this.invalidStringToken(codePos, "Bad character escape sequence");
		}
		return /** @type {number} */ (n);
	}

	/**
	 * acorn `readString`: the escape-bearing string reader (the fast path cooks
	 * only the common single-char escapes inline).
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {number} quote quote char code
	 * @returns {void}
	 */
	_readStringCold(quote) {
		const input = this.input;
		let out = "";
		let chunkStart = ++this.pos;
		for (;;) {
			if (this.pos >= input.length) {
				this.raise(this.start, "Unterminated string constant");
			}
			const ch = input.charCodeAt(this.pos);
			if (ch === quote) break;
			if (ch === 92) {
				out += input.slice(chunkStart, this.pos);
				out += this._readEscapedChar(false);
				chunkStart = this.pos;
			} else if (ch === 0x2028 || ch === 0x2029) {
				if (this._ecmaVersion < 10) {
					this.raise(this.start, "Unterminated string constant");
				}
				++this.pos;
			} else {
				if (isNewLine(ch)) {
					this.raise(this.start, "Unterminated string constant");
				}
				++this.pos;
			}
		}
		out += input.slice(chunkStart, this.pos++);
		return this.finishToken(tokTypes.string, out);
	}

	/**
	 * acorn `readTmplToken`: the escape/CR-normalizing template reader. Invalid
	 * escapes throw acorn's sentinel via `invalidStringToken`, which acorn's own
	 * `tryReadTemplateToken` (not overridden here) catches to re-read raw.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @returns {void}
	 */
	_readTmplTokenCold() {
		const input = this.input;
		let out = "";
		let chunkStart = this.pos;
		for (;;) {
			if (this.pos >= input.length) {
				this.raise(this.start, "Unterminated template");
			}
			const ch = input.charCodeAt(this.pos);
			if (ch === 96 || (ch === 36 && input.charCodeAt(this.pos + 1) === 123)) {
				if (
					this.pos === this.start &&
					(this.type === tokTypes.template ||
						this.type === tokTypes.invalidTemplate)
				) {
					if (ch === 36) {
						this.pos += 2;
						return this.finishToken(tokTypes.dollarBraceL);
					}
					++this.pos;
					return this.finishToken(tokTypes.backQuote);
				}
				out += input.slice(chunkStart, this.pos);
				return this.finishToken(tokTypes.template, out);
			}
			if (ch === 92) {
				out += input.slice(chunkStart, this.pos);
				out += this._readEscapedChar(true);
				chunkStart = this.pos;
			} else if (isNewLine(ch)) {
				out += input.slice(chunkStart, this.pos);
				++this.pos;
				// only CR(LF) cooks to "\n"; LS/PS cook to themselves
				if (ch === 13) {
					if (input.charCodeAt(this.pos) === 10) ++this.pos;
					out += "\n";
				} else if (ch === 10) {
					out += "\n";
				} else {
					out += String.fromCharCode(ch);
				}
				chunkStart = this.pos;
			} else {
				++this.pos;
			}
		}
	}

	/**
	 * acorn `readEscapedChar`: cooks one backslash escape in a string/template.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {boolean} inTemplate whether the escape is in a template literal
	 * @returns {string} the cooked replacement (may be `""`)
	 */
	_readEscapedChar(inTemplate) {
		const input = this.input;
		let ch = input.charCodeAt(++this.pos);
		++this.pos;
		switch (ch) {
			case 110:
				return "\n";
			case 114:
				return "\r";
			case 120:
				return String.fromCharCode(this._readHexChar(2));
			case 117:
				return codePointToString(this._readCodePoint());
			case 116:
				return "\t";
			case 98:
				return "\b";
			case 118:
				return "\u000B";
			case 102:
				return "\f";
			case 13:
				if (input.charCodeAt(this.pos) === 10) ++this.pos;
			// falls through
			case 10:
				return "";
			case 56:
			case 57:
				if (this.strict) {
					this.invalidStringToken(this.pos - 1, "Invalid escape sequence");
				}
				if (inTemplate) {
					this.invalidStringToken(
						this.pos - 1,
						"Invalid escape sequence in template string"
					);
				}
			// falls through
			default:
				if (ch >= 48 && ch <= 55) {
					let octalStr = /** @type {RegExpMatchArray} */ (
						input.slice(this.pos - 1, this.pos + 2).match(/^[0-7]+/)
					)[0];
					let octal = Number.parseInt(octalStr, 8);
					if (octal > 255) {
						octalStr = octalStr.slice(0, -1);
						octal = Number.parseInt(octalStr, 8);
					}
					this.pos += octalStr.length - 1;
					ch = input.charCodeAt(this.pos);
					if (
						(octalStr !== "0" || ch === 56 || ch === 57) &&
						(this.strict || inTemplate)
					) {
						this.invalidStringToken(
							this.pos - 1 - octalStr.length,
							inTemplate
								? "Octal literal in template string"
								: "Octal literal in strict mode"
						);
					}
					return String.fromCharCode(octal);
				}
				if (isNewLine(ch)) return "";
				return String.fromCharCode(ch);
		}
	}

	/**
	 * acorn `readWord1`: the escape-/astral-aware identifier reader (the fast
	 * path handles pure-ASCII words).
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @returns {string} the (possibly escape-cooked) word
	 */
	_readWord1Cold() {
		const input = this.input;
		this.containsEsc = false;
		let word = "";
		let first = true;
		let chunkStart = this.pos;
		const astral = this._ecmaVersion >= 6;
		while (this.pos < input.length) {
			const ch = this.fullCharCodeAtPos();
			if (isIdentifierChar(ch, astral)) {
				this.pos += ch <= 0xffff ? 1 : 2;
			} else if (ch === 92) {
				this.containsEsc = true;
				word += input.slice(chunkStart, this.pos);
				const escStart = this.pos;
				if (input.charCodeAt(++this.pos) !== 117) {
					this.invalidStringToken(
						this.pos,
						"Expecting Unicode escape sequence \\uXXXX"
					);
				}
				++this.pos;
				const esc = this._readCodePoint();
				if (!(first ? isIdentifierStart : isIdentifierChar)(esc, astral)) {
					this.invalidStringToken(escStart, "Invalid Unicode escape");
				}
				word += codePointToString(esc);
				chunkStart = this.pos;
			} else {
				break;
			}
			first = false;
		}
		return word + input.slice(chunkStart, this.pos);
	}

	/**
	 * acorn `skipSpace` (location-free): unicode whitespace and comments. Only
	 * reached from the lazy fast paths, which keep no line bookkeeping.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @returns {void}
	 */
	_skipSpaceCold() {
		const input = this.input;
		const len = input.length;
		loop: while (this.pos < len) {
			const ch = input.charCodeAt(this.pos);
			switch (ch) {
				case 32:
				case 160:
					++this.pos;
					break;
				case 13:
					if (input.charCodeAt(this.pos + 1) === 10) ++this.pos;
				// falls through
				case 10:
				case 8232:
				case 8233:
					++this.pos;
					break;
				case 47:
					switch (input.charCodeAt(this.pos + 1)) {
						case 42:
							this.skipBlockComment();
							break;
						case 47:
							this.skipLineComment(2);
							break;
						default:
							break loop;
					}
					break;
				default:
					if (
						(ch > 8 && ch < 14) ||
						(ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch)))
					) {
						++this.pos;
					} else {
						break loop;
					}
			}
		}
	}

	/**
	 * acorn `readToken_numberSign` plus `getTokenFromCode`'s default: a private
	 * identifier (`#x`) or an unexpected-character error.
	 * https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/tokenize.js
	 * @param {number} code current char code
	 * @returns {void}
	 */
	_getUnknownOrPrivate(code) {
		if (code === 35 && this._ecmaVersion >= 13) {
			++this.pos;
			const next = this.fullCharCodeAtPos();
			if (isIdentifierStart(next, true) || next === 92) {
				return this.finishToken(tokTypes.privateId, this.readWord1());
			}
			this.raise(this.pos, `Unexpected character '${codePointToString(next)}'`);
		}
		this.raise(this.pos, `Unexpected character '${codePointToString(code)}'`);
	}

	// ----- word classification (Map/Set lookups, replaces acorn's regexps) -----

	// WHY: this method, `readString` and `finishToken` are each held under V8's
	// callee bytecode limit (460 at the time of writing) so TurboFan inlines them
	// — `readWord` into `nextToken`, `finishToken` at its call sites. That is
	// where this file's tokenizer speed comes from, and it is invisible: adding
	// one argument here once cost 1.4% by pushing it to 465. Before growing any
	// of the three, read the size back with
	// `node --print-bytecode --print-bytecode-filter=readWord` and keep the cold
	// arms (`_readWordIntoCache`, `_readWordUncacheable`, `_readStringCold`,
	// `_updateContext`) carrying whatever does not fit.
	/** @returns {void} */
	readWord() {
		const input = this.input;
		const start = this.pos;
		const len = input.length;
		let hash = 0;
		let pos = start;
		let cls = CLS_OTHER;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			cls = CHAR_CLASS[ch];
			// CLS_DIGIT and CLS_IDENT are the two identifier-continue classes
			if (cls > CLS_IDENT) break;
			hash = (Math.imul(hash, 33) + ch) | 0;
			pos++;
		}
		if (cls === CLS_UNICODE) {
			// an escape or a non-ASCII char: restart cold, where those rules live
			return this._finishWordSlow(this._readWord1Cold());
		}
		this.containsEsc = false;
		this.pos = pos;
		const wordLen = pos - start;
		if (wordLen < 2 || wordLen > WORD_CACHE_MAX_LEN) {
			return this._readWordUncacheable(start, pos);
		}
		const slot = (hash & WORD_CACHE_MASK) * WORD_CACHE_STRIDE;
		const cached = /** @type {string} */ (WORD_CACHE[slot]);
		// the keyword-set check comes before the char compare: it is one load, and
		// the arm below re-checks the text either way
		if (
			WORD_CACHE[slot + 2] !== this._wordLookups.id ||
			!wordMatches(input, start, cached, wordLen)
		) {
			return this._readWordIntoCache(start, pos, wordLen, slot);
		}
		const type = /** @type {TokenType} */ (WORD_CACHE[slot + 1]);
		this._wordKind = /** @type {number} */ (WORD_CACHE[slot + 3]);
		if (type !== tokTypes.name || !this._inlineFinish) {
			return this.finishToken(type, cached);
		}
		// finishToken(name) inlined: offset/type/value stores plus
		// name.updateContext (as in the owned finishToken)
		this.end = pos;
		const prevType = this.type;
		this.type = type;
		this.value = cached;
		this.exprAllowed =
			((cached === "of" && !this.exprAllowed) ||
				(cached === "yield" && this.inGeneratorContext())) &&
			prevType !== tokTypes.dot &&
			this._ecmaVersion >= 6;
	}

	/**
	 * `readWord`'s miss arm: classify the word, record it against the current
	 * keyword set and finish the token. A stale keyword set lands here too, and
	 * keeps the cached text when it still matches.
	 * @param {number} start where the word starts
	 * @param {number} end where it ends
	 * @param {number} wordLen its length
	 * @param {number} slot its base index in WORD_CACHE
	 * @returns {void}
	 */
	_readWordIntoCache(start, end, wordLen, slot) {
		const input = this.input;
		const lookups = this._wordLookups;
		const cached = /** @type {string} */ (WORD_CACHE[slot]);
		const word = wordMatches(input, start, cached, wordLen)
			? cached
			: /** @type {string} */ (WORD_CACHE[slot] = input.slice(start, end));
		const type = classifyWord(word, lookups.keywords);
		WORD_CACHE[slot + 1] = type;
		WORD_CACHE[slot + 2] = lookups.id;
		this._wordKind = WORD_CACHE[slot + 3] = wordKind(word, lookups);
		this.finishToken(type, word);
	}

	/**
	 * `readWord`'s tail for the lengths WORD_CACHE does not hold. Every keyword is
	 * 2-10 chars, so such a word is a plain name: neither `of` nor `yield`, nor
	 * reserved.
	 * @param {number} start where the word starts
	 * @param {number} end where it ends
	 * @returns {void}
	 */
	_readWordUncacheable(start, end) {
		const word = this.input.slice(start, end);
		this._wordKind = 0;
		if (!this._inlineFinish) return this._finishWordSlow(word);
		this.end = end;
		this.type = tokTypes.name;
		this.value = word;
		this.exprAllowed = false;
	}

	/**
	 * Uncached tail of `readWord`: escaped/non-ASCII, 1-char and over-long words.
	 * @param {string} word the word
	 * @returns {void}
	 */
	_finishWordSlow(word) {
		this._wordKind = -1;
		this.finishToken(classifyWord(word, this._wordLookups.keywords), word);
	}

	/**
	 * Report a name that the surrounding code may not use as an identifier.
	 * @param {Identifier} ref the identifier node
	 * @returns {void}
	 */
	checkUnreserved(ref) {
		const { start, end, name } = ref;
		// a string-literal module export name lands here (`export { "x" }`);
		// acorn no-ops on it — no keyword or reserved set contains a non-string
		if (typeof name !== "string") return;
		// every reserved word and special name below is short lowercase ASCII, so
		// most identifiers exit on this shape gate before any string compare;
		// `yield`, `await` and `arguments` all pass it
		const lookups = this._wordLookups;
		const nameLen = name.length;
		if (nameLen < 2 || nameLen > lookups.reservedMaxLen) return;
		const firstCC = name.charCodeAt(0);
		if (firstCC < 97 || firstCC > 122) return;
		// name-first ordering: acorn's `inGenerator`/`inAsync` are getters that
		// walk the scope stack, so gate them behind the cheap string compare —
		// a plain identifier never triggers them
		if (name === "yield" && this.inGenerator) {
			this.raiseRecoverable(
				start,
				"Cannot use 'yield' as identifier inside a generator"
			);
		} else if (name === "await" && this.inAsync) {
			this.raiseRecoverable(
				start,
				"Cannot use 'await' as identifier inside an async function"
			);
		}
		if (name === "arguments" && !(this.currentThisScope().flags & SCOPE_VAR)) {
			this.raiseRecoverable(
				start,
				"Cannot use 'arguments' in class field initializer"
			);
		}
		if ((name === "arguments" || name === "await") && this.inClassStaticBlock) {
			this.raise(
				start,
				`Cannot use ${name} in class static initialization block`
			);
		}
		const kind = lookups.reservedKinds.get(name);
		if (kind === undefined) return;
		if (kind === 1) {
			this.raise(start, `Unexpected keyword '${name}'`);
		}
		if (
			this.options.ecmaVersion < 6 &&
			this.input.slice(start, end).includes("\\")
		) {
			return;
		}
		if (kind === 2 || (kind === 3 && this.strict)) {
			if (name === "await" && !this.inAsync) {
				this.raiseRecoverable(
					start,
					"Cannot use keyword 'await' outside an async function"
				);
			}
			this.raiseRecoverable(start, `The keyword '${name}' is reserved`);
		}
	}

	/**
	 * Check a target that may only be an identifier or member expression, and
	 * record the binding it introduces.
	 * @param {AnyNode} expr the target
	 * @param {number=} bindingType what kind of binding it introduces
	 * @param {ClashCheck=} checkClashes where duplicate parameter names are tracked
	 * @returns {void}
	 */
	checkLValSimple(expr, bindingType, checkClashes) {
		if (bindingType === undefined) bindingType = BIND_NONE;
		const isBind = bindingType !== BIND_NONE;
		const type = expr.type;
		if (type === "Identifier") {
			const name = expr.name;
			if (this.strict) {
				const lookups = this._wordLookups;
				let reserved;
				if (this.reservedWordsStrictBind === lookups.reservedBindTest) {
					const len = name.length;
					reserved =
						len >= lookups.reservedBindMinLen &&
						len <= lookups.reservedBindMaxLen &&
						lookups.reservedBindSet.has(name);
				} else {
					reserved = this.reservedWordsStrictBind.test(name);
				}
				if (reserved) {
					this.raiseRecoverable(
						expr.start,
						`${isBind ? "Binding " : "Assigning to "}${name} in strict mode`
					);
				}
			}
			if (isBind) {
				if (bindingType === BIND_LEXICAL && name === "let") {
					this.raiseRecoverable(
						expr.start,
						"let is disallowed as a lexically bound name"
					);
				}
				if (checkClashes) {
					// the owned checkParams tracks names in a Set; acorn's passes an object
					if (checkClashes instanceof Set) {
						if (checkClashes.has(name)) {
							this.raiseRecoverable(expr.start, "Argument name clash");
						}
						checkClashes.add(name);
					} else {
						if (Object.prototype.hasOwnProperty.call(checkClashes, name)) {
							this.raiseRecoverable(expr.start, "Argument name clash");
						}
						checkClashes[name] = true;
					}
				}
				if (bindingType !== BIND_OUTSIDE) {
					this.declareName(name, bindingType, expr.start);
				}
			}
			return;
		}
		if (type === "ChainExpression") {
			this.raiseRecoverable(
				expr.start,
				"Optional chaining cannot appear in left-hand side"
			);
			return;
		}
		if (type === "MemberExpression") {
			if (isBind) {
				this.raiseRecoverable(expr.start, "Binding member expression");
			}
			return;
		}
		if (type === "ParenthesizedExpression") {
			if (isBind) {
				this.raiseRecoverable(expr.start, "Binding parenthesized expression");
			}
			return this.checkLValSimple(expr.expression, bindingType, checkClashes);
		}
		this.raise(expr.start, `${isBind ? "Binding" : "Assigning to"} rvalue`);
	}

	/**
	 * @param {AnyNode} expr the target
	 * @param {number=} bindingType what kind of binding it introduces
	 * @param {ClashCheck=} checkClashes where duplicate parameter names are tracked
	 * @returns {void}
	 */
	checkLValPattern(expr, bindingType, checkClashes) {
		const type = expr.type;
		if (type === "ObjectPattern") {
			const properties = expr.properties;
			for (let i = 0; i < properties.length; i++) {
				this.checkLValInnerPattern(properties[i], bindingType, checkClashes);
			}
		} else if (type === "ArrayPattern") {
			const elements = expr.elements;
			for (let i = 0; i < elements.length; i++) {
				const element = elements[i];
				if (element) {
					this.checkLValInnerPattern(element, bindingType, checkClashes);
				}
			}
		} else {
			this.checkLValSimple(expr, bindingType, checkClashes);
		}
	}

	/**
	 * @param {AnyNode} expr the target
	 * @param {number=} bindingType what kind of binding it introduces
	 * @param {ClashCheck=} checkClashes where duplicate parameter names are tracked
	 * @returns {void}
	 */
	checkLValInnerPattern(expr, bindingType, checkClashes) {
		const type = expr.type;
		if (type === "Property") {
			// AssignmentProperty has type === "Property"
			this.checkLValInnerPattern(
				/** @type {Pattern} */ (
					/** @type {Node & { value?: Node }} */ (expr).value
				),
				bindingType,
				checkClashes
			);
		} else if (type === "AssignmentPattern") {
			this.checkLValPattern(
				/** @type {Pattern} */ (
					/** @type {Node & { left?: Node }} */ (expr).left
				),
				bindingType,
				checkClashes
			);
		} else if (type === "RestElement") {
			this.checkLValPattern(
				/** @type {Pattern} */ (
					/** @type {Node & { argument?: Node }} */ (expr).argument
				),
				bindingType,
				checkClashes
			);
		} else {
			this.checkLValPattern(expr, bindingType, checkClashes);
		}
	}

	/** @returns {boolean} whether a semicolon may be inserted here */
	canInsertSemicolon() {
		if (this.type === tokTypes.eof || this.type === tokTypes.braceR) {
			return true;
		}
		// the owned nextToken already classified the gap; 2 (comment/unicode in
		// the gap, or a token from acorn's tokenizer) falls back to the scan
		return this._gapHasNewline();
	}

	// ----- comment collection without eager text slicing -----

	/**
	 * @param {number} startSkip how many characters open the comment
	 * @returns {void}
	 */
	skipLineComment(startSkip) {
		const comments = this._lazyComments;
		if (
			comments === undefined &&
			(this._noLocations !== true ||
				/** @type {Options} */ (this.options).onComment)
		) {
			return base.skipLineComment.call(this, startSkip);
		}
		const input = this.input;
		const start = this.pos;
		const len = input.length;
		let pos = start + startSkip;
		while (pos < len) {
			const ch = input.charCodeAt(pos);
			// LF, CR, LS, PS terminate the comment but are not part of it
			if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) break;
			pos++;
		}
		this.pos = pos;
		if (comments !== undefined) {
			comments.push(
				new LazyComment(false, start + startSkip, start, pos, input)
			);
		}
	}

	/** @returns {void} */
	skipBlockComment() {
		const comments = this._lazyComments;
		if (
			comments === undefined &&
			(this._noLocations !== true ||
				/** @type {Options} */ (this.options).onComment)
		) {
			return base.skipBlockComment.call(this);
		}
		const start = this.pos;
		const end = this.input.indexOf("*/", (this.pos += 2));
		if (end === -1) this.raise(this.pos - 2, "Unterminated comment");
		this.pos = end + 2;
		if (comments !== undefined) {
			comments.push(
				new LazyComment(true, start + 2, start, this.pos, this.input)
			);
		}
	}

	// ----- lazy range -----

	/** @returns {UnfinishedNode} a node starting at the current token */
	startNode() {
		if (!this._lazy) return base.startNode.call(this);
		return new LazyLocNode(this.start);
	}

	/**
	 * @param {number} pos where the node starts
	 * @param {PositionLike | null=} loc its start position
	 * @returns {UnfinishedNode} the node
	 */
	startNodeAt(pos, loc) {
		if (!this._lazy) return base.startNodeAt.call(this, pos, loc);
		return new LazyLocNode(pos);
	}

	/**
	 * @template {AnyNodeType} T
	 * @param {UnfinishedNode} node the node to finish
	 * @param {T} type its ESTree type
	 * @returns {NodeOf<T>} the same node
	 */
	finishNode(node, type) {
		if (!this._lazy) {
			return /** @type {NodeOf<T>} */ (base.finishNode.call(this, node, type));
		}
		node.type = type;
		node.end = this.lastTokEnd;
		return /** @type {NodeOf<T>} */ (node);
	}

	/**
	 * @template {AnyNode} T
	 * @param {T} node the node to copy
	 * @returns {T} the copy
	 */
	copyNode(node) {
		const newNode = /** @type {T} */ (
			this.startNodeAt(node.start, this.startLoc)
		);
		const from = node;
		const to = newNode;
		for (const prop in from) to[prop] = from[prop];
		return newNode;
	}

	// ----- owned node construction (single-shape nodes, replaces acorn's
	// start-empty-then-mutate flow one node type at a time) -----

	/**
	 * @param {boolean=} liberal whether a keyword may be read as a name
	 * @returns {Identifier} the identifier
	 */
	parseIdent(liberal) {
		if (!this._lazy) return base.parseIdent.call(this, liberal);
		/** @type {string} */
		let name;
		const type = this.type;
		if (type === tokTypes.name) {
			name = /** @type {string} */ (this.value);
		} else if (type.keyword) {
			// keyword as identifier (`obj.class`, `{ default: x }`)
			name = type.keyword;
			// acorn #575: class/function pushed a context this use never pops,
			// except after `.` where updateContext already ignored the keyword
			if (
				(name === "class" || name === "function") &&
				(this.lastTokEnd !== this.lastTokStart + 1 ||
					this.input.charCodeAt(this.lastTokStart) !== 46)
			) {
				this.context.pop();
			}
			this.type = tokTypes.name;
		} else {
			return base.parseIdent.call(this, liberal);
		}
		const node = /** @type {Identifier} */ (
			new IdentifierNode(this.start, this.end, name)
		);
		// the reserved kind readWord memoized for this token, before next() replaces it
		const kind = this._wordKind;
		this.next(Boolean(liberal));
		if (!liberal) {
			if (kind !== 0) this.checkUnreserved(node);
			if (node.name === "await" && !this.awaitIdentPos) {
				this.awaitIdentPos = node.start;
			}
		}
		return node;
	}

	/**
	 * @param {Expression | NodeOf<"Super">} baseExpr what the subscripts apply to
	 * @param {number} startPos where the whole expression starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {boolean} noCalls whether a call would belong to an enclosing `new`
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseSubscripts(baseExpr, startPos, startLoc, noCalls, forInit) {
		if (!this._subscriptFastPath || !this._arrowFastPath) {
			return base.parseSubscripts.call(
				this,
				baseExpr,
				startPos,
				startLoc,
				noCalls,
				forInit
			);
		}
		// acorn's probe order is (ecmaVersion, type, name, offsets, ASI,
		// potentialArrowAt); every operand is pure, so the SMI compares run
		// first and the megamorphic `.type` load only when `async` matched
		let maybeAsyncArrow =
			baseExpr.start === this.potentialArrowAt &&
			baseExpr.end - baseExpr.start === 5 &&
			this.lastTokEnd === baseExpr.end &&
			this._ecmaVersion >= 8 &&
			/** @type {Identifier} */ (baseExpr).name === "async" &&
			baseExpr.type === "Identifier" &&
			!this.canInsertSemicolon();
		let optionalChained = false;
		for (;;) {
			const element = this.parseSubscript(
				baseExpr,
				startPos,
				startLoc,
				noCalls,
				maybeAsyncArrow,
				optionalChained,
				forInit
			);
			if (
				/** @type {Expression & { optional?: boolean }} */ (element).optional
			) {
				optionalChained = true;
			}
			if (element.end === baseExpr.end || element === this._lastArrow) {
				if (optionalChained) {
					const chainNode =
						/** @type {Node & { expression?: Expression }} */
						(this.startNodeAt(startPos, startLoc));
					chainNode.expression = /** @type {Expression} */ (element);
					return /** @type {Expression} */ (
						this.finishNode(/** @type {Node} */ (chainNode), "ChainExpression")
					);
				}
				return /** @type {Expression} */ (element);
			}
			baseExpr = element;
			maybeAsyncArrow = false;
		}
	}

	/**
	 * @param {Expression | NodeOf<"Super">} baseExpr what the subscript applies to
	 * @param {number} startPos where the whole expression starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {boolean} noCalls whether a call would belong to an enclosing `new`
	 * @param {boolean} maybeAsyncArrow whether the base may head an async arrow
	 * @param {boolean} optionalChained whether an earlier link was optional
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression | NodeOf<"Super">} the expression, or the base when nothing applied
	 */
	parseSubscript(
		baseExpr,
		startPos,
		startLoc,
		noCalls,
		maybeAsyncArrow,
		optionalChained,
		forInit
	) {
		if (!this._subscriptFastPath) {
			return base.parseSubscript.call(
				this,
				baseExpr,
				startPos,
				startLoc,
				noCalls,
				maybeAsyncArrow,
				optionalChained,
				forInit
			);
		}
		const optional = this.eat(tokTypes.questionDot);
		if (noCalls && optional) {
			this.raise(
				this.lastTokStart,
				"Optional chaining cannot appear in the callee of new expressions"
			);
		}

		const computed = this.eat(tokTypes.bracketL);
		if (
			computed ||
			(optional &&
				this.type !== tokTypes.parenL &&
				this.type !== tokTypes.backQuote) ||
			this.eat(tokTypes.dot)
		) {
			/** @type {Node} */
			let property;
			if (computed) {
				property = this.parseExpression();
				this.expect(tokTypes.bracketR);
			} else if (
				this.type === tokTypes.privateId &&
				/** @type {string} */ (baseExpr.type) !== "Super"
			) {
				property = this.parsePrivateIdent();
			} else {
				property = this.parseIdent(this.options.allowReserved !== "never");
			}
			return /** @type {Expression} */ (
				new MemberExpressionNode(
					startPos,
					this.lastTokEnd,
					baseExpr,
					property,
					computed,
					optional
				)
			);
		} else if (!noCalls && this.eat(tokTypes.parenL)) {
			const refDestructuringErrors = this._acquireDestructuringErrors();
			const oldYieldPos = this.yieldPos;
			const oldAwaitPos = this.awaitPos;
			const oldAwaitIdentPos = this.awaitIdentPos;
			this.yieldPos = 0;
			this.awaitPos = 0;
			this.awaitIdentPos = 0;
			// no holes: the list is read with `allowEmpty` off
			const exprList = /** @type {(Expression | NodeOf<"SpreadElement">)[]} */ (
				this.parseExprList(tokTypes.parenR, true, false, refDestructuringErrors)
			);
			if (maybeAsyncArrow && !optional && this.shouldParseAsyncArrow()) {
				this.checkPatternErrors(refDestructuringErrors, false);
				this._releaseDestructuringErrors();
				this.checkYieldAwaitInDefaultParams();
				if (this.awaitIdentPos > 0) {
					this.raise(
						this.awaitIdentPos,
						"Cannot use 'await' as identifier inside an async function"
					);
				}
				this.yieldPos = oldYieldPos;
				this.awaitPos = oldAwaitPos;
				this.awaitIdentPos = oldAwaitIdentPos;
				return this.parseSubscriptAsyncArrow(
					startPos,
					startLoc,
					exprList,
					forInit
				);
			}
			this.checkExpressionErrors(refDestructuringErrors, true);
			this._releaseDestructuringErrors();
			this.yieldPos = oldYieldPos || this.yieldPos;
			this.awaitPos = oldAwaitPos || this.awaitPos;
			this.awaitIdentPos = oldAwaitIdentPos || this.awaitIdentPos;
			return /** @type {Expression} */ (
				new CallExpressionNode(
					startPos,
					this.lastTokEnd,
					baseExpr,
					exprList,
					optional
				)
			);
		} else if (this.type === tokTypes.backQuote) {
			if (optional || optionalChained) {
				this.raise(
					this.start,
					"Optional chaining cannot appear in the tag of tagged template expressions"
				);
			}
			const node = this.startNodeAt(startPos, startLoc);
			/** @type {Node & { tag?: Expression, quasi?: Node }} */
			(node).tag = /** @type {Expression} */ (baseExpr);
			/** @type {Node & { tag?: Expression, quasi?: Node }} */
			(node).quasi = this.parseTemplate({ isTagged: true });
			return /** @type {Expression} */ (
				this.finishNode(node, "TaggedTemplateExpression")
			);
		}
		return baseExpr;
	}

	/**
	 * @returns {Program} the parsed program
	 */
	parse() {
		if (!this._lazy || this.options.program) {
			return base.parse.call(this);
		}
		const node = /** @type {NodeInProgress<"Program">} */ (this.startNode());
		this.nextToken();
		try {
			return this.parseTopLevel(node);
		} catch (err) {
			// acorn's catchStackOverflow: translate an engine stack-overflow error
			if (
				err instanceof Error &&
				(/\bstack\b.*\b(exceeded|overflow)\b/i.test(err.message) ||
					/\btoo much recursion\b/i.test(err.message))
			) {
				this.raise(this.start, "Not enough stack space to parse input");
			}
			throw err;
		}
	}

	/**
	 * Read statements until the end of input and wrap them in a `Program`.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {NodeInProgress<"Program">} node the program node to fill
	 * @returns {Program} the finished program
	 */
	parseTopLevel(node) {
		// a `program` continuation node must be appended to and returned as-is,
		// which only acorn's copy does (base parse() reaches here via dispatch)
		if (!this._lazy || this.options.program) {
			const program = base.parseTopLevel.call(this, node);
			/** @type {Program & { [HOISTED_DECLARATIONS]?: HoistedDeclarations }} */
			(program)[HOISTED_DECLARATIONS] = this._takeHoisted(this.scopeStack[0]);
			// acorn's own loop reports no statement order, so what precedes each
			// module declaration is unknown and the record is not kept
			this._moduleDeclarations = undefined;
			return program;
		}
		/** @type {Record<string, boolean>} */
		const exports = Object.create(null);
		/** @type {(Statement | ModuleDeclaration)[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let blockMask = 0;
		/** @type {number[] | undefined} */
		let blockOverflow;
		while (this.type !== tokTypes.eof) {
			const statement = this.parseStatement(null, true, exports);
			if (declaresIntoBlock(statement)) {
				if (blockOverflow !== undefined) blockOverflow.push(count);
				else if (count <= MAX_MASKED_INDEX) blockMask |= 1 << count;
				else blockOverflow = overflowFrom(blockMask, count);
			}
			scratch[count++] = statement;
			this._previousTopLevelStatement = statement;
		}
		const body = this._releaseScratch(scratch, count);
		if (this.inModule) {
			for (const name of Object.keys(this.undefinedExports)) {
				this.raiseRecoverable(
					this.undefinedExports[name].start,
					`Export '${name}' is not defined`
				);
			}
		}
		this.adaptDirectivePrologue(body);
		this.next();
		const program = new ProgramNode(
			node.start,
			this.lastTokEnd,
			body,
			this.options.sourceType === "commonjs"
				? "script"
				: this.options.sourceType
		);
		program[HOISTED_DECLARATIONS] = this._takeHoisted(this.scopeStack[0]);
		program[MODULE_DECLARATIONS] = this._takeModuleDeclarations();
		program[BLOCK_DECLARATIONS] =
			blockOverflow === undefined ? blockMask : blockOverflow;
		return /** @type {Program} */ (program);
	}

	/**
	 * Read one statement. A statement head that is only a keyword by position —
	 * `let`, `async`, `using` — is settled by the probes above.
	 * @param {string | null=} context what encloses the statement
	 * @param {boolean=} topLevel whether it sits at the top level
	 * @param {ExportedNames=} exports where module exports are tracked
	 * @returns {Statement | ModuleDeclaration} the statement
	 */
	parseStatement(context, topLevel, exports) {
		if (!this._stmtFastPath) {
			return base.parseStatement.call(this, context, topLevel, exports);
		}
		const startType = this.type;
		switch (startType) {
			case tokTypes._var:
			case tokTypes._const: {
				const kind = /** @type {VariableKind} */ (this.value);
				if (context && kind !== "var") this.unexpected();
				return this._parseVarStatementAt(this.start, kind);
			}
			case tokTypes._if:
				return this._parseIfStatementAt(this.start);
			case tokTypes._return:
				return this._parseReturnStatementAt(this.start);
			case tokTypes.braceL:
				// the owned parseBlock never reads the started node
				return this.parseBlock(true);
			case tokTypes._function: {
				if (!this._funcStmtOwn) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				// acorn's hanging-function guard (context = single-statement position)
				if (
					context &&
					(this.strict || (context !== "if" && context !== "label"))
				) {
					this.unexpected();
				}
				// parseFunctionStatement, inlined without the started node
				const start = this.start;
				this.next();
				return /** @type {NodeOf<"FunctionDeclaration">} */ (
					this._parseFunctionAt(
						start,
						FUNC_STATEMENT | (context ? FUNC_HANGING_STATEMENT : 0),
						false,
						false,
						undefined
					)
				);
			}
			case tokTypes._break:
			case tokTypes._continue:
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				return this._parseBreakContinueStatementAt(
					this.start,
					/** @type {string} */ (startType.keyword)
				);
			case tokTypes._for:
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				return this._parseForStatementAt(this.start);
			case tokTypes._switch:
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				return this._parseSwitchStatementAt(this.start);
			case tokTypes._throw:
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				return this._parseThrowStatementAt(this.start);
			case tokTypes._try:
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				return this._parseTryStatementAt(this.start);
			case tokTypes._while:
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				return this._parseWhileStatementAt(this.start);
			case tokTypes.semi: {
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				const start = this.start;
				this.next();
				return /** @type {NodeOf<"EmptyStatement">} */ (
					new EmptyStatementNode(start, this.lastTokEnd)
				);
			}
			case tokTypes._debugger: {
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				const start = this.start;
				this.next();
				this.semicolon();
				return /** @type {NodeOf<"DebuggerStatement">} */ (
					new DebuggerStatementNode(start, this.lastTokEnd)
				);
			}
			case tokTypes._do: {
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				const start = this.start;
				this.next();
				this.labels.push(LOOP_LABEL);
				const body = /** @type {Statement} */ (this.parseStatement("do"));
				this.labels.pop();
				this.expect(tokTypes._while);
				const test = this.parseParenExpression();
				// fast-path gate guarantees ES6+, where the semicolon is optional
				this.eat(tokTypes.semi);
				return /** @type {NodeOf<"DoWhileStatement">} */ (
					new DoWhileStatementNode(start, this.lastTokEnd, body, test)
				);
			}
			case tokTypes._with: {
				if (!this._stmt2FastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				if (this.strict) this.raise(this.start, "'with' in strict mode");
				const start = this.start;
				this.next();
				const object = this.parseParenExpression();
				const body = /** @type {Statement} */ (this.parseStatement("with"));
				return /** @type {NodeOf<"WithStatement">} */ (
					new WithStatementNode(start, this.lastTokEnd, object, body)
				);
			}
			case tokTypes._export:
			case tokTypes._import: {
				if (!this._moduleDeclFastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				// import( / import. head is an expression statement, peeked without
				// acorn's skipWhiteSpace.exec allocation (gate guarantees ES11+)
				if (startType === tokTypes._import) {
					const next = this._scanGap(this.pos);
					const nextCh = this.input.charCodeAt(next);
					if (nextCh === 40 || nextCh === 46) {
						const start = this.start;
						return this._parseExpressionStatementAt(
							start,
							this.parseExpression()
						);
					}
				}
				if (!this.options.allowImportExportEverywhere) {
					if (!topLevel) {
						this.raise(
							this.start,
							"'import' and 'export' may only appear at the top level"
						);
					}
					if (!this.inModule) {
						this.raise(
							this.start,
							"'import' and 'export' may appear only with 'sourceType: module'"
						);
					}
				}
				const node = this.startNode();
				return startType === tokTypes._import
					? this.parseImport(node)
					: this.parseExport(node, exports);
			}
			case tokTypes._class: {
				if (!this._classFastPath) {
					return base.parseStatement.call(this, context, topLevel, exports);
				}
				if (context) this.unexpected();
				return /** @type {NodeOf<"ClassDeclaration">} */ (
					this.parseClass(this.startNode(), true)
				);
			}
			default: {
				if (startType === tokTypes.name) {
					const value = this.value;
					if (value === "let") {
						if (this.isLet(context)) {
							// mirrors acorn's `context && kind !== "var"` rejection
							if (context) this.unexpected();
							return this._parseVarStatementAt(this.start, "let");
						}
						// `let` as a plain identifier: expression/label tail below
					} else if (value === "async") {
						if (!this._funcStmtOwn) {
							return base.parseStatement.call(this, context, topLevel, exports);
						}
						// `async function` statement head, acorn's dispatch with the
						// started node folded away
						if (this.isAsyncFunction()) {
							// a truthy context raises, so the statement bit stands alone
							if (context) this.unexpected();
							const start = this.start;
							this.next();
							this.next();
							return /** @type {NodeOf<"FunctionDeclaration">} */ (
								this._parseFunctionAt(
									start,
									FUNC_STATEMENT,
									false,
									true,
									undefined
								)
							);
						}
						// plain `async` identifier: expression/label tail below
					} else if (value === "using" || value === "await") {
						const usingKind = this.isAwaitUsing(false)
							? "await using"
							: this.isUsing(false)
								? "using"
								: null;
						if (usingKind !== null) {
							// using declarations keep acorn's dispatch (rare)
							return base.parseStatement.call(this, context, topLevel, exports);
						}
						// plain identifier or top-level await: expression/label tail below
					}
				}
				// unambiguous expression statement, with acorn's label tail
				const start = this.start;
				const maybeName = /** @type {string} */ (this.value);
				const expr = this.parseExpression();
				if (
					startType === tokTypes.name &&
					expr.type === "Identifier" &&
					this.eat(tokTypes.colon)
				) {
					// labels are rare enough to pay for the started node
					return this.parseLabeledStatement(
						this.startNodeAt(start),
						maybeName,
						expr,
						context
					);
				}
				return this._parseExpressionStatementAt(start, expr);
			}
		}
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {string} kind which declaration keyword opened it
	 * @param {boolean=} allowMissingInitializer whether a declarator may lack one
	 * @returns {NodeOf<"VariableDeclaration">} the finished statement
	 */
	parseVarStatement(node, kind, allowMissingInitializer) {
		if (!this._lazy || !this._varIdOwn) {
			const declaration = base.parseVarStatement.call(
				this,
				node,
				kind,
				allowMissingInitializer
			);
			if (kind === "var") this._recordHoisted(declaration);
			return declaration;
		}
		return this._parseVarStatementAt(
			node.start,
			/** @type {VariableKind} */ (kind),
			allowMissingInitializer
		);
	}

	/**
	 * Statement-position `var`/`let`/`const` without a started node: the
	 * declaration lands directly on `VariableDeclarationNode`'s single shape.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @param {VariableKind} kind declaration kind
	 * @param {boolean=} allowMissingInitializer whether `const x;` is allowed
	 * @returns {NodeOf<"VariableDeclaration">} variable declaration
	 */
	_parseVarStatementAt(start, kind, allowMissingInitializer) {
		this.next();
		/** @type {NodeOf<"VariableDeclarator">[]} */
		const scratch = this._acquireScratch();
		const count = this._parseVarInto(
			scratch,
			false,
			kind,
			allowMissingInitializer
		);
		const declarations = this._releaseScratch(scratch, count);
		this.semicolon();
		const node = /** @type {NodeOf<"VariableDeclaration">} */ (
			new VariableDeclarationNode(start, this.lastTokEnd, declarations, kind)
		);
		if (kind === "var") this._recordHoisted(node);
		return node;
	}

	/**
	 * @param {UnfinishedNode} node the declaration node
	 * @param {boolean} isFor whether it heads a `for` statement
	 * @param {string} kind which declaration keyword opened it
	 * @param {boolean=} allowMissingInitializer whether a declarator may lack one
	 * @returns {UnfinishedNode} the filled declaration
	 */
	parseVar(node, isFor, kind, allowMissingInitializer) {
		if (!this._lazy || !this._varIdOwn) {
			return base.parseVar.call(
				this,
				node,
				isFor,
				kind,
				allowMissingInitializer
			);
		}
		const target =
			/** @type {NodeLike & { declarations: NodeOf<"VariableDeclarator">[] | null, kind: string }} */ (
				node
			);
		// null placeholder keeps acorn's declarations-then-kind key order while
		// the list is built in scratch (the node is a local in every caller
		// until this returns, so the placeholder is unobservable)
		target.declarations = null;
		target.kind = kind;
		/** @type {NodeOf<"VariableDeclarator">[]} */
		const scratch = this._acquireScratch();
		const count = this._parseVarInto(
			scratch,
			isFor,
			/** @type {VariableKind} */ (kind),
			allowMissingInitializer
		);
		target.declarations = this._releaseScratch(scratch, count);
		return node;
	}

	/**
	 * The declarator loop of the owned `parseVar`, shared with the owned
	 * `parseStatement`'s node-free statement path.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {NodeOf<"VariableDeclarator">[]} declarations scratch array for the declarators
	 * @param {boolean} isFor whether parsing a `for` head
	 * @param {VariableKind} kind declaration kind
	 * @param {boolean=} allowMissingInitializer whether `const x;` is allowed
	 * @returns {number} number of declarators written
	 */
	_parseVarInto(declarations, isFor, kind, allowMissingInitializer) {
		const ecmaVersion = this._ecmaVersion;
		const usingKind = kind === "using" || kind === "await using";
		let count = 0;
		for (;;) {
			const declStart = this.start;
			const id = usingKind ? this.parseIdent() : this.parseBindingAtom();
			this.checkLValPattern(
				id,
				kind === "var" ? BIND_VAR : BIND_LEXICAL,
				false
			);
			/** @type {Expression | null} */
			let init = null;
			if (this.eat(tokTypes.eq)) {
				init = this.parseMaybeAssign(isFor);
			} else if (
				!allowMissingInitializer &&
				kind === "const" &&
				!(
					this.type === tokTypes._in ||
					(ecmaVersion >= 6 && this.isContextual("of"))
				)
			) {
				this.unexpected();
			} else if (
				!allowMissingInitializer &&
				usingKind &&
				ecmaVersion >= 17 &&
				this.type !== tokTypes._in &&
				!this.isContextual("of")
			) {
				this.raise(
					this.lastTokEnd,
					`Missing initializer in ${kind} declaration`
				);
			} else if (
				!allowMissingInitializer &&
				id.type !== "Identifier" &&
				!(isFor && (this.type === tokTypes._in || this.isContextual("of")))
			) {
				this.raise(
					this.lastTokEnd,
					"Complex binding patterns require an initialization value"
				);
			}
			declarations[count++] = /** @type {NodeOf<"VariableDeclarator">} */ (
				new VariableDeclaratorNode(declStart, this.lastTokEnd, id, init)
			);
			if (!this.eat(tokTypes.comma)) break;
		}
		return count;
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {Expression} expr the expression it holds
	 * @returns {NodeOf<"ExpressionStatement">} the finished statement
	 */
	parseExpressionStatement(node, expr) {
		if (!this._lazy) {
			return base.parseExpressionStatement.call(this, node, expr);
		}
		return this._parseExpressionStatementAt(node.start, expr);
	}

	/**
	 * `parseExpressionStatement` without a started node.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @param {Expression} expr the parsed expression
	 * @returns {NodeOf<"ExpressionStatement">} expression statement
	 */
	_parseExpressionStatementAt(start, expr) {
		this.semicolon();
		return /** @type {NodeOf<"ExpressionStatement">} */ (
			new ExpressionStatementNode(start, this.lastTokEnd, expr)
		);
	}

	/**
	 * @param {boolean=} createNewLexicalScope whether the block opens a scope
	 * @param {UnfinishedNode=} node the block node
	 * @param {boolean=} exitStrict whether to leave strict mode after it
	 * @returns {NodeOf<"BlockStatement">} the finished block
	 */
	parseBlock(createNewLexicalScope, node, exitStrict) {
		if (!this._lazy) {
			return base.parseBlock.call(
				this,
				createNewLexicalScope,
				node,
				exitStrict
			);
		}
		if (createNewLexicalScope === undefined) createNewLexicalScope = true;
		const start = this.start;
		/** @type {(Statement | ModuleDeclaration)[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let blockMask = 0;
		/** @type {number[] | undefined} */
		let blockOverflow;
		this.expect(tokTypes.braceL);
		if (createNewLexicalScope) this.enterScope(0);
		while (this.type !== tokTypes.braceR) {
			const statement = this.parseStatement(null);
			if (declaresIntoBlock(statement)) {
				if (blockOverflow !== undefined) blockOverflow.push(count);
				else if (count <= MAX_MASKED_INDEX) blockMask |= 1 << count;
				else blockOverflow = overflowFrom(blockMask, count);
			}
			scratch[count++] = statement;
		}
		if (exitStrict) this.strict = false;
		this.next();
		if (createNewLexicalScope) this.exitScope();
		const block = new BlockStatementNode(
			start,
			this.lastTokEnd,
			/** @type {Statement[]} */ (this._releaseScratch(scratch, count))
		);
		block[BLOCK_DECLARATIONS] =
			blockOverflow === undefined ? blockMask : blockOverflow;
		return /** @type {NodeOf<"BlockStatement">} */ (block);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"IfStatement">} the finished statement
	 */
	parseIfStatement(node) {
		if (!this._lazy) return base.parseIfStatement.call(this, node);
		return this._parseIfStatementAt(node.start);
	}

	/**
	 * `parseIfStatement` without a started node.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"IfStatement">} if statement
	 */
	_parseIfStatementAt(start) {
		this.next();
		const test = this.parseParenExpression();
		// function declarations are allowed in branches outside strict mode
		const consequent = /** @type {Statement} */ (this.parseStatement("if"));
		const alternate = this.eat(tokTypes._else)
			? /** @type {Statement} */ (this.parseStatement("if"))
			: null;
		return /** @type {NodeOf<"IfStatement">} */ (
			new IfStatementNode(start, this.lastTokEnd, test, consequent, alternate)
		);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @returns {NodeOf<"ReturnStatement">} the finished statement
	 */
	parseReturnStatement(node) {
		if (!this._lazy) {
			if (!this.allowReturn) this._tryModuleFallback();
			return base.parseReturnStatement.call(this, node);
		}
		return this._parseReturnStatementAt(node.start);
	}

	/**
	 * In `auto` source type a top-level `return` is script-only syntax. With no
	 * module construct seen yet, switch to script in place and retain strictness
	 * required by the source or parser options.
	 * @returns {boolean} true when the parse was downgraded to script
	 */
	_tryModuleFallback() {
		if (
			!this._moduleFallback ||
			this._moduleSyntaxSeen ||
			!(this.currentVarScope().flags & SCOPE_TOP)
		) {
			return false;
		}
		this.options.allowReturnOutsideFunction = true;
		// acorn stamps Program.sourceType from this at parseTopLevel's end
		this.options.sourceType = "script";
		this.strict = this.strictDirective(0);
		this.inModule = false;
		this._moduleFallback = false;
		return true;
	}

	/**
	 * `parseReturnStatement` without a started node.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"ReturnStatement">} return statement
	 */
	_parseReturnStatementAt(start) {
		if (!this.allowReturn && !this._tryModuleFallback()) {
			this.raise(this.start, "'return' outside of function");
		}
		this.next();
		/** @type {Expression | null} */
		let argument = null;
		// `return` takes an optional argument, so eagerly look for a semicolon
		if (!(this.eat(tokTypes.semi) || this.insertSemicolon())) {
			argument = this.parseExpression();
			this.semicolon();
		}
		return /** @type {NodeOf<"ReturnStatement">} */ (
			new ReturnStatementNode(start, this.lastTokEnd, argument)
		);
	}

	/**
	 * `parseBreakContinueStatement` without a started node, landing on
	 * `BreakContinueNode`'s single shape.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @param {string} keyword `break` or `continue`
	 * @returns {NodeOf<"BreakStatement" | "ContinueStatement">} break or continue statement
	 */
	_parseBreakContinueStatementAt(start, keyword) {
		const isBreak = keyword === "break";
		this.next();
		/** @type {Identifier | null} */
		let label = null;
		if (this.eat(tokTypes.semi) || this.insertSemicolon()) {
			// no label
		} else if (this.type !== tokTypes.name) {
			this.unexpected();
		} else {
			label = this.parseIdent();
			this.semicolon();
		}
		// verify there is an actual destination to break or continue to
		const labels = this.labels;
		let i = 0;
		for (; i < labels.length; ++i) {
			const lab = labels[i];
			if (label === null || lab.name === label.name) {
				if (
					lab.kind !== null &&
					lab.kind !== undefined &&
					(isBreak || lab.kind === "loop")
				) {
					break;
				}
				if (label && isBreak) break;
			}
		}
		if (i === labels.length) this.raise(start, `Unsyntactic ${keyword}`);
		return /** @type {NodeOf<"BreakStatement" | "ContinueStatement">} */ (
			new BreakContinueNode(
				start,
				this.lastTokEnd,
				isBreak ? "BreakStatement" : "ContinueStatement",
				label
			)
		);
	}

	/**
	 * `parseForStatement` without a started node (fast-path gate guarantees
	 * `ecmaVersion >= 9`, folding acorn's version probes).
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"ForStatement" | "ForInStatement" | "ForOfStatement">} for/for-in/for-of statement
	 */
	_parseForStatementAt(start) {
		this.next();
		const awaitAt =
			this.canAwait && this.eatContextual("await") ? this.lastTokStart : -1;
		this.labels.push(LOOP_LABEL);
		this.enterScope(0);
		this.expect(tokTypes.parenL);
		if (this.type === tokTypes.semi) {
			if (awaitAt > -1) this.unexpected(awaitAt);
			return this._parseForAt(start, null);
		}
		const isLet = this.isLet();
		if (this.type === tokTypes._var || this.type === tokTypes._const || isLet) {
			const initStart = this.start;
			const kind = isLet ? "let" : /** @type {VariableKind} */ (this.value);
			this.next();
			/** @type {NodeOf<"VariableDeclarator">[]} */
			const scratch = this._acquireScratch();
			const count = this._parseVarInto(scratch, true, kind);
			const declarations = this._releaseScratch(scratch, count);
			const init = /** @type {NodeOf<"VariableDeclaration">} */ (
				new VariableDeclarationNode(
					initStart,
					this.lastTokEnd,
					declarations,
					kind
				)
			);
			if (kind === "var") this._recordHoisted(init);
			return this._parseForAfterInitAt(start, init, awaitAt);
		}
		const startsWithLet = this.isContextual("let");
		let isForOf = false;
		const usingKind = this.isUsing(true)
			? "using"
			: this.isAwaitUsing(true)
				? "await using"
				: null;
		if (usingKind) {
			const initStart = this.start;
			this.next();
			if (usingKind === "await using") {
				if (!this.canAwait) {
					this.raise(
						this.start,
						"Await using cannot appear outside of async function"
					);
				}
				this.next();
			}
			/** @type {NodeOf<"VariableDeclarator">[]} */
			const scratch = this._acquireScratch();
			const count = this._parseVarInto(scratch, true, usingKind);
			const declarations = this._releaseScratch(scratch, count);
			const init = /** @type {NodeOf<"VariableDeclaration">} */ (
				new VariableDeclarationNode(
					initStart,
					this.lastTokEnd,
					declarations,
					usingKind
				)
			);
			return this._parseForAfterInitAt(start, init, awaitAt);
		}
		const containsEsc = this.containsEsc;
		const refDestructuringErrors = this._acquireDestructuringErrors();
		const initPos = this.start;
		const init =
			awaitAt > -1
				? this.parseExprSubscripts(refDestructuringErrors, "await")
				: this.parseExpression(true, refDestructuringErrors);
		if (this.type === tokTypes._in || (isForOf = this.isContextual("of"))) {
			let isAwait = false;
			if (awaitAt > -1) {
				if (this.type === tokTypes._in) this.unexpected(awaitAt);
				isAwait = true;
			} else if (
				isForOf &&
				init.start === initPos &&
				!containsEsc &&
				init.type === "Identifier" &&
				init.name === "async"
			) {
				this.unexpected();
			}
			if (startsWithLet && isForOf) {
				this.raise(
					init.start,
					"The left-hand side of a for-of loop may not start with 'let'."
				);
			}
			this.toAssignable(init, false, refDestructuringErrors);
			this.checkLValPattern(init);
			this._releaseDestructuringErrors();
			return this._parseForInAt(start, isAwait, /** @type {Pattern} */ (init));
		}
		this.checkExpressionErrors(refDestructuringErrors, true);
		this._releaseDestructuringErrors();
		if (awaitAt > -1) this.unexpected(awaitAt);
		return this._parseForAt(start, init);
	}

	/**
	 * `parseForAfterInit` without a started node.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @param {NodeOf<"VariableDeclaration">} init parsed variable declaration
	 * @param {number} awaitAt offset of a consumed `await`, or -1
	 * @returns {NodeOf<"ForStatement" | "ForInStatement" | "ForOfStatement">} for/for-in/for-of statement
	 */
	_parseForAfterInitAt(start, init, awaitAt) {
		const target =
			/** @type {Node & { declarations: Node[], kind: string }} */ (init);
		if (
			(this.type === tokTypes._in || this.isContextual("of")) &&
			target.declarations.length === 1
		) {
			if (this.type === tokTypes._in) {
				if (
					(target.kind === "using" || target.kind === "await using") &&
					!(
						/** @type {Node & { init: Node | null }} */ (target.declarations[0])
							.init
					)
				) {
					this.raise(
						this.start,
						"Using declaration is not allowed in for-in loops"
					);
				}
				if (awaitAt > -1) this.unexpected(awaitAt);
				return this._parseForInAt(start, false, init);
			}
			return this._parseForInAt(start, awaitAt > -1, init);
		}
		if (awaitAt > -1) this.unexpected(awaitAt);
		return this._parseForAt(start, init);
	}

	/**
	 * `parseFor` without a started node, landing on `ForStatementNode`'s
	 * single shape.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @param {NodeOf<"VariableDeclaration"> | Expression | null} init init clause
	 * @returns {NodeOf<"ForStatement">} for statement
	 */
	_parseForAt(start, init) {
		this.expect(tokTypes.semi);
		const test = this.type === tokTypes.semi ? null : this.parseExpression();
		this.expect(tokTypes.semi);
		const update =
			this.type === tokTypes.parenR ? null : this.parseExpression();
		this.expect(tokTypes.parenR);
		const body = /** @type {Statement} */ (this.parseStatement("for"));
		this.exitScope();
		this.labels.pop();
		return /** @type {NodeOf<"ForStatement">} */ (
			new ForStatementNode(start, this.lastTokEnd, init, test, update, body)
		);
	}

	/**
	 * `parseForIn` without a started node, landing on `ForInStatementNode`/
	 * `ForOfStatementNode`'s single shapes.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @param {boolean} isAwait whether this is `for await`
	 * @param {NodeOf<"VariableDeclaration"> | Pattern} init loop target
	 * @returns {NodeOf<"ForInStatement" | "ForOfStatement">} for-in or for-of statement
	 */
	_parseForInAt(start, isAwait, init) {
		const isForIn = this.type === tokTypes._in;
		this.next();
		if (
			init.type === "VariableDeclaration" &&
			/** @type {Node & { init: Node | null }} */ (
				/** @type {Node & { declarations: Node[] }} */ (init).declarations[0]
			).init !== null &&
			(!isForIn ||
				this.strict ||
				/** @type {Node & { kind: string }} */ (init).kind !== "var" ||
				/** @type {Node & { id: Node }} */ (
					/** @type {Node & { declarations: Node[] }} */ (init).declarations[0]
				).id.type !== "Identifier")
		) {
			this.raise(
				init.start,
				`${
					isForIn ? "for-in" : "for-of"
				} loop variable declaration may not have an initializer`
			);
		}
		const right = isForIn ? this.parseExpression() : this.parseMaybeAssign();
		this.expect(tokTypes.parenR);
		const body = /** @type {Statement} */ (this.parseStatement("for"));
		this.exitScope();
		this.labels.pop();
		return /** @type {NodeOf<"ForInStatement" | "ForOfStatement">} */ (
			isForIn
				? new ForInStatementNode(start, this.lastTokEnd, init, right, body)
				: new ForOfStatementNode(
						start,
						this.lastTokEnd,
						isAwait,
						init,
						right,
						body
					)
		);
	}

	/**
	 * `parseSwitchStatement` without a started node, landing on
	 * `SwitchStatementNode`'s single shape with pre-shaped `SwitchCaseNode`s.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"SwitchStatement">} switch statement
	 */
	_parseSwitchStatementAt(start) {
		this.next();
		const discriminant = this.parseParenExpression();
		/** @type {NodeOf<"SwitchCase">[]} */
		const cases = [];
		this.expect(tokTypes.braceL);
		this.labels.push(SWITCH_LABEL);
		this.enterScope(SCOPE_SWITCH);
		/** @type {SwitchCaseNode | undefined} */
		let cur;
		let caseMask = 0;
		/** @type {number[] | undefined} */
		let caseOverflow;
		for (let sawDefault = false; this.type !== tokTypes.braceR;) {
			if (this.type === tokTypes._case || this.type === tokTypes._default) {
				const isCase = this.type === tokTypes._case;
				if (cur) {
					cur[BLOCK_DECLARATIONS] =
						caseOverflow === undefined ? caseMask : caseOverflow;
					this.finishNode(/** @type {Node} */ (cur), "SwitchCase");
				}
				caseMask = 0;
				caseOverflow = undefined;
				cur = new SwitchCaseNode(this.start);
				cases.push(/** @type {NodeOf<"SwitchCase">} */ (cur));
				cur.consequent = [];
				this.next();
				if (isCase) {
					cur.test = this.parseExpression();
				} else {
					if (sawDefault) {
						this.raiseRecoverable(
							this.lastTokStart,
							"Multiple default clauses"
						);
					}
					sawDefault = true;
					cur.test = null;
				}
				this.expect(tokTypes.colon);
			} else {
				if (!cur) this.unexpected();
				const consequent =
					/** @type {Node[]} */
					(cur.consequent);
				const statement = this.parseStatement(null);
				if (declaresIntoBlock(statement)) {
					const index = consequent.length;
					if (caseOverflow !== undefined) caseOverflow.push(index);
					else if (index <= MAX_MASKED_INDEX) caseMask |= 1 << index;
					else caseOverflow = overflowFrom(caseMask, index);
				}
				consequent.push(statement);
			}
		}
		this.exitScope();
		if (cur) {
			cur[BLOCK_DECLARATIONS] =
				caseOverflow === undefined ? caseMask : caseOverflow;
			this.finishNode(/** @type {Node} */ (cur), "SwitchCase");
		}
		this.next(); // closing brace
		this.labels.pop();
		return /** @type {NodeOf<"SwitchStatement">} */ (
			new SwitchStatementNode(start, this.lastTokEnd, discriminant, cases)
		);
	}

	/**
	 * `parseThrowStatement` without a started node; the illegal-newline probe
	 * runs on the tokenizer's newline flag instead of acorn's gap slice+regexp.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"ThrowStatement">} throw statement
	 */
	_parseThrowStatementAt(start) {
		this.next();
		if (this._gapHasNewline()) {
			this.raise(this.lastTokEnd, "Illegal newline after throw");
		}
		const argument = this.parseExpression();
		this.semicolon();
		return /** @type {NodeOf<"ThrowStatement">} */ (
			new ThrowStatementNode(start, this.lastTokEnd, argument)
		);
	}

	/**
	 * `parseTryStatement` without a started node, landing on
	 * `TryStatementNode`/`CatchClauseNode`'s single shapes.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"TryStatement">} try statement
	 */
	_parseTryStatementAt(start) {
		this.next();
		const block = this.parseBlock();
		/** @type {NodeOf<"CatchClause"> | null} */
		let handler = null;
		if (this.type === tokTypes._catch) {
			const clauseStart = this.start;
			this.next();
			/** @type {Node | null} */
			let param;
			if (this.eat(tokTypes.parenL)) {
				param = this.parseCatchClauseParam();
			} else {
				if (this._ecmaVersion < 10) this.unexpected();
				param = null;
				this.enterScope(0);
			}
			const body = this.parseBlock(false);
			this.exitScope();
			handler = /** @type {NodeOf<"CatchClause">} */ (
				new CatchClauseNode(clauseStart, this.lastTokEnd, param, body)
			);
		}
		const finalizer = this.eat(tokTypes._finally) ? this.parseBlock() : null;
		if (!handler && !finalizer) {
			this.raise(start, "Missing catch or finally clause");
		}
		return /** @type {NodeOf<"TryStatement">} */ (
			new TryStatementNode(start, this.lastTokEnd, block, handler, finalizer)
		);
	}

	/**
	 * `parseWhileStatement` without a started node, landing on
	 * `WhileStatementNode`'s single shape.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start statement start offset
	 * @returns {NodeOf<"WhileStatement">} while statement
	 */
	_parseWhileStatementAt(start) {
		this.next();
		const test = this.parseParenExpression();
		this.labels.push(LOOP_LABEL);
		const body = /** @type {Statement} */ (this.parseStatement("while"));
		this.labels.pop();
		return /** @type {NodeOf<"WhileStatement">} */ (
			new WhileStatementNode(start, this.lastTokEnd, test, body)
		);
	}

	/**
	 * @param {UnfinishedNode} node the statement node
	 * @param {LabelName} maybeName the label's name
	 * @param {Identifier} expr the identifier that named it
	 * @param {string | null=} context what encloses the statement
	 * @returns {NodeOf<"LabeledStatement">} the finished statement
	 */
	parseLabeledStatement(node, maybeName, expr, context) {
		if (!this._lazy) {
			return base.parseLabeledStatement.call(
				this,
				node,
				maybeName,
				expr,
				context
			);
		}
		const labels = this.labels;
		for (let i = 0; i < labels.length; i++) {
			if (labels[i].name === maybeName) {
				this.raise(expr.start, `Label '${maybeName}' is already declared`);
			}
		}
		const kind = /** @type {TokenTypeInternal} */ (this.type).isLoop
			? "loop"
			: this.type === tokTypes._switch
				? "switch"
				: null;
		for (let i = labels.length - 1; i >= 0; i--) {
			const label = labels[i];
			if (label.statementStart === node.start) {
				// update information about previous labels on this node
				label.statementStart = this.start;
				label.kind = kind;
			} else {
				break;
			}
		}
		labels.push({ name: maybeName, kind, statementStart: this.start });
		const body = /** @type {Statement} */ (
			this.parseStatement(
				context
					? !context.includes("label")
						? `${context}label`
						: context
					: "label"
			)
		);
		labels.pop();
		return /** @type {NodeOf<"LabeledStatement">} */ (
			new LabeledStatementNode(node.start, this.lastTokEnd, body, expr)
		);
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @returns {NodeOf<"SpreadElement">} the finished element
	 */
	parseSpread(refDestructuringErrors) {
		if (!this._lazy) {
			return base.parseSpread.call(this, refDestructuringErrors);
		}
		const start = this.start;
		this.next();
		const argument = this.parseMaybeAssign(false, refDestructuringErrors);
		return /** @type {NodeOf<"SpreadElement">} */ (
			new RestSpreadNode(start, this.lastTokEnd, "SpreadElement", argument)
		);
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {Expression} the expression
	 */
	parseMaybeConditional(forInit, refDestructuringErrors) {
		if (!this._lazy) {
			return base.parseMaybeConditional.call(
				this,
				forInit,
				refDestructuringErrors
			);
		}
		const startPos = this.start;
		const expr = this.parseExprOps(forInit, refDestructuringErrors);
		if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
		const isArrowAtStart = this._arrowFastPath
			? expr === this._lastArrow && expr.start === startPos
			: expr.type === "ArrowFunctionExpression" && expr.start === startPos;
		if (!isArrowAtStart && this.eat(tokTypes.question)) {
			const consequent = this.parseMaybeAssign();
			this.expect(tokTypes.colon);
			const alternate = this.parseMaybeAssign(forInit);
			return /** @type {Expression} */ (
				new ConditionalExpressionNode(
					startPos,
					this.lastTokEnd,
					expr,
					consequent,
					alternate
				)
			);
		}
		return expr;
	}

	/**
	 * @param {UnfinishedNode} node the arrow node
	 * @param {(Expression | Pattern | NodeOf<"SpreadElement">)[]} params what stood before the arrow
	 * @param {boolean} isAsync whether it is async
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"ArrowFunctionExpression">} the arrow function
	 */
	parseArrowExpression(node, params, isAsync, forInit) {
		if (!this._funcFastPath) {
			const result = base.parseArrowExpression.call(
				this,
				node,
				params,
				isAsync,
				forInit
			);
			this._lastArrow = result;
			return result;
		}
		// acorn's body with the started node replaced by the pre-shaped
		// FunctionNode (initFunction's writes are its constructor defaults)
		const functionNode =
			/** @type {NodeInProgress<"ArrowFunctionExpression">} */ (
				new FunctionNode(node.start)
			);
		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		const oldAwaitIdentPos = this.awaitIdentPos;
		this.enterScope(SCOPE_FUNCTION | (isAsync ? SCOPE_ASYNC : 0) | SCOPE_ARROW);
		functionNode.async = Boolean(isAsync);
		this.yieldPos = 0;
		this.awaitPos = 0;
		this.awaitIdentPos = 0;
		functionNode.params = /** @type {Pattern[]} */ (
			this.toAssignableList(params, true)
		);
		this.parseFunctionBody(functionNode, true, false, forInit);
		this.yieldPos = oldYieldPos;
		this.awaitPos = oldAwaitPos;
		this.awaitIdentPos = oldAwaitIdentPos;
		const result = this.finishNode(functionNode, "ArrowFunctionExpression");
		this._lastArrow = result;
		return result;
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @param {number} statement how the function was reached
	 * @param {boolean=} allowExpressionBody whether a concise body is allowed
	 * @param {boolean=} isAsync whether it is async
	 * @param {(boolean | string)=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"FunctionDeclaration"> | NodeOf<"FunctionExpression">} the finished function
	 */
	parseFunction(node, statement, allowExpressionBody, isAsync, forInit) {
		if (!this._funcFastPath) {
			return base.parseFunction.call(
				this,
				node,
				statement,
				allowExpressionBody,
				isAsync,
				forInit
			);
		}
		return this._parseFunctionAt(
			node.start,
			statement,
			allowExpressionBody === true,
			isAsync === true,
			forInit
		);
	}

	/**
	 * `parseFunction` without a started node (fast-path gate guarantees
	 * `ecmaVersion >= 9`, folding acorn's version probes).
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @param {number} start function start offset
	 * @param {number} statement FUNC_* statement bit flags
	 * @param {boolean} allowExpressionBody whether an expression body is allowed
	 * @param {boolean} isAsync whether the function is async
	 * @param {boolean | string=} forInit for-init context flag
	 * @returns {NodeOf<"FunctionDeclaration" | "FunctionExpression">} function declaration or expression
	 */
	_parseFunctionAt(start, statement, allowExpressionBody, isAsync, forInit) {
		const node =
			/** @type {Node & { id: Identifier | null, generator: boolean, async: boolean }} */ (
				new FunctionNode(start)
			);
		if (this.type === tokTypes.star && statement & FUNC_HANGING_STATEMENT) {
			this.unexpected();
		}
		node.generator = this.eat(tokTypes.star);
		node.async = isAsync;
		if (statement & FUNC_STATEMENT) {
			node.id =
				statement & FUNC_NULLABLE_ID && this.type !== tokTypes.name
					? null
					: this.parseIdent();
			if (node.id && !(statement & FUNC_HANGING_STATEMENT)) {
				// Annex B: sloppy-mode function declarations bind like vars
				this.checkLValSimple(
					node.id,
					this.strict || node.generator || node.async
						? this.treatFunctionsAsVar
							? BIND_VAR
							: BIND_LEXICAL
						: /* BIND_FUNCTION */ 3
				);
			}
		}
		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		const oldAwaitIdentPos = this.awaitIdentPos;
		this.yieldPos = 0;
		this.awaitPos = 0;
		this.awaitIdentPos = 0;
		this.enterScope(
			SCOPE_FUNCTION |
				(node.async ? SCOPE_ASYNC : 0) |
				(node.generator ? SCOPE_GENERATOR : 0)
		);
		if (!(statement & FUNC_STATEMENT)) {
			node.id = this.type === tokTypes.name ? this.parseIdent() : null;
		}
		this.parseFunctionParams(node);
		this.parseFunctionBody(node, allowExpressionBody, false, forInit);
		this.yieldPos = oldYieldPos;
		this.awaitPos = oldAwaitPos;
		this.awaitIdentPos = oldAwaitIdentPos;
		const finished = this.finishNode(
			node,
			statement & FUNC_STATEMENT ? "FunctionDeclaration" : "FunctionExpression"
		);
		if (statement & FUNC_STATEMENT) this._recordHoisted(finished);
		return finished;
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @param {boolean=} isArrowFunction whether it is an arrow
	 * @param {boolean=} isMethod whether it is a method
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {void}
	 */
	parseFunctionBody(node, isArrowFunction, isMethod, forInit) {
		if (!this._funcFastPath) {
			base.parseFunctionBody.call(
				this,
				node,
				isArrowFunction,
				isMethod,
				forInit
			);
			const body = /** @type {Node & { body: Node }} */ (node).body;
			if (body.type === "BlockStatement") {
				/** @type {Node & { [HOISTED_DECLARATIONS]?: HoistedDeclarations }} */
				(body)[HOISTED_DECLARATIONS] = !this._recordsHoisted()
					? undefined
					: this._exitedHoisted === undefined
						? NO_HOISTED_DECLARATIONS
						: this._exitedHoisted;
			}
			return;
		}
		const target =
			/** @type {Node & { id: Identifier | null, params: Node[], body: Node, expression: boolean }} */ (
				node
			);
		const isExpression = isArrowFunction && this.type !== tokTypes.braceL;
		const oldStrict = this.strict;
		let useStrict = false;
		if (isExpression) {
			target.body = this.parseMaybeAssign(forInit);
			target.expression = true;
			this.checkParams(node, false);
		} else {
			// acorn's isSimpleParamList, inlined and shared by both its call sites
			const params = target.params;
			let simpleParams = true;
			for (let i = 0; i < params.length; i++) {
				if (params[i].type !== "Identifier") {
					simpleParams = false;
					break;
				}
			}
			if (!oldStrict || !simpleParams) {
				useStrict = this.strictDirective(this.end);
				if (useStrict && !simpleParams) {
					this.raiseRecoverable(
						node.start,
						"Illegal 'use strict' directive in function with non-simple parameter list"
					);
				}
			}
			const oldLabels = this.labels;
			// pooled per nesting depth like _arrStack: one array per function body
			// otherwise, and labels are pushed, so stale entries must be dropped
			const labelsStack = this._labelsStack;
			const labelsDepth = this._labelsDepth++;
			let labels = labelsStack[labelsDepth];
			if (labels === undefined) labelsStack[labelsDepth] = labels = [];
			else labels.length = 0;
			this.labels = labels;
			if (useStrict) this.strict = true;
			this.checkParams(
				node,
				!oldStrict &&
					!useStrict &&
					!isArrowFunction &&
					!isMethod &&
					simpleParams
			);
			if (this.strict && target.id) {
				this.checkLValSimple(target.id, BIND_OUTSIDE);
			}
			target.body = this.parseBlock(false, undefined, useStrict && !oldStrict);
			target.expression = false;
			this.adaptDirectivePrologue(
				/** @type {NodeOf<"BlockStatement">} */ (target.body).body
			);
			this._labelsDepth--;
			this.labels = oldLabels;
			/** @type {Node & { [HOISTED_DECLARATIONS]?: HoistedDeclarations }} */
			(target.body)[HOISTED_DECLARATIONS] = this._takeHoisted(
				this.currentScope()
			);
			this._exitedHoisted = undefined;
		}
		this.exitScope();
		// the lazy path runs no base prototype code: `rereadYieldContext` inlined
		if (this.type === tokTypes.name && this.value === "yield") {
			this.exprAllowed = this.inGeneratorContext();
		}
	}

	/**
	 * @param {UnfinishedNode} node the function node
	 * @param {boolean} allowDuplicates whether repeated parameter names are allowed
	 * @returns {void}
	 */
	checkParams(node, allowDuplicates) {
		const params = /** @type {Pattern[]} */ (node.params);
		const names =
			allowDuplicates ||
			params.length === 0 ||
			(params.length === 1 && params[0].type === "Identifier")
				? null
				: this._takeSet();
		for (let i = 0; i < params.length; i++) {
			this.checkLValInnerPattern(params[i], BIND_VAR, names);
		}
		if (names !== null) {
			names.clear();
			this._setPool.push(names);
		}
	}

	/**
	 * Rewrite an expression as the binding pattern it turns out to be, which is
	 * only known once the `=` or `of` after it has been read.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/lval.js
	 * @template {AnyNode | null} T
	 * @param {T} node the parsed expression
	 * @param {boolean=} isBinding whether it binds rather than assigns
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @returns {T} the same node, rewritten
	 */
	toAssignable(node, isBinding, refDestructuringErrors) {
		if (!this._lazy) {
			return /** @type {T} */ (
				base.toAssignable.call(this, node, isBinding, refDestructuringErrors)
			);
		}
		if (this._ecmaVersion >= 6 && node) {
			const target =
				/** @type {Node & { name?: string, properties?: Node[], elements?: (Node | null)[], argument?: Node, value?: Node, key?: Node, kind?: string, operator?: string, left?: Node, expression?: Node }} */ (
					node
				);
			switch (node.type) {
				case "Identifier":
					if (this.inAsync && target.name === "await") {
						this.raise(
							node.start,
							"Cannot use 'await' as identifier inside an async function"
						);
					}
					break;
				case "ObjectPattern":
				case "ArrayPattern":
				case "AssignmentPattern":
				case "RestElement":
					break;
				case "ObjectExpression": {
					/** @type {NodeLike} */ (node).type = "ObjectPattern";
					if (refDestructuringErrors) {
						this.checkPatternErrors(refDestructuringErrors, true);
					}
					const properties = /** @type {Node[]} */ (target.properties);
					for (let i = 0; i < properties.length; i++) {
						const prop =
							/** @type {Node & { argument?: Node }} */
							(properties[i]);
						this.toAssignable(prop, isBinding);
						// a rest property's target must not be a nested pattern
						const restArgument = /** @type {Node | undefined} */ (
							prop.argument
						);
						if (
							prop.type === "RestElement" &&
							restArgument !== undefined &&
							(restArgument.type === "ArrayPattern" ||
								restArgument.type === "ObjectPattern")
						) {
							this.raise(
								/** @type {Node} */ (prop.argument).start,
								"Unexpected token"
							);
						}
					}
					break;
				}
				case "Property":
					// AssignmentProperty has type === "Property"
					if (target.kind !== "init") {
						this.raise(
							/** @type {Node} */ (target.key).start,
							"Object pattern can't contain getter or setter"
						);
					}
					this.toAssignable(/** @type {Node} */ (target.value), isBinding);
					break;
				case "ArrayExpression":
					/** @type {NodeLike} */ (node).type = "ArrayPattern";
					if (refDestructuringErrors) {
						this.checkPatternErrors(refDestructuringErrors, true);
					}
					this.toAssignableList(
						/** @type {Node[]} */ (target.elements),
						Boolean(isBinding)
					);
					break;
				case "SpreadElement":
					/** @type {NodeLike} */ (node).type = "RestElement";
					this.toAssignable(/** @type {Node} */ (target.argument), isBinding);
					if (
						/** @type {Node} */ (target.argument).type === "AssignmentPattern"
					) {
						this.raise(
							/** @type {Node} */ (target.argument).start,
							"Rest elements cannot have a default value"
						);
					}
					break;
				case "AssignmentExpression":
					if (target.operator !== "=") {
						this.raise(
							/** @type {Node} */ (target.left).end,
							"Only '=' operator can be used for specifying default value."
						);
					}
					/** @type {NodeLike} */ (node).type = "AssignmentPattern";
					delete target.operator;
					this.toAssignable(/** @type {Node} */ (target.left), isBinding);
					break;
				case "ParenthesizedExpression":
					this.toAssignable(
						/** @type {Node} */ (target.expression),
						isBinding,
						refDestructuringErrors
					);
					break;
				case "ChainExpression":
					this.raiseRecoverable(
						node.start,
						"Optional chaining cannot appear in left-hand side"
					);
					break;
				case "MemberExpression":
					if (!isBinding) break;
				// falls through
				default:
					this.raise(node.start, "Assigning to rvalue");
			}
		} else if (refDestructuringErrors) {
			this.checkPatternErrors(refDestructuringErrors, true);
		}
		return node;
	}

	/**
	 * @template {AnyNode | null} T
	 * @param {T[]} exprList the parsed expressions
	 * @param {boolean=} isBinding whether they bind rather than assign
	 * @returns {T[]} the same list, rewritten
	 */
	toAssignableList(exprList, isBinding) {
		if (!this._lazy) {
			return /** @type {T[]} */ (
				base.toAssignableList.call(this, exprList, isBinding)
			);
		}
		const end = exprList.length;
		for (let i = 0; i < end; i++) {
			const element = exprList[i];
			if (element) this.toAssignable(element, isBinding);
		}
		if (end) {
			const last = /** @type {Node & { argument?: Node }} */ (
				exprList[end - 1]
			);
			if (
				this._ecmaVersion === 6 &&
				isBinding &&
				last &&
				last.type === "RestElement" &&
				/** @type {Node} */ (last.argument).type !== "Identifier"
			) {
				this.unexpected(/** @type {Node} */ (last.argument).start);
			}
		}
		return exprList;
	}

	/**
	 * @param {number} startPos where the target starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {Pattern=} left the already-parsed target
	 * @returns {Pattern} the target, wrapped when it has a default
	 */
	parseMaybeDefault(startPos, startLoc, left) {
		if (!this._lazy) {
			return base.parseMaybeDefault.call(this, startPos, startLoc, left);
		}
		const target = left || this.parseBindingAtom();
		if (this._ecmaVersion < 6 || !this.eat(tokTypes.eq)) return target;
		const right = this.parseMaybeAssign();
		return /** @type {NodeOf<"AssignmentPattern">} */ (
			new AssignmentPatternNode(startPos, this.lastTokEnd, target, right)
		);
	}

	/** @returns {NodeOf<"RestElement">} the finished element */
	parseRestBinding() {
		if (!this._lazy) return base.parseRestBinding.call(this);
		const start = this.start;
		this.next();
		// a rest element inside a function parameter must be an identifier in ES6
		if (this._ecmaVersion === 6 && this.type !== tokTypes.name) {
			this.unexpected();
		}
		const argument = this.parseBindingAtom();
		return /** @type {NodeOf<"RestElement">} */ (
			new RestSpreadNode(start, this.lastTokEnd, "RestElement", argument)
		);
	}

	/** @returns {Pattern} the binding target */
	parseBindingAtom() {
		if (!this._lazy || this._ecmaVersion < 6) {
			return base.parseBindingAtom.call(this);
		}
		const type = this.type;
		if (type === tokTypes.bracketL) {
			const node = /** @type {NodeInProgress<"ArrayPattern">} */ (
				this.startNode()
			);
			this.next();
			node.elements = this.parseBindingList(tokTypes.bracketR, true, true);
			return this.finishNode(node, "ArrayPattern");
		}
		if (type === tokTypes.braceL) return this.parseObj(true);
		return this.parseIdent();
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseExprSubscripts(refDestructuringErrors, forInit) {
		if (!this._arrowFastPath) {
			return base.parseExprSubscripts.call(
				this,
				refDestructuringErrors,
				forInit
			);
		}
		const startPos = this.start;
		const startLoc = this.startLoc;
		let oldDoubleProto = -1;
		let oldShorthandAssign = -1;
		if (refDestructuringErrors) {
			oldDoubleProto = refDestructuringErrors.doubleProto;
			oldShorthandAssign = refDestructuringErrors.shorthandAssign;
			refDestructuringErrors.doubleProto =
				refDestructuringErrors.shorthandAssign = -1;
		}
		const expr = this.parseExprAtom(refDestructuringErrors, forInit);
		if (
			expr === this._lastArrow &&
			// acorn compares the last token's text against ")"; a ")" token always
			// spans exactly one char, so two offset probes replace the gap slice
			!(
				this.lastTokEnd - this.lastTokStart === 1 &&
				this.input.charCodeAt(this.lastTokStart) === 41
			)
		) {
			return expr;
		}
		const result = this.parseSubscripts(
			expr,
			startPos,
			startLoc,
			false,
			forInit
		);
		if (refDestructuringErrors) {
			if (result.end > expr.end) {
				this.checkExpressionErrors(refDestructuringErrors, true);
				if (refDestructuringErrors.parenthesizedAssign >= result.start) {
					refDestructuringErrors.parenthesizedAssign = -1;
				}
				if (refDestructuringErrors.parenthesizedBind >= result.start) {
					refDestructuringErrors.parenthesizedBind = -1;
				}
				if (refDestructuringErrors.trailingComma >= result.start) {
					refDestructuringErrors.trailingComma = -1;
				}
			}
			if (oldDoubleProto > -1) {
				refDestructuringErrors.doubleProto = oldDoubleProto;
			}
			if (oldShorthandAssign > -1) {
				refDestructuringErrors.shorthandAssign = oldShorthandAssign;
			}
		}
		return result;
	}

	/**
	 * @param {TokenType} close the token that ends the list
	 * @param {boolean} allowTrailingComma whether a trailing comma is allowed
	 * @param {boolean} allowEmpty whether holes are allowed
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {NodeOf<"ArrayExpression">["elements"]} the expressions
	 */
	parseExprList(close, allowTrailingComma, allowEmpty, refDestructuringErrors) {
		if (!this._lazy) {
			return base.parseExprList.call(
				this,
				close,
				allowTrailingComma,
				allowEmpty,
				refDestructuringErrors
			);
		}
		/** @type {(Expression | NodeOf<"SpreadElement"> | null)[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let first = true;
		while (!this.eat(close)) {
			if (!first) {
				this.expect(tokTypes.comma);
				if (allowTrailingComma && this.afterTrailingComma(close)) break;
			} else {
				first = false;
			}
			let elt;
			if (allowEmpty && this.type === tokTypes.comma) {
				elt = null;
			} else if (this.type === tokTypes.ellipsis) {
				elt = this.parseSpread(refDestructuringErrors);
				if (
					refDestructuringErrors &&
					this.type === tokTypes.comma &&
					refDestructuringErrors.trailingComma < 0
				) {
					refDestructuringErrors.trailingComma = this.start;
				}
			} else {
				elt = this.parseMaybeAssign(false, refDestructuringErrors);
			}
			scratch[count++] = elt;
		}
		return this._releaseScratch(scratch, count);
	}

	/**
	 * @param {TokenType} close the token that ends the list
	 * @param {boolean} allowEmpty whether holes are allowed
	 * @param {boolean} allowTrailingComma whether a trailing comma is allowed
	 * @param {boolean=} allowModifiers whether items may carry modifiers
	 * @returns {(Pattern | null)[]} the bound targets
	 */
	parseBindingList(close, allowEmpty, allowTrailingComma, allowModifiers) {
		if (!this._lazy) {
			return base.parseBindingList.call(
				this,
				close,
				allowEmpty,
				allowTrailingComma,
				allowModifiers
			);
		}
		/** @type {(Pattern | null)[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let first = true;
		while (!this.eat(close)) {
			if (first) first = false;
			else this.expect(tokTypes.comma);
			if (allowEmpty && this.type === tokTypes.comma) {
				scratch[count++] = null;
			} else if (allowTrailingComma && this.afterTrailingComma(close)) {
				break;
			} else if (this.type === tokTypes.ellipsis) {
				const rest = this.parseRestBinding();
				this.parseBindingListItem(rest);
				scratch[count++] = rest;
				if (this.type === tokTypes.comma) {
					this.raiseRecoverable(
						this.start,
						"Comma is not permitted after the rest element"
					);
				}
				this.expect(close);
				break;
			} else {
				scratch[count++] = this.parseAssignableListItem(allowModifiers);
			}
		}
		return this._releaseScratch(scratch, count);
	}

	/**
	 * Read a parenthesized expression, which is only known to be an arrow's
	 * parameter list once the `=>` after the closing paren is seen.
	 * @param {boolean} canBeArrow whether an arrow may start here
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseParenAndDistinguishExpression(canBeArrow, forInit) {
		if (!this._lazy || this._ecmaVersion < 6) {
			return base.parseParenAndDistinguishExpression.call(
				this,
				canBeArrow,
				forInit
			);
		}
		const startPos = this.start;
		const startLoc = this.startLoc;
		const allowTrailingComma = this._ecmaVersion >= 8;
		this.next();

		const innerStartPos = this.start;
		const innerStartLoc = this.startLoc;
		/** @type {Expression[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let first = true;
		let lastIsComma = false;
		const refDestructuringErrors = this._acquireDestructuringErrors();
		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		let spreadStart;
		this.yieldPos = 0;
		this.awaitPos = 0;
		// Do not save awaitIdentPos to allow checking awaits nested in parameters
		while (this.type !== tokTypes.parenR) {
			if (first) first = false;
			else this.expect(tokTypes.comma);
			if (
				allowTrailingComma &&
				this.afterTrailingComma(tokTypes.parenR, true)
			) {
				lastIsComma = true;
				break;
			} else if (this.type === tokTypes.ellipsis) {
				spreadStart = this.start;
				// a rest element is only valid as an arrow parameter, which is
				// what the list becomes when it is one
				scratch[count++] = /** @type {Expression} */ (
					/** @type {AnyNode} */ (this.parseParenItem(this.parseRestBinding()))
				);
				if (this.type === tokTypes.comma) {
					this.raiseRecoverable(
						this.start,
						"Comma is not permitted after the rest element"
					);
				}
				break;
			} else {
				scratch[count++] = this.parseMaybeAssign(
					false,
					refDestructuringErrors,
					/** @type {AfterLeftParse} */ (this.parseParenItem)
				);
			}
		}
		/** @type {Expression} */
		const singleExpression = scratch[0];
		/** @type {Expression[] | undefined} */
		let exprList;
		if (
			count === 1 &&
			this.shouldParseArrow === WebpackParser.prototype.shouldParseArrow
		) {
			// Save the expression before releasing its slot for nested parsing.
			this._arrDepth--;
		} else {
			exprList = this._releaseScratch(scratch, count);
		}
		const innerEndPos = this.lastTokEnd;
		this.expect(tokTypes.parenR);

		if (
			canBeArrow &&
			(exprList
				? this.shouldParseArrow(exprList)
				: !this.canInsertSemicolon()) &&
			this.eat(tokTypes.arrow)
		) {
			this.checkPatternErrors(refDestructuringErrors, false);
			this.checkYieldAwaitInDefaultParams();
			// last read of the pooled record was above, so the arrow body parse
			// below can already reuse its pool slot
			this._releaseDestructuringErrors();
			this.yieldPos = oldYieldPos;
			this.awaitPos = oldAwaitPos;
			return this.parseParenArrowList(
				startPos,
				startLoc,
				exprList || [singleExpression],
				forInit
			);
		}

		if ((exprList && exprList.length === 0) || lastIsComma) {
			this.unexpected(this.lastTokStart);
		}
		if (spreadStart) this.unexpected(spreadStart);
		this.checkExpressionErrors(refDestructuringErrors, true);
		this._releaseDestructuringErrors();
		this.yieldPos = oldYieldPos || this.yieldPos;
		this.awaitPos = oldAwaitPos || this.awaitPos;

		/** @type {Expression} */
		let val;
		if (exprList && exprList.length > 1) {
			const seq = /** @type {Node & { expressions?: Expression[] }} */ (
				this.startNodeAt(innerStartPos, innerStartLoc)
			);
			seq.expressions = exprList;
			this.finishNodeAt(
				/** @type {Node} */ (seq),
				"SequenceExpression",
				innerEndPos,
				undefined
			);
			val = /** @type {Expression} */ (seq);
		} else {
			val = exprList ? exprList[0] : singleExpression;
		}

		if (this.options.preserveParens) {
			const par = /** @type {Node & { expression?: Expression }} */ (
				this.startNodeAt(startPos, startLoc)
			);
			par.expression = val;
			return /** @type {Expression} */ (
				/** @type {Expression | ParenthesizedExpression} */ (
					this.finishNode(/** @type {Node} */ (par), "ParenthesizedExpression")
				)
			);
		}
		return val;
	}

	/**
	 * @param {TokenType} tokType the token that may close the list
	 * @param {boolean=} notNext whether to leave the token unconsumed
	 * @returns {boolean | undefined} whether a trailing comma was accepted
	 */
	afterTrailingComma(tokType, notNext) {
		if (this.type === tokType) {
			const onTrailingComma =
				/** @type {Options & { onTrailingComma?: (pos: number, loc?: Position) => void }} */
				(this.options).onTrailingComma;
			if (onTrailingComma) {
				onTrailingComma(
					this.lastTokStart,
					this.lastTokStartLoc === null ? undefined : this.lastTokStartLoc
				);
			}
			if (!notNext) this.next();
			return true;
		}
		return false;
	}

	/**
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @param {boolean=} andThrow whether to raise rather than report
	 * @returns {boolean | undefined} whether anything was recorded
	 */
	checkExpressionErrors(refDestructuringErrors, andThrow) {
		if (!refDestructuringErrors) return false;
		const shorthandAssign = refDestructuringErrors.shorthandAssign;
		const doubleProto = refDestructuringErrors.doubleProto;
		if (!andThrow) return shorthandAssign >= 0 || doubleProto >= 0;
		if (shorthandAssign >= 0) {
			this.raise(
				shorthandAssign,
				"Shorthand property assignments are valid only in destructuring patterns"
			);
		}
		if (doubleProto >= 0) {
			this.raiseRecoverable(doubleProto, "Redefinition of __proto__ property");
		}
		return false;
	}

	/**
	 * Read a full expression, including the comma operator.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/expression.js
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {Expression} the expression
	 */
	parseExpression(forInit, refDestructuringErrors) {
		if (!this._lazy) {
			return base.parseExpression.call(this, forInit, refDestructuringErrors);
		}
		try {
			const startPos = this.start;
			const startLoc = this.startLoc;
			const expr = this.parseMaybeAssign(forInit, refDestructuringErrors);
			if (this.type === tokTypes.comma) {
				const node =
					/** @type {NodeInProgress<"SequenceExpression">} */
					(this.startNodeAt(startPos, startLoc));
				node.expressions = [expr];
				while (this.eat(tokTypes.comma)) {
					node.expressions.push(
						this.parseMaybeAssign(forInit, refDestructuringErrors)
					);
				}
				return /** @type {Expression} */ (
					this.finishNode(node, "SequenceExpression")
				);
			}
			return expr;
		} catch (err) {
			// acorn's catchStackOverflow: translate an engine stack-overflow error
			if (
				err instanceof Error &&
				(/\bstack\b.*\b(exceeded|overflow)\b/i.test(err.message) ||
					/\btoo much recursion\b/i.test(err.message))
			) {
				this.raise(this.start, "Not enough stack space to parse input");
			}
			throw err;
		}
	}

	/**
	 * Read a `new` expression, whose callee takes subscripts but not a call —
	 * the argument list belongs to the `new` itself.
	 * @returns {NodeOf<"NewExpression"> | MetaProperty} the expression
	 */
	parseNew() {
		if (!this._lazy) return base.parseNew.call(this);
		if (this.containsEsc) {
			this.raiseRecoverable(this.start, "Escape sequence in keyword new");
		}
		const nodeStart = this.start;
		this.next();
		if (this._ecmaVersion >= 6 && this.type === tokTypes.dot) {
			const node =
				/** @type {NodeInProgress<"MetaProperty">} */
				(this.startNodeAt(nodeStart, undefined));
			const meta =
				/** @type {NodeInProgress<"Identifier">} */
				(this.startNodeAt(nodeStart, undefined));
			meta.name = "new";
			node.meta = this.finishNode(meta, "Identifier");
			this.next();
			const containsEsc = this.containsEsc;
			const property = this.parseIdent(true);
			node.property = property;
			if (property.name !== "target") {
				this.raiseRecoverable(
					property.start,
					"The only valid meta property for new is 'new.target'"
				);
			}
			if (containsEsc) {
				this.raiseRecoverable(
					nodeStart,
					"'new.target' must not contain escaped characters"
				);
			}
			if (!this.allowNewDotTarget) {
				this.raiseRecoverable(
					nodeStart,
					"'new.target' can only be used in functions and class static block"
				);
			}
			return this.finishNode(node, "MetaProperty");
		}
		const startPos = this.start;
		const startLoc = this.startLoc;
		const callee = this.parseSubscripts(
			this.parseExprAtom(null, false, true),
			startPos,
			startLoc,
			true,
			false
		);
		if (/** @type {string} */ (callee.type) === "Super") {
			this.raiseRecoverable(startPos, "Invalid use of 'super'");
		}
		/** @type {CallArguments} */
		const args = this.eat(tokTypes.parenL)
			? /** @type {CallArguments} */ (
					this.parseExprList(tokTypes.parenR, this._ecmaVersion >= 8, false)
				)
			: EMPTY_NEW_ARGS;
		return /** @type {NodeOf<"NewExpression">} */ (
			new NewExpressionNode(nodeStart, this.lastTokEnd, callee, args)
		);
	}

	/**
	 * @param {{ isTagged: boolean }} opts whether the template is tagged
	 * @returns {NodeOf<"TemplateElement">} the finished element
	 */
	parseTemplateElement(opts) {
		if (!this._lazy) return base.parseTemplateElement.call(this, opts);
		const start = this.start;
		/** @type {{ raw: string, cooked: string | null }} */
		let value;
		if (this.type === tokTypes.invalidTemplate) {
			if (!opts.isTagged) {
				this.raiseRecoverable(
					this.start,
					"Bad escape sequence in untagged template literal"
				);
			}
			value = {
				raw: /** @type {string} */ (this.value).replace(/\r\n?/g, "\n"),
				cooked: null
			};
		} else {
			const cooked = /** @type {string} */ (this.value);
			// every escape and CRLF cook shorter and a lone CR cooks to the LF raw
			// normalization yields: an equal-length cooked string IS the normalized raw
			value = {
				raw:
					cooked.length === this.end - this.start
						? cooked
						: this.input.slice(this.start, this.end).replace(/\r\n?/g, "\n"),
				cooked
			};
		}
		this.next();
		return /** @type {NodeOf<"TemplateElement">} */ (
			new TemplateElementNode(
				start,
				this.lastTokEnd,
				value,
				this.type === tokTypes.backQuote
			)
		);
	}

	/**
	 * @param {{ isTagged?: boolean }=} opts whether the template is tagged
	 * @returns {NodeOf<"TemplateLiteral">} the finished template
	 */
	parseTemplate(opts) {
		if (!this._lazy) {
			return base.parseTemplate.call(
				this,
				/** @type {{ isTagged: boolean }} */ (opts)
			);
		}
		const isTagged = (opts !== undefined && opts.isTagged) === true;
		const start = this.start;
		this.next();
		/** @type {Expression[]} */
		const exprScratch = this._acquireScratch();
		/** @type {NodeOf<"TemplateElement">[]} */
		const quasiScratch = this._acquireScratch();
		let exprCount = 0;
		let quasiCount = 0;
		// one options object for all chunks (only `isTagged` is ever read)
		const eltOpts = { isTagged };
		let curElt = this.parseTemplateElement(eltOpts);
		quasiScratch[quasiCount++] = curElt;
		while (!curElt.tail) {
			if (this.type === tokTypes.eof) {
				this.raise(this.pos, "Unterminated template literal");
			}
			this.expect(tokTypes.dollarBraceL);
			exprScratch[exprCount++] = this.parseExpression();
			this.expect(tokTypes.braceR);
			curElt = this.parseTemplateElement(eltOpts);
			quasiScratch[quasiCount++] = curElt;
		}
		this.next();
		const quasis = this._releaseScratch(quasiScratch, quasiCount);
		const expressions = this._releaseScratch(exprScratch, exprCount);
		return /** @type {NodeOf<"TemplateLiteral">} */ (
			new TemplateLiteralNode(start, this.lastTokEnd, expressions, quasis)
		);
	}

	/**
	 * @param {RefDestructuringErrors | undefined} refDestructuringErrors where to record deferred errors
	 * @param {boolean=} sawUnary whether a unary operator was already read
	 * @param {boolean=} incDec whether the operand belongs to `++`/`--`
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression | NodeOf<"PrivateIdentifier">} the expression
	 */
	parseMaybeUnary(refDestructuringErrors, sawUnary, incDec, forInit) {
		if (!this._lazy) {
			return base.parseMaybeUnary.call(
				this,
				refDestructuringErrors,
				sawUnary,
				incDec,
				forInit
			);
		}
		const startPos = this.start;
		const startLoc = this.startLoc;
		/** @type {Expression} */
		let expr;
		if (this.isContextual("await") && this.canAwait) {
			expr = this.parseAwait(forInit);
			sawUnary = true;
		} else if (/** @type {TokenTypeInternal} */ (this.type).prefix) {
			const update = this.type === tokTypes.incDec;
			const operator = /** @type {UnaryOperator | UpdateOperator} */ (
				this.value
			);
			this.next();
			const argument = this.parseMaybeUnary(null, true, update, forInit);
			this.checkExpressionErrors(refDestructuringErrors, true);
			if (update) {
				this.checkLValSimple(argument);
			} else if (
				this.strict &&
				operator === "delete" &&
				isLocalVariableAccess(argument)
			) {
				this.raiseRecoverable(
					startPos,
					"Deleting local variable in strict mode"
				);
			} else if (operator === "delete" && isPrivateFieldAccess(argument)) {
				this.raiseRecoverable(startPos, "Private fields can not be deleted");
			} else {
				sawUnary = true;
			}
			expr = /** @type {Expression} */ (
				new UnaryNode(
					startPos,
					this.lastTokEnd,
					update ? "UpdateExpression" : "UnaryExpression",
					operator,
					true,
					/** @type {Expression} */ (argument)
				)
			);
		} else if (!sawUnary && this.type === tokTypes.privateId) {
			if (
				(forInit || this.privateNameStack.length === 0) &&
				this.options.checkPrivateFields
			) {
				this.unexpected();
			}
			expr = /** @type {Expression} */ (
				/** @type {Expression | PrivateIdentifier} */ (this.parsePrivateIdent())
			);
			// a private name is only valid as `#x in obj`
			if (this.type !== tokTypes._in) this.unexpected();
		} else {
			expr = this.parseExprSubscripts(refDestructuringErrors, forInit);
			if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
			while (
				/** @type {TokenTypeInternal} */ (this.type).postfix &&
				!this.canInsertSemicolon()
			) {
				const operator = /** @type {UpdateOperator} */ (this.value);
				this.checkLValSimple(expr);
				this.next();
				expr = /** @type {Expression} */ (
					new UnaryNode(
						startPos,
						this.lastTokEnd,
						"UpdateExpression",
						operator,
						false,
						expr
					)
				);
			}
		}

		if (!incDec && this.type === tokTypes.starstar) {
			// acorn 8.18: `**` must not take a naked arrow function as its left
			// operand; leaving the token unconsumed makes the caller raise there
			const isArrowAtStart = this._arrowFastPath
				? expr === this._lastArrow && expr.start === startPos
				: expr.type === "ArrowFunctionExpression" && expr.start === startPos;
			if (!isArrowAtStart) {
				this.next();
				if (sawUnary) {
					this.unexpected(this.lastTokStart);
				} else {
					return this.buildBinary(
						startPos,
						startLoc,
						expr,
						this.parseMaybeUnary(null, false, false, forInit),
						"**",
						false
					);
				}
			}
		}
		return expr;
	}

	/**
	 * Collapses the commonest expression shape — a name/number/string/keyword
	 * literal or `this` whose next source character can only begin a token that
	 * cannot continue an expression — into the atom, skipping the seven-layer
	 * `parseMaybeAssign` → `parseExprAtom` descent and its destructuring-error
	 * bookkeeping (which is a net no-op for a bare atom).
	 * @param {boolean | string=} forInit for-init context flag
	 * @returns {Expression | null} the atom, or `null` when the shape does not apply
	 */
	_parseTrivialAtom(forInit) {
		const code = this.input.charCodeAt(this.end);
		if (code >= 128 || EXPRESSION_END_CHAR[code] !== 1) return null;
		const type = this.type;
		if (type === tokTypes.name) {
			// both steer the chain themselves (`yield x`, `await x`)
			const name = /** @type {string} */ (this.value);
			if (name === "yield" || name === "await") return null;
			this.potentialArrowAt = this.start;
			this.potentialArrowInForAwait = forInit === "await";
			return /** @type {Expression} */ (this.parseIdent(false));
		}
		if (type === tokTypes.num || type === tokTypes.string) {
			return /** @type {Expression} */ (
				this.parseLiteral(/** @type {string | number | bigint} */ (this.value))
			);
		}
		if (type === tokTypes._this) {
			const node = new ThisNode(this.start, this.end);
			this.next();
			return /** @type {Expression} */ (node);
		}
		if (
			type === tokTypes._null ||
			type === tokTypes._true ||
			type === tokTypes._false
		) {
			const node = new LiteralNode(
				this.start,
				this.end,
				type === tokTypes._null ? null : type === tokTypes._true,
				/** @type {string} */ (type.keyword)
			);
			this.next();
			return /** @type {Expression} */ (node);
		}
		return null;
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @param {AfterLeftParse=} afterLeftParse a hook run on the parsed left side
	 * @returns {Expression} the expression
	 */
	parseMaybeAssign(forInit, refDestructuringErrors, afterLeftParse) {
		if (!this._lazy) {
			return base.parseMaybeAssign.call(
				this,
				forInit,
				refDestructuringErrors,
				afterLeftParse
			);
		}
		if (this._exprFastPath && afterLeftParse === undefined) {
			const atom = this._parseTrivialAtom(forInit);
			if (atom !== null) return atom;
		}
		if (this.isContextual("yield")) {
			if (this.inGenerator) return this.parseYield(forInit);
			// the tokenizer assumed an expression follows `yield`, but this
			// isn't that kind of yield
			this.exprAllowed = false;
		}

		let ownDestructuringErrors = false;
		let oldParenAssign = -1;
		let oldTrailingComma = -1;
		let oldDoubleProto = -1;
		if (refDestructuringErrors) {
			oldParenAssign = refDestructuringErrors.parenthesizedAssign;
			oldTrailingComma = refDestructuringErrors.trailingComma;
			oldDoubleProto = refDestructuringErrors.doubleProto;
			refDestructuringErrors.parenthesizedAssign =
				refDestructuringErrors.trailingComma = -1;
		} else {
			refDestructuringErrors = this._acquireDestructuringErrors();
			ownDestructuringErrors = true;
		}

		const startPos = this.start;
		const startLoc = this.startLoc;
		if (this.type === tokTypes.parenL || this.type === tokTypes.name) {
			this.potentialArrowAt = this.start;
			this.potentialArrowInForAwait = forInit === "await";
		}
		let left = this.parseMaybeConditional(forInit, refDestructuringErrors);
		if (afterLeftParse) {
			left = afterLeftParse.call(this, left, startPos, startLoc);
		}
		if (/** @type {TokenTypeInternal} */ (this.type).isAssign) {
			const operator = /** @type {AssignmentOperator} */ (this.value);
			if (this.type === tokTypes.eq) {
				left = /** @type {Expression} */ (
					this.toAssignable(left, false, refDestructuringErrors)
				);
			}
			if (!ownDestructuringErrors) {
				refDestructuringErrors.parenthesizedAssign =
					refDestructuringErrors.trailingComma = -1;
				if (refDestructuringErrors.shorthandAssign >= left.start) {
					refDestructuringErrors.shorthandAssign = -1;
				}
				if (refDestructuringErrors.doubleProto >= left.start) {
					refDestructuringErrors.doubleProto = -1;
				}
			}
			if (this.type === tokTypes.eq) this.checkLValPattern(left);
			else this.checkLValSimple(left);
			// the own record's last read was above, so the nested right-side
			// parse below can already reuse its pool slot
			if (ownDestructuringErrors) this._releaseDestructuringErrors();
			this.next();
			const right = this.parseMaybeAssign(forInit);
			if (oldDoubleProto > -1) {
				refDestructuringErrors.doubleProto = oldDoubleProto;
			}
			return /** @type {Expression} */ (
				new AssignmentNode(
					startPos,
					this.lastTokEnd,
					operator,
					/** @type {NodeOf<"AssignmentExpression">["left"]} */ (left),
					right
				)
			);
		} else if (ownDestructuringErrors) {
			this.checkExpressionErrors(refDestructuringErrors, true);
			this._releaseDestructuringErrors();
		}
		if (oldParenAssign > -1) {
			refDestructuringErrors.parenthesizedAssign = oldParenAssign;
		}
		if (oldTrailingComma > -1) {
			refDestructuringErrors.trailingComma = oldTrailingComma;
		}
		return left;
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {Expression} the expression
	 */
	parseExprOps(forInit, refDestructuringErrors) {
		if (!this._lazy) {
			return base.parseExprOps.call(this, forInit, refDestructuringErrors);
		}
		const startPos = this.start;
		const startLoc = this.startLoc;
		// a private name stands only before `in`, which `parseExprOp` consumes
		const expr = /** @type {Expression} */ (
			this.parseMaybeUnary(
				refDestructuringErrors || null,
				false,
				false,
				forInit
			)
		);
		if (this.checkExpressionErrors(refDestructuringErrors)) return expr;
		const isArrow = this._arrowFastPath
			? expr === this._lastArrow && expr.start === startPos
			: expr.start === startPos && expr.type === "ArrowFunctionExpression";
		return isArrow
			? expr
			: this.parseExprOp(expr, startPos, startLoc, -1, forInit);
	}

	/**
	 * Read binary operators by precedence climbing, stopping where an operator
	 * binds less tightly than the caller is parsing.
	 * @param {Expression | NodeOf<"PrivateIdentifier">} left the left operand
	 * @param {number} leftStartPos where the left operand starts
	 * @param {PositionLike | null | undefined} leftStartLoc its start position
	 * @param {number} minPrec the precedence to stop below
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {Expression} the expression
	 */
	parseExprOp(left, leftStartPos, leftStartLoc, minPrec, forInit) {
		if (!this._lazy) {
			return base.parseExprOp.call(
				this,
				left,
				leftStartPos,
				leftStartLoc,
				minPrec,
				forInit
			);
		}
		for (;;) {
			const type = /** @type {TokenTypeInternal} */ (this.type);
			let prec = type.binop;
			// acorn's TokenType sets binop to null (never undefined) when absent
			if (
				prec === null ||
				prec <= minPrec ||
				(forInit && type === tokTypes._in)
			) {
				return /** @type {Expression} */ (left);
			}
			const logical =
				type === tokTypes.logicalOR || type === tokTypes.logicalAND;
			const coalesce = type === tokTypes.coalesce;
			if (coalesce) {
				// acorn parses `??`'s right at logical precedence so the mixing
				// check below sees any unparenthesized `&&`/`||` as a sibling
				prec = /** @type {number} */ (
					/** @type {TokenTypeInternal} */ (tokTypes.logicalAND).binop
				);
			}
			const op = /** @type {BinaryOperator | LogicalOperator} */ (this.value);
			this.next();
			const startPos = this.start;
			const right = this.parseExprOp(
				this.parseMaybeUnary(null, false, false, forInit),
				startPos,
				this.startLoc,
				prec,
				forInit
			);
			left = this.buildBinary(
				leftStartPos,
				leftStartLoc,
				left,
				right,
				op,
				logical || coalesce
			);
			if (
				(logical && this.type === tokTypes.coalesce) ||
				(coalesce &&
					(this.type === tokTypes.logicalOR ||
						this.type === tokTypes.logicalAND))
			) {
				this.raiseRecoverable(
					this.start,
					"Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses"
				);
			}
		}
	}

	/**
	 * @param {number} startPos where the expression starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {Expression | NodeOf<"PrivateIdentifier">} left the left operand
	 * @param {Expression | NodeOf<"PrivateIdentifier">} right the right operand
	 * @param {BinaryOperator | LogicalOperator} op the operator
	 * @param {boolean} logical whether it is a logical operator
	 * @returns {NodeOf<"BinaryExpression"> | NodeOf<"LogicalExpression">} the expression
	 */
	buildBinary(startPos, startLoc, left, right, op, logical) {
		if (!this._lazy) {
			return base.buildBinary.call(
				this,
				startPos,
				startLoc,
				left,
				right,
				op,
				logical
			);
		}
		const rightNode = /** @type {Node} */ (right);
		if (rightNode.type === "PrivateIdentifier") {
			this.raise(
				rightNode.start,
				"Private identifier can only be left side of binary expression"
			);
		}
		return /** @type {NodeOf<"BinaryExpression" | "LogicalExpression">} */ (
			new BinaryNode(
				startPos,
				this.lastTokEnd,
				logical ? "LogicalExpression" : "BinaryExpression",
				left,
				op,
				/** @type {Expression} */ (right)
			)
		);
	}

	/**
	 * Read an expression that no operator binds into: a token that is an
	 * expression on its own, or one that punctuation encloses.
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @param {boolean=} forNew whether an enclosing `new` reads the callee
	 * @returns {Expression | NodeOf<"Super">} the expression
	 */
	parseExprAtom(refDestructuringErrors, forInit, forNew) {
		if (!this._lazy) {
			return base.parseExprAtom.call(
				this,
				refDestructuringErrors,
				forInit,
				forNew
			);
		}
		// division in expression position: the tokenizer got confused, force a
		// regexp re-read (mirrors the top of acorn's parseExprAtom)
		if (this.type === tokTypes.slash) this.readRegexp();
		const type = this.type;
		if (type === tokTypes.name) {
			const canBeArrow = this.potentialArrowAt === this.start;
			const startPos = this.start;
			const startLoc = this.startLoc;
			const containsEsc = this.containsEsc;
			let id = this.parseIdent(false);
			// `async` compare first — almost every identifier fails it, skipping
			// the version probe (all operands are pure)
			if (
				id.name === "async" &&
				!containsEsc &&
				this._ecmaVersion >= 8 &&
				!this.canInsertSemicolon() &&
				this.eat(tokTypes._function)
			) {
				this.overrideContext(tokContexts.f_expr);
				return /** @type {NodeOf<"FunctionExpression">} */ (
					this.parseFunction(
						this.startNodeAt(startPos, startLoc),
						0,
						false,
						true,
						forInit
					)
				);
			}
			if (canBeArrow && !this.canInsertSemicolon()) {
				if (this.eat(tokTypes.arrow)) {
					return this.parseArrowExpression(
						this.startNodeAt(startPos, startLoc),
						[id],
						false,
						forInit
					);
				}
				if (
					id.name === "async" &&
					!containsEsc &&
					this._ecmaVersion >= 8 &&
					this.type === tokTypes.name &&
					(!this.potentialArrowInForAwait ||
						this.value !== "of" ||
						this.containsEsc)
				) {
					id = this.parseIdent(false);
					if (this.canInsertSemicolon() || !this.eat(tokTypes.arrow)) {
						this.unexpected();
					}
					return this.parseArrowExpression(
						this.startNodeAt(startPos, startLoc),
						[id],
						true,
						forInit
					);
				}
			}
			return id;
		}
		if (type === tokTypes.num || type === tokTypes.string) {
			return /** @type {Expression} */ (
				this.parseLiteral(/** @type {string | number | bigint} */ (this.value))
			);
		}
		if (
			type === tokTypes._null ||
			type === tokTypes._true ||
			type === tokTypes._false
		) {
			const node = new LiteralNode(
				this.start,
				this.end,
				type === tokTypes._null ? null : type === tokTypes._true,
				/** @type {string} */ (type.keyword)
			);
			this.next();
			return /** @type {Expression} */ (node);
		}
		if (type === tokTypes._this) {
			const node = new ThisNode(this.start, this.end);
			this.next();
			return /** @type {Expression} */ (node);
		}
		if (type === tokTypes.bracketL) {
			const start = this.start;
			this.next();
			const elements = this.parseExprList(
				tokTypes.bracketR,
				true,
				true,
				refDestructuringErrors
			);
			return /** @type {Expression} */ (
				new ArrayExpressionNode(start, this.lastTokEnd, elements)
			);
		}
		// the remaining hot atoms only dispatch to methods owned above, so the
		// bounce through acorn's own switch is skipped (acorn's exact bodies)
		if (type === tokTypes.parenL) {
			const canBeArrow = this.potentialArrowAt === this.start;
			const start = this.start;
			const expr = this.parseParenAndDistinguishExpression(canBeArrow, forInit);
			if (refDestructuringErrors) {
				if (
					refDestructuringErrors.parenthesizedAssign < 0 &&
					!this.isSimpleAssignTarget(expr)
				) {
					refDestructuringErrors.parenthesizedAssign = start;
				}
				if (refDestructuringErrors.parenthesizedBind < 0) {
					refDestructuringErrors.parenthesizedBind = start;
				}
			}
			return expr;
		}
		if (type === tokTypes.braceL) {
			this.overrideContext(CTX_B_EXPR);
			return /** @type {Expression} */ (
				this.parseObj(false, refDestructuringErrors)
			);
		}
		if (type === tokTypes._function) {
			if (this._funcStmtOwn) {
				const start = this.start;
				this.next();
				return /** @type {NodeOf<"FunctionExpression">} */ (
					this._parseFunctionAt(start, 0, false, false, undefined)
				);
			}
			const node = this.startNode();
			this.next();
			return /** @type {NodeOf<"FunctionExpression">} */ (
				this.parseFunction(node, 0)
			);
		}
		if (type === tokTypes._new) return this.parseNew();
		if (type === tokTypes.backQuote) {
			return /** @type {Expression} */ (this.parseTemplate());
		}
		if (type === tokTypes._class) {
			return /** @type {Expression} */ (
				this.parseClass(this.startNode(), false)
			);
		}
		if (type === tokTypes._import && this._ecmaVersion >= 11) {
			return this.parseExprImport(forNew === true);
		}
		if (type === tokTypes._super) {
			if (!this.allowSuper) {
				this.raise(this.start, "'super' keyword outside a method");
			}
			const node = this.startNode();
			this.next();
			if (this.type === tokTypes.parenL && !this.allowDirectSuper) {
				this.raise(
					node.start,
					"super() call outside constructor of a subclass"
				);
			}
			// `super` may only start super[expr], super.name or super(args)
			if (
				this.type !== tokTypes.dot &&
				this.type !== tokTypes.bracketL &&
				this.type !== tokTypes.parenL
			) {
				this.unexpected();
			}
			return /** @type {Expression} */ (
				/** @type {Expression | Super} */ (this.finishNode(node, "Super"))
			);
		}
		if (type === tokTypes.regexp) {
			const value =
				/** @type {{ pattern: string, flags: string, value: RegExp | null }} */ (
					this.value
				);
			const node =
				/** @type {Expression & { regex?: { pattern: string, flags: string } }} */ (
					this.parseLiteral(value.value)
				);
			node.regex = { pattern: value.pattern, flags: value.flags };
			return node;
		}
		return base.parseExprAtom.call(
			this,
			refDestructuringErrors,
			forInit,
			forNew
		);
	}

	/**
	 * @template {BindsPattern} P
	 * @param {P} isPattern whether it binds rather than builds a value
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {ObjectNodeFor<P>} the object literal or pattern
	 */
	parseObj(isPattern, refDestructuringErrors) {
		if (!this._subscriptFastPath) {
			return /** @type {ObjectNodeFor<P>} */ (
				base.parseObj.call(this, isPattern, refDestructuringErrors)
			);
		}
		const start = this.start;
		let first = true;
		// `checkPropClash` only touches `.proto`, so records pool by depth like
		// `_deStack` (a raise aborts the parse: no unwind); overrides get a fresh `{}`
		const pooled = this._propHashFastPath;
		/** @type {{ proto?: boolean }} */
		let propHash;
		if (pooled) {
			const stack = this._propHashStack;
			const depth = this._propHashDepth++;
			const cached = stack[depth];
			if (cached !== undefined) {
				cached.proto = false;
				propHash = cached;
			} else {
				propHash = stack[depth] = { proto: false };
			}
		} else {
			propHash = {};
		}
		/** @type {(NodeOf<"Property"> | NodeOf<"SpreadElement"> | NodeOf<"RestElement">)[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		this.next();
		while (!this.eat(tokTypes.braceR)) {
			if (!first) {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) break;
			} else {
				first = false;
			}
			const prop = this.parseProperty(isPattern, refDestructuringErrors);
			if (!isPattern) {
				this.checkPropClash(
					/** @type {NodeOf<"Property" | "SpreadElement">} */ (prop),
					propHash,
					refDestructuringErrors
				);
			}
			scratch[count++] = prop;
		}
		if (pooled) this._propHashDepth--;
		return /** @type {ObjectNodeFor<P>} */ (
			new ObjectNode(
				start,
				this.lastTokEnd,
				isPattern ? "ObjectPattern" : "ObjectExpression",
				/** @type {NodeOf<"ObjectExpression" | "ObjectPattern">["properties"]} */ (
					this._releaseScratch(scratch, count)
				)
			)
		);
	}

	/**
	 * @param {boolean} isPattern whether it binds rather than builds a value
	 * @param {RefDestructuringErrors=} refDestructuringErrors where to record deferred errors
	 * @returns {NodeOf<"Property"> | NodeOf<"SpreadElement"> | NodeOf<"RestElement">} the property
	 */
	parseProperty(isPattern, refDestructuringErrors) {
		if (!this._subscriptFastPath) {
			return base.parseProperty.call(this, isPattern, refDestructuringErrors);
		}
		const nodeStart = this.start;
		if (this.eat(tokTypes.ellipsis)) {
			if (isPattern) {
				const argument = this.parseIdent(false);
				if (this.type === tokTypes.comma) {
					this.raiseRecoverable(
						this.start,
						"Comma is not permitted after the rest element"
					);
				}
				return /** @type {NodeOf<"RestElement">} */ (
					new RestSpreadNode(
						nodeStart,
						this.lastTokEnd,
						"RestElement",
						argument
					)
				);
			}
			const argument = this.parseMaybeAssign(false, refDestructuringErrors);
			// disallow trailing comma via `this.toAssignable()`
			if (
				this.type === tokTypes.comma &&
				refDestructuringErrors &&
				refDestructuringErrors.trailingComma < 0
			) {
				refDestructuringErrors.trailingComma = this.start;
			}
			return /** @type {NodeOf<"SpreadElement">} */ (
				new RestSpreadNode(
					nodeStart,
					this.lastTokEnd,
					"SpreadElement",
					argument
				)
			);
		}
		const prop = /** @type {KeyedNode} */ (new PropertyNode(nodeStart));
		let isGenerator = false;
		/** @type {number | undefined} */
		let startPos;
		/** @type {Position | undefined} */
		let startLoc;
		if (isPattern || refDestructuringErrors) {
			startPos = this.start;
			startLoc = this.startLoc;
		}
		if (!isPattern) isGenerator = this.eat(tokTypes.star);
		const containsEsc = this.containsEsc;
		this.parsePropertyName(prop);
		let isAsync = false;
		if (!isPattern && !containsEsc && !isGenerator && this.isAsyncProp(prop)) {
			isAsync = true;
			isGenerator = this.eat(tokTypes.star);
			this.parsePropertyName(prop);
		}
		this.parsePropertyValue(
			prop,
			isPattern,
			isGenerator,
			isAsync,
			startPos,
			startLoc,
			refDestructuringErrors,
			containsEsc
		);
		return this.finishNode(prop, "Property");
	}

	// ----- class bodies (declarations/expressions, methods, fields) -----

	/**
	 * @param {UnfinishedNode} node the class node
	 * @param {(boolean | string)=} isStatement whether it is a declaration
	 * @returns {NodeOf<"ClassDeclaration"> | NodeOf<"ClassExpression">} the finished class
	 */
	parseClass(node, isStatement) {
		if (!this._classFastPath) {
			const parsed = base.parseClass.call(this, node, isStatement);
			if (isStatement) this._recordHoisted(parsed);
			return parsed;
		}
		const classNode = /** @type {UnfinishedNode} */ (new ClassNode(node.start));
		this.next();
		// ecma-262 14.6: a class definition is always strict mode code
		const oldStrict = this.strict;
		this.strict = true;
		this.parseClassId(classNode, isStatement);
		this.parseClassSuper(classNode);
		const privateNameMap = this.enterClassBody();
		const bodyStart = this.start;
		let hadConstructor = false;
		const scratch = this._acquireScratch();
		let count = 0;
		this.expect(tokTypes.braceL);
		while (this.type !== tokTypes.braceR) {
			const element =
				/** @type {(Node & { kind?: string, static?: boolean, key?: Node & { name: string } }) | null} */ (
					this.parseClassElement(classNode.superClass !== null)
				);
			if (element) {
				scratch[count++] = element;
				if (
					element.type === "MethodDefinition" &&
					element.kind === "constructor"
				) {
					if (hadConstructor) {
						this.raiseRecoverable(
							element.start,
							"Duplicate constructor in the same class"
						);
					}
					hadConstructor = true;
				} else if (
					element.key &&
					element.key.type === "PrivateIdentifier" &&
					isPrivateNameConflicted(privateNameMap, element)
				) {
					this.raiseRecoverable(
						element.key.start,
						`Identifier '#${element.key.name}' has already been declared`
					);
				}
			}
		}
		this.strict = oldStrict;
		this.next();
		classNode.body = /** @type {NodeOf<"ClassBody">} */ (
			new ClassBodyNode(
				bodyStart,
				this.lastTokEnd,
				/** @type {NodeOf<"ClassBody">["body"]} */ (
					this._releaseScratch(scratch, count)
				)
			)
		);
		this.exitClassBody();
		const finished = this.finishNode(
			classNode,
			isStatement ? "ClassDeclaration" : "ClassExpression"
		);
		if (isStatement) this._recordHoisted(finished);
		return finished;
	}

	/**
	 * @param {boolean} constructorAllowsSuper whether the class has a superclass
	 * @returns {NodeOf<"MethodDefinition"> | NodeOf<"PropertyDefinition"> | NodeOf<"StaticBlock"> | null} the element, or `null` for a stray semicolon
	 */
	parseClassElement(constructorAllowsSuper) {
		if (!this._classFastPath) {
			return base.parseClassElement.call(this, constructorAllowsSuper);
		}
		if (this.eat(tokTypes.semi)) return null;
		const start = this.start;
		let keyName = "";
		let isGenerator = false;
		let isAsync = false;
		/** @type {"method" | "get" | "set"} */
		let kind = "method";
		let isStatic = false;
		if (this.eatContextual("static")) {
			// static initialization block
			if (this.eat(tokTypes.braceL)) {
				const block = /** @type {NodeOf<"StaticBlock">} */ (
					new StaticBlockNode(start)
				);
				this.parseClassStaticBlock(block);
				return block;
			}
			if (this.isClassElementNameStart() || this.type === tokTypes.star) {
				isStatic = true;
			} else {
				keyName = "static";
			}
		}
		if (!keyName && this.eatContextual("async")) {
			if (
				(this.isClassElementNameStart() || this.type === tokTypes.star) &&
				!this.canInsertSemicolon()
			) {
				isAsync = true;
			} else {
				keyName = "async";
			}
		}
		if (!keyName && this.eat(tokTypes.star)) {
			isGenerator = true;
		}
		if (!keyName && !isAsync && !isGenerator) {
			const lastValue = this.value;
			if (this.eatContextual("get") || this.eatContextual("set")) {
				if (this.isClassElementNameStart()) {
					kind = /** @type {"get" | "set"} */ (lastValue);
				} else {
					keyName = /** @type {string} */ (lastValue);
				}
			}
		}
		const element =
			/** @type {NodeInProgress<"MethodDefinition"> & { static: boolean, computed: boolean }} */ (
				new MethodDefinitionNode(start)
			);
		element.static = isStatic;
		if (keyName) {
			// `async`/`get`/`set`/`static` was the element name, not a modifier
			element.computed = false;
			element.key = /** @type {Identifier} */ (
				new IdentifierNode(this.lastTokStart, this.lastTokEnd, keyName)
			);
		} else {
			this.parseClassElementName(element);
		}
		if (
			this.type === tokTypes.parenL ||
			kind !== "method" ||
			isGenerator ||
			isAsync
		) {
			const isConstructor =
				!element.static &&
				checkKeyName(
					/** @type {NodeOf<"MethodDefinition">} */ (element),
					"constructor"
				);
			const allowsDirectSuper = isConstructor && constructorAllowsSuper;
			// acorn keeps this check out of parseClassMethod for backward compat
			if (isConstructor && kind !== "method") {
				this.raise(
					/** @type {Node} */ (element.key).start,
					"Constructor can't have get/set modifier"
				);
			}
			element.kind = isConstructor ? "constructor" : kind;
			this.parseClassMethod(element, isGenerator, isAsync, allowsDirectSuper);
			return /** @type {NodeOf<"MethodDefinition">} */ (element);
		}
		// field: rebuild on PropertyDefinition's own `kind`-less shape
		const field = /** @type {NodeOf<"PropertyDefinition">} */ (
			new PropertyDefinitionNode(
				start,
				element.static,
				element.computed,
				/** @type {NodeOf<"PropertyDefinition">["key"]} */ (element.key)
			)
		);
		this.parseClassField(field);
		return field;
	}

	/**
	 * @param {UnfinishedNode} element the element node
	 * @returns {void}
	 */
	parseClassElementName(element) {
		if (!this._classFastPath) {
			return base.parseClassElementName.call(this, element);
		}
		const target =
			/** @type {Node & { computed: boolean, key: Node | null }} */ (element);
		if (this.type === tokTypes.privateId) {
			if (this.value === "constructor") {
				this.raise(
					this.start,
					"Classes can't have an element named '#constructor'"
				);
			}
			target.computed = false;
			target.key = this.parsePrivateIdent();
		} else {
			this.parsePropertyName(element);
		}
	}

	/**
	 * @param {UnfinishedNode} method the method node
	 * @param {boolean} isGenerator whether it is a generator
	 * @param {boolean} isAsync whether it is async
	 * @param {boolean} allowsDirectSuper whether `super()` may be called in it
	 * @returns {NodeOf<"MethodDefinition">} the finished method
	 */
	parseClassMethod(method, isGenerator, isAsync, allowsDirectSuper) {
		if (!this._classFastPath) {
			return base.parseClassMethod.call(
				this,
				method,
				isGenerator,
				isAsync,
				allowsDirectSuper
			);
		}
		const target =
			/** @type {Node & { static: boolean, kind: string, key: Node, value: Node | null }} */ (
				method
			);
		const key = target.key;
		if (target.kind === "constructor") {
			if (isGenerator) {
				this.raise(key.start, "Constructor can't be a generator");
			}
			if (isAsync) {
				this.raise(key.start, "Constructor can't be an async method");
			}
		} else if (target.static && checkKeyName(target, "prototype")) {
			this.raise(
				key.start,
				"Classes may not have a static property named prototype"
			);
		}
		const value = /** @type {Node & { params: Node[] }} */ (
			target.value = /** @type {Node} */ (
				this.parseMethod(isGenerator, isAsync, allowsDirectSuper)
			)
		);
		if (target.kind === "get" && value.params.length !== 0) {
			this.raiseRecoverable(value.start, "getter should have no params");
		}
		if (target.kind === "set" && value.params.length !== 1) {
			this.raiseRecoverable(
				value.start,
				"setter should have exactly one param"
			);
		}
		if (target.kind === "set" && value.params[0].type === "RestElement") {
			this.raiseRecoverable(
				value.params[0].start,
				"Setter cannot use rest params"
			);
		}
		return this.finishNode(method, "MethodDefinition");
	}

	/**
	 * @param {UnfinishedNode} field the field node
	 * @returns {NodeOf<"PropertyDefinition">} the finished field
	 */
	parseClassField(field) {
		if (!this._classFastPath) {
			return base.parseClassField.call(this, field);
		}
		const target =
			/** @type {Node & { static: boolean, key: Node, value: Node | null }} */ (
				field
			);
		if (checkKeyName(target, "constructor")) {
			this.raise(
				target.key.start,
				"Classes can't have a field named 'constructor'"
			);
		} else if (target.static && checkKeyName(target, "prototype")) {
			this.raise(
				target.key.start,
				"Classes can't have a static field named 'prototype'"
			);
		}
		if (this.eat(tokTypes.eq)) {
			// a dedicated scope so `arguments` in the initializer raises
			this.enterScope(SCOPE_CLASS_FIELD_INIT | SCOPE_SUPER);
			target.value = /** @type {Node} */ (this.parseMaybeAssign());
			this.exitScope();
		} else {
			target.value = null;
		}
		this.semicolon();
		return this.finishNode(field, "PropertyDefinition");
	}

	/**
	 * @param {UnfinishedNode} node the static block node
	 * @returns {NodeOf<"StaticBlock">} the finished block
	 */
	parseClassStaticBlock(node) {
		if (!this._classFastPath) {
			return base.parseClassStaticBlock.call(this, node);
		}
		const target = /** @type {Node & { body: Node[] }} */ (node);
		target.body = [];
		const oldLabels = this.labels;
		this.labels = [];
		let blockMask = 0;
		/** @type {number[] | undefined} */
		let blockOverflow;
		this.enterScope(SCOPE_CLASS_STATIC_BLOCK | SCOPE_SUPER);
		while (this.type !== tokTypes.braceR) {
			const statement = this.parseStatement(null);
			if (declaresIntoBlock(statement)) {
				const index = target.body.length;
				if (blockOverflow !== undefined) blockOverflow.push(index);
				else if (index <= MAX_MASKED_INDEX) blockMask |= 1 << index;
				else blockOverflow = overflowFrom(blockMask, index);
			}
			target.body.push(statement);
		}
		/** @type {{ [BLOCK_DECLARATIONS]?: BlockDeclarations }} */
		(target)[BLOCK_DECLARATIONS] =
			blockOverflow === undefined ? blockMask : blockOverflow;
		this.next();
		this.exitScope();
		this.labels = oldLabels;
		return this.finishNode(node, "StaticBlock");
	}

	/**
	 * @param {UnfinishedNode} node the class node
	 * @param {(boolean | string)=} isStatement whether it is a declaration
	 * @returns {void}
	 */
	parseClassId(node, isStatement) {
		if (!this._classFastPath) {
			return base.parseClassId.call(this, node, isStatement);
		}
		const target = /** @type {Node & { id: Identifier | null }} */ (node);
		if (this.type === tokTypes.name) {
			target.id = this.parseIdent();
			if (isStatement) this.checkLValSimple(target.id, BIND_LEXICAL, false);
		} else {
			if (isStatement === true) this.unexpected();
			target.id = null;
		}
	}

	/**
	 * @param {UnfinishedNode} node the class node
	 * @returns {void}
	 */
	parseClassSuper(node) {
		if (!this._classFastPath) {
			return base.parseClassSuper.call(this, node);
		}
		/** @type {Node & { superClass: Expression | null }} */ (node).superClass =
			this.eat(tokTypes._extends)
				? this.parseExprSubscripts(null, false)
				: null;
	}

	/** @returns {PrivateNameMap} the map of private names this body declares */
	enterClassBody() {
		if (!this._classFastPath) return base.enterClassBody.call(this);
		/** @type {PrivateNameStackEntry} */
		const element = { declared: Object.create(null), used: [] };
		this.privateNameStack.push(element);
		return element.declared;
	}

	/** @returns {void} */
	exitClassBody() {
		if (!this._classFastPath) return base.exitClassBody.call(this);
		const entry = /** @type {PrivateNameStackEntry} */ (
			this.privateNameStack.pop()
		);
		const declared = entry.declared;
		const used = entry.used;
		if (!this.options.checkPrivateFields) return;
		const len = this.privateNameStack.length;
		const parent = len === 0 ? null : this.privateNameStack[len - 1];
		for (let i = 0; i < used.length; ++i) {
			const id = used[i];
			if (!Object.prototype.hasOwnProperty.call(declared, id.name)) {
				if (parent) {
					parent.used.push(id);
				} else {
					this.raiseRecoverable(
						id.start,
						`Private field '#${id.name}' must be declared in an enclosing class`
					);
				}
			}
		}
	}

	/**
	 * @param {boolean=} isGenerator whether it is a generator
	 * @param {boolean=} isAsync whether it is async
	 * @param {boolean=} allowDirectSuper whether `super()` may be called in it
	 * @returns {NodeOf<"FunctionExpression">} the method's function expression
	 */
	parseMethod(isGenerator, isAsync, allowDirectSuper) {
		if (!this._funcFastPath) {
			return base.parseMethod.call(
				this,
				isGenerator,
				isAsync,
				allowDirectSuper
			);
		}
		const node = /** @type {NodeInProgress<"FunctionExpression">} */ (
			new FunctionNode(this.start)
		);
		const oldYieldPos = this.yieldPos;
		const oldAwaitPos = this.awaitPos;
		const oldAwaitIdentPos = this.awaitIdentPos;
		node.generator = /** @type {boolean} */ (isGenerator);
		node.async = Boolean(isAsync);
		this.yieldPos = 0;
		this.awaitPos = 0;
		this.awaitIdentPos = 0;
		this.enterScope(
			SCOPE_FUNCTION |
				(isAsync ? SCOPE_ASYNC : 0) |
				(node.generator ? SCOPE_GENERATOR : 0) |
				SCOPE_SUPER |
				(allowDirectSuper ? SCOPE_DIRECT_SUPER : 0)
		);
		this.expect(tokTypes.parenL);
		// fast-path gate guarantees ES9+, so trailing commas are allowed
		node.params = /** @type {Pattern[]} */ (
			this.parseBindingList(tokTypes.parenR, false, true)
		);
		this.checkYieldAwaitInDefaultParams();
		this.parseFunctionBody(/** @type {Node} */ (node), false, true, false);
		this.yieldPos = oldYieldPos;
		this.awaitPos = oldAwaitPos;
		this.awaitIdentPos = oldAwaitIdentPos;
		return this.finishNode(node, "FunctionExpression");
	}

	/**
	 * @param {UnfinishedNode} prop the property to name
	 * @returns {NodeOf<"Property">["key"]} the key
	 */
	parsePropertyName(prop) {
		if (!this._subscriptFastPath) {
			return base.parsePropertyName.call(this, prop);
		}
		const target =
			/** @type {Node & { computed: boolean, key: Expression | null }} */ (
				prop
			);
		if (this.eat(tokTypes.bracketL)) {
			target.computed = true;
			target.key = this.parseMaybeAssign();
			this.expect(tokTypes.bracketR);
			return target.key;
		}
		target.computed = false;
		return (target.key =
			this.type === tokTypes.num || this.type === tokTypes.string
				? /** @type {Expression} */ (this.parseExprAtom())
				: /** @type {Expression} */ (
						this.parseIdent(this.options.allowReserved !== "never")
					));
	}

	/**
	 * @param {KeyedNode} prop the property whose key was read
	 * @param {boolean} isPattern whether it binds rather than builds a value
	 * @param {boolean | undefined} isGenerator whether it is a generator method
	 * @param {boolean | undefined} isAsync whether it is an async method
	 * @param {number | undefined} startPos where the property starts
	 * @param {PositionLike | null | undefined} startLoc its start position
	 * @param {RefDestructuringErrors | undefined} refDestructuringErrors where to record deferred errors
	 * @param {boolean} containsEsc whether the key was written with an escape
	 * @returns {void}
	 */
	parsePropertyValue(
		prop,
		isPattern,
		isGenerator,
		isAsync,
		startPos,
		startLoc,
		refDestructuringErrors,
		containsEsc
	) {
		if (!this._subscriptFastPath) {
			return base.parsePropertyValue.call(
				this,
				prop,
				isPattern,
				isGenerator,
				isAsync,
				startPos,
				startLoc,
				refDestructuringErrors,
				containsEsc
			);
		}
		const target =
			/** @type {Node & { computed: boolean, key: Node, value: Node | null, kind: string, method: boolean, shorthand: boolean }} */ (
				prop
			);
		if ((isGenerator || isAsync) && this.type === tokTypes.colon) {
			this.unexpected();
		}
		// the parsed key's plain name, read once for the get/set and shorthand arms
		const plainKeyName =
			!target.computed && target.key.type === "Identifier"
				? /** @type {Identifier} */ (target.key).name
				: null;
		if (this.eat(tokTypes.colon)) {
			target.value = isPattern
				? this.parseMaybeDefault(this.start, this.startLoc)
				: /** @type {Node} */ (
						this.parseMaybeAssign(false, refDestructuringErrors)
					);
			target.kind = "init";
		} else if (this.type === tokTypes.parenL) {
			if (isPattern) this.unexpected();
			target.method = true;
			target.value = /** @type {Node} */ (
				this.parseMethod(isGenerator, isAsync)
			);
			target.kind = "init";
		} else if (
			!isPattern &&
			!containsEsc &&
			(plainKeyName === "get" || plainKeyName === "set") &&
			this.type !== tokTypes.comma &&
			this.type !== tokTypes.braceR &&
			this.type !== tokTypes.eq
		) {
			if (isGenerator || isAsync) this.unexpected();
			this.parseGetterSetter(prop);
		} else if (plainKeyName !== null) {
			if (isGenerator || isAsync) this.unexpected();
			const key = /** @type {Identifier} */ (target.key);
			this.checkUnreserved(key);
			if (key.name === "await" && !this.awaitIdentPos) {
				// acorn assigns startPos raw; it is undefined only where acorn's is
				this.awaitIdentPos = /** @type {number} */ (startPos);
			}
			if (isPattern) {
				target.value = this.parseMaybeDefault(
					/** @type {number} */ (startPos),
					startLoc,
					/** @type {Identifier} */ (
						new IdentifierNode(key.start, key.end, key.name)
					)
				);
			} else if (this.type === tokTypes.eq && refDestructuringErrors) {
				if (refDestructuringErrors.shorthandAssign < 0) {
					refDestructuringErrors.shorthandAssign = this.start;
				}
				target.value = this.parseMaybeDefault(
					/** @type {number} */ (startPos),
					startLoc,
					/** @type {Identifier} */ (
						new IdentifierNode(key.start, key.end, key.name)
					)
				);
			} else {
				target.value = /** @type {Node} */ (
					new IdentifierNode(key.start, key.end, key.name)
				);
			}
			target.kind = "init";
			target.shorthand = true;
		} else {
			this.unexpected();
		}
	}

	/**
	 * @param {KeyedNode} prop the property whose key was just read
	 * @returns {boolean} whether `async` there was a modifier rather than the name
	 */
	isAsyncProp(prop) {
		if (!this._subscriptFastPath) {
			return base.isAsyncProp.call(this, prop);
		}
		const target = /** @type {Node & { computed: boolean, key: Node }} */ (
			prop
		);
		return (
			!target.computed &&
			target.key.type === "Identifier" &&
			/** @type {Identifier} */ (target.key).name === "async" &&
			(this.type === tokTypes.name ||
				this.type === tokTypes.num ||
				this.type === tokTypes.string ||
				this.type === tokTypes.bracketL ||
				Boolean(this.type.keyword) ||
				this.type === tokTypes.star) &&
			!this._gapHasNewline()
		);
	}

	/**
	 * Report a property name that may not be repeated: a getter or setter that
	 * clashes, and under ES5 a repeated `init` in strict mode.
	 * @param {ObjectMember} prop the property
	 * @param {PropHash} propHash where names seen so far are tracked
	 * @param {RefDestructuringErrors=} refDestructuringErrors what the expression parser recorded
	 * @returns {void}
	 */
	checkPropClash(prop, propHash, refDestructuringErrors) {
		if (!this._subscriptFastPath) {
			return base.checkPropClash.call(
				this,
				prop,
				propHash,
				refDestructuringErrors
			);
		}
		const target =
			/** @type {Node & { computed?: boolean, method?: boolean, shorthand?: boolean, key?: Node, kind?: string }} */ (
				prop
			);
		if (target.type === "SpreadElement") return;
		if (target.computed || target.method || target.shorthand) return;
		const key = /** @type {Node & { name?: string, value?: LiteralValue }} */ (
			target.key
		);
		/** @type {string} */
		let name;
		switch (key.type) {
			case "Identifier":
				name = key.name;
				break;
			case "Literal":
				name = String(key.value);
				break;
			default:
				return;
		}
		if (name === "__proto__" && target.kind === "init") {
			if (propHash.proto) {
				if (refDestructuringErrors) {
					if (refDestructuringErrors.doubleProto < 0) {
						refDestructuringErrors.doubleProto = key.start;
					}
				} else {
					this.raiseRecoverable(
						key.start,
						"Redefinition of __proto__ property"
					);
				}
			}
			propHash.proto = true;
		}
	}

	/**
	 * @param {LiteralValue} value the literal's value
	 * @returns {NodeOf<"Literal">} the finished literal
	 */
	parseLiteral(value) {
		if (!this._lazy) return base.parseLiteral.call(this, value);
		const start = this.start;
		const end = this.end;
		const len = end - start;
		const raw =
			len >= 2 && len <= SLICE_CACHE_MAX_LEN
				? getShortSlice(this.input, start, end)
				: this.input.slice(start, end);
		const node = new LiteralNode(start, end, value, raw);
		if (raw.charCodeAt(raw.length - 1) === 110) {
			// acorn falls back to normalizing `raw` when `BigInt` is missing;
			// every Node version webpack supports has it, so `value` is set
			/** @type {LiteralNode & { bigint?: string }} */ (node).bigint =
				/** @type {bigint} */ (value).toString();
		}
		this.next();
		return /** @type {NodeOf<"Literal">} */ (node);
	}

	/** @returns {void} */
	readRegexp() {
		const input = this.input;
		const start = this.pos;
		const len = input.length;
		let escaped = false;
		let inClass = false;
		let pos = start;
		for (;;) {
			if (pos >= len) this.raise(start, "Unterminated regular expression");
			const ch = input.charCodeAt(pos);
			// LF, CR, LS, PS
			if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) {
				this.raise(start, "Unterminated regular expression");
			}
			if (escaped) {
				escaped = false;
			} else {
				if (ch === 91) inClass = true;
				else if (ch === 93 && inClass) inClass = false;
				else if (ch === 47 && !inClass) break;
				escaped = ch === 92;
			}
			pos++;
		}
		const pattern = input.slice(start, pos);
		this.pos = pos + 1;
		const flagsStart = this.pos;
		const flags = this.readWord1();
		if (this.containsEsc) this.unexpected(flagsStart);

		// acorn's per-ecmaVersion flag validation, kept for its exact errors;
		// the whitelist is precomputed in the constructor
		const ecmaVersion = this._ecmaVersion;
		const validFlags = this._validRegexpFlags;
		let hasU = false;
		let hasV = false;
		for (let i = 0; i < flags.length; i++) {
			const flag = flags.charAt(i);
			if (!validFlags.includes(flag)) {
				this.raise(start, "Invalid regular expression flag");
			}
			if (flags.includes(flag, i + 1)) {
				this.raise(start, "Duplicate regular expression flag");
			}
			if (flag === "u") hasU = true;
			if (flag === "v") hasV = true;
		}
		if (ecmaVersion >= 15 && hasU && hasV) {
			this.raise(start, "Invalid regular expression flag");
		}

		let value = null;
		try {
			value = new RegExp(pattern, flags);
		} catch (_err) {
			// This engine cannot build it, so acorn re-reads the literal: its
			// validator says whether the pattern is invalid or merely newer
			this.pos = start;
			return base.readRegexp.call(this);
		}
		return this.finishToken(tokTypes.regexp, { pattern, flags, value });
	}

	// ----- regexp validation (V8-backed, replaces acorn's JS revalidation) -----

	/**
	 * Check a regexp literal's pattern, re-reading it once a group name shows
	 * that the named-capture goal symbol was the right one.
	 * @param {import("./regexp").RegExpValidationState} state the validation state
	 * @returns {void}
	 */
	validateRegExpPattern(state) {
		try {
			// eslint-disable-next-line no-new
			new RegExp(state.source, state.flags);
		} catch (_err) {
			base.validateRegExpPattern.call(this, state);
		}
	}

	// ----- scope tracking (Set-based, replaces acorn's array + indexOf) -----

	/**
	 * @param {number} flags what kind of scope it is
	 * @returns {void}
	 */
	enterScope(flags) {
		// the top-level scope enters from acorn's base constructor, before the
		// subclass constructor body created the pool
		const pool = this._scopePool;
		const cached = pool === undefined ? undefined : pool.pop();
		if (cached !== undefined) {
			cached.flags = flags;
			this.scopeStack.push(cached);
			return;
		}
		this.scopeStack.push(new ParserScope(flags));
	}

	/** @returns {void} */
	exitScope() {
		const scope = /** @type {ParserScope} */ (this.scopeStack.pop());
		// harvest the name sets emptied (an idle pooled scope must not retain
		// names) so declareName reuses the wrappers
		const setPool = this._setPool;
		const varSet = scope.var;
		if (varSet !== undefined) {
			varSet.clear();
			setPool.push(varSet);
			scope.var = undefined;
		}
		const lexicalSet = scope.lexical;
		if (lexicalSet !== undefined) {
			lexicalSet.clear();
			setPool.push(lexicalSet);
			scope.lexical = undefined;
		}
		const functionsSet = scope.functions;
		if (functionsSet !== undefined) {
			functionsSet.clear();
			setPool.push(functionsSet);
			scope.functions = undefined;
		}
		scope.firstLexical = undefined;
		this._exitedHoisted = /** @type {HoistedDeclarations | undefined} */ (
			scope.hoisted
		);
		scope.hoisted = undefined;
		// a recorded list is the consumer's now, and an empty one is still a
		// record — what must never reach it is the list of some earlier scope
		this._scopePool.push(scope);
	}

	/**
	 * @returns {Set<string>} empty name set, pooled by `exitScope`
	 */
	_takeSet() {
		const cached = this._setPool.pop();
		return cached === undefined ? new Set() : cached;
	}

	/**
	 * Record a declaration statement on the var scope it hoists to, in source
	 * order. A declaration under an `export` head belongs to that statement.
	 * @param {HoistedDeclarations[number] | NodeOf<"FunctionExpression" | "ClassExpression">} node the declaration statement
	 * @returns {void}
	 */
	_recordHoisted(node) {
		if (!this._recordsHoisted()) return;
		if (this.scopeStack.length === this._exportHeadDepth) return;
		// only a declaration in statement position arrives, whichever form it is typed as
		const scope =
			/** @type {{ hoisted: (HoistedDeclarations[number] | NodeOf<"FunctionExpression" | "ClassExpression">)[] | undefined }} */ (
				this.currentVarScope()
			);
		if (scope.hoisted === undefined) scope.hoisted = [node];
		else scope.hoisted.push(node);
	}

	/**
	 * Hand the scope's recorded declarations to the node that owns it, leaving
	 * the slot empty for the pool. Call it before the matching `exitScope`.
	 * @param {ParserScope} scope the scope being closed
	 * @returns {HoistedDeclarations | undefined} the declarations, in source order
	 */
	_takeHoisted(scope) {
		if (!this._recordsHoisted()) return undefined;
		const hoisted = /** @type {HoistedDeclarations | undefined} */ (
			scope.hoisted
		);
		if (hoisted === undefined) return NO_HOISTED_DECLARATIONS;
		scope.hoisted = undefined;
		return hoisted;
	}

	/**
	 * Whether the declarations of each scope are recorded at all: only when every
	 * production that reaches a recording site is this class's own, since an
	 * override of any of them leaves the record silently short.
	 * @returns {boolean} whether the record is kept
	 */
	_recordsHoisted() {
		return (
			this._lazy &&
			this._stmtFastPath &&
			this._funcStmtOwn &&
			this._stmt2FastPath &&
			this.parseClass === WebpackParser.prototype.parseClass
		);
	}

	/**
	 * Record an import or re-export the program states directly. One nested
	 * deeper belongs to no program, so only the top-level list is kept.
	 * @param {NodeOf<"ImportDeclaration" | "ExportNamedDeclaration" | "ExportDefaultDeclaration" | "ExportAllDeclaration">} node the finished declaration
	 * @returns {void}
	 */
	_recordModuleDeclaration(node) {
		if (this.scopeStack.length !== 1) return;
		const type = node.type;
		if (
			type !== "ImportDeclaration" &&
			type !== "ExportNamedDeclaration" &&
			type !== "ExportAllDeclaration"
		) {
			return;
		}
		const previous = this._previousTopLevelStatement;
		if (this._moduleDeclarations === undefined) {
			this._moduleDeclarations = [node, previous];
		} else {
			this._moduleDeclarations.push(node, previous);
		}
	}

	/**
	 * Hand the program its module declarations, leaving the slot empty.
	 * @returns {ModuleDeclarations | undefined} them, in source order
	 */
	_takeModuleDeclarations() {
		if (!this._recordsModuleDeclarations()) return undefined;
		const declarations = this._moduleDeclarations;
		if (declarations === undefined) return NO_MODULE_DECLARATIONS;
		this._moduleDeclarations = undefined;
		return declarations;
	}

	/**
	 * Whether imports and re-exports are recorded at all: only when this class
	 * reads them, since an override of either is what would go unrecorded.
	 * @returns {boolean} whether the record is kept
	 */
	_recordsModuleDeclarations() {
		const proto = WebpackParser.prototype;
		return (
			this._lazy &&
			this.parseImport === proto.parseImport &&
			this.parseExport === proto.parseExport
		);
	}

	/**
	 * Run `parse` with declaration recording held back for the scope depth it
	 * opens at, which is how an `export` head keeps its own declaration.
	 * @template T
	 * @param {() => T} parse what reads the declaration
	 * @returns {T} whatever it read
	 */
	_inExportHead(parse) {
		const previous = this._exportHeadDepth;
		this._exportHeadDepth = this.scopeStack.length;
		const declaration = parse();
		this._exportHeadDepth = previous;
		return declaration;
	}

	/**
	 * Record a name in the scope its binding belongs to, and report a name that
	 * was already declared there.
	 * @param {string} name the bound name
	 * @param {number} bindingType what kind of binding it is
	 * @param {number} pos where it was written
	 * @returns {void}
	 */
	declareName(name, bindingType, pos) {
		let redeclared = false;
		if (bindingType === BIND_LEXICAL) {
			const scope = this.currentScope();
			redeclared =
				(scope.lexical !== undefined && scope.lexical.has(name)) ||
				(scope.functions !== undefined && scope.functions.has(name)) ||
				(scope.var !== undefined && scope.var.has(name));
			if (scope.lexical === undefined) {
				scope.firstLexical = name;
				scope.lexical = this._takeSet();
			}
			scope.lexical.add(name);
			if (this.inModule && scope.flags & SCOPE_TOP) {
				delete this.undefinedExports[name];
			}
		} else if (bindingType === /* BIND_SIMPLE_CATCH */ 4) {
			const scope = this.currentScope();
			if (scope.lexical === undefined) {
				scope.firstLexical = name;
				scope.lexical = this._takeSet();
			}
			scope.lexical.add(name);
		} else if (bindingType === /* BIND_FUNCTION */ 3) {
			const scope = this.currentScope();
			redeclared = this.treatFunctionsAsVar
				? scope.lexical !== undefined && scope.lexical.has(name)
				: (scope.lexical !== undefined && scope.lexical.has(name)) ||
					(scope.var !== undefined && scope.var.has(name));
			(scope.functions || (scope.functions = this._takeSet())).add(name);
		} else {
			for (let i = this.scopeStack.length - 1; i >= 0; --i) {
				const scope = this.scopeStack[i];
				if (
					(scope.lexical !== undefined &&
						scope.lexical.has(name) &&
						!(
							scope.flags & SCOPE_SIMPLE_CATCH && scope.firstLexical === name
						)) ||
					// lazy-Set check first: `functions` is undefined for almost all
					// scopes, and the method call walks no state worth paying for then
					(scope.functions !== undefined &&
						!this.treatFunctionsAsVarInScope(scope) &&
						scope.functions.has(name))
				) {
					redeclared = true;
					break;
				}
				(scope.var || (scope.var = this._takeSet())).add(name);
				if (this.inModule && scope.flags & SCOPE_TOP) {
					delete this.undefinedExports[name];
				}
				if (scope.flags & SCOPE_VAR) break;
			}
		}
		if (redeclared) {
			this.raiseRecoverable(
				pos,
				`Identifier '${name}' has already been declared`
			);
		}
	}

	/**
	 * @param {Identifier} id the exported identifier
	 * @returns {void}
	 */
	checkLocalExport(id) {
		const topScope = this.scopeStack[0];
		if (
			!(topScope.lexical !== undefined && topScope.lexical.has(id.name)) &&
			!(topScope.var !== undefined && topScope.var.has(id.name))
		) {
			this.undefinedExports[id.name] = id;
		}
	}

	// ----- import attributes (`with { ... }` / legacy `assert { ... }`) -----

	/** @returns {ImportAttribute[]} the attributes a `with` clause states */
	parseWithClause() {
		/** @type {ImportAttribute[] & { [LEGACY_ASSERT_ATTRIBUTES]?: boolean }} */
		const nodes = [];

		// The legacy `assert` keyword takes no line break before it, so a statement
		// calling `assert(...)` on the next line is not an import assertion.
		const isAssertLegacy =
			this.isContextual("assert") && !this._gapHasNewline();

		if (isAssertLegacy) {
			if (!this.eat(tokTypes.name)) {
				return nodes;
			}
		} else if (!this.eat(tokTypes._with)) {
			return nodes;
		}

		this.expect(tokTypes.braceL);

		/** @type {Record<string, boolean>} */
		const attributeKeys = {};
		let first = true;

		while (!this.eat(tokTypes.braceR)) {
			if (!first) {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) {
					break;
				}
			} else {
				first = false;
			}

			const attr = this.parseImportAttribute();
			const keyName =
				/** @type {string} */
				(attr.key.type === "Identifier" ? attr.key.name : attr.key.value);

			if (Object.prototype.hasOwnProperty.call(attributeKeys, keyName)) {
				this.raiseRecoverable(
					attr.key.start,
					`Duplicate attribute key '${keyName}'`
				);
			}

			attributeKeys[keyName] = true;
			nodes.push(attr);
		}

		if (isAssertLegacy) {
			nodes[LEGACY_ASSERT_ATTRIBUTES] = true;
		}

		return nodes;
	}

	// ----- import phases (`import defer/source`, `import.defer/source()`) -----

	/** @returns {ImportAttribute} the finished attribute */
	parseImportAttribute() {
		if (!this._moduleDeclFastPath) {
			return base.parseImportAttribute.call(this);
		}
		const start = this.start;
		const key =
			this.type === tokTypes.string
				? /** @type {NodeOf<"Literal">} */ (this.parseExprAtom())
				: this.parseIdent(this.options.allowReserved !== "never");
		this.expect(tokTypes.colon);
		if (this.type !== tokTypes.string) {
			this.unexpected();
		}
		const value = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
		return /** @type {ImportAttribute} */ (
			new ImportAttributeNode(start, this.lastTokEnd, key, value)
		);
	}

	/** @returns {ModuleExportName} the name, as an identifier or a string literal */
	parseModuleExportName() {
		if (!this._moduleDeclFastPath) {
			return base.parseModuleExportName.call(this);
		}
		if (this.type === tokTypes.string) {
			const stringLiteral =
				/** @type {NodeOf<"Literal"> & { value: string }} */ (
					this.parseLiteral(/** @type {string} */ (this.value))
				);
			if (LONE_SURROGATE_REGEXP.test(stringLiteral.value)) {
				this.raise(
					stringLiteral.start,
					"An export name cannot include a lone surrogate."
				);
			}
			return stringLiteral;
		}
		return this.parseIdent(true);
	}

	/**
	 * @param {UnfinishedNode} node the import node
	 * @returns {NodeOf<"ImportDeclaration">} the finished import
	 */
	parseImport(node) {
		const declaration = this._parseImport(node);
		this._recordModuleDeclaration(declaration);
		return declaration;
	}

	/**
	 * @param {UnfinishedNode & { phase?: ImportPhase }} node the import node
	 * @returns {NodeOf<"ImportDeclaration">} the finished import
	 */
	_parseImport(node) {
		this._moduleSyntaxSeen = true;
		this._importPhase = null;
		if (!this._moduleDeclFastPath) {
			const result = base.parseImport.call(this, node);
			if (this._importPhase) node.phase = this._importPhase;
			return result;
		}
		this.next();
		/** @type {AnyImportSpecifier[]} */
		let specifiers;
		/** @type {NodeOf<"Literal">} */
		let source;
		// import '...'
		if (this.type === tokTypes.string) {
			specifiers = EMPTY_IMPORT_SPECIFIERS;
			source = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
		} else {
			specifiers = this.parseImportSpecifiers();
			this.expectContextual("from");
			source =
				this.type === tokTypes.string
					? /** @type {NodeOf<"Literal">} */ (this.parseExprAtom())
					: this.unexpected();
		}
		const attributes = this.parseWithClause();
		this.semicolon();
		const result =
			/** @type {NodeOf<"ImportDeclaration"> & { phase?: ImportPhase }} */
			(
				new ImportDeclarationNode(
					node.start,
					this.lastTokEnd,
					specifiers,
					source,
					attributes
				)
			);
		if (this._importPhase) result.phase = this._importPhase;
		return result;
	}

	/** @returns {ImportSpecifier} the finished specifier */
	parseImportSpecifier() {
		if (!this._moduleDeclFastPath) {
			return base.parseImportSpecifier.call(this);
		}
		const start = this.start;
		const imported = this.parseModuleExportName();
		/** @type {Identifier} */
		let local;
		if (this.eatContextual("as")) {
			local = this.parseIdent();
		} else {
			this.checkUnreserved(/** @type {Identifier} */ (imported));
			local = /** @type {Identifier} */ (imported);
		}
		this.checkLValSimple(local, BIND_LEXICAL);
		return /** @type {ImportSpecifier} */ (
			new ImportSpecifierNode(start, this.lastTokEnd, imported, local)
		);
	}

	/** @returns {ImportDefaultSpecifier} the finished specifier */
	parseImportDefaultSpecifier() {
		if (!this._moduleDeclFastPath) {
			return base.parseImportDefaultSpecifier.call(this);
		}
		// import defaultObj, { x, y as z } from '...'
		const start = this.start;
		const local = this.parseIdent();
		this.checkLValSimple(local, BIND_LEXICAL);
		return /** @type {ImportDefaultSpecifier} */ (
			new ImportSimpleSpecifierNode(
				start,
				this.lastTokEnd,
				"ImportDefaultSpecifier",
				local
			)
		);
	}

	/** @returns {ImportNamespaceSpecifier} the finished specifier */
	parseImportNamespaceSpecifier() {
		if (!this._moduleDeclFastPath) {
			return base.parseImportNamespaceSpecifier.call(this);
		}
		const start = this.start;
		this.next();
		this.expectContextual("as");
		const local = this.parseIdent();
		this.checkLValSimple(local, BIND_LEXICAL);
		return /** @type {ImportNamespaceSpecifier} */ (
			new ImportSimpleSpecifierNode(
				start,
				this.lastTokEnd,
				"ImportNamespaceSpecifier",
				local
			)
		);
	}

	/**
	 * Acorn's `parseImportSpecifiers` list body over a pooled scratch array;
	 * the phase detection wrapping it lives in `parseImportSpecifiers`.
	 * acorn source: https://github.com/acornjs/acorn/blob/8.18.0/acorn/src/statement.js
	 * @returns {AnyImportSpecifier[]} import specifiers
	 */
	_parseImportSpecifiersBase() {
		if (!this._moduleDeclFastPath) {
			return base.parseImportSpecifiers.call(this);
		}
		/** @type {AnyImportSpecifier[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let first = true;
		if (this.type === tokTypes.name) {
			scratch[count++] = this.parseImportDefaultSpecifier();
			if (!this.eat(tokTypes.comma)) {
				return this._releaseScratch(scratch, count);
			}
		}
		if (this.type === tokTypes.star) {
			scratch[count++] = this.parseImportNamespaceSpecifier();
			return this._releaseScratch(scratch, count);
		}
		this.expect(tokTypes.braceL);
		while (!this.eat(tokTypes.braceR)) {
			if (!first) {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) break;
			} else {
				first = false;
			}
			scratch[count++] = this.parseImportSpecifier();
		}
		return this._releaseScratch(scratch, count);
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportAllDeclaration"> | NodeOf<"ExportDefaultDeclaration"> | NodeOf<"ExportNamedDeclaration">} the finished export
	 */
	parseExport(node, exports) {
		const declaration = this._parseExport(node, exports);
		this._recordModuleDeclaration(declaration);
		return declaration;
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportNamedDeclaration" | "ExportDefaultDeclaration" | "ExportAllDeclaration">} the finished export
	 */
	_parseExport(node, exports) {
		this._moduleSyntaxSeen = true;
		if (!this._moduleDeclFastPath) {
			return base.parseExport.call(this, node, exports);
		}
		this.next();
		// export * from '...'
		if (this.eat(tokTypes.star)) {
			return this.parseExportAllDeclaration(node, exports);
		}
		if (this.eat(tokTypes._default)) {
			// export default ...
			this.checkExport(exports, "default", this.lastTokStart);
			const declaration = this.parseExportDefaultDeclaration();
			return /** @type {NodeOf<"ExportDefaultDeclaration">} */ (
				new ExportDefaultDeclarationNode(
					node.start,
					this.lastTokEnd,
					declaration
				)
			);
		}
		/** @type {Declaration | null} */
		let declaration;
		/** @type {NodeOf<"ExportSpecifier">[]} */
		let specifiers;
		/** @type {NodeOf<"Literal"> | null} */
		let source;
		/** @type {ImportAttribute[]} */
		let attributes;
		// export var|const|let|function|class ...
		if (this.shouldParseExportStatement()) {
			declaration = this.parseExportDeclaration(node);
			if (declaration.type === "VariableDeclaration") {
				this.checkVariableExport(exports, declaration.declarations);
			} else {
				const id = /** @type {Node & { id: Identifier }} */ (declaration).id;
				this.checkExport(exports, id, id.start);
			}
			specifiers = [];
			source = null;
			attributes = [];
		} else {
			// export { x, y as z } [from '...']
			declaration = null;
			specifiers = this.parseExportSpecifiers(exports);
			if (this.eatContextual("from")) {
				if (this.type !== tokTypes.string) this.unexpected();
				source = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
				attributes = this.parseWithClause();
			} else {
				for (let i = 0; i < specifiers.length; i++) {
					// check for keywords used as local names
					const local = /** @type {Node & { local: Node }} */ (specifiers[i])
						.local;
					this.checkUnreserved(/** @type {Identifier} */ (local));
					// check if export is defined
					this.checkLocalExport(/** @type {Identifier} */ (local));
					if (local.type === "Literal") {
						this.raise(
							local.start,
							"A string literal cannot be used as an exported binding without `from`."
						);
					}
				}
				source = null;
				attributes = [];
			}
			this.semicolon();
		}
		return /** @type {NodeOf<"ExportNamedDeclaration">} */ (
			new ExportNamedDeclarationNode(
				node.start,
				this.lastTokEnd,
				declaration,
				specifiers,
				source,
				attributes
			)
		);
	}

	/**
	 * @param {UnfinishedNode} node the export node
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportAllDeclaration">} the finished export
	 */
	parseExportAllDeclaration(node, exports) {
		if (!this._moduleDeclFastPath) {
			return base.parseExportAllDeclaration.call(this, node, exports);
		}
		/** @type {ModuleExportName | null} */
		let exported;
		if (this.eatContextual("as")) {
			exported = this.parseModuleExportName();
			this.checkExport(exports, exported, this.lastTokStart);
		} else {
			exported = null;
		}
		this.expectContextual("from");
		if (this.type !== tokTypes.string) this.unexpected();
		const source = /** @type {NodeOf<"Literal">} */ (this.parseExprAtom());
		const attributes = this.parseWithClause();
		this.semicolon();
		return /** @type {NodeOf<"ExportAllDeclaration">} */ (
			new ExportAllDeclarationNode(
				node.start,
				this.lastTokEnd,
				exported,
				source,
				attributes
			)
		);
	}

	/** @returns {NodeOf<"ExportDefaultDeclaration">["declaration"]} what `export default` exports */
	parseExportDefaultDeclaration() {
		return this._inExportHead(() => this._parseExportDefaultDeclaration());
	}

	/** @returns {MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration | Expression} what `export default` exports */
	_parseExportDefaultDeclaration() {
		if (!this._moduleDeclFastPath) {
			return base.parseExportDefaultDeclaration.call(this);
		}
		let isAsync = false;
		if (
			this.type === tokTypes._function ||
			(isAsync = this.isAsyncFunction())
		) {
			const functionNode = this.startNode();
			this.next();
			if (isAsync) this.next();
			return /** @type {MaybeNamedFunctionDeclaration} */ (
				this.parseFunction(
					functionNode,
					FUNC_STATEMENT | FUNC_NULLABLE_ID,
					false,
					isAsync
				)
			);
		}
		if (this.type === tokTypes._class) {
			const classNode = this.startNode();
			return this.parseClass(classNode, "nullableID");
		}
		const declaration = this.parseMaybeAssign();
		this.semicolon();
		return declaration;
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @param {ExportedName} name the exported name
	 * @param {number} pos where it was written
	 * @returns {void}
	 */
	checkExport(exports, name, pos) {
		if (!exports) return;
		const record = /** @type {Record<string, boolean>} */ (exports);
		const key =
			typeof name === "string"
				? name
				: /** @type {string} */ (
						name.type === "Identifier" ? name.name : name.value
					);
		if (Object.prototype.hasOwnProperty.call(record, key)) {
			this.raiseRecoverable(pos, `Duplicate export '${key}'`);
		}
		record[key] = true;
	}

	/**
	 * @param {ExportedNames} exports where module exports are tracked
	 * @param {Pattern | AssignmentProperty} pat the pattern that binds the exported names
	 * @returns {void}
	 */
	checkPatternExport(exports, pat) {
		const type = pat.type;
		if (type === "Identifier") {
			this.checkExport(exports, pat, pat.start);
		} else if (type === "ObjectPattern") {
			for (const prop of pat.properties) {
				this.checkPatternExport(exports, prop);
			}
		} else if (type === "ArrayPattern") {
			for (const element of pat.elements) {
				if (element) this.checkPatternExport(exports, element);
			}
		} else if (type === "Property") {
			this.checkPatternExport(
				exports,
				/** @type {Node & { value: Pattern }} */ (pat).value
			);
		} else if (type === "AssignmentPattern") {
			this.checkPatternExport(exports, pat.left);
		} else if (type === "RestElement") {
			this.checkPatternExport(exports, pat.argument);
		}
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @param {NodeOf<"VariableDeclarator">[]} declarations the declarators being exported
	 * @returns {void}
	 */
	checkVariableExport(exports, declarations) {
		if (!exports) return;
		for (const declaration of declarations) {
			this.checkPatternExport(exports, declaration.id);
		}
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportSpecifier">} the finished specifier
	 */
	parseExportSpecifier(exports) {
		if (!this._moduleDeclFastPath) {
			return base.parseExportSpecifier.call(this, exports);
		}
		const start = this.start;
		const local = this.parseModuleExportName();
		const exported = this.eatContextual("as")
			? this.parseModuleExportName()
			: local;
		this.checkExport(exports, exported, exported.start);
		return /** @type {NodeOf<"ExportSpecifier">} */ (
			new ExportSpecifierNode(start, this.lastTokEnd, local, exported)
		);
	}

	/**
	 * @param {ExportedNames | undefined} exports where module exports are tracked
	 * @returns {NodeOf<"ExportSpecifier">[]} the specifiers
	 */
	parseExportSpecifiers(exports) {
		if (!this._moduleDeclFastPath) {
			return base.parseExportSpecifiers.call(this, exports);
		}
		/** @type {NodeOf<"ExportSpecifier">[]} */
		const scratch = this._acquireScratch();
		let count = 0;
		let first = true;
		// export { x, y as z } [from '...']
		this.expect(tokTypes.braceL);
		while (!this.eat(tokTypes.braceR)) {
			if (!first) {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.braceR)) break;
			} else {
				first = false;
			}
			scratch[count++] = this.parseExportSpecifier(exports);
		}
		return this._releaseScratch(scratch, count);
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"AwaitExpression">} the await expression
	 */
	parseAwait(forInit) {
		if (this.inModule && this.currentVarScope().flags & SCOPE_TOP) {
			this._moduleSyntaxSeen = true;
		}
		if (!this._lazy) return base.parseAwait.call(this, forInit);
		if (!this.awaitPos) this.awaitPos = this.start;
		const start = this.start;
		this.next();
		const argument = /** @type {Expression} */ (
			this.parseMaybeUnary(null, true, false, forInit)
		);
		return /** @type {NodeOf<"AwaitExpression">} */ (
			new AwaitExpressionNode(start, this.lastTokEnd, argument)
		);
	}

	/**
	 * @param {boolean | string=} forInit whether `in` is held back as an operator
	 * @returns {NodeOf<"YieldExpression">} the yield expression
	 */
	parseYield(forInit) {
		if (!this._lazy) return base.parseYield.call(this, forInit);
		if (!this.yieldPos) this.yieldPos = this.start;
		const start = this.start;
		this.next();
		let delegate = false;
		/** @type {Expression | null} */
		let argument = null;
		if (!(
			this.type === tokTypes.semi ||
			this.canInsertSemicolon() ||
			(this.type !== tokTypes.star &&
				!(/** @type {TokenTypeInternal} */ (this.type).startsExpr))
		)) {
			delegate = this.eat(tokTypes.star);
			argument = this.parseMaybeAssign(forInit);
		}
		return /** @type {NodeOf<"YieldExpression">} */ (
			new YieldExpressionNode(start, this.lastTokEnd, delegate, argument)
		);
	}

	/** @returns {NodeOf<"PrivateIdentifier">} the private identifier */
	parsePrivateIdent() {
		if (!this._lazy) return base.parsePrivateIdent.call(this);
		if (this.type !== tokTypes.privateId) this.unexpected();
		const node = new PrivateIdentifierNode(
			this.start,
			this.end,
			/** @type {string} */ (this.value)
		);
		this.next();
		// for validating existence when the enclosing class closes
		if (this.options.checkPrivateFields) {
			if (this.privateNameStack.length === 0) {
				this.raise(
					node.start,
					`Private field '#${node.name}' must be declared in an enclosing class`
				);
			} else {
				this.privateNameStack[this.privateNameStack.length - 1].used.push(node);
			}
		}
		return /** @type {NodeOf<"PrivateIdentifier">} */ (node);
	}

	/** @returns {AnyImportSpecifier[]} the specifiers */
	parseImportSpecifiers() {
		if (!this._importPhasesEnabled) {
			return this._parseImportSpecifiersBase();
		}

		/** @type {ImportPhase | null} */
		const phase = this.isContextual("defer")
			? "defer"
			: this.isContextual("source")
				? "source"
				: null;
		if (!phase) return this._parseImportSpecifiersBase();

		const phaseId = this.parseIdent();
		if (this.isContextual("from") || this.type === tokTypes.comma) {
			// `defer`/`source` was the default import name, not a phase modifier
			const defaultSpecifier =
				/** @type {ImportDefaultSpecifier} */
				(
					this.startNodeAt(
						phaseId.start,
						phaseId.loc ? phaseId.loc.start : undefined
					)
				);
			defaultSpecifier.local = phaseId;
			this.checkLValSimple(phaseId, BIND_LEXICAL);

			/** @type {AnyImportSpecifier[]} */
			const nodes = [
				/** @type {ImportDefaultSpecifier} */
				(this.finishNode(defaultSpecifier, "ImportDefaultSpecifier"))
			];
			if (this.eat(tokTypes.comma)) {
				if (this.type !== tokTypes.star && this.type !== tokTypes.braceL) {
					this.unexpected();
				}
				nodes.push(...this._parseImportSpecifiersBase());
			}
			return nodes;
		}

		this._importPhase = phase;

		if (phase === "defer") {
			if (this.type !== tokTypes.star) {
				this.raiseRecoverable(
					phaseId.start,
					"'import defer' can only be used with namespace imports ('import defer * as identifierName from ...')."
				);
			}
		} else if (this.type !== tokTypes.name) {
			this.raiseRecoverable(
				phaseId.start,
				"'import source' can only be used with direct identifier specifier imports."
			);
		}

		const specifiers = this._parseImportSpecifiersBase();

		if (
			phase === "source" &&
			specifiers.some((s) => s.type !== "ImportDefaultSpecifier")
		) {
			this.raiseRecoverable(
				phaseId.start,
				"'import source' can only be used with direct identifier specifier imports ('import source identifierName from ...')."
			);
		}

		return specifiers;
	}

	/**
	 * @param {UnfinishedNode} node the import expression node
	 * @returns {ImportExpression} the finished expression
	 */
	parseDynamicImport(node) {
		this.next();

		node.source = this.parseMaybeAssign();

		if (this.options.ecmaVersion >= 16) {
			if (this.eat(tokTypes.parenR)) {
				node.options = null;
			} else {
				this.expect(tokTypes.comma);
				if (this.afterTrailingComma(tokTypes.parenR)) {
					node.options = null;
				} else {
					node.options = this.parseMaybeAssign();
					if (!this.eat(tokTypes.parenR)) {
						this.expect(tokTypes.comma);
						if (!this.afterTrailingComma(tokTypes.parenR)) this.unexpected();
					}
				}
			}
		} else if (!this.eat(tokTypes.parenR)) {
			const errorPos = this.start;
			if (this.eat(tokTypes.comma) && this.eat(tokTypes.parenR)) {
				this.raiseRecoverable(
					errorPos,
					"Trailing comma is not allowed in import()"
				);
			} else {
				this.unexpected(errorPos);
			}
		}

		return this.finishNode(node, "ImportExpression");
	}

	/**
	 * @param {boolean=} forNew whether an enclosing `new` reads the callee
	 * @returns {ImportExpression | MetaProperty} the import expression or meta property
	 */
	parseExprImport(forNew) {
		const start = this.startNode();
		// `import` reads as an identifier for `import.meta`, and `parseIdent(true)`
		// does not check escapes, so the keyword's own check is here
		if (this.containsEsc) {
			this.raiseRecoverable(this.start, "Escape sequence in keyword import");
		}
		this.next();
		/** @type {ImportExpression | MetaProperty} */
		let node;
		if (this.type === tokTypes.parenL && !forNew) {
			node = this.parseDynamicImport(start);
		} else if (this.type === tokTypes.dot) {
			const meta = this.startNodeAt(
				start.start,
				start.loc ? start.loc.start : undefined
			);
			meta.name = "import";
			start.meta = this.finishNode(meta, "Identifier");
			node = this.parseImportMeta(start);
		} else {
			return this.unexpected();
		}

		if (
			this._importPhasesEnabled &&
			node.type === "MetaProperty" &&
			(node.property.name === "defer" || node.property.name === "source")
		) {
			if (this.type === tokTypes.parenL) {
				if (forNew) {
					// same guard acorn applies to `new import(...)`
					this.raise(node.start, "import call cannot be the target of `new`");
				}
				const dynImport =
					/** @type {ImportExpression & { phase?: ImportPhase }} */
					(
						this.parseDynamicImport(
							this.startNodeAt(
								node.start,
								node.loc ? node.loc.start : undefined
							)
						)
					);
				dynImport.phase = node.property.name;
				return dynImport;
			}
			this.raiseRecoverable(
				node.start,
				`'import.${node.property.name}' can only be used in a dynamic import.`
			);
		}

		return node;
	}

	/**
	 * @param {UnfinishedNode} node the meta property node
	 * @returns {MetaProperty} the finished expression
	 */
	parseImportMeta(node) {
		if (!this._importPhasesEnabled) {
			// only `import.meta` is accepted here, and it is module-only
			this._moduleSyntaxSeen = true;
			this.next();
			const onlyEsc = this.containsEsc;
			const onlyProperty = this.parseIdent(true);
			node.property = onlyProperty;
			if (onlyProperty.name !== "meta") {
				this.raiseRecoverable(
					onlyProperty.start,
					"The only valid meta property for import is 'import.meta'"
				);
			}
			if (onlyEsc) {
				this.raiseRecoverable(
					node.start,
					"'import.meta' must not contain escaped characters"
				);
			}
			if (
				this.options.sourceType !== "module" &&
				!this.options.allowImportExportEverywhere
			) {
				this.raiseRecoverable(
					node.start,
					"Cannot use 'import.meta' outside a module"
				);
			}
			return this.finishNode(node, "MetaProperty");
		}

		this.next();

		const containsEsc = this.containsEsc;
		const property = this.parseIdent(true);
		node.property = property;
		const { name } = property;

		// only `import.meta` is module-only; `import.defer`/`.source` are dynamic
		if (name === "meta") this._moduleSyntaxSeen = true;

		if (name !== "meta" && name !== "defer" && name !== "source") {
			this.raiseRecoverable(
				property.start,
				"The only valid meta property for import is 'import.meta'"
			);
		}
		if (containsEsc) {
			this.raiseRecoverable(
				node.start,
				`'import.${name}' must not contain escaped characters`
			);
		}
		if (
			name === "meta" &&
			this.options.sourceType !== "module" &&
			!this.options.allowImportExportEverywhere
		) {
			this.raiseRecoverable(
				node.start,
				"Cannot use 'import.meta' outside a module"
			);
		}

		return this.finishNode(node, "MetaProperty");
	}
}

/** @import { SourcePosition } from "../graph/Dependency" */

/**
 * Whether a raw string literal contains a legacy octal (`\47`, `\0` followed by
 * a digit) or non-octal decimal (`\8`, `\9`) escape — all SyntaxErrors in
 * strict mode. Escaped backslashes (`\\`) and `\x` / `\u` escapes are skipped.
 * @param {string} raw raw string literal text, including quotes
 * @returns {boolean} true when a strict-forbidden escape is present
 */
const hasOctalEscape = (raw) => {
	for (let i = 0; i < raw.length; i++) {
		if (raw.charCodeAt(i) !== 92) continue;
		const next = raw.charCodeAt(i + 1);
		if (next === 92) {
			i++;
			continue;
		}
		// `\0` is a valid NUL escape unless a digit follows it.
		if (next === 48) {
			const after = raw.charCodeAt(i + 2);
			if (after >= 48 && after <= 57) return true;
			i++;
			continue;
		}
		if (next >= 49 && next <= 57) return true;
	}
	return false;
};

// Location decoding for lazy-mode output: nodes carry only offsets (the parser
// skips acorn's location tracking), so line/column are derived on demand from
// these two helpers.

/**
 * Exact char-code scan matching acorn's `lineBreak` semantics (CRLF is one
 * break). Kept as the fallback for the terminators `buildLineStarts` cannot
 * find with a native search.
 * @param {string} source source code
 * @returns {number[]} line start offsets
 */
const scanLineStarts = (source) => {
	const len = source.length;
	const lineStarts = [0];
	for (let i = 0; i < len; i++) {
		const ch = source.charCodeAt(i);
		if (ch === 10) {
			lineStarts.push(i + 1);
		} else if (ch === 13) {
			if (source.charCodeAt(i + 1) === 10) i++;
			lineStarts.push(i + 1);
		} else if (ch === 0x2028 || ch === 0x2029) {
			lineStarts.push(i + 1);
		}
	}
	return lineStarts;
};

/**
 * Offset of each line's first character. `indexOf` searches the source natively,
 * which beats a char-code loop by ~3x; CRLF needs no special case because it
 * starts the next line after its `\n` either way. A lone `\r` or a unicode
 * separator does break that equivalence, so those fall back to the char scan.
 * @param {string} source source code
 * @returns {number[]} line start offsets
 */
const buildLineStarts = (source) => {
	if (!source.includes("\u2028") && !source.includes("\u2029")) {
		let cr = source.indexOf("\r");
		while (cr !== -1 && source.charCodeAt(cr + 1) === 10) {
			cr = source.indexOf("\r", cr + 2);
		}
		if (cr === -1) {
			const lineStarts = [0];
			let i = source.indexOf("\n");
			while (i !== -1) {
				lineStarts.push(i + 1);
				i = source.indexOf("\n", i + 1);
			}
			return lineStarts;
		}
	}
	return scanLineStarts(source);
};

/**
 * Binary search for the line containing the offset.
 * @param {number[]} lineStarts line start offsets
 * @param {number} offset source offset
 * @returns {SourcePosition} position (1-based line, 0-based column)
 */
const positionAt = (lineStarts, offset) => {
	let lo = 0;
	let hi = lineStarts.length - 1;
	while (lo < hi) {
		const mid = (lo + hi + 1) >>> 1;
		if (lineStarts[mid] <= offset) lo = mid;
		else hi = mid - 1;
	}
	return { line: lo + 1, column: offset - lineStarts[lo] };
};

const KEYWORDS_BEFORE_REGEXP = new Set([
	"await",
	"case",
	"delete",
	"do",
	"else",
	"in",
	"instanceof",
	"new",
	"of",
	"return",
	"throw",
	"typeof",
	"void",
	"yield"
]);

/**
 * Whether a `/` at this point starts a regexp rather than division, decided by
 * what precedes it: after a value (name, number, `)`, `]`) it divides, after an
 * operator or a keyword it opens a regexp. `)` and `]` are always read as
 * division — telling `if (a) /re/` from `(a) / b` needs paren-context tracking
 * this single pass does not carry.
 * @param {string} source source code
 * @param {number} slash offset of the `/`
 * @returns {boolean} true when a regexp literal starts here
 */
const startsRegExp = (source, slash) => {
	let i = slash - 1;
	while (i >= 0) {
		const code = source.charCodeAt(i);
		if (code === 32 || code === 9 || code === 10 || code === 13) i--;
		else break;
	}
	if (i < 0) return true;
	const code = source.charCodeAt(i);
	if (code === 41 || code === 93) return false;
	if (!isIdentifierChar(code)) return true;
	// a keyword before `/` is an operator position (`return /re/`), a name is not
	let start = i;
	while (start >= 0 && isIdentifierChar(source.charCodeAt(start))) start--;
	return KEYWORDS_BEFORE_REGEXP.has(source.slice(start + 1, i + 1));
};

/**
 * Skips whitespace and comments, returning the next code offset.
 * @param {string} source source code
 * @param {number} i offset to start at
 * @returns {number} offset of the next significant character
 */
const skipTrivia = (source, i) => {
	while (i < source.length) {
		const code = source.charCodeAt(i);
		if (code === 32 || code === 9 || code === 10 || code === 13) {
			i++;
		} else if (code === 47 && source.charCodeAt(i + 1) === 47) {
			const end = source.indexOf("\n", i + 2);
			i = end === -1 ? source.length : end + 1;
		} else if (code === 47 && source.charCodeAt(i + 1) === 42) {
			const end = source.indexOf("*/", i + 2);
			i = end === -1 ? source.length : end + 2;
		} else {
			break;
		}
	}
	return i;
};

/** @type {Map<number, string>} */
const SINGLE_CHARACTER_ESCAPES = new Map([
	[110, "\n"],
	[114, "\r"],
	[116, "\t"],
	[98, "\b"],
	[118, "\u000B"],
	[102, "\f"]
]);

/**
 * Reads the hex digits of a `\x`/`\u` escape.
 * @param {string} source source code
 * @param {number} i offset of the first digit
 * @param {number} length how many digits to read
 * @returns {number} the code point, or `-1` when a digit is missing
 */
const readHexEscapeValue = (source, i, length) => {
	let value = 0;
	for (let j = i; j < i + length; j++) {
		const digit = Number.parseInt(source[j], 16);
		if (Number.isNaN(digit)) return -1;
		value = value * 16 + digit;
	}
	return value;
};

/**
 * Cooks one escape sequence, sloppy-mode script semantics (legacy octal included).
 * @param {string} source source code
 * @param {number} i offset just past the backslash
 * @returns {{ value: string, end: number } | undefined} the cooked text, or `undefined` when malformed
 */
const readEscape = (source, i) => {
	if (i >= source.length) return;
	const code = source.charCodeAt(i);
	const single = SINGLE_CHARACTER_ESCAPES.get(code);
	if (single !== undefined) return { value: single, end: i + 1 };
	// a line continuation cooks to nothing; `\r\n` counts as one terminator
	if (code === 13) {
		return { value: "", end: source.charCodeAt(i + 1) === 10 ? i + 2 : i + 1 };
	}
	if (code === 10 || code === 0x2028 || code === 0x2029) {
		return { value: "", end: i + 1 };
	}
	if (code === 120) {
		const value = readHexEscapeValue(source, i + 1, 2);
		if (value === -1) return;
		return { value: String.fromCodePoint(value), end: i + 3 };
	}
	if (code === 117) {
		if (source.charCodeAt(i + 1) === 123) {
			const end = source.indexOf("}", i + 2);
			if (end === -1 || end === i + 2) return;
			const value = readHexEscapeValue(source, i + 2, end - i - 2);
			if (value === -1 || value > 0x10ffff) return;
			return { value: String.fromCodePoint(value), end: end + 1 };
		}
		const value = readHexEscapeValue(source, i + 1, 4);
		if (value === -1) return;
		return { value: String.fromCodePoint(value), end: i + 5 };
	}
	if (code >= 48 && code <= 55) {
		let digits = /** @type {RegExpMatchArray} */ (
			source.slice(i, i + 3).match(/^[0-7]+/)
		)[0];
		if (Number.parseInt(digits, 8) > 255) digits = digits.slice(0, -1);
		return {
			value: String.fromCharCode(Number.parseInt(digits, 8)),
			end: i + digits.length
		};
	}
	return { value: source[i], end: i + 1 };
};

/**
 * Reads a `'`/`"` string starting at `i`, cooking its escapes the way the ast
 * this replaces did — a `Literal` node carries the cooked value, so a specifier
 * written `"\x2e/a"` is the same dependency as `"./a"`. Templates are not
 * specifiers here: the ast only matched `Literal` nodes.
 * @param {string} source source code
 * @param {number} i offset of the opening quote
 * @returns {{ value: string, end: number } | undefined} the literal
 */
const readStringLiteral = (source, i) => {
	const quote = source.charCodeAt(i);
	if (quote !== 34 && quote !== 39) return;
	for (let j = i + 1; j < source.length; j++) {
		const code = source.charCodeAt(j);
		if (code === 10 || code === 13) return;
		if (code === 92) return readEscapedStringLiteral(source, i, j);
		if (code === quote) return { value: source.slice(i + 1, j), end: j + 1 };
	}
};

/**
 * Continues `readStringLiteral` from its first escape, so the unescaped case
 * stays a single slice.
 * @param {string} source source code
 * @param {number} i offset of the opening quote
 * @param {number} firstEscape offset of the first backslash
 * @returns {{ value: string, end: number } | undefined} the literal
 */
const readEscapedStringLiteral = (source, i, firstEscape) => {
	const quote = source.charCodeAt(i);
	let value = "";
	let segment = i + 1;
	for (let j = firstEscape; j < source.length; j++) {
		const code = source.charCodeAt(j);
		if (code === 10 || code === 13) return;
		if (code === quote) {
			return { value: value + source.slice(segment, j), end: j + 1 };
		}
		if (code !== 92) continue;
		const escape = readEscape(source, j + 1);
		if (escape === undefined) return;
		value += source.slice(segment, j) + escape.value;
		segment = escape.end;
		j = escape.end - 1;
	}
};

/**
 * Scans a template body from `i` (just past a backtick or a `}` resuming one).
 * @param {string} source source code
 * @param {number} i offset inside the template
 * @returns {{ end: number, substitution: boolean }} where the body stops
 */
const skipTemplateBody = (source, i) => {
	for (; i < source.length; i++) {
		const code = source.charCodeAt(i);
		if (code === 92) {
			i++;
		} else if (code === 96) {
			return { end: i, substitution: false };
		} else if (code === 36 && source.charCodeAt(i + 1) === 123) {
			return { end: i + 1, substitution: true };
		}
	}
	return { end: source.length, substitution: false };
};

/**
 * Collects the specifiers of static `require("…")` calls. A scan rather than a
 * parse: it only has to skip what could hide or fake a call — comments, strings,
 * templates and regexps — so it costs one pass and builds no ast. Template
 * substitutions are scanned as the code they are, which is why the brace depth
 * of each open `${` is tracked.
 * @param {string} source source code
 * @returns {Set<string>} required specifiers
 */
const collectCjsRequireSpecifiers = (source) => {
	/** @type {Set<string>} */
	const specifiers = new Set();
	/** @type {number[]} */
	const templateBraceDepths = [];
	let braceDepth = 0;
	for (let i = 0; i < source.length; i++) {
		const code = source.charCodeAt(i);
		if (code === 96) {
			const body = skipTemplateBody(source, i + 1);
			i = body.end;
			if (body.substitution) {
				templateBraceDepths.push(braceDepth);
				braceDepth++;
			}
			continue;
		}
		if (code === 123) {
			braceDepth++;
			continue;
		}
		if (code === 125) {
			braceDepth--;
			if (
				templateBraceDepths.length > 0 &&
				templateBraceDepths[templateBraceDepths.length - 1] === braceDepth
			) {
				templateBraceDepths.pop();
				const body = skipTemplateBody(source, i + 1);
				i = body.end;
				if (body.substitution) {
					templateBraceDepths.push(braceDepth);
					braceDepth++;
				}
			}
			continue;
		}
		if (code === 34 || code === 39) {
			for (i++; i < source.length; i++) {
				const inner = source.charCodeAt(i);
				if (inner === 92) i++;
				else if (inner === code) break;
			}
			continue;
		}
		if (code === 47) {
			const next = source.charCodeAt(i + 1);
			if (next === 47) {
				const end = source.indexOf("\n", i + 2);
				i = end === -1 ? source.length : end;
				continue;
			}
			if (next === 42) {
				const end = source.indexOf("*/", i + 2);
				i = end === -1 ? source.length : end + 1;
				continue;
			}
			if (!startsRegExp(source, i)) continue;
			let inClass = false;
			for (i++; i < source.length; i++) {
				const inner = source.charCodeAt(i);
				if (inner === 92) i++;
				else if (inner === 91) inClass = true;
				else if (inner === 93) inClass = false;
				else if (inner === 10) break;
				else if (inner === 47 && !inClass) break;
			}
			continue;
		}
		// `r` of `require`, as a whole word that is not a member access
		if (code !== 114 || !source.startsWith("require", i)) continue;
		if (isIdentifierChar(source.charCodeAt(i + 7))) continue;
		if (i > 0) {
			const before = source.charCodeAt(i - 1);
			if (isIdentifierChar(before)) continue;
			// a dot is a member access, except as the last of a `...` spread
			if (before === 46 && !source.startsWith("...", i - 3)) continue;
		}
		let j = skipTrivia(source, i + 7);
		if (source.charCodeAt(j) !== 40) continue;
		j = skipTrivia(source, j + 1);
		// a cast wraps the specifier in its own parens: `require(/** … */ ("x"))`
		let parens = 0;
		while (source.charCodeAt(j) === 40) {
			parens++;
			j = skipTrivia(source, j + 1);
		}
		const literal = readStringLiteral(source, j);
		if (literal === undefined) {
			i += 6;
			continue;
		}
		j = skipTrivia(source, literal.end);
		while (parens > 0 && source.charCodeAt(j) === 41) {
			parens--;
			j = skipTrivia(source, j + 1);
		}
		if (parens !== 0 || source.charCodeAt(j) !== 41) {
			i += 6;
			continue;
		}
		specifiers.add(literal.value);
		i = j;
	}
	return specifiers;
};

/**
 * Parses `input` into an estree program. The entry point for everything outside
 * this module, so no caller needs the parser class or its underlying types.
 * @param {string} input source code
 * @param {ParserOptions} options parser options
 * @returns {import("estree").Program} the parsed program
 */
const parse = (input, options) =>
	/** @type {import("estree").Program} */ (
		WebpackParser.parse(input, /** @type {Options} */ (options))
	);

// A field of a `SyntaxTree` node holds a child node, a list of them, a string,
// a flag, a literal's value, or a string the node lacks while unset.
const FIELD_NODE = 0;
const FIELD_LIST = 1;
const FIELD_STRING = 2;
const FIELD_FLAG = 3;
const FIELD_VALUE = 4;
const FIELD_OPTIONAL = 5;

/** @type {Record<string, number>} */
const FIELD_KINDS = {
	node: FIELD_NODE,
	list: FIELD_LIST,
	string: FIELD_STRING,
	flag: FIELD_FLAG,
	value: FIELD_VALUE,
	optional: FIELD_OPTIONAL
};

/**
 * @typedef {object} TreeField
 * @property {string} name the property the ESTree object holds it under
 * @property {number} kind what the field holds
 * @property {number} slot its place in the node's record, or its `Flag` bit
 */

/**
 * @typedef {object} TreeNodeType
 * @property {string} name the ESTree type
 * @property {TreeField[]} fields the fields, in the object's order
 * @property {Map<string, TreeField>} byName the fields by name
 * @property {number} slotCount how many slots a node's record takes
 * @property {Int8Array} fieldSlots by `Field`, twice the slot plus one for a list, `-1` where the type lacks it
 * @property {Int8Array} partSlots by `Part`, the slot, `-1` where the type lacks it
 * @property {number} nameSlot the slot of an identifier's name, `-1` for none
 * @property {number} valueSlot the slot of a literal's value, `-1` for none
 */

/** @typedef {string | number | boolean | bigint | RegExp | null | TemplateElement["value"]} TreeValue what a `SyntaxTree` value field holds: a name, an operator, a keyword, or a literal's or template element's value */

/** @typedef {EstreeObject & { [field: string]: EstreeObject | (EstreeObject | null)[] | TreeValue | { pattern: string, flags: string } | undefined }} EstreeTarget an ESTree object while its fields are built */

/** @type {(TreeNodeType | undefined)[] | undefined} */
let treeNodeTypes;

/**
 * Reads the node types off `NODE_FIELDS` on first use, so a parse that never
 * builds a `SyntaxTree` allocates none of them.
 * @returns {(TreeNodeType | undefined)[]} every node type, by its `NodeType`
 */
const getTreeNodeTypes = () => {
	if (treeNodeTypes !== undefined) return treeNodeTypes;
	/** @type {(TreeNodeType | undefined)[]} */
	const types = [undefined];
	const fieldCount = Object.keys(Field).length + 1;
	const partCount = Object.keys(Part).length + 1;
	for (const name of Object.keys(NODE_FIELDS)) {
		const spec = NODE_FIELDS[name];
		/** @type {TreeNodeType} */
		const type = {
			name,
			fields: [],
			byName: new Map(),
			slotCount: 0,
			fieldSlots: new Int8Array(fieldCount).fill(-1),
			partSlots: new Int8Array(partCount).fill(-1),
			nameSlot: -1,
			valueSlot: -1
		};
		for (const part of spec === "" ? [] : spec.split(" ")) {
			const [fieldName, kindName, slotText] = part.split(":");
			const kind = FIELD_KINDS[kindName];
			const slot = Number(slotText);
			/** @type {TreeField} */
			const field = { name: fieldName, kind, slot };
			type.fields.push(field);
			type.byName.set(fieldName, field);
			if (kind === FIELD_FLAG) continue;
			type.slotCount++;
			if (kind === FIELD_NODE || kind === FIELD_LIST) {
				type.fieldSlots[
					/** @type {Record<string, number>} */ (Field)[fieldName]
				] = slot * 2 + (kind === FIELD_LIST ? 1 : 0);
			} else if (kind === FIELD_VALUE) {
				type.valueSlot = slot;
			} else if (fieldName === "name") {
				type.nameSlot = slot;
			} else {
				type.partSlots[
					/** @type {Record<string, number>} */ (Part)[fieldName]
				] = slot;
			}
		}
		types[/** @type {Record<string, number>} */ (NodeType)[name]] = type;
	}
	return (treeNodeTypes = types);
};

/**
 * A node `toEstreeObject` builds: ESTree's fields, with `range` read off its
 * offsets on first access, as the parser's own nodes serve it.
 */
class EstreeObject extends LazyLocNode {
	/**
	 * @param {string} type the ESTree type
	 * @param {number} start where it starts
	 * @param {number} end where it ends
	 */
	constructor(type, start, end) {
		super(start);
		this.type = type;
		this.end = end;
	}
}

/**
 * @template {Int32Array | Uint16Array | Uint8Array} T
 * @param {T} array the column to grow
 * @param {number} capacity the length it needs at least
 * @returns {T} the column, or a copy holding at least `capacity` entries
 */
const growColumn = (array, capacity) => {
	if (capacity <= array.length) return array;
	const copy = new /** @type {new (length: number) => T} */ (array.constructor)(
		Math.max(array.length * 2, capacity, 1024)
	);
	copy.set(array);
	return copy;
};

/**
 * A JavaScript syntax tree held as a struct of arrays: a node is an integer
 * id from 1, `0` is no node, and it reads as the CSS and HTML paths do.
 */
class SyntaxTree {
	constructor() {
		this._count = 0;
		this._types = new Uint8Array(0);
		this._starts = new Int32Array(0);
		this._ends = new Int32Array(0);
		this._flags = new Uint16Array(0);
		this._bases = new Int32Array(0);
		this._complete = new Uint8Array(0);
		this._slots = new Int32Array(0);
		this._slotTop = 0;
		this._listStarts = new Int32Array(0);
		this._listLengths = new Int32Array(0);
		this._listCount = 0;
		this._listItems = new Int32Array(0);
		this._listItemTop = 0;
		/** @type {(TreeValue | undefined)[]} */
		this._values = [];
		/** @type {(EstreeObject | undefined)[]} */
		this._objects = [];
	}

	/**
	 * Forgets every node, keeping the columns for the next tree.
	 * @returns {void}
	 */
	reset() {
		this._complete.fill(0, 0, this._count + 1);
		this._count = 0;
		this._slotTop = 0;
		this._listCount = 0;
		this._listItemTop = 0;
		this._values.length = 0;
		this._objects = [];
	}

	/**
	 * @param {number} type the node's `NodeType`
	 * @param {number} start where it starts
	 * @param {number} end where it ends
	 * @returns {number} the new node
	 */
	add(type, start, end) {
		const descriptor = getTreeNodeTypes()[type];
		if (descriptor === undefined) {
			throw new Error(`Unknown node type ${type}`);
		}
		const node = ++this._count;
		if (node >= this._types.length) {
			const capacity = node + 1;
			this._types = growColumn(this._types, capacity);
			this._starts = growColumn(this._starts, capacity);
			this._ends = growColumn(this._ends, capacity);
			this._flags = growColumn(this._flags, capacity);
			this._bases = growColumn(this._bases, capacity);
			this._complete = growColumn(this._complete, capacity);
		}
		const base = this._slotTop;
		this._slotTop += descriptor.slotCount;
		this._slots = growColumn(this._slots, this._slotTop);
		this._slots.fill(0, base, this._slotTop);
		this._types[node] = type;
		this._starts[node] = start;
		this._ends[node] = end;
		this._flags[node] = 0;
		this._bases[node] = base;
		return node;
	}

	/**
	 * Sets one field of a node: a child node, an array of them for a list,
	 * `0` or null where ESTree has null.
	 * @param {number} node the node
	 * @param {string} name the field's ESTree name
	 * @param {number | (number | null)[] | TreeValue | undefined} value what it holds
	 * @returns {void}
	 */
	set(node, name, value) {
		const descriptor = /** @type {TreeNodeType} */ (
			getTreeNodeTypes()[this._types[node]]
		);
		const field = descriptor.byName.get(name);
		if (field === undefined) {
			throw new Error(`${descriptor.name} has no field ${name}`);
		}
		const index = this._bases[node] + field.slot;
		switch (field.kind) {
			case FIELD_NODE:
				this._slots[index] = value === null ? 0 : /** @type {number} */ (value);
				break;
			case FIELD_LIST:
				this._slots[index] = this._addList(
					/** @type {(number | null)[]} */ (value)
				);
				break;
			case FIELD_FLAG:
				if (value) this._flags[node] |= field.slot;
				else this._flags[node] &= ~field.slot;
				break;
			default:
				// A value's slot is its index in `_values` plus one, `0` for none.
				this._slots[index] =
					(field.kind === FIELD_OPTIONAL && value === undefined) ||
					(field.kind !== FIELD_VALUE && value === null)
						? 0
						: this._values.push(/** @type {TreeValue | undefined} */ (value));
		}
	}

	/**
	 * Writes a parsed node and everything under it, its children taking ids
	 * after it in source order; a child already written stands as its id.
	 * @param {import("estree").Node} root the node the parser made
	 * @returns {number} its id
	 */
	addEstree(root) {
		const types = getTreeNodeTypes();
		// Pairs of a node and where its id goes: a slot from 0, a list item as
		// `-2 - index`, `-1` for the root.
		/** @type {(Node | number)[]} */
		const work = [/** @type {Node} */ (root), -1];
		let rootNode = 0;
		while (work.length > 0) {
			const target = /** @type {number} */ (work.pop());
			const object =
				/** @type {{ type: string, start: number, end: number, [field: string]: Node | (Node | number | null)[] | string | number | boolean | bigint | RegExp | null | undefined }} */ (
					work.pop()
				);
			const type = /** @type {Record<string, number>} */ (NodeType)[
				object.type
			];
			const descriptor = types[type];
			if (descriptor === undefined) {
				throw new Error(`Unknown node type ${object.type}`);
			}
			const node = this.add(type, object.start, object.end);
			if (target === -1) rootNode = node;
			else if (target >= 0) this._slots[target] = node;
			else this._listItems[-2 - target] = node;
			const base = this._bases[node];
			const fields = descriptor.fields;
			// Pushed last field first, so the first comes off the stack first.
			for (let f = fields.length - 1; f >= 0; f--) {
				const field = fields[f];
				const value = object[field.name];
				switch (field.kind) {
					case FIELD_NODE:
						if (typeof value === "number") {
							this._slots[base + field.slot] = value;
						} else if (value) {
							work.push(/** @type {Node} */ (value), base + field.slot);
						}
						break;
					case FIELD_LIST: {
						// An edition before attributes leaves them off an import or export.
						const length =
							value === undefined
								? 0
								: /** @type {(Node | number | null)[]} */ (value).length;
						const list = this._reserveList(length);
						this._slots[base + field.slot] = list;
						const start = this._listStarts[list];
						for (let i = length - 1; i >= 0; i--) {
							const item = /** @type {(Node | number | null)[]} */ (value)[i];
							if (typeof item === "number") {
								this._listItems[start + i] = item;
							} else if (item !== null) {
								work.push(item, -2 - (start + i));
							}
						}
						break;
					}
					case FIELD_FLAG:
						if (value) this._flags[node] |= field.slot;
						break;
					case FIELD_OPTIONAL:
						if (value !== undefined) {
							this._slots[base + field.slot] = this._values.push(
								/** @type {TreeValue} */ (value)
							);
						}
						break;
					case FIELD_VALUE:
						this._slots[base + field.slot] = this._values.push(
							/** @type {TreeValue} */ (value)
						);
						break;
					default:
						if (value !== null) {
							this._slots[base + field.slot] = this._values.push(
								/** @type {TreeValue | undefined} */ (value)
							);
						}
				}
			}
		}
		return rootNode;
	}

	/**
	 * @param {(number | null)[]} nodes the list's items, null for a hole
	 * @returns {number} the list, from 1
	 */
	_addList(nodes) {
		const list = this._reserveList(nodes.length);
		const start = this._listStarts[list];
		for (let i = 0; i < nodes.length; i++) {
			const item = nodes[i];
			this._listItems[start + i] = item === null ? 0 : item;
		}
		return list;
	}

	/**
	 * @param {number} length how many items the list holds, each `0` until set
	 * @returns {number} the list, from 1
	 */
	_reserveList(length) {
		const list = ++this._listCount;
		this._listStarts = growColumn(this._listStarts, list + 1);
		this._listLengths = growColumn(this._listLengths, list + 1);
		const start = this._listItemTop;
		this._listItemTop += length;
		this._listItems = growColumn(this._listItems, this._listItemTop);
		this._listItems.fill(0, start, this._listItemTop);
		this._listStarts[list] = start;
		this._listLengths[list] = length;
		return list;
	}

	/**
	 * @param {number} node the node
	 * @returns {number} its `NodeType`
	 */
	type(node) {
		return this._types[node];
	}

	/**
	 * @param {number} node the node
	 * @returns {number} where it starts in the input
	 */
	start(node) {
		return this._starts[node];
	}

	/**
	 * @param {number} node the node
	 * @returns {number} where it ends in the input
	 */
	end(node) {
		return this._ends[node];
	}

	/**
	 * @param {number} node the node
	 * @returns {[number, number]} where it starts and ends in the input
	 */
	range(node) {
		return [this._starts[node], this._ends[node]];
	}

	/**
	 * @param {number} node the node
	 * @returns {string} an identifier's name, `""` for any other node
	 */
	name(node) {
		const slot = /** @type {TreeNodeType} */ (
			getTreeNodeTypes()[this._types[node]]
		).nameSlot;
		if (slot === -1) return "";
		const value = this._slots[this._bases[node] + slot];
		return value === 0 ? "" : /** @type {string} */ (this._values[value - 1]);
	}

	/**
	 * @param {number} node the node
	 * @returns {TreeValue | undefined} a literal's or template element's value, `""` for any other node
	 */
	value(node) {
		const slot = /** @type {TreeNodeType} */ (
			getTreeNodeTypes()[this._types[node]]
		).valueSlot;
		if (slot === -1) return "";
		const value = this._slots[this._bases[node] + slot];
		return value === 0 ? "" : this._values[value - 1];
	}

	/**
	 * @param {number} field the `Field`
	 * @param {number} node the node
	 * @returns {number} how many nodes the field holds, at most one for a single node
	 */
	fieldCount(field, node) {
		const encoded = /** @type {TreeNodeType} */ (
			getTreeNodeTypes()[this._types[node]]
		).fieldSlots[field];
		if (encoded === -1) return 0;
		const slot = this._slots[this._bases[node] + (encoded >> 1)];
		if ((encoded & 1) === 0) return slot === 0 ? 0 : 1;
		return this._listLengths[slot];
	}

	/**
	 * @param {number} i the item's index
	 * @param {number} field the `Field`
	 * @param {number} node the node
	 * @returns {number} the field's `i`-th node, `0` past the end or for a hole
	 */
	field(i, field, node) {
		const encoded = /** @type {TreeNodeType} */ (
			getTreeNodeTypes()[this._types[node]]
		).fieldSlots[field];
		if (encoded === -1) return 0;
		const slot = this._slots[this._bases[node] + (encoded >> 1)];
		if ((encoded & 1) === 0) return i === 0 ? slot : 0;
		return i < this._listLengths[slot]
			? this._listItems[this._listStarts[slot] + i]
			: 0;
	}

	/**
	 * @param {number} flag the `Flag`
	 * @param {number} node the node
	 * @returns {boolean} whether the flag is set
	 */
	flag(flag, node) {
		return (this._flags[node] & flag) !== 0;
	}

	/**
	 * @param {number} part the `Part`
	 * @param {number} node the node
	 * @returns {string | null} the part's text, null where the node has none
	 */
	textOf(part, node) {
		const slot = /** @type {TreeNodeType} */ (
			getTreeNodeTypes()[this._types[node]]
		).partSlots[part];
		if (slot === -1) return null;
		const value = this._slots[this._bases[node] + slot];
		return value === 0 ? null : /** @type {string} */ (this._values[value - 1]);
	}
}

/**
 * Adds a regular expression's `regex` and a bigint's `bigint`, which ESTree
 * spells out beside a literal's value and the tree reads off it.
 * @param {SyntaxTree} tree the tree
 * @param {number} node the literal
 * @param {EstreeTarget} target the literal's object
 * @param {string[]=} fields the fields asked for, or every field
 * @returns {void}
 */
const buildLiteralForms = (tree, node, target, fields) => {
	const value = tree.value(node);
	const raw = tree.textOf(Part.raw, node);
	if (
		(fields === undefined || fields.includes("regex")) &&
		raw !== null &&
		raw.charCodeAt(0) === 47 &&
		!("regex" in target)
	) {
		const end = raw.lastIndexOf("/");
		target.regex = { pattern: raw.slice(1, end), flags: raw.slice(end + 1) };
	}
	if (
		(fields === undefined || fields.includes("bigint")) &&
		typeof value === "bigint" &&
		!("bigint" in target)
	) {
		target.bigint = String(value);
	}
};

/**
 * The object of a node, made with only its type and offsets the first time.
 * @param {SyntaxTree} tree the tree holding the node
 * @param {number} node the node, or `0` for none
 * @returns {EstreeObject | null} its object, or null for none
 */
const estreeObjectOf = (tree, node) => {
	if (node === 0) return null;
	let object = tree._objects[node];
	if (object === undefined) {
		object = new EstreeObject(
			/** @type {TreeNodeType} */ (getTreeNodeTypes()[tree._types[node]]).name,
			tree._starts[node],
			tree._ends[node]
		);
		tree._objects[node] = object;
	}
	return object;
};

/**
 * Builds a node's fields onto its object, pushing each child whose object
 * still lacks fields onto `pending` rather than recursing into it.
 * @param {SyntaxTree} tree the tree holding the node
 * @param {number} node the node
 * @param {string[] | undefined} fields the fields to build, or every field
 * @param {number[]} pending the nodes left to build
 * @returns {void}
 */
const buildFields = (tree, node, fields, pending) => {
	const type = tree._types[node];
	const target = /** @type {EstreeTarget} */ (estreeObjectOf(tree, node));
	const base = tree._bases[node];
	for (const field of /** @type {TreeNodeType} */ (getTreeNodeTypes()[type])
		.fields) {
		if (fields !== undefined && !fields.includes(field.name)) continue;
		if (field.name in target) continue;
		if (field.kind === FIELD_FLAG) {
			target[field.name] = (tree._flags[node] & field.slot) !== 0;
			continue;
		}
		const slot = tree._slots[base + field.slot];
		switch (field.kind) {
			case FIELD_NODE:
				target[field.name] = estreeObjectOf(tree, slot);
				if (slot !== 0 && tree._complete[slot] === 0) pending.push(slot);
				break;
			case FIELD_LIST: {
				const start = tree._listStarts[slot];
				const length = tree._listLengths[slot];
				/** @type {(EstreeObject | null)[]} */
				const items = [];
				for (let i = 0; i < length; i++) {
					const item = tree._listItems[start + i];
					items.push(estreeObjectOf(tree, item));
					if (item !== 0 && tree._complete[item] === 0) pending.push(item);
				}
				target[field.name] = items;
				break;
			}
			case FIELD_OPTIONAL:
				if (slot !== 0) target[field.name] = tree._values[slot - 1];
				break;
			default:
				target[field.name] = slot === 0 ? null : tree._values[slot - 1];
		}
	}
	if (type === NodeType.Literal) buildLiteralForms(tree, node, target, fields);
	if (fields === undefined) tree._complete[node] = 1;
};

/**
 * The ESTree object of a node, the same object each time it is asked for.
 * With `fields`, only those are built, each a whole subtree; a later call adds
 * what it lacks.
 * @param {SyntaxTree} tree the tree holding the node
 * @param {number} node the node, or `0` for none
 * @param {string[]=} fields the fields to build, or every field
 * @returns {import("estree").Node | null} the node's object, or null for none
 */
const toEstreeObject = (tree, node, fields) => {
	const object = /** @type {Node | null} */ (estreeObjectOf(tree, node));
	if (object === null || tree._complete[node] === 1) return object;
	/** @type {number[]} */
	const pending = [];
	buildFields(tree, node, fields, pending);
	while (pending.length > 0) {
		const child = /** @type {number} */ (pending.pop());
		buildFields(tree, child, undefined, pending);
	}
	return object;
};

module.exports.BIND_FUNCTION = BIND_FUNCTION;
module.exports.BIND_LEXICAL = BIND_LEXICAL;
module.exports.BIND_NONE = BIND_NONE;
module.exports.BIND_OUTSIDE = BIND_OUTSIDE;
module.exports.BIND_SIMPLE_CATCH = BIND_SIMPLE_CATCH;
module.exports.BIND_VAR = BIND_VAR;
module.exports.Field = Field;
module.exports.Flag = Flag;
module.exports.DestructuringErrors = DestructuringErrors;
module.exports.Label = Label;
module.exports.ParserNode = ParserNode;
module.exports.Parser = SyntaxParser;
module.exports.Position = Position;
module.exports.SCOPE_ARROW = SCOPE_ARROW;
module.exports.SCOPE_ASYNC = SCOPE_ASYNC;
module.exports.SCOPE_CLASS_FIELD_INIT = SCOPE_CLASS_FIELD_INIT;
module.exports.SCOPE_CLASS_STATIC_BLOCK = SCOPE_CLASS_STATIC_BLOCK;
module.exports.SCOPE_DIRECT_SUPER = SCOPE_DIRECT_SUPER;
module.exports.SCOPE_FUNCTION = SCOPE_FUNCTION;
module.exports.SCOPE_GENERATOR = SCOPE_GENERATOR;
module.exports.SCOPE_SIMPLE_CATCH = SCOPE_SIMPLE_CATCH;
module.exports.SCOPE_SUPER = SCOPE_SUPER;
module.exports.SCOPE_SWITCH = SCOPE_SWITCH;
module.exports.SCOPE_TOP = SCOPE_TOP;
module.exports.SCOPE_VAR = SCOPE_VAR;
module.exports.Scope = ParserScope;
module.exports.ParserSourceLocation = ParserSourceLocation;
module.exports.TokContext = TokContext;
module.exports.Token = Token;
module.exports.TokenType = TokenType;
module.exports.codePointToString = codePointToString;
module.exports.defaultOptions = defaultOptions;
module.exports.functionFlags = functionFlags;
module.exports.getLineInfo = getLineInfo;
module.exports.getOptions = getOptions;
module.exports.isIdentifierChar = isIdentifierChar;
module.exports.isIdentifierStart = isIdentifierStart;
module.exports.isLocalVariableAccess = isLocalVariableAccess;
module.exports.isNewLine = isNewLine;
module.exports.isPrivateFieldAccess = isPrivateFieldAccess;
module.exports.keywordTypes = keywordTypes;
module.exports.lineBreak = lineBreak;
module.exports.lineBreakG = lineBreakG;
module.exports.nextLineBreak = nextLineBreak;
module.exports.nonASCIIwhitespace = nonASCIIwhitespace;
module.exports.skipWhiteSpace = skipWhiteSpace;
module.exports.stringToNumber = stringToNumber;
module.exports.tokContexts = tokContexts;
module.exports.tokTypes = tokTypes;
module.exports.wordsRegexp = wordsRegexp;
module.exports.BLOCK_DECLARATIONS = BLOCK_DECLARATIONS;
module.exports.MAX_MASKED_INDEX = MAX_MASKED_INDEX;
module.exports.HOISTED_DECLARATIONS = HOISTED_DECLARATIONS;
module.exports.MODULE_DECLARATIONS = MODULE_DECLARATIONS;
module.exports.LEGACY_ASSERT_ATTRIBUTES = LEGACY_ASSERT_ATTRIBUTES;
module.exports.NodeType = NodeType;
module.exports.ParserPosition = ParserPosition;
module.exports.Part = Part;
module.exports.SyntaxTree = SyntaxTree;
module.exports.WebpackParser = WebpackParser;
module.exports.buildLineStarts = buildLineStarts;
module.exports.collectCjsRequireSpecifiers = collectCjsRequireSpecifiers;
module.exports.declaresIntoBlock = declaresIntoBlock;
module.exports.overflowFrom = overflowFrom;
module.exports.hasOctalEscape = hasOctalEscape;
module.exports.parse = parse;
module.exports.positionAt = positionAt;
module.exports.releaseParserCaches = releaseParserCaches;
module.exports.toEstreeObject = toEstreeObject;