Skip to content

Commit a00974d

Browse files
authored
Fix type computation for many optional properties (#1473)
1 parent 5762a08 commit a00974d

File tree

3 files changed

+122
-76
lines changed

3 files changed

+122
-76
lines changed

packages/langium/src/grammar/type-system/type-collector/inferred-types.ts

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface TypePart {
1919
ruleCalls: string[]
2020
parents: TypePart[]
2121
children: TypePart[]
22+
end?: TypePart
2223
actionWithAssignment: boolean
2324
}
2425

@@ -40,6 +41,7 @@ interface TypeCollectionContext {
4041

4142
interface TypePath {
4243
alt: TypeAlternative
44+
current: TypePart
4345
next: TypePart[]
4446
}
4547

@@ -53,20 +55,57 @@ class TypeGraph {
5355
}
5456

5557
getTypes(): TypePath[] {
56-
const rootType: TypeAlternative = {
57-
name: this.root.name!,
58-
properties: this.root.properties,
59-
ruleCalls: this.root.ruleCalls,
60-
super: []
61-
};
62-
if (this.root.children.length === 0) {
63-
return [{ alt: rootType, next: [] }];
64-
} else {
65-
return this.applyNext(this.root, {
66-
alt: rootType,
67-
next: this.root.children
68-
});
58+
return this.iterate(this.root, [{
59+
alt: {
60+
name: this.root.name!,
61+
properties: this.root.properties,
62+
ruleCalls: this.root.ruleCalls,
63+
super: []
64+
},
65+
current: this.root,
66+
next: this.root.children
67+
}]);
68+
}
69+
70+
private iterate(root: TypePart, paths: TypePath[]): TypePath[] {
71+
const finished = paths.filter(e => e.next.length === 0);
72+
do {
73+
const next = this.recurse(root, paths);
74+
const unfinished: TypePath[] = [];
75+
for (const path of next) {
76+
if (path.next.length > 0) {
77+
unfinished.push(path);
78+
} else {
79+
finished.push(path);
80+
}
81+
}
82+
paths = unfinished;
83+
} while (paths.length > 0);
84+
85+
return finished;
86+
}
87+
88+
private recurse(root: TypePart, paths: TypePath[], end?: TypePart): TypePath[] {
89+
const all: TypePath[] = [];
90+
for (const path of paths) {
91+
const node = path.current;
92+
if (node !== end && node.children.length > 0) {
93+
const nextPaths = this.applyNext(root, path);
94+
const subPaths = this.recurse(root, nextPaths, node.end ?? end);
95+
all.push(...subPaths);
96+
} else {
97+
all.push(path);
98+
}
99+
}
100+
const map = new MultiMap<TypePart, TypePath>();
101+
for (const path of all) {
102+
map.add(path.current, path);
69103
}
104+
const unique: TypePath[] = [];
105+
for (const [node, groupedPaths] of map.entriesGroupedByKey()) {
106+
unique.push(...flattenTypes(groupedPaths, node));
107+
}
108+
return unique;
70109
}
71110

72111
private applyNext(root: TypePart, nextPath: TypePath): TypePath[] {
@@ -80,6 +119,7 @@ class TypeGraph {
80119
// We already add a new path, since the next part of the part refers to a new inferred type
81120
paths.push({
82121
alt: copyTypeAlternative(split),
122+
current: part,
83123
next: []
84124
});
85125
}
@@ -101,16 +141,13 @@ class TypeGraph {
101141
split.ruleCalls.push(...part.ruleCalls);
102142
const path: TypePath = {
103143
alt: split,
144+
current: part,
104145
next: part.children
105146
};
106-
if (path.next.length === 0) {
107-
path.alt.super = path.alt.super.filter(e => e !== path.alt.name);
108-
paths.push(path);
109-
} else {
110-
paths.push(...this.applyNext(root, path));
111-
}
147+
path.alt.super = path.alt.super.filter(e => e !== path.alt.name);
148+
paths.push(path);
112149
}
113-
return flattenTypes(paths);
150+
return paths;
114151
}
115152

116153
private splitType(type: TypeAlternative, count: number): TypeAlternative[] {
@@ -307,9 +344,9 @@ function getRuleTypes(context: TypeCollectionContext, rule: ParserRule): TypePat
307344
const type = newTypePart(rule);
308345
const graph = new TypeGraph(context, type);
309346
if (rule.definition) {
310-
collectElement(graph, graph.root, rule.definition);
347+
type.end = collectElement(graph, graph.root, rule.definition);
311348
}
312-
return graph.getTypes();
349+
return flattenTypes(graph.getTypes(), type.end ?? newTypePart());
313350
}
314351

315352
function newTypePart(element?: ParserRule | Action | string): TypePart {
@@ -341,7 +378,9 @@ function collectElement(graph: TypeGraph, current: TypePart, element: AbstractEl
341378
const altType = graph.connect(current, newTypePart());
342379
children.push(collectElement(graph, altType, alt));
343380
}
344-
return graph.merge(...children);
381+
const mergeNode = graph.merge(...children);
382+
current.end = mergeNode;
383+
return mergeNode;
345384
} else if (isGroup(element) || isUnorderedGroup(element)) {
346385
let groupNode = graph.connect(current, newTypePart());
347386
let skipNode: TypePart | undefined;
@@ -352,7 +391,9 @@ function collectElement(graph: TypeGraph, current: TypePart, element: AbstractEl
352391
groupNode = collectElement(graph, groupNode, item);
353392
}
354393
if (skipNode) {
355-
return graph.merge(skipNode, groupNode);
394+
const mergeNode = graph.merge(skipNode, groupNode);
395+
current.end = mergeNode;
396+
return mergeNode;
356397
} else {
357398
return groupNode;
358399
}
@@ -479,7 +520,9 @@ function getFragmentProperties(fragment: ParserRule, context: TypeCollectionCont
479520
function calculateInterfaces(alternatives: TypePath[]): PlainInterface[] {
480521
const interfaces = new Map<string, PlainInterface>();
481522
const ruleCallAlternatives: TypeAlternative[] = [];
482-
const flattened = flattenTypes(alternatives).map(e => e.alt);
523+
const flattened = alternatives.length > 0
524+
? flattenTypes(alternatives, alternatives[0].current).map(e => e.alt)
525+
: [];
483526

484527
for (const flat of flattened) {
485528
const interfaceType: PlainInterface = {
@@ -516,14 +559,14 @@ function calculateInterfaces(alternatives: TypePath[]): PlainInterface[] {
516559
return Array.from(interfaces.values());
517560
}
518561

519-
function flattenTypes(alternatives: TypePath[]): TypePath[] {
562+
function flattenTypes(alternatives: TypePath[], part: TypePart): TypePath[] {
520563
const nameToAlternatives = alternatives.reduce((acc, e) => acc.add(e.alt.name, e), new MultiMap<string, TypePath>());
521564
const types: TypePath[] = [];
522565

523566
for (const [name, namedAlternatives] of nameToAlternatives.entriesGroupedByKey()) {
524567
const properties: PlainProperty[] = [];
525568
const ruleCalls = new Set<string>();
526-
const type: TypePath = { alt: { name, properties, ruleCalls: [], super: [] }, next: [] };
569+
const type: TypePath = { alt: { name, properties, ruleCalls: [], super: [] }, next: [], current: part };
527570
for (const path of namedAlternatives) {
528571
const alt = path.alt;
529572
type.alt.super.push(...alt.super);
@@ -541,6 +584,7 @@ function flattenTypes(alternatives: TypePath[]): TypePath[] {
541584
alt.ruleCalls.forEach(ruleCall => ruleCalls.add(ruleCall));
542585
}
543586
for (const path of namedAlternatives) {
587+
type.next = Array.from(new Set(type.next));
544588
const alt = path.alt;
545589
// A type with rule calls is not a real member of the type
546590
// Any missing properties are therefore not associated with the current type

packages/langium/src/grammar/type-system/type-collector/plain-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ function typeEquals(first: PlainPropertyType, second: PlainPropertyType): boolea
237237
return typeEquals(first.referenceType, second.referenceType);
238238
} else if (isPlainValueType(first) && isPlainValueType(second)) {
239239
return first.value === second.value;
240+
} else if (isPlainPrimitiveType(first) && isPlainPrimitiveType(second)) {
241+
return first.primitive === second.primitive;
240242
} else {
241243
return false;
242244
}

packages/langium/test/grammar/type-system/inferred-types.test.ts

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -831,55 +831,55 @@ describe('generated types from declared types include all of them', () => {
831831

832832
});
833833

834-
// TODO @msujew: Test case for https://github.com/eclipse-langium/langium/issues/775
835-
// describe('type merging runs in non-exponential time', () => {
836-
//
837-
// test('grammar with many optional groups is processed correctly', async () => {
838-
// const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar;
839-
// const document = await parseHelper<Grammar>(grammarServices)(`
840-
// grammar Test
841-
//
842-
// entry Model:
843-
// (title1=INT ';')?
844-
// (title2=INT ';')?
845-
// (title3=INT ';')?
846-
// (title4=INT ';')?
847-
// (title5=INT ';')?
848-
// (title6=INT ';')?
849-
// (title7=INT ';')?
850-
// (title8=INT ';')?
851-
// (title9=INT ';')?
852-
// (title10=INT ';')?
853-
// (title11=INT ';')?
854-
// (title12=INT ';')?
855-
// (title13=INT ';')?
856-
// (title14=INT ';')?
857-
// (title15=INT ';')?
858-
// (title16=INT ';')?
859-
// (title17=INT ';')?
860-
// (title18=INT ';')?
861-
// (title19=INT ';')?
862-
// (title20=INT ';')?
863-
// (title21=INT ';')?
864-
// (title22=INT ';')?
865-
// (title23=INT ';')?
866-
// (title24=INT ';')?
867-
// (title25=INT ';')?
868-
// (title26=INT ';')?
869-
// (title27=INT ';')?
870-
// (title28=INT ';')?
871-
// (title29=INT ';')?
872-
// (title30=INT ';')?
873-
// ;
874-
// terminal INT returns number: ('0'..'9')+;
875-
// `);
876-
// const { interfaces } = collectAst(document.parseResult.value);
877-
// const model = interfaces[0];
878-
// expect(model.properties).toHaveLength(30);
879-
// expect(model.properties.every(e => e.optional)).toBeTruthy();
880-
// });
881-
//
882-
// });
834+
// https://github.com/eclipse-langium/langium/issues/775
835+
describe('type merging runs in non-exponential time', () => {
836+
837+
test('grammar with many optional groups is processed correctly', async () => {
838+
const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar;
839+
const document = await parseHelper<Grammar>(grammarServices)(`
840+
grammar Test
841+
842+
entry Model:
843+
(title1=INT ';')?
844+
(title2=INT ';')?
845+
(title3=INT ';')?
846+
(title4=INT ';')?
847+
(title5=INT ';')?
848+
(title6=INT ';')?
849+
(title7=INT ';')?
850+
(title8=INT ';')?
851+
(title9=INT ';')?
852+
(title10=INT ';')?
853+
(title11=INT ';')?
854+
(title12=INT ';')?
855+
(title13=INT ';')?
856+
(title14=INT ';')?
857+
(title15=INT ';')?
858+
(title16=INT ';')?
859+
(title17=INT ';')?
860+
(title18=INT ';')?
861+
(title19=INT ';')?
862+
(title20=INT ';')?
863+
(title21=INT ';')?
864+
(title22=INT ';')?
865+
(title23=INT ';')?
866+
(title24=INT ';')?
867+
(title25=INT ';')?
868+
(title26=INT ';')?
869+
(title27=INT ';')?
870+
(title28=INT ';')?
871+
(title29=INT ';')?
872+
(title30=INT ';')?
873+
;
874+
terminal INT returns number: ('0'..'9')+;
875+
`);
876+
const { interfaces } = collectAst(document.parseResult.value);
877+
const model = interfaces[0];
878+
expect(model.properties).toHaveLength(30);
879+
expect(model.properties.every(e => e.optional)).toBeTruthy();
880+
});
881+
882+
});
883883

884884
const services = createLangiumGrammarServices(EmptyFileSystem).grammar;
885885
const helper = parseHelper<Grammar>(services);

0 commit comments

Comments
 (0)