Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented option for parsing text with an alternative entry rule #1407

Merged
merged 2 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 20 additions & 4 deletions packages/langium/src/parser/langium-parser.ts
Expand Up @@ -53,6 +53,7 @@ interface AssignmentElement {

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

protected allRules = new Map<string, RuleResult>();
protected mainRule!: RuleResult;

constructor(services: LangiumCoreServices) {
this.lexer = services.parser.Lexer;
const tokens = this.lexer.definition;
Expand Down Expand Up @@ -106,6 +110,10 @@ export abstract class AbstractLangiumParser implements BaseParser {
abstract action($type: string, action: Action): void;
abstract construct(): unknown;

getRule(name: string): RuleResult | undefined {
return this.allRules.get(name);
}

isRecording(): boolean {
return this.wrapper.IS_RECORDING;
}
Expand All @@ -123,13 +131,16 @@ export abstract class AbstractLangiumParser implements BaseParser {
}
}

export interface ParserOptions {
rule?: string
}

export class LangiumParser extends AbstractLangiumParser {
private readonly linker: Linker;
private readonly converter: ValueConverter;
private readonly astReflection: AstReflection;
private readonly nodeBuilder = new CstNodeBuilder();
private stack: any[] = [];
private mainRule!: RuleResult;
private assignmentMap = new Map<AbstractElement, AssignmentElement | undefined>();

private get current(): any {
Expand All @@ -146,17 +157,22 @@ export class LangiumParser extends AbstractLangiumParser {
rule(rule: ParserRule, impl: RuleImpl): RuleResult {
const type = rule.fragment ? undefined : isDataTypeRule(rule) ? DatatypeSymbol : getTypeName(rule);
const ruleMethod = this.wrapper.DEFINE_RULE(withRuleSuffix(rule.name), this.startImplementation(type, impl).bind(this));
this.allRules.set(rule.name, ruleMethod);
if (rule.entry) {
this.mainRule = ruleMethod;
}
return ruleMethod;
}

parse<T extends AstNode = AstNode>(input: string): ParseResult<T> {
parse<T extends AstNode = AstNode>(input: string, options: ParserOptions = {}): ParseResult<T> {
this.nodeBuilder.buildRootNode(input);
const lexerResult = this.lexer.tokenize(input);
this.wrapper.input = lexerResult.tokens;
const result = this.mainRule.call(this.wrapper, {});
const ruleMethod = options.rule ? this.allRules.get(options.rule) : this.mainRule;
if (!ruleMethod) {
throw new Error(options.rule ? `No rule found with name '${options.rule}'` : 'No main rule available.');
}
const result = ruleMethod.call(this.wrapper, {});
this.nodeBuilder.addHiddenTokens(lexerResult.hidden);
this.unorderedGroups.clear();
return {
Expand Down Expand Up @@ -424,7 +440,6 @@ export interface CompletionParserResult {
}

export class LangiumCompletionParser extends AbstractLangiumParser {
private mainRule!: RuleResult;
private tokens: IToken[] = [];

private elementStack: AbstractElement[] = [];
Expand Down Expand Up @@ -457,6 +472,7 @@ export class LangiumCompletionParser extends AbstractLangiumParser {

rule(rule: ParserRule, impl: RuleImpl): RuleResult {
const ruleMethod = this.wrapper.DEFINE_RULE(withRuleSuffix(rule.name), this.startImplementation(impl).bind(this));
this.allRules.set(rule.name, ruleMethod);
if (rule.entry) {
this.mainRule = ruleMethod;
}
Expand Down
10 changes: 2 additions & 8 deletions packages/langium/src/parser/parser-builder-base.ts
Expand Up @@ -26,7 +26,6 @@ type RuleContext = {
type ParserContext = {
parser: BaseParser
tokens: TokenTypeDictionary
rules: Map<string, Rule>
ruleNames: Map<AstNode, string>
}

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

export function createParser<T extends BaseParser>(grammar: Grammar, parser: T, tokens: TokenTypeDictionary): T {
const rules = new Map<string, Rule>();
const parserContext: ParserContext = {
parser,
tokens,
rules,
ruleNames: new Map()
};
buildRules(parserContext, grammar);
Expand All @@ -62,10 +59,7 @@ function buildRules(parserContext: ParserContext, grammar: Grammar): void {
many: 1,
or: 1
};
ctx.rules.set(
rule.name,
parserContext.parser.rule(rule, buildElement(ctx, rule.definition))
);
parserContext.parser.rule(rule, buildElement(ctx, rule.definition));
}
}

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

function getRule(ctx: ParserContext, element: ParserRule | AbstractElement): Rule {
const name = getRuleName(ctx, element);
const rule = ctx.rules.get(name);
const rule = ctx.parser.getRule(name);
if (!rule) throw new Error(`Rule "${name}" not found."`);
return rule;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/langium/src/test/langium-test.ts
Expand Up @@ -9,23 +9,28 @@ import type { LangiumCoreServices, LangiumSharedCoreServices } from '../services
import type { AstNode, CstNode, Properties } from '../syntax-tree.js';
import { type LangiumDocument, TextDocument } from '../workspace/documents.js';
import type { BuildOptions } from '../workspace/document-builder.js';
import type { AsyncDisposable } from '../utils/disposable.js';
import type { LangiumServices, LangiumSharedLSPServices } from '../lsp/lsp-services.js';
import type { ParserOptions } from '../parser/langium-parser.js';
import { DiagnosticSeverity, MarkupContent } from 'vscode-languageserver-types';
import { escapeRegExp } from '../utils/regexp-utils.js';
import { URI } from '../utils/uri-utils.js';
import { findNodeForProperty } from '../utils/grammar-utils.js';
import { SemanticTokensDecoder } from '../lsp/semantic-token-provider.js';
import * as assert from 'node:assert';
import { stream } from '../utils/stream.js';
import type { AsyncDisposable } from '../utils/disposable.js';
import { Disposable } from '../utils/disposable.js';
import { normalizeEOL } from '../generate/template-string.js';
import type { LangiumServices, LangiumSharedLSPServices } from '../lsp/lsp-services.js';

export interface ParseHelperOptions extends BuildOptions {
/**
* Specifies the URI of the generated document. Will use a counter variable if not specified.
*/
documentUri?: string;
/**
* Options passed to the LangiumParser.
*/
parserOptions?: ParserOptions
}

let nextDocumentId = 1;
Expand All @@ -35,7 +40,7 @@ export function parseHelper<T extends AstNode = AstNode>(services: LangiumCoreSe
const documentBuilder = services.shared.workspace.DocumentBuilder;
return async (input, options) => {
const uri = URI.parse(options?.documentUri ?? `file:///${nextDocumentId++}${metaData.fileExtensions[0] ?? ''}`);
const document = services.shared.workspace.LangiumDocumentFactory.fromString<T>(input, uri);
const document = services.shared.workspace.LangiumDocumentFactory.fromString<T>(input, uri, options?.parserOptions);
services.shared.workspace.LangiumDocuments.addDocument(document);
await documentBuilder.build([document], options);
return document;
Expand Down
36 changes: 18 additions & 18 deletions packages/langium/src/workspace/documents.ts
Expand Up @@ -15,7 +15,7 @@ export { TextDocument } from 'vscode-languageserver-textdocument';

import type { Diagnostic, Range } from 'vscode-languageserver-types';
import type { FileSystemProvider } from './file-system-provider.js';
import type { ParseResult } from '../parser/langium-parser.js';
import type { ParseResult, ParserOptions } from '../parser/langium-parser.js';
import type { ServiceRegistry } from '../service-registry.js';
import type { LangiumSharedCoreServices } from '../services.js';
import type { AstNode, AstNodeDescription, Mutable, Reference } from '../syntax-tree.js';
Expand Down Expand Up @@ -126,7 +126,7 @@ export interface LangiumDocumentFactory {
/**
* Create a Langium document from a `TextDocument` (usually associated with a file).
*/
fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI): LangiumDocument<T>;
fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI, options?: ParserOptions): LangiumDocument<T>;
/**
* Create a Langium document from a `TextDocument` asynchronously. This action can be cancelled if a cancellable parser implementation has been provided.
*/
Expand All @@ -135,7 +135,7 @@ export interface LangiumDocumentFactory {
/**
* Create an Langium document from an in-memory string.
*/
fromString<T extends AstNode = AstNode>(text: string, uri: URI): LangiumDocument<T>;
fromString<T extends AstNode = AstNode>(text: string, uri: URI, options?: ParserOptions): LangiumDocument<T>;
/**
* Create a Langium document from an in-memory string asynchronously. This action can be cancelled if a cancellable parser implementation has been provided.
*/
Expand Down Expand Up @@ -178,42 +178,42 @@ export class DefaultLangiumDocumentFactory implements LangiumDocumentFactory {
return this.createAsync<T>(uri, content, cancellationToken);
}

fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI): LangiumDocument<T>;
fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI, options?: ParserOptions): LangiumDocument<T>;
fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri: URI | undefined, cancellationToken: CancellationToken): Promise<LangiumDocument<T>>;
fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI, cancellationToken?: CancellationToken): LangiumDocument<T> | Promise<LangiumDocument<T>> {
fromTextDocument<T extends AstNode = AstNode>(textDocument: TextDocument, uri?: URI, token?: CancellationToken | ParserOptions): LangiumDocument<T> | Promise<LangiumDocument<T>> {
uri = uri ?? URI.parse(textDocument.uri);
if (cancellationToken) {
return this.createAsync<T>(uri, textDocument, cancellationToken);
if (CancellationToken.is(token)) {
return this.createAsync<T>(uri, textDocument, token);
} else {
return this.create<T>(uri, textDocument);
return this.create<T>(uri, textDocument, token);
}
}

fromString<T extends AstNode = AstNode>(text: string, uri: URI): LangiumDocument<T>;
fromString<T extends AstNode = AstNode>(text: string, uri: URI, options?: ParserOptions): LangiumDocument<T>;
fromString<T extends AstNode = AstNode>(text: string, uri: URI, cancellationToken: CancellationToken): Promise<LangiumDocument<T>>;
fromString<T extends AstNode = AstNode>(text: string, uri: URI, cancellationToken?: CancellationToken): LangiumDocument<T> | Promise<LangiumDocument<T>> {
if (cancellationToken) {
return this.createAsync<T>(uri, text, cancellationToken);
fromString<T extends AstNode = AstNode>(text: string, uri: URI, token?: CancellationToken | ParserOptions): LangiumDocument<T> | Promise<LangiumDocument<T>> {
if (CancellationToken.is(token)) {
return this.createAsync<T>(uri, text, token);
} else {
return this.create<T>(uri, text);
return this.create<T>(uri, text, token);
}
}

fromModel<T extends AstNode = AstNode>(model: T, uri: URI): LangiumDocument<T> {
return this.create<T>(uri, { $model: model });
}

protected create<T extends AstNode = AstNode>(uri: URI, content: string | TextDocument | { $model: T }): LangiumDocument<T> {
protected create<T extends AstNode = AstNode>(uri: URI, content: string | TextDocument | { $model: T }, options?: ParserOptions): LangiumDocument<T> {
if (typeof content === 'string') {
const parseResult = this.parse<T>(uri, content);
const parseResult = this.parse<T>(uri, content, options);
return this.createLangiumDocument<T>(parseResult, uri, undefined, content);

} else if ('$model' in content) {
const parseResult = { value: content.$model, parserErrors: [], lexerErrors: [] };
return this.createLangiumDocument<T>(parseResult, uri);

} else {
const parseResult = this.parse<T>(uri, content.getText());
const parseResult = this.parse<T>(uri, content.getText(), options);
return this.createLangiumDocument(parseResult, uri, content);
}
}
Expand Down Expand Up @@ -300,9 +300,9 @@ export class DefaultLangiumDocumentFactory implements LangiumDocumentFactory {
return document;
}

protected parse<T extends AstNode>(uri: URI, text: string): ParseResult<T> {
protected parse<T extends AstNode>(uri: URI, text: string, options?: ParserOptions): ParseResult<T> {
const services = this.serviceRegistry.getServices(uri);
return services.parser.LangiumParser.parse<T>(text);
return services.parser.LangiumParser.parse<T>(text, options);
}

protected parseAsync<T extends AstNode>(uri: URI, text: string, cancellationToken: CancellationToken): Promise<ParseResult<T>> {
Expand Down
71 changes: 71 additions & 0 deletions packages/langium/test/parser/langium-parser.test.ts
@@ -0,0 +1,71 @@
/******************************************************************************
* Copyright 2024 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import type { AstNode, LangiumCoreServices } from 'langium';
import { describe, expect, test, beforeEach } from 'vitest';
import { createServicesForGrammar } from 'langium/grammar';
import { parseHelper } from 'langium/test';

describe('Partial parsing', () => {
const content = `
grammar Test
entry Model: 'model' (a+=A | b+=B)*;
A: 'a' name=ID;
B: 'b' name=ID;
terminal ID: /[_a-zA-Z][\\w_]*/;
hidden terminal WS: /\\s+/;
`;

let services: LangiumCoreServices;

beforeEach(async () => {
services = await createServicesForGrammar({ grammar: content });
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function expectCorrectParse(text: string, rule?: string): any {
const result = services.parser.LangiumParser.parse(text, { rule });
expect(result.parserErrors.length).toBe(0);
return result.value;
}

function expectErrorneousParse(text: string, rule?: string): void {
const result = services.parser.LangiumParser.parse(text, { rule });
expect(result.parserErrors.length).toBeGreaterThan(0);
}

test('Should parse correctly with normal entry rule', () => {
const result = expectCorrectParse('model a Foo b Bar');
expect(result.a[0].name).toEqual('Foo');
expect(result.b[0].name).toEqual('Bar');
});

test('Should parse correctly with alternative entry rule A', () => {
const result = expectCorrectParse('a Foo', 'A');
expect(result.name).toEqual('Foo');
expectErrorneousParse('model a Foo', 'A');
expectErrorneousParse('b Bar', 'A');
});

test('Should parse correctly with alternative entry rule B', () => {
const result = expectCorrectParse('b Foo', 'B');
expect(result.name).toEqual('Foo');
expectErrorneousParse('model b Foo', 'B');
expectErrorneousParse('a Foo', 'B');
});

test('Parse helper supports using alternative entry rule A', async () => {
const parse = parseHelper<A>(services);
const document = await parse('a Foo', { parserOptions: { rule: 'A' } });
expect(document.parseResult.parserErrors.length).toBe(0);
expect(document.parseResult.value.name).toEqual('Foo');
});

});

interface A extends AstNode {
name: string
}