Skip to content

Commit 48585a5

Browse files
authored
Merge pull request #206 from Mingun/debug-helpers
Debug helpers - new output type `ast` / CLI option `-a/--ast`
2 parents 7852239 + 925b9dd commit 48585a5

15 files changed

+219
-94
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Released: TBD
2929
- [#285](https://github.com/peggyjs/peggy/issues/285) Require that a non-empty
3030
string be given as a grammarSource if you are generating a source map, from
3131
@hildjj
32+
- [#206](https://github.com/peggyjs/peggy/pull/206): New output type `ast` and
33+
an `--ast` flag for the CLI to get an internal grammar AST for investigation
34+
(can be useful for plugin writers), from @Mingun
3235

3336
### Bug Fixes
3437

bin/peggy-cli.js

+30-12
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ exports.CommanderError = CommanderError;
1414
exports.InvalidArgumentError = InvalidArgumentError;
1515

1616
// Options that aren't for the API directly:
17-
const PROG_OPTIONS = ["input", "output", "sourceMap", "startRule", "test", "testFile", "verbose"];
17+
const PROG_OPTIONS = ["ast", "input", "output", "sourceMap", "startRule", "test", "testFile", "verbose"];
1818
const MODULE_FORMATS = ["amd", "bare", "commonjs", "es", "globals", "umd"];
1919
const MODULE_FORMATS_WITH_DEPS = ["amd", "commonjs", "es", "umd"];
2020
const MODULE_FORMATS_WITH_GLOBAL = ["globals", "umd"];
@@ -173,14 +173,22 @@ class PeggyCLI extends Command {
173173
"-m, --source-map [mapfile]",
174174
"Generate a source map. If name is not specified, the source map will be named \"<input_file>.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"
175175
)
176+
.addOption(
177+
new Option(
178+
"--ast",
179+
"Output a grammar AST instead of a parser code"
180+
)
181+
.default(false)
182+
.conflicts(["test", "testFile", "sourceMap"])
183+
)
176184
.option(
177185
"-S, --start-rule <rule>",
178186
"When testing, use the given rule as the start rule. If this rule is not in the allowed start rules, it will be added."
179187
)
180-
.addOption(new Option(
188+
.option(
181189
"-t, --test <text>",
182190
"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"
183-
).conflicts("test-file"))
191+
)
184192
.addOption(new Option(
185193
"-T, --test-file <filename>",
186194
"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 {
313321
}
314322
}
315323

324+
if (this.progOptions.ast) {
325+
this.argv.output = "ast";
326+
}
327+
316328
// Empty string is a valid test input. Don't just test for falsy.
317329
if (typeof this.progOptions.test === "string") {
318330
this.testText = this.progOptions.test;
@@ -511,7 +523,7 @@ class PeggyCLI extends Command {
511523
});
512524
}
513525

514-
writeParser(outputStream, source) {
526+
writeOutput(outputStream, source) {
515527
return new Promise((resolve, reject) => {
516528
if (!outputStream) {
517529
resolve();
@@ -626,18 +638,24 @@ class PeggyCLI extends Command {
626638
this.verbose("CLI", errorText = "parsing grammar");
627639
const source = peggy.generate(input, this.argv); // All of the real work.
628640

629-
this.verbose("CLI", errorText = "writing to output file");
641+
this.verbose("CLI", errorText = "open output stream");
630642
const outputStream = await this.openOutputStream();
631643

632-
this.verbose("CLI", errorText = "writing sourceMap");
633-
const mappedSource = await this.writeSourceMap(source);
644+
// If option `--ast` is specified, `generate()` returns an AST object
645+
if (this.progOptions.ast) {
646+
this.verbose("CLI", errorText = "writing AST");
647+
await this.writeOutput(outputStream, JSON.stringify(source, null, 2));
648+
} else {
649+
this.verbose("CLI", errorText = "writing sourceMap");
650+
const mappedSource = await this.writeSourceMap(source);
634651

635-
this.verbose("CLI", errorText = "writing parser");
636-
await this.writeParser(outputStream, mappedSource);
652+
this.verbose("CLI", errorText = "writing parser");
653+
await this.writeOutput(outputStream, mappedSource);
637654

638-
exitCode = 2;
639-
this.verbose("CLI", errorText = "running test");
640-
this.test(mappedSource);
655+
exitCode = 2;
656+
this.verbose("CLI", errorText = "running test");
657+
this.test(mappedSource);
658+
}
641659
} catch (error) {
642660
// Will either exit or throw.
643661
this.error(errorText, {

docs/documentation.html

+15-5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ <h3 id="generating-a-parser-command-line">Command Line</h3>
171171
<dd>Comma-separated list of rules the parser will be allowed to start parsing
172172
from (default: only the first rule in the grammar).</dd>
173173

174+
<dt><code>--ast</code></dt>
175+
<dd>Outputting an internal AST representation of the grammar after
176+
all optimizations instead of the parser source code. Useful for plugin authors
177+
to see how their plugin changes the AST. This option cannot be mixed with the
178+
<code>-t/--test</code>, <code>-T/--test-file</code> and <code>-m/--source-map</code>
179+
options.</dd>
180+
174181
<dt><code>--cache</code></dt>
175182
<dd>Makes the parser cache results, avoiding exponential parsing time in
176183
pathological cases but making the parser slower.</dd>
@@ -227,21 +234,21 @@ <h3 id="generating-a-parser-command-line">Command Line</h3>
227234
<dt><code>-t</code>, <code>--test &lt;text&gt;</code></dt>
228235
<dd>Test the parser with the given text, outputting the result of running
229236
the parser against this input.
230-
If the input to be tested is not parsed, the CLI will exit with code 2</dd>
237+
If the input to be tested is not parsed, the CLI will exit with code 2.</dd>
231238

232239
<dt><code>-T</code>, <code>--test-file &lt;text&gt;</code></dt>
233240
<dd>Test the parser with the contents of the given file, outputting the
234241
result of running the parser against this input.
235-
If the input to be tested is not parsed, the CLI will exit with code 2</dd>
242+
If the input to be tested is not parsed, the CLI will exit with code 2.</dd>
236243

237244
<dt><code>--trace</code></dt>
238245
<dd>Makes the parser trace its progress.</dd>
239246

240247
<dt><code>-v</code>, <code>--version</code></dt>
241-
<dd>Output the version number</dd>
248+
<dd>Output the version number.</dd>
242249

243250
<dt><code>-h</code>, <code>--help</code></dt>
244-
<dd>Display help for the command</dd>
251+
<dd>Display help for the command.</dd>
245252

246253
</dl>
247254

@@ -270,7 +277,7 @@ <h3 id="generating-a-parser-command-line">Command Line</h3>
270277
<p>
271278
You can test generated parser immediately if you specify the <code>-t/--test</code> or <code>-T/--test-file</code>
272279
option. This option conflicts with the option <code>-m/--source-map</code> unless <code>-o/--output</code> is
273-
also specified.
280+
also specified. This option conflicts with the <code>--ast</code> option.
274281
</p>
275282

276283
<p>The CLI will exit with the code:</p>
@@ -411,6 +418,9 @@ <h3 id="generating-a-parser-javascript-api">JavaScript API</h3>
411418
with an embedded source map as a <code>data:</code> URI. This option
412419
leads to a larger output string, but is the easiest to integrate with
413420
developer tooling.</li>
421+
<li><code>"ast"</code> - return the internal AST of the grammar as a JSON
422+
string. Useful for plugin authors to explore internals of Peggy and
423+
for automation.</li>
414424
</ul>
415425
<p>(default: <code>"parser"</code>)</p>
416426
<blockquote>

lib/compiler/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ const compiler = {
141141
`;
142142
}
143143

144+
case "ast":
145+
return ast;
146+
144147
default:
145148
throw new Error("Invalid output format: " + options.output + ".");
146149
}

lib/compiler/passes/generate-js.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ function generateJS(ast, options) {
258258

259259
function generateRuleFunction(rule) {
260260
const parts = [];
261-
const stack = new Stack(rule.name, "s", "var");
261+
const stack = new Stack(rule.name, "s", "var", rule.bytecode);
262262

263263
function compile(bc) {
264264
let ip = 0;
@@ -528,7 +528,7 @@ function generateJS(ast, options) {
528528

529529
// istanbul ignore next Because we never generate invalid bytecode we cannot reach this branch
530530
default:
531-
throw new Error("Invalid opcode: " + bc[ip] + ".");
531+
throw new Error("Invalid opcode: " + bc[ip] + ".", { rule: rule.name, bytecode: bc });
532532
}
533533
}
534534

lib/compiler/stack.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ class Stack {
88
* @param {string} ruleName The name of rule that will be used in error messages
99
* @param {string} varName The prefix for generated names of variables
1010
* @param {string} type The type of the variables. For JavaScript there are `var` or `let`
11+
* @param {number[]} bytecode Bytecode for error messages
1112
*/
12-
constructor(ruleName, varName, type) {
13+
constructor(ruleName, varName, type, bytecode) {
1314
/** Last used variable in the stack. */
1415
this.sp = -1;
1516
/** Maximum stack size. */
1617
this.maxSp = -1;
1718
this.varName = varName;
1819
this.ruleName = ruleName;
1920
this.type = type;
21+
this.bytecode = bytecode;
2022
}
2123

2224
/**
@@ -30,7 +32,7 @@ class Stack {
3032
name(i) {
3133
if (i < 0) {
3234
throw new RangeError(
33-
`Rule '${this.ruleName}': The variable stack underflow: attempt to use a variable '${this.varName}<x>' at an index ${i}`
35+
`Rule '${this.ruleName}': The variable stack underflow: attempt to use a variable '${this.varName}<x>' at an index ${i}.\nBytecode: ${this.bytecode}`
3436
);
3537
}
3638

@@ -90,7 +92,7 @@ class Stack {
9092
index(i) {
9193
if (i < 0) {
9294
throw new RangeError(
93-
`Rule '${this.ruleName}': The variable stack overflow: attempt to get a variable at a negative index ${i}`
95+
`Rule '${this.ruleName}': The variable stack overflow: attempt to get a variable at a negative index ${i}.\nBytecode: ${this.bytecode}`
9496
);
9597
}
9698

@@ -107,7 +109,7 @@ class Stack {
107109
result() {
108110
if (this.maxSp < 0) {
109111
throw new RangeError(
110-
`Rule '${this.ruleName}': The variable stack is empty, can't get the result'`
112+
`Rule '${this.ruleName}': The variable stack is empty, can't get the result.\nBytecode: ${this.bytecode}`
111113
);
112114
}
113115

@@ -154,7 +156,8 @@ class Stack {
154156
throw new Error(
155157
"Rule '" + this.ruleName + "', position " + pos + ": "
156158
+ "Branches of a condition can't move the stack pointer differently "
157-
+ "(before: " + baseSp + ", after then: " + thenSp + ", after else: " + this.sp + ")."
159+
+ "(before: " + baseSp + ", after then: " + thenSp + ", after else: " + this.sp + "). "
160+
+ "Bytecode: " + this.bytecode
158161
);
159162
}
160163
}
@@ -178,7 +181,8 @@ class Stack {
178181
throw new Error(
179182
"Rule '" + this.ruleName + "', position " + pos + ": "
180183
+ "Body of a loop can't move the stack pointer "
181-
+ "(before: " + baseSp + ", after: " + this.sp + ")."
184+
+ "(before: " + baseSp + ", after: " + this.sp + "). "
185+
+ "Bytecode: " + this.bytecode
182186
);
183187
}
184188
}

lib/peg.d.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ export type SourceOutputs =
10791079
"source-with-inline-map";
10801080

10811081
/** Base options for all source-generating formats. */
1082-
interface SourceOptionsBase<Output extends SourceOutputs>
1082+
interface SourceOptionsBase<Output>
10831083
extends BuildOptionsBase {
10841084
/**
10851085
* If set to `"parser"`, the method will return generated parser object;
@@ -1246,5 +1246,26 @@ export function generate(
12461246
options: SourceBuildOptions<SourceOutputs>
12471247
): string | SourceNode;
12481248

1249+
/**
1250+
* Returns the generated AST for the grammar. Unlike result of the
1251+
* `peggy.compiler.compile(...)` an AST returned by this method is augmented
1252+
* with data from passes. In other words, the compiler gives you the raw AST,
1253+
* and this method provides the final AST after all optimizations and
1254+
* transformations.
1255+
*
1256+
* @param grammar String in the format described by the meta-grammar in the
1257+
* `parser.pegjs` file
1258+
* @param options Options that allow you to customize returned AST
1259+
*
1260+
* @throws {SyntaxError} If the grammar contains a syntax error, for example,
1261+
* an unclosed brace
1262+
* @throws {GrammarError} If the grammar contains a semantic error, for example,
1263+
* duplicated labels
1264+
*/
1265+
export function generate(
1266+
grammar: string,
1267+
options: SourceOptionsBase<"ast">
1268+
): ast.Grammar;
1269+
12491270
// Export all exported stuff under a global variable PEG in non-module environments
12501271
export as namespace PEG;

package-lock.json

+19-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@typescript-eslint/parser": "^5.27.0",
6363
"browser-sync": "^2.27.10",
6464
"chai": "^4.3.6",
65+
"chai-like": "^1.1.1",
6566
"copyfiles": "^2.4.1",
6667
"eslint": "^8.16.0",
6768
"express": "4.18.1",

test/api/pegjs-api.spec.js

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ exports.peggyVersion = function peggyVersion() {
1010
return peg.VERSION;
1111
};
1212

13+
chai.use(require("chai-like"));
14+
1315
beforeEach(() => {
1416
// In the browser, initialize SourceMapConsumer's wasm bits.
1517
// This is *async*, so make sure to return the promise to make
@@ -173,6 +175,15 @@ describe("Peggy API", () => {
173175
expect(eval(source).parse("a")).to.equal("a");
174176
});
175177
});
178+
179+
describe("when |output| is set to |\"ast\"|", () => {
180+
it("returns generated parser AST", () => {
181+
const ast = peg.generate(grammar, { output: "ast" });
182+
183+
expect(ast).to.be.an("object");
184+
expect(ast).to.be.like(peg.parser.parse(grammar));
185+
});
186+
});
176187
});
177188

178189
// The |format|, |exportVars|, and |dependencies| options are not tested

0 commit comments

Comments
 (0)