171 lines
5.3 KiB
JavaScript
171 lines
5.3 KiB
JavaScript
"use strict";
|
|
|
|
const addImportedRules = require("./passes/add-imported-rules");
|
|
const fixLibraryNumbers = require("./passes/fix-library-numbers");
|
|
const generateBytecode = require("./passes/generate-bytecode");
|
|
const generateJS = require("./passes/generate-js");
|
|
const inferenceMatchResult = require("./passes/inference-match-result");
|
|
const removeProxyRules = require("./passes/remove-proxy-rules");
|
|
const mergeCharacterClasses = require("./passes/merge-character-classes");
|
|
const reportDuplicateImports = require("./passes/report-duplicate-imports");
|
|
const reportDuplicateLabels = require("./passes/report-duplicate-labels");
|
|
const reportDuplicateRules = require("./passes/report-duplicate-rules");
|
|
const reportInfiniteRecursion = require("./passes/report-infinite-recursion");
|
|
const reportInfiniteRepetition = require("./passes/report-infinite-repetition");
|
|
const reportUndefinedRules = require("./passes/report-undefined-rules");
|
|
const reportIncorrectPlucking = require("./passes/report-incorrect-plucking");
|
|
const Session = require("./session");
|
|
const visitor = require("./visitor");
|
|
const { base64 } = require("./utils");
|
|
|
|
function processOptions(options, defaults) {
|
|
const processedOptions = {};
|
|
|
|
Object.keys(options).forEach(name => {
|
|
processedOptions[name] = options[name];
|
|
});
|
|
|
|
Object.keys(defaults).forEach(name => {
|
|
if (!Object.prototype.hasOwnProperty.call(processedOptions, name)) {
|
|
processedOptions[name] = defaults[name];
|
|
}
|
|
});
|
|
|
|
return processedOptions;
|
|
}
|
|
|
|
function isSourceMapCapable(target) {
|
|
if (typeof target === "string") {
|
|
return target.length > 0;
|
|
}
|
|
return target && (typeof target.offset === "function");
|
|
}
|
|
|
|
const compiler = {
|
|
// AST node visitor builder. Useful mainly for plugins which manipulate the
|
|
// AST.
|
|
visitor,
|
|
|
|
// Compiler passes.
|
|
//
|
|
// Each pass is a function that is passed the AST. It can perform checks on it
|
|
// or modify it as needed. If the pass encounters a semantic error, it throws
|
|
// |peg.GrammarError|.
|
|
passes: {
|
|
prepare: [
|
|
addImportedRules,
|
|
reportInfiniteRecursion,
|
|
],
|
|
check: [
|
|
reportUndefinedRules,
|
|
reportDuplicateRules,
|
|
reportDuplicateLabels,
|
|
reportInfiniteRepetition,
|
|
reportIncorrectPlucking,
|
|
reportDuplicateImports,
|
|
],
|
|
transform: [
|
|
fixLibraryNumbers,
|
|
removeProxyRules,
|
|
mergeCharacterClasses,
|
|
inferenceMatchResult,
|
|
],
|
|
generate: [
|
|
generateBytecode,
|
|
generateJS,
|
|
],
|
|
},
|
|
|
|
// Generates a parser from a specified grammar AST. Throws |peg.GrammarError|
|
|
// if the AST contains a semantic error. Note that not all errors are detected
|
|
// during the generation and some may protrude to the generated parser and
|
|
// cause its malfunction.
|
|
compile(ast, passes, options) {
|
|
options = options !== undefined ? options : {};
|
|
|
|
options = processOptions(options, {
|
|
allowedStartRules: [ast.rules[0].name],
|
|
cache: false,
|
|
dependencies: {},
|
|
exportVar: null,
|
|
format: "bare",
|
|
output: "parser",
|
|
trace: false,
|
|
});
|
|
|
|
if (!Array.isArray(options.allowedStartRules)) {
|
|
throw new Error("allowedStartRules must be an array");
|
|
}
|
|
if (options.allowedStartRules.length === 0) {
|
|
throw new Error("Must have at least one start rule");
|
|
}
|
|
const allRules = ast.rules.map(r => r.name);
|
|
// "*" means all rules are start rules. "*" is not a valid rule name.
|
|
if (options.allowedStartRules.some(r => r === "*")) {
|
|
options.allowedStartRules = allRules;
|
|
} else {
|
|
for (const rule of options.allowedStartRules) {
|
|
if (allRules.indexOf(rule) === -1) {
|
|
throw new Error(`Unknown start rule "${rule}"`);
|
|
}
|
|
}
|
|
}
|
|
// Due to https://github.com/mozilla/source-map/issues/444
|
|
// grammarSource is required
|
|
if (((options.output === "source-and-map")
|
|
|| (options.output === "source-with-inline-map"))
|
|
&& !isSourceMapCapable(options.grammarSource)) {
|
|
throw new Error("Must provide grammarSource (as a string or GrammarLocation) in order to generate source maps");
|
|
}
|
|
|
|
const session = new Session(options);
|
|
Object.keys(passes).forEach(stage => {
|
|
session.stage = stage;
|
|
session.info(`Process stage ${stage}`);
|
|
|
|
passes[stage].forEach(pass => {
|
|
session.info(`Process pass ${stage}.${pass.name}`);
|
|
|
|
pass(ast, options, session);
|
|
});
|
|
|
|
// Collect all errors by stage
|
|
session.checkErrors();
|
|
});
|
|
|
|
switch (options.output) {
|
|
case "parser":
|
|
// eslint-disable-next-line no-eval -- Required
|
|
return eval(ast.code.toString());
|
|
|
|
case "source":
|
|
return ast.code.toString();
|
|
|
|
case "source-and-map":
|
|
return ast.code;
|
|
|
|
case "source-with-inline-map": {
|
|
if (typeof TextEncoder === "undefined") {
|
|
throw new Error("TextEncoder is not supported by this platform");
|
|
}
|
|
const sourceMap = ast.code.toStringWithSourceMap();
|
|
const encoder = new TextEncoder();
|
|
const b64 = base64(
|
|
encoder.encode(JSON.stringify(sourceMap.map.toJSON()))
|
|
);
|
|
return sourceMap.code + `\
|
|
//\x23 sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}
|
|
`;
|
|
}
|
|
|
|
case "ast":
|
|
return ast;
|
|
|
|
default:
|
|
throw new Error("Invalid output format: " + options.output + ".");
|
|
}
|
|
},
|
|
};
|
|
|
|
module.exports = compiler;
|