Skip to content

Commit 0fc2c1d

Browse files
committed
Grammar compilation error
1 parent 90ef278 commit 0fc2c1d

File tree

8 files changed

+332
-130
lines changed

8 files changed

+332
-130
lines changed

ts/packages/actionGrammar/src/grammarCompiler.ts

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,93 @@ import { Rule, RuleDefinition } from "./grammarRuleParser.js";
1111

1212
type DefinitionMap = Map<
1313
string,
14-
{ rules: Rule[]; grammarRules?: GrammarRule[] }
14+
{ rules: Rule[]; pos: number | undefined; grammarRules?: GrammarRule[] }
1515
>;
1616

17-
export function compileGrammar(definitions: RuleDefinition[]): Grammar {
17+
type GrammarCompileResult = {
18+
grammar: Grammar;
19+
errors: GrammarCompileError[];
20+
warnings: GrammarCompileError[];
21+
};
22+
23+
export type GrammarCompileError = {
24+
message: string;
25+
definition?: string | undefined;
26+
pos?: number | undefined;
27+
};
28+
29+
type CompileContext = {
30+
ruleDefMap: DefinitionMap;
31+
currentDefinition?: string | undefined;
32+
errors: GrammarCompileError[];
33+
warnings: GrammarCompileError[];
34+
};
35+
36+
export function compileGrammar(
37+
definitions: RuleDefinition[],
38+
start: string,
39+
): GrammarCompileResult {
1840
const ruleDefMap: DefinitionMap = new Map();
41+
const context: CompileContext = {
42+
ruleDefMap,
43+
errors: [],
44+
warnings: [],
45+
};
46+
1947
for (const def of definitions) {
2048
const existing = ruleDefMap.get(def.name);
2149
if (existing === undefined) {
22-
ruleDefMap.set(def.name, { rules: [...def.rules] });
50+
ruleDefMap.set(def.name, { rules: [...def.rules], pos: def.pos });
2351
} else {
2452
existing.rules.push(...def.rules);
2553
}
2654
}
27-
return { rules: createGrammarRules(ruleDefMap, "Start") };
55+
const grammar = { rules: createGrammarRules(context, start) };
56+
57+
for (const [name, record] of ruleDefMap.entries()) {
58+
if (record.grammarRules === undefined) {
59+
context.warnings.push({
60+
message: `Rule '<${name}>' is defined but never used.`,
61+
pos: record.pos,
62+
});
63+
}
64+
}
65+
return {
66+
grammar,
67+
errors: context.errors,
68+
warnings: context.warnings,
69+
};
2870
}
2971

72+
const emptyRecord = { rules: [], pos: undefined, grammarRules: [] };
3073
function createGrammarRules(
31-
ruleDefMap: DefinitionMap,
74+
context: CompileContext,
3275
name: string,
76+
pos?: number,
3377
): GrammarRule[] {
34-
const record = ruleDefMap.get(name);
78+
const record = context.ruleDefMap.get(name);
3579
if (record === undefined) {
36-
throw new Error(`Missing rule definition for '<${name}>'`);
80+
context.errors.push({
81+
message: `Missing rule definition for '<${name}>'`,
82+
definition: context.currentDefinition,
83+
pos,
84+
});
85+
context.ruleDefMap.set(name, emptyRecord);
86+
return emptyRecord.grammarRules;
3787
}
3888
if (record.grammarRules === undefined) {
3989
record.grammarRules = [];
90+
const prev = context.currentDefinition;
91+
context.currentDefinition = name;
4092
for (const r of record.rules) {
41-
record.grammarRules.push(createGrammarRule(ruleDefMap, r));
93+
record.grammarRules.push(createGrammarRule(context, r));
4294
}
95+
context.currentDefinition = prev;
4396
}
4497
return record.grammarRules;
4598
}
4699

47-
function createGrammarRule(ruleDefMap: DefinitionMap, rule: Rule): GrammarRule {
100+
function createGrammarRule(context: CompileContext, rule: Rule): GrammarRule {
48101
const { expressions, value } = rule;
49102
const parts: GrammarPart[] = [];
50103
for (const expr of expressions) {
@@ -59,15 +112,15 @@ function createGrammarRule(ruleDefMap: DefinitionMap, rule: Rule): GrammarRule {
59112
break;
60113
}
61114
case "variable": {
62-
const { name, typeName, ruleReference } = expr;
115+
const { name, typeName, ruleReference, ruleRefPos } = expr;
63116
if (ruleReference) {
64-
const rules = ruleDefMap.get(typeName);
65-
if (rules === undefined) {
66-
throw new Error(`No rule named ${typeName}`);
67-
}
68117
parts.push({
69118
type: "rules",
70-
rules: createGrammarRules(ruleDefMap, typeName),
119+
rules: createGrammarRules(
120+
context,
121+
typeName,
122+
ruleRefPos,
123+
),
71124
variable: name,
72125
name: typeName,
73126
optional: expr.optional,
@@ -91,23 +144,23 @@ function createGrammarRule(ruleDefMap: DefinitionMap, rule: Rule): GrammarRule {
91144
case "ruleReference":
92145
parts.push({
93146
type: "rules",
94-
rules: createGrammarRules(ruleDefMap, expr.name),
147+
rules: createGrammarRules(context, expr.name, expr.pos),
95148
name: expr.name,
96149
});
97150
break;
98151
case "rules": {
99152
const { rules, optional } = expr;
100153
parts.push({
101154
type: "rules",
102-
rules: rules.map((r) => createGrammarRule(ruleDefMap, r)),
155+
rules: rules.map((r) => createGrammarRule(context, r)),
103156
optional,
104157
});
105158

106159
break;
107160
}
108161
default:
109162
throw new Error(
110-
`Unknown expression type ${(expr as any).type}`,
163+
`Internal Error: Unknown expression type ${(expr as any).type}`,
111164
);
112165
}
113166
}
Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import { compileGrammar } from "./grammarCompiler.js";
4+
import { compileGrammar, GrammarCompileError } from "./grammarCompiler.js";
55
import { parseGrammarRules } from "./grammarRuleParser.js";
66
import { Grammar } from "./grammarTypes.js";
7+
import { getLineCol } from "./utils.js";
78

8-
export function loadGrammarRules(fileName: string, content: string): Grammar {
9+
// REVIEW: start symbol should be configurable
10+
const start = "Start";
11+
12+
function convertCompileError(
13+
fileName: string,
14+
content: string,
15+
type: "error" | "warning",
16+
errors: GrammarCompileError[],
17+
) {
18+
return errors.map((e) => {
19+
const lineCol = getLineCol(content, e.pos ?? 0);
20+
return `${fileName}(${lineCol.line},${lineCol.col}): ${type}: ${e.message}${e.definition ? ` in definition '<${e.definition}>'` : ""}`;
21+
});
22+
}
23+
export function loadGrammarRules(
24+
fileName: string,
25+
content: string,
26+
errors?: string[],
27+
warnings?: string[],
28+
): Grammar {
929
const definitions = parseGrammarRules(fileName, content);
10-
const grammar = compileGrammar(definitions);
11-
return grammar;
30+
const result = compileGrammar(definitions, start);
31+
32+
if (result.errors.length > 0) {
33+
const errorMessages = convertCompileError(
34+
fileName,
35+
content,
36+
"error",
37+
result.errors,
38+
);
39+
if (errors) {
40+
errors.push(...errorMessages);
41+
} else {
42+
const errorStr = result.errors.length === 1 ? "error" : "errors";
43+
errorMessages.unshift(
44+
`Error detected in grammar compilation '${fileName}': ${result.errors.length} ${errorStr}.`,
45+
);
46+
throw new Error(errorMessages.join("\n"));
47+
}
48+
}
49+
50+
if (result.warnings.length > 0 && warnings !== undefined) {
51+
warnings.push(
52+
...convertCompileError(
53+
fileName,
54+
content,
55+
"warning",
56+
result.warnings,
57+
),
58+
);
59+
}
60+
return result.grammar;
1261
}

ts/packages/actionGrammar/src/grammarRuleParser.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ const debugParse = registerDebug("typeagent:grammar:parse");
5050
export function parseGrammarRules(
5151
fileName: string,
5252
content: string,
53+
position: boolean = true,
5354
): RuleDefinition[] {
54-
const parser = new GrammarRuleParser(fileName, content);
55+
const parser = new GrammarRuleParser(fileName, content, position);
5556
const definitions = parser.parse();
5657
debugParse(JSON.stringify(definitions, undefined, 2));
5758
return definitions;
@@ -67,6 +68,7 @@ type StrExpr = {
6768
type RuleRefExpr = {
6869
type: "ruleReference";
6970
name: string;
71+
pos?: number | undefined;
7072
};
7173

7274
type RulesExpr = {
@@ -80,6 +82,7 @@ type VarDefExpr = {
8082
name: string;
8183
typeName: string;
8284
ruleReference: boolean;
85+
ruleRefPos?: number | undefined;
8386
optional?: boolean;
8487
};
8588

@@ -113,6 +116,7 @@ export type Rule = {
113116
export type RuleDefinition = {
114117
name: string;
115118
rules: Rule[];
119+
pos?: number | undefined;
116120
};
117121

118122
export function isWhitespace(char: string) {
@@ -152,8 +156,13 @@ class GrammarRuleParser {
152156
constructor(
153157
private readonly fileName: string,
154158
private readonly content: string,
159+
private readonly position: boolean = true,
155160
) {}
156161

162+
private get pos(): number | undefined {
163+
return this.position ? this.curr : undefined;
164+
}
165+
157166
private isAtWhiteSpace() {
158167
return !this.isAtEnd() && isWhitespace(this.content[this.curr]);
159168
}
@@ -319,14 +328,16 @@ class GrammarRuleParser {
319328
const id = this.parseId("Variable name");
320329
let typeName: string = "string";
321330
let ruleReference: boolean = false;
331+
let ruleRefPos: number | undefined = undefined;
322332

323333
if (this.isAt(":")) {
324334
// Consume colon
325335
this.skipWhitespace(1);
326336

327337
if (this.isAt("<")) {
328-
typeName = this.parseRuleName();
338+
ruleRefPos = this.pos;
329339
ruleReference = true;
340+
typeName = this.parseRuleName();
330341
} else {
331342
typeName = this.parseId("Type name");
332343
}
@@ -336,18 +347,17 @@ class GrammarRuleParser {
336347
name: id,
337348
typeName,
338349
ruleReference,
350+
ruleRefPos,
339351
};
340352
}
341353

342354
private parseExpression(): Expr[] {
343355
const expNodes: Expr[] = [];
344356
do {
345357
if (this.isAt("<")) {
346-
const n = this.parseRuleName();
347-
expNodes.push({
348-
type: "ruleReference",
349-
name: n,
350-
});
358+
const pos = this.pos;
359+
const name = this.parseRuleName();
360+
expNodes.push({ type: "ruleReference", name, pos });
351361
continue;
352362
}
353363
if (this.isAt("$(")) {
@@ -575,13 +585,11 @@ class GrammarRuleParser {
575585

576586
private parseRuleDefinition(): RuleDefinition {
577587
this.consume("@", "start of rule");
578-
const n = this.parseRuleName();
588+
const pos = this.pos;
589+
const name = this.parseRuleName();
579590
this.consume("=", "after rule identifier");
580-
const r = this.parseRules();
581-
return {
582-
name: n,
583-
rules: r,
584-
};
591+
const rules = this.parseRules();
592+
return { name, rules, pos };
585593
}
586594

587595
private consume(expected: string, reason?: string) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
export function getLineCol(content: string, pos: number) {
5+
let line = 1;
6+
let col = 1;
7+
for (let i = 0; i < pos && i < content.length; i++) {
8+
if (content[i] === "\n") {
9+
line++;
10+
col = 1;
11+
} else {
12+
col++;
13+
}
14+
}
15+
return { line, col };
16+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { loadGrammarRules } from "../src/grammarLoader.js";
5+
6+
describe("Grammar Compiler", () => {
7+
describe("Error", () => {
8+
it("Undefined rule reference", () => {
9+
const grammarText = `
10+
@<Start> = <Pause>
11+
@<Pause> = <Undefined>
12+
`;
13+
const errors: string[] = [];
14+
loadGrammarRules("test", grammarText, errors);
15+
expect(errors.length).toBe(1);
16+
expect(errors[0]).toContain(
17+
"error: Missing rule definition for '<Undefined>'",
18+
);
19+
});
20+
21+
it("Undefined rule reference in variable", () => {
22+
const grammarText = `
23+
@<Start> = <Pause>
24+
@<Pause> = $(x:<Undefined>)
25+
`;
26+
const errors: string[] = [];
27+
loadGrammarRules("test", grammarText, errors);
28+
expect(errors.length).toBe(1);
29+
expect(errors[0]).toContain(
30+
"error: Missing rule definition for '<Undefined>'",
31+
);
32+
});
33+
});
34+
describe("Warning", () => {
35+
it("Unused", () => {
36+
const grammarText = `
37+
@<Start> = <Pause>
38+
@<Pause> = pause
39+
@<Unused> = unused
40+
`;
41+
const errors: string[] = [];
42+
const warnings: string[] = [];
43+
loadGrammarRules("test", grammarText, errors, warnings);
44+
expect(errors.length).toBe(0);
45+
expect(warnings.length).toBe(1);
46+
expect(warnings[0]).toContain(
47+
"warning: Rule '<Unused>' is defined but never used.",
48+
);
49+
});
50+
});
51+
});

0 commit comments

Comments
 (0)