From 5becca88d9edc389ca5979e436c8bf3113c80fc1 Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 25 Oct 2021 23:13:49 +0500 Subject: [PATCH 1/4] Add explanations to bytecode --- test/unit/compiler/passes/generate-bytecode.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/compiler/passes/generate-bytecode.spec.js b/test/unit/compiler/passes/generate-bytecode.spec.js index 10130c64..b2fa51ab 100644 --- a/test/unit/compiler/passes/generate-bytecode.spec.js +++ b/test/unit/compiler/passes/generate-bytecode.spec.js @@ -151,7 +151,7 @@ describe("compiler pass |generateBytecode|", () => { 5, // PUSH_CURR_POS 18, 0, 2, 2, 22, 0, 23, 0, // 15, 6, 0, // IF_NOT_ERROR - 24, 1, // * LOAD_SAVED_POS + 24, 1, // * LOAD_SAVED_POS <1> 26, 0, 1, 0, // CALL <0> 9, // NIP ])); @@ -184,7 +184,7 @@ describe("compiler pass |generateBytecode|", () => { 5, // PUSH_CURR_POS 18, 0, 2, 2, 22, 0, 23, 0, // 15, 7, 0, // IF_NOT_ERROR - 24, 1, // * LOAD_SAVED_POS + 24, 1, // * LOAD_SAVED_POS <1> 26, 0, 1, 1, 0, // CALL <0> 9, // NIP ])); @@ -221,7 +221,7 @@ describe("compiler pass |generateBytecode|", () => { 15, 24, 4, // IF_NOT_ERROR 18, 2, 2, 2, 22, 2, 23, 2, // * 15, 9, 4, // IF_NOT_ERROR - 24, 3, // * LOAD_SAVED_POS + 24, 3, // * LOAD_SAVED_POS <3> 26, 0, 4, 3, 2, 1, 0, // CALL <0> 8, 3, // * POP_N <3> 7, // POP_CURR_POS From d5d8da7bedec0520c69056482988289582be3082 Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 25 Oct 2021 20:15:27 +0500 Subject: [PATCH 2/4] Add bytecode information to error messages to facilitate debugging --- lib/compiler/passes/generate-js.js | 4 +-- lib/compiler/stack.js | 16 ++++++---- test/unit/compiler/stack.spec.js | 48 +++++++++++++++++------------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index b0a09b4b..6ec7dc87 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -258,7 +258,7 @@ function generateJS(ast, options) { function generateRuleFunction(rule) { const parts = []; - const stack = new Stack(rule.name, "s", "var"); + const stack = new Stack(rule.name, "s", "var", rule.bytecode); function compile(bc) { let ip = 0; @@ -528,7 +528,7 @@ function generateJS(ast, options) { // istanbul ignore next Because we never generate invalid bytecode we cannot reach this branch default: - throw new Error("Invalid opcode: " + bc[ip] + "."); + throw new Error("Invalid opcode: " + bc[ip] + ".", { rule: rule.name, bytecode: bc }); } } diff --git a/lib/compiler/stack.js b/lib/compiler/stack.js index d52dd4c2..785606e5 100644 --- a/lib/compiler/stack.js +++ b/lib/compiler/stack.js @@ -8,8 +8,9 @@ class Stack { * @param {string} ruleName The name of rule that will be used in error messages * @param {string} varName The prefix for generated names of variables * @param {string} type The type of the variables. For JavaScript there are `var` or `let` + * @param {number[]} bytecode Bytecode for error messages */ - constructor(ruleName, varName, type) { + constructor(ruleName, varName, type, bytecode) { /** Last used variable in the stack. */ this.sp = -1; /** Maximum stack size. */ @@ -17,6 +18,7 @@ class Stack { this.varName = varName; this.ruleName = ruleName; this.type = type; + this.bytecode = bytecode; } /** @@ -30,7 +32,7 @@ class Stack { name(i) { if (i < 0) { throw new RangeError( - `Rule '${this.ruleName}': The variable stack underflow: attempt to use a variable '${this.varName}' at an index ${i}` + `Rule '${this.ruleName}': The variable stack underflow: attempt to use a variable '${this.varName}' at an index ${i}.\nBytecode: ${this.bytecode}` ); } @@ -90,7 +92,7 @@ class Stack { index(i) { if (i < 0) { throw new RangeError( - `Rule '${this.ruleName}': The variable stack overflow: attempt to get a variable at a negative index ${i}` + `Rule '${this.ruleName}': The variable stack overflow: attempt to get a variable at a negative index ${i}.\nBytecode: ${this.bytecode}` ); } @@ -107,7 +109,7 @@ class Stack { result() { if (this.maxSp < 0) { throw new RangeError( - `Rule '${this.ruleName}': The variable stack is empty, can't get the result'` + `Rule '${this.ruleName}': The variable stack is empty, can't get the result.\nBytecode: ${this.bytecode}` ); } @@ -154,7 +156,8 @@ class Stack { throw new Error( "Rule '" + this.ruleName + "', position " + pos + ": " + "Branches of a condition can't move the stack pointer differently " - + "(before: " + baseSp + ", after then: " + thenSp + ", after else: " + this.sp + ")." + + "(before: " + baseSp + ", after then: " + thenSp + ", after else: " + this.sp + "). " + + "Bytecode: " + this.bytecode ); } } @@ -178,7 +181,8 @@ class Stack { throw new Error( "Rule '" + this.ruleName + "', position " + pos + ": " + "Body of a loop can't move the stack pointer " - + "(before: " + baseSp + ", after: " + this.sp + ")." + + "(before: " + baseSp + ", after: " + this.sp + "). " + + "Bytecode: " + this.bytecode ); } } diff --git a/test/unit/compiler/stack.spec.js b/test/unit/compiler/stack.spec.js index bab49377..05d3aa09 100644 --- a/test/unit/compiler/stack.spec.js +++ b/test/unit/compiler/stack.spec.js @@ -9,20 +9,20 @@ describe("utility class Stack", () => { describe("for an empty stack", () => { let stack; - beforeEach(() => { stack = new Stack("rule", "v", "let"); }); + beforeEach(() => { stack = new Stack("rule", "v", "let", [42]); }); describe("throws an error when attempting", () => { it("`pop`", () => { expect(() => stack.pop()).to.throw( RangeError, - "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -1" + "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -1.\nBytecode: 42" ); }); it("`top`", () => { expect(() => stack.top()).to.throw( RangeError, - "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -1" + "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -1.\nBytecode: 42" ); }); @@ -36,15 +36,15 @@ describe("utility class Stack", () => { it("`index`", () => { expect(() => stack.index(-2)).to.throw( RangeError, - "Rule 'rule': The variable stack overflow: attempt to get a variable at a negative index -2" + "Rule 'rule': The variable stack overflow: attempt to get a variable at a negative index -2.\nBytecode: 42" ); expect(() => stack.index(0)).to.throw( RangeError, - "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -1" + "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -1.\nBytecode: 42" ); expect(() => stack.index(2)).to.throw( RangeError, - "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -3" + "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -3.\nBytecode: 42" ); }); }); @@ -55,18 +55,18 @@ describe("utility class Stack", () => { }); it("throws an error when attempting `pop` more than `push`", () => { - const stack = new Stack("rule", "v", "let"); + const stack = new Stack("rule", "v", "let", [42]); stack.push("1"); expect(() => stack.pop(3)).to.throw( RangeError, - "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -2" + "Rule 'rule': The variable stack underflow: attempt to use a variable 'v' at an index -2.\nBytecode: 42" ); }); it("returns a variable with an index 0 for `result`", () => { - const stack = new Stack("rule", "v", "let"); + const stack = new Stack("rule", "v", "let", []); stack.push("1"); @@ -74,7 +74,7 @@ describe("utility class Stack", () => { }); it("`defines` returns a define expression for all used variables", () => { - const stack = new Stack("rule", "v", "let"); + const stack = new Stack("rule", "v", "let", []); stack.push("1"); stack.push("2"); @@ -88,7 +88,7 @@ describe("utility class Stack", () => { let stack; beforeEach(() => { - stack = new Stack("rule", "v", "let"); + stack = new Stack("rule", "v", "let", [42]); stack.push("1"); }); @@ -152,7 +152,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Branches of a condition can't move the stack pointer differently " - + "(before: 0, after then: 0, after else: -1)." + + "(before: 0, after then: 0, after else: -1). " + + "Bytecode: 42" ); }); it("decreases in `if` and was not moving in `then`", () => { @@ -162,7 +163,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Branches of a condition can't move the stack pointer differently " - + "(before: 0, after then: -1, after else: 0)." + + "(before: 0, after then: -1, after else: 0). " + + "Bytecode: 42" ); }); @@ -173,7 +175,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Branches of a condition can't move the stack pointer differently " - + "(before: 0, after then: 0, after else: 1)." + + "(before: 0, after then: 0, after else: 1). " + + "Bytecode: 42" ); }); it("increases in `if` and was not moving in `then`", () => { @@ -183,7 +186,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Branches of a condition can't move the stack pointer differently " - + "(before: 0, after then: 1, after else: 0)." + + "(before: 0, after then: 1, after else: 0). " + + "Bytecode: 42" ); }); @@ -194,7 +198,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Branches of a condition can't move the stack pointer differently " - + "(before: 0, after then: -1, after else: 1)." + + "(before: 0, after then: -1, after else: 1). " + + "Bytecode: 42" ); }); it("increases in `if` and decreases in `then`", () => { @@ -204,7 +209,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Branches of a condition can't move the stack pointer differently " - + "(before: 0, after then: 1, after else: -1)." + + "(before: 0, after then: 1, after else: -1). " + + "Bytecode: 42" ); }); }); @@ -214,7 +220,7 @@ describe("utility class Stack", () => { let stack; beforeEach(() => { - stack = new Stack("rule", "v", "let"); + stack = new Stack("rule", "v", "let", [42]); stack.push("1"); }); @@ -247,7 +253,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Body of a loop can't move the stack pointer " - + "(before: 0, after: 1)." + + "(before: 0, after: 1). " + + "Bytecode: 42" ); }); @@ -256,7 +263,8 @@ describe("utility class Stack", () => { Error, "Rule 'rule', position 0: " + "Body of a loop can't move the stack pointer " - + "(before: 0, after: -1)." + + "(before: 0, after: -1). " + + "Bytecode: 42" ); }); }); From f346279412647a768114f816f78c53c0f9fbcae8 Mon Sep 17 00:00:00 2001 From: Mingun Date: Tue, 2 Jan 2018 16:38:52 +0500 Subject: [PATCH 3/4] Use chai-like instead of own properties matcher Maybe in the future it will implement proper diff generation... --- package-lock.json | 21 +++++++++++-- package.json | 1 + test/unit/compiler/passes/helpers.js | 46 ++-------------------------- 3 files changed, 23 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3c59716..1c048815 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "peggy", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "peggy", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { "commander": "^9.3.0", @@ -29,6 +29,7 @@ "@typescript-eslint/parser": "^5.27.0", "browser-sync": "^2.27.10", "chai": "^4.3.6", + "chai-like": "^1.1.1", "copyfiles": "^2.4.1", "eslint": "^8.16.0", "express": "4.18.1", @@ -2302,6 +2303,15 @@ "node": ">=4" } }, + "node_modules/chai-like": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chai-like/-/chai-like-1.1.1.tgz", + "integrity": "sha512-VKa9z/SnhXhkT1zIjtPACFWSoWsqVoaz1Vg+ecrKo5DCKVlgL30F/pEyEvXPBOVwCgLZcWUleCM/C1okaKdTTA==", + "dev": true, + "peerDependencies": { + "chai": "2 - 4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -9601,6 +9611,13 @@ "type-detect": "^4.0.5" } }, + "chai-like": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chai-like/-/chai-like-1.1.1.tgz", + "integrity": "sha512-VKa9z/SnhXhkT1zIjtPACFWSoWsqVoaz1Vg+ecrKo5DCKVlgL30F/pEyEvXPBOVwCgLZcWUleCM/C1okaKdTTA==", + "dev": true, + "requires": {} + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/package.json b/package.json index 446c391f..962df017 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@typescript-eslint/parser": "^5.27.0", "browser-sync": "^2.27.10", "chai": "^4.3.6", + "chai-like": "^1.1.1", "copyfiles": "^2.4.1", "eslint": "^8.16.0", "express": "4.18.1", diff --git a/test/unit/compiler/passes/helpers.js b/test/unit/compiler/passes/helpers.js index 0979b114..779eb5cf 100644 --- a/test/unit/compiler/passes/helpers.js +++ b/test/unit/compiler/passes/helpers.js @@ -7,58 +7,18 @@ const Session = require("../../../../lib/compiler/session"); module.exports = function(chai, utils) { const Assertion = chai.Assertion; + chai.use(require("chai-like")); + Assertion.addMethod("changeAST", function(grammar, props, options) { options = options !== undefined ? options : {}; - function matchProps(value, props) { - function isArray(value) { - return Object.prototype.toString.apply(value) === "[object Array]"; - } - - function isObject(value) { - return value !== null && typeof value === "object"; - } - - if (isArray(props)) { - if (!isArray(value)) { return false; } - - if (value.length !== props.length) { return false; } - for (let i = 0; i < props.length; i++) { - if (!matchProps(value[i], props[i])) { return false; } - } - - return true; - } else if (isObject(props)) { - if (!isObject(value)) { return false; } - - const keys = Object.keys(props); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - - if (!(key in value)) { return false; } - - if (!matchProps(value[key], props[key])) { return false; } - } - - return true; - } else { - return value === props; - } - } - const ast = parser.parse(grammar); utils.flag(this, "object")(ast, options, new Session({ error(stage, ...args) { throw new GrammarError(...args); }, })); - this.assert( - matchProps(ast, props), - "expected #{this} to change the AST to match #{exp}", - "expected #{this} to not change the AST to match #{exp}", - props, - ast - ); + new Assertion(ast).like(props); }); Assertion.addMethod("reportError", function(grammar, props) { From 925b9dd89bffc385d12391fa8c79509795cdfb7d Mon Sep 17 00:00:00 2001 From: Mingun Date: Sat, 8 Nov 2014 23:11:50 +0500 Subject: [PATCH 4/4] Add new output type "ast" --- CHANGELOG.md | 3 ++ bin/peggy-cli.js | 42 +++++++++++++++++------- docs/documentation.html | 20 +++++++++--- lib/compiler/index.js | 3 ++ lib/peg.d.ts | 23 ++++++++++++- test/api/pegjs-api.spec.js | 11 +++++++ test/cli/run.spec.ts | 66 ++++++++++++++++++++++++++++++++++++++ test/types/peg.test-d.ts | 3 ++ 8 files changed, 153 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf978601..ac4cfa10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ Released: TBD - [#285](https://github.com/peggyjs/peggy/issues/285) Require that a non-empty string be given as a grammarSource if you are generating a source map, from @hildjj +- [#206](https://github.com/peggyjs/peggy/pull/206): New output type `ast` and + an `--ast` flag for the CLI to get an internal grammar AST for investigation + (can be useful for plugin writers), from @Mingun ### Bug Fixes diff --git a/bin/peggy-cli.js b/bin/peggy-cli.js index 06aa1377..eedf3dbd 100644 --- a/bin/peggy-cli.js +++ b/bin/peggy-cli.js @@ -14,7 +14,7 @@ exports.CommanderError = CommanderError; exports.InvalidArgumentError = InvalidArgumentError; // Options that aren't for the API directly: -const PROG_OPTIONS = ["input", "output", "sourceMap", "startRule", "test", "testFile", "verbose"]; +const PROG_OPTIONS = ["ast", "input", "output", "sourceMap", "startRule", "test", "testFile", "verbose"]; const MODULE_FORMATS = ["amd", "bare", "commonjs", "es", "globals", "umd"]; const MODULE_FORMATS_WITH_DEPS = ["amd", "commonjs", "es", "umd"]; const MODULE_FORMATS_WITH_GLOBAL = ["globals", "umd"]; @@ -173,14 +173,22 @@ class PeggyCLI extends Command { "-m, --source-map [mapfile]", "Generate a source map. If name is not specified, the source map will be named \".map\" if input is a file and \"source.map\" if input is a standard input. If the special filename `inline` is given, the sourcemap will be embedded in the output file as a data URI. If the filename is prefixed with `hidden:`, no mapping URL will be included so that the mapping can be specified with an HTTP SourceMap: header. This option conflicts with the `-t/--test` and `-T/--test-file` options unless `-o/--output` is also specified" ) + .addOption( + new Option( + "--ast", + "Output a grammar AST instead of a parser code" + ) + .default(false) + .conflicts(["test", "testFile", "sourceMap"]) + ) .option( "-S, --start-rule ", "When testing, use the given rule as the start rule. If this rule is not in the allowed start rules, it will be added." ) - .addOption(new Option( + .option( "-t, --test ", "Test the parser with the given text, outputting the result of running the parser instead of the parser itself. If the input to be tested is not parsed, the CLI will exit with code 2" - ).conflicts("test-file")) + ) .addOption(new Option( "-T, --test-file ", "Test the parser with the contents of the given file, outputting the result of running the parser instead of the parser itself. If the input to be tested is not parsed, the CLI will exit with code 2" @@ -313,6 +321,10 @@ class PeggyCLI extends Command { } } + if (this.progOptions.ast) { + this.argv.output = "ast"; + } + // Empty string is a valid test input. Don't just test for falsy. if (typeof this.progOptions.test === "string") { this.testText = this.progOptions.test; @@ -511,7 +523,7 @@ class PeggyCLI extends Command { }); } - writeParser(outputStream, source) { + writeOutput(outputStream, source) { return new Promise((resolve, reject) => { if (!outputStream) { resolve(); @@ -626,18 +638,24 @@ class PeggyCLI extends Command { this.verbose("CLI", errorText = "parsing grammar"); const source = peggy.generate(input, this.argv); // All of the real work. - this.verbose("CLI", errorText = "writing to output file"); + this.verbose("CLI", errorText = "open output stream"); const outputStream = await this.openOutputStream(); - this.verbose("CLI", errorText = "writing sourceMap"); - const mappedSource = await this.writeSourceMap(source); + // If option `--ast` is specified, `generate()` returns an AST object + if (this.progOptions.ast) { + this.verbose("CLI", errorText = "writing AST"); + await this.writeOutput(outputStream, JSON.stringify(source, null, 2)); + } else { + this.verbose("CLI", errorText = "writing sourceMap"); + const mappedSource = await this.writeSourceMap(source); - this.verbose("CLI", errorText = "writing parser"); - await this.writeParser(outputStream, mappedSource); + this.verbose("CLI", errorText = "writing parser"); + await this.writeOutput(outputStream, mappedSource); - exitCode = 2; - this.verbose("CLI", errorText = "running test"); - this.test(mappedSource); + exitCode = 2; + this.verbose("CLI", errorText = "running test"); + this.test(mappedSource); + } } catch (error) { // Will either exit or throw. this.error(errorText, { diff --git a/docs/documentation.html b/docs/documentation.html index c6371c65..6345af4f 100644 --- a/docs/documentation.html +++ b/docs/documentation.html @@ -171,6 +171,13 @@

Command Line

Comma-separated list of rules the parser will be allowed to start parsing from (default: only the first rule in the grammar).
+
--ast
+
Outputting an internal AST representation of the grammar after + all optimizations instead of the parser source code. Useful for plugin authors + to see how their plugin changes the AST. This option cannot be mixed with the + -t/--test, -T/--test-file and -m/--source-map + options.
+
--cache
Makes the parser cache results, avoiding exponential parsing time in pathological cases but making the parser slower.
@@ -227,21 +234,21 @@

Command Line

-t, --test <text>
Test the parser with the given text, outputting the result of running the parser against this input. - If the input to be tested is not parsed, the CLI will exit with code 2
+ If the input to be tested is not parsed, the CLI will exit with code 2.
-T, --test-file <text>
Test the parser with the contents of the given file, outputting the result of running the parser against this input. - If the input to be tested is not parsed, the CLI will exit with code 2
+ If the input to be tested is not parsed, the CLI will exit with code 2.
--trace
Makes the parser trace its progress.
-v, --version
-
Output the version number
+
Output the version number.
-h, --help
-
Display help for the command
+
Display help for the command.
@@ -270,7 +277,7 @@

Command Line

You can test generated parser immediately if you specify the -t/--test or -T/--test-file option. This option conflicts with the option -m/--source-map unless -o/--output is -also specified. +also specified. This option conflicts with the --ast option.

The CLI will exit with the code:

@@ -411,6 +418,9 @@

JavaScript API

with an embedded source map as a data: URI. This option leads to a larger output string, but is the easiest to integrate with developer tooling. +
  • "ast" - return the internal AST of the grammar as a JSON + string. Useful for plugin authors to explore internals of Peggy and + for automation.
  • (default: "parser")

    diff --git a/lib/compiler/index.js b/lib/compiler/index.js index 4896b398..bff2a0ec 100644 --- a/lib/compiler/index.js +++ b/lib/compiler/index.js @@ -141,6 +141,9 @@ const compiler = { `; } + case "ast": + return ast; + default: throw new Error("Invalid output format: " + options.output + "."); } diff --git a/lib/peg.d.ts b/lib/peg.d.ts index c120acb9..6af2a60c 100644 --- a/lib/peg.d.ts +++ b/lib/peg.d.ts @@ -1079,7 +1079,7 @@ export type SourceOutputs = "source-with-inline-map"; /** Base options for all source-generating formats. */ -interface SourceOptionsBase +interface SourceOptionsBase extends BuildOptionsBase { /** * If set to `"parser"`, the method will return generated parser object; @@ -1246,5 +1246,26 @@ export function generate( options: SourceBuildOptions ): string | SourceNode; +/** + * Returns the generated AST for the grammar. Unlike result of the + * `peggy.compiler.compile(...)` an AST returned by this method is augmented + * with data from passes. In other words, the compiler gives you the raw AST, + * and this method provides the final AST after all optimizations and + * transformations. + * + * @param grammar String in the format described by the meta-grammar in the + * `parser.pegjs` file + * @param options Options that allow you to customize returned AST + * + * @throws {SyntaxError} If the grammar contains a syntax error, for example, + * an unclosed brace + * @throws {GrammarError} If the grammar contains a semantic error, for example, + * duplicated labels + */ +export function generate( + grammar: string, + options: SourceOptionsBase<"ast"> +): ast.Grammar; + // Export all exported stuff under a global variable PEG in non-module environments export as namespace PEG; diff --git a/test/api/pegjs-api.spec.js b/test/api/pegjs-api.spec.js index bfe5a08b..a93fbd06 100644 --- a/test/api/pegjs-api.spec.js +++ b/test/api/pegjs-api.spec.js @@ -10,6 +10,8 @@ exports.peggyVersion = function peggyVersion() { return peg.VERSION; }; +chai.use(require("chai-like")); + beforeEach(() => { // In the browser, initialize SourceMapConsumer's wasm bits. // This is *async*, so make sure to return the promise to make @@ -173,6 +175,15 @@ describe("Peggy API", () => { expect(eval(source).parse("a")).to.equal("a"); }); }); + + describe("when |output| is set to |\"ast\"|", () => { + it("returns generated parser AST", () => { + const ast = peg.generate(grammar, { output: "ast" }); + + expect(ast).to.be.an("object"); + expect(ast).to.be.like(peg.parser.parse(grammar)); + }); + }); }); // The |format|, |exportVars|, and |dependencies| options are not tested diff --git a/test/cli/run.spec.ts b/test/cli/run.spec.ts index 72dde451..ad2e57d7 100644 --- a/test/cli/run.spec.ts +++ b/test/cli/run.spec.ts @@ -356,6 +356,8 @@ Options: This option conflicts with the \`-t/--test\` and \`-T/--test-file\` options unless \`-o/--output\` is also specified + --ast Output a grammar AST instead of a parser + code (default: false) -S, --start-rule When testing, use the given rule as the start rule. If this rule is not in the allowed start rules, it will be added. @@ -633,6 +635,70 @@ Options: }); }); + describe("--ast option", () => { + it("conflicts with --test/--test-file/--source-map", async() => { + await exec({ + args: ["--ast", "--test", "1"], + stdin: 'foo = "1"', + error: CommanderError, + errorCode: "commander.conflictingOption", + exitCode: 1, + expected: "error: option '--ast' cannot be used with option '-t, --test '\n", + }); + await exec({ + args: ["--ast", "--test-file", "file"], + stdin: 'foo = "1"', + error: CommanderError, + errorCode: "commander.conflictingOption", + exitCode: 1, + expected: "error: option '--ast' cannot be used with option '-T, --test-file '\n", + }); + await exec({ + args: ["--ast", "--source-map"], + stdin: 'foo = "1"', + error: CommanderError, + errorCode: "commander.conflictingOption", + exitCode: 1, + expected: "error: option '--ast' cannot be used with option '-m, --source-map [mapfile]'\n", + }); + await exec({ + args: ["--ast", "--source-map", "file"], + stdin: 'foo = "1"', + error: CommanderError, + errorCode: "commander.conflictingOption", + exitCode: 1, + expected: "error: option '--ast' cannot be used with option '-m, --source-map [mapfile]'\n", + }); + }); + + it("produces AST", async() => { + const output = await exec({ + args: ["--ast"], + stdin: 'foo = "1"', + }); + + // Do not check exact location information and concrete values of some other fields + expect(JSON.parse(output)).toMatchObject({ + type: "grammar", + topLevelInitializer: null, + initializer: null, + location: {}, + rules: [{ + type: "rule", + name: "foo", + location: {}, + expression: { + type: "literal", + value: "1", + ignoreCase: false, + location: {}, + }, + }], + code: expect.anything(), + }); + }); + }); + it("doesn't fail with optimize", async() => { await exec({ args: ["--optimize", "anything"], diff --git a/test/types/peg.test-d.ts b/test/types/peg.test-d.ts index 155e8c69..7c0190e6 100644 --- a/test/types/peg.test-d.ts +++ b/test/types/peg.test-d.ts @@ -104,6 +104,9 @@ describe("peg.d.ts", () => { const p2 = peggy.generate(src, { output: "source", grammarSource: { foo: "src" } }); expectType(p2); + + const p3 = peggy.generate(src, { output: "ast", grammarSource: { foo: "src" } }); + expectType(p3); }); it("generates a source map", () => {