Skip to content

Commit 10f216c

Browse files
committed
Implemented option for parsing text with an alternative entry rule
1 parent 3a5a539 commit 10f216c

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

packages/langium/src/parser/langium-parser.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface AssignmentElement {
5353

5454
export interface BaseParser {
5555
rule(rule: ParserRule, impl: RuleImpl): RuleResult;
56+
getRule(name: string): RuleResult | undefined;
5657
alternatives(idx: number, choices: Array<IOrAlt<any>>): void;
5758
optional(idx: number, callback: DSLMethodOpts<unknown>): void;
5859
many(idx: number, callback: DSLMethodOpts<unknown>): void;
@@ -75,6 +76,9 @@ export abstract class AbstractLangiumParser implements BaseParser {
7576
protected readonly wrapper: ChevrotainWrapper;
7677
protected _unorderedGroups: Map<string, boolean[]> = new Map<string, boolean[]>();
7778

79+
protected allRules = new Map<string, RuleResult>();
80+
protected mainRule!: RuleResult;
81+
7882
constructor(services: LangiumCoreServices) {
7983
this.lexer = services.parser.Lexer;
8084
const tokens = this.lexer.definition;
@@ -106,6 +110,10 @@ export abstract class AbstractLangiumParser implements BaseParser {
106110
abstract action($type: string, action: Action): void;
107111
abstract construct(): unknown;
108112

113+
getRule(name: string): RuleResult | undefined {
114+
return this.allRules.get(name);
115+
}
116+
109117
isRecording(): boolean {
110118
return this.wrapper.IS_RECORDING;
111119
}
@@ -129,7 +137,6 @@ export class LangiumParser extends AbstractLangiumParser {
129137
private readonly astReflection: AstReflection;
130138
private readonly nodeBuilder = new CstNodeBuilder();
131139
private stack: any[] = [];
132-
private mainRule!: RuleResult;
133140
private assignmentMap = new Map<AbstractElement, AssignmentElement | undefined>();
134141

135142
private get current(): any {
@@ -146,17 +153,22 @@ export class LangiumParser extends AbstractLangiumParser {
146153
rule(rule: ParserRule, impl: RuleImpl): RuleResult {
147154
const type = rule.fragment ? undefined : isDataTypeRule(rule) ? DatatypeSymbol : getTypeName(rule);
148155
const ruleMethod = this.wrapper.DEFINE_RULE(withRuleSuffix(rule.name), this.startImplementation(type, impl).bind(this));
156+
this.allRules.set(rule.name, ruleMethod);
149157
if (rule.entry) {
150158
this.mainRule = ruleMethod;
151159
}
152160
return ruleMethod;
153161
}
154162

155-
parse<T extends AstNode = AstNode>(input: string): ParseResult<T> {
163+
parse<T extends AstNode = AstNode>(input: string, options: { rule?: string } = {}): ParseResult<T> {
156164
this.nodeBuilder.buildRootNode(input);
157165
const lexerResult = this.lexer.tokenize(input);
158166
this.wrapper.input = lexerResult.tokens;
159-
const result = this.mainRule.call(this.wrapper, {});
167+
const ruleMethod = options.rule ? this.allRules.get(options.rule) : this.mainRule;
168+
if (!ruleMethod) {
169+
throw new Error(options.rule ? `No rule found with name '${options.rule}'` : 'No main rule available.');
170+
}
171+
const result = ruleMethod.call(this.wrapper, {});
160172
this.nodeBuilder.addHiddenTokens(lexerResult.hidden);
161173
this.unorderedGroups.clear();
162174
return {
@@ -424,7 +436,6 @@ export interface CompletionParserResult {
424436
}
425437

426438
export class LangiumCompletionParser extends AbstractLangiumParser {
427-
private mainRule!: RuleResult;
428439
private tokens: IToken[] = [];
429440

430441
private elementStack: AbstractElement[] = [];
@@ -457,6 +468,7 @@ export class LangiumCompletionParser extends AbstractLangiumParser {
457468

458469
rule(rule: ParserRule, impl: RuleImpl): RuleResult {
459470
const ruleMethod = this.wrapper.DEFINE_RULE(withRuleSuffix(rule.name), this.startImplementation(impl).bind(this));
471+
this.allRules.set(rule.name, ruleMethod);
460472
if (rule.entry) {
461473
this.mainRule = ruleMethod;
462474
}

packages/langium/src/parser/parser-builder-base.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ type RuleContext = {
2626
type ParserContext = {
2727
parser: BaseParser
2828
tokens: TokenTypeDictionary
29-
rules: Map<string, Rule>
3029
ruleNames: Map<AstNode, string>
3130
}
3231

@@ -39,11 +38,9 @@ type Predicate = (args: Args) => boolean;
3938
type Method = (args: Args) => void;
4039

4140
export function createParser<T extends BaseParser>(grammar: Grammar, parser: T, tokens: TokenTypeDictionary): T {
42-
const rules = new Map<string, Rule>();
4341
const parserContext: ParserContext = {
4442
parser,
4543
tokens,
46-
rules,
4744
ruleNames: new Map()
4845
};
4946
buildRules(parserContext, grammar);
@@ -62,10 +59,7 @@ function buildRules(parserContext: ParserContext, grammar: Grammar): void {
6259
many: 1,
6360
or: 1
6461
};
65-
ctx.rules.set(
66-
rule.name,
67-
parserContext.parser.rule(rule, buildElement(ctx, rule.definition))
68-
);
62+
parserContext.parser.rule(rule, buildElement(ctx, rule.definition));
6963
}
7064
}
7165

@@ -369,7 +363,7 @@ function wrap(ctx: RuleContext, guard: Condition | undefined, method: Method, ca
369363

370364
function getRule(ctx: ParserContext, element: ParserRule | AbstractElement): Rule {
371365
const name = getRuleName(ctx, element);
372-
const rule = ctx.rules.get(name);
366+
const rule = ctx.parser.getRule(name);
373367
if (!rule) throw new Error(`Rule "${name}" not found."`);
374368
return rule;
375369
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/******************************************************************************
2+
* Copyright 2024 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import type { LangiumParser } from 'langium';
8+
import { describe, expect, test, beforeEach } from 'vitest';
9+
import { createServicesForGrammar } from 'langium/grammar';
10+
11+
describe('Partial parsing', () => {
12+
const content = `
13+
grammar Test
14+
entry Model: (a+=A | b+=B)*;
15+
A: 'a' name=ID;
16+
B: 'b' name=ID;
17+
terminal ID: /[_a-zA-Z][\\w_]*/;
18+
hidden terminal WS: /\\s+/;
19+
`;
20+
21+
let parser: LangiumParser;
22+
23+
beforeEach(async () => {
24+
parser = await parserFromGrammar(content);
25+
});
26+
27+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28+
function expectCorrectParse(text: string, rule?: string): any {
29+
const result = parser.parse(text, { rule });
30+
expect(result.parserErrors.length).toBe(0);
31+
return result.value;
32+
}
33+
34+
function expectErrorneousParse(text: string, rule?: string): void {
35+
const result = parser.parse(text, { rule });
36+
expect(result.parserErrors.length).toBeGreaterThan(0);
37+
}
38+
39+
test('Should parse correctly with normal entry rule', () => {
40+
const result = expectCorrectParse('a Foo b Bar');
41+
expect(result.a[0].name).toEqual('Foo');
42+
expect(result.b[0].name).toEqual('Bar');
43+
});
44+
45+
test('Should parse correctly with alternative entry rule A', () => {
46+
const result = expectCorrectParse('a Foo', 'A');
47+
expect(result.name).toEqual('Foo');
48+
expectErrorneousParse('b Bar', 'A');
49+
});
50+
51+
test('Should parse correctly with alternative entry rule B', () => {
52+
const result = expectCorrectParse('b Foo', 'B');
53+
expect(result.name).toEqual('Foo');
54+
expectErrorneousParse('a Foo', 'B');
55+
});
56+
});
57+
58+
async function parserFromGrammar(grammar: string): Promise<LangiumParser> {
59+
return (await createServicesForGrammar({ grammar })).parser.LangiumParser;
60+
}

0 commit comments

Comments
 (0)