From 2f7295f7474c4aad9be714589e4b2deb4ad7802a Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 16 Feb 2026 19:40:40 -0800 Subject: [PATCH 1/5] Replace getter property with string reference. --- src/flast.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/flast.js b/src/flast.js index 2cf91a0..d85f962 100644 --- a/src/flast.js +++ b/src/flast.js @@ -38,15 +38,6 @@ const generateFlatASTDefaultOptions = { }, }; -/** - * Return a function which retrieves a node's source on demand - * @param {string} src - * @returns {function(number, number): string} - */ -function createSrcClosure(src) { - return function(start, end) {return src.slice(start, end);}; -} - /** * @param {string} inputCode * @param {object} opts Optional changes to behavior. See generateFlatASTDefaultOptions for available options. @@ -96,13 +87,13 @@ function generateRootNode(inputCode, opts = {}) { let rootNode; try { rootNode = parseCode(inputCode, parseOpts); - if (opts.includeSrc) rootNode.srcClosure = createSrcClosure(inputCode); + if (opts.includeSrc) rootNode.src = inputCode; } catch (e) { // If any parse error occurs and alternateSourceTypeOnFailure is set, try 'script' mode if (opts.alternateSourceTypeOnFailure) { try { rootNode = parseCode(inputCode, {...parseOpts, sourceType: 'script'}); - if (opts.includeSrc) rootNode.srcClosure = createSrcClosure(inputCode); + if (opts.includeSrc) rootNode.src = inputCode; } catch (e2) { logger.debug('Failed to parse as module and script:', e, e2); } @@ -172,9 +163,7 @@ function extractNodesFromRoot(rootNode, opts) { } } // Add a getter for the node's source code - if (opts.includeSrc && !node.src) Object.defineProperty(node, 'src', { - get() {return rootNode.srcClosure(node.start, node.end);}, - }); + if (opts.includeSrc && !node.src) node.src = rootNode.src.substring(node.start,node.end); } if (opts.detailed) { const identifiers = typeMap.Identifier || []; From fb3afb8db78abe9365a02f034761497106c59685 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 16 Feb 2026 20:50:32 -0800 Subject: [PATCH 2/5] Update comment. --- src/flast.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/flast.js b/src/flast.js index d85f962..6cfb975 100644 --- a/src/flast.js +++ b/src/flast.js @@ -162,8 +162,12 @@ function extractNodesFromRoot(rootNode, opts) { node.lineage.push(node.scope.scopeId); } } - // Add a getter for the node's source code - if (opts.includeSrc && !node.src) node.src = rootNode.src.substring(node.start,node.end); + // Avoid using a getter with a closure around source here, as the + // memory requirement for a function per node is far greater than using + // a string reference for sufficiently large AST Trees + // (~2.4 nodes for 3 Gib). + if (opts.includeSrc && !node.src) + node.src = rootNode.src.substring(node.start,node.end); } if (opts.detailed) { const identifiers = typeMap.Identifier || []; From a2e3b707d88007bd3afa58b87c76c0dcdd4b6604 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 16 Feb 2026 12:18:53 -0800 Subject: [PATCH 3/5] Seperate node parsing from traversal. --- src/flast.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/flast.js b/src/flast.js index 6cfb975..a5b5c3b 100644 --- a/src/flast.js +++ b/src/flast.js @@ -116,10 +116,8 @@ function extractNodesFromRoot(rootNode, opts) { const allNodes = []; const scopes = opts.detailed ? getAllScopes(rootNode) : {}; - const stack = [rootNode]; - while (stack.length) { - const node = stack.shift(); - if (node.nodeId) continue; + function parseNode (node) { + if (node.nodeId) return; node.childNodes = node.childNodes || []; const childrenLoc = {}; // Store the location of child nodes to sort them by order node.parentKey = node.parentKey || ''; // Make sure parentKey exists @@ -148,7 +146,6 @@ function extractNodesFromRoot(rootNode, opts) { } } // Add the child nodes to top of the stack and populate the node's childNodes array - stack.unshift(...Object.values(childrenLoc)); node.childNodes.push(...Object.values(childrenLoc)); allNodes.push(node); @@ -168,7 +165,14 @@ function extractNodesFromRoot(rootNode, opts) { // (~2.4 nodes for 3 Gib). if (opts.includeSrc && !node.src) node.src = rootNode.src.substring(node.start,node.end); + return childrenLoc; } + + const stack = [rootNode]; + while (stack.length) { + stack.unshift(...Object.values(parseNode(stack.shift()))); + } + if (opts.detailed) { const identifiers = typeMap.Identifier || []; const scopeVarMaps = buildScopeVarMaps(scopes); From 10f972f9c727543a779fd758f037d1bc59eea9da Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 16 Feb 2026 15:36:36 -0800 Subject: [PATCH 4/5] Convert extractNodesFromRoot traversal to be stack-less. --- src/flast.js | 127 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/src/flast.js b/src/flast.js index a5b5c3b..5a3c437 100644 --- a/src/flast.js +++ b/src/flast.js @@ -104,6 +104,63 @@ function generateRootNode(inputCode, opts = {}) { return rootNode; } +/** + * @param {object} opts + * @param {ASTNode} rootNode + * @param {{number: ASTScope}} scopes + * @param {number} nodeId + * @param {ASTNode} node + * @return {ASTNode} + */ +function parseNode (opts, rootNode, scopes, nodeId, node) { + if (node.nodeId) return; + node.childNodes = node.childNodes || []; + const childrenLoc = {}; // Store the location of child nodes to sort them by order + node.parentKey = node.parentKey || ''; // Make sure parentKey exists + // Iterate over all keys of the node to find child nodes + const keys = Object.keys(node); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (excludedParentKeys.includes(key)) continue; + const content = node[key]; + if (content && typeof content === 'object') { + // Sort each child node by its start position + // and set the parentNode and parentKey attributes + if (Array.isArray(content)) { + for (let j = 0; j < content.length; j++) { + const childNode = content[j]; + if (!childNode) continue; + childNode.parentNode = node; + childNode.parentKey = key; + childrenLoc[childNode.start] = childNode; + } + } else { + content.parentNode = node; + content.parentKey = key; + childrenLoc[content.start] = content; + } + } + } + // Add the child nodes to top of the stack and populate the node's childNodes array + node.childNodes.push(...Object.values(childrenLoc)); + + node.nodeId = nodeId; + if (opts.detailed) { + node.scope = scopes[node.scopeId] || node.parentNode?.scope; + node.lineage = [...node.parentNode?.lineage || []]; + if (!node.lineage.includes(node.scope.scopeId)) { + node.lineage.push(node.scope.scopeId); + } + } + // Avoid using a getter with a closure around source here, as the + // memory requirement for a function per node is far greater than using + // a string reference for sufficiently large AST Trees + // (~2.4 nodes for 3 Gib). + if (opts.includeSrc && !node.src) + node.src = rootNode.src.substring(node.start,node.end); + return node; +} + /** * @param rootNode * @param opts @@ -111,66 +168,30 @@ function generateRootNode(inputCode, opts = {}) { */ function extractNodesFromRoot(rootNode, opts) { opts = {...generateFlatASTDefaultOptions, ...opts}; - let nodeId = 0; const typeMap = {}; const allNodes = []; const scopes = opts.detailed ? getAllScopes(rootNode) : {}; - function parseNode (node) { - if (node.nodeId) return; - node.childNodes = node.childNodes || []; - const childrenLoc = {}; // Store the location of child nodes to sort them by order - node.parentKey = node.parentKey || ''; // Make sure parentKey exists - // Iterate over all keys of the node to find child nodes - const keys = Object.keys(node); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (excludedParentKeys.includes(key)) continue; - const content = node[key]; - if (content && typeof content === 'object') { - // Sort each child node by its start position - // and set the parentNode and parentKey attributes - if (Array.isArray(content)) { - for (let j = 0; j < content.length; j++) { - const childNode = content[j]; - if (!childNode) continue; - childNode.parentNode = node; - childNode.parentKey = key; - childrenLoc[childNode.start] = childNode; - } - } else { - content.parentNode = node; - content.parentKey = key; - childrenLoc[content.start] = content; - } - } + let nodeId = 0; + let visitor = rootNode; + while (visitor) { + if(!visitor.childNodes){ + allNodes.push(parseNode(opts, rootNode, scopes, nodeId, visitor)); + nodeId++; + typeMap[visitor.type] = typeMap[visitor.type] || []; + typeMap[visitor.type].push(visitor); } - // Add the child nodes to top of the stack and populate the node's childNodes array - node.childNodes.push(...Object.values(childrenLoc)); - - allNodes.push(node); - node.nodeId = nodeId++; - typeMap[node.type] = typeMap[node.type] || []; - typeMap[node.type].push(node); - if (opts.detailed) { - node.scope = scopes[node.scopeId] || node.parentNode?.scope; - node.lineage = [...node.parentNode?.lineage || []]; - if (!node.lineage.includes(node.scope.scopeId)) { - node.lineage.push(node.scope.scopeId); + let parsedAllChildNodes = true; + for(let i = 0; i < visitor.childNodes.length; ++i){ + if(!visitor.childNodes[i].nodeId){ + parsedAllChildNodes = false; + visitor = visitor.childNodes[i]; + break; } } - // Avoid using a getter with a closure around source here, as the - // memory requirement for a function per node is far greater than using - // a string reference for sufficiently large AST Trees - // (~2.4 nodes for 3 Gib). - if (opts.includeSrc && !node.src) - node.src = rootNode.src.substring(node.start,node.end); - return childrenLoc; - } - - const stack = [rootNode]; - while (stack.length) { - stack.unshift(...Object.values(parseNode(stack.shift()))); + if(parsedAllChildNodes){ + visitor = visitor.parentNode; + } } if (opts.detailed) { From ba2d6a19a341ae3fad4018d9b8ef3f2cc20e3aa5 Mon Sep 17 00:00:00 2001 From: alice Date: Mon, 16 Feb 2026 16:25:52 -0800 Subject: [PATCH 5/5] Replace for loop with map. --- src/flast.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/flast.js b/src/flast.js index 5a3c437..2186d5f 100644 --- a/src/flast.js +++ b/src/flast.js @@ -174,23 +174,20 @@ function extractNodesFromRoot(rootNode, opts) { let nodeId = 0; let visitor = rootNode; + const lastParsed = {}; while (visitor) { if(!visitor.childNodes){ - allNodes.push(parseNode(opts, rootNode, scopes, nodeId, visitor)); - nodeId++; + allNodes.push(parseNode(opts, rootNode, scopes, nodeId++, visitor)); typeMap[visitor.type] = typeMap[visitor.type] || []; typeMap[visitor.type].push(visitor); + lastParsed[visitor.nodeId] = 0; } - let parsedAllChildNodes = true; - for(let i = 0; i < visitor.childNodes.length; ++i){ - if(!visitor.childNodes[i].nodeId){ - parsedAllChildNodes = false; - visitor = visitor.childNodes[i]; - break; - } - } - if(parsedAllChildNodes){ + let visitorId = visitor.nodeId; + if(lastParsed[visitorId] < visitor.childNodes.length){ + visitor = visitor.childNodes[lastParsed[visitorId]++]; + }else{ visitor = visitor.parentNode; + delete lastParsed[visitorId]; } }