123 lines
3.7 KiB
JavaScript
123 lines
3.7 KiB
JavaScript
/* eslint-env node */
|
|
|
|
'use strict';
|
|
|
|
// --------------------------------------------------------------------
|
|
// Imports
|
|
// --------------------------------------------------------------------
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ohm = require('ohm-js');
|
|
|
|
// --------------------------------------------------------------------
|
|
// Helpers
|
|
// --------------------------------------------------------------------
|
|
|
|
// Take an Array of nodes, and whenever an _iter node is encountered, splice in its
|
|
// recursively-flattened children instead.
|
|
function flattenIterNodes(nodes) {
|
|
const result = [];
|
|
for (let i = 0; i < nodes.length; ++i) {
|
|
if (nodes[i]._node.ctorName === '_iter') {
|
|
result.push(...flattenIterNodes(nodes[i].children));
|
|
} else {
|
|
result.push(nodes[i]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Comparison function for sorting nodes based on their interval's start index.
|
|
function compareByInterval(node, otherNode) {
|
|
return node.source.startIdx - otherNode.source.startIdx;
|
|
}
|
|
|
|
function nodeToES5(node, children) {
|
|
const flatChildren = flattenIterNodes(children).sort(compareByInterval);
|
|
|
|
// Keeps track of where the previous sibling ended, so that we can re-insert discarded
|
|
// whitespace into the final output.
|
|
let prevEndIdx = node.source.startIdx;
|
|
|
|
let code = '';
|
|
for (let i = 0; i < flatChildren.length; ++i) {
|
|
const child = flatChildren[i];
|
|
|
|
// Restore any discarded whitespace between this node and the previous one.
|
|
if (child.source.startIdx > prevEndIdx) {
|
|
code += node.source.sourceString.slice(prevEndIdx, child.source.startIdx);
|
|
}
|
|
code += child.toES5();
|
|
prevEndIdx = child.source.endIdx;
|
|
}
|
|
return code;
|
|
}
|
|
|
|
// Instantiate the ES5 grammar.
|
|
const contents = fs.readFileSync(path.join(__dirname, 'es5.ohm'));
|
|
const g = ohm.grammars(contents).ES5;
|
|
const semantics = g.createSemantics();
|
|
|
|
semantics.addOperation('toES5()', {
|
|
Program(_, sourceElements) {
|
|
// Top-level leading and trailing whitespace is not handled by nodeToES5(), so do it here.
|
|
const {sourceString} = this.source;
|
|
return (
|
|
sourceString.slice(0, this.source.startIdx) +
|
|
nodeToES5(this, [sourceElements]) +
|
|
sourceString.slice(this.source.endIdx)
|
|
);
|
|
},
|
|
_nonterminal(...children) {
|
|
return nodeToES5(this, children);
|
|
},
|
|
_terminal() {
|
|
return this.sourceString;
|
|
},
|
|
});
|
|
|
|
// Implements hoisting of variable and function declarations.
|
|
// See https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
|
|
// Note that on its own, this operation doesn't create nested lexical environments,
|
|
// but it should be possible to use it as a helper for another operation that would.
|
|
semantics.addOperation('hoistDeclarations()', {
|
|
FunctionDeclaration(_, ident, _1, _2, _3, _4, _5, _6) {
|
|
// Don't hoist from the function body, only return this function's identifier.
|
|
return new Map([[ident.sourceString, [ident.source]]]);
|
|
},
|
|
FunctionExpression(_) {
|
|
return new Map();
|
|
},
|
|
VariableDeclaration(ident, _) {
|
|
return new Map([[ident.sourceString, [ident.source]]]);
|
|
},
|
|
_iter: mergeBindings,
|
|
_nonterminal: mergeBindings,
|
|
_terminal() {
|
|
return new Map();
|
|
},
|
|
});
|
|
|
|
// Merge the bindings from the given `nodes` into a single map, where the value
|
|
// is an array of source locations that name is bound.
|
|
function mergeBindings(...nodes) {
|
|
const bindings = new Map();
|
|
for (const child of nodes.filter(c => !c.isLexical())) {
|
|
child.hoistDeclarations().forEach((sources, ident) => {
|
|
if (bindings.has(ident)) {
|
|
bindings.get(ident).push(...sources); // Shadowed binding.
|
|
} else {
|
|
bindings.set(ident, sources); // Not shadowed at this level.
|
|
}
|
|
});
|
|
}
|
|
return bindings;
|
|
}
|
|
|
|
module.exports = {
|
|
grammar: g,
|
|
semantics,
|
|
};
|
|
|