Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
bin/* text eol=lf
*.js text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.ts text eol=lf
*.cts text eol=lf
*.mts text eol=lf
*.json text eol=lf
*.html text eol=lf
*.css text eol=lf
Expand Down
315 changes: 230 additions & 85 deletions lib/compiler/visitor.js
Original file line number Diff line number Diff line change
@@ -1,100 +1,245 @@
"use strict";

// Simple AST node visitor builder.
const visitor = {
build(functions) {
function visit(node, ...args) {
return functions[node.type](node, ...args);
// {
// /* eslint-disable @stylistic/max-len */
// /* eslint-disable no-unused-vars */
// /* eslint-disable no-warning-comments */
// /* eslint-disable @stylistic/quotes */
// /* eslint-disable class-methods-use-this */
// /* eslint-disable no-useless-constructor */
// /* eslint-disable no-empty-function */
// /* eslint-disable @stylistic/no-trailing-spaces */
// }

/**
* Each one of these evaluates to {@see ast.AllNodes}, basically.
* But perhaps later this can be more specific.
*
* Later iteration (thru {@link VisitorUtils}) and {@link Object.entries} generally follows the order below.
*/
const NODE_PROPS_THAT_CAN_BE_CHECKED = {
imports: true,
topLevelInitializer: true,
initializer: true,
rules: true,

delimiter: true,
expression: true,
alternatives: true,
elements: true,
};

class VisitorUtils {
static isNode(obj) {
if (obj === null || obj === undefined) {
return false;
}

if (typeof obj !== "object") {
return false;
}

const typeIsExpected = typeof obj.type === "string";
const locationIsExpected = typeof obj.location === "object";

// This occurs in plugin-api.spec.js -- but seems to be counter to spec?
const locationIsMissing = obj.location === undefined;

// This check isn't great, but it's a good hook for typescript `obj is ast.Node<T>` assertions later.
return typeIsExpected && (locationIsExpected || locationIsMissing);
}

static singularizeResultOrThrow(maybeResultArray) {
if (!Array.isArray(maybeResultArray)) {
return maybeResultArray;
}

if (maybeResultArray.length === 1) {
return maybeResultArray[0];
}

throw new Error("Result of expression could not be singularized.");
}

static asSingleNodeOrThrow(maybeNode) {
if (maybeNode === null || maybeNode === undefined) {
return maybeNode;
}

function visitNop() {
// Do nothing.
if (Array.isArray(maybeNode)) {
throw new Error(`Prop was array ${JSON.stringify(maybeNode, null, " ")}`);
}

function visitExpression(node, ...args) {
return visit(node.expression, ...args);
if (VisitorUtils.isNode(maybeNode)) {
return maybeNode;
}

function visitChildren(property) {
return function(node, ...args) {
// We do not use .map() here, because if you need the result
// of applying visitor to children you probable also need to
// process it in some way, therefore you anyway have to override
// this method. If you do not needed that, we do not waste time
// and memory for creating the output array
node[property].forEach(child => visit(child, ...args));
};
throw new Error(`Prop was not undefined, null, or node -- ${JSON.stringify(maybeNode, null, " ")}`);
}

static asNodeArrayOrThrow(maybeNodes) {
if (Array.isArray(maybeNodes)) {
if (maybeNodes.every(maybeNode => VisitorUtils.isNode(maybeNode))) {
return [...maybeNodes];
}
}

if (VisitorUtils.isNode(maybeNodes)) {
return [maybeNodes];
}

if (maybeNodes === null || maybeNodes === undefined) {
return [];
}

throw new Error(`Prop was not node array ${JSON.stringify(maybeNodes, null, " ")}`);
}

static propToNodeArrayOrThrow(node, maybeProp) {
try {
return VisitorUtils.asNodeArrayOrThrow(node[maybeProp]);
} catch (ex) {
throw new Error(`[${node.type}.${maybeProp}] ${ex.message}`);
}
}

const DEFAULT_FUNCTIONS = {
grammar(node, ...args) {
for (const imp of node.imports) {
visit(imp, ...args);
}

if (node.topLevelInitializer) {
if (Array.isArray(node.topLevelInitializer)) {
for (const tli of node.topLevelInitializer) {
visit(tli, ...args);
}
} else {
visit(node.topLevelInitializer, ...args);
}
}

if (node.initializer) {
if (Array.isArray(node.initializer)) {
for (const init of node.initializer) {
visit(init, ...args);
}
} else {
visit(node.initializer, ...args);
}
}

node.rules.forEach(rule => visit(rule, ...args));
},

grammar_import: visitNop,
top_level_initializer: visitNop,
initializer: visitNop,
rule: visitExpression,
named: visitExpression,
choice: visitChildren("alternatives"),
action: visitExpression,
sequence: visitChildren("elements"),
labeled: visitExpression,
text: visitExpression,
simple_and: visitExpression,
simple_not: visitExpression,
optional: visitExpression,
zero_or_more: visitExpression,
one_or_more: visitExpression,
repeated(node, ...args) {
if (node.delimiter) {
visit(node.delimiter, ...args);
}

return visit(node.expression, ...args);
},
group: visitExpression,
semantic_and: visitNop,
semantic_not: visitNop,
rule_ref: visitNop,
library_ref: visitNop,
literal: visitNop,
class: visitNop,
any: visitNop,
};

Object.keys(DEFAULT_FUNCTIONS).forEach(type => {
if (!Object.prototype.hasOwnProperty.call(functions, type)) {
functions[type] = DEFAULT_FUNCTIONS[type];
static propsToChildNodes(
node,
propsToCheck = NODE_PROPS_THAT_CAN_BE_CHECKED
) {
const outcome = {};

for (const [prop, wantToCheck] of Object.entries(propsToCheck)) {
const canCheck = NODE_PROPS_THAT_CAN_BE_CHECKED[prop];
if (canCheck && wantToCheck) {
outcome[prop] = VisitorUtils.propToNodeArrayOrThrow(node, prop);
}
});
}

return outcome;
}

/** Produces a single nodeChild or */
static nodeChild(_node) {
return undefined;
}
}

return visit;
const TYPE_TO_PROPS = {
grammar: {
imports: true,
topLevelInitializer: true,
initializer: true,
rules: true,
},
};

module.exports = visitor;
class VisitorBase {
visit(node, ...args) {
return this.visitAllChildNodes(node, ...args);
}

visitAllChildNodes(node, ...args) {
const childNodesObj = VisitorUtils.propsToChildNodes(node);
return this.visitChildrenByNodeObj(childNodesObj, ...args);
}

visitChildByProp(prop, node, ...args) {
const childNode = VisitorUtils.asSingleNodeOrThrow(node[prop]);
return this.visit(childNode, ...args);
}

visitChildrenByProp(prop, node, ...args) {
const childNodes = VisitorUtils.asNodeArrayOrThrow(node[prop]);
return childNodes.map(childNode => this.visit(childNode, ...args));
}

visitChildrenByProps(whichPropsObj, node, ...args) {
const childNodesObj = VisitorUtils.propsToChildNodes(node, whichPropsObj);
return this.visitChildrenByNodeObj(childNodesObj, ...args);
}

visitChildrenByNodeObj(childNodesObj, ...args) {
const outputs = {};

for (const [key, childNode] of Object.entries(childNodesObj)) {
outputs[key] = childNode.map(child => this.visit(child, ...args));
}

return outputs;
}
}

// Simple AST node visitor builder.
class VisitorAdapter extends VisitorBase {
constructor(functions) {
super();
this.functions = functions;
}

visit(node, ...args) {
const visitorOverride = this.functions[node.type];
if (visitorOverride) { return visitorOverride(node, ...args); }

return this.visitAdaptations(node, ...args);
}

visitAdaptations(node, ...args) {
switch (node.type) {
case "grammar":
return super.visitChildrenByProps(
TYPE_TO_PROPS.grammar, node, ...args
);

case "rule":
case "named":
case "action":
case "labeled":
case "text":
case "simple_and":
case "simple_not":
case "optional":
case "zero_or_more":
case "one_or_more":
case "group":
// Expressions are a special case for backwards compat -- some callers expect it to return a singular value
return super.visitChildByProp("expression", node, ...args);

case "choice": // Originally: visitChildren("alternatives"),
return super.visitChildrenByProp("alternatives", node, ...args);

case "sequence": // Originally: visitChildren("elements"),
return super.visitChildrenByProp("elements", node, ...args);

// Leaf Nodes. Originally called to `visitNop` which does nothing.
case "grammar_import":
case "top_level_initializer":
case "initializer":
case "semantic_and":
case "semantic_not":
case "rule_ref":
case "library_ref":
case "literal":
case "class":
case "any":
return undefined;

case "repeated": // Originally: visit (node.delimiter)? -> node.expression
return [
...this.visitChildrenByProp("delimiter", node, ...args),
this.visitChildByProp("expression", node, ...args),
];

// By default, attempt to recurse into all known recurse-able sub-properties
default:
throw new Error(`[${node.type}] Node type not part of adapted specification.`);
}
}

static build(functions) {
const adapter = new VisitorAdapter(functions);
return (node, ...args) => adapter.visit(node, ...args);
}
}

module.exports = VisitorAdapter;
Loading