Skip to content

Commit 8aec8c2

Browse files
committed
Grammar compilation error
1 parent 90ef278 commit 8aec8c2

File tree

8 files changed

+343
-142
lines changed

8 files changed

+343
-142
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: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,70 @@
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+
24+
export function loadGrammarRules(fileName: string, content: string): Grammar;
25+
export function loadGrammarRules(
26+
fileName: string,
27+
content: string,
28+
errors: string[],
29+
warnings?: string[],
30+
): Grammar | undefined;
31+
export function loadGrammarRules(
32+
fileName: string,
33+
content: string,
34+
errors?: string[],
35+
warnings?: string[],
36+
): Grammar | undefined {
937
const definitions = parseGrammarRules(fileName, content);
10-
const grammar = compileGrammar(definitions);
11-
return grammar;
38+
const result = compileGrammar(definitions, start);
39+
40+
if (result.warnings.length > 0 && warnings !== undefined) {
41+
warnings.push(
42+
...convertCompileError(
43+
fileName,
44+
content,
45+
"warning",
46+
result.warnings,
47+
),
48+
);
49+
}
50+
51+
if (result.errors.length > 0) {
52+
const errorMessages = convertCompileError(
53+
fileName,
54+
content,
55+
"error",
56+
result.errors,
57+
);
58+
if (errors) {
59+
errors.push(...errorMessages);
60+
} else {
61+
const errorStr = result.errors.length === 1 ? "error" : "errors";
62+
errorMessages.unshift(
63+
`Error detected in grammar compilation '${fileName}': ${result.errors.length} ${errorStr}.`,
64+
);
65+
throw new Error(errorMessages.join("\n"));
66+
}
67+
}
68+
69+
return result.grammar;
1270
}

ts/packages/actionGrammar/src/grammarRuleParser.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import registerDebug from "debug";
5+
import { getLineCol } from "./utils.js";
56

67
const debugParse = registerDebug("typeagent:grammar:parse");
78
/**
@@ -50,8 +51,9 @@ const debugParse = registerDebug("typeagent:grammar:parse");
5051
export function parseGrammarRules(
5152
fileName: string,
5253
content: string,
54+
position: boolean = true,
5355
): RuleDefinition[] {
54-
const parser = new GrammarRuleParser(fileName, content);
56+
const parser = new GrammarRuleParser(fileName, content, position);
5557
const definitions = parser.parse();
5658
debugParse(JSON.stringify(definitions, undefined, 2));
5759
return definitions;
@@ -67,6 +69,7 @@ type StrExpr = {
6769
type RuleRefExpr = {
6870
type: "ruleReference";
6971
name: string;
72+
pos?: number | undefined;
7073
};
7174

7275
type RulesExpr = {
@@ -80,6 +83,7 @@ type VarDefExpr = {
8083
name: string;
8184
typeName: string;
8285
ruleReference: boolean;
86+
ruleRefPos?: number | undefined;
8387
optional?: boolean;
8488
};
8589

@@ -113,6 +117,7 @@ export type Rule = {
113117
export type RuleDefinition = {
114118
name: string;
115119
rules: Rule[];
120+
pos?: number | undefined;
116121
};
117122

118123
export function isWhitespace(char: string) {
@@ -152,8 +157,13 @@ class GrammarRuleParser {
152157
constructor(
153158
private readonly fileName: string,
154159
private readonly content: string,
160+
private readonly position: boolean = true,
155161
) {}
156162

163+
private get pos(): number | undefined {
164+
return this.position ? this.curr : undefined;
165+
}
166+
157167
private isAtWhiteSpace() {
158168
return !this.isAtEnd() && isWhitespace(this.content[this.curr]);
159169
}
@@ -319,14 +329,16 @@ class GrammarRuleParser {
319329
const id = this.parseId("Variable name");
320330
let typeName: string = "string";
321331
let ruleReference: boolean = false;
332+
let ruleRefPos: number | undefined = undefined;
322333

323334
if (this.isAt(":")) {
324335
// Consume colon
325336
this.skipWhitespace(1);
326337

327338
if (this.isAt("<")) {
328-
typeName = this.parseRuleName();
339+
ruleRefPos = this.pos;
329340
ruleReference = true;
341+
typeName = this.parseRuleName();
330342
} else {
331343
typeName = this.parseId("Type name");
332344
}
@@ -336,18 +348,17 @@ class GrammarRuleParser {
336348
name: id,
337349
typeName,
338350
ruleReference,
351+
ruleRefPos,
339352
};
340353
}
341354

342355
private parseExpression(): Expr[] {
343356
const expNodes: Expr[] = [];
344357
do {
345358
if (this.isAt("<")) {
346-
const n = this.parseRuleName();
347-
expNodes.push({
348-
type: "ruleReference",
349-
name: n,
350-
});
359+
const pos = this.pos;
360+
const name = this.parseRuleName();
361+
expNodes.push({ type: "ruleReference", name, pos });
351362
continue;
352363
}
353364
if (this.isAt("$(")) {
@@ -575,13 +586,11 @@ class GrammarRuleParser {
575586

576587
private parseRuleDefinition(): RuleDefinition {
577588
this.consume("@", "start of rule");
578-
const n = this.parseRuleName();
589+
const pos = this.pos;
590+
const name = this.parseRuleName();
579591
this.consume("=", "after rule identifier");
580-
const r = this.parseRules();
581-
return {
582-
name: n,
583-
rules: r,
584-
};
592+
const rules = this.parseRules();
593+
return { name, rules, pos };
585594
}
586595

587596
private consume(expected: string, reason?: string) {
@@ -594,18 +603,7 @@ class GrammarRuleParser {
594603
}
595604

596605
private getLineCol(pos: number) {
597-
let line = 1;
598-
let col = 1;
599-
const content = this.content;
600-
for (let i = 0; i < pos && i < content.length; i++) {
601-
if (content[i] === "\n") {
602-
line++;
603-
col = 1;
604-
} else {
605-
col++;
606-
}
607-
}
608-
return { line, col };
606+
return getLineCol(this.content, pos);
609607
}
610608

611609
private throwError(message: string, pos: number = this.curr): never {
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+
}

0 commit comments

Comments
 (0)