Skip to content

Commit e50c220

Browse files
committed
Add new output type "ast" (TODO: failed test)
1 parent 6b3212d commit e50c220

File tree

8 files changed

+130
-18
lines changed

8 files changed

+130
-18
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ Released: 2022-05-28
9797
XML and source-mapping, from @hildjj
9898
- [#204](https://github.com/peggyjs/peggy/pull/204): Increase coverage for the
9999
tests, from @Mingun
100+
- [#206](https://github.com/peggyjs/peggy/pull/206): New output type `ast` and a
101+
flag `-a/--ast` for the CLI to get an internal grammar AST for investigation
102+
(can be useful for plugin writers), from @Mingun
100103
- [#210](https://github.com/peggyjs/peggy/pull/210): Refactor CLI testing,
101104
from @hildjj
102105

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+
"-a, --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

+19-5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ <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>-a</code>, <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> and <code>-T/--test-file</code> options.</dd>
179+
174180
<dt><code>--cache</code></dt>
175181
<dd>Makes the parser cache results, avoiding exponential parsing time in
176182
pathological cases but making the parser slower.</dd>
@@ -227,21 +233,21 @@ <h3 id="generating-a-parser-command-line">Command Line</h3>
227233
<dt><code>-t</code>, <code>--test &lt;text&gt;</code></dt>
228234
<dd>Test the parser with the given text, outputting the result of running
229235
the parser against this input.
230-
If the input to be tested is not parsed, the CLI will exit with code 2</dd>
236+
If the input to be tested is not parsed, the CLI will exit with code 2.</dd>
231237

232238
<dt><code>-T</code>, <code>--test-file &lt;text&gt;</code></dt>
233239
<dd>Test the parser with the contents of the given file, outputting the
234240
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>
241+
If the input to be tested is not parsed, the CLI will exit with code 2.</dd>
236242

237243
<dt><code>--trace</code></dt>
238244
<dd>Makes the parser trace its progress.</dd>
239245

240246
<dt><code>-v</code>, <code>--version</code></dt>
241-
<dd>Output the version number</dd>
247+
<dd>Output the version number.</dd>
242248

243249
<dt><code>-h</code>, <code>--help</code></dt>
244-
<dd>Display help for the command</dd>
250+
<dd>Display help for the command.</dd>
245251

246252
</dl>
247253

@@ -270,7 +276,7 @@ <h3 id="generating-a-parser-command-line">Command Line</h3>
270276
<p>
271277
You can test generated parser immediately if you specify the <code>-t/--test</code> or <code>-T/--test-file</code>
272278
option. This option conflicts with the option <code>-m/--source-map</code> unless <code>-o/--output</code> is
273-
also specified.
279+
also specified. This option conflicts with the <code>-a/--ast</code> option.
274280
</p>
275281

276282
<p>The CLI will exit with the code:</p>
@@ -411,6 +417,9 @@ <h3 id="generating-a-parser-javascript-api">JavaScript API</h3>
411417
with an embedded source map as a <code>data:</code> URI. This option
412418
leads to a larger output string, but is the easiest to integrate with
413419
developer tooling.</li>
420+
<li><code>"ast"</code> - return the internal AST of the grammar as a JSON
421+
string. Useful for plugin authors to explore internals of PeggyJs and
422+
for automation.</li>
414423
</ul>
415424
<p>(default: <code>"parser"</code>)</p>
416425
<blockquote>
@@ -426,6 +435,11 @@ <h3 id="generating-a-parser-javascript-api">JavaScript API</h3>
426435
</blockquote>
427436
</dd>
428437

438+
<blockquote>
439+
<p><strong>Note</strong>: because of bug <a href="https://github.com/mozilla/source-map/issues/444">source-map/444</a> you should also set <code>grammarSource</code> to
440+
a not-empty string if you set this value to <code>"source-and-map"</code></p>
441+
</blockquote></dd>
442+
429443
<dt><code>plugins</code></dt>
430444
<dd>Plugins to use. See the <a href="#plugins-api">Plugins API</a> section.</dd>
431445

lib/compiler/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ const compiler = {
133133
`;
134134
}
135135

136+
case "ast":
137+
return ast;
138+
136139
default:
137140
throw new Error("Invalid output format: " + options.output + ".");
138141
}

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;

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

test/cli/run.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ Options:
334334
This option conflicts with the \`-t/--test\`
335335
and \`-T/--test-file\` options unless
336336
\`-o/--output\` is also specified
337+
-a, --ast Output a grammar AST instead of a parser
338+
code (default: false)
337339
-S, --start-rule <rule> When testing, use the given rule as the
338340
start rule. If this rule is not in the
339341
allowed start rules, it will be added.
@@ -611,6 +613,43 @@ Options:
611613
});
612614
});
613615

616+
describe("handles ast output", () => {
617+
it("conflicts with --test/--test-file", async() => {
618+
await exec({
619+
args: ["--ast", "--test", "1"],
620+
stdin: 'foo = "1"',
621+
exitCode: 1,
622+
expected: "CommanderError: error: option '-a, --ast' cannot be used with option '-t, --test <text>'",
623+
});
624+
await exec({
625+
args: ["--ast", "--test-file", "file"],
626+
stdin: 'foo = "1"',
627+
exitCode: 1,
628+
expected: "CommanderError: error: option '-a, --ast' cannot be used with option '-t, --test <text>'",
629+
});
630+
});
631+
632+
it("produces AST", async() => {
633+
const output = await exec({
634+
args: ["--ast"],
635+
stdin: 'foo = "1"',
636+
exitCode: 0,
637+
});
638+
639+
expect(output).toBeInstanceOf(String);
640+
expect(() => JSON.parse(output)).toMatchObject({
641+
type: "grammar",
642+
rules: [{
643+
type: "rule",
644+
topLevelInitializer: null,
645+
initializer: null,
646+
name: "foo",
647+
expression: {},
648+
}],
649+
});
650+
});
651+
});
652+
614653
it("doesn't fail with optimize", async() => {
615654
await exec({
616655
args: ["--optimize", "anything"],

test/types/peg.test-d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ describe("peg.d.ts", () => {
104104

105105
const p2 = peggy.generate(src, { output: "source", grammarSource: { foo: "src" } });
106106
expectType<string>(p2);
107+
108+
const p3 = peggy.generate(src, { output: "ast", grammarSource: { foo: "src" } });
109+
expectType<peggy.ast.Grammar>(p3);
107110
});
108111

109112
it("generates a source map", () => {

0 commit comments

Comments
 (0)