Skip to content

Commit 8a53552

Browse files
AbstractTypes need to include InferredTypes (#1328)
* AbstractTypes need to include InferredTypes, since cross-references might use InferredTypes as their types * simplify some type declarations, since InferredType is an AbstractType now * use InferredType as type for implicitly and explicitly inferred types in scopes, improved comments * an Action is no AbstractType anymore - since Actions either reuse already declared types or contribute a new type with an InferredType - and InferredTypes are AbstractTypes now * fixed remaining problem: export the explicitly inferred type of an action, not the action itself * fixed the identification of the name-assignment for inferred types used in cross-references - this bug was there even before changing the way how Actions and InferredTypes are used as AbstractTypes!
1 parent 0239db7 commit 8a53552

File tree

9 files changed

+129
-48
lines changed

9 files changed

+129
-48
lines changed

packages/langium/src/grammar/generated/grammar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3907,13 +3907,13 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar
39073907
{
39083908
"$type": "SimpleType",
39093909
"typeRef": {
3910-
"$ref": "#/rules@27/definition/elements@0"
3910+
"$ref": "#/rules@17"
39113911
}
39123912
},
39133913
{
39143914
"$type": "SimpleType",
39153915
"typeRef": {
3916-
"$ref": "#/rules@17"
3916+
"$ref": "#/rules@18"
39173917
}
39183918
}
39193919
]

packages/langium/src/grammar/internal-grammar-util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function isStringTypeInternal(type: ast.AbstractType | ast.TypeDefinition, visit
6464
return false;
6565
}
6666

67-
export function getTypeNameWithoutError(type?: ast.AbstractType | ast.InferredType): string | undefined {
67+
export function getTypeNameWithoutError(type?: ast.AbstractType | ast.Action): string | undefined {
6868
if (!type) {
6969
return undefined;
7070
}

packages/langium/src/grammar/langium-grammar.langium

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ SimpleType infers TypeDefinition:
5555
PrimitiveType returns string:
5656
'string' | 'number' | 'boolean' | 'Date' | 'bigint';
5757

58-
type AbstractType = Interface | Type | Action | ParserRule;
58+
type AbstractType = Interface | Type | ParserRule | InferredType;
5959

6060
Type:
6161
'type' name=ID '=' type=TypeDefinition ';'?;

packages/langium/src/grammar/references/grammar-scope.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ import { DefaultScopeProvider } from '../../references/scope-provider.js';
1717
import { findRootNode, getContainerOfType, getDocument, streamAllContents } from '../../utils/ast-utils.js';
1818
import { toDocumentSegment } from '../../utils/cst-utils.js';
1919
import { stream } from '../../utils/stream.js';
20-
import { AbstractType, Interface, isAction, isGrammar, isParserRule, isReturnType, Type } from '../../languages/generated/ast.js';
20+
import { AbstractType, InferredType, Interface, isAction, isGrammar, isParserRule, isReturnType, Type } from '../../languages/generated/ast.js';
2121
import { resolveImportUri } from '../internal-grammar-util.js';
22-
import { getActionType } from '../../utils/grammar-utils.js';
2322

2423
export class LangiumGrammarScopeProvider extends DefaultScopeProvider {
2524

@@ -46,7 +45,7 @@ export class LangiumGrammarScopeProvider extends DefaultScopeProvider {
4645
if (precomputed && rootNode) {
4746
const allDescriptions = precomputed.get(rootNode);
4847
if (allDescriptions.length > 0) {
49-
localScope = stream(allDescriptions).filter(des => des.type === Interface || des.type === Type);
48+
localScope = stream(allDescriptions).filter(des => des.type === Interface || des.type === Type || des.type === InferredType);
5049
}
5150
}
5251

@@ -67,7 +66,7 @@ export class LangiumGrammarScopeProvider extends DefaultScopeProvider {
6766
this.gatherImports(grammar, importedUris);
6867
let importedElements = this.indexManager.allElements(referenceType, importedUris);
6968
if (referenceType === AbstractType) {
70-
importedElements = importedElements.filter(des => des.type === Interface || des.type === Type);
69+
importedElements = importedElements.filter(des => des.type === Interface || des.type === Type || des.type === InferredType);
7170
}
7271
return new MapScope(importedElements);
7372
}
@@ -99,60 +98,74 @@ export class LangiumGrammarScopeComputation extends DefaultScopeComputation {
9998
}
10099

101100
protected override exportNode(node: AstNode, exports: AstNodeDescription[], document: LangiumDocument): void {
101+
// this function is called in order to export nodes to the GLOBAL scope
102+
103+
/* Among others, TYPES need to be exported.
104+
* There are three ways to define types:
105+
* - explicit "type" declarations
106+
* - explicit "interface" declarations
107+
* - "inferred types", which can be distinguished into ...
108+
* - inferred types with explicitly declared names, i.e. parser rules with "infers", actions with "infer"
109+
* Note, that multiple explicitly inferred types might have the same name! Cross-references to such types are resolved to the first declaration.
110+
* - implicitly inferred types, i.e. parser rules without "infers" and without "returns",
111+
* which implicitly declare a type with the same name as the parser rule
112+
* Note, that implicitly inferred types are unique, since names of parser rules must be unique.
113+
*/
114+
115+
// export the top-level elements: parser rules, terminal rules, types, interfaces
102116
super.exportNode(node, exports, document);
117+
118+
// additionally, export inferred types:
103119
if (isParserRule(node)) {
104120
if (!node.returnType && !node.dataType) {
105-
// Export inferred rule type as interface
121+
// Export implicitly and explicitly inferred type from parser rule
106122
const typeNode = node.inferredType ?? node;
107-
exports.push(this.createInterfaceDescription(typeNode, typeNode.name, document));
123+
exports.push(this.createInferredTypeDescription(typeNode, typeNode.name, document));
108124
}
109125
streamAllContents(node).forEach(childNode => {
110126
if (isAction(childNode) && childNode.inferredType) {
111-
const typeName = getActionType(childNode);
112-
if (typeName) {
113-
// Export inferred action type as interface
114-
exports.push(this.createInterfaceDescription(childNode, typeName, document));
115-
}
127+
// Export explicitly inferred type from action
128+
exports.push(this.createInferredTypeDescription(childNode.inferredType, childNode.inferredType.name, document));
116129
}
117130
});
118131
}
119132
}
120133

121134
protected override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes): void {
122-
if (isReturnType(node)) return;
135+
// for the precompution of the local scope
136+
if (isReturnType(node)) {
137+
return;
138+
}
123139
this.processTypeNode(node, document, scopes);
124140
this.processActionNode(node, document, scopes);
125141
super.processNode(node, document, scopes);
126142
}
127143

128144
/**
129-
* Add synthetic Interface in case of explicitly or implicitly inferred type:<br>
145+
* Add synthetic type into the scope in case of explicitly or implicitly inferred type:<br>
130146
* cases: `ParserRule: ...;` or `ParserRule infers Type: ...;`
131147
*/
132148
protected processTypeNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes): void {
133149
const container = node.$container;
134150
if (container && isParserRule(node) && !node.returnType && !node.dataType) {
135151
const typeNode = node.inferredType ?? node;
136-
scopes.add(container, this.createInterfaceDescription(typeNode, typeNode.name, document));
152+
scopes.add(container, this.createInferredTypeDescription(typeNode, typeNode.name, document));
137153
}
138154
}
139155

140156
/**
141-
* Add synthetic Interface in case of explicitly inferred type:
157+
* Add synthetic type into the scope in case of explicitly inferred type:
142158
*
143159
* case: `{infer Action}`
144160
*/
145161
protected processActionNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes): void {
146162
const container = findRootNode(node);
147163
if (container && isAction(node) && node.inferredType) {
148-
const typeName = getActionType(node);
149-
if (typeName) {
150-
scopes.add(container, this.createInterfaceDescription(node, typeName, document));
151-
}
164+
scopes.add(container, this.createInferredTypeDescription(node.inferredType, node.inferredType.name, document));
152165
}
153166
}
154167

155-
protected createInterfaceDescription(node: AstNode, name: string, document: LangiumDocument = getDocument(node)): AstNodeDescription {
168+
protected createInferredTypeDescription(node: AstNode, name: string, document: LangiumDocument = getDocument(node)): AstNodeDescription {
156169
let nameNodeSegment: DocumentSegment | undefined;
157170
const nameSegmentGetter = () => nameNodeSegment ??= toDocumentSegment(this.nameProvider.getNameNode(node) ?? node.$cstNode);
158171
return {
@@ -162,7 +175,7 @@ export class LangiumGrammarScopeComputation extends DefaultScopeComputation {
162175
return nameSegmentGetter();
163176
},
164177
selectionSegment: toDocumentSegment(node.$cstNode),
165-
type: 'Interface',
178+
type: InferredType,
166179
documentUri: document.uri,
167180
path: this.astNodeLocator.getAstNodePath(node)
168181
};

packages/langium/src/grammar/validation/validator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ export class LangiumGrammarValidator {
372372
}
373373
}
374374

375-
private getActionType(rule: ast.Action): ast.AbstractType | ast.InferredType | undefined {
375+
private getActionType(rule: ast.Action): ast.AbstractType | undefined {
376376
if (rule.type) {
377377
return rule.type?.ref;
378378
} else if (rule.inferredType) {

packages/langium/src/languages/generated/ast.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function isAbstractRule(item: unknown): item is AbstractRule {
2525
return reflection.isInstance(item, AbstractRule);
2626
}
2727

28-
export type AbstractType = Action | Interface | ParserRule | Type;
28+
export type AbstractType = InferredType | Interface | ParserRule | Type;
2929

3030
export const AbstractType = 'AbstractType';
3131

@@ -643,9 +643,7 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection {
643643

644644
protected override computeIsSubtype(subtype: string, supertype: string): boolean {
645645
switch (subtype) {
646-
case Action: {
647-
return this.isSubtype(AbstractElement, supertype) || this.isSubtype(AbstractType, supertype);
648-
}
646+
case Action:
649647
case Alternatives:
650648
case Assignment:
651649
case CharacterRange:
@@ -684,6 +682,7 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection {
684682
case ParameterReference: {
685683
return this.isSubtype(Condition, supertype);
686684
}
685+
case InferredType:
687686
case Interface:
688687
case Type: {
689688
return this.isSubtype(AbstractType, supertype);

packages/langium/src/utils/grammar-utils.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
* terms of the MIT License, which is available in the project root.
55
******************************************************************************/
66

7-
import type { AstNode, CstNode } from '../syntax-tree.js';
7+
import { assertUnreachable } from '../utils/errors.js';
88
import * as ast from '../languages/generated/ast.js';
9+
import type { AstNode, CstNode } from '../syntax-tree.js';
910
import { isCompositeCstNode } from '../syntax-tree.js';
1011
import { getContainerOfType, streamAllContents } from './ast-utils.js';
1112
import { streamCst } from './cst-utils.js';
@@ -234,29 +235,41 @@ export function findAssignment(cstNode: CstNode): ast.Assignment | undefined {
234235
* from a parser rule, and that rule must contain an assignment to the `name` property. In all other cases,
235236
* this function returns `undefined`.
236237
*/
237-
export function findNameAssignment(type: ast.AbstractType | ast.InferredType): ast.Assignment | undefined {
238-
if (ast.isInferredType(type)) {
239-
// inferred type is unexpected, extract AbstractType first
240-
type = type.$container;
238+
export function findNameAssignment(type: ast.AbstractType): ast.Assignment | undefined {
239+
let startNode: AstNode = type;
240+
if (ast.isInferredType(startNode)) {
241+
// for inferred types, the location to start searching for the name-assignment is different
242+
if (ast.isAction(startNode.$container)) {
243+
// a type which is explicitly inferred by an action: investigate the sibbling of the Action node, i.e. start searching at the Action's parent
244+
startNode = startNode.$container.$container!;
245+
} else if (ast.isParserRule(startNode.$container)) {
246+
// investigate the parser rule with the explicitly inferred type
247+
startNode = startNode.$container;
248+
} else {
249+
assertUnreachable(startNode.$container);
250+
}
241251
}
242-
return findNameAssignmentInternal(type, new Map());
252+
return findNameAssignmentInternal(type, startNode, new Map());
243253
}
244254

245-
function findNameAssignmentInternal(type: ast.AbstractType, cache: Map<ast.AbstractType, ast.Assignment | undefined>): ast.Assignment | undefined {
255+
function findNameAssignmentInternal(type: ast.AbstractType, startNode: AstNode, cache: Map<ast.AbstractType, ast.Assignment | undefined>): ast.Assignment | undefined {
256+
// the cache is only required to prevent infinite loops
246257
function go(node: AstNode, refType: ast.AbstractType): ast.Assignment | undefined {
247258
let childAssignment: ast.Assignment | undefined = undefined;
248259
const parentAssignment = getContainerOfType(node, ast.isAssignment);
249260
// No parent assignment implies unassigned rule call
250261
if (!parentAssignment) {
251-
childAssignment = findNameAssignmentInternal(refType, cache);
262+
childAssignment = findNameAssignmentInternal(refType, refType, cache);
252263
}
253264
cache.set(type, childAssignment);
254265
return childAssignment;
255266
}
256267

257-
if (cache.has(type)) return cache.get(type);
268+
if (cache.has(type)) {
269+
return cache.get(type);
270+
}
258271
cache.set(type, undefined);
259-
for (const node of streamAllContents(type)) {
272+
for (const node of streamAllContents(startNode)) {
260273
if (ast.isAssignment(node) && node.feature.toLowerCase() === 'name') {
261274
cache.set(type, node);
262275
return node;
@@ -395,7 +408,7 @@ export function getExplicitRuleType(rule: ast.ParserRule): string | undefined {
395408
return undefined;
396409
}
397410

398-
export function getTypeName(type: ast.AbstractType | ast.InferredType): string {
411+
export function getTypeName(type: ast.AbstractType | ast.Action): string {
399412
if (ast.isParserRule(type)) {
400413
return isDataTypeRule(type) ? type.name : getExplicitRuleType(type) ?? type.name;
401414
} else if (ast.isInterface(type) || ast.isType(type) || ast.isReturnType(type)) {
@@ -412,7 +425,7 @@ export function getTypeName(type: ast.AbstractType | ast.InferredType): string {
412425
}
413426

414427
export function getActionType(action: ast.Action): string | undefined {
415-
if(action.inferredType) {
428+
if (action.inferredType) {
416429
return action.inferredType.name;
417430
} else if (action.type?.ref) {
418431
return getTypeName(action.type.ref);

packages/langium/test/grammar/grammar-validator.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,42 @@ describe('Check Rule Fragment Validation', () => {
252252
});
253253
});
254254

255+
describe('Check cross-references to inferred types', () => {
256+
test('infer after the parser rules names', async () => {
257+
const validationResult = await validate(`
258+
grammar HelloWorld
259+
260+
entry Model: a+=A*;
261+
262+
A infers B: 'a' name=ID (otherA=[B])?; // works
263+
264+
hidden terminal WS: /\\s+/;
265+
terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/;
266+
`.trim());
267+
expectNoIssues(validationResult);
268+
// expect(validationResult.diagnostics).toHaveLength(1);
269+
// expect(validationResult.diagnostics[0].message).toBe('Cannot infer terminal or data type rule for cross-reference.');
270+
// expectError(validationResult, 'Cannot use rule fragments in types.');
271+
});
272+
273+
test('infer in the parser rules body', async () => {
274+
const validationResult = await validate(`
275+
grammar HelloWorld
276+
277+
entry Model: a+=A*;
278+
279+
A: {infer B} 'a' name=ID (otherA=[B])?;
280+
281+
hidden terminal WS: /\\s+/;
282+
terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/;
283+
`.trim());
284+
expectNoIssues(validationResult);
285+
// expect(validationResult.diagnostics).toHaveLength(1);
286+
// expect(validationResult.diagnostics[0].message).toBe('Cannot infer terminal or data type rule for cross-reference.');
287+
// expectError(validationResult, 'Cannot use rule fragments in types.');
288+
});
289+
});
290+
255291
describe('Checked Named CrossRefs', () => {
256292
const input = `
257293
grammar g

packages/langium/test/grammar/type-system/type-validator.test.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { DiagnosticSeverity } from 'vscode-languageserver';
1010
import { AstUtils, EmptyFileSystem, GrammarAST } from 'langium';
1111
import { createLangiumGrammarServices } from 'langium/grammar';
1212
import { expectError, expectNoIssues, parseDocument, validationHelper } from 'langium/test';
13+
import { isCrossReference, isInferredType, isParserRule } from '../../../src/languages/generated/ast.js';
14+
import type { Assignment, CrossReference, Group, ParserRule } from '../../../src/languages/generated/ast.js';
1315

1416
const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar;
1517
const validate = validationHelper<GrammarAST.Grammar>(grammarServices);
@@ -86,19 +88,37 @@ describe('validate params in types', () => {
8688
});
8789

8890
describe('validate inferred types', () => {
91+
const prog = `
92+
A infers B: 'a' name=ID (otherA=[B])?;
93+
hidden terminal WS: /\\s+/;
94+
terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/;
95+
`.trim();
8996

9097
test('inferred type in cross-reference should not produce an error', async () => {
91-
const prog = `
92-
A infers B: 'a' name=ID (otherA=[B])?;
93-
hidden terminal WS: /\\s+/;
94-
terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/;
95-
`.trim();
96-
9798
const validation = await validate(prog);
9899
expectNoIssues(validation, {
99100
severity: DiagnosticSeverity.Error
100101
});
101102
});
103+
104+
test('AbstractType for cross-reference includes InferredType', async () => {
105+
const validation = await validate(prog);
106+
// parser rule with explicitly inferred type
107+
const rule: ParserRule = validation.document.parseResult.value.rules.filter(r => isParserRule(r))[0] as ParserRule;
108+
expect(rule.inferredType).toBeTruthy();
109+
expect(rule.inferredType!.name).toBe('B');
110+
// determine the cross-reference
111+
const assignment: Assignment = (rule.definition as Group).elements[2] as Assignment;
112+
expect(isCrossReference(assignment.terminal)).toBeTruthy();
113+
const crossRef: CrossReference = assignment.terminal as CrossReference;
114+
const refType = crossRef.type.ref!;
115+
// the type of the cross-reference is the inferred type of the parser rule
116+
expect(isInferredType(refType)).toBeTruthy();
117+
expect(refType.$type).toBe('InferredType');
118+
if (isInferredType(refType)) {
119+
expect(isParserRule(refType.$container)).toBeTruthy();
120+
}
121+
});
102122
});
103123

104124
describe('Work with imported declared types', () => {

0 commit comments

Comments
 (0)