|
| 1 | +// Functions to compile 1 or more visitor objects into a single compiled visitor. |
| 2 | +// |
| 3 | +// # Visitor objects |
| 4 | +// |
| 5 | +// Visitor objects which are generated by rules' `create` functions have keys being either: |
| 6 | +// * Name of an AST type. or |
| 7 | +// * Name of an AST type postfixed with `:exit`. |
| 8 | +// |
| 9 | +// Each property value must be a function that handles that AST node. |
| 10 | +// |
| 11 | +// e.g.: |
| 12 | +// |
| 13 | +// ``` |
| 14 | +// { |
| 15 | +// BinaryExpression(node) { |
| 16 | +// // Do stuff on enter |
| 17 | +// }, |
| 18 | +// 'BinaryExpression:exit'(node) { |
| 19 | +// // Do stuff on exit |
| 20 | +// }, |
| 21 | +// } |
| 22 | +// ``` |
| 23 | +// |
| 24 | +// # Compiled visitor |
| 25 | +// |
| 26 | +// Compiled visitor is an array with `NODE_TYPES_COUNT` length, keyed by the ID of the node type. |
| 27 | +// `NODE_TYPE_IDS_MAP` maps from type name to ID. |
| 28 | +// |
| 29 | +// Each element of compiled array is one of: |
| 30 | +// * No visitor for this type = `null`. |
| 31 | +// * Visitor for leaf node = visit function. |
| 32 | +// * Visitor for non-leaf node = object of form `{ enter, exit }`, |
| 33 | +// where each property is either a visitor function or `null`. |
| 34 | +// |
| 35 | +// e.g.: |
| 36 | +// |
| 37 | +// ``` |
| 38 | +// [ |
| 39 | +// // Leaf nodes |
| 40 | +// function(node) { /* do stuff */ }, |
| 41 | +// // ... |
| 42 | +// |
| 43 | +// // Non-leaf nodes |
| 44 | +// { |
| 45 | +// enter: function(node) { /* do stuff */ }, |
| 46 | +// exit: null, |
| 47 | +// }, |
| 48 | +// // ... |
| 49 | +// ] |
| 50 | +// ``` |
| 51 | +// |
| 52 | +// # Object reuse |
| 53 | +// |
| 54 | +// No more than 1 compiled visitor exists at any time, so we reuse a single array `compiledVisitor`, |
| 55 | +// rather than creating a new array for each file being linted. |
| 56 | +// |
| 57 | +// To compile visitors, call: |
| 58 | +// * `initCompiledVisitor` once. |
| 59 | +// * `addVisitorToCompiled` with each visitor object. |
| 60 | +// * `finalizeCompiledVisitor` once. |
| 61 | +// |
| 62 | +// After this sequence of calls, `compiledVisitor` is ready to be used to walk the AST. |
| 63 | + |
| 64 | +import types from '../../parser/generated/lazy/types.js'; |
| 65 | + |
| 66 | +const { LEAF_NODE_TYPES_COUNT, NODE_TYPE_IDS_MAP, NODE_TYPES_COUNT } = types; |
| 67 | + |
| 68 | +const { isArray } = Array; |
| 69 | + |
| 70 | +// Compiled visitor used for visiting each file. |
| 71 | +// Same array is reused for each file. |
| 72 | +// |
| 73 | +// Initialized with `.push()` to ensure V8 treats the array as "packed" (linear array), |
| 74 | +// not "holey" (hash map). This is critical, as looking up elements in this array is a very hot path |
| 75 | +// during AST visitation, and holey arrays are much slower. |
| 76 | +// https://v8.dev/blog/elements-kinds |
| 77 | +export const compiledVisitor = []; |
| 78 | + |
| 79 | +for (let i = NODE_TYPES_COUNT; i !== 0; i--) { |
| 80 | + compiledVisitor.push(null); |
| 81 | +} |
| 82 | + |
| 83 | +// Arrays containing type IDs of types which have multiple visit functions defined for them. |
| 84 | +// |
| 85 | +// Filled with `0` initially up to maximum size they could ever need to be so: |
| 86 | +// 1. These arrays never need to grow. |
| 87 | +// 2. V8 treats these arrays as "PACKED_SMI_ELEMENTS". |
| 88 | +const mergedLeafVisitorTypeIds = [], |
| 89 | + mergedEnterVisitorTypeIds = [], |
| 90 | + mergedExitVisitorTypeIds = []; |
| 91 | + |
| 92 | +for (let i = LEAF_NODE_TYPES_COUNT; i !== 0; i--) { |
| 93 | + mergedLeafVisitorTypeIds.push(0); |
| 94 | +} |
| 95 | + |
| 96 | +for (let i = NODE_TYPES_COUNT - LEAF_NODE_TYPES_COUNT; i !== 0; i--) { |
| 97 | + mergedEnterVisitorTypeIds.push(0); |
| 98 | + mergedExitVisitorTypeIds.push(0); |
| 99 | +} |
| 100 | + |
| 101 | +mergedLeafVisitorTypeIds.length = 0; |
| 102 | +mergedEnterVisitorTypeIds.length = 0; |
| 103 | +mergedExitVisitorTypeIds.length = 0; |
| 104 | + |
| 105 | +// `true` if `addVisitor` has been called with a visitor which visits at least one AST type |
| 106 | +let hasActiveVisitors = false; |
| 107 | + |
| 108 | +/** |
| 109 | + * Initialize compiled visitor, ready for calls to `addVisitor`. |
| 110 | + * @returns {undefined} |
| 111 | + */ |
| 112 | +export function initCompiledVisitor() { |
| 113 | + // Reset `compiledVisitor` array after previous compilation |
| 114 | + for (let i = 0; i < NODE_TYPES_COUNT; i++) { |
| 115 | + compiledVisitor[i] = null; |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +/** |
| 120 | + * Add a visitor to compiled visitor. |
| 121 | + * |
| 122 | + * @param {Object} visitor - Visitor object |
| 123 | + * @returns {undefined} |
| 124 | + */ |
| 125 | +export function addVisitorToCompiled(visitor) { |
| 126 | + if (visitor === null || typeof visitor !== 'object') { |
| 127 | + throw new TypeError('Visitor returned from `create` method must be an object'); |
| 128 | + } |
| 129 | + |
| 130 | + // Exit if is empty visitor |
| 131 | + const keys = Object.keys(visitor); |
| 132 | + if (keys.length === 0) return; |
| 133 | + |
| 134 | + hasActiveVisitors = true; |
| 135 | + |
| 136 | + // Populate visitors array from provided object |
| 137 | + for (let name of keys) { |
| 138 | + const visitFn = visitor[name]; |
| 139 | + if (typeof visitFn !== 'function') { |
| 140 | + throw new TypeError(`'${name}' property of visitor object is not a function`); |
| 141 | + } |
| 142 | + |
| 143 | + const isExit = name.endsWith(':exit'); |
| 144 | + if (isExit) name = name.slice(0, -5); |
| 145 | + |
| 146 | + const typeId = NODE_TYPE_IDS_MAP.get(name); |
| 147 | + if (typeId === void 0) throw new Error(`Unknown node type '${name}' in visitor object`); |
| 148 | + |
| 149 | + const existing = compiledVisitor[typeId]; |
| 150 | + if (typeId < LEAF_NODE_TYPES_COUNT) { |
| 151 | + // Leaf node - store just 1 function, not enter+exit pair |
| 152 | + if (existing === null) { |
| 153 | + compiledVisitor[typeId] = visitFn; |
| 154 | + } else if (isArray(existing)) { |
| 155 | + if (isExit) { |
| 156 | + existing.push(visitFn); |
| 157 | + } else { |
| 158 | + // Insert before last in array in case last was enter visit function from the current rule, |
| 159 | + // to ensure enter is called before exit. |
| 160 | + // It could also be either an enter or exit visitor function for another rule, but the order |
| 161 | + // rules are called in doesn't matter. We only need to make sure that a rule's exit visitor |
| 162 | + // isn't called before enter visitor *for that same rule*. |
| 163 | + existing.splice(existing.length - 1, 0, visitFn); |
| 164 | + } |
| 165 | + } else { |
| 166 | + // Same as above, enter visitor is put to front of list to make sure enter is called before exit |
| 167 | + compiledVisitor[typeId] = isExit ? [existing, visitFn] : [visitFn, existing]; |
| 168 | + mergedLeafVisitorTypeIds.push(typeId); |
| 169 | + } |
| 170 | + } else { |
| 171 | + // Not leaf node - store enter+exit pair |
| 172 | + if (existing === null) { |
| 173 | + compiledVisitor[typeId] = isExit |
| 174 | + ? { enter: null, exit: visitFn } |
| 175 | + : { enter: visitFn, exit: null }; |
| 176 | + } else if (isExit) { |
| 177 | + let { exit } = existing; |
| 178 | + if (exit === null) { |
| 179 | + existing.exit = visitFn; |
| 180 | + } else if (isArray(exit)) { |
| 181 | + exit.push(visitFn); |
| 182 | + } else { |
| 183 | + existing.exit = [exit, visitFn]; |
| 184 | + mergedExitVisitorTypeIds.push(typeId); |
| 185 | + } |
| 186 | + } else { |
| 187 | + let { enter } = existing; |
| 188 | + if (enter === null) { |
| 189 | + existing.enter = visitFn; |
| 190 | + } else if (isArray(enter)) { |
| 191 | + enter.push(visitFn); |
| 192 | + } else { |
| 193 | + existing.enter = [enter, visitFn]; |
| 194 | + mergedEnterVisitorTypeIds.push(typeId); |
| 195 | + } |
| 196 | + } |
| 197 | + } |
| 198 | + } |
| 199 | +} |
| 200 | + |
| 201 | +/** |
| 202 | + * Finalize compiled visitor. |
| 203 | + * |
| 204 | + * After calling this function, `compiledVisitor` is ready to be used to walk the AST. |
| 205 | + * |
| 206 | + * @returns {boolean} - `true` if compiled visitor visits at least 1 AST type |
| 207 | + */ |
| 208 | +export function finalizeCompiledVisitor() { |
| 209 | + if (hasActiveVisitors === false) return false; |
| 210 | + |
| 211 | + // Merge visit functions for node types which have multiple visitors from different rules, |
| 212 | + // or enter+exit functions for leaf nodes |
| 213 | + for (const typeId of mergedLeafVisitorTypeIds) { |
| 214 | + compiledVisitor[typeId] = mergeVisitFns(compiledVisitor[typeId]); |
| 215 | + } |
| 216 | + for (const typeId of mergedEnterVisitorTypeIds) { |
| 217 | + const enterExit = compiledVisitor[typeId]; |
| 218 | + enterExit.enter = mergeVisitFns(enterExit.enter); |
| 219 | + } |
| 220 | + for (const typeId of mergedExitVisitorTypeIds) { |
| 221 | + const enterExit = compiledVisitor[typeId]; |
| 222 | + enterExit.exit = mergeVisitFns(enterExit.exit); |
| 223 | + } |
| 224 | + |
| 225 | + // Reset state, ready for next time |
| 226 | + mergedLeafVisitorTypeIds.length = 0; |
| 227 | + mergedEnterVisitorTypeIds.length = 0; |
| 228 | + mergedExitVisitorTypeIds.length = 0; |
| 229 | + |
| 230 | + hasActiveVisitors = false; |
| 231 | + |
| 232 | + return true; |
| 233 | +} |
| 234 | + |
| 235 | +/** |
| 236 | + * Merge array of visit functions into a single function, which calls each of input functions in turn. |
| 237 | + * |
| 238 | + * The merged function is statically defined and does not contain a loop, to hopefully allow |
| 239 | + * JS engine to heavily optimize it. |
| 240 | + * |
| 241 | + * `mergers` contains pre-defined functions to merge up to 5 visit functions. |
| 242 | + * Merger functions for merging more than 5 visit functions are created dynamically on demand. |
| 243 | + * |
| 244 | + * @param {Array<function>} visitFns - Array of visit functions |
| 245 | + * @returns {function} - Function which calls all of `visitFns` in turn. |
| 246 | + */ |
| 247 | +function mergeVisitFns(visitFns) { |
| 248 | + const numVisitFns = visitFns.length; |
| 249 | + |
| 250 | + // Get or create merger for merging `numVisitFns` functions |
| 251 | + let merger; |
| 252 | + if (mergers.length <= numVisitFns) { |
| 253 | + while (mergers.length < numVisitFns) { |
| 254 | + mergers.push(null); |
| 255 | + } |
| 256 | + merger = createMerger(numVisitFns); |
| 257 | + mergers.push(merger); |
| 258 | + } else { |
| 259 | + merger = mergers[numVisitFns]; |
| 260 | + if (merger === null) merger = mergers[numVisitFns] = createMerger(numVisitFns); |
| 261 | + } |
| 262 | + |
| 263 | + // Merge functions |
| 264 | + return merger(...visitFns); |
| 265 | +} |
| 266 | + |
| 267 | +/** |
| 268 | + * Create a merger function that merges `fnCount` functions. |
| 269 | + * |
| 270 | + * @param {number} fnCount - Number of functions to be merged |
| 271 | + * @returns {function} - Function to merge `fnCount` functions |
| 272 | + */ |
| 273 | +function createMerger(fnCount) { |
| 274 | + const args = []; |
| 275 | + let body = 'return node=>{'; |
| 276 | + for (let i = 1; i <= fnCount; i++) { |
| 277 | + args.push(`visit${i}`); |
| 278 | + body += `visit${i}(node);`; |
| 279 | + } |
| 280 | + body += '}'; |
| 281 | + args.push(body); |
| 282 | + return new Function(...args); |
| 283 | +} |
| 284 | + |
| 285 | +// Pre-defined mergers for merging up to 5 functions |
| 286 | +const mergers = [ |
| 287 | + null, // No merger for 0 functions |
| 288 | + null, // No merger for 1 function |
| 289 | + (visit1, visit2) => node => { |
| 290 | + visit1(node); |
| 291 | + visit2(node); |
| 292 | + }, |
| 293 | + (visit1, visit2, visit3) => node => { |
| 294 | + visit1(node); |
| 295 | + visit2(node); |
| 296 | + visit3(node); |
| 297 | + }, |
| 298 | + (visit1, visit2, visit3, visit4) => node => { |
| 299 | + visit1(node); |
| 300 | + visit2(node); |
| 301 | + visit3(node); |
| 302 | + visit4(node); |
| 303 | + }, |
| 304 | + (visit1, visit2, visit3, visit4, visit5) => node => { |
| 305 | + visit1(node); |
| 306 | + visit2(node); |
| 307 | + visit3(node); |
| 308 | + visit4(node); |
| 309 | + visit5(node); |
| 310 | + }, |
| 311 | +]; |
0 commit comments