mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
7946ebaea3
Perf optimization to merging visit function when compiling AST visitors. Previously we took an array of `VisitProp` objects, extracted the `fn` property from each into a temporary array, and then called the merger function using spread `merger(...visitFns)`. Remove the temp array and the mapping entirely by just passing the arrays of `VisitProp` objects directly to the merger function, and destructuring it internally. This is simpler and also more performant. My concern with taking this approach originally was that the merged functions might capture the `VisitProp[]` passed in to outer function in its scope, which would bloat them. These functions are extremely hot - called 1,000s or 10,000s of times during AST visitation - so even a small slow-down in these functions would be measurable. But analysis using V8 built-in functions (`%DebugPrint`) to inspect what these functions capture in their scopes reveals that this concern was unwarranted - they only capture the variables that they actually use, and do not capture to `VisitProp[]` arrays. So they are unchanged after this PR.
787 lines
27 KiB
TypeScript
787 lines
27 KiB
TypeScript
// Functions to compile 1 or more visitor objects into a single compiled visitor.
|
|
//
|
|
// # Visitor objects
|
|
//
|
|
// Visitor objects which are generated by rules' `create` functions have keys being any of:
|
|
//
|
|
// 1. Name of an AST type
|
|
// 2. Arbitrary selector (e.g. `:matches(BinaryExpression, LogicalExpression)`)
|
|
// 3. Name of an AST type, postfixed with `:exit`
|
|
// 4. Arbitrary selector, postfixed with `:exit`
|
|
// 5. CFG handler name (e.g. `onCodePathStart`)
|
|
//
|
|
// Each property value must be a function that handles that AST node / selector target / CFG event arguments.
|
|
//
|
|
// e.g.:
|
|
//
|
|
// ```
|
|
// {
|
|
// BinaryExpression(node) {
|
|
// // Called when entering a `BinaryExpression` node
|
|
// },
|
|
// 'BinaryExpression:exit'(node) {
|
|
// // Called when exiting a `BinaryExpression` node
|
|
// },
|
|
// ':matches(BinaryExpression, LogicalExpression)'(node) {
|
|
// // Called when entering a `BinaryExpression` or `LogicalExpression` node
|
|
// },
|
|
// '*:exit'(node) {
|
|
// // Called when exiting any node
|
|
// },
|
|
// onCodePathStart(codePath, node) {
|
|
// // Called when CFG event occurs
|
|
// },
|
|
// }
|
|
// ```
|
|
//
|
|
// # Compiled visitor
|
|
//
|
|
// Compiled visitor is an array with `TYPE_IDS_COUNT` length, keyed by the ID of the node type / CFG event.
|
|
// `NODE_TYPE_IDS_MAP` maps from type name / CFG event name to ID.
|
|
//
|
|
// Each element of compiled array is one of:
|
|
// * No visitor for this type = `null`.
|
|
// * Visitor for leaf node = visit function.
|
|
// * Visitor for non-leaf node = object of form `{ enter, exit }`,
|
|
// where each property is either a visitor function or `null`.
|
|
// * Visitor for CFG event = visit function.
|
|
//
|
|
// e.g.:
|
|
//
|
|
// ```
|
|
// [
|
|
// // Leaf nodes
|
|
// function(node) { /* do stuff */ },
|
|
// // ...
|
|
//
|
|
// // Non-leaf nodes
|
|
// {
|
|
// enter: function(node) { /* do stuff */ },
|
|
// exit: null,
|
|
// },
|
|
// // ...
|
|
//
|
|
// // CFG events
|
|
// function(codePath, node) { /* do stuff */ },
|
|
// ]
|
|
// ```
|
|
//
|
|
// # Object reuse
|
|
//
|
|
// No more than 1 compiled visitor exists at any time, so we reuse a single array `compiledVisitor`,
|
|
// rather than creating a new array for each file being linted.
|
|
//
|
|
// To compile a visitor and use it:
|
|
//
|
|
// 1. Call `addVisitorToCompiled` with each visitor object.
|
|
// 2. Call `finalizeCompiledVisitor` once.
|
|
// 3. Walk AST with compiled visitor.
|
|
// 4. Call `resetCompiledVisitor` once.
|
|
//
|
|
// We also recycle:
|
|
//
|
|
// * `{ enter, exit }` objects which are stored in compiled visitor.
|
|
// * `VisitProp` objects which are used while compiling visitor.
|
|
// * `{ enter: [], exit: [] }` objects which are used while compiling visitor.
|
|
//
|
|
// The aim is to reduce pressure on the garbage collector. All these recycled objects are long-lived
|
|
// and will graduate to "old space", which leaves as much capacity as possible in "new space"
|
|
// for objects created by user code in visitors. If ephemeral user-created objects all fit in new space,
|
|
// it will avoid full GC runs, which should greatly improve performance.
|
|
|
|
import {
|
|
LEAF_NODE_TYPES_COUNT,
|
|
NODE_TYPE_IDS_MAP,
|
|
NODE_TYPES_COUNT,
|
|
TYPE_IDS_COUNT,
|
|
} from "../generated/type_ids.ts";
|
|
import {
|
|
parseSelector,
|
|
wrapVisitFnWithSelectorMatch,
|
|
EXIT_FLAG,
|
|
IDENTIFIER_COUNT_INCREMENT,
|
|
} from "./selector.ts";
|
|
import { debugAssert, debugAssertIsNonNull, typeAssertIs } from "../utils/asserts.ts";
|
|
|
|
import type { Node } from "./types.ts";
|
|
import type { VisitorObject } from "../generated/visitor.d.ts";
|
|
|
|
// Visit function for a specific AST node type.
|
|
export type VisitFn = (node: Node) => void;
|
|
|
|
// Visit function for a specific CFG event.
|
|
export type CfgVisitFn = (...args: unknown[]) => void;
|
|
|
|
// Enter+exit pair, for non-leaf nodes in compiled visitor.
|
|
export interface EnterExit {
|
|
enter: VisitFn | null;
|
|
exit: VisitFn | null;
|
|
}
|
|
|
|
// Visitor function and details of its specificity.
|
|
interface VisitProp<Visit = VisitFn> {
|
|
// Visit function.
|
|
// Only `null` in between compilations - this field is only nullable to allow `VisitProp` objects to be reused,
|
|
// while allowing the `VisitFn` to be garbage collected.
|
|
fn: Visit | null;
|
|
// Specificity of visitor function.
|
|
specificity: number;
|
|
// Selector string e.g. `Program`, `Program > ExpressionStatement`, `*`.
|
|
// Does not include trailing `:exit`.
|
|
// Used as tie-breaker for specificity.
|
|
// Only `null` in between compilations - this field is only nullable to allow `VisitProp` objects to be reused,
|
|
// while allowing the string to be garbage collected.
|
|
selectorStr: string | null;
|
|
}
|
|
|
|
// Visitor function and details of its specificity, where the visitor function is for a CFG event.
|
|
type CfgVisitProp = VisitProp<CfgVisitFn>;
|
|
|
|
// Entry and exit visitors for a non-leaf node.
|
|
interface CompilingNonLeafVisitorEntry {
|
|
// Visitor props for entry
|
|
enter: VisitProp[];
|
|
// Visitor props for exit
|
|
exit: VisitProp[];
|
|
}
|
|
|
|
// Visitor state.
|
|
// Returned by `finalizeCompiledVisitor`.
|
|
// This a "poor man's enum", which compiles down better than TS enums.
|
|
export type VisitorState = typeof VISITOR_EMPTY | typeof VISITOR_NOT_EMPTY | typeof VISITOR_CFG;
|
|
|
|
export const VISITOR_EMPTY = 0; // Empty visitor (no need to walk AST)
|
|
export const VISITOR_NOT_EMPTY = 1; // Not empty visitor (need to walk AST)
|
|
export const VISITOR_CFG = 2; // Not empty visitor, and contains CFG event listeners (need to construct CFG)
|
|
|
|
// Count of non-leaf node types
|
|
const NON_LEAF_NODE_TYPES_COUNT = NODE_TYPES_COUNT - LEAF_NODE_TYPES_COUNT;
|
|
// Count of CFG event types
|
|
const CFG_EVENTS_COUNT = TYPE_IDS_COUNT - NODE_TYPES_COUNT;
|
|
|
|
// During compilation, arrays of visitor props are stored in these arrays.
|
|
// These arrays are initialized with empty arrays, and never modified thereafter.
|
|
// Only the arrays they contain are modified - they're filled up during compilation,
|
|
// and emptied at the end in `finalizeCompiledVisitor`, so can be reused for next compilation.
|
|
const compilingLeafVisitor: VisitProp[][] = [];
|
|
const compilingNonLeafVisitor: CompilingNonLeafVisitorEntry[] = [];
|
|
const compilingCfgVisitor: CfgVisitProp[][] = [];
|
|
|
|
for (let i = LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
|
|
compilingLeafVisitor.push([]);
|
|
}
|
|
|
|
for (let i = NON_LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
|
|
compilingNonLeafVisitor.push({ enter: [], exit: [] });
|
|
}
|
|
|
|
for (let i = CFG_EVENTS_COUNT; i !== 0; i--) {
|
|
compilingCfgVisitor.push([]);
|
|
}
|
|
|
|
// Compiled visitor used for visiting each file.
|
|
//
|
|
// * `VisitFn | null` for leaf nodes.
|
|
// * `EnterExit | null` for non-leaf nodes.
|
|
//
|
|
// Same array is reused for each file.
|
|
//
|
|
// Initialized with `.push()` to ensure V8 treats the array as "packed" (linear array),
|
|
// not "holey" (hash map). This is critical, as looking up elements in this array is a very hot path
|
|
// during AST visitation, and holey arrays are much slower.
|
|
// https://v8.dev/blog/elements-kinds
|
|
export const compiledVisitor: (VisitFn | EnterExit | CfgVisitFn | null)[] = [];
|
|
|
|
for (let i = TYPE_IDS_COUNT; i !== 0; i--) {
|
|
compiledVisitor.push(null);
|
|
}
|
|
|
|
// Arrays containing type IDs of types which have visit functions defined for them.
|
|
// All three share a single underlying buffer.
|
|
const activeTypeIdsBuffer = new ArrayBuffer(TYPE_IDS_COUNT);
|
|
const activeLeafVisitorTypeIds = new Uint8Array(activeTypeIdsBuffer, 0, LEAF_NODE_TYPES_COUNT);
|
|
const activeNonLeafVisitorTypeIds = new Uint8Array(
|
|
activeTypeIdsBuffer,
|
|
LEAF_NODE_TYPES_COUNT,
|
|
NON_LEAF_NODE_TYPES_COUNT,
|
|
);
|
|
const activeCfgVisitorTypeIds = new Uint8Array(
|
|
activeTypeIdsBuffer,
|
|
LEAF_NODE_TYPES_COUNT + NON_LEAF_NODE_TYPES_COUNT,
|
|
CFG_EVENTS_COUNT,
|
|
);
|
|
|
|
// Debug check that a `u8` is large enough to hold all type IDs
|
|
if (DEBUG) {
|
|
const maxTypeId = NODE_TYPE_IDS_MAP.values().reduce((max, id) => Math.max(max, id), 0);
|
|
if (maxTypeId > 255) throw new Error("All type IDs must be <= 255 to fit in a `u8`");
|
|
}
|
|
|
|
// Number of active visitors of each type
|
|
let activeLeafVisitorsCount = 0,
|
|
activeNonLeafVisitorsCount = 0,
|
|
activeCfgVisitorsCount = 0;
|
|
|
|
// `true` if `addVisitor` has been called with a visitor which visits at least one AST type
|
|
let hasActiveVisitors = false;
|
|
|
|
// Enter+exit object cache.
|
|
//
|
|
// `compiledVisitor` may contain many `{ enter, exit }` objects.
|
|
// Use this cache to reuse those objects across all visitor compilations.
|
|
//
|
|
// Pre-populate the cache with enough `EnterExit` objects for all non-leaf AST node types.
|
|
// This removes the need to check if cache contains enough objects before using it in `finalizeCompiledVisitor`.
|
|
//
|
|
// More importantly, allocating all these objects in one go will put them together in memory in "new space".
|
|
// When these objects graduate to "old space", V8 does not necessarily keep them together in memory,
|
|
// but it will tend to - it makes it more likely they'll be grouped together.
|
|
// These objects are accessed over and over during AST walk, so having them all close together in memory
|
|
// makes L1 cache misses less likely.
|
|
//
|
|
// `activeNonLeafVisitorsCount` is the number of active non-leaf visitors in `compiledVisitor`,
|
|
// and therefore the number of `EnterExit` objects currently in use.
|
|
const enterExitObjectCache: EnterExit[] = [];
|
|
|
|
for (let i = NON_LEAF_NODE_TYPES_COUNT; i !== 0; i--) {
|
|
enterExitObjectCache.push({ enter: null, exit: null });
|
|
}
|
|
|
|
// `VisitProp` object cache.
|
|
//
|
|
// During compilation, many such objects may be required, and then they're discarded in `finalizeCompiledVisitor`.
|
|
// Use this cache to reuse those objects across all visitor compilations.
|
|
//
|
|
// `visitPropsCacheNextIndex` is the index of first object in cache which is currently unused.
|
|
// It may point to the end of the cache array.
|
|
const visitPropsCache: VisitProp<VisitFn | CfgVisitFn>[] = [];
|
|
let visitPropsCacheNextIndex = 0;
|
|
|
|
/**
|
|
* Add a visitor to compiled visitor.
|
|
*
|
|
* @param visitor - Visitor object
|
|
*/
|
|
export function addVisitorToCompiled(visitor: VisitorObject): void {
|
|
if (visitor === null || typeof visitor !== "object") {
|
|
throw new TypeError("Visitor returned from `create` method must be an object");
|
|
}
|
|
|
|
// Exit if is empty visitor
|
|
const keys = Object.keys(visitor),
|
|
keysLen = keys.length;
|
|
if (keysLen === 0) return;
|
|
|
|
hasActiveVisitors = true;
|
|
|
|
// Populate cache of `VisitProp` objects with enough for all properties of `visitor`.
|
|
// After warming up over first few files, the cache will be large enough to service all files,
|
|
// and this loop will be skipped. This avoids the main loop below from having to branch repeatedly
|
|
// on whether there are enough `VisitProp` objects in cache, and to create one if not.
|
|
const visitPropsCacheRequiredLen = visitPropsCacheNextIndex + keysLen;
|
|
while (visitPropsCache.length < visitPropsCacheRequiredLen) {
|
|
visitPropsCache.push({ fn: null, specificity: 0, selectorStr: null });
|
|
}
|
|
|
|
// Populate visitors array from provided object
|
|
for (let i = 0; i < keysLen; i++) {
|
|
let name = keys[i];
|
|
|
|
const visitFn = visitor[name] as VisitFn | CfgVisitFn;
|
|
if (typeof visitFn !== "function") {
|
|
throw new TypeError(`'${name}' property of visitor object is not a function`);
|
|
}
|
|
|
|
// If this is an exit visit fn, set `specificity` initially to `EXIT_FLAG`, otherwise 0.
|
|
// Visit fns are sorted in ascending order of `specificity`.
|
|
// For leaf nodes, the high `specificity` ensures that exit visit fns are sorted after enter visit fns,
|
|
// and therefore `exit` fns are called after `enter` fns.
|
|
let specificity = 0;
|
|
|
|
const isExit = name.endsWith(":exit");
|
|
if (isExit) {
|
|
name = name.slice(0, -5);
|
|
specificity = EXIT_FLAG;
|
|
}
|
|
|
|
// Set up `VisitProp` object.
|
|
// Reuse `VisitProp` object from cache. Loop above ensures cache is filled with enough objects.
|
|
const visitProp = visitPropsCache[visitPropsCacheNextIndex++];
|
|
visitProp.fn = visitFn;
|
|
visitProp.specificity = specificity;
|
|
visitProp.selectorStr = name;
|
|
|
|
// TODO: Combine the two hashmaps `NODE_TYPE_IDS_MAP` and selectors cache into one `Map`
|
|
// to avoid 2 hashmap lookups for selectors?
|
|
let typeId = NODE_TYPE_IDS_MAP.get(name);
|
|
if (typeId !== undefined) {
|
|
// Single type visit function e.g. `Program`.
|
|
// Add a single identifier to `specificity`. Use `|=` to keep exit flag.
|
|
visitProp.specificity |= IDENTIFIER_COUNT_INCREMENT;
|
|
|
|
if (typeId < LEAF_NODE_TYPES_COUNT) {
|
|
addLeafVisitFn(typeId, visitProp as VisitProp);
|
|
} else if (typeId < NODE_TYPES_COUNT) {
|
|
addNonLeafVisitFn(typeId, visitProp as VisitProp, isExit);
|
|
} else {
|
|
addCfgVisitFn(typeId, visitProp as CfgVisitProp, isExit);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// CFG event handlers have been dealt with above
|
|
typeAssertIs<VisitFn>(visitFn);
|
|
typeAssertIs<VisitProp>(visitProp);
|
|
|
|
// `*` matches any node without any filtering, so no need to wrap it
|
|
if (name !== "*") {
|
|
// Selector.
|
|
// Parse selector.
|
|
// Wrap `visitFn` so it only executes if the selector matches.
|
|
// If selector is simple (unconditionally matches certain types e.g. `:matches(X, Y)`), skip wrapping.
|
|
const selector = parseSelector(name);
|
|
|
|
// Update specificity. Use `|=` to keep exit flag.
|
|
visitProp.specificity |= selector.specificity;
|
|
|
|
if (selector.isComplex) {
|
|
visitProp.fn = wrapVisitFnWithSelectorMatch(visitFn, selector.esquerySelector);
|
|
}
|
|
|
|
const { typeIds } = selector;
|
|
if (typeIds !== null) {
|
|
// Selector matches a specific set of node types.
|
|
// Note: If `typeIds` is empty, `visitProp` will not be used.
|
|
// We could return it to the cache, but this should be a very rare case, so don't bother.
|
|
for (let i = 0, len = typeIds.length; i < len; i++) {
|
|
const typeId = typeIds[i];
|
|
debugAssert(typeId < NODE_TYPES_COUNT, "`typeId` should be of a leaf or non-leaf node");
|
|
if (typeId < LEAF_NODE_TYPES_COUNT) {
|
|
addLeafVisitFn(typeId, visitProp);
|
|
} else {
|
|
addNonLeafVisitFn(typeId, visitProp, isExit);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// `*` selector or some other selector that matches all node types
|
|
// TODO: Add 2 arrays for "enter all" and "exit all" selectors.
|
|
// Where visitor contains multiple selectors which match all node types, this would avoid merging them
|
|
// into a single visit function 100+ times for each node type. Could just merge them once and assign the same
|
|
// merged function to all node types (except those which have other visit functions).
|
|
for (typeId = 0; typeId < LEAF_NODE_TYPES_COUNT; typeId++) {
|
|
addLeafVisitFn(typeId, visitProp);
|
|
}
|
|
for (; typeId < NODE_TYPES_COUNT; typeId++) {
|
|
addNonLeafVisitFn(typeId, visitProp, isExit);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add visit function for a leaf node to compiled visitor.
|
|
*
|
|
* Stored as just 1 function, not enter+exit pair.
|
|
*
|
|
* @param typeId - Node type ID
|
|
* @param visitProp - Visitor property
|
|
*/
|
|
function addLeafVisitFn(typeId: number, visitProp: VisitProp): void {
|
|
const visitProps = compilingLeafVisitor[typeId]!;
|
|
if (visitProps.length === 0) activeLeafVisitorTypeIds[activeLeafVisitorsCount++] = typeId;
|
|
visitProps.push(visitProp);
|
|
}
|
|
|
|
/**
|
|
* Add visit function for a non-leaf node to compiled visitor.
|
|
*
|
|
* Stored as enter+exit pair.
|
|
*
|
|
* @param typeId - Node type ID
|
|
* @param visitProp - Visitor property
|
|
* @param isExit - `true` if is an exit visit fn
|
|
*/
|
|
function addNonLeafVisitFn(typeId: number, visitProp: VisitProp, isExit: boolean): void {
|
|
const { enter, exit } = compilingNonLeafVisitor[typeId - LEAF_NODE_TYPES_COUNT]!;
|
|
if (enter.length === 0 && exit.length === 0) {
|
|
activeNonLeafVisitorTypeIds[activeNonLeafVisitorsCount++] = typeId;
|
|
}
|
|
|
|
if (isExit) {
|
|
exit.push(visitProp);
|
|
} else {
|
|
enter.push(visitProp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add visit function for a CFG event to compiled visitor.
|
|
*
|
|
* Stored as just 1 function, not enter+exit pair.
|
|
*
|
|
* @param typeId - Node type ID
|
|
* @param visitProp - Visitor property
|
|
* @param isExit - `true` if is an exit visit fn
|
|
*/
|
|
function addCfgVisitFn(typeId: number, visitProp: CfgVisitProp, isExit: boolean): void {
|
|
if (isExit) throw new Error(`Invalid visitor key: \`${visitProp.selectorStr}:exit\``);
|
|
|
|
const visitProps = compilingCfgVisitor[typeId - NODE_TYPES_COUNT]!;
|
|
if (visitProps.length === 0) activeCfgVisitorTypeIds[activeCfgVisitorsCount++] = typeId;
|
|
visitProps.push(visitProp);
|
|
}
|
|
|
|
/**
|
|
* Finalize compiled visitor.
|
|
*
|
|
* After calling this function, `compiledVisitor` is ready to be used to walk the AST.
|
|
*
|
|
* @returns - `true` if compiled visitor visits at least 1 AST type
|
|
*/
|
|
export function finalizeCompiledVisitor(): VisitorState {
|
|
if (hasActiveVisitors === false) return VISITOR_EMPTY;
|
|
|
|
// Merge visitors for leaf nodes
|
|
for (let i = activeLeafVisitorsCount - 1; i >= 0; i--) {
|
|
const typeId = activeLeafVisitorTypeIds[i];
|
|
compiledVisitor[typeId] = mergeVisitFns(compilingLeafVisitor[typeId]);
|
|
}
|
|
|
|
// Merge visitors for non-leaf nodes
|
|
for (let i = 0; i < activeNonLeafVisitorsCount; i++) {
|
|
const typeId = activeNonLeafVisitorTypeIds[i];
|
|
const entry = compilingNonLeafVisitor[typeId - LEAF_NODE_TYPES_COUNT];
|
|
|
|
// Reuse `EnterExit` object from cache.
|
|
// Cache is pre-populated with enough objects that `i` cannot be out of bounds.
|
|
const enterExit = enterExitObjectCache[i];
|
|
debugAssertIsNonNull(enterExit, "`enterExit` should not be null");
|
|
|
|
// Merge enter and exit visitors
|
|
const enterArr = entry.enter;
|
|
if (enterArr.length !== 0) enterExit.enter = mergeVisitFns(enterArr);
|
|
|
|
const exitArr = entry.exit;
|
|
if (exitArr.length !== 0) enterExit.exit = mergeVisitFns(exitArr);
|
|
|
|
// Store merged enter-exit object in compiled visitor
|
|
compiledVisitor[typeId] = enterExit;
|
|
}
|
|
|
|
// Merge visitors for CFG events
|
|
let visitState: VisitorState = VISITOR_NOT_EMPTY;
|
|
|
|
if (activeCfgVisitorsCount > 0) {
|
|
visitState = VISITOR_CFG;
|
|
|
|
for (let i = activeCfgVisitorsCount - 1; i >= 0; i--) {
|
|
const typeId = activeCfgVisitorTypeIds[i];
|
|
compiledVisitor[typeId] = mergeCfgVisitFns(compilingCfgVisitor[typeId - NODE_TYPES_COUNT]);
|
|
}
|
|
|
|
// Reset state, ready for next time
|
|
activeCfgVisitorsCount = 0;
|
|
}
|
|
|
|
// Reset `visitPropsCache`.
|
|
// Setting these properties to `null` is not necessary for correctness, but it allows them to be garbage collected.
|
|
// `specificity` is only an SMI (stored inline), and is overwritten in `addVisitorToCompiled`, so no need to reset it.
|
|
for (let i = visitPropsCacheNextIndex - 1; i >= 0; i--) {
|
|
const visitProp = visitPropsCache[i];
|
|
visitProp.fn = null;
|
|
visitProp.selectorStr = null;
|
|
}
|
|
visitPropsCacheNextIndex = 0;
|
|
|
|
// Reset state, ready for next time.
|
|
// `activeNonLeafVisitorsCount` is not reset, as it's used later in `resetCompiledVisitor`.
|
|
activeLeafVisitorsCount = 0;
|
|
|
|
hasActiveVisitors = false;
|
|
|
|
return visitState;
|
|
}
|
|
|
|
/**
|
|
* Reset compiled visitor.
|
|
*
|
|
* This frees visit functions stored in `compiledVisitor`, and makes them eligible for garbage collection.
|
|
*
|
|
* After calling this function, `compiledVisitor` is in a clean state, ready for next file's visitor to be compiled.
|
|
*/
|
|
export function resetCompiledVisitor(): void {
|
|
// Reset `compiledVisitor` array
|
|
compiledVisitor.fill(null);
|
|
|
|
// Reset `EnterExit` objects
|
|
for (let i = 0; i < activeNonLeafVisitorsCount; i++) {
|
|
const enterExit = enterExitObjectCache[i];
|
|
enterExit.enter = null;
|
|
enterExit.exit = null;
|
|
}
|
|
activeNonLeafVisitorsCount = 0;
|
|
}
|
|
|
|
/**
|
|
* Merger that merges visit functions for an AST node type.
|
|
*/
|
|
type Merger = (visitProps: VisitProp[]) => VisitFn;
|
|
|
|
/**
|
|
* Merger that merges handler functions for a CFG event.
|
|
*/
|
|
type CfgMerger = (visitProps: CfgVisitProp[]) => CfgVisitFn;
|
|
|
|
/**
|
|
* Merge array of visit functions into a single function, which calls each of input functions in turn.
|
|
*
|
|
* The array passed is cleared (length set to 0), so the array can be reused.
|
|
*
|
|
* The merged function is statically defined and does not contain a loop, to hopefully allow
|
|
* JS engine to heavily optimize it.
|
|
*
|
|
* `mergers` contains pre-defined functions to merge up to 5 visit functions.
|
|
* Merger functions for merging more than 5 visit functions are created dynamically on demand.
|
|
*
|
|
* @param visitProps - Array of `VisitProp` objects
|
|
* @returns Function which calls all visit functions in turn
|
|
*/
|
|
function mergeVisitFns(visitProps: VisitProp[]): VisitFn {
|
|
const numVisitFns = visitProps.length;
|
|
|
|
debugAssert(numVisitFns > 0, "`visitProps` should have at least 1 element");
|
|
|
|
let mergedFn: VisitFn;
|
|
if (numVisitFns === 1) {
|
|
// Only 1 visit function, so no need to merge
|
|
debugAssertIsNonNull(visitProps[0].fn);
|
|
mergedFn = visitProps[0].fn;
|
|
} else {
|
|
// Sort in ascending order of specificity.
|
|
// Selector string as tie-breaker.
|
|
visitProps.sort((a, b) => {
|
|
const diff = a.specificity - b.specificity;
|
|
if (diff !== 0) return diff;
|
|
|
|
const strA = a.selectorStr,
|
|
strB = b.selectorStr;
|
|
debugAssertIsNonNull(strA);
|
|
debugAssertIsNonNull(strB);
|
|
// Note: It's possible for different rules to use the same selector,
|
|
// so we need to handle equality here, as well as less than / greater than
|
|
return strA === strB ? 0 : strA < strB ? -1 : 1;
|
|
});
|
|
|
|
// Get or create merger for merging `numVisitFns` functions
|
|
while (mergers.length <= numVisitFns) {
|
|
mergers.push(null);
|
|
}
|
|
|
|
let merger = mergers[numVisitFns];
|
|
if (merger === null) merger = mergers[numVisitFns] = createMerger(numVisitFns, false);
|
|
|
|
// Merge functions
|
|
mergedFn = merger(visitProps);
|
|
}
|
|
|
|
// Empty `visitProps` array, so it can be reused
|
|
visitProps.length = 0;
|
|
|
|
return mergedFn;
|
|
}
|
|
|
|
/**
|
|
* Merge array of CFG visit functions into a single function, which calls each of input functions in turn.
|
|
*
|
|
* The difference between this function and `mergeVisitFns` is that this function is for CFG event handlers.
|
|
* Unlike all other visit functions, CFG event handlers are called with more than 1 argument.
|
|
* We keep this separate from `mergeVisitFns` because the merger functions use `...args` to pass all arguments,
|
|
* which is likely less performant than the simpler version which passes a single argument.
|
|
* AST node visitation is a lot more common than CFG event visitation, and we want to keep the common case fast.
|
|
*
|
|
* The array passed is cleared (length set to 0), so the array can be reused.
|
|
*
|
|
* The merged function is statically defined and does not contain a loop, to hopefully allow
|
|
* JS engine to heavily optimize it.
|
|
*
|
|
* `cfgMergers` contains pre-defined functions to merge up to 3 CFG visit functions.
|
|
* Merger functions for merging more than 3 visit functions are created dynamically on demand.
|
|
*
|
|
* @param visitProps - Array of `CfgVisitProp` objects
|
|
* @returns Function which calls all CFG visit functions in turn
|
|
*/
|
|
function mergeCfgVisitFns(visitProps: CfgVisitProp[]): CfgVisitFn {
|
|
const numVisitFns = visitProps.length;
|
|
|
|
debugAssert(numVisitFns > 0, "`visitProps` should have at least 1 element");
|
|
|
|
let mergedFn: CfgVisitFn;
|
|
if (numVisitFns === 1) {
|
|
// Only 1 visit function, so no need to merge
|
|
debugAssertIsNonNull(visitProps[0].fn);
|
|
mergedFn = visitProps[0].fn;
|
|
} else {
|
|
// No need to sort in order of specificity, because each rule can only have 1 handler for each CFG event
|
|
|
|
// Get or create merger for merging `numVisitFns` functions
|
|
while (cfgMergers.length <= numVisitFns) {
|
|
cfgMergers.push(null);
|
|
}
|
|
|
|
let merger = cfgMergers[numVisitFns];
|
|
if (merger === null) merger = cfgMergers[numVisitFns] = createMerger(numVisitFns, true);
|
|
|
|
// Merge functions
|
|
mergedFn = merger(visitProps);
|
|
}
|
|
|
|
// Empty `visitProps` array, so it can be reused
|
|
visitProps.length = 0;
|
|
|
|
return mergedFn;
|
|
}
|
|
|
|
/**
|
|
* Create a merger function that merges `fnCount` functions.
|
|
*
|
|
* Generated code for `fnCount: 3`, `isCfg: false` (AST node visit function merger):
|
|
* ```js
|
|
* function(p) {
|
|
* var v0 = p[0].fn,
|
|
* v1 = p[1].fn,
|
|
* v2 = p[2].fn;
|
|
* return (n) => {
|
|
* v0(n);
|
|
* v1(n);
|
|
* v2(n);
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* Generated code for `fnCount: 2`, `isCfg: true` (CFG event handler merger):
|
|
* ```js
|
|
* function(p) {
|
|
* var v0 = p[0].fn,
|
|
* v1 = p[1].fn;
|
|
* return (...a) => {
|
|
* v0(...a);
|
|
* v1(...a);
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param fnCount - Number of functions to be merged
|
|
* @param isCfg - `true` if creating a merger for CFG event handlers
|
|
* @returns Function to merge `fnCount` functions
|
|
*/
|
|
function createMerger(fnCount: number, isCfg: false): Merger;
|
|
function createMerger(fnCount: number, isCfg: true): CfgMerger;
|
|
function createMerger(fnCount: number, isCfg: boolean): Merger | CfgMerger {
|
|
const visitArgs = isCfg ? "...a" : "n";
|
|
let body = "var ";
|
|
let fnBody = `;return (${visitArgs})=>{`;
|
|
for (let i = 0; i < fnCount; i++) {
|
|
if (i !== 0) body += ",";
|
|
body += `v${i}=p[${i}].fn`;
|
|
fnBody += `v${i}(${visitArgs});`;
|
|
}
|
|
fnBody += "}";
|
|
|
|
body += fnBody;
|
|
|
|
// oxlint-disable-next-line typescript-eslint/no-implied-eval
|
|
return new Function("p", body) as Merger | CfgMerger;
|
|
}
|
|
|
|
// Pre-defined mergers for merging up to 5 functions.
|
|
//
|
|
// We use `var` statements instead of `const` to avoid the cost of TDZ checks, because merged functions
|
|
// can be on a very hot path.
|
|
const mergers: (Merger | null)[] = [
|
|
null, // No merger for 0 functions
|
|
null, // No merger for 1 function
|
|
(visitProps: VisitProp[]) => {
|
|
debugAssert(visitProps.length === 2, "`visitProps` should have 2 elements");
|
|
var visit1 = visitProps[0].fn!;
|
|
var visit2 = visitProps[1].fn!;
|
|
return (node: Node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
};
|
|
},
|
|
(visitProps: VisitProp[]) => {
|
|
debugAssert(visitProps.length === 3, "`visitProps` should have 3 elements");
|
|
var visit1 = visitProps[0].fn!;
|
|
var visit2 = visitProps[1].fn!;
|
|
var visit3 = visitProps[2].fn!;
|
|
return (node: Node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
visit3(node);
|
|
};
|
|
},
|
|
(visitProps: VisitProp[]) => {
|
|
debugAssert(visitProps.length === 4, "`visitProps` should have 4 elements");
|
|
var visit1 = visitProps[0].fn!;
|
|
var visit2 = visitProps[1].fn!;
|
|
var visit3 = visitProps[2].fn!;
|
|
var visit4 = visitProps[3].fn!;
|
|
return (node: Node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
visit3(node);
|
|
visit4(node);
|
|
};
|
|
},
|
|
(visitProps: VisitProp[]) => {
|
|
debugAssert(visitProps.length === 5, "`visitProps` should have 5 elements");
|
|
var visit1 = visitProps[0].fn!;
|
|
var visit2 = visitProps[1].fn!;
|
|
var visit3 = visitProps[2].fn!;
|
|
var visit4 = visitProps[3].fn!;
|
|
var visit5 = visitProps[4].fn!;
|
|
return (node: Node) => {
|
|
visit1(node);
|
|
visit2(node);
|
|
visit3(node);
|
|
visit4(node);
|
|
visit5(node);
|
|
};
|
|
},
|
|
];
|
|
|
|
// Pre-defined CFG mergers for merging up to 3 functions.
|
|
//
|
|
// CFG event handlers are rarely used, so we only pre-define mergers for up to 3 functions,
|
|
// unlike the AST node visit fns, where we pre-define 5.
|
|
//
|
|
// We use `var` statements instead of `const` to avoid the cost of TDZ checks, because merged functions
|
|
// can be on a very hot path.
|
|
const cfgMergers: (CfgMerger | null)[] = [
|
|
null, // No merger for 0 functions
|
|
null, // No merger for 1 function
|
|
(visitProps: CfgVisitProp[]) => {
|
|
debugAssert(visitProps.length === 2, "`visitProps` should have 2 elements");
|
|
var visit1 = visitProps[0].fn!;
|
|
var visit2 = visitProps[1].fn!;
|
|
return (...args: unknown[]) => {
|
|
visit1(...args);
|
|
visit2(...args);
|
|
};
|
|
},
|
|
(visitProps: CfgVisitProp[]) => {
|
|
debugAssert(visitProps.length === 3, "`visitProps` should have 3 elements");
|
|
var visit1 = visitProps[0].fn!;
|
|
var visit2 = visitProps[1].fn!;
|
|
var visit3 = visitProps[2].fn!;
|
|
return (...args: unknown[]) => {
|
|
visit1(...args);
|
|
visit2(...args);
|
|
visit3(...args);
|
|
};
|
|
},
|
|
];
|