diff --git a/examples/arithmetics/src/language-server/generated/ast.ts b/examples/arithmetics/src/language-server/generated/ast.ts index 468101593..41dd4c16f 100644 --- a/examples/arithmetics/src/language-server/generated/ast.ts +++ b/examples/arithmetics/src/language-server/generated/ast.ts @@ -149,7 +149,7 @@ export class ArithmeticsAstReflection extends AbstractAstReflection { return this.isSubtype(AbstractDefinition, supertype); } case Definition: { - return this.isSubtype(Statement, supertype) || this.isSubtype(AbstractDefinition, supertype); + return this.isSubtype(AbstractDefinition, supertype) || this.isSubtype(Statement, supertype); } case Evaluation: { return this.isSubtype(Statement, supertype); diff --git a/examples/arithmetics/src/language-server/generated/grammar.ts b/examples/arithmetics/src/language-server/generated/grammar.ts index c0bdd58e6..a2d46d2e3 100644 --- a/examples/arithmetics/src/language-server/generated/grammar.ts +++ b/examples/arithmetics/src/language-server/generated/grammar.ts @@ -588,25 +588,24 @@ export const ArithmeticsGrammar = (): Grammar => loadedArithmeticsGrammar ?? (lo "types": [ { "$type": "Type", - "typeAlternatives": [ - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@2" - }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@3" + "name": "AbstractDefinition", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@2" + } }, - "isArray": false, - "isRef": false - } - ], - "name": "AbstractDefinition" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@3" + } + } + ] + } } ], "definesHiddenTokens": false, diff --git a/examples/arithmetics/syntaxes/arithmetics.monarch.ts b/examples/arithmetics/syntaxes/arithmetics.monarch.ts index 610f5615c..3456a6cfa 100644 --- a/examples/arithmetics/syntaxes/arithmetics.monarch.ts +++ b/examples/arithmetics/syntaxes/arithmetics.monarch.ts @@ -4,9 +4,9 @@ export default { 'def','module' ], operators: [ - '-',',',';',':','*','/','+' + '*','+',',','-','/',':',';' ], - symbols: /-|,|;|:|\(|\)|\*|/|\+/, + symbols: /\(|\)|\*|\+|,|-|/|:|;/, tokenizer: { initial: [ diff --git a/examples/domainmodel/syntaxes/domainmodel.monarch.ts b/examples/domainmodel/syntaxes/domainmodel.monarch.ts index 12be0be98..b91faf714 100644 --- a/examples/domainmodel/syntaxes/domainmodel.monarch.ts +++ b/examples/domainmodel/syntaxes/domainmodel.monarch.ts @@ -4,9 +4,9 @@ export default { 'datatype','entity','extends','many','package' ], operators: [ - ':','.' + '.',':' ], - symbols: /:|\.|\{|\}/, + symbols: /\.|:|\{|\}/, tokenizer: { initial: [ diff --git a/examples/statemachine/syntaxes/statemachine.monarch.ts b/examples/statemachine/syntaxes/statemachine.monarch.ts index 7808518ae..3012f0a97 100644 --- a/examples/statemachine/syntaxes/statemachine.monarch.ts +++ b/examples/statemachine/syntaxes/statemachine.monarch.ts @@ -6,7 +6,7 @@ export default { operators: [ '=>' ], - symbols: /\{|\}|=>/, + symbols: /=>|\{|\}/, tokenizer: { initial: [ diff --git a/packages/langium-cli/src/generator/ast-generator.ts b/packages/langium-cli/src/generator/ast-generator.ts index 08d333923..969b934d9 100644 --- a/packages/langium-cli/src/generator/ast-generator.ts +++ b/packages/langium-cli/src/generator/ast-generator.ts @@ -7,7 +7,7 @@ import { GeneratorNode, Grammar, IndentNode, CompositeGeneratorNode, NL, toString, streamAllContents, MultiMap, LangiumServices, GrammarAST } from 'langium'; -import { AstTypes, collectAllProperties, collectAst, Property } from 'langium/lib/grammar/type-system'; +import { AstTypes, collectAst, collectTypeHierarchy, findReferenceTypes, hasArrayType, hasBooleanType, mergeTypesAndInterfaces, Property } from 'langium/lib/grammar/type-system'; import { LangiumConfig } from '../package'; import { generatedHeader } from './util'; @@ -24,8 +24,8 @@ export function generateAst(services: LangiumServices, grammars: Grammar[], conf `import { AstNode, AbstractAstReflection${crossRef ? ', Reference' : ''}, ReferenceInfo, TypeMetaData } from '${importFrom}';`, NL, NL ); - astTypes.unions.forEach(union => fileNode.append(union.toAstTypesString(), NL)); - astTypes.interfaces.forEach(iFace => fileNode.append(iFace.toAstTypesString(), NL)); + astTypes.unions.forEach(union => fileNode.append(union.toAstTypesString(true), NL)); + astTypes.interfaces.forEach(iFace => fileNode.append(iFace.toAstTypesString(true), NL)); astTypes.unions = astTypes.unions.filter(e => e.reflection); fileNode.append(generateAstReflection(config, astTypes)); @@ -84,11 +84,10 @@ function buildTypeMetaDataMethod(astTypes: AstTypes): GeneratorNode { const typeSwitchNode = new IndentNode(); typeSwitchNode.append('switch (type) {', NL); typeSwitchNode.indent(caseNode => { - const allProperties = collectAllProperties(astTypes.interfaces); for (const interfaceType of astTypes.interfaces) { - const props = allProperties.get(interfaceType.name)!; - const arrayProps = props.filter(e => e.typeAlternatives.some(e => e.array)); - const booleanProps = props.filter(e => e.typeAlternatives.every(e => !e.array && e.types.includes('boolean'))); + const props = interfaceType.properties; + const arrayProps = props.filter(e => hasArrayType(e.type)); + const booleanProps = props.filter(e => hasBooleanType(e.type)); if (arrayProps.length > 0 || booleanProps.length > 0) { caseNode.append(`case '${interfaceType.name}': {`, NL); caseNode.indent(caseContent => { @@ -174,18 +173,19 @@ function buildCrossReferenceTypes(astTypes: AstTypes): CrossReferenceType[] { const crossReferences = new MultiMap(); for (const typeInterface of astTypes.interfaces) { for (const property of typeInterface.properties.sort((a, b) => a.name.localeCompare(b.name))) { - property.typeAlternatives.filter(e => e.reference).flatMap(e => e.types).forEach(type => + const refTypes = findReferenceTypes(property.type); + for (const refType of refTypes) { crossReferences.add(typeInterface.name, { type: typeInterface.name, feature: property.name, - referenceType: type - }) - ); + referenceType: refType + }); + } } // Since the types are topologically sorted we can assume // that all super type properties have already been processed - for (const superType of typeInterface.printingSuperTypes) { - const superTypeCrossReferences = crossReferences.get(superType).map(e => ({ + for (const superType of typeInterface.interfaceSuperTypes) { + const superTypeCrossReferences = crossReferences.get(superType.name).map(e => ({ ...e, type: typeInterface.name })); @@ -210,7 +210,7 @@ function buildIsSubtypeMethod(astTypes: AstTypes): GeneratorNode { switchNode.contents.pop(); switchNode.append(' {', NL); switchNode.indent(caseNode => { - caseNode.append(`return ${superTypes.split(':').map(e => `this.isSubtype(${e}, supertype)`).join(' || ')};`); + caseNode.append(`return ${superTypes.split(':').sort().map(e => `this.isSubtype(${e}, supertype)`).join(' || ')};`); }); switchNode.append(NL, '}', NL); } @@ -225,19 +225,11 @@ function buildIsSubtypeMethod(astTypes: AstTypes): GeneratorNode { return methodNode; } -type ChildToSuper = { - name: string, - realSuperTypes: Set -} - function groupBySupertypes(astTypes: AstTypes): MultiMap { - const allTypes: ChildToSuper[] = (astTypes.interfaces as ChildToSuper[]) - .concat(astTypes.unions) - .filter(e => e.realSuperTypes.size > 0); - + const hierarchy = collectTypeHierarchy(mergeTypesAndInterfaces(astTypes)); const superToChild = new MultiMap(); - for (const item of allTypes) { - superToChild.add([...item.realSuperTypes].join(':'), item.name); + for (const [name, superTypes] of hierarchy.superTypes.entriesGroupedByKey()) { + superToChild.add(superTypes.join(':'), name); } return superToChild; diff --git a/packages/langium-cli/src/generator/util.ts b/packages/langium-cli/src/generator/util.ts index bb0c34c3b..86700e638 100644 --- a/packages/langium-cli/src/generator/util.ts +++ b/packages/langium-cli/src/generator/util.ts @@ -63,7 +63,7 @@ export function collectKeywords(grammar: Grammar): string[] { keywords.add(keyword.value); } - return Array.from(keywords).sort((a, b) => a.localeCompare(b)); + return Array.from(keywords).sort(); } export function getUserInput(text: string): Promise { diff --git a/packages/langium/src/grammar/ast-reflection-interpreter.ts b/packages/langium/src/grammar/ast-reflection-interpreter.ts index b7b7ecd2f..b412f3d78 100644 --- a/packages/langium/src/grammar/ast-reflection-interpreter.ts +++ b/packages/langium/src/grammar/ast-reflection-interpreter.ts @@ -4,13 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstReflection, isAstNode, ReferenceInfo, TypeMandatoryProperty, TypeMetaData } from '../syntax-tree'; +import { AbstractAstReflection, AstReflection, ReferenceInfo, TypeMandatoryProperty, TypeMetaData } from '../syntax-tree'; import { MultiMap } from '../utils/collections'; import { LangiumDocuments } from '../workspace/documents'; import { Grammar, isGrammar } from './generated/ast'; import { collectAst } from './type-system/ast-collector'; import { AstTypes, Property } from './type-system/type-collector/types'; -import { collectAllProperties } from './type-system/types-util'; +import { collectTypeHierarchy, findReferenceTypes, hasArrayType, hasBooleanType, mergeTypesAndInterfaces } from './type-system/types-util'; export function interpretAstReflection(astTypes: AstTypes): AstReflection; export function interpretAstReflection(grammar: Grammar, documents?: LangiumDocuments): AstReflection; @@ -24,56 +24,78 @@ export function interpretAstReflection(grammarOrTypes: Grammar | AstTypes, docum const allTypes = collectedTypes.interfaces.map(e => e.name).concat(collectedTypes.unions.map(e => e.name)); const references = buildReferenceTypes(collectedTypes); const metaData = buildTypeMetaData(collectedTypes); - const superTypeMap = buildSupertypeMap(collectedTypes); + const superTypes = collectTypeHierarchy(mergeTypesAndInterfaces(collectedTypes)).superTypes; - return { - getAllTypes() { - return allTypes; - }, - getReferenceType(refInfo: ReferenceInfo): string { - const referenceId = `${refInfo.container.$type}:${refInfo.property}`; - const referenceType = references.get(referenceId); - if (referenceType) { - return referenceType; - } - throw new Error('Could not find reference type for ' + referenceId); - }, - getTypeMetaData(type: string): TypeMetaData { - return metaData.get(type) ?? { - name: type, - mandatory: [] - }; - }, - isInstance(node: unknown, type: string): boolean { - return isAstNode(node) && this.isSubtype(node.$type, type); - }, - isSubtype(subtype: string, originalSuperType: string): boolean { - if (subtype === originalSuperType) { + return new InterpretedAstReflection({ + allTypes, + references, + metaData, + superTypes + }); +} + +class InterpretedAstReflection extends AbstractAstReflection { + + private readonly allTypes: string[]; + private readonly references: Map; + private readonly metaData: Map; + private readonly superTypes: MultiMap; + + constructor(options: { + allTypes: string[] + references: Map + metaData: Map + superTypes: MultiMap + }) { + super(); + this.allTypes = options.allTypes; + this.references = options.references; + this.metaData = options.metaData; + this.superTypes = options.superTypes; + } + + getAllTypes(): string[] { + return this.allTypes; + } + + getReferenceType(refInfo: ReferenceInfo): string { + const referenceId = `${refInfo.container.$type}:${refInfo.property}`; + const referenceType = this.references.get(referenceId); + if (referenceType) { + return referenceType; + } + throw new Error('Could not find reference type for ' + referenceId); + } + + getTypeMetaData(type: string): TypeMetaData { + return this.metaData.get(type) ?? { + name: type, + mandatory: [] + }; + } + + protected computeIsSubtype(subtype: string, originalSuperType: string): boolean { + const superTypes = this.superTypes.get(subtype); + for (const superType of superTypes) { + if (this.isSubtype(superType, originalSuperType)) { return true; } - const superTypes = superTypeMap.get(subtype); - for (const superType of superTypes) { - if (this.isSubtype(superType, originalSuperType)) { - return true; - } - } - return false; } - }; + return false; + } + } function buildReferenceTypes(astTypes: AstTypes): Map { const references = new MultiMap(); for (const interfaceType of astTypes.interfaces) { for (const property of interfaceType.properties) { - for (const propertyAlternative of property.typeAlternatives) { - if (propertyAlternative.reference) { - references.add(interfaceType.name, [property.name, propertyAlternative.types[0]]); - } + for (const referenceType of findReferenceTypes(property.type)) { + references.add(interfaceType.name, [property.name, referenceType]); } } - for (const superType of interfaceType.printingSuperTypes) { - const superTypeReferences = references.get(superType); + for (const superType of interfaceType.interfaceSuperTypes) { + const superTypeReferences = references.get(superType.name); references.addAll(interfaceType.name, superTypeReferences); } } @@ -86,11 +108,10 @@ function buildReferenceTypes(astTypes: AstTypes): Map { function buildTypeMetaData(astTypes: AstTypes): Map { const map = new Map(); - const allProperties = collectAllProperties(astTypes.interfaces); for (const interfaceType of astTypes.interfaces) { - const props = allProperties.get(interfaceType.name)!; - const arrayProps = props.filter(e => e.typeAlternatives.some(e => e.array)); - const booleanProps = props.filter(e => e.typeAlternatives.every(e => !e.array && e.types.includes('boolean'))); + const props = interfaceType.superProperties; + const arrayProps = props.filter(e => hasArrayType(e.type)); + const booleanProps = props.filter(e => !hasArrayType(e.type) && hasBooleanType(e.type)); if (arrayProps.length > 0 || booleanProps.length > 0) { map.set(interfaceType.name, { name: interfaceType.name, @@ -113,14 +134,3 @@ function buildMandatoryMetaData(arrayProps: Property[], booleanProps: Property[] } return array; } - -function buildSupertypeMap(astTypes: AstTypes): MultiMap { - const map = new MultiMap(); - for (const interfaceType of astTypes.interfaces) { - map.addAll(interfaceType.name, interfaceType.realSuperTypes); - } - for (const unionType of astTypes.unions) { - map.addAll(unionType.name, unionType.realSuperTypes); - } - return map; -} diff --git a/packages/langium/src/grammar/generated/ast.ts b/packages/langium/src/grammar/generated/ast.ts index d169e659f..69b48ce0b 100644 --- a/packages/langium/src/grammar/generated/ast.ts +++ b/packages/langium/src/grammar/generated/ast.ts @@ -30,12 +30,20 @@ export function isCondition(item: unknown): item is Condition { return reflection.isInstance(item, Condition); } -export type FeatureName = string; +export type FeatureName = 'current' | 'entry' | 'extends' | 'false' | 'fragment' | 'grammar' | 'hidden' | 'import' | 'infer' | 'infers' | 'interface' | 'returns' | 'terminal' | 'true' | 'type' | 'with' | PrimitiveType | string; export type PrimitiveType = 'Date' | 'bigint' | 'boolean' | 'number' | 'string'; +export type TypeDefinition = ArrayType | ReferenceType | SimpleType | UnionType; + +export const TypeDefinition = 'TypeDefinition'; + +export function isTypeDefinition(item: unknown): item is TypeDefinition { + return reflection.isInstance(item, TypeDefinition); +} + export interface AbstractElement extends AstNode { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'AbstractElement' | 'Action' | 'Alternatives' | 'Assignment' | 'CharacterRange' | 'CrossReference' | 'Group' | 'Keyword' | 'NegatedToken' | 'RegexToken' | 'RuleCall' | 'TerminalAlternatives' | 'TerminalGroup' | 'TerminalRuleCall' | 'UnorderedGroup' | 'UntilToken' | 'Wildcard'; cardinality?: '*' | '+' | '?' } @@ -46,20 +54,16 @@ export function isAbstractElement(item: unknown): item is AbstractElement { return reflection.isInstance(item, AbstractElement); } -export interface AtomType extends AstNode { - readonly $container: Type | TypeAttribute; - readonly $type: 'AtomType'; - isArray: boolean - isRef: boolean - keywordType?: Keyword - primitiveType?: PrimitiveType - refType?: Reference +export interface ArrayType extends AstNode { + readonly $container: ArrayType | ReferenceType | Type | TypeAttribute | UnionType; + readonly $type: 'ArrayType'; + elementType: TypeDefinition } -export const AtomType = 'AtomType'; +export const ArrayType = 'ArrayType'; -export function isAtomType(item: unknown): item is AtomType { - return reflection.isInstance(item, AtomType); +export function isArrayType(item: unknown): item is ArrayType { + return reflection.isInstance(item, ArrayType); } export interface Conjunction extends AstNode { @@ -132,7 +136,7 @@ export function isInferredType(item: unknown): item is InferredType { } export interface Interface extends AstNode { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Interface'; attributes: Array name: string @@ -208,7 +212,7 @@ export function isParameterReference(item: unknown): item is ParameterReference } export interface ParserRule extends AstNode { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'ParserRule'; dataType?: PrimitiveType definesHiddenTokens: boolean @@ -229,6 +233,18 @@ export function isParserRule(item: unknown): item is ParserRule { return reflection.isInstance(item, ParserRule); } +export interface ReferenceType extends AstNode { + readonly $container: ArrayType | ReferenceType | Type | TypeAttribute | UnionType; + readonly $type: 'ReferenceType'; + referenceType: TypeDefinition +} + +export const ReferenceType = 'ReferenceType'; + +export function isReferenceType(item: unknown): item is ReferenceType { + return reflection.isInstance(item, ReferenceType); +} + export interface ReturnType extends AstNode { readonly $container: TerminalRule; readonly $type: 'ReturnType'; @@ -241,8 +257,22 @@ export function isReturnType(item: unknown): item is ReturnType { return reflection.isInstance(item, ReturnType); } +export interface SimpleType extends AstNode { + readonly $container: ArrayType | ReferenceType | Type | TypeAttribute | UnionType; + readonly $type: 'SimpleType'; + primitiveType?: PrimitiveType + stringType?: string + typeRef?: Reference +} + +export const SimpleType = 'SimpleType'; + +export function isSimpleType(item: unknown): item is SimpleType { + return reflection.isInstance(item, SimpleType); +} + export interface TerminalRule extends AstNode { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'TerminalRule'; definition: AbstractElement fragment: boolean @@ -258,10 +288,10 @@ export function isTerminalRule(item: unknown): item is TerminalRule { } export interface Type extends AstNode { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Type'; name: string - typeAlternatives: Array + type: TypeDefinition } export const Type = 'Type'; @@ -275,7 +305,7 @@ export interface TypeAttribute extends AstNode { readonly $type: 'TypeAttribute'; isOptional: boolean name: FeatureName - typeAlternatives: Array + type: TypeDefinition } export const TypeAttribute = 'TypeAttribute'; @@ -284,8 +314,20 @@ export function isTypeAttribute(item: unknown): item is TypeAttribute { return reflection.isInstance(item, TypeAttribute); } +export interface UnionType extends AstNode { + readonly $container: ArrayType | ReferenceType | Type | TypeAttribute | UnionType; + readonly $type: 'UnionType'; + types: Array +} + +export const UnionType = 'UnionType'; + +export function isUnionType(item: unknown): item is UnionType { + return reflection.isInstance(item, UnionType); +} + export interface Action extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Action'; feature?: FeatureName inferredType?: InferredType @@ -300,7 +342,7 @@ export function isAction(item: unknown): item is Action { } export interface Alternatives extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Alternatives'; elements: Array } @@ -312,7 +354,7 @@ export function isAlternatives(item: unknown): item is Alternatives { } export interface Assignment extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Assignment'; feature: FeatureName operator: '+=' | '=' | '?=' @@ -326,7 +368,7 @@ export function isAssignment(item: unknown): item is Assignment { } export interface CharacterRange extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'CharacterRange'; left: Keyword right?: Keyword @@ -339,7 +381,7 @@ export function isCharacterRange(item: unknown): item is CharacterRange { } export interface CrossReference extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'CrossReference'; deprecatedSyntax: boolean terminal?: AbstractElement @@ -353,7 +395,7 @@ export function isCrossReference(item: unknown): item is CrossReference { } export interface Group extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Group'; elements: Array guardCondition?: Condition @@ -366,7 +408,7 @@ export function isGroup(item: unknown): item is Group { } export interface Keyword extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Keyword'; value: string } @@ -378,7 +420,7 @@ export function isKeyword(item: unknown): item is Keyword { } export interface NegatedToken extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'NegatedToken'; terminal: AbstractElement } @@ -390,7 +432,7 @@ export function isNegatedToken(item: unknown): item is NegatedToken { } export interface RegexToken extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'RegexToken'; regex: string } @@ -402,7 +444,7 @@ export function isRegexToken(item: unknown): item is RegexToken { } export interface RuleCall extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'RuleCall'; arguments: Array rule: Reference @@ -415,7 +457,7 @@ export function isRuleCall(item: unknown): item is RuleCall { } export interface TerminalAlternatives extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'TerminalAlternatives'; elements: Array } @@ -427,7 +469,7 @@ export function isTerminalAlternatives(item: unknown): item is TerminalAlternati } export interface TerminalGroup extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'TerminalGroup'; elements: Array } @@ -439,7 +481,7 @@ export function isTerminalGroup(item: unknown): item is TerminalGroup { } export interface TerminalRuleCall extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'TerminalRuleCall'; rule: Reference } @@ -451,7 +493,7 @@ export function isTerminalRuleCall(item: unknown): item is TerminalRuleCall { } export interface UnorderedGroup extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'UnorderedGroup'; elements: Array } @@ -463,7 +505,7 @@ export function isUnorderedGroup(item: unknown): item is UnorderedGroup { } export interface UntilToken extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'UntilToken'; terminal: AbstractElement } @@ -475,7 +517,7 @@ export function isUntilToken(item: unknown): item is UntilToken { } export interface Wildcard extends AbstractElement { - readonly $container: Alternatives | Assignment | AtomType | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; + readonly $container: Alternatives | Assignment | CharacterRange | CrossReference | Grammar | Group | NegatedToken | ParserRule | TerminalAlternatives | TerminalGroup | TerminalRule | UnorderedGroup | UntilToken; readonly $type: 'Wildcard'; } @@ -491,8 +533,8 @@ export interface LangiumGrammarAstType { AbstractType: AbstractType Action: Action Alternatives: Alternatives + ArrayType: ArrayType Assignment: Assignment - AtomType: AtomType CharacterRange: CharacterRange Condition: Condition Conjunction: Conjunction @@ -511,15 +553,19 @@ export interface LangiumGrammarAstType { Parameter: Parameter ParameterReference: ParameterReference ParserRule: ParserRule + ReferenceType: ReferenceType RegexToken: RegexToken ReturnType: ReturnType RuleCall: RuleCall + SimpleType: SimpleType TerminalAlternatives: TerminalAlternatives TerminalGroup: TerminalGroup TerminalRule: TerminalRule TerminalRuleCall: TerminalRuleCall Type: Type TypeAttribute: TypeAttribute + TypeDefinition: TypeDefinition + UnionType: UnionType UnorderedGroup: UnorderedGroup UntilToken: UntilToken Wildcard: Wildcard @@ -528,28 +574,11 @@ export interface LangiumGrammarAstType { export class LangiumGrammarAstReflection extends AbstractAstReflection { getAllTypes(): string[] { - return ['AbstractElement', 'AbstractRule', 'AbstractType', 'Action', 'Alternatives', 'Assignment', 'AtomType', 'CharacterRange', 'Condition', 'Conjunction', 'CrossReference', 'Disjunction', 'Grammar', 'GrammarImport', 'Group', 'InferredType', 'Interface', 'Keyword', 'LiteralCondition', 'NamedArgument', 'NegatedToken', 'Negation', 'Parameter', 'ParameterReference', 'ParserRule', 'RegexToken', 'ReturnType', 'RuleCall', 'TerminalAlternatives', 'TerminalGroup', 'TerminalRule', 'TerminalRuleCall', 'Type', 'TypeAttribute', 'UnorderedGroup', 'UntilToken', 'Wildcard']; + return ['AbstractElement', 'AbstractRule', 'AbstractType', 'Action', 'Alternatives', 'ArrayType', 'Assignment', 'CharacterRange', 'Condition', 'Conjunction', 'CrossReference', 'Disjunction', 'Grammar', 'GrammarImport', 'Group', 'InferredType', 'Interface', 'Keyword', 'LiteralCondition', 'NamedArgument', 'NegatedToken', 'Negation', 'Parameter', 'ParameterReference', 'ParserRule', 'ReferenceType', 'RegexToken', 'ReturnType', 'RuleCall', 'SimpleType', 'TerminalAlternatives', 'TerminalGroup', 'TerminalRule', 'TerminalRuleCall', 'Type', 'TypeAttribute', 'TypeDefinition', 'UnionType', 'UnorderedGroup', 'UntilToken', 'Wildcard']; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { switch (subtype) { - case Conjunction: - case Disjunction: - case LiteralCondition: - case Negation: - case ParameterReference: { - return this.isSubtype(Condition, supertype); - } - case Interface: - case Type: { - return this.isSubtype(AbstractType, supertype); - } - case ParserRule: { - return this.isSubtype(AbstractRule, supertype) || this.isSubtype(AbstractType, supertype); - } - case TerminalRule: { - return this.isSubtype(AbstractRule, supertype); - } case Action: { return this.isSubtype(AbstractElement, supertype) || this.isSubtype(AbstractType, supertype); } @@ -570,6 +599,29 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection { case Wildcard: { return this.isSubtype(AbstractElement, supertype); } + case ArrayType: + case ReferenceType: + case SimpleType: + case UnionType: { + return this.isSubtype(TypeDefinition, supertype); + } + case Conjunction: + case Disjunction: + case LiteralCondition: + case Negation: + case ParameterReference: { + return this.isSubtype(Condition, supertype); + } + case Interface: + case Type: { + return this.isSubtype(AbstractType, supertype); + } + case ParserRule: { + return this.isSubtype(AbstractRule, supertype) || this.isSubtype(AbstractType, supertype); + } + case TerminalRule: { + return this.isSubtype(AbstractRule, supertype); + } default: { return false; } @@ -580,10 +632,10 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection { const referenceId = `${refInfo.container.$type}:${refInfo.property}`; switch (referenceId) { case 'Action:type': - case 'AtomType:refType': case 'CrossReference:type': case 'Interface:superTypes': - case 'ParserRule:returnType': { + case 'ParserRule:returnType': + case 'SimpleType:typeRef': { return AbstractType; } case 'Grammar:hiddenTokens': @@ -609,15 +661,6 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection { getTypeMetaData(type: string): TypeMetaData { switch (type) { - case 'AtomType': { - return { - name: 'AtomType', - mandatory: [ - { name: 'isArray', type: 'boolean' }, - { name: 'isRef', type: 'boolean' } - ] - }; - } case 'Grammar': { return { name: 'Grammar', @@ -680,20 +723,19 @@ export class LangiumGrammarAstReflection extends AbstractAstReflection { ] }; } - case 'Type': { + case 'TypeAttribute': { return { - name: 'Type', + name: 'TypeAttribute', mandatory: [ - { name: 'typeAlternatives', type: 'array' } + { name: 'isOptional', type: 'boolean' } ] }; } - case 'TypeAttribute': { + case 'UnionType': { return { - name: 'TypeAttribute', + name: 'UnionType', mandatory: [ - { name: 'isOptional', type: 'boolean' }, - { name: 'typeAlternatives', type: 'array' } + { name: 'types', type: 'array' } ] }; } diff --git a/packages/langium/src/grammar/generated/grammar.ts b/packages/langium/src/grammar/generated/grammar.ts index 38ca150cb..605ce2686 100644 --- a/packages/langium/src/grammar/generated/grammar.ts +++ b/packages/langium/src/grammar/generated/grammar.ts @@ -38,7 +38,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -62,7 +62,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -88,7 +88,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -127,12 +127,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -153,12 +153,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -188,7 +188,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@12" }, "arguments": [] }, @@ -204,7 +204,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "arguments": [] } @@ -228,7 +228,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@7" + "$ref": "#/rules@10" }, "arguments": [] } @@ -261,7 +261,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -282,6 +282,13 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "type": { "$ref": "#/types@0" }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@59" + }, + "arguments": [] + }, "deprecatedSyntax": false } }, @@ -301,6 +308,13 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "type": { "$ref": "#/types@0" }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@59" + }, + "arguments": [] + }, "deprecatedSyntax": false } } @@ -380,7 +394,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@58" }, "arguments": [] } @@ -399,16 +413,101 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "$type": "Keyword", "value": ":" }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": ";", + "cardinality": "?" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "TypeDefinition", + "definition": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "UnionType", + "inferredType": { + "$type": "InferredType", + "name": "TypeDefinition" + }, + "definition": { + "$type": "Group", + "elements": [ { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@6" }, "arguments": [] }, { - "$type": "Keyword", - "value": ";", + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "UnionType" + }, + "feature": "types", + "operator": "+=" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "|" + }, + { + "$type": "Assignment", + "feature": "types", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + } + } + ], + "cardinality": "+" + } + ], "cardinality": "?" } ] @@ -422,65 +521,169 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar }, { "$type": "ParserRule", - "name": "TypeAlternatives", - "fragment": true, + "name": "ArrayType", + "inferredType": { + "$type": "InferredType", + "name": "TypeDefinition" + }, "definition": { "$type": "Group", "elements": [ { - "$type": "Assignment", - "feature": "typeAlternatives", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "ArrayType" + }, + "feature": "elementType", + "operator": "=" }, - "arguments": [] - } + { + "$type": "Keyword", + "value": "[" + }, + { + "$type": "Keyword", + "value": "]" + } + ], + "cardinality": "?" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "ReferenceType", + "inferredType": { + "$type": "InferredType", + "name": "TypeDefinition" + }, + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] }, { "$type": "Group", "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "ReferenceType" + } + }, { "$type": "Keyword", - "value": "|" + "value": "@" }, { "$type": "Assignment", - "feature": "typeAlternatives", - "operator": "+=", + "feature": "referenceType", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@8" }, "arguments": [] } } - ], - "cardinality": "*" + ] } ] }, "definesHiddenTokens": false, "entry": false, + "fragment": false, "hiddenTokens": [], "parameters": [], "wildcard": false }, { "$type": "ParserRule", - "name": "AtomType", + "name": "SimpleType", + "inferredType": { + "$type": "InferredType", + "name": "TypeDefinition" + }, "definition": { "$type": "Alternatives", "elements": [ { "$type": "Group", "elements": [ + { + "$type": "Keyword", + "value": "(" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + { + "$type": "Keyword", + "value": ")" + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "SimpleType" + } + }, { "$type": "Alternatives", "elements": [ + { + "$type": "Assignment", + "feature": "typeRef", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/types@0" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@59" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + }, { "$type": "Assignment", "feature": "primitiveType", @@ -488,63 +691,26 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@9" }, "arguments": [] } }, { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "isRef", - "operator": "?=", - "terminal": { - "$type": "Keyword", - "value": "@" - }, - "cardinality": "?" + "$type": "Assignment", + "feature": "stringType", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@60" }, - { - "$type": "Assignment", - "feature": "refType", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/types@0" - }, - "deprecatedSyntax": false - } - } - ] + "arguments": [] + } } ] - }, - { - "$type": "Assignment", - "feature": "isArray", - "operator": "?=", - "terminal": { - "$type": "Keyword", - "value": "[]" - }, - "cardinality": "?" } ] - }, - { - "$type": "Assignment", - "feature": "keywordType", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [] - } } ] }, @@ -608,7 +774,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -618,11 +784,16 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "value": "=" }, { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } }, { "$type": "Keyword", @@ -647,14 +818,14 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@13" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@46" }, "arguments": [] } @@ -684,7 +855,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@60" }, "arguments": [] } @@ -736,7 +907,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@15" }, "arguments": [] }, @@ -774,7 +945,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -788,7 +959,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@9" }, "arguments": [] } @@ -804,7 +975,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@14" }, "arguments": [ { @@ -847,12 +1018,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -873,12 +1044,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -909,7 +1080,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@17" }, "arguments": [] } @@ -947,7 +1118,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "guardCondition": { "$type": "ParameterReference", "parameter": { - "$ref": "#/rules@11/parameters@0" + "$ref": "#/rules@14/parameters@0" } }, "elements": [ @@ -964,7 +1135,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "value": { "$type": "ParameterReference", "parameter": { - "$ref": "#/rules@11/parameters@0" + "$ref": "#/rules@14/parameters@0" } } }, @@ -984,7 +1155,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -1011,7 +1182,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -1033,7 +1204,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@16" }, "arguments": [] } @@ -1052,7 +1223,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@16" }, "arguments": [] } @@ -1088,7 +1259,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -1113,7 +1284,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@18" }, "arguments": [] }, @@ -1143,7 +1314,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@18" }, "arguments": [] } @@ -1176,7 +1347,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@19" }, "arguments": [] }, @@ -1201,7 +1372,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@29" }, "arguments": [] } @@ -1217,7 +1388,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@21" }, "arguments": [] }, @@ -1247,7 +1418,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@20" }, "arguments": [] }, @@ -1277,7 +1448,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@20" }, "arguments": [] } @@ -1310,7 +1481,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@21" }, "arguments": [] }, @@ -1333,7 +1504,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@21" }, "arguments": [] }, @@ -1364,14 +1535,14 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@22" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@23" }, "arguments": [] } @@ -1400,14 +1571,14 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@37" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@24" }, "arguments": [] } @@ -1481,7 +1652,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1495,7 +1666,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@14" }, "arguments": [ { @@ -1525,7 +1696,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@58" }, "arguments": [] } @@ -1581,42 +1752,42 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@26" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@43" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@35" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@36" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@44" }, "arguments": [] } @@ -1639,7 +1810,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@60" }, "arguments": [] } @@ -1664,12 +1835,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1690,7 +1861,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@27" }, "arguments": [] } @@ -1709,7 +1880,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@27" }, "arguments": [] } @@ -1749,12 +1920,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@13" + "$ref": "#/rules@16" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -1780,7 +1951,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@29" }, "arguments": [] } @@ -1835,7 +2006,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@30" }, "arguments": [] }, @@ -1862,7 +2033,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@27" + "$ref": "#/rules@30" }, "arguments": [] } @@ -1892,7 +2063,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@31" }, "arguments": [] }, @@ -1919,7 +2090,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@31" }, "arguments": [] } @@ -1949,7 +2120,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@32" }, "arguments": [] }, @@ -1974,7 +2145,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@31" }, "arguments": [] } @@ -2003,21 +2174,21 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@34" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@33" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@28" }, "arguments": [] } @@ -2047,7 +2218,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@29" }, "arguments": [] }, @@ -2074,12 +2245,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@13" + "$ref": "#/rules@16" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -2123,7 +2294,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@60" }, "arguments": [] } @@ -2167,12 +2338,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@8" + "$ref": "#/rules@11" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -2193,7 +2364,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@27" }, "arguments": [] } @@ -2212,7 +2383,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@27" }, "arguments": [] } @@ -2274,7 +2445,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@58" }, "arguments": [] } @@ -2308,7 +2479,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2335,28 +2506,28 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@26" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@39" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2386,7 +2557,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@40" }, "arguments": [] }, @@ -2416,7 +2587,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@38" }, "arguments": [] }, @@ -2446,7 +2617,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2527,7 +2698,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2561,14 +2732,14 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@26" }, "arguments": [] } @@ -2598,7 +2769,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@17" }, "arguments": [] }, @@ -2649,7 +2820,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@17" }, "arguments": [] } @@ -2680,14 +2851,14 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@9" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -2743,7 +2914,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -2760,7 +2931,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -2779,7 +2950,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@45" }, "arguments": [] } @@ -2802,7 +2973,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@47" }, "arguments": [] } @@ -2833,7 +3004,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@48" }, "arguments": [] }, @@ -2860,7 +3031,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@48" }, "arguments": [] } @@ -2890,7 +3061,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@49" }, "arguments": [] }, @@ -2913,7 +3084,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@49" }, "arguments": [] }, @@ -2944,7 +3115,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@50" }, "arguments": [] }, @@ -2993,49 +3164,49 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@57" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@52" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@51" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@53" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@54" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@55" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@56" }, "arguments": [] } @@ -3065,7 +3236,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@47" }, "arguments": [] }, @@ -3106,12 +3277,12 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@43" + "$ref": "#/rules@46" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -3155,7 +3326,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@50" }, "arguments": [] } @@ -3197,7 +3368,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@50" }, "arguments": [] } @@ -3235,7 +3406,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@61" }, "arguments": [] } @@ -3303,7 +3474,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] } @@ -3322,7 +3493,7 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@25" }, "arguments": [] } @@ -3413,14 +3584,14 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@9" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@59" }, "arguments": [] } @@ -3501,41 +3672,36 @@ export const LangiumGrammarGrammar = (): Grammar => loadedLangiumGrammarGrammar "types": [ { "$type": "Type", - "typeAlternatives": [ - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@1" - }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@7" + "name": "AbstractType", + "type": { + "$type": "UnionType", + "types": [ + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@1" + } }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@20/definition/elements@0" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@10" + } }, - "isArray": false, - "isRef": false - }, - { - "$type": "AtomType", - "refType": { - "$ref": "#/rules@10" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@23/definition/elements@0" + } }, - "isArray": false, - "isRef": false - } - ], - "name": "AbstractType" + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@13" + } + } + ] + } } ], "definesHiddenTokens": false, diff --git a/packages/langium/src/grammar/internal-grammar-util.ts b/packages/langium/src/grammar/internal-grammar-util.ts index 13f96aba5..aa7766655 100644 --- a/packages/langium/src/grammar/internal-grammar-util.ts +++ b/packages/langium/src/grammar/internal-grammar-util.ts @@ -97,11 +97,14 @@ export function getTypeName(type: ast.AbstractType | ast.InferredType): string { throw new TypeResolutionError('Cannot get name of Unknown Type', type.$cstNode); } -export function getTypeNameWithoutError(type: ast.AbstractType | ast.InferredType): string { +export function getTypeNameWithoutError(type?: ast.AbstractType | ast.InferredType): string | undefined { + if (!type) { + return undefined; + } try { return getTypeName(type); } catch { - return 'never'; + return undefined; } } diff --git a/packages/langium/src/grammar/langium-grammar.langium b/packages/langium/src/grammar/langium-grammar.langium index a7e854338..1f91a846b 100644 --- a/packages/langium/src/grammar/langium-grammar.langium +++ b/packages/langium/src/grammar/langium-grammar.langium @@ -15,20 +15,30 @@ entry Grammar: Interface: 'interface' name=ID - ('extends' superTypes+=[AbstractType] (',' superTypes+=[AbstractType])*)? + ('extends' superTypes+=[AbstractType:ID] (',' superTypes+=[AbstractType:ID])*)? SchemaType; fragment SchemaType: '{' attributes+=TypeAttribute* '}' ';'?; TypeAttribute: - name=FeatureName (isOptional?='?')? ':' TypeAlternatives ';'?; + name=FeatureName (isOptional?='?')? ':' type=TypeDefinition ';'?; -fragment TypeAlternatives: - typeAlternatives+=AtomType ('|' typeAlternatives+=AtomType)*; +TypeDefinition: UnionType; -AtomType: - (primitiveType=PrimitiveType | isRef?='@'? refType=[AbstractType]) isArray?='[]'? | keywordType=Keyword; +UnionType infers TypeDefinition: + ArrayType ({infer UnionType.types+=current} ('|' types+=ArrayType)+)?; + +ArrayType infers TypeDefinition: + ReferenceType ({infer ArrayType.elementType=current} '[' ']')? ; + +ReferenceType infers TypeDefinition: + SimpleType | + {infer ReferenceType} '@' referenceType=SimpleType; + +SimpleType infers TypeDefinition: + '(' TypeDefinition ')' | + {infer SimpleType} (typeRef=[AbstractType:ID] | primitiveType=PrimitiveType | stringType=STRING); PrimitiveType returns string: 'string' | 'number' | 'boolean' | 'Date' | 'bigint'; @@ -36,7 +46,7 @@ PrimitiveType returns string: type AbstractType = Interface | Type | Action | ParserRule; Type: - 'type' name=ID '=' TypeAlternatives ';'?; + 'type' name=ID '=' type=TypeDefinition ';'?; AbstractRule: ParserRule | TerminalRule; diff --git a/packages/langium/src/grammar/lsp/grammar-semantic-tokens.ts b/packages/langium/src/grammar/lsp/grammar-semantic-tokens.ts index 9f215d76e..b346668a2 100644 --- a/packages/langium/src/grammar/lsp/grammar-semantic-tokens.ts +++ b/packages/langium/src/grammar/lsp/grammar-semantic-tokens.ts @@ -7,7 +7,7 @@ import { SemanticTokenTypes } from 'vscode-languageserver'; import { AstNode } from '../../syntax-tree'; import { AbstractSemanticTokenProvider, SemanticTokenAcceptor } from '../../lsp/semantic-token-provider'; -import { isAction, isAssignment, isAtomType, isParameter, isParameterReference, isReturnType, isRuleCall, isTypeAttribute } from '../generated/ast'; +import { isAction, isAssignment, isParameter, isParameterReference, isReturnType, isRuleCall, isSimpleType, isTypeAttribute } from '../generated/ast'; export class LangiumGrammarSemanticTokenProvider extends AbstractSemanticTokenProvider { @@ -32,11 +32,11 @@ export class LangiumGrammarSemanticTokenProvider extends AbstractSemanticTokenPr property: 'name', type: SemanticTokenTypes.type }); - } else if (isAtomType(node)) { - if (node.primitiveType || node.refType) { + } else if (isSimpleType(node)) { + if (node.primitiveType || node.typeRef) { acceptor({ node, - property: node.primitiveType ? 'primitiveType' : 'refType', + property: node.primitiveType ? 'primitiveType' : 'typeRef', type: SemanticTokenTypes.type }); } diff --git a/packages/langium/src/grammar/type-system/ast-collector.ts b/packages/langium/src/grammar/type-system/ast-collector.ts index 5b052d4d4..bce847fd6 100644 --- a/packages/langium/src/grammar/type-system/ast-collector.ts +++ b/packages/langium/src/grammar/type-system/ast-collector.ts @@ -6,52 +6,80 @@ import { Grammar } from '../generated/ast'; import { LangiumDocuments } from '../../workspace/documents'; -import { addSubTypes, sortInterfacesTopologically } from './types-util'; -import { AstTypes, InterfaceType, isInterfaceType, isUnionType, TypeOption, UnionType } from './type-collector/types'; -import { collectTypeResources } from './type-collector/all-types'; -import { isPrimitiveType } from '../internal-grammar-util'; +import { collectTypeHierarchy, sortInterfacesTopologically } from './types-util'; +import { AstTypes, isArrayType, isInterfaceType, isPrimitiveType, isPropertyUnion, isStringType, isUnionType, isValueType, PropertyType, TypeOption, UnionType } from './type-collector/types'; +import { collectTypeResources, ValidationAstTypes } from './type-collector/all-types'; +import { PlainAstTypes, PlainInterface, plainToTypes, PlainUnion } from './type-collector/plain-types'; /** * Collects all types for the generated AST. The types collector entry point. - * @param documents Documents to resolve imports that were used in the given grammars. - * @param grammars Grammars for which it's necessary to build an AST. + * + * @param grammars All grammars involved in the type generation process + * @param documents Additional documents so that imports can be resolved as necessary */ export function collectAst(grammars: Grammar | Grammar[], documents?: LangiumDocuments): AstTypes { const { inferred, declared } = collectTypeResources(grammars, documents); + return createAstTypes(inferred, declared); +} + +/** + * Collects all types used during the validation process. + * The validation process requires us to compare our inferred types with our declared types. + * + * @param grammars All grammars involved in the validation process + * @param documents Additional documents so that imports can be resolved as necessary + */ +export function collectValidationAst(grammars: Grammar | Grammar[], documents?: LangiumDocuments): ValidationAstTypes { + const { inferred, declared, astResources } = collectTypeResources(grammars, documents); - const astTypes = { - interfaces: sortInterfacesTopologically(mergeAndRemoveDuplicates(inferred.interfaces, declared.interfaces)), - unions: mergeAndRemoveDuplicates(inferred.unions, declared.unions), + return { + astResources, + inferred: createAstTypes(declared, inferred), + declared: createAstTypes(inferred, declared) + }; +} + +export function createAstTypes(first: PlainAstTypes, second?: PlainAstTypes): AstTypes { + const astTypes: PlainAstTypes = { + interfaces: sortInterfacesTopologically(mergeAndRemoveDuplicates(...first.interfaces, ...second?.interfaces ?? [])), + unions: mergeAndRemoveDuplicates(...first.unions, ...second?.unions ?? []), }; - specifyAstNodeProperties(astTypes); - return astTypes; + const finalTypes = plainToTypes(astTypes); + specifyAstNodeProperties(finalTypes); + return finalTypes; } -function mergeAndRemoveDuplicates(inferred: T[], declared: T[]): T[] { - return Array.from(inferred.concat(declared) +/** + * Merges the lists of given elements into a single list and removes duplicates. Elements later in the lists get precedence over earlier elements. + * + * The distinction is performed over the `name` property of the element. The result is a name-sorted list of elements. + */ +function mergeAndRemoveDuplicates(...elements: T[]): T[] { + return Array.from(elements .reduce((acc, type) => { acc.set(type.name, type); return acc; }, new Map()) .values()).sort((a, b) => a.name.localeCompare(b.name)); } export function specifyAstNodeProperties(astTypes: AstTypes) { const nameToType = filterInterfaceLikeTypes(astTypes); - addSubTypes(nameToType); - buildContainerTypes(nameToType); - buildTypeTypes(nameToType); + const array = Array.from(nameToType.values()); + addSubTypes(array); + buildContainerTypes(array); + buildTypeNames(nameToType); } -function buildTypeTypes(nameToType: Map) { +function buildTypeNames(nameToType: Map) { const queue = Array.from(nameToType.values()).filter(e => e.subTypes.size === 0); const visited = new Set(); for (const type of queue) { visited.add(type); - type.typeTypes.add(type.name); - const superTypes = Array.from(type.realSuperTypes) - .map(superType => nameToType.get(superType)) + type.typeNames.add(type.name); + const superTypes = Array.from(type.superTypes) + .map(superType => nameToType.get(superType.name)) .filter(e => e !== undefined) as TypeOption[]; for (const superType of superTypes) { - type.typeTypes.forEach(e => superType.typeTypes.add(e)); + type.typeNames.forEach(e => superType.typeNames.add(e)); } queue.push(...superTypes.filter(e => !visited.has(e))); } @@ -66,27 +94,9 @@ function filterInterfaceLikeTypes({ interfaces, unions }: AstTypes): Map { acc.set(e.name, e); return acc; }, new Map()); const cache = new Map(); - function isDataTypeUnion(union: UnionType, visited = new Set()): boolean { - if (cache.has(union)) return cache.get(union)!; - if (visited.has(union)) return true; - visited.add(union); - - const ruleCalls = union.alternatives.flatMap(e => e.types).filter(e => !isPrimitiveType(e)); - if (ruleCalls.length === 0) { - return true; - } - for (const ruleCall of ruleCalls) { - const type = nameToType.get(ruleCall); - if (type && (isInterfaceType(type) || isUnionType(type) && !isDataTypeUnion(type, visited))) { - return false; - } - } - return true; - } for (const union of unions) { - const isDataType = isDataTypeUnion(union); - cache.set(union, isDataType); + cache.set(union, isDataType(union.type, new Set())); } for (const [union, isDataType] of cache) { if (isDataType) { @@ -96,19 +106,44 @@ function filterInterfaceLikeTypes({ interfaces, unions }: AstTypes): Map): boolean { + if (visited.has(property)) { + return true; + } + visited.add(property); + if (isPropertyUnion(property)) { + return property.types.every(e => isDataType(e, visited)); + } else if (isValueType(property)) { + const value = property.value; + if (isUnionType(value)) { + return isDataType(value.type, visited); + } else { + return false; + } + } else { + return isPrimitiveType(property) || isStringType(property); + } +} + +function addSubTypes(types: TypeOption[]) { + for (const interfaceType of types) { + for (const superTypeName of interfaceType.superTypes) { + superTypeName.subTypes.add(interfaceType); + } + } +} + /** * Builds container types for given interfaces. * @param interfaces The interfaces that have to get container types. */ -function buildContainerTypes(nameToType: Map) { - const types = Array.from(nameToType.values()); - +function buildContainerTypes(types: TypeOption[]) { // 1st stage: collect container types - const interfaces = types.filter(e => isInterfaceType(e)) as InterfaceType[]; + const interfaces = types.filter(isInterfaceType); for (const interfaceType of interfaces) { - const refTypes = interfaceType.properties.flatMap(property => property.typeAlternatives.filter(e => !e.reference).flatMap(e => e.types)); + const refTypes = interfaceType.properties.flatMap(property => findChildTypes(property.type, new Set())); for (const refType of refTypes) { - nameToType.get(refType)?.containerTypes.add(interfaceType.name); + refType.containerTypes.add(interfaceType); } } @@ -117,26 +152,46 @@ function buildContainerTypes(nameToType: Map) { shareContainerTypes(connectedComponents); } +function findChildTypes(type: PropertyType, set: Set): TypeOption[] { + if (isPropertyUnion(type)) { + return type.types.flatMap(e => findChildTypes(e, set)); + } else if (isValueType(type)) { + if (set.has(type.value)) { + return []; + } else { + set.add(type.value); + } + return [type.value]; + } else if (isArrayType(type)) { + return findChildTypes(type.elementType, set); + } else { + return []; + } +} + function calculateConnectedComponents(interfaces: TypeOption[]): TypeOption[][] { function dfs(typeInterface: TypeOption): TypeOption[] { const component: TypeOption[] = [typeInterface]; - visited.add(typeInterface.name); - const allTypes = [...typeInterface.subTypes, ...typeInterface.realSuperTypes]; - for (const nextTypeInterfaceName of allTypes) { - if (!visited.has(nextTypeInterfaceName)) { - const nextTypeInterface = interfaces.find(e => e.name === nextTypeInterfaceName); - if (nextTypeInterface) { - component.push(...dfs(nextTypeInterface)); - } + visited.add(typeInterface); + const allTypes = [ + ...hierarchy.subTypes.get(typeInterface.name), + ...hierarchy.superTypes.get(typeInterface.name) + ]; + for (const nextTypeInterface of allTypes) { + const nextType = map.get(nextTypeInterface); + if (nextType && !visited.has(nextType)) { + component.push(...dfs(nextType)); } } return component; } + const map = new Map(interfaces.map(e => [e.name, e])); const connectedComponents: TypeOption[][] = []; - const visited: Set = new Set(); + const hierarchy = collectTypeHierarchy(interfaces); + const visited = new Set(); for (const typeInterface of interfaces) { - if (!visited.has(typeInterface.name)) { + if (!visited.has(typeInterface)) { connectedComponents.push(dfs(typeInterface)); } } @@ -145,7 +200,7 @@ function calculateConnectedComponents(interfaces: TypeOption[]): TypeOption[][] function shareContainerTypes(connectedComponents: TypeOption[][]): void { for (const component of connectedComponents) { - const superSet = new Set(); + const superSet = new Set(); component.forEach(type => type.containerTypes.forEach(e => superSet.add(e))); component.forEach(type => type.containerTypes = superSet); } diff --git a/packages/langium/src/grammar/type-system/type-collector/all-types.ts b/packages/langium/src/grammar/type-system/type-collector/all-types.ts index c10a88520..0092b7d52 100644 --- a/packages/langium/src/grammar/type-system/type-collector/all-types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/all-types.ts @@ -6,14 +6,13 @@ import { collectInferredTypes } from './inferred-types'; import { collectDeclaredTypes } from './declared-types'; -import { LangiumDocuments, Grammar } from '../../..'; import { getDocument } from '../../../utils/ast-util'; -import { MultiMap } from '../../../utils/collections'; -import { ParserRule, Interface, Type, isParserRule } from '../../generated/ast'; +import { ParserRule, Interface, Type, isParserRule, Grammar } from '../../generated/ast'; import { isDataTypeRule, resolveImport } from '../../internal-grammar-util'; -import { mergeInterfaces } from '../types-util'; -import { AstTypes, InterfaceType, isInterfaceType } from './types'; import { URI } from 'vscode-uri'; +import { LangiumDocuments } from '../../../workspace/documents'; +import { PlainAstTypes } from './plain-types'; +import { AstTypes } from './types'; export type AstResources = { parserRules: ParserRule[], @@ -23,68 +22,27 @@ export type AstResources = { } export type TypeResources = { - inferred: AstTypes, - declared: AstTypes, + inferred: PlainAstTypes, + declared: PlainAstTypes, astResources: AstResources, } +export interface ValidationAstTypes { + inferred: AstTypes + declared: AstTypes + astResources: AstResources +} + export function collectTypeResources(grammars: Grammar | Grammar[], documents?: LangiumDocuments): TypeResources { const astResources = collectAllAstResources(grammars, documents); const declared = collectDeclaredTypes(astResources.interfaces, astResources.types); const inferred = collectInferredTypes(astResources.parserRules, astResources.datatypeRules, declared); - shareSuperTypesFromUnions(inferred, declared); - addSuperProperties(mergeInterfaces(inferred, declared)); - - return { astResources, inferred, declared }; -} - -function addSuperProperties(allTypes: InterfaceType[]) { - function addSuperPropertiesInternal(type: InterfaceType, visited = new Set()) { - if (visited.has(type)) return; - visited.add(type); - - for (const superTypeName of type.printingSuperTypes) { - const superType = allTypes.find(e => e.name === superTypeName); - - if (superType && isInterfaceType(superType)) { - addSuperPropertiesInternal(superType, visited); - superType.superProperties - .entriesGroupedByKey() - .forEach(propInfo => type.superProperties.addAll(propInfo[0], propInfo[1])); - } - } - } - - const visited = new Set(); - for (const type of allTypes) { - addSuperPropertiesInternal(type, visited); - } -} - -function shareSuperTypesFromUnions(inferred: AstTypes, declared: AstTypes): void { - const childToSuper = new MultiMap(); - const allUnions = inferred.unions.concat(declared.unions); - for (const union of allUnions) { - if (union.reflection) { - for (const propType of union.alternatives) { - propType.types.forEach(type => childToSuper.add(type, union.name)); - } - } - } - - function addSuperTypes(types: AstTypes, child: string, parents: string[]) { - const childType = types.interfaces.find(e => e.name === child) ?? - types.unions.find(e => e.name === child); - if (childType) { - parents.forEach(e => childType.realSuperTypes.add(e)); - } - } - - for (const [child, parents] of childToSuper.entriesGroupedByKey()) { - addSuperTypes(inferred, child, parents); - addSuperTypes(declared, child, parents); - } + return { + astResources, + inferred, + declared + }; } /////////////////////////////////////////////////////////////////////////////// diff --git a/packages/langium/src/grammar/type-system/type-collector/declared-types.ts b/packages/langium/src/grammar/type-system/type-collector/declared-types.ts index fa3c98c96..7eea8881e 100644 --- a/packages/langium/src/grammar/type-system/type-collector/declared-types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/declared-types.ts @@ -4,41 +4,99 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { Interface, Type, AtomType } from '../../generated/ast'; -import { getTypeNameWithoutError } from '../../internal-grammar-util'; -import { AstTypes, Property, InterfaceType, UnionType, PropertyType } from './types'; +import { Interface, Type, TypeDefinition, isArrayType, isReferenceType, isUnionType, isSimpleType } from '../../generated/ast'; +import { getTypeName, getTypeNameWithoutError, isPrimitiveType } from '../../internal-grammar-util'; +import { PlainAstTypes, PlainInterface, PlainProperty, PlainPropertyType, PlainUnion } from './plain-types'; -export function collectDeclaredTypes(interfaces: Interface[], unions: Type[]): AstTypes { - const declaredTypes: AstTypes = { unions: [], interfaces: [] }; +export function collectDeclaredTypes(interfaces: Interface[], unions: Type[]): PlainAstTypes { + const declaredTypes: PlainAstTypes = { unions: [], interfaces: [] }; // add interfaces for (const type of interfaces) { - const superTypes = type.superTypes.filter(e => e.ref).map(e => getTypeNameWithoutError(e.ref!)); - const properties: Property[] = type.attributes.map(e => { - name: e.name, - optional: e.isOptional === true, - typeAlternatives: e.typeAlternatives.map(atomTypeToPropertyType), - astNodes: new Set([e]) - }); - declaredTypes.interfaces.push(new InterfaceType(type.name, superTypes, properties)); + const properties: PlainProperty[] = []; + for (const attribute of type.attributes) { + properties.push({ + name: attribute.name, + optional: attribute.isOptional, + astNodes: new Set([attribute]), + type: typeDefinitionToPropertyType(attribute.type) + }); + } + const superTypes = new Set(); + for (const superType of type.superTypes) { + if (superType.ref) { + superTypes.add(getTypeName(superType.ref)); + } + } + const interfaceType: PlainInterface = { + name: type.name, + declared: true, + abstract: false, + properties: properties, + superTypes: superTypes, + subTypes: new Set() + }; + declaredTypes.interfaces.push(interfaceType); } // add types - for (const type of unions) { - const alternatives = type.typeAlternatives.map(atomTypeToPropertyType); - const reflection = type.typeAlternatives.length > 1 && type.typeAlternatives.some(e => e.refType?.ref !== undefined); - declaredTypes.unions.push(new UnionType(type.name, alternatives, { reflection })); + for (const union of unions) { + const unionType: PlainUnion = { + name: union.name, + declared: true, + reflection: true, + type: typeDefinitionToPropertyType(union.type), + superTypes: new Set(), + subTypes: new Set() + }; + declaredTypes.unions.push(unionType); } return declaredTypes; } -function atomTypeToPropertyType(type: AtomType): PropertyType { - let types: string[] = []; - if (type.refType) { - types = [type.refType.ref ? getTypeNameWithoutError(type.refType.ref) : type.refType.$refText]; - } else { - types = [type.primitiveType ?? `'${type.keywordType?.value}'`]; +export function typeDefinitionToPropertyType(type: TypeDefinition): PlainPropertyType { + if (isArrayType(type)) { + return { + elementType: typeDefinitionToPropertyType(type.elementType) + }; + } else if (isReferenceType(type)) { + return { + referenceType: typeDefinitionToPropertyType(type.referenceType) + }; + } else if (isUnionType(type)) { + return { + types: type.types.map(typeDefinitionToPropertyType) + }; + } else if (isSimpleType(type)) { + let value: string | undefined; + if (type.primitiveType) { + value = type.primitiveType; + return { + primitive: value + }; + } else if (type.stringType) { + value = type.stringType; + return { + string: value + }; + } else if (type.typeRef) { + const ref = type.typeRef.ref; + const value = getTypeNameWithoutError(ref); + if (value) { + if (isPrimitiveType(value)) { + return { + primitive: value + }; + } else { + return { + value + }; + } + } + } } - return { types, reference: type.isRef === true, array: type.isArray === true }; + return { + primitive: 'unknown' + }; } diff --git a/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts b/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts index 7323e9109..69b9e09ff 100644 --- a/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/inferred-types.ts @@ -6,15 +6,13 @@ import { isNamed } from '../../../references/name-provider'; import { MultiMap } from '../../../utils/collections'; -import { stream } from '../../../utils/stream'; -import { ParserRule, isAlternatives, isKeyword, Action, isParserRule, isAction, AbstractElement, isGroup, isUnorderedGroup, isAssignment, isRuleCall, Assignment, isCrossReference, RuleCall } from '../../generated/ast'; -import { getExplicitRuleType, getTypeNameWithoutError, isOptionalCardinality, getRuleType } from '../../internal-grammar-util'; -import { comparePropertyType } from '../types-util'; -import { Property, AstTypes, UnionType, PropertyType, InterfaceType } from './types'; +import { ParserRule, isAlternatives, isKeyword, Action, isParserRule, isAction, AbstractElement, isGroup, isUnorderedGroup, isAssignment, isRuleCall, Assignment, isCrossReference, RuleCall, isTerminalRule } from '../../generated/ast'; +import { getTypeNameWithoutError, isOptionalCardinality, getRuleType, isPrimitiveType } from '../../internal-grammar-util'; +import { mergePropertyTypes, PlainAstTypes, PlainInterface, PlainProperty, PlainPropertyType, PlainUnion } from './plain-types'; interface TypePart { name?: string - properties: Property[] + properties: PlainProperty[] ruleCalls: string[] parents: TypePart[] children: TypePart[] @@ -24,17 +22,17 @@ interface TypePart { type TypeAlternative = { name: string super: string[] - properties: Property[] + properties: PlainProperty[] ruleCalls: string[] } type TypeCollection = { - types: Set, + types: Set reference: boolean } interface TypeCollectionContext { - fragments: Map + fragments: Map } interface TypePath { @@ -78,7 +76,7 @@ class TypeGraph { // If the path enters an action with an assignment which changes the current name // We already add a new path, since the next part of the part refers to a new inferred type paths.push({ - alt: this.copyTypeAlternative(split), + alt: copyTypeAlternative(split), next: [] }); } @@ -115,28 +113,11 @@ class TypeGraph { private splitType(type: TypeAlternative, count: number): TypeAlternative[] { const alternatives: TypeAlternative[] = []; for (let i = 0; i < count; i++) { - alternatives.push(this.copyTypeAlternative(type)); + alternatives.push(copyTypeAlternative(type)); } return alternatives; } - private copyTypeAlternative(value: TypeAlternative): TypeAlternative { - function copyProperty(value: Property): Property { - return { - name: value.name, - optional: value.optional, - typeAlternatives: value.typeAlternatives, - astNodes: value.astNodes, - }; - } - return { - name: value.name, - super: value.super, - ruleCalls: value.ruleCalls, - properties: value.properties.map(e => copyProperty(e)) - }; - } - getSuperTypes(node: TypePart): string[] { const set = new Set(); this.collectSuperTypes(node, node, set); @@ -184,9 +165,52 @@ class TypeGraph { } return node; } + + hasLeafNode(part: TypePart): boolean { + return this.partHasLeafNode(part); + } + + private partHasLeafNode(part: TypePart, ignore?: TypePart): boolean { + if (part.children.some(e => e !== ignore)) { + return true; + } else if (part.name) { + return false; + } else { + return part.parents.some(e => this.partHasLeafNode(e, part)); + } + } } -export function collectInferredTypes(parserRules: ParserRule[], datatypeRules: ParserRule[], declaredTypes: AstTypes): AstTypes { +function copyTypePart(value: TypePart): TypePart { + return { + name: value.name, + children: [], + parents: [], + actionWithAssignment: value.actionWithAssignment, + ruleCalls: [...value.ruleCalls], + properties: value.properties.map(copyProperty), + }; +} + +function copyTypeAlternative(value: TypeAlternative): TypeAlternative { + return { + name: value.name, + super: value.super, + ruleCalls: value.ruleCalls, + properties: value.properties.map(e => copyProperty(e)) + }; +} + +function copyProperty(value: PlainProperty): PlainProperty { + return { + name: value.name, + optional: value.optional, + type: value.type, + astNodes: value.astNodes, + }; +} + +export function collectInferredTypes(parserRules: ParserRule[], datatypeRules: ParserRule[], declared: PlainAstTypes): PlainAstTypes { // extract interfaces and types from parser rules const allTypes: TypePath[] = []; const context: TypeCollectionContext = { @@ -197,18 +221,84 @@ export function collectInferredTypes(parserRules: ParserRule[], datatypeRules: P } const interfaces = calculateInterfaces(allTypes); const unions = buildSuperUnions(interfaces); - const astTypes = extractUnions(interfaces, unions, declaredTypes); + const astTypes = extractUnions(interfaces, unions, declared); // extract types from datatype rules for (const rule of datatypeRules) { - const types = isAlternatives(rule.definition) && rule.definition.elements.every(e => isKeyword(e)) ? - stream(rule.definition.elements).filter(isKeyword).map(e => `'${e.value}'`).toArray() : - [getExplicitRuleType(rule) ?? 'string']; - astTypes.unions.push(new UnionType(rule.name, toPropertyType(false, false, types))); + const type = getDataRuleType(rule); + astTypes.unions.push({ + name: rule.name, + reflection: false, + declared: false, + type, + subTypes: new Set(), + superTypes: new Set() + }); } return astTypes; } +function getDataRuleType(rule: ParserRule): PlainPropertyType { + if (rule.dataType && rule.dataType !== 'string') { + return { + primitive: rule.dataType + }; + } + let cancelled = false; + const cancel = (): PlainPropertyType => { + cancelled = true; + return { + primitive: 'unknown' + }; + }; + const type = buildDataRuleType(rule.definition, cancel); + if (cancelled) { + return { + primitive: 'string' + }; + } else { + return type; + } +} + +function buildDataRuleType(element: AbstractElement, cancel: () => PlainPropertyType): PlainPropertyType { + if (element.cardinality) { + // Multiplicity/optionality is not supported for types + return cancel(); + } + if (isAlternatives(element)) { + return { + types: element.elements.map(e => buildDataRuleType(e, cancel)) + }; + } else if (isGroup(element) || isUnorderedGroup(element)) { + if (element.elements.length !== 1) { + return cancel(); + } else { + return buildDataRuleType(element.elements[0], cancel); + } + } else if (isRuleCall(element)) { + const ref = element.rule?.ref; + if (ref) { + if (isTerminalRule(ref)) { + return { + primitive: ref.type?.name ?? 'string' + }; + } else { + return { + value: ref.name + }; + } + } else { + return cancel(); + } + } else if (isKeyword(element)) { + return { + string: element.value + }; + } + return cancel(); +} + function getRuleTypes(context: TypeCollectionContext, rule: ParserRule): TypePath[] { const type = newTypePart(rule); const graph = new TypeGraph(context, type); @@ -250,11 +340,14 @@ function collectElement(graph: TypeGraph, current: TypePart, element: AbstractEl return graph.merge(...children); } else if (isGroup(element) || isUnorderedGroup(element)) { let groupNode = graph.connect(current, newTypePart()); + let skipNode: TypePart | undefined; + if (optional) { + skipNode = graph.connect(current, newTypePart()); + } for (const item of element.elements) { groupNode = collectElement(graph, groupNode, item); } - if (optional) { - const skipNode = graph.connect(current, newTypePart()); + if (skipNode) { return graph.merge(skipNode, groupNode); } else { return groupNode; @@ -270,6 +363,16 @@ function collectElement(graph: TypeGraph, current: TypePart, element: AbstractEl } function addAction(graph: TypeGraph, parent: TypePart, action: Action): TypePart { + + // We create a copy of the current type part + // This is essentially a leaf node of the current type + // Otherwise we might lose information, such as properties + // We do this if there's no leaf node for the current type yet + if (!graph.hasLeafNode(parent)) { + const copy = copyTypePart(parent); + graph.connect(parent, copy); + } + const typeNode = graph.connect(parent, newTypePart(action)); if (action.type) { @@ -286,7 +389,7 @@ function addAction(graph: TypeGraph, parent: TypePart, action: Action): TypePart typeNode.properties.push({ name: action.feature, optional: false, - typeAlternatives: toPropertyType( + type: toPropertyType( action.operator === '+=', false, graph.root.ruleCalls.length !== 0 ? graph.root.ruleCalls : graph.getSuperTypes(typeNode)), @@ -300,7 +403,7 @@ function addAssignment(current: TypePart, assignment: Assignment): void { const typeItems: TypeCollection = { types: new Set(), reference: false }; findTypes(assignment.terminal, typeItems); - const typeAlternatives: PropertyType[] = toPropertyType( + const type: PlainPropertyType = toPropertyType( assignment.operator === '+=', typeItems.reference, assignment.operator === '?=' ? ['boolean'] : Array.from(typeItems.types) @@ -309,7 +412,7 @@ function addAssignment(current: TypePart, assignment: Assignment): void { current.properties.push({ name: assignment.feature, optional: isOptionalCardinality(assignment.cardinality), - typeAlternatives, + type, astNodes: new Set([assignment]) }); } @@ -324,7 +427,10 @@ function findTypes(terminal: AbstractElement, types: TypeCollection): void { } else if (isRuleCall(terminal) && terminal.rule.ref) { types.types.add(getRuleType(terminal.rule.ref)); } else if (isCrossReference(terminal) && terminal.type.ref) { - types.types.add(getTypeNameWithoutError(terminal.type.ref)); + const refTypeName = getTypeNameWithoutError(terminal.type.ref); + if (refTypeName) { + types.types.add(refTypeName); + } types.reference = true; } } @@ -347,12 +453,12 @@ function addRuleCall(graph: TypeGraph, current: TypePart, ruleCall: RuleCall): v } } -function getFragmentProperties(fragment: ParserRule, context: TypeCollectionContext): Property[] { +function getFragmentProperties(fragment: ParserRule, context: TypeCollectionContext): PlainProperty[] { const existing = context.fragments.get(fragment); if (existing) { return existing; } - const properties: Property[] = []; + const properties: PlainProperty[] = []; context.fragments.set(fragment, properties); const fragmentName = getTypeNameWithoutError(fragment); const typeAlternatives = getRuleTypes(context, fragment).filter(e => e.alt.name === fragmentName); @@ -366,13 +472,20 @@ function getFragmentProperties(fragment: ParserRule, context: TypeCollectionCont * @param alternatives The type branches that will be squashed in interfaces. * @returns Interfaces. */ -function calculateInterfaces(alternatives: TypePath[]): InterfaceType[] { - const interfaces = new Map(); +function calculateInterfaces(alternatives: TypePath[]): PlainInterface[] { + const interfaces = new Map(); const ruleCallAlternatives: TypeAlternative[] = []; const flattened = flattenTypes(alternatives).map(e => e.alt); for (const flat of flattened) { - const interfaceType = new InterfaceType(flat.name, flat.super, flat.properties); + const interfaceType: PlainInterface = { + name: flat.name, + properties: flat.properties, + superTypes: new Set(flat.super), + subTypes: new Set(), + declared: false, + abstract: false + }; interfaces.set(interfaceType.name, interfaceType); if (flat.ruleCalls.length > 0) { ruleCallAlternatives.push(flat); @@ -391,7 +504,7 @@ function calculateInterfaces(alternatives: TypePath[]): InterfaceType[] { const calledInterface = interfaces.get(ruleCall); if (calledInterface) { if (calledInterface.name !== ruleCallType.name) { - calledInterface.realSuperTypes.add(ruleCallType.name); + calledInterface.superTypes.add(ruleCallType.name); } } } @@ -404,7 +517,7 @@ function flattenTypes(alternatives: TypePath[]): TypePath[] { const types: TypePath[] = []; for (const [name, namedAlternatives] of nameToAlternatives.entriesGroupedByKey()) { - const properties: Property[] = []; + const properties: PlainProperty[] = []; const ruleCalls = new Set(); const type: TypePath = { alt: { name, properties, ruleCalls: [], super: [] }, next: [] }; for (const path of namedAlternatives) { @@ -415,9 +528,7 @@ function flattenTypes(alternatives: TypePath[]): TypePath[] { for (const altProperty of altProperties) { const existingProperty = properties.find(e => e.name === altProperty.name); if (existingProperty) { - altProperty.typeAlternatives - .filter(isNotInTypeAlternatives(existingProperty.typeAlternatives)) - .forEach(type => existingProperty.typeAlternatives.push(type)); + existingProperty.type = mergePropertyTypes(existingProperty.type, altProperty.type); altProperty.astNodes.forEach(e => existingProperty.astNodes.add(e)); } else { properties.push({ ...altProperty }); @@ -444,27 +555,26 @@ function flattenTypes(alternatives: TypePath[]): TypePath[] { return types; } -function isNotInTypeAlternatives(typeAlternatives: PropertyType[]): (type: PropertyType) => boolean { - return (type: PropertyType) => { - return !typeAlternatives.some(e => comparePropertyType(e, type)); - }; -} - -function buildSuperUnions(interfaces: InterfaceType[]): UnionType[] { - const unions: UnionType[] = []; +function buildSuperUnions(interfaces: PlainInterface[]): PlainUnion[] { + const interfaceMap = new Map(interfaces.map(e => [e.name, e])); + const unions: PlainUnion[] = []; const allSupertypes = new MultiMap(); for (const interfaceType of interfaces) { - for (const superType of interfaceType.realSuperTypes) { + for (const superType of interfaceType.superTypes) { allSupertypes.add(superType, interfaceType.name); } } for (const [superType, types] of allSupertypes.entriesGroupedByKey()) { - if (!interfaces.some(e => e.name === superType)) { - unions.push(new UnionType( - superType, - toPropertyType(false, false, types), - { reflection: true } - )); + if (!interfaceMap.has(superType)) { + const union: PlainUnion = { + declared: false, + name: superType, + reflection: true, + subTypes: new Set(), + superTypes: new Set(), + type: toPropertyType(false, false, types) + }; + unions.push(union); } } @@ -477,29 +587,45 @@ function buildSuperUnions(interfaces: InterfaceType[]): UnionType[] { * @param interfaces The interfaces that have to be transformed on demand. * @returns Types and not transformed interfaces. */ -function extractUnions(interfaces: InterfaceType[], unions: UnionType[], declaredTypes: AstTypes): AstTypes { +function extractUnions(interfaces: PlainInterface[], unions: PlainUnion[], declared: PlainAstTypes): { + interfaces: PlainInterface[], + unions: PlainUnion[] +} { + const subTypes = new MultiMap(); for (const interfaceType of interfaces) { - for (const superTypeName of interfaceType.realSuperTypes) { - interfaces.find(e => e.name === superTypeName) - ?.subTypes.add(interfaceType.name); + for (const superTypeName of interfaceType.superTypes) { + subTypes.add(superTypeName, interfaceType.name); } } - - const astTypes: AstTypes = { interfaces: [], unions }; - const typeNames = new Set(unions.map(e => e.name)); - const declaredInterfaces = new Set(declaredTypes.interfaces.map(e => e.name)); + const declaredInterfaces = new Set(declared.interfaces.map(e => e.name)); + const astTypes = { interfaces: [] as PlainInterface[], unions }; + const unionTypes = new Map(unions.map(e => [e.name, e])); for (const interfaceType of interfaces) { - // the criterion for converting an interface into a type - if (interfaceType.properties.length === 0 && interfaceType.subTypes.size > 0 && !declaredInterfaces.has(interfaceType.name)) { - const alternatives: PropertyType[] = toPropertyType(false, false, Array.from(interfaceType.subTypes)); - const existingUnion = unions.find(e => e.name === interfaceType.name); - if (existingUnion) { - existingUnion.alternatives.push(...alternatives); + const interfaceSubTypes = new Set(subTypes.get(interfaceType.name)); + // Convert an interface into a union type if it has subtypes and no properties on its own + if (interfaceType.properties.length === 0 && interfaceSubTypes.size > 0) { + // In case we have an explicitly declared interface + // Mark the interface as `abstract` and do not create a union type + if (declaredInterfaces.has(interfaceType.name)) { + interfaceType.abstract = true; + astTypes.interfaces.push(interfaceType); } else { - const type = new UnionType(interfaceType.name, alternatives, { reflection: true }); - type.realSuperTypes = interfaceType.realSuperTypes; - astTypes.unions.push(type); - typeNames.add(interfaceType.name); + const interfaceTypeValue = toPropertyType(false, false, Array.from(interfaceSubTypes)); + const existingUnion = unionTypes.get(interfaceType.name); + if (existingUnion) { + existingUnion.type = mergePropertyTypes(existingUnion.type, interfaceTypeValue); + } else { + const unionType: PlainUnion = { + name: interfaceType.name, + declared: false, + reflection: true, + subTypes: interfaceSubTypes, + superTypes: interfaceType.superTypes, + type: interfaceTypeValue + }; + astTypes.unions.push(unionType); + unionTypes.set(interfaceType.name, unionType); + } } } else { astTypes.interfaces.push(interfaceType); @@ -507,16 +633,39 @@ function extractUnions(interfaces: InterfaceType[], unions: UnionType[], declare } // After converting some interfaces into union types, these interfaces are no longer valid super types for (const interfaceType of astTypes.interfaces) { - interfaceType.printingSuperTypes = [...interfaceType.realSuperTypes].filter(superType => !typeNames.has(superType)); + interfaceType.superTypes = new Set([...interfaceType.superTypes].filter(superType => !unionTypes.has(superType))); } return astTypes; } -function toPropertyType(array: boolean, reference: boolean, types: string[]): PropertyType[] { - if (array || reference) { - return [{ array, reference, types }]; +function toPropertyType(array: boolean, reference: boolean, types: string[]): PlainPropertyType { + if (array) { + return { + elementType: toPropertyType(false, reference, types) + }; + } else if (reference) { + return { + referenceType: toPropertyType(false, false, types) + }; + } else if (types.length === 1) { + const type = types[0]; + if (type.startsWith("'")) { + return { + string: type.substring(1, type.length - 1) + }; + } + if (isPrimitiveType(type)) { + return { + primitive: type + }; + } else { + return { + value: type + }; + } + } else { + return { + types: types.map(e => toPropertyType(false, false, [e])) + }; } - return types.map(type => { return { - array, reference, types: [type] - }; }); } diff --git a/packages/langium/src/grammar/type-system/type-collector/plain-types.ts b/packages/langium/src/grammar/type-system/type-collector/plain-types.ts new file mode 100644 index 000000000..a01d09df6 --- /dev/null +++ b/packages/langium/src/grammar/type-system/type-collector/plain-types.ts @@ -0,0 +1,236 @@ +/****************************************************************************** + * Copyright 2022 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 { Action, Assignment, TypeAttribute } from '../../generated/ast'; +import { AstTypes, InterfaceType, Property, PropertyType, UnionType } from './types'; + +export interface PlainAstTypes { + interfaces: PlainInterface[]; + unions: PlainUnion[]; +} + +export type PlainType = PlainInterface | PlainUnion; + +export interface PlainInterface { + name: string; + superTypes: Set; + subTypes: Set; + properties: PlainProperty[]; + declared: boolean; + abstract: boolean; +} + +export function isPlainInterface(type: PlainType): type is PlainInterface { + return !isPlainUnion(type); +} + +export interface PlainUnion { + name: string; + superTypes: Set; + subTypes: Set; + type: PlainPropertyType; + reflection: boolean; + declared: boolean; +} + +export function isPlainUnion(type: PlainType): type is PlainUnion { + return 'type' in type; +} + +export interface PlainProperty { + name: string; + optional: boolean; + astNodes: Set; + type: PlainPropertyType; +} + +export type PlainPropertyType = + | PlainReferenceType + | PlainArrayType + | PlainPropertyUnion + | PlainValueType + | PlainPrimitiveType + | PlainStringType; + +export interface PlainReferenceType { + referenceType: PlainPropertyType; +} + +export function isPlainReferenceType(propertyType: PlainPropertyType): propertyType is PlainReferenceType { + return 'referenceType' in propertyType; +} + +export interface PlainArrayType { + elementType: PlainPropertyType; +} + +export function isPlainArrayType(propertyType: PlainPropertyType): propertyType is PlainArrayType { + return 'elementType' in propertyType; +} + +export interface PlainPropertyUnion { + types: PlainPropertyType[]; +} + +export function isPlainPropertyUnion(propertyType: PlainPropertyType): propertyType is PlainPropertyUnion { + return 'types' in propertyType; +} + +export interface PlainValueType { + value: string; +} + +export function isPlainValueType(propertyType: PlainPropertyType): propertyType is PlainValueType { + return 'value' in propertyType; +} + +export interface PlainPrimitiveType { + primitive: string; +} + +export function isPlainPrimitiveType(propertyType: PlainPropertyType): propertyType is PlainPrimitiveType { + return 'primitive' in propertyType; +} + +export interface PlainStringType { + string: string; +} + +export function isPlainStringType(propertyType: PlainPropertyType): propertyType is PlainStringType { + return 'string' in propertyType; +} + +export function plainToTypes(plain: PlainAstTypes): AstTypes { + const interfaceTypes = new Map(); + const unionTypes = new Map(); + for (const interfaceValue of plain.interfaces) { + const type = new InterfaceType(interfaceValue.name, interfaceValue.declared, interfaceValue.abstract); + interfaceTypes.set(interfaceValue.name, type); + } + for (const unionValue of plain.unions) { + const type = new UnionType(unionValue.name, { + reflection: unionValue.reflection, + declared: unionValue.declared + }); + unionTypes.set(unionValue.name, type); + } + for (const interfaceValue of plain.interfaces) { + const type = interfaceTypes.get(interfaceValue.name)!; + for (const superTypeName of interfaceValue.superTypes) { + const superType = interfaceTypes.get(superTypeName) || unionTypes.get(superTypeName); + if (superType) { + type.superTypes.add(superType); + } + } + for (const subTypeName of interfaceValue.subTypes) { + const subType = interfaceTypes.get(subTypeName) || unionTypes.get(subTypeName); + if (subType) { + type.subTypes.add(subType); + } + } + for (const property of interfaceValue.properties) { + const prop = plainToProperty(property, interfaceTypes, unionTypes); + type.properties.push(prop); + } + } + for (const unionValue of plain.unions) { + const type = unionTypes.get(unionValue.name)!; + type.type = plainToPropertyType(unionValue.type, type, interfaceTypes, unionTypes); + } + return { + interfaces: Array.from(interfaceTypes.values()), + unions: Array.from(unionTypes.values()) + }; +} + +function plainToProperty(property: PlainProperty, interfaces: Map, unions: Map): Property { + return { + name: property.name, + optional: property.optional, + astNodes: property.astNodes, + type: plainToPropertyType(property.type, undefined, interfaces, unions) + }; +} + +function plainToPropertyType(type: PlainPropertyType, union: UnionType | undefined, interfaces: Map, unions: Map): PropertyType { + if (isPlainArrayType(type)) { + return { + elementType: plainToPropertyType(type.elementType, union, interfaces, unions) + }; + } else if (isPlainReferenceType(type)) { + return { + referenceType: plainToPropertyType(type.referenceType, undefined, interfaces, unions) + }; + } else if (isPlainPropertyUnion(type)) { + return { + types: type.types.map(e => plainToPropertyType(e, union, interfaces, unions)) + }; + } else if (isPlainStringType(type)) { + return { + string: type.string + }; + } else if (isPlainPrimitiveType(type)) { + return { + primitive: type.primitive + }; + } else if (isPlainValueType(type)) { + const value = interfaces.get(type.value) || unions.get(type.value); + if (!value) { + return { + primitive: 'unknown' + }; + } + if (union) { + union.subTypes.add(value); + } + return { + value + }; + } else { + throw new Error('Invalid property type'); + } +} + +export function mergePropertyTypes(first: PlainPropertyType, second: PlainPropertyType): PlainPropertyType { + const flattenedFirst = flattenPlainType(first); + const flattenedSecond = flattenPlainType(second); + for (const second of flattenedSecond) { + if (!includesType(flattenedFirst, second)) { + flattenedFirst.push(second); + } + } + if (flattenedFirst.length === 1) { + return flattenedFirst[0]; + } else { + return { + types: flattenedFirst + }; + } +} + +function includesType(list: PlainPropertyType[], value: PlainPropertyType): boolean { + return list.some(e => typeEquals(e, value)); +} + +function typeEquals(first: PlainPropertyType, second: PlainPropertyType): boolean { + if (isPlainArrayType(first) && isPlainArrayType(second)) { + return typeEquals(first.elementType, second.elementType); + } else if (isPlainReferenceType(first) && isPlainReferenceType(second)) { + return typeEquals(first.referenceType, second.referenceType); + } else if (isPlainValueType(first) && isPlainValueType(second)) { + return first.value === second.value; + } else { + return false; + } +} + +export function flattenPlainType(type: PlainPropertyType): PlainPropertyType[] { + if (isPlainPropertyUnion(type)) { + return type.types.flatMap(e => flattenPlainType(e)); + } else { + return [type]; + } +} \ No newline at end of file diff --git a/packages/langium/src/grammar/type-system/type-collector/types.ts b/packages/langium/src/grammar/type-system/type-collector/types.ts index e9ffd6b07..59fb6e561 100644 --- a/packages/langium/src/grammar/type-system/type-collector/types.ts +++ b/packages/langium/src/grammar/type-system/type-collector/types.ts @@ -5,22 +5,84 @@ ******************************************************************************/ import { CompositeGeneratorNode, NL, toString } from '../../../generator/generator-node'; +import { processGeneratorNode } from '../../../generator/node-processor'; import { CstNode } from '../../../syntax-tree'; -import { MultiMap } from '../../../utils/collections'; import { Assignment, Action, TypeAttribute } from '../../generated/ast'; import { distinctAndSorted } from '../types-util'; export type Property = { name: string, optional: boolean, - typeAlternatives: PropertyType[], + type: PropertyType, astNodes: Set, } -export type PropertyType = { - types: string[], - reference: boolean, - array: boolean, +export type PropertyType = + | ReferenceType + | ArrayType + | PropertyUnion + | ValueType + | PrimitiveType + | StringType; + +export interface ReferenceType { + referenceType: PropertyType +} + +export function isReferenceType(propertyType: PropertyType): propertyType is ReferenceType { + return 'referenceType' in propertyType; +} + +export interface ArrayType { + elementType: PropertyType +} + +export function isArrayType(propertyType: PropertyType): propertyType is ArrayType { + return 'elementType' in propertyType; +} + +export interface PropertyUnion { + types: PropertyType[] +} + +export function isPropertyUnion(propertyType: PropertyType): propertyType is PropertyUnion { + return 'types' in propertyType; +} + +export function flattenPropertyUnion(propertyType: PropertyType): PropertyType[] { + if (isPropertyUnion(propertyType)) { + const items: PropertyType[] = []; + for (const type of propertyType.types) { + items.push(...flattenPropertyUnion(type)); + } + return items; + } else { + return [propertyType]; + } +} + +export interface ValueType { + value: TypeOption +} + +export function isValueType(propertyType: PropertyType): propertyType is ValueType { + return 'value' in propertyType; +} + +export interface PrimitiveType { + primitive: string +} + +export function isPrimitiveType(propertyType: PropertyType): propertyType is PrimitiveType { + return 'primitive' in propertyType; +} + +export interface StringType { + string: string +} + +export function isStringType(propertyType: PropertyType): propertyType is StringType { + return 'string' in propertyType; } export type AstTypes = { @@ -29,7 +91,7 @@ export type AstTypes = { } export function isUnionType(type: TypeOption): type is UnionType { - return type && 'alternatives' in type; + return type && 'type' in type; } export function isInterfaceType(type: TypeOption): type is InterfaceType { @@ -40,25 +102,28 @@ export type TypeOption = InterfaceType | UnionType; export class UnionType { name: string; - realSuperTypes = new Set(); - subTypes = new Set(); - containerTypes = new Set(); - typeTypes = new Set(); - - alternatives: PropertyType[]; + type: PropertyType; + superTypes = new Set(); + subTypes = new Set(); + containerTypes = new Set(); + typeNames = new Set(); reflection: boolean; + declared: boolean; - constructor(name: string, alts: PropertyType[], options?: { reflection: boolean }) { + constructor(name: string, options?: { + reflection: boolean + declared: boolean + }) { this.name = name; - this.alternatives = alts; this.reflection = options?.reflection ?? false; + this.declared = options?.declared ?? false; } - toAstTypesString(): string { + toAstTypesString(reflectionInfo: boolean): string { const unionNode = new CompositeGeneratorNode(); - unionNode.append(`export type ${this.name} = ${propertyTypesToString(this.alternatives, 'AstType')};`, NL); + unionNode.append(`export type ${this.name} = ${propertyTypeToString(this.type, 'AstType')};`, NL); - if (this.reflection) { + if (this.reflection && reflectionInfo) { unionNode.append(NL); pushReflectionInfo(unionNode, this.name); } @@ -67,49 +132,92 @@ export class UnionType { toDeclaredTypesString(reservedWords: Set): string { const unionNode = new CompositeGeneratorNode(); - unionNode.append(`type ${escapeReservedWords(this.name, reservedWords)} = ${propertyTypesToString(this.alternatives, 'DeclaredType')};`, NL); - return toString(unionNode); + unionNode.append(`type ${escapeReservedWords(this.name, reservedWords)} = ${propertyTypeToString(this.type, 'DeclaredType')};`, NL); + return processGeneratorNode(unionNode); } } export class InterfaceType { name: string; - realSuperTypes = new Set(); - subTypes = new Set(); - containerTypes = new Set(); - typeTypes = new Set(); + superTypes = new Set(); + subTypes = new Set(); + containerTypes = new Set(); + typeNames = new Set(); + declared = false; + abstract = false; + + properties: Property[] = []; + + get superProperties(): Property[] { + const map = new Map(); + for (const property of this.properties) { + map.set(property.name, property); + } + for (const superType of this.interfaceSuperTypes) { + const allSuperProperties = superType.superProperties; + for (const superProp of allSuperProperties) { + if (!map.has(superProp.name)) { + map.set(superProp.name, superProp); + } + } + } + return Array.from(map.values()); + } + + get allProperties(): Property[] { + const map = new Map(this.superProperties.map(e => [e.name, e])); + for (const subType of this.subTypes) { + this.getSubTypeProperties(subType, map); + } + const superProps = Array.from(map.values()); + return superProps; + } + + private getSubTypeProperties(type: TypeOption, map: Map): void { + const props = isInterfaceType(type) ? type.properties : []; + for (const prop of props) { + if (!map.has(prop.name)) { + map.set(prop.name, prop); + } + } + for (const subType of type.subTypes) { + this.getSubTypeProperties(subType, map); + } + } - printingSuperTypes: string[] = []; - properties: Property[]; - superProperties: MultiMap = new MultiMap(); + get interfaceSuperTypes(): InterfaceType[] { + return Array.from(this.superTypes).filter((e): e is InterfaceType => e instanceof InterfaceType); + } - constructor(name: string, superTypes: string[], properties: Property[]) { + constructor(name: string, declared: boolean, abstract: boolean) { this.name = name; - this.realSuperTypes = new Set(superTypes); - this.printingSuperTypes = [...superTypes]; - this.properties = properties; - properties.forEach(prop => this.superProperties.add(prop.name, prop)); + this.declared = declared; + this.abstract = abstract; } - toAstTypesString(): string { + toAstTypesString(reflectionInfo: boolean): string { const interfaceNode = new CompositeGeneratorNode(); - const superTypes = this.printingSuperTypes.length > 0 ? distinctAndSorted([...this.printingSuperTypes]) : ['AstNode']; + const interfaceSuperTypes = this.interfaceSuperTypes.map(e => e.name); + const superTypes = interfaceSuperTypes.length > 0 ? distinctAndSorted([...interfaceSuperTypes]) : ['AstNode']; interfaceNode.append(`export interface ${this.name} extends ${superTypes.join(', ')} {`, NL); interfaceNode.indent(body => { if (this.containerTypes.size > 0) { - body.append(`readonly $container: ${distinctAndSorted([...this.containerTypes]).join(' | ')};`, NL); + body.append(`readonly $container: ${distinctAndSorted([...this.containerTypes].map(e => e.name)).join(' | ')};`, NL); } - if (this.typeTypes.size > 0) { - body.append(`readonly $type: ${distinctAndSorted([...this.typeTypes]).map(e => `'${e}'`).join(' | ')};`, NL); + if (this.typeNames.size > 0) { + body.append(`readonly $type: ${distinctAndSorted([...this.typeNames]).map(e => `'${e}'`).join(' | ')};`, NL); } pushProperties(body, this.properties, 'AstType'); }); interfaceNode.append('}', NL); - interfaceNode.append(NL); - pushReflectionInfo(interfaceNode, this.name); + if (reflectionInfo) { + interfaceNode.append(NL); + pushReflectionInfo(interfaceNode, this.name); + } + return toString(interfaceNode); } @@ -117,7 +225,7 @@ export class InterfaceType { const interfaceNode = new CompositeGeneratorNode(); const name = escapeReservedWords(this.name, reservedWords); - const superTypes = Array.from(this.printingSuperTypes).join(', '); + const superTypes = distinctAndSorted(this.interfaceSuperTypes.map(e => e.name)).join(', '); interfaceNode.append(`interface ${name}${superTypes.length > 0 ? ` extends ${superTypes}` : ''} {`, NL); interfaceNode.indent(body => pushProperties(body, this.properties, 'DeclaredType', reservedWords)); @@ -138,26 +246,90 @@ export class TypeResolutionError extends Error { } -export function propertyTypesToString(alternatives: PropertyType[], mode: 'AstType' | 'DeclaredType'='AstType'): string { - function propertyTypeToString(propertyType: PropertyType): string { - let res = distinctAndSorted(propertyType.types).join(' | '); - res = propertyType.reference ? (mode === 'AstType' ? `Reference<${res}>` : `@${res}`) : res; - res = propertyType.array ? (mode === 'AstType' ? `Array<${res}>` : `${res}[]`) : res; - return res; +export function isTypeAssignable(from: PropertyType, to: PropertyType): boolean { + if (isPropertyUnion(from)) { + return from.types.every(fromType => isTypeAssignable(fromType, to)); + } else if (isPropertyUnion(to)) { + return to.types.some(toType => isTypeAssignable(from, toType)); + } else if (isReferenceType(from)) { + return isReferenceType(to) && isTypeAssignable(from.referenceType, to.referenceType); + } else if (isArrayType(from)) { + return isArrayType(to) && isTypeAssignable(from.elementType, to.elementType); + } else if (isValueType(from)) { + if (isUnionType(from.value)) { + if (isValueType(to) && to.value.name === from.value.name) { + return true; + } + return isTypeAssignable(from.value.type, to); + } + if (!isValueType(to)) { + return false; + } + if (isUnionType(to.value)) { + return isTypeAssignable(from, to.value.type); + } else { + return isInterfaceAssignable(from.value, to.value); + } + } else if (isPrimitiveType(from)) { + return isPrimitiveType(to) && from.primitive === to.primitive; + } + else if (isStringType(from)) { + return (isPrimitiveType(to) && to.primitive === 'string') || (isStringType(to) && to.string === from.string); } + return false; +} - return distinctAndSorted(alternatives.map(propertyTypeToString)).join(' | '); +function isInterfaceAssignable(from: InterfaceType, to: InterfaceType): boolean { + if (from.name === to.name) { + return true; + } + for (const superType of from.superTypes) { + if (isInterfaceType(superType) && isInterfaceAssignable(superType, to)) { + return true; + } + } + return false; } -function pushProperties(node: CompositeGeneratorNode, properties: Property[], - mode: 'AstType' | 'DeclaredType', reserved = new Set()) { +export function propertyTypeToString(type: PropertyType, mode: 'AstType' | 'DeclaredType' = 'AstType'): string { + if (isReferenceType(type)) { + const refType = propertyTypeToString(type.referenceType, mode); + return mode === 'AstType' ? `Reference<${refType}>` : `@${typeParenthesis(type.referenceType, refType)}`; + } else if (isArrayType(type)) { + const arrayType = propertyTypeToString(type.elementType, mode); + return mode === 'AstType' ? `Array<${arrayType}>` : `${typeParenthesis(type.elementType, arrayType)}[]`; + } else if (isPropertyUnion(type)) { + const types = type.types.map(e => typeParenthesis(e, propertyTypeToString(e, mode))); + return distinctAndSorted(types).join(' | '); + } else if (isValueType(type)) { + return type.value.name; + } else if (isPrimitiveType(type)) { + return type.primitive; + } else if (isStringType(type)) { + return `'${type.string}'`; + } + throw new Error('Invalid type'); +} + +function typeParenthesis(type: PropertyType, name: string): string { + const needsParenthesis = isPropertyUnion(type); + if (needsParenthesis) { + name = `(${name})`; + } + return name; +} + +function pushProperties( + node: CompositeGeneratorNode, + properties: Property[], + mode: 'AstType' | 'DeclaredType', + reserved = new Set() +) { function propertyToString(property: Property): string { const name = mode === 'AstType' ? property.name : escapeReservedWords(property.name, reserved); - const optional = property.optional && - !property.typeAlternatives.some(e => e.array) && - !property.typeAlternatives.every(e => e.types.length === 1 && e.types[0] === 'boolean'); - const propType = propertyTypesToString(property.typeAlternatives, mode); + const optional = property.optional && !isMandatoryPropertyType(property.type); + const propType = propertyTypeToString(property.type, mode); return `${name}${optional ? '?' : ''}: ${propType}`; } @@ -165,6 +337,21 @@ function pushProperties(node: CompositeGeneratorNode, properties: Property[], .forEach(property => node.append(propertyToString(property), NL)); } +function isMandatoryPropertyType(propertyType: PropertyType): boolean { + if (isArrayType(propertyType)) { + return true; + } else if (isReferenceType(propertyType)) { + return false; + } else if (isPropertyUnion(propertyType)) { + return propertyType.types.every(e => isMandatoryPropertyType(e)); + } else if (isPrimitiveType(propertyType)) { + const value = propertyType.primitive; + return value === 'boolean'; + } else { + return false; + } +} + function pushReflectionInfo(node: CompositeGeneratorNode, name: string) { node.append(`export const ${name} = '${name}';`, NL); node.append(NL); diff --git a/packages/langium/src/grammar/type-system/types-util.ts b/packages/langium/src/grammar/type-system/types-util.ts index 9683f23e5..6bc8a018a 100644 --- a/packages/langium/src/grammar/type-system/types-util.ts +++ b/packages/langium/src/grammar/type-system/types-util.ts @@ -8,20 +8,21 @@ import { References } from '../../references/references'; import { MultiMap } from '../../utils/collections'; import { AstNodeLocator } from '../../workspace/ast-node-locator'; import { LangiumDocuments } from '../../workspace/documents'; -import { Interface, Type, AbstractType, isInterface, isType } from '../generated/ast'; -import { AstTypes, InterfaceType, Property, PropertyType, TypeOption } from './type-collector/types'; +import { Interface, Type, AbstractType, isInterface, isType, TypeDefinition, isUnionType, isSimpleType } from '../generated/ast'; +import { PlainInterface, PlainProperty } from './type-collector/plain-types'; +import { AstTypes, InterfaceType, isArrayType, isPrimitiveType, isPropertyUnion, isReferenceType, isValueType, PropertyType, TypeOption } from './type-collector/types'; /** * Collects all properties of all interface types. Includes super type properties. * @param interfaces A topologically sorted array of interfaces. */ -export function collectAllProperties(interfaces: InterfaceType[]): MultiMap { - const map = new MultiMap(); +export function collectAllPlainProperties(interfaces: PlainInterface[]): MultiMap { + const map = new MultiMap(); for (const interfaceType of interfaces) { map.addAll(interfaceType.name, interfaceType.properties); } for (const interfaceType of interfaces) { - for (const superType of interfaceType.printingSuperTypes) { + for (const superType of interfaceType.superTypes) { const superTypeProperties = map.get(superType); if (superTypeProperties) { map.addAll(interfaceType.name, superTypeProperties); @@ -53,6 +54,37 @@ export function collectChildrenTypes(interfaceNode: Interface, references: Refer return childrenTypes; } +export function collectTypeHierarchy(types: TypeOption[]): { + superTypes: MultiMap + subTypes: MultiMap +} { + const duplicateSuperTypes = new MultiMap(); + const duplicateSubTypes = new MultiMap(); + for (const type of types) { + for (const superType of type.superTypes) { + duplicateSuperTypes.add(type.name, superType.name); + duplicateSubTypes.add(superType.name, type.name); + } + for (const subType of type.subTypes) { + duplicateSuperTypes.add(subType.name, type.name); + duplicateSubTypes.add(type.name, subType.name); + } + } + const superTypes = new MultiMap(); + const subTypes = new MultiMap(); + // Deduplicate and sort + for (const [name, superTypeList] of Array.from(duplicateSuperTypes.entriesGroupedByKey()).sort(([aName], [bName]) => aName.localeCompare(bName))) { + superTypes.addAll(name, Array.from(new Set(superTypeList))); + } + for (const [name, subTypeList] of Array.from(duplicateSubTypes.entriesGroupedByKey()).sort(([aName], [bName]) => aName.localeCompare(bName))) { + subTypes.addAll(name, Array.from(new Set(subTypeList))); + } + return { + superTypes, + subTypes + }; +} + export function collectSuperTypes(ruleNode: AbstractType): Set { const superTypes = new Set(); if (isInterface(ruleNode)) { @@ -67,31 +99,27 @@ export function collectSuperTypes(ruleNode: AbstractType): Set { } }); } else if (isType(ruleNode)) { - ruleNode.typeAlternatives.forEach(typeAlternative => { - if (typeAlternative.refType?.ref) { - if (isInterface(typeAlternative.refType.ref) || isType(typeAlternative.refType.ref)) { - const collectedSuperTypes = collectSuperTypes(typeAlternative.refType.ref); - for (const superType of collectedSuperTypes) { - superTypes.add(superType); - } - } + const usedTypes = collectUsedTypes(ruleNode.type); + for (const usedType of usedTypes) { + const collectedSuperTypes = collectSuperTypes(usedType); + for (const superType of collectedSuperTypes) { + superTypes.add(superType); } - }); + } } return superTypes; } -export function comparePropertyType(a: PropertyType, b: PropertyType): boolean { - return a.array === b.array && - a.reference === b.reference && - compareLists(a.types, b.types); -} - -function compareLists(a: T[], b: T[], eq: (x: T, y: T) => boolean = (x, y) => x === y): boolean { - const distinctAndSortedA = distinctAndSorted(a); - const distinctAndSortedB = distinctAndSorted(b); - if (distinctAndSortedA.length !== distinctAndSortedB.length) return false; - return distinctAndSortedB.every((e, i) => eq(e, distinctAndSortedA[i])); +function collectUsedTypes(typeDefinition: TypeDefinition): Array { + if (isUnionType(typeDefinition)) { + return typeDefinition.types.flatMap(e => collectUsedTypes(e)); + } else if (isSimpleType(typeDefinition)) { + const value = typeDefinition.typeRef?.ref; + if (isType(value) || isInterface(value)) { + return [value]; + } + } + return []; } export function mergeInterfaces(inferred: AstTypes, declared: AstTypes): InterfaceType[] { @@ -107,9 +135,9 @@ export function mergeTypesAndInterfaces(astTypes: AstTypes): TypeOption[] { * @param interfaces The interfaces to sort topologically. * @returns A topologically sorted set of interfaces. */ -export function sortInterfacesTopologically(interfaces: InterfaceType[]): InterfaceType[] { +export function sortInterfacesTopologically(interfaces: PlainInterface[]): PlainInterface[] { type TypeNode = { - value: InterfaceType; + value: PlainInterface; nodes: TypeNode[]; } @@ -117,12 +145,11 @@ export function sortInterfacesTopologically(interfaces: InterfaceType[]): Interf .sort((a, b) => a.name.localeCompare(b.name)) .map(e => { value: e, nodes: [] }); for (const node of nodes) { - node.nodes = nodes.filter(e => node.value.realSuperTypes.has(e.value.name)); + node.nodes = nodes.filter(e => node.value.superTypes.has(e.value.name)); } const l: TypeNode[] = []; const s = nodes.filter(e => e.nodes.length === 0); while (s.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const n = s.shift()!; if (!l.includes(n)) { l.push(n); @@ -134,11 +161,36 @@ export function sortInterfacesTopologically(interfaces: InterfaceType[]): Interf return l.map(e => e.value); } -export function addSubTypes(nameToType: Map) { - for (const interfaceType of nameToType.values()) { - for (const superTypeName of interfaceType.realSuperTypes) { - nameToType.get(superTypeName) - ?.subTypes.add(interfaceType.name); +export function hasArrayType(type: PropertyType): boolean { + if (isPropertyUnion(type)) { + return type.types.some(e => hasArrayType(e)); + } else if (isArrayType(type)) { + return true; + } else { + return false; + } +} + +export function hasBooleanType(type: PropertyType): boolean { + if (isPropertyUnion(type)) { + return type.types.some(e => hasBooleanType(e)); + } else if (isPrimitiveType(type)) { + return type.primitive === 'boolean'; + } else { + return false; + } +} + +export function findReferenceTypes(type: PropertyType): string[] { + if (isPropertyUnion(type)) { + return type.types.flatMap(e => findReferenceTypes(e)); + } else if (isReferenceType(type)) { + const refType = type.referenceType; + if (isValueType(refType)) { + return [refType.value.name]; } + } else if (isArrayType(type)) { + return findReferenceTypes(type.elementType); } + return []; } diff --git a/packages/langium/src/grammar/validation/types-validator.ts b/packages/langium/src/grammar/validation/types-validator.ts index f6f51ffa6..4c906e356 100644 --- a/packages/langium/src/grammar/validation/types-validator.ts +++ b/packages/langium/src/grammar/validation/types-validator.ts @@ -4,14 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import * as ast from '../generated/ast'; import { MultiMap } from '../../utils/collections'; import { DiagnosticInfo, ValidationAcceptor, ValidationChecks } from '../../validation/validation-registry'; -import * as ast from '../generated/ast'; import { extractAssignments } from '../internal-grammar-util'; import { LangiumGrammarServices } from '../langium-grammar-module'; -import { InterfaceType, isInterfaceType, isUnionType, Property, PropertyType, propertyTypesToString } from '../type-system/type-collector/types'; -import { distinctAndSorted } from '../type-system/types-util'; -import { DeclaredInfo, InferredInfo, isDeclared, isInferred, isInferredAndDeclared, LangiumGrammarDocument, TypeToValidationInfo } from '../workspace/documents'; +import { flattenPropertyUnion, InterfaceType, isInterfaceType, isReferenceType, isTypeAssignable, isUnionType, Property, PropertyType, propertyTypeToString } from '../type-system/type-collector/types'; +import { DeclaredInfo, InferredInfo, isDeclared, isInferred, isInferredAndDeclared, LangiumGrammarDocument } from '../workspace/documents'; export function registerTypeValidationChecks(services: LangiumGrammarServices): void { const registry = services.validation.ValidationRegistry; @@ -36,8 +35,8 @@ export class LangiumGrammarTypesValidator { for (const typeInfo of validationResources.typeToValidationInfo.values()) { if (isDeclared(typeInfo) && isInterfaceType(typeInfo.declared) && ast.isInterface(typeInfo.declaredNode)) { const declInterface = typeInfo as { declared: InterfaceType, declaredNode: ast.Interface }; - validateInterfaceSuperTypes(declInterface, validationResources.typeToValidationInfo, accept); - validateSuperTypesConsistency(declInterface, validationResources.typeToSuperProperties, accept); + validateInterfaceSuperTypes(declInterface, accept); + validateSuperTypesConsistency(declInterface, accept); } } } @@ -51,7 +50,7 @@ export class LangiumGrammarTypesValidator { validateInferredInterface(typeInfo.inferred as InterfaceType, accept); } if (isInferredAndDeclared(typeInfo)) { - validateDeclaredAndInferredConsistency(typeInfo, validationResources.typeToAliases, accept); + validateDeclaredAndInferredConsistency(typeInfo, accept); } } } @@ -67,17 +66,20 @@ export class LangiumGrammarTypesValidator { /////////////////////////////////////////////////////////////////////////////// function validateInferredInterface(inferredInterface: InterfaceType, accept: ValidationAcceptor): void { - inferredInterface.properties.filter(prop => prop.typeAlternatives.length > 1).forEach(prop => { - const typeKind = (type: PropertyType) => type.reference ? 'ref' : 'other'; - const firstKind = typeKind(prop.typeAlternatives[0]); - if (prop.typeAlternatives.slice(1).some(type => typeKind(type) !== firstKind)) { - const targetNode = prop.astNodes.values().next().value; - if(targetNode) { - accept( - 'error', - `Mixing a cross-reference with other types is not supported. Consider splitting property "${prop.name}" into two or more different properties.`, - { node: targetNode } - ); + inferredInterface.properties.forEach(prop => { + const flattened = flattenPropertyUnion(prop.type); + if (flattened.length > 1) { + const typeKind = (type: PropertyType) => isReferenceType(type) ? 'ref' : 'other'; + const firstKind = typeKind(flattened[0]); + if (flattened.slice(1).some(type => typeKind(type) !== firstKind)) { + const targetNode = prop.astNodes.values().next()?.value; + if(targetNode) { + accept( + 'error', + `Mixing a cross-reference with other types is not supported. Consider splitting property "${prop.name}" into two or more different properties.`, + { node: targetNode} + ); + } } } }); @@ -85,16 +87,14 @@ function validateInferredInterface(inferredInterface: InterfaceType, accept: Val function validateInterfaceSuperTypes( { declared, declaredNode }: { declared: InterfaceType, declaredNode: ast.Interface }, - validationInfo: TypeToValidationInfo, accept: ValidationAcceptor): void { - declared.printingSuperTypes.forEach((superTypeName, i) => { - const superType = validationInfo.get(superTypeName); + Array.from(declared.superTypes).forEach((superType, i) => { if (superType) { - if (isInferred(superType) && isUnionType(superType.inferred) || isDeclared(superType) && isUnionType(superType.declared)) { + if (isUnionType(superType)) { accept('error', 'Interfaces cannot extend union types.', { node: declaredNode, property: 'superTypes', index: i }); } - if (isInferred(superType) && !isDeclared(superType)) { + if (!superType.declared) { accept('error', 'Extending an inferred type is discouraged.', { node: declaredNode, property: 'superTypes', index: i }); } } @@ -102,8 +102,7 @@ function validateInterfaceSuperTypes( } function validateSuperTypesConsistency( - { declared, declaredNode }: { declared: InterfaceType, declaredNode: ast.Interface}, - properties: Map, + { declared, declaredNode }: { declared: InterfaceType, declaredNode: ast.Interface }, accept: ValidationAcceptor): void { const nameToProp = declared.properties.reduce((acc, e) => acc.add(e.name, e), new MultiMap()); @@ -118,13 +117,13 @@ function validateSuperTypesConsistency( } } - const allSuperTypes = declared.printingSuperTypes; + const allSuperTypes = Array.from(declared.superTypes); for (let i = 0; i < allSuperTypes.length; i++) { for (let j = i + 1; j < allSuperTypes.length; j++) { const outerType = allSuperTypes[i]; const innerType = allSuperTypes[j]; - const outerProps = properties.get(outerType) ?? []; - const innerProps = properties.get(innerType) ?? []; + const outerProps = isInterfaceType(outerType) ? outerType.superProperties : []; + const innerProps = isInterfaceType(innerType) ? innerType.superProperties : []; const nonIdentical = getNonIdenticalProps(outerProps, innerProps); if (nonIdentical.length > 0) { accept('error', `Cannot simultaneously inherit from '${outerType}' and '${innerType}'. Their ${nonIdentical.map(e => "'" + e + "'").join(', ')} properties are not identical.`, { @@ -136,15 +135,14 @@ function validateSuperTypesConsistency( } const allSuperProps = new Set(); for (const superType of allSuperTypes) { - const props = properties.get(superType) ?? []; + const props = isInterfaceType(superType) ? superType.superProperties : []; for (const prop of props) { allSuperProps.add(prop.name); } } for (const ownProp of declared.properties) { if (allSuperProps.has(ownProp.name)) { - const interfaceNode = declaredNode as ast.Interface; - const propNode = interfaceNode.attributes.find(e => e.name === ownProp.name); + const propNode = declaredNode.attributes.find(e => e.name === ownProp.name); if (propNode) { accept('error', `Cannot redeclare property '${ownProp.name}'. It is already inherited from another interface.`, { node: propNode, @@ -167,28 +165,17 @@ function getNonIdenticalProps(a: readonly Property[], b: readonly Property[]): s } function arePropTypesIdentical(a: Property, b: Property): boolean { - if (a.optional !== b.optional || a.typeAlternatives.length !== b.typeAlternatives.length) { - return false; - } - for (const firstTypes of a.typeAlternatives) { - const found = b.typeAlternatives.some(otherTypes => { - return otherTypes.array === firstTypes.array - && otherTypes.reference === firstTypes.reference - && otherTypes.types.length === firstTypes.types.length - && otherTypes.types.every(e => firstTypes.types.includes(e)); - }); - if (!found) return false; - } - return true; + return isTypeAssignable(a.type, b.type) && isTypeAssignable(b.type, a.type); } -function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & DeclaredInfo, typeToAliases: Map>, accept: ValidationAcceptor) { +/////////////////////////////////////////////////////////////////////////////// + +function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & DeclaredInfo, accept: ValidationAcceptor) { const { inferred, declared, declaredNode, inferredNodes } = typeInfo; const typeName = declared.name; const applyErrorToRulesAndActions = (msgPostfix?: string) => (errorMsg: string) => - inferredNodes.forEach(node => accept('error', - `${errorMsg[-1] === '.' ? errorMsg.slice(0, -1) : errorMsg}${msgPostfix ? ` ${msgPostfix}` : ''}.`, + inferredNodes.forEach(node => accept('error', `${errorMsg}${msgPostfix ? ` ${msgPostfix}` : ''}.`, (node?.inferredType) ? >{ node: node?.inferredType, property: 'name' } : >{ node, property: ast.isAction(node) ? 'type' : 'name' } @@ -199,15 +186,20 @@ function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & Declare accept('error', errorMessage, { node, property: ast.isAssignment(node) || ast.isAction(node) ? 'feature' : 'name' }) ); + // todo add actions // currently we don't track which assignments belong to which actions and can't apply this error const applyMissingPropErrorToRules = (missingProp: string) => { inferredNodes.forEach(node => { if (ast.isParserRule(node)) { const assignments = extractAssignments(node.definition); if (assignments.find(e => e.feature === missingProp) === undefined) { - accept('error', + accept( + 'error', `Property '${missingProp}' is missing in a rule '${node.name}', but is required in type '${typeName}'.`, - {node, property: 'parameters'} + { + node, + property: 'parameters' + } ); } } @@ -215,13 +207,11 @@ function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & Declare }; if (isUnionType(inferred) && isUnionType(declared)) { - validateAlternativesConsistency(inferred.alternatives, declared.alternatives, - typeToAliases, + validateAlternativesConsistency(inferred.type, declared.type, applyErrorToRulesAndActions(`in a rule that returns type '${typeName}'`), ); } else if (isInterfaceType(inferred) && isInterfaceType(declared)) { - validatePropertiesConsistency(inferred.superProperties, declared.superProperties, - typeToAliases, + validatePropertiesConsistency(inferred, declared, applyErrorToRulesAndActions(`in a rule that returns type '${typeName}'`), applyErrorToProperties, applyMissingPropErrorToRules @@ -233,120 +223,63 @@ function validateDeclaredAndInferredConsistency(typeInfo: InferredInfo & Declare } } -type ErrorInfo = { - errorMessage: string; - typeAsString: string; -} - -function validateAlternativesConsistency(inferred: PropertyType[], declared: PropertyType[], - typeToAliases: Map>, - applyErrorToInferredTypes: (errorMessage: string) => void) { - - const errorsInfo = checkAlternativesConsistencyHelper(inferred, declared, typeToAliases); - for (const errorInfo of errorsInfo) { - applyErrorToInferredTypes(`A type '${errorInfo.typeAsString}' ${errorInfo.errorMessage}`); - } -} - -function getAllAliases(expected: PropertyType, typeToAliases: Map>): string[] { - const allAliases = expected.types.map(typeName => Array.from(typeToAliases.get(typeName) ?? new Set([typeName]))); - let branches: string[][] = []; - for (const aliasGroup of allAliases) { - if (branches.length === 0) { - branches.push([]); - } - if (aliasGroup.length === 1) { - branches.forEach(branch => branch.push(aliasGroup[0])); - } else { - const backup_branches = JSON.parse(JSON.stringify(branches)); - branches = []; - for (const alias of aliasGroup) { - const alias_branches: string[][] = JSON.parse(JSON.stringify(backup_branches)); - alias_branches.forEach(alias_branch => alias_branch.push(alias)); - branches.push(...alias_branches); - } - } - } - return branches.map(branch => distinctAndSorted(branch).join(' | ')); -} - -function typeAsStringKeywordsReplacement(found: PropertyType): string { - const propTypeWithStr = found.types.filter(e => !e.startsWith('\'')); - propTypeWithStr.push('string'); - return distinctAndSorted(propTypeWithStr).join(' | '); -} - -function checkAlternativesConsistencyHelper(found: PropertyType[], expected: PropertyType[], typeToAliases: Map>): ErrorInfo[] { - const arrayReferenceError = (found: PropertyType, expected: PropertyType) => - found.array && !expected.array && found.reference && !expected.reference ? 'can\'t be an array and a reference' : - !found.array && expected.array && !found.reference && expected.reference ? 'has to be an array and a reference' : - found.array && !expected.array ? 'can\'t be an array' : - !found.array && expected.array ? 'has to be an array' : - found.reference && !expected.reference ? 'can\'t be a reference' : - !found.reference && expected.reference ? 'has to be a reference' : ''; - - const stringToFound = found.reduce((acc, propType) => acc.set(distinctAndSorted(propType.types).join(' | '), propType), - new Map()); - - const stringToExpected = expected.reduce((acc, propType) => { - getAllAliases(propType, typeToAliases).forEach(alias => acc.set(alias, propType)); - return acc; - }, new Map()); - const errorsInfo: ErrorInfo[] = []; - - // detects extra type alternatives & check matched ones on consistency by 'array' and 'reference' - for (const [typeAsString, foundPropertyType] of stringToFound) { - const expectedPropertyType = stringToExpected.get(typeAsString) ?? stringToExpected.get(typeAsStringKeywordsReplacement(foundPropertyType)); - if (!expectedPropertyType) { - errorsInfo.push({ typeAsString, errorMessage: 'is not expected' }); - } else if (expectedPropertyType.array !== foundPropertyType.array || expectedPropertyType.reference !== foundPropertyType.reference) { - errorsInfo.push({ typeAsString, errorMessage: arrayReferenceError(foundPropertyType, expectedPropertyType) }); - } +function validateAlternativesConsistency( + inferred: PropertyType, + declared: PropertyType, + applyErrorToInferredTypes: (errorMessage: string) => void +) { + if (!isTypeAssignable(inferred, declared)) { + applyErrorToInferredTypes(`Cannot assign type '${propertyTypeToString(inferred, 'DeclaredType')}' to '${propertyTypeToString(declared, 'DeclaredType')}'`); } - return errorsInfo; } -function validatePropertiesConsistency(inferred: MultiMap, declared: MultiMap, - typeToAliases: Map>, +function validatePropertiesConsistency( + inferred: InterfaceType, + declared: InterfaceType, applyErrorToType: (errorMessage: string) => void, applyErrorToProperties: (nodes: Set, errorMessage: string) => void, applyMissingPropErrorToRules: (missingProp: string) => void ) { - const areBothNotArrays = (found: Property, expected: Property) => - !(found.typeAlternatives.length === 1 && found.typeAlternatives[0].array) && - !(expected.typeAlternatives.length === 1 && expected.typeAlternatives[0].array); + const ownInferredProps = new Set(inferred.properties.map(e => e.name)); + // This field also contains properties of sub types + const allInferredProps = new Map(inferred.allProperties.map(e => [e.name, e])); + // This field only contains properties of itself or super types + const declaredProps = new Map(declared.superProperties.map(e => [e.name, e])); // detects extra properties & validates matched ones on consistency by the 'optional' property - for (const [name, foundProps] of inferred.entriesGroupedByKey()) { - const foundProp = foundProps[0]; - const expectedProp = declared.get(name)[0]; + for (const [name, foundProp] of allInferredProps.entries()) { + const expectedProp = declaredProps.get(name); if (expectedProp) { - const foundTypeAsStr = propertyTypesToString(foundProp.typeAlternatives); - const expectedTypeAsStr = propertyTypesToString(expectedProp.typeAlternatives); - if (foundTypeAsStr !== expectedTypeAsStr) { - const typeAlternativesErrors = checkAlternativesConsistencyHelper(foundProp.typeAlternatives, expectedProp.typeAlternatives, typeToAliases); - if (typeAlternativesErrors.length > 0) { - const errorMsgPrefix = `The assigned type '${foundTypeAsStr}' is not compatible with the declared property '${name}' of type '${expectedTypeAsStr}'`; - const propErrors = typeAlternativesErrors - .map(errorInfo => ` '${errorInfo.typeAsString}' ${errorInfo.errorMessage}`) - .join('; '); - applyErrorToProperties(foundProp.astNodes, `${errorMsgPrefix}: ${propErrors}.`); - } + const foundTypeAsStr = propertyTypeToString(foundProp.type, 'DeclaredType'); + const expectedTypeAsStr = propertyTypeToString(expectedProp.type, 'DeclaredType'); + const typeAlternativesErrors = isTypeAssignable(foundProp.type, expectedProp.type); + if (!typeAlternativesErrors) { + const errorMsgPrefix = `The assigned type '${foundTypeAsStr}' is not compatible with the declared property '${name}' of type '${expectedTypeAsStr}'.`; + applyErrorToProperties(foundProp.astNodes, errorMsgPrefix); } - if (!expectedProp.optional && foundProp.optional && areBothNotArrays(foundProp, expectedProp)) { + if (!expectedProp.optional && foundProp.optional) { applyMissingPropErrorToRules(name); } - } else { + } else if (ownInferredProps.has(name)) { + // Only apply the superfluous property error on properties which are actually declared on the current type applyErrorToProperties(foundProp.astNodes, `A property '${name}' is not expected.`); } } - // detects lack of properties - for (const [name, expectedProperties] of declared.entriesGroupedByKey()) { - const foundProperty = inferred.get(name); - if (foundProperty.length === 0 && !expectedProperties.some(e => e.optional)) { - applyErrorToType(`A property '${name}' is expected.`); + // Detect any missing properties + const missingProps = new Set(); + for (const [name, expectedProperties] of declaredProps.entries()) { + const foundProperty = allInferredProps.get(name); + if (!foundProperty && !expectedProperties.optional) { + missingProps.add(name); } } + + if (missingProps.size > 0) { + const prefix = missingProps.size > 1 ? 'Properties' : 'A property'; + const postfix = missingProps.size > 1 ? 'are expected' : 'is expected'; + const props = Array.from(missingProps).map(e => `'${e}'`).sort().join(', '); + applyErrorToType(`${prefix} ${props} ${postfix}.`); + } } diff --git a/packages/langium/src/grammar/validation/validation-resources-collector.ts b/packages/langium/src/grammar/validation/validation-resources-collector.ts index 882e5fdaa..5555dbc32 100644 --- a/packages/langium/src/grammar/validation/validation-resources-collector.ts +++ b/packages/langium/src/grammar/validation/validation-resources-collector.ts @@ -9,11 +9,12 @@ import { stream } from '../../utils/stream'; import { LangiumDocuments } from '../../workspace/documents'; import { AbstractElement, Action, Grammar, Interface, isAction, isAlternatives, isGroup, isUnorderedGroup, ParserRule, Type } from '../generated/ast'; import { getActionType, getRuleType } from '../internal-grammar-util'; -import { AstResources, collectTypeResources, TypeResources } from '../type-system/type-collector/all-types'; -import { addSubTypes, mergeInterfaces, mergeTypesAndInterfaces } from '../type-system/types-util'; -import { isUnionType, Property, TypeOption } from '../type-system/type-collector/types'; -import { isDeclared, TypeToValidationInfo, ValidationResources } from '../workspace/documents'; +import { AstResources, ValidationAstTypes } from '../type-system/type-collector/all-types'; +import { mergeInterfaces, mergeTypesAndInterfaces } from '../type-system/types-util'; +import { TypeToValidationInfo, ValidationResources } from '../workspace/documents'; import { LangiumGrammarServices } from '../langium-grammar-module'; +import { collectValidationAst } from '../type-system/ast-collector'; +import { InterfaceType, Property } from '../type-system'; export class LangiumGrammarValidationResourcesCollector { private readonly documents: LangiumDocuments; @@ -23,14 +24,14 @@ export class LangiumGrammarValidationResourcesCollector { } collectValidationResources(grammar: Grammar): ValidationResources { - const typeResources = collectTypeResources(grammar, this.documents); - const typeToValidationInfo = this.collectValidationInfo(typeResources); - const typeToSuperProperties = this.collectSuperProperties(typeResources); - const typeToAliases = this.collectSubTypesAndAliases(typeToValidationInfo); - return { typeToValidationInfo, typeToSuperProperties, typeToAliases }; + const typeResources = collectValidationAst(grammar, this.documents); + return { + typeToValidationInfo: this.collectValidationInfo(typeResources), + typeToSuperProperties: this.collectSuperProperties(typeResources), + }; } - private collectValidationInfo({ astResources, inferred, declared }: TypeResources) { + private collectValidationInfo({ astResources, inferred, declared }: ValidationAstTypes) { const res: TypeToValidationInfo = new Map(); const typeNameToRulesActions = collectNameToRulesActions(astResources); @@ -52,7 +53,7 @@ export class LangiumGrammarValidationResourcesCollector { const inferred = res.get(type.name); res.set( type.name, - inferred ? {...inferred, declared: type, declaredNode: node } : { declared: type, declaredNode: node } + { ...inferred ?? {}, declared: type, declaredNode: node } ); } } @@ -60,58 +61,29 @@ export class LangiumGrammarValidationResourcesCollector { return res; } - private collectSuperProperties({ inferred, declared }: TypeResources): Map { + private collectSuperProperties({ inferred, declared }: ValidationAstTypes): Map { const typeToSuperProperties: Map = new Map(); + const interfaces = mergeInterfaces(inferred, declared); + const interfaceMap = new Map(interfaces.map(e => [e.name, e])); for (const type of mergeInterfaces(inferred, declared)) { - typeToSuperProperties.set(type.name, Array.from(type.superProperties.values())); + typeToSuperProperties.set(type.name, this.addSuperProperties(type, interfaceMap, new Set())); } return typeToSuperProperties; } - private collectSubTypesAndAliases(typeToValidationInfo: TypeToValidationInfo): Map> { - const nameToType = stream(typeToValidationInfo.entries()) - .reduce((acc, [name, info]) => { acc.set(name, isDeclared(info) ? info.declared : info.inferred); return acc; }, - new Map() - ); - addSubTypes(nameToType); - - const typeToAliases = new Map>(); - function addAlias(name: string, alias: string) { - const aliases = typeToAliases.get(name); - if (aliases) { - aliases.add(alias); - } else { - typeToAliases.set(name, new Set([alias])); - } + private addSuperProperties(interfaceType: InterfaceType, map: Map, visited: Set): Property[] { + if (visited.has(interfaceType.name)) { + return []; } - - const queue = Array.from(nameToType.values()).filter(e => e.subTypes.size === 0); - const visited = new Set(); - for (const type of queue) { - visited.add(type); - addAlias(type.name, type.name); - - for (const superTypeName of stream(type.realSuperTypes)) { - addAlias(superTypeName, type.name); - - const superType = nameToType.get(superTypeName); - if (superType && !visited.has(superType)) { - queue.push(superType); - } - } - - if (isUnionType(type) && type.alternatives.length === 1) { - type.alternatives - .filter(alt => !alt.array && !alt.reference) - .flatMap(alt => alt.types) - .forEach(e => { - addAlias(type.name, e); - addAlias(e, e); - addAlias(e, type.name); - }); + visited.add(interfaceType.name); + const properties: Property[] = [...interfaceType.properties]; + for (const superType of interfaceType.superTypes) { + const value = map.get(superType.name); + if (value) { + properties.push(...this.addSuperProperties(value, map, visited)); } } - return typeToAliases; + return properties; } } diff --git a/packages/langium/src/grammar/validation/validator.ts b/packages/langium/src/grammar/validation/validator.ts index c05ca891e..4e3ba6f33 100644 --- a/packages/langium/src/grammar/validation/validator.ts +++ b/packages/langium/src/grammar/validation/validator.ts @@ -19,6 +19,8 @@ import * as ast from '../generated/ast'; import { isParserRule, isRuleCall } from '../generated/ast'; import { getTypeNameWithoutError, isDataTypeRule, isOptionalCardinality, isPrimitiveType, resolveImport, resolveTransitiveImports, terminalRegex } from '../internal-grammar-util'; import type { LangiumGrammarServices } from '../langium-grammar-module'; +import { typeDefinitionToPropertyType } from '../type-system/type-collector/declared-types'; +import { flattenPlainType, isPlainReferenceType } from '../type-system/type-collector/plain-types'; export function registerValidationChecks(services: LangiumGrammarServices): void { const registry = services.validation.ValidationRegistry; @@ -81,10 +83,8 @@ export function registerValidationChecks(services: LangiumGrammarServices): void validator.checkCrossRefTerminalType, validator.checkCrossRefType ], - AtomType: [ - validator.checkAtomTypeRefType, - validator.checkFragmentsInTypes - ] + SimpleType: validator.checkFragmentsInTypes, + ReferenceType: validator.checkReferenceTypeUnion }; registry.register(checks, validator); } @@ -310,7 +310,7 @@ export class LangiumGrammarValidator { if (actionType) { const isInfers = Boolean(action.inferredType); const typeName = getTypeNameWithoutError(action); - if (action.type && types.has(typeName) === isInfers) { + if (action.type && typeName && types.has(typeName) === isInfers) { const keywordNode = isInfers ? findNodeForKeyword(action.$cstNode, 'infer') : findNodeForKeyword(action.$cstNode, '{'); accept('error', getMessage(typeName, isInfers), { node: action, @@ -318,7 +318,7 @@ export class LangiumGrammarValidator { code: isInfers ? IssueCodes.SuperfluousInfer : IssueCodes.MissingInfer, data: toDocumentSegment(keywordNode) }); - } else if (actionType && types.has(typeName) && isInfers) { + } else if (actionType && typeName && types.has(typeName) && isInfers) { // error: action infers type that is already defined if (action.$cstNode) { const inferredTypeNode = findNodeForProperty(action.inferredType?.$cstNode, 'name'); @@ -647,26 +647,53 @@ export class LangiumGrammarValidator { return; } let firstType: 'ref' | 'other'; - const foundMixed = streamAllContents(assignment.terminal).map(node => ast.isCrossReference(node) ? 'ref' : 'other').find(type => { - if (!firstType) { - firstType = type; - return false; - } - return type !== firstType; - }); + const foundMixed = streamAllContents(assignment.terminal) + .map(node => ast.isCrossReference(node) ? 'ref' : 'other') + .find(type => { + if (!firstType) { + firstType = type; + return false; + } + return type !== firstType; + }); if (foundMixed) { - accept('error', this.createMixedTypeError(assignment.feature), { node: assignment, property: 'terminal' }); + accept( + 'error', + this.createMixedTypeError(assignment.feature), + { + node: assignment, + property: 'terminal' + } + ); } } checkInterfacePropertyTypes(interfaceDecl: ast.Interface, accept: ValidationAcceptor): void { - interfaceDecl.attributes.filter(attr => attr.typeAlternatives.length > 1).forEach(attr => { - const typeKind = (type: ast.AtomType) => type.isRef ? 'ref' : 'other'; - const firstKind = typeKind(attr.typeAlternatives[0]); - if (attr.typeAlternatives.slice(1).some(type => typeKind(type) !== firstKind)) { - accept('error', this.createMixedTypeError(attr.name), { node: attr, property: 'typeAlternatives' }); + for (const attribute of interfaceDecl.attributes) { + if (attribute.type) { + const plainType = typeDefinitionToPropertyType(attribute.type); + const flattened = flattenPlainType(plainType); + let hasRef = false; + let hasNonRef = false; + for (const flat of flattened) { + if (isPlainReferenceType(flat)) { + hasRef = true; + } else if (!isPlainReferenceType(flat)) { + hasNonRef = true; + } + } + if (hasRef && hasNonRef) { + accept( + 'error', + this.createMixedTypeError(attribute.name), + { + node: attribute, + property: 'type' + } + ); + } } - }); + } } protected createMixedTypeError(propName: string) { @@ -711,18 +738,15 @@ export class LangiumGrammarValidator { } } - checkAtomTypeRefType(atomType: ast.AtomType, accept: ValidationAcceptor): void { - if (atomType?.refType) { - const issue = this.checkReferenceToRuleButNotType(atomType?.refType); - if (issue) { - accept('error', issue, { node: atomType, property: 'refType' }); - } + checkFragmentsInTypes(type: ast.SimpleType, accept: ValidationAcceptor): void { + if (ast.isParserRule(type.typeRef?.ref) && type.typeRef?.ref.fragment) { + accept('error', 'Cannot use rule fragments in types.', { node: type, property: 'typeRef' }); } } - checkFragmentsInTypes(atomType: ast.AtomType, accept: ValidationAcceptor): void { - if (ast.isParserRule(atomType.refType?.ref) && atomType.refType?.ref.fragment) { - accept('error', 'Cannot use rule fragments in types.', { node: atomType, property: 'refType' }); + checkReferenceTypeUnion(type: ast.ReferenceType, accept: ValidationAcceptor): void { + if (!ast.isSimpleType(type.referenceType)) { + accept('error', 'Only direct rule references are allowed in reference types.', { node: type, property: 'referenceType' }); } } diff --git a/packages/langium/src/grammar/workspace/documents.ts b/packages/langium/src/grammar/workspace/documents.ts index 46ec90ee3..05dde53ff 100644 --- a/packages/langium/src/grammar/workspace/documents.ts +++ b/packages/langium/src/grammar/workspace/documents.ts @@ -6,7 +6,7 @@ import { LangiumDocument } from '../../workspace/documents'; import { Action, Grammar, Interface, ParserRule, Type } from '../generated/ast'; -import { Property, TypeOption } from '../type-system/type-collector/types'; +import { Property, TypeOption } from '../type-system'; /** * A Langium document holds the parse result (AST and CST) and any additional state that is derived @@ -19,7 +19,6 @@ export interface LangiumGrammarDocument extends LangiumDocument { export type ValidationResources = { typeToValidationInfo: TypeToValidationInfo, typeToSuperProperties: Map, - typeToAliases: Map>, } export type TypeToValidationInfo = Map; diff --git a/packages/langium/src/test/langium-test.ts b/packages/langium/src/test/langium-test.ts index e19d2dd57..080d136dc 100644 --- a/packages/langium/src/test/langium-test.ts +++ b/packages/langium/src/test/langium-test.ts @@ -426,9 +426,9 @@ export function expectWarning { const allDocs = services.shared.workspace.LangiumDocuments.all.map(x => x.uri).toArray(); - services.shared.workspace.DocumentBuilder.update([], allDocs); + return services.shared.workspace.DocumentBuilder.update([], allDocs); } export interface DecodedSemanticTokensWithRanges { diff --git a/packages/langium/src/utils/grammar-util.ts b/packages/langium/src/utils/grammar-util.ts index 31c103340..8be0b20bf 100644 --- a/packages/langium/src/utils/grammar-util.ts +++ b/packages/langium/src/utils/grammar-util.ts @@ -270,8 +270,8 @@ function findNameAssignmentInternal(type: ast.AbstractType, cache: Map { describe('Inheritance with sub- and super-types', () => { - const superType = new InterfaceType('Super', [], [ - { - name: 'A', - optional: false, - typeAlternatives: [{ - array: true, - reference: false, - types: ['string'] - }], - astNodes: new Set() - }, - { - name: 'Ref', - optional: true, - typeAlternatives: [{ - array: false, - reference: true, - types: ['RefTarget'] - }], - astNodes: new Set() + const superType = new InterfaceType('Super', false, false); + + superType.properties.push({ + name: 'A', + astNodes: new Set(), + optional: false, + type: { + elementType: { + primitive: 'string' + } + } + }, { + name: 'Ref', + astNodes: new Set(), + optional: true, + type: { + referenceType: { + value: superType + } } - ]); + }); - const subType = new InterfaceType('Sub', ['Super'], [ - { - name: 'B', - optional: false, - typeAlternatives: [{ - array: true, - reference: false, - types: ['string'] - }], - astNodes: new Set() + const subType = new InterfaceType('Sub', false, false); + subType.properties.push({ + name: 'B', + astNodes: new Set(), + optional: false, + type: { + elementType: { + primitive: 'string' + } } - ]); + }); + subType.superTypes.add(superType); const reflectionForInheritance = interpretAstReflection({ interfaces: [superType, subType], @@ -71,14 +70,14 @@ describe('AST reflection interpreter', () => { }, property: 'Ref', reference: undefined! - })).toBe('RefTarget'); + })).toBe('Super'); expect(reflectionForInheritance.getReferenceType({ container: { $type: 'Sub' }, property: 'Ref', reference: undefined! - })).toBe('RefTarget'); + })).toBe('Super'); }); test('Creates metadata with super types', () => { diff --git a/packages/langium/test/grammar/langium-grammar-validator.test.ts b/packages/langium/test/grammar/langium-grammar-validator.test.ts index ceaa6a04c..4eed26470 100644 --- a/packages/langium/test/grammar/langium-grammar-validator.test.ts +++ b/packages/langium/test/grammar/langium-grammar-validator.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeAll, describe, expect, test } from 'vitest'; import { DiagnosticSeverity } from 'vscode-languageserver'; import { AstNode, createLangiumGrammarServices, EmptyFileSystem, GrammarAST, Properties, streamAllContents, streamContents } from '../../src'; -import { Assignment, isAssignment } from '../../src/grammar/generated/ast'; +import { Assignment, isAssignment, UnionType } from '../../src/grammar/generated/ast'; import { IssueCodes } from '../../src/grammar/validation/validator'; import { clearDocuments, expectError, expectIssue, expectNoIssues, expectWarning, parseHelper, validationHelper, ValidationResult } from '../../src/test'; @@ -177,10 +177,10 @@ describe('checkReferenceToRuleButNotType', () => { }); test('AtomType validation', () => { - const type = validationResult.document.parseResult.value.types[0]; + const unionType = validationResult.document.parseResult.value.types[0].type as UnionType; + const missingType = unionType.types[0]; expectError(validationResult, "Could not resolve reference to AbstractType named 'Reference'.", { - node: type, - property: 'typeAlternatives' + node: missingType }); }); diff --git a/packages/langium/test/grammar/type-system/inferred-types.test.ts b/packages/langium/test/grammar/type-system/inferred-types.test.ts index ad9b40e9c..d582ba025 100644 --- a/packages/langium/test/grammar/type-system/inferred-types.test.ts +++ b/packages/langium/test/grammar/type-system/inferred-types.test.ts @@ -4,696 +4,362 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { beforeAll, describe, expect, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { createLangiumGrammarServices, Grammar, EmptyFileSystem, expandToString, EOL } from '../../../src'; -import { collectAst, specifyAstNodeProperties } from '../../../src/grammar/type-system/ast-collector'; -import { collectAllAstResources, collectTypeResources } from '../../../src/grammar/type-system/type-collector/all-types'; -import { collectDeclaredTypes } from '../../../src/grammar/type-system/type-collector/declared-types'; -import { collectInferredTypes } from '../../../src/grammar/type-system/type-collector/inferred-types'; -import { AstTypes, InterfaceType, Property, PropertyType, UnionType } from '../../../src/grammar/type-system/type-collector/types'; -import { sortInterfacesTopologically } from '../../../src/grammar/type-system/types-util'; -import { parseHelper } from '../../../src/test'; - -function describeTypes(name: string, grammar: string, description: (types: AstTypes) => void | Promise): void { - describe(name, () => { - const types: AstTypes = { - interfaces: [], - unions: [] - }; - beforeAll(async () => { - const newTypes = await getTypes(grammar); - types.interfaces = newTypes.interfaces; - types.unions = newTypes.unions; - }); - description.call(undefined, types); - }); -} - -describeTypes('inferred types of simple grammars', ` - A: name=ID value=NUMBER?; - B: name=FQN values+=NUMBER; - C: 'c' ref=[A]; - - FQN: ID; - terminal ID returns string: /string/; - terminal NUMBER returns number: /number/; -`, types => { - test('A is inferred with name:string and value?:number', () => { - const a = getType(types, 'A') as InterfaceType; - expect(a).toBeDefined(); - expectProperty(a, { - name: 'name', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); - expectProperty(a, { - name: 'value', - optional: true, - typeAlternatives: [{ - array: false, - reference: false, - types: ['number'] - }], - astNodes: new Set(), - }); - }); - - test('B is inferred with name:FQN and values:number[]', () => { - const b = getType(types, 'B') as InterfaceType; - expect(b).toBeDefined(); - expectProperty(b, { - name: 'name', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['FQN'], - }], - astNodes: new Set(), - }); - expectProperty(b, { - name: 'values', - optional: false, - typeAlternatives: [{ - array: true, - reference: false, - types: ['number'] - }], - astNodes: new Set(), - }); - }); - - test('C is inferred with ref:@A', () => { - const c = getType(types, 'C') as InterfaceType; - expect(c).toBeDefined(); - expectProperty(c, { - name: 'ref', - optional: false, - typeAlternatives: [{ - array: false, - reference: true, - types: ['A'] - }], - astNodes: new Set(), - }); - }); - - test('FQN is created as an union type', () => { - const fqn = getType(types, 'FQN') as UnionType; - expectUnion(fqn, [ - { - array: false, - reference: false, - types: ['string'] +import { mergeTypesAndInterfaces } from '../../../src/grammar/type-system'; +import { collectAst } from '../../../src/grammar/type-system/ast-collector'; +import { AstTypes } from '../../../src/grammar/type-system/type-collector/types'; +import { clearDocuments, parseHelper } from '../../../src/test'; + +describe('Inferred types', () => { + + test('Should infer types of simple grammars', async () => { + await expectTypes(` + A: name=ID value=NUMBER?; + B: name=FQN values+=NUMBER; + C: 'c' ref=[A]; + + FQN: ID; + terminal ID returns string: /string/; + terminal NUMBER returns number: /number/; + `, expandToString` + export interface A extends AstNode { + readonly $type: 'A'; + name: string + value?: number } - ]); - }); -}); - -describeTypes('inferred types for alternatives', ` - A: name=ID | name=NUMBER; - B: name=(ID | NUMBER); - C: A | B; - D: A | B | name=ID; - E: name=ID | value=NUMBER; - - - terminal ID returns string: /string/; - terminal NUMBER returns number: /number/; -`, types => { - test('A is inferred with name:(string)|(number)', () => { - const a = getType(types, 'A') as InterfaceType; - expect(a).toBeDefined(); - expect(a.printingSuperTypes).toEqual(['D']); - expect(a.properties).toHaveLength(1); - expectProperty(a, { - name: 'name', - optional: false, - typeAlternatives: [ - { - array: false, - reference: false, - types: ['string'] - }, - { - array: false, - reference: false, - types: ['number'] - } - ], - astNodes: new Set(), - }); - }); - - test('B is inferred with name:(number|string)', () => { - const b = getType(types, 'B') as InterfaceType; - expect(b).toBeDefined(); - expect(b.printingSuperTypes).toEqual(['D']); - expect(b.properties).toHaveLength(1); - expectProperty(b, { - name: 'name', - optional: false, - typeAlternatives: [ - { - array: false, - reference: false, - types: ['string'] - }, - { - array: false, - reference: false, - types: ['number'] - } - ], - astNodes: new Set(), - }); - }); - - test('C is inferred as union type A | B', () => { - const c = getType(types, 'C') as UnionType; - expect(c).toBeDefined(); - expectUnion(c, [ - { - array: false, - reference: false, - types: ['A'] - }, - { - array: false, - reference: false, - types: ['B'] + export interface B extends AstNode { + readonly $type: 'B'; + name: FQN + values: Array } - ]); - }); - - test('D is inferred as name:string', () => { - const d = getType(types, 'D') as InterfaceType; - expect(d).toBeDefined(); - expect(d.printingSuperTypes).toHaveLength(0); - expect(d.properties).toHaveLength(1); - expectProperty(d, { - name: 'name', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); - }); - - test('E is inferred with name?:string, value?: number', () => { - const e = getType(types, 'E') as InterfaceType; - expect(e).toBeDefined(); - expectProperty(e, { - name: 'name', - optional: true, - typeAlternatives: [ - { - array: false, - reference: false, - types: ['string'] - } - ], - astNodes: new Set(), - }); - expectProperty(e, { - name: 'value', - optional: true, - typeAlternatives: [ - { - array: false, - reference: false, - types: ['number'] - } - ], - astNodes: new Set(), - }); - }); - -}); - -describeTypes('inferred types using chained actions', ` - A: a=ID ({infer B} b=ID ({infer C} c=ID)?)? d=ID; - D: E ({infer D.e=current} d=ID)?; - E: e=ID; - F: value=ID ({infer F.item=current} value=ID)*; - G: ({infer X} x=ID | {infer Y} y=ID | {infer Z} z=ID) {infer G.front=current} back=ID; - H infers E: E {infer H} h=ID; - - Entry: - Expr; - Expr: - {infer Ref} ref=ID - ({infer Access.receiver=current} '.' member=ID)*; - Ref: - {infer Ref} ref=ID; - Access: - {infer Access} '.' member=ID; - - IdRule: - 'id' name=ID; - RuleName infers RuleType: - IdRule ( - {infer FirstBranch.value=current} FirstBranchFragment - | {infer SecondBranch.value=current} SecondBranchFragment - ); - fragment FirstBranchFragment: 'First' first=ID; - fragment SecondBranchFragment: 'Second' second=ID; - - terminal ID returns string: /string/; -`, types => { - - test('RuleType is inferred as a union type', () => { - const ruleType = getType(types, 'RuleType') as UnionType; - expect(ruleType).toBeDefined(); - expectUnion(ruleType, [ - { - array: false, - reference: false, - types: ['IdRule'] - }, - { - array: false, - reference: false, - types: ['FirstBranch'] - }, - { - array: false, - reference: false, - types: ['SecondBranch'] - }, - ]); - }); - - test('FirstBranch is inferred as first:string, value:IdRule', () => { - const firstBranch = getType(types, 'FirstBranch') as InterfaceType; - expect(firstBranch).toBeDefined(); - expect(firstBranch.printingSuperTypes).toHaveLength(0); - expect(firstBranch.properties).toHaveLength(2); - expectProperty(firstBranch, { - name: 'first', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); - expectProperty(firstBranch, { - name: 'value', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['IdRule'] - }], - astNodes: new Set(), - }); - - }); - - test('Entry is inferred as a union type', () => { - const entry = getType(types, 'Entry') as UnionType; - expect(entry).toBeDefined(); - expectUnion(entry, [{ - array: false, - reference: false, - types: ['Expr'] - }]); - }); - - test('Expr is inferred as a union type', () => { - const expr = getType(types, 'Expr') as UnionType; - expect(expr).toBeDefined(); - expectUnion(expr, [ - { - array: false, - reference: false, - types: ['Ref'] - }, - { - array: false, - reference: false, - types: ['Access'] - }, - ]); - }); - - test('Ref is inferred as ref:string', () => { - const ref = getType(types, 'Ref') as InterfaceType; - expect(ref).toBeDefined(); - expect(ref.printingSuperTypes).toHaveLength(0); - expect(ref.properties).toHaveLength(1); - expectProperty(ref, { - name: 'ref', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); - }); - - test('Access is inferred as receiver:Ref, member:string', () => { - const access = getType(types, 'Access') as InterfaceType; - expect(access).toBeDefined(); - expect(access.printingSuperTypes).toHaveLength(0); - expect(access.properties).toHaveLength(2); - expectProperty(access, { - name: 'receiver', - optional: true, - typeAlternatives: [{ - array: false, - reference: false, - types: ['Ref'] - }], - astNodes: new Set(), - }); - expectProperty(access, { - name: 'member', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); + export interface C extends AstNode { + readonly $type: 'C'; + ref: Reference + } + export type FQN = string; + `); }); - test('A is inferred with a:string, d:string', () => { - const a = getType(types, 'A') as InterfaceType; - expect(a).toBeDefined(); - expect(a.printingSuperTypes).toHaveLength(0); - expect(a.properties).toHaveLength(2); - expectProperty(a, 'a'); - expectProperty(a, 'd'); - }); + test('Should infer types for alternatives', async () => { + await expectTypes(` + A: name=ID | name=NUMBER; + B: name=(ID | NUMBER); + C: A | B; + D: A | B | name=ID; + E: name=ID | value=NUMBER; - test('B is inferred with super type A and a:string, b:string, d:string', () => { - const b = getType(types, 'B') as InterfaceType; - expect(b).toBeDefined(); - expect(b.printingSuperTypes).toEqual(['A']); - expect(b.properties).toHaveLength(2); - expectProperty(b, 'b'); - expectProperty(b, 'd'); - }); - - test('C is inferred with super type B and a:string, b:string, c:string d:string', () => { - const c = getType(types, 'C') as InterfaceType; - expect(c).toBeDefined(); - expect(c.printingSuperTypes).toEqual(['B']); - expect(c.properties).toHaveLength(2); - expectProperty(c, 'c'); - expectProperty(c, 'd'); + terminal ID returns string: /string/; + terminal NUMBER returns number: /number/; + `, expandToString` + export interface D extends AstNode { + readonly $type: 'A' | 'B' | 'D'; + name: string + } + export interface E extends AstNode { + readonly $type: 'E'; + name?: string + value?: number + } + export interface A extends D { + readonly $type: 'A'; + name: number | string + } + export interface B extends D { + readonly $type: 'B'; + name: number | string + } + export type C = A | B; + `); }); - test('D is inferred with e:E, d:string', () => { - const d = getType(types, 'D') as InterfaceType; - expect(d).toBeDefined(); - expect(d.printingSuperTypes).toHaveLength(0); - expect(d.properties).toHaveLength(2); - expectProperty(d, { - name: 'e', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['E'] - }], - astNodes: new Set(), - }); - expectProperty(d, 'd'); + test('Should correctly infer types using chained actions', async () => { + await expectTypes(` + A: a=ID ({infer B} b=ID ({infer C} c=ID)?)? d=ID; + terminal ID returns string: /string/; + `, expandToString` + export interface A extends AstNode { + readonly $type: 'A' | 'B' | 'C'; + a: string + d: string + } + export interface B extends A { + readonly $type: 'B' | 'C'; + b: string + d: string + } + export interface C extends B { + readonly $type: 'C'; + c: string + d: string + } + `); }); - test('E is inferred with super type D and property e', () => { - const e = getType(types, 'E') as InterfaceType; - expect(e).toBeDefined(); - expect(e.printingSuperTypes).toEqual(['D']); - expect(e.properties).toHaveLength(1); - expectProperty(e, 'e'); + test('Should correctly infer types using chained actions with assignments', async () => { + await expectTypes(` + A: B ({infer B.b=current} c=ID)?; + B: a=ID; + terminal ID returns string: /string/; + `, expandToString` + export interface B extends AstNode { + readonly $container: B; + readonly $type: 'B'; + a?: string + b?: B + c?: string + } + export type A = B; + `); }); - test('F is inferred with value:string, item?:F', () => { - const f = getType(types, 'F') as InterfaceType; - expect(f).toBeDefined(); - expect(f.printingSuperTypes).toHaveLength(0); - expect(f.properties).toHaveLength(2); - expectProperty(f, 'value'); - expectProperty(f, { - name: 'item', - optional: true, - typeAlternatives: [{ - array: false, - reference: false, - types: ['F'] - }], - astNodes: new Set(), - }); + test('Should correctly infer types using actions in repetitions', async () => { + await expectTypes(` + A: value=ID ({infer A.item=current} value=ID)*; + terminal ID returns string: /string/; + `, expandToString` + export interface A extends AstNode { + readonly $container: A; + readonly $type: 'A'; + item?: A + value: string + } + `); }); - test('G is inferred with front:(X|Y|Z), back:string', () => { - const G = getType(types, 'G') as InterfaceType; - expect(G).toBeDefined(); - expect(G.printingSuperTypes).toHaveLength(0); - expect(G.properties).toHaveLength(2); - expectProperty(G, { - name: 'front', - optional: false, - typeAlternatives: [ - { - array: false, - reference: false, - types: ['X'] - }, - { - array: false, - reference: false, - types: ['Y'] - }, - { - array: false, - reference: false, - types: ['Z'] - } - ], - astNodes: new Set(), - }); + test('Should correctly infer types using unassinged and assigned actions', async () => { + await expectTypes(` + A: ({infer X} x=ID | {infer Y} y=ID | {infer Z} z=ID) {infer B.front=current} back=ID; + terminal ID returns string: /string/; + `, expandToString` + export interface B extends AstNode { + readonly $container: B; + readonly $type: 'B'; + back: string + front: X | Y | Z + } + export interface X extends AstNode { + readonly $container: B; + readonly $type: 'X'; + x: string + } + export interface Y extends AstNode { + readonly $container: B; + readonly $type: 'Y'; + y: string + } + export interface Z extends AstNode { + readonly $container: B; + readonly $type: 'Z'; + z: string + } + export type A = B | X | Y | Z; + `); }); - test('H is inferred as extending E and with h:string', () => { - const H = getType(types, 'H') as InterfaceType; - expect(H).toBeDefined(); - expect(H.printingSuperTypes).toEqual(['E']); - expect(H.properties).toHaveLength(1); - expectProperty(H, { - name: 'h', - optional: false, - typeAlternatives: [ - { - array: false, - reference: false, - types: ['string'] - } - ], - astNodes: new Set(), - }); + test('Should correctly infer types using actions and infer keyword', async () => { + await expectTypes(` + A: a=ID; + B infers A: A {infer B} b=ID; + terminal ID returns string: /string/; + `, expandToString` + export interface A extends AstNode { + readonly $type: 'A' | 'B'; + a: string + } + export interface B extends A { + readonly $type: 'B'; + b: string + } + `); }); - test('X, Y, Z are inferred from G as simple types', () => { - const x = getType(types, 'X') as InterfaceType; - expect(x).toBeDefined(); - expect(x.printingSuperTypes).toEqual(['G']); - expect(x.properties).toHaveLength(1); - expectProperty(x, 'x'); - const y = getType(types, 'Y') as InterfaceType; - expect(y).toBeDefined(); - expect(x.printingSuperTypes).toEqual(['G']); - expect(y.properties).toHaveLength(1); - expectProperty(y, 'y'); - const z = getType(types, 'Z') as InterfaceType; - expect(z).toBeDefined(); - expect(x.printingSuperTypes).toEqual(['G']); - expect(z.properties).toHaveLength(1); - expectProperty(z, 'z'); + test('Should infer actions with alternatives and fragments correctly', async () => { + await expectTypes(` + Entry: + Expr; + Expr: + {infer Ref} ref=ID + ({infer Access.receiver=current} '.' member=ID)*; + Ref: + {infer Ref} ref=ID; + Access: + {infer Access} '.' member=ID; + + IdRule: + 'id' name=ID; + RuleName infers RuleType: + IdRule ( + {infer FirstBranch.value=current} FirstBranchFragment + | {infer SecondBranch.value=current} SecondBranchFragment + ); + fragment FirstBranchFragment: 'First' first=ID; + fragment SecondBranchFragment: 'Second' second=ID; + + terminal ID returns string: /string/; + `, expandToString` + export interface Access extends AstNode { + readonly $container: Access; + readonly $type: 'Access'; + member: string + receiver?: Ref + } + export interface FirstBranch extends AstNode { + readonly $container: FirstBranch | SecondBranch; + readonly $type: 'FirstBranch'; + first: string + value: IdRule + } + export interface IdRule extends AstNode { + readonly $container: FirstBranch | SecondBranch; + readonly $type: 'IdRule'; + name: string + } + export interface Ref extends AstNode { + readonly $container: Access; + readonly $type: 'Ref'; + ref: string + } + export interface SecondBranch extends AstNode { + readonly $container: FirstBranch | SecondBranch; + readonly $type: 'SecondBranch'; + second: string + value: IdRule + } + export type Entry = Expr; + export type Expr = Access | Ref; + export type RuleType = FirstBranch | IdRule | SecondBranch; + `); }); -}); - -describeTypes('inferred types with common names', ` - A infers X: a=ID; - B infers X: a=ID b=ID; - C infers X: a=ID c=ID?; - - terminal ID returns string: /string/; -`, types => { - test('X is inferred with a:string, b?:string, c?:string', () => { - const x = getType(types, 'X') as InterfaceType; - expect(x).toBeDefined(); - expect(x.printingSuperTypes).toHaveLength(0); - expect(x.properties).toHaveLength(3); - expectProperty(x, 'a'); - expectProperty(x, { - name: 'b', - optional: true, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); - expectProperty(x, { - name: 'c', - optional: true, - typeAlternatives: [{ - array: false, - reference: false, - types: ['string'] - }], - astNodes: new Set(), - }); - }); + test('Should correctly infer types with common names', async () => { + await expectTypes(` + A infers X: a=ID; + B infers X: a=ID b=ID; + C infers X: a=ID c=ID?; - test('A, B, C are not inferred', () => { - const a = getType(types, 'A') as InterfaceType; - expect(a).toBeUndefined(); - const b = getType(types, 'B') as InterfaceType; - expect(b).toBeUndefined(); - const c = getType(types, 'C') as InterfaceType; - expect(c).toBeUndefined(); + terminal ID returns string: /string/; + `, expandToString` + export interface X extends AstNode { + readonly $type: 'X'; + a: string + b?: string + c?: string + } + `); }); -}); - -describeTypes('inferred types with common names and actions', ` - A infers X: {infer A} a=ID; - B infers X: {infer B} b=ID; - - C: D ({infer C.item=current} value=ID); - D infers Y: y=ID; + test('Should infer types with common names and actions', async () => { + await expectTypes(` + A infers X: {infer A} a=ID; + B infers X: {infer B} b=ID; - terminal ID returns string: /string/; -`, types => { - test('A is inferred with a:string', () => { - const a = getType(types, 'A') as InterfaceType; - expect(a).toBeDefined(); - // Since 'X' is not an actual type (but a union), it is removed as a super type. - expect(a.printingSuperTypes).toHaveLength(0); - expect(a.properties).toHaveLength(1); - expectProperty(a, 'a'); - }); - - test('B is inferred with b:string', () => { - const b = getType(types, 'B') as InterfaceType; - expect(b).toBeDefined(); - // Since 'X' is not an actual type (but a union), it is removed as a super type. - expect(b.printingSuperTypes).toHaveLength(0); - expect(b.properties).toHaveLength(1); - expectProperty(b, 'b'); - }); + C: D ({infer C.item=current} value=ID); + D infers Y: y=ID; - test('X is inferred as A | B', () => { - const x = getType(types, 'X') as UnionType; - expect(x).toBeDefined(); - expectUnion(x, [ - { - array: false, - reference: false, - types: ['A'] - }, - { - array: false, - reference: false, - types: ['B'] + terminal ID returns string: /string/; + `, expandToString` + export interface A extends AstNode { + readonly $type: 'A'; + a: string } - ]); - }); - - test('C is inferred with super type Y and properties item:Y, value:ID', () => { - const c = getType(types, 'C') as InterfaceType; - expect(c).toBeDefined(); - expect(c.printingSuperTypes).toHaveLength(0); - expect(c.properties).toHaveLength(2); - expectProperty(c, 'value'); - expectProperty(c, { - name: 'item', - optional: false, - typeAlternatives: [{ - array: false, - reference: false, - types: ['Y'] - }], - astNodes: new Set(), - }); + export interface B extends AstNode { + readonly $type: 'B'; + b: string + } + export interface C extends AstNode { + readonly $container: C; + readonly $type: 'C' | 'Y'; + item: Y + value: string + } + export interface Y extends C { + readonly $container: C; + readonly $type: 'Y'; + y: string + } + export type X = A | B; + `); }); - test('Y is inferred from D with y:ID', () => { - const y = getType(types, 'Y') as InterfaceType; - expect(y).toBeDefined(); - expect(y.printingSuperTypes).toEqual(['C']); - expect(y.properties).toHaveLength(1); - expectProperty(y, 'y'); + test('Should infer data type rules as unions', async () => { + expectTypes(` + Strings returns string: 'a' | 'b' | 'c'; + MoreStrings returns string: Strings | 'd' | 'e'; + Complex returns string: ID ('.' ID)*; + DateLike returns Date: 'x'; + terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/; + `, expandToString` + export type Complex = string; + export type DateLike = Date; + export type MoreStrings = 'd' | 'e' | Strings; + export type Strings = 'a' | 'b' | 'c'; + `); }); - test('D is not inferred', () => { - const d = getType(types, 'D'); - expect(d).toBeUndefined(); + test('Infers X as a super interface of Y and Z with property `id`', async () => { + expectTypes(` + entry X: id=ID ({infer Y} 'a' | {infer Z} 'b'); + terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/; + `, expandToString` + export interface X extends AstNode { + readonly $type: 'X' | 'Y' | 'Z'; + id: string + } + export interface Y extends X { + readonly $type: 'Y'; + } + export interface Z extends X { + readonly $type: 'Z'; + } + `); }); - }); -describeTypes('inferred types that are used by the grammar', ` - A infers B: 'a' name=ID (otherA=[B])?; - hidden terminal WS: /\\s+/; - terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/; - `, types => { - - test('B is defined and A is not', () => { - const a = getType(types, 'A') as InterfaceType; - expect(a).toBeUndefined(); - const b = getType(types, 'B') as InterfaceType; - expect(b).toBeDefined(); +describe('inferred types that are used by the grammar', () => { + test('B is defined and A is not', async () => { + expectTypes(` + A infers B: 'a' name=ID (otherA=[B])?; + hidden terminal WS: /\\s+/; + terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/; + `, expandToString` + export interface B extends AstNode { + readonly $type: 'B'; + name: string + otherA?: Reference + } + `); }); }); -describeTypes('inferred and declared types', ` - X returns X: Y | Z; - Y: y='y'; - Z: z='z'; - - interface X { } -`, types => { - - test('X is preserved as an interface', () => { - const x = getType(types, 'X') as InterfaceType; - expect(x).toBeDefined(); - expect(x.properties).toHaveLength(0); - const y = getType(types, 'Y') as InterfaceType; - expect(y).toBeDefined(); - expect(y.realSuperTypes).toContain('X'); - const z = getType(types, 'Z') as InterfaceType; - expect(z).toBeDefined(); - expect(z.realSuperTypes).toContain('X'); +describe('inferred and declared types', () => { + test('Declared interfaces should be preserved as interfaces', async () => { + expectTypes(` + X returns X: Y | Z; + Y: y='y'; + Z: z='z'; + + interface X { } + `, expandToString` + export interface X extends AstNode { + readonly $type: 'X' | 'Y' | 'Z'; + } + export interface Y extends X { + readonly $type: 'Y'; + y: 'y' + } + export interface Z extends X { + readonly $type: 'Z'; + z: 'z' + } + `); }); - }); describe('expression rules with inferred and declared interfaces', () => { test('separate rules with assigned actions with inferred type and declared sub type of the former', async () => { - const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar; - const document = await parseHelper(grammarServices)(` + await checkTypes(` interface Symbol {} interface SuperMemberAccess extends MemberAccess {} @@ -721,16 +387,10 @@ describe('expression rules with inferred and declared interfaces', () => { {infer BooleanLiteral} value?='true' | 'false' ; `); - - expect(document.parseResult.lexerErrors).toHaveLength(0); - expect(document.parseResult.parserErrors).toHaveLength(0); - - checkTypes(document.parseResult.value); }); test('single rule with two assigned actions with inferred type and declared sub type of the former', async () => { - const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar; - const document = await parseHelper(grammarServices)(` + await checkTypes(` interface Symbol {} interface SuperMemberAccess extends MemberAccess {} @@ -753,72 +413,13 @@ describe('expression rules with inferred and declared interfaces', () => { {infer BooleanLiteral} value?='true' | 'false' ; `); - - expect(document.parseResult.lexerErrors).toHaveLength(0); - expect(document.parseResult.parserErrors).toHaveLength(0); - - checkTypes(document.parseResult.value); }); // todo make tests like in this PR: https://github.com/langium/langium/pull/670 // the PR #670 fixes the demonstrated bug, but cancels type inferrence for declared actions // we should fix the issue another way - function checkTypes(grammar: Grammar) { - const { parserRules, datatypeRules, interfaces, types } = collectAllAstResources([grammar]); - - // check only inferred types - const inferred = collectInferredTypes(parserRules, datatypeRules, { - interfaces: [], - unions: [] - }); - inferred.interfaces = sortInterfacesTopologically(inferred.interfaces); - specifyAstNodeProperties(inferred); - - const inferredInterfacesString = inferred.interfaces.map(toSubstring).join(EOL).trim(); - expect(inferredInterfacesString).toBe(expandToString` - export interface BooleanLiteral extends AstNode { - readonly $container: MemberAccess | SuperMemberAccess; - readonly $type: 'BooleanLiteral'; - value: boolean - } - export interface MemberAccess extends AstNode { - readonly $container: MemberAccess | SuperMemberAccess; - readonly $type: 'MemberAccess'; - member: Reference - receiver: PrimaryExpression - } - export interface SuperMemberAccess extends AstNode { - readonly $container: MemberAccess | SuperMemberAccess; - readonly $type: 'SuperMemberAccess'; - member: Reference - receiver: PrimaryExpression - } - `); - - // check only declared types - const declared = collectDeclaredTypes(interfaces, types); - inferred.interfaces = sortInterfacesTopologically(declared.interfaces); - specifyAstNodeProperties(declared); - - expect(declared.interfaces.map(toSubstring).join(EOL).trim()).toBe(expandToString` - export interface SuperMemberAccess extends MemberAccess { - readonly $type: 'SuperMemberAccess'; - } - export interface Symbol extends AstNode { - readonly $type: 'Symbol'; - } - `); - - // check ast.ts types - const allTypes = collectAst(grammar); - - expect(allTypes.unions.map(toSubstring).join(EOL).trim()).toBe(expandToString` - export type Expression = MemberAccess | PrimaryExpression | SuperMemberAccess; - export type PrimaryExpression = BooleanLiteral; - `); - - const allInterfacesString = allTypes.interfaces.map(toSubstring).join(EOL).trim(); - expect(allInterfacesString).toBe(expandToString` + async function checkTypes(grammar: string): Promise { + await expectTypes(grammar, expandToString` export interface BooleanLiteral extends AstNode { readonly $container: MemberAccess; readonly $type: 'BooleanLiteral'; @@ -837,12 +438,9 @@ describe('expression rules with inferred and declared interfaces', () => { readonly $container: MemberAccess; readonly $type: 'SuperMemberAccess'; } + export type Expression = MemberAccess | PrimaryExpression | SuperMemberAccess; + export type PrimaryExpression = BooleanLiteral; `); - - // the idea of the following is to double check that the declared definitions don't overwrite any - // inferred definition; because of the sorting before joining the 'startsWith' doesn't work in general, - // but it does work here due to the smart rule and interface name choice ;-) - expect(allInterfacesString.startsWith(inferredInterfacesString)); } }); @@ -850,21 +448,13 @@ describe('types of `$container` and `$type` are correct', () => { // `$container`-types are appear only from inferred types test('types of `$container` and `$type` for declared types', async () => { - const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar; - const document = await parseHelper(grammarServices)(` + await expectTypes(` interface A { strA: string } interface B { strB: string } interface C extends A, B { strC: string } interface D { a: A } interface E { b: B } - `); - const { unions, interfaces } = collectAst(document.parseResult.value); - - const unionsString = unions.map(toSubstring).join(EOL).trim(); - expect(unionsString).toBe(expandToString``); - - const interfacesString = sortInterfacesTopologically(interfaces).map(toSubstring).join(EOL).trim(); - expect(interfacesString).toBe(expandToString` + `, expandToString` export interface A extends AstNode { readonly $container: D | E; readonly $type: 'A' | 'C'; @@ -892,25 +482,14 @@ describe('types of `$container` and `$type` are correct', () => { }); test('types of `$container` and `$type` for inferred types', async () => { - const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar; - const document = await parseHelper(grammarServices)(` + await expectTypes(` terminal ID: /[_a-zA-Z][\\w_]*/; A: 'A' C; B: 'B' C; C: 'C' strC=ID; D: 'D' a=A; E: 'E' b=B; - `); - const { unions, interfaces } = collectAst(document.parseResult.value); - - const unionsString = unions.map(toSubstring).join(EOL).trim(); - expect(unionsString).toBe(expandToString` - export type A = C; - export type B = C; - `); - - const interfacesString = sortInterfacesTopologically(interfaces).map(toSubstring).join(EOL).trim(); - expect(interfacesString).toBe(expandToString` + `, expandToString` export interface C extends AstNode { readonly $container: D | E; readonly $type: 'C'; @@ -924,12 +503,13 @@ describe('types of `$container` and `$type` are correct', () => { readonly $type: 'E'; b: B } + export type A = C; + export type B = C; `); }); test('types of `$container` and `$type` for inferred and declared types', async () => { - const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar; - const document = await parseHelper(grammarServices)(` + await expectTypes(` A: 'A' strA=ID; B: 'B' strB=ID; C returns C: 'C' strA=ID strB=ID strC=ID; @@ -937,14 +517,7 @@ describe('types of `$container` and `$type` are correct', () => { D: 'D' a=A; E: 'E' b=B; terminal ID: /[a-zA-Z_][a-zA-Z0-9_]*/; - `); - const { unions, interfaces } = collectAst(document.parseResult.value); - - const unionsString = unions.map(toSubstring).join(EOL).trim(); - expect(unionsString).toBe(expandToString``); - - const interfacesString = interfaces.map(toSubstring).join(EOL).trim(); - expect(interfacesString).toBe(expandToString` + `, expandToString` export interface A extends AstNode { readonly $container: D | E; readonly $type: 'A' | 'C'; @@ -969,7 +542,6 @@ describe('types of `$container` and `$type` are correct', () => { strC: string } `); - }); }); @@ -995,70 +567,77 @@ describe('generated types from declared types include all of them', () => { `); const typesWithDeclared = collectAst(documentWithDeclaredTypes.parseResult.value); - expect(typesWithDeclared.unions.map(toSubstring).join(EOL).trim()) - .toBe(types.unions.map(toSubstring).join(EOL).trim()); + expect(typesWithDeclared.unions.map(e => e.toAstTypesString(false)).join(EOL).trim()) + .toBe(types.unions.map(e => e.toAstTypesString(false)).join(EOL).trim()); - expect(typesWithDeclared.interfaces.map(toSubstring).join(EOL).trim()) - .toBe(types.interfaces.map(toSubstring).join(EOL).trim()); + expect(typesWithDeclared.interfaces.map(e => e.toAstTypesString(false)).join(EOL).trim()) + .toBe(types.interfaces.map(e => e.toAstTypesString(false)).join(EOL).trim()); }); }); +// TODO @msujew: Test case for https://github.com/langium/langium/issues/775 +// describe('type merging runs in non-exponential time', () => { +// +// test('grammar with many optional groups is processed correctly', async () => { +// const grammarServices = createLangiumGrammarServices(EmptyFileSystem).grammar; +// const document = await parseHelper(grammarServices)(` +// grammar Test +// +// entry Model: +// (title1=INT ';')? +// (title2=INT ';')? +// (title3=INT ';')? +// (title4=INT ';')? +// (title5=INT ';')? +// (title6=INT ';')? +// (title7=INT ';')? +// (title8=INT ';')? +// (title9=INT ';')? +// (title10=INT ';')? +// (title11=INT ';')? +// (title12=INT ';')? +// (title13=INT ';')? +// (title14=INT ';')? +// (title15=INT ';')? +// (title16=INT ';')? +// (title17=INT ';')? +// (title18=INT ';')? +// (title19=INT ';')? +// (title20=INT ';')? +// (title21=INT ';')? +// (title22=INT ';')? +// (title23=INT ';')? +// (title24=INT ';')? +// (title25=INT ';')? +// (title26=INT ';')? +// (title27=INT ';')? +// (title28=INT ';')? +// (title29=INT ';')? +// (title30=INT ';')? +// ; +// terminal INT returns number: ('0'..'9')+; +// `); +// const { interfaces } = collectAst(document.parseResult.value); +// const model = interfaces[0]; +// expect(model.properties).toHaveLength(30); +// expect(model.properties.every(e => e.optional)).toBeTruthy(); +// }); +// +// }); + +const services = createLangiumGrammarServices(EmptyFileSystem).grammar; + async function getTypes(grammar: string): Promise { - const services = createLangiumGrammarServices(EmptyFileSystem).grammar; + await clearDocuments(services); const helper = parseHelper(services); const result = await helper(grammar); const gram = result.parseResult.value; - return collectTypeResources(gram).inferred; -} - -function getType(types: AstTypes, name: string): InterfaceType | UnionType | undefined { - const interfaceType = types.interfaces.find(e => e.name === name); - const unionType = types.unions.find(e => e.name === name); - return interfaceType || unionType; -} - -// general purpose property getter for interfaces, does not verify the property exists -function getProperty(interfaceType: InterfaceType, property: string): Property | undefined { - return interfaceType.properties.find(e => e.name === property); -} - -function expectProperty(interfaceType: InterfaceType, property: Property | string): void { - if (typeof property === 'string') { - const prop = getProperty(interfaceType, property)!; - expect(prop).toBeDefined(); - expect(prop.optional).toStrictEqual(false); - } else { - const prop = getProperty(interfaceType, property.name)!; - expect(prop).toBeDefined(); - expect(prop.optional).toStrictEqual(property.optional); - expect(prop.typeAlternatives.length).toStrictEqual(property.typeAlternatives.length); - for (let i = 0; i < prop.typeAlternatives.length; i++) { - const actualType = prop.typeAlternatives[i]; - const expectedType = property.typeAlternatives[i]; - expect(actualType.types).toEqual(expectedType.types); - expect(actualType.array).toEqual(expectedType.array); - expect(actualType.reference).toEqual(expectedType.reference); - } - } + return collectAst(gram); } -function expectUnion(unionType: UnionType, types: PropertyType[]): void { - expect(unionType.alternatives.length).toStrictEqual(types.length); - for (let i = 0; i < unionType.alternatives.length; i++) { - const actualType = unionType.alternatives[i]; - const expectedType = types[i]; - expect(actualType.types).toEqual(expectedType.types); - expect(actualType.array).toEqual(expectedType.array); - expect(actualType.reference).toEqual(expectedType.reference); - } +async function expectTypes(grammar: string, types: string): Promise { + const grammarTypes = await getTypes(grammar); + const allTypes = mergeTypesAndInterfaces(grammarTypes); + expect(allTypes.map(type => type.toAstTypesString(false)).join('').trim()).toBe(types); } - -const toSubstring = (o: { toAstTypesString: () => string }) => { - // this specialized 'toString' function uses the default 'toString' that is producing the - // code generation output, and strips everything not belonging to the actual interface/type declaration - const sRep = o.toAstTypesString(); - return sRep.substring( - 0, 1 + (sRep.includes('interface') ? sRep.indexOf('}') : Math.min(sRep.indexOf(';') )) - ); -}; diff --git a/packages/langium/test/grammar/type-system/type-validator.test.ts b/packages/langium/test/grammar/type-system/type-validator.test.ts index c16804e43..c54adcac6 100644 --- a/packages/langium/test/grammar/type-system/type-validator.test.ts +++ b/packages/langium/test/grammar/type-system/type-validator.test.ts @@ -366,8 +366,7 @@ describe('Property type is not a mix of cross-ref and non-cross-ref types.', () expect(attribute).not.toBe(undefined); expectError(validation, /Mixing a cross-reference with other types is not supported. Consider splitting property /, { - node: attribute!, - property: 'typeAlternatives' + node: attribute }); }); }); @@ -492,7 +491,7 @@ describe('Property types validation takes in account types hierarchy', () => { `); const assignment = streamAllContents(validation.document.parseResult.value).filter(isAssignment).toArray()[0]; - expectError(validation, "The assigned type 'Z2' is not compatible with the declared property 'y' of type 'Z1': 'Z2' is not expected.", { + expectError(validation, "The assigned type 'Z2' is not compatible with the declared property 'y' of type 'Z1'.", { node: assignment, property: 'feature' }); diff --git a/packages/langium/test/lsp/find-references.test.ts b/packages/langium/test/lsp/find-references.test.ts index 0defdf446..a26e2ea46 100644 --- a/packages/langium/test/lsp/find-references.test.ts +++ b/packages/langium/test/lsp/find-references.test.ts @@ -265,7 +265,7 @@ describe('findReferences', () => { type C = A | B ; - ruleC returns C: <|na<|>me|>=ID; + ruleC returns C: {A} <|na<|>me|>=ID; `; await findReferences({ @@ -312,30 +312,6 @@ describe('findReferences', () => { }); }); - test('Must find references to a property assigned in action returning union type', async () => { - const grammar = `grammar test - hidden terminal WS: /\\s+/; - terminal ID: /\\w+/; - - interface A { - <|na<|>me|>: string - } - - interface B { - foo: string - } - - type C = A | B; - - ActionRule: {C} <|na<|>me|>=ID; - `; - - await findReferences({ - text: grammar, - includeDeclaration: true - }); - }); - test('Must find references to property assigned in parser rule returning child of child', async () =>{ const grammar = `grammar test hidden terminal WS: /\\s+/; diff --git a/packages/langium/test/parser/langium-parser-builder.test.ts b/packages/langium/test/parser/langium-parser-builder.test.ts index 807b5c7bd..e3074519b 100644 --- a/packages/langium/test/parser/langium-parser-builder.test.ts +++ b/packages/langium/test/parser/langium-parser-builder.test.ts @@ -315,7 +315,7 @@ describe('Boolean value converter', () => { describe('BigInt Parser value converter', () => { const content = ` grammar G - entry M: value=BIGINT; + entry M: value=BIGINT?; terminal BIGINT returns bigint: /[0-9]+/; hidden terminal WS: /\\s+/; `; @@ -339,6 +339,10 @@ describe('BigInt Parser value converter', () => { expectValue('149587349587234971', BigInt('149587349587234971')); expectValue('9007199254740991', BigInt('9007199254740991')); // === 0x1fffffffffffff }); + + test('Missing value is implicitly undefined', () => { + expectValue('', undefined); + }); }); describe('Date Parser value converter', () => { @@ -438,25 +442,25 @@ describe('Parser calls value converter', () => { test('Should parse bool correctly', () => { expectValue('b true', true); - // this is the current 'boolean' behavior when a prop type can't be resolved to just a boolean - // either true/undefined, no false in this case - expectValue('b false', undefined); - // ...then no distinguishing between the bad parse case when the type is unclear - expectValue('b asdfg', undefined); + expectValue('b false', false); + // Any value that cannot be parsed correctly is automatically false + expectValue('b asdfg', false); }); test('Should parse BigInt correctly', () => { expectValue('big 9007199254740991n', BigInt('9007199254740991')); - expectValue('big 9007199254740991', undefined); - expectValue('big 1.1', undefined); - expectValue('big -19458438592374', undefined); + // Any value that cannot be parsed correctly is automatically false + expectValue('big 9007199254740991', false); + expectValue('big 1.1', false); + expectValue('big -19458438592374', false); }); test('Should parse Date correctly', () => { expectEqual('d 2020-01-01', new Date('2020-01-01')); expectEqual('d 2020-01-01T00:00', new Date('2020-01-01T00:00')); expectEqual('d 2022-10-04T12:13', new Date('2022-10-04T12:13')); - expectEqual('d 2022-Peach', undefined); + // Any value that cannot be parsed correctly is automatically false + expectEqual('d 2022-Peach', false); }); });