From a2bb2eea145dd8e8459ab646e445e1d5f5e45b51 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 12 Nov 2024 14:28:40 +0100 Subject: [PATCH 01/19] first working version of type definitions in "wrong" order for classes #22, refactorings, improved identifiers (WIP) --- .../language/type-system/lox-type-checking.ts | 8 +- examples/lox/test/lox-type-checking.test.ts | 4 +- packages/typir/src/features/conversion.ts | 2 +- packages/typir/src/features/equality.ts | 2 +- packages/typir/src/features/inference.ts | 12 +- packages/typir/src/features/validation.ts | 19 +- packages/typir/src/graph/type-graph.ts | 45 +- packages/typir/src/graph/type-node.ts | 186 ++++++- packages/typir/src/index.ts | 1 + packages/typir/src/kinds/bottom-kind.ts | 7 +- packages/typir/src/kinds/class-kind.ts | 525 +++++++++++------- .../typir/src/kinds/fixed-parameters-kind.ts | 3 +- packages/typir/src/kinds/function-kind.ts | 27 +- packages/typir/src/kinds/multiplicity-kind.ts | 12 +- packages/typir/src/kinds/primitive-kind.ts | 7 +- packages/typir/src/kinds/top-kind.ts | 7 +- .../typir/src/utils/type-initialization.ts | 50 ++ packages/typir/src/utils/utils-definitions.ts | 398 ++++++++++++- packages/typir/test/type-definitions.test.ts | 18 +- 19 files changed, 1041 insertions(+), 292 deletions(-) create mode 100644 packages/typir/src/utils/type-initialization.ts diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 72dc663..b67cd99 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -218,7 +218,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // class types (nominal typing): if (isClass(node)) { const className = node.name; - const classType = this.classKind.getOrCreateClassType({ + const classType = this.classKind.createClassType({ className, superClasses: node.superClass?.ref, // note that type inference is used here; TODO delayed fields: node.members @@ -245,13 +245,15 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { }, // inference rule for accessing fields inferenceRuleForFieldAccess: (domainElement: unknown) => isMemberCall(domainElement) && isFieldMember(domainElement.element?.ref) && domainElement.element!.ref.$container === node - ? domainElement.element!.ref.name : 'N/A', // as an alternative, use 'InferenceRuleNotApplicable' instead, what should we recommend? + ? domainElement.element!.ref.name : InferenceRuleNotApplicable, }); // TODO conversion 'nil' to classes ('TopClass')! // any class !== all classes; here we want to say, that 'nil' is assignable to each concrete Class type! // this.typir.conversion.markAsConvertible(typeNil, this.classKind.getOrCreateTopClassType({}), 'IMPLICIT_EXPLICIT'); - this.typir.conversion.markAsConvertible(this.primitiveKind.getPrimitiveType({ primitiveName: 'nil' })!, classType, 'IMPLICIT_EXPLICIT'); + classType.addListener(type => { + this.typir.conversion.markAsConvertible(this.primitiveKind.getPrimitiveType({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT'); + }); } } } diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index e23fd85..a41125f 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -319,7 +319,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance describe('LOX', () => { // this test case will work after having the support for cyclic type definitions, since it will solve also issues with topological order of type definitions - test.todo('complete with difficult order of classes', async () => await validate(` + test.only('complete with difficult order of classes', async () => await validate(` class SuperClass { a: number } @@ -340,7 +340,7 @@ describe('LOX', () => { var x = SubClass(); // Assigning nil to a class type var nilTest = SubClass(); - nilTest = nil; + // nilTest = nil; // TODO failed // Accessing members of a class var value = x.nested.method() + "wasd"; diff --git a/packages/typir/src/features/conversion.ts b/packages/typir/src/features/conversion.ts index 48c2e60..f336807 100644 --- a/packages/typir/src/features/conversion.ts +++ b/packages/typir/src/features/conversion.ts @@ -142,7 +142,7 @@ export class DefaultTypeConversion implements TypeConversion { */ const hasIntroducedCycle = this.existsEdgePath(from, from, mode); if (hasIntroducedCycle) { - throw new Error(`Adding the conversion from ${from.identifier} to ${to.identifier} with mode ${mode} has introduced a cycle in the type graph.`); + throw new Error(`Adding the conversion from ${from.getIdentifier()} to ${to.getIdentifier()} with mode ${mode} has introduced a cycle in the type graph.`); } } } diff --git a/packages/typir/src/features/equality.ts b/packages/typir/src/features/equality.ts index 8de6972..2dc1c65 100644 --- a/packages/typir/src/features/equality.ts +++ b/packages/typir/src/features/equality.ts @@ -99,7 +99,7 @@ export class DefaultTypeEquality implements TypeEquality { if (type1 === type2) { return undefined; } - if (type1.identifier === type2.identifier) { // this works, since identifiers are unique! + if (type1.getIdentifier() === type2.getIdentifier()) { // this works, since identifiers are unique! return undefined; } diff --git a/packages/typir/src/features/inference.ts b/packages/typir/src/features/inference.ts index 94a0de5..81bc725 100644 --- a/packages/typir/src/features/inference.ts +++ b/packages/typir/src/features/inference.ts @@ -118,7 +118,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty } addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void { - const key = boundToType?.identifier ?? ''; + const key = this.getBoundToTypeKey(boundToType); let rules = this.inferenceRules.get(key); if (!rules) { rules = []; @@ -127,6 +127,10 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty rules.push(rule); } + protected getBoundToTypeKey(boundToType?: Type): string { + return boundToType?.getIdentifier() ?? ''; + } + inferType(domainElement: unknown): Type | InferenceProblem[] { // is the result already in the cache? const cached = this.cacheGet(domainElement); @@ -264,11 +268,11 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty /* Get informed about deleted types in order to remove inference rules which are bound to them. */ - addedType(_newType: Type): void { + addedType(_newType: Type, _key: string): void { // do nothing } - removedType(type: Type): void { - this.inferenceRules.delete(type.identifier); + removedType(type: Type, _key: string): void { + this.inferenceRules.delete(this.getBoundToTypeKey(type)); } addedEdge(_edge: TypeEdge): void { // do nothing diff --git a/packages/typir/src/features/validation.ts b/packages/typir/src/features/validation.ts index a4a9240..39477ec 100644 --- a/packages/typir/src/features/validation.ts +++ b/packages/typir/src/features/validation.ts @@ -105,7 +105,7 @@ export class DefaultValidationConstraints implements ValidationConstraints { domainProperty: details.domainProperty, domainIndex: details.domainIndex, severity: details.severity ?? 'error', - message: details.message ?? `'${actualType.identifier}' is ${negated ? '' : 'not '}related to '${expectedType.identifier}' regarding ${strategy}.`, + message: details.message ?? `'${actualType.getIdentifier()}' is ${negated ? '' : 'not '}related to '${expectedType.getIdentifier()}' regarding ${strategy}.`, subProblems: [comparisonResult] }]; } @@ -118,7 +118,7 @@ export class DefaultValidationConstraints implements ValidationConstraints { domainProperty: details.domainProperty, domainIndex: details.domainIndex, severity: details.severity ?? 'error', - message: details.message ?? `'${actualType.identifier}' is ${negated ? '' : 'not '}related to '${expectedType.identifier}' regarding ${strategy}.`, + message: details.message ?? `'${actualType.getIdentifier()}' is ${negated ? '' : 'not '}related to '${expectedType.getIdentifier()}' regarding ${strategy}.`, subProblems: [] // no sub-problems are available! }]; } else { @@ -209,7 +209,7 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap } addValidationRule(rule: ValidationRule, boundToType?: Type): void { - const key = boundToType?.identifier ?? ''; + const key = this.getBoundToTypeKey(boundToType); let rules = this.validationRules.get(key); if (!rules) { rules = []; @@ -219,7 +219,7 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap } addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void { - const key = boundToType?.identifier ?? ''; + const key = this.getBoundToTypeKey(boundToType); let rules = this.validationRulesBeforeAfter.get(key); if (!rules) { rules = []; @@ -228,15 +228,18 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap rules.push(rule); } + protected getBoundToTypeKey(boundToType?: Type): string { + return boundToType?.getIdentifier() ?? ''; + } /* Get informed about deleted types in order to remove validation rules which are bound to them. */ - addedType(_newType: Type): void { + addedType(_newType: Type, _key: string): void { // do nothing } - removedType(type: Type): void { - this.validationRules.delete(type.identifier); - this.validationRulesBeforeAfter.delete(type.identifier); + removedType(type: Type, _key: string): void { + this.validationRules.delete(this.getBoundToTypeKey(type)); + this.validationRulesBeforeAfter.delete(this.getBoundToTypeKey(type)); } addedEdge(_edge: TypeEdge): void { // do nothing diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index a0ecc44..1eff152 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -24,21 +24,24 @@ export class TypeGraph { protected readonly listeners: TypeGraphListener[] = []; /** - * Usually this method is called by kinds after creating a a corresponding type. + * Usually this method is called by kinds after creating a corresponding type. * Therefore it is usually not needed to call this method in an other context. * @param type the new type + * @param key an optional key to register the type, since it is allowed to register the same type with different keys in the graph + * TODO oder stattdessen einen ProxyType verwenden? wie funktioniert das mit isClassType und isSubType? wie funktioniert removeType? */ - addNode(type: Type): void { - const key = type.identifier; - if (this.nodes.has(key)) { - if (this.nodes.get(key) === type) { + addNode(type: Type, key?: string): void { + // TODO überprüfen, dass Identifiable-State erreicht ist?? + const mapKey = key ?? type.getIdentifier(); + if (this.nodes.has(mapKey)) { + if (this.nodes.get(mapKey) === type) { // this type is already registered => that is OK } else { - throw new Error(`Names of types must be unique: ${key}`); + throw new Error(`Names of types must be unique: ${mapKey}`); } } else { - this.nodes.set(key, type); - this.listeners.forEach(listener => listener.addedType(type)); + this.nodes.set(mapKey, type); + this.listeners.forEach(listener => listener.addedType(type, mapKey)); } } @@ -48,28 +51,32 @@ export class TypeGraph { * This is the central API call to remove a type from the type system in case that it is no longer valid/existing/needed. * It is not required to directly inform the kind of the removed type yourself, since the kind itself will take care of removed types. * @param type the type to remove + * @param key an optional key to register the type, since it is allowed to register the same type with different keys in the graph */ - removeNode(type: Type): void { - const key = type.identifier; + removeNode(type: Type, key?: string): void { + const mapKey = key ?? type.getIdentifier(); // remove all edges which are connected to the type to remove type.getAllIncomingEdges().forEach(e => this.removeEdge(e)); type.getAllOutgoingEdges().forEach(e => this.removeEdge(e)); // remove the type itself - const contained = this.nodes.delete(key); + const contained = this.nodes.delete(mapKey); if (contained) { - this.listeners.forEach(listener => listener.removedType(type)); + this.listeners.forEach(listener => listener.removedType(type, mapKey)); } else { - throw new Error(`Type does not exist: ${key}`); + throw new Error(`Type does not exist: ${mapKey}`); } } - getNode(name: string): Type | undefined { - return this.nodes.get(name); + getNode(key: string): Type | undefined { + return this.nodes.get(key); } - getType(name: string): Type | undefined { - return this.getNode(name); + getType(key: string): Type | undefined { + return this.getNode(key); } + getAllRegisteredTypes(): Type[] { + return [...this.nodes.values()]; + } addEdge(edge: TypeEdge): void { // check constraints: no duplicated edges (same values for: from, to, $relation) @@ -129,8 +136,8 @@ export class TypeGraph { } export interface TypeGraphListener { - addedType(type: Type): void; - removedType(type: Type): void; + addedType(type: Type, key: string): void; + removedType(type: Type, key: string): void; addedEdge(edge: TypeEdge): void; removedEdge(edge: TypeEdge): void; } diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 32f5655..e21a813 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -5,9 +5,18 @@ ******************************************************************************/ import { Kind, isKind } from '../kinds/kind.js'; -import { TypirProblem } from '../utils/utils-definitions.js'; +import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForResolvedTypeReferences } from '../utils/utils-definitions.js'; +import { assertTrue, assertUnreachable } from '../utils/utils.js'; import { TypeEdge } from './type-edge.js'; +// export type TypeInitializationState = 'Created' | 'Identifiable' | 'Completed'; +export type TypeInitializationState = 'Invalid' | 'Identifiable' | 'Completed'; + +export interface PreconditionsForInitializationState { + refsToBeIdentified?: TypeReference[]; // or later/more + refsToBeCompleted?: TypeReference[]; // or later/more +} + /** * Design decisions: * - features of types are realized/determined by their kinds @@ -15,27 +24,34 @@ import { TypeEdge } from './type-edge.js'; */ export abstract class Type { readonly kind: Kind; // => $kind: string, required for isXType() checks - /** - * Identifiers must be unique and stable for all types known in a single Typir instance, since they are used as key to store types in maps. - * Identifiers might have a naming schema for calculatable values. - */ /* Design decision for the name of this attribute * - identifier * - ID: sounds like an arbitrary, internal value without schema behind * - name: what is the name of a union type? + * 'undefined' is required for cases, when the identifier is calculated later, since required information is not yet available. */ - readonly identifier: string; + protected identifier: string | undefined; // this is required only to apply graph algorithms in a generic way! // $relation is used as key protected readonly edgesIncoming: Map = new Map(); protected readonly edgesOutgoing: Map = new Map(); - constructor(identifier: string) { + constructor(identifier: string | undefined) { this.identifier = identifier; } + /** + * Identifiers must be unique and stable for all types known in a single Typir instance, since they are used as key to store types in maps. + * Identifiers might have a naming schema for calculatable values. + */ + getIdentifier(): string { + this.assertStateOrLater('Identifiable'); + assertTrue(this.identifier !== undefined); + return this.identifier; + } + /** * Returns a string value containing a short representation of the type to be shown to users of the type-checked elements. * This value don't need to be unique for all types. @@ -55,6 +71,152 @@ export abstract class Type { abstract getUserRepresentation(): string; + + protected initialization: TypeInitializationState = 'Invalid'; // TODO or Identifiable + + getInitializationState(): TypeInitializationState { + return this.initialization; + } + + protected assertState(expectedState: TypeInitializationState): void { + if (this.isInState(expectedState) === false) { + throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but ${expectedState} is expected.`); + } + } + protected assertNotState(expectedState: TypeInitializationState): void { + if (this.isNotInState(expectedState) === false) { + throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but this state is not expected.`); + } + } + protected assertStateOrLater(expectedState: TypeInitializationState): void { + if (this.isInStateOrLater(expectedState) === false) { + throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but this state is not expected.`); + } + } + + isInState(state: TypeInitializationState): boolean { + return this.initialization === state; + } + isNotInState(state: TypeInitializationState): boolean { + return this.initialization !== state; + } + isInStateOrLater(state: TypeInitializationState): boolean { + switch (state) { + case 'Invalid': + return true; + case 'Identifiable': + return this.initialization !== 'Invalid'; + case 'Completed': + return this.initialization === 'Completed'; + default: + assertUnreachable(state); + } + } + + // manage listeners for updated state of the current type + + protected stateListeners: TypeStateListener[] = []; + + addListener(listener: TypeStateListener, informIfAlreadyFulfilled: boolean): void { + this.stateListeners.push(listener); + if (informIfAlreadyFulfilled) { + const currentState = this.getInitializationState(); + switch (currentState) { + case 'Invalid': + // TODO? + break; + case 'Identifiable': + listener.switchedToIdentifiable(this); + break; + case 'Completed': + listener.switchedToIdentifiable(this); + listener.switchedToCompleted(this); + break; + default: + assertUnreachable(currentState); + } + } + } + + removeListener(listener: TypeStateListener): void { + const index = this.stateListeners.indexOf(listener); + if (index >= 0) { + this.stateListeners.splice(index, 1); + } + } + + + // to be called at the end of the constructor of each specific Type implementation! + protected completeInitialization(preconditions: { + preconditionsForInitialization?: PreconditionsForInitializationState, + preconditionsForCompletion?: PreconditionsForInitializationState, + referencesRelevantForInvalidation?: TypeReference[], + onIdentification?: () => void, + onCompletion?: () => void, + onInvalidation?: () => void, + }): void { + // specify the preconditions: + // invalid --> identifiable + const init1 = new WaitingForResolvedTypeReferences( + preconditions.preconditionsForInitialization?.refsToBeIdentified, + preconditions.preconditionsForInitialization?.refsToBeCompleted, + ); + // identifiable --> completed + const init2 = new WaitingForResolvedTypeReferences( + preconditions.preconditionsForCompletion?.refsToBeIdentified, + preconditions.preconditionsForCompletion?.refsToBeCompleted, + ); + // completed --> invalid, TODO wie genau wird das realisiert?? triggert jetzt schon!! + const init3 = new WaitingForInvalidTypeReferences( + preconditions.referencesRelevantForInvalidation ?? [], + ); + + // store the reactions + this.onIdentification = preconditions.onIdentification ?? (() => {}); + this.onCompletion = preconditions.onCompletion ?? (() => {}); + this.onInvalidation = preconditions.onInvalidation ?? (() => {}); + + // specify the transitions between the states: + init1.addListener(() => this.switchFromInvalidToIdentifiable(), true); + init2.addListener(() => { + if (init1.isFulfilled()) { + this.switchFromIdentifiableToCompleted(); + } else { + // TODO ?? + } + }, true); + init3.addListener(() => this.switchFromCompleteOrIdentifiableToInvalid(), false); // no initial trigger! + // TODO noch sicherstellen, dass keine Phasen übersprungen werden?? + // TODO trigger start?? + } + + protected onIdentification: () => void; // typical use cases: calculate the identifier + protected onCompletion: () => void; // typical use cases: determine all properties which depend on other types to be created + protected onInvalidation: () => void; // TODO ist jetzt anders; typical use cases: register inference rules for the type object already now! + + protected switchFromInvalidToIdentifiable(): void { + this.assertState('Invalid'); + this.onIdentification(); + this.initialization = 'Identifiable'; + this.stateListeners.forEach(listener => listener.switchedToIdentifiable(this)); + } + + protected switchFromIdentifiableToCompleted(): void { + this.assertState('Identifiable'); + this.onCompletion(); + this.initialization = 'Completed'; + this.stateListeners.forEach(listener => listener.switchedToCompleted(this)); + } + + protected switchFromCompleteOrIdentifiableToInvalid(): void { + this.assertNotState('Invalid'); + this.onInvalidation(); + this.initialization = 'Invalid'; + this.stateListeners.forEach(listener => listener.switchedToInvalid(this)); + } + + + /** * Analyzes, whether two types are equal. * @param otherType to be compared with the current type @@ -162,5 +324,13 @@ export abstract class Type { } export function isType(type: unknown): type is Type { - return typeof type === 'object' && type !== null && typeof (type as Type).identifier === 'string' && isKind((type as Type).kind); + return typeof type === 'object' && type !== null && typeof (type as Type).getIdentifier === 'function' && isKind((type as Type).kind); +} + + +export interface TypeStateListener { + switchedToInvalid(type: Type): void; + switchedToIdentifiable(type: Type): void; + switchedToCompleted(type: Type): void; } +// TODO brauchen wir das überhaupt? stattdessen in TypeReference direkt realisieren? diff --git a/packages/typir/src/index.ts b/packages/typir/src/index.ts index b38db96..71d0d3d 100644 --- a/packages/typir/src/index.ts +++ b/packages/typir/src/index.ts @@ -26,5 +26,6 @@ export * from './kinds/primitive-kind.js'; export * from './kinds/top-kind.js'; export * from './utils/dependency-injection.js'; export * from './utils/utils.js'; +export * from './utils/type-initialization.js'; export * from './utils/utils-definitions.js'; export * from './utils/utils-type-comparison.js'; diff --git a/packages/typir/src/kinds/bottom-kind.ts b/packages/typir/src/kinds/bottom-kind.ts index 53d22cb..5d9672b 100644 --- a/packages/typir/src/kinds/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom-kind.ts @@ -20,14 +20,15 @@ export class BottomType extends Type { constructor(kind: BottomKind, identifier: string) { super(identifier); this.kind = kind; + this.completeInitialization({}); // no preconditions } override getName(): string { - return this.identifier; + return this.getIdentifier(); } override getUserRepresentation(): string { - return this.identifier; + return this.getIdentifier(); } override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { @@ -86,7 +87,7 @@ export const BottomKindName = 'BottomKind'; export class BottomKind implements Kind { readonly $name: 'BottomKind'; readonly services: TypirServices; - readonly options: BottomKindOptions; + readonly options: Readonly; protected instance: BottomType | undefined; constructor(services: TypirServices, options?: Partial) { diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index e86c868..4f34b22 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -9,65 +9,105 @@ import { TypeEqualityProblem } from '../features/equality.js'; import { InferenceProblem, InferenceRuleNotApplicable } from '../features/inference.js'; import { SubTypeProblem } from '../features/subtype.js'; import { ValidationProblem, ValidationRuleWithBeforeAfter } from '../features/validation.js'; -import { Type, isType } from '../graph/type-node.js'; +import { Type, TypeStateListener, isType } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; -import { TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; +import { TypeReference, TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; import { IndexedTypeConflict, MapListConverter, TypeCheckStrategy, checkNameTypesMap, checkValueForConflict, createKindConflict, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; import { assertTrue, assertType, toArray } from '../utils/utils.js'; import { CreateFunctionTypeDetails, FunctionKind, FunctionKindName, FunctionType, isFunctionKind, isFunctionType } from './function-kind.js'; import { Kind, isKind } from './kind.js'; +import { TypeInitializer } from '../utils/type-initialization.js'; +// TODO irgendwann die Dateien auseinander ziehen und Packages einführen! + +// TODO wenn die Initialisierung von ClassType abgeschlossen ist, sollte darüber aktiv benachrichtigt werden! export class ClassType extends Type { override readonly kind: ClassKind; readonly className: string; /** The super classes are readonly, since they might be used to calculate the identifier of the current class, which must be stable. */ - protected readonly superClasses: readonly ClassType[]; // if necessary, the array could be replaced by Map: name/form -> ClassType, for faster look-ups + protected superClasses: Array>; // if necessary, the array could be replaced by Map: name/form -> ClassType, for faster look-ups protected readonly subClasses: ClassType[] = []; // additional sub classes might be added later on! - protected readonly fields: FieldDetails[]; - protected readonly methods: MethodDetails[]; + protected readonly fields: Map = new Map(); // unordered + protected methods: MethodDetails[]; // unordered - constructor(kind: ClassKind, identifier: string, typeDetails: ClassTypeDetails) { - super(identifier); + constructor(kind: ClassKind, typeDetails: ClassTypeDetails) { + super(undefined); this.kind = kind; this.className = typeDetails.className; // resolve the super classes this.superClasses = toArray(typeDetails.superClasses).map(superr => { - const cls = resolveTypeSelector(this.kind.services, superr); - assertType(cls, isClassType); - return cls; + const superRef = new TypeReference(superr, kind.services); + superRef.addReactionOnTypeCompleted((_ref, superType) => { + // after the super-class is complete, register this class as sub-class for that super-class + superType.subClasses.push(this); + }, true); + superRef.addReactionOnTypeUnresolved((_ref, superType) => { + // if the superType gets invalid, de-register this class as sub-class of the super-class + superType.subClasses.splice(superType.subClasses.indexOf(this), 1); + }, true); + return superRef; }); - // register this class as sub-class for all super-classes - this.getDeclaredSuperClasses().forEach(superr => superr.subClasses.push(this)); - // check number of allowed super classes - if (this.kind.options.maximumNumberOfSuperClasses >= 0) { - if (this.kind.options.maximumNumberOfSuperClasses < this.getDeclaredSuperClasses().length) { - throw new Error(`Only ${this.kind.options.maximumNumberOfSuperClasses} super-classes are allowed.`); - } - } - // check for cycles in sub-type-relationships - if (this.getAllSuperClasses(false).has(this)) { - throw new Error(`Circles in super-sub-class-relationships are not allowed: ${this.getName()}`); - } - // fields - this.fields = typeDetails.fields.map(field => { - name: field.name, - type: resolveTypeSelector(this.kind.services, field.type), - }); - // check collisions of field names - if (this.getFields(false).size !== typeDetails.fields.length) { - throw new Error('field names must be unique!'); - } + // resolve fields + typeDetails.fields + .map(field => { + name: field.name, + type: new TypeReference(field.type, kind.services), + }) + .forEach(field => { + if (this.fields.has(field.name)) { + // check collisions of field names + throw new Error(`The field name '${field.name}' is not unique for class '${this.className}'.`); + } else { + this.fields.set(field.name, field); + } + }); + const refFields: TypeReference[] = []; + [...this.fields.values()].forEach(f => refFields.push(f.type)); - // methods - this.methods = typeDetails.methods.map(method => { - const methodType = this.kind.getFunctionKind().getOrCreateFunctionType(method); - return { - type: methodType, - }; + // resolve methods + this.methods = typeDetails.methods.map(method => { + type: new TypeReference(kind.getMethodKind().createFunctionType(method), kind.services), + }); + const refMethods = this.methods.map(m => m.type); + // the uniqueness of methods can be checked with the predefined UniqueMethodValidation below + + // calculate the Identifier, based on the resolved type references + // const all: Array> = []; + const all: Array> = []; + all.push(...refFields); + all.push(...(refMethods as unknown as Array>)); // TODO dirty hack?! + // all.push(...refMethods); // does not work + + this.completeInitialization({ + preconditionsForInitialization: { + refsToBeIdentified: all, + }, + preconditionsForCompletion: { + refsToBeCompleted: this.superClasses as unknown as Array>, + }, + onIdentification: () => { + this.identifier = this.kind.calculateIdentifier(typeDetails); + // TODO identifier erst hier berechnen?! registering?? + }, + onCompletion: () => { + // when all super classes are completely available, do the following checks: + // check number of allowed super classes + if (this.kind.options.maximumNumberOfSuperClasses >= 0) { + if (this.kind.options.maximumNumberOfSuperClasses < this.getDeclaredSuperClasses().length) { + throw new Error(`Only ${this.kind.options.maximumNumberOfSuperClasses} super-classes are allowed.`); + } + } + // check for cycles in sub-type-relationships + if (this.getAllSuperClasses(false).has(this)) { + throw new Error(`Circles in super-sub-class-relationships are not allowed: ${this.getName()}`); + } + }, + onInvalidation: () => { + // TODO remove all listeners, ... + }, }); - // TODO check uniqueness?? } override getName(): string { @@ -75,16 +115,28 @@ export class ClassType extends Type { } override getUserRepresentation(): string { + const slots: string[] = []; // fields const fields: string[] = []; for (const field of this.getFields(false).entries()) { fields.push(`${field[0]}: ${field[1].getName()}`); } + if (fields.length >= 1) { + slots.push(fields.join(', ')); + } + // methods + const methods: string[] = []; + for (const method of this.getMethods(false)) { + methods.push(`${method.getUserRepresentation()}`); + } + if (methods.length >= 1) { + slots.push(methods.join(', ')); + } // super classes const superClasses = this.getDeclaredSuperClasses(); const extendedClasses = superClasses.length <= 0 ? '' : ` extends ${superClasses.map(c => c.getName()).join(', ')}`; - // whole representation - return `${this.className} { ${fields.join(', ')} }${extendedClasses}`; + // complete representation + return `${this.className}${extendedClasses} { ${slots.join(', ')} }`; } override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { @@ -95,7 +147,7 @@ export class ClassType extends Type { (t1, t2) => this.kind.services.equality.getTypeEqualityProblem(t1, t2)); } else if (this.kind.options.typing === 'Nominal') { // for nominal typing: - return checkValueForConflict(this.identifier, otherType.identifier, 'name'); + return checkValueForConflict(this.getIdentifier(), otherType.getIdentifier(), 'name'); } else { assertUnreachable(this.kind.options.typing); } @@ -187,8 +239,15 @@ export class ClassType extends Type { } } - getDeclaredSuperClasses(): readonly ClassType[] { - return this.superClasses; + getDeclaredSuperClasses(): ClassType[] { + return this.superClasses.map(superr => { + const superType = superr.getType(); + if (superType) { + return superType; + } else { + throw new Error('Not all super class types are resolved.'); + } + }); } getDeclaredSubClasses(): ClassType[] { @@ -253,15 +312,27 @@ export class ClassType extends Type { } } // own fields - this.fields.forEach(edge => { - result.set(edge.name, edge.type); + this.fields.forEach(fieldDetails => { + const field = fieldDetails.type.getType(); + if (field) { + result.set(fieldDetails.name, field); + } else { + throw new Error('Not all fields are resolved.'); + } }); return result; } getMethods(withSuperClassMethods: boolean): FunctionType[] { // own methods - const result: FunctionType[] = this.methods.map(m => m.type); + const result = this.methods.map(m => { + const method = m.type.getType(); + if (method) { + return method; + } else { + throw new Error('Not all methods are resolved.'); + } + }); // methods of super classes if (withSuperClassMethods) { for (const superClass of this.getDeclaredSuperClasses()) { @@ -294,7 +365,7 @@ export const ClassKindName = 'ClassKind'; export interface FieldDetails { name: string; - type: Type; + type: TypeReference; } export interface CreateFieldDetails { name: string; @@ -302,7 +373,7 @@ export interface CreateFieldDetails { } export interface MethodDetails { - type: FunctionType; + type: TypeReference; // methods might have some more properties in the future } @@ -337,13 +408,13 @@ export type InferClassLiteral = { export class ClassKind implements Kind { readonly $name: 'ClassKind'; readonly services: TypirServices; - readonly options: ClassKindOptions; + readonly options: Readonly; constructor(services: TypirServices, options?: Partial) { this.$name = ClassKindName; this.services = services; this.services.kinds.register(this); - this.options = { + this.options = { // TODO in eigene Methode auslagern! // the default values: typing: 'Nominal', maximumNumberOfSuperClasses: 1, @@ -355,167 +426,90 @@ export class ClassKind implements Kind { assertTrue(this.options.maximumNumberOfSuperClasses >= 0); // no negative values } - getClassType(typeDetails: ClassTypeDetails | string): ClassType | undefined { // string for nominal typing - const key = this.calculateIdentifier(typeof typeDetails === 'string' ? { className: typeDetails, fields: [], methods: [] } : typeDetails); - return this.services.graph.getType(key) as ClassType; - } + // zwei verschiedene Use cases für Calls: Reference/use (e.g. Var-Type) VS Creation (e.g. Class-Declaration) - getOrCreateClassType(typeDetails: CreateClassTypeDetails): ClassType { - const classType = this.getClassType(typeDetails); - if (classType) { - this.registerInferenceRules(typeDetails, classType); - return classType; + /** + * For the use case, that a type is used/referenced, e.g. to specify the type of a variable declaration. + * @param typeDetails all information needed to identify the class + * @returns a reference to the class type, which might be resolved in the future, if the class type does not yet exist + */ + getClassType(typeDetails: ClassTypeDetails | string): TypeReference { // string for nominal typing + if (typeof typeDetails === 'string') { + // nominal typing + return new TypeReference(typeDetails, this.services); + } else { + // structural typing + // TODO does this case occur in practise? + return new TypeReference(() => this.calculateIdentifier(typeDetails), this.services); } - return this.createClassType(typeDetails); - } - - createClassType(typeDetails: CreateClassTypeDetails): ClassType { - assertTrue(this.getClassType(typeDetails) === undefined, `${typeDetails.className}`); - - // create the class type - const classType = new ClassType(this, this.calculateIdentifier(typeDetails), typeDetails as CreateClassTypeDetails); - this.services.graph.addNode(classType); - - // register inference rules - this.registerInferenceRules(typeDetails, classType); - - return classType; } - protected registerInferenceRules(typeDetails: CreateClassTypeDetails, classType: ClassType) { - if (typeDetails.inferenceRuleForDeclaration) { - this.services.inference.addInferenceRule({ - inferTypeWithoutChildren(domainElement, _typir) { - if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { - return classType; - } else { - return InferenceRuleNotApplicable; - } - }, - inferTypeWithChildrensTypes(_domainElement, _childrenTypes, _typir) { - // TODO check values for fields for nominal typing! - return classType; - }, - }, classType); - } - if (typeDetails.inferenceRuleForLiteral) { - this.registerInferenceRuleForLiteral(typeDetails.inferenceRuleForLiteral, this, classType); - } - if (typeDetails.inferenceRuleForReference) { - this.registerInferenceRuleForLiteral(typeDetails.inferenceRuleForReference, this, classType); - } - if (typeDetails.inferenceRuleForFieldAccess) { - this.services.inference.addInferenceRule((domainElement, _typir) => { - const result = typeDetails.inferenceRuleForFieldAccess!(domainElement); - if (result === InferenceRuleNotApplicable) { - return InferenceRuleNotApplicable; - } else if (typeof result === 'string') { - // get the type of the given field name - const fieldType = classType.getFields(true).get(result); - if (fieldType) { - return fieldType; - } - return { - $problem: InferenceProblem, - domainElement, - inferenceCandidate: classType, - location: `unknown field '${result}'`, - // rule: this, // this does not work with functions ... - subProblems: [], - }; - } else { - return result; // do the type inference for this element instead - } - }, classType); - } - } + /** + * For the use case, that a new type needs to be created in Typir, e.g. for a class declaration. + * This function ensures, that the same type is created only once, even if this function is called multiple times, if e.g. the same type might be created for different type declaration. + * Nevertheless, usually a validation should produce an error in this case. + * @param typeDetails all information needed to create a new class + * @returns an initializer which creates and returns the new class type, when all depending types are resolved + */ + createClassType(typeDetails: CreateClassTypeDetails): TypeInitializer { + // assertTrue(this.getClassType(typeDetails) === undefined, `The class '${typeDetails.className}' already exists!`); // ensures, that no duplicated classes are created! - protected registerInferenceRuleForLiteral(rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): void { - const mapListConverter = new MapListConverter(); - this.services.inference.addInferenceRule({ - inferTypeWithoutChildren(domainElement, _typir) { - const result = rule.filter(domainElement); - if (result) { - const matching = rule.matching(domainElement); - if (matching) { - const inputArguments = rule.inputValuesForFields(domainElement); - if (inputArguments.size >= 1) { - return mapListConverter.toList(inputArguments); - } else { - // there are no operands to check - return classType; // this case occurs only, if the current class has no fields (including fields of super types) or is nominally typed - } - } else { - // the domain element is slightly different - } - } else { - // the domain element has a completely different purpose - } - // does not match at all - return InferenceRuleNotApplicable; - }, - inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { - const allExpectedFields = classType.getFields(true); - // this class type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 - const checkedFieldsProblems = checkNameTypesMap( - mapListConverter.toMap(childrenTypes), - allExpectedFields, - createTypeCheckStrategy(classKind.options.subtypeFieldChecking, typir) - ); - if (checkedFieldsProblems.length >= 1) { - // (only) for overloaded functions, the types of the parameters need to be inferred in order to determine an exact match - return { - $problem: InferenceProblem, - domainElement, - inferenceCandidate: classType, - location: 'values for fields', - rule: this, - subProblems: checkedFieldsProblems, - }; - } else { - // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors - return classType; - } - }, - }, classType); + return new ClassTypeInitializer(this.services, this, typeDetails); } - calculateIdentifier(typeDetails: ClassTypeDetails): string { - return this.printClassType(typeDetails); + getIdentifierPrefix(): string { + return this.options.identifierPrefix ? this.options.identifierPrefix + '-' : ''; } - protected printClassType(typeDetails: ClassTypeDetails): string { - const prefix = this.options.identifierPrefix; + /** + * TODO + * + * Design decisions: + * - This method is part of the ClassKind and not part of ClassType, since the ClassKind requires it for 'getClassType'! + * - The kind might use/add additional prefixes for the identifiers to make them "even more unique". + * + * @param typeDetails the details + * @returns the new identifier + */ + calculateIdentifier(typeDetails: ClassTypeDetails): string { // TODO kann keinen Identifier liefern, wenn noch nicht resolved! + // purpose of identifier: distinguish different types; NOT: not uniquely overloaded types + const prefix = this.getIdentifierPrefix(); if (this.options.typing === 'Structural') { // fields - const fields: string[] = []; - for (const [fieldNUmber, fieldDetails] of typeDetails.fields.entries()) { - fields.push(`${fieldNUmber}:${fieldDetails.name}`); - } + const fields: string = typeDetails.fields + .map(f => `${f.name}:${resolveTypeSelector(this.services, f.type)}`) // the names and the types of the fields are relevant, since different field types lead to different class types! + .sort() // the order of fields does not matter, therefore we need a stable order to make the identifiers comparable + .join(','); // methods - const methods: string[] = []; - for (const method of typeDetails.methods) { - const methodType = this.getFunctionKind().getOrCreateFunctionType(method); - methods.push(methodType.identifier); // TODO is ".identifier" too strict here? - } - // super classes - const superClasses = toArray(typeDetails.superClasses).map(selector => { - const type = resolveTypeSelector(this.services, selector); - assertType(type, isClassType); - return type; - }); - const extendedClasses = superClasses.length <= 0 ? '' : `-extends-${superClasses.map(c => c.identifier).join(',')}`; - // whole representation - return `${prefix}-${typeDetails.className}{${fields.join(',')}}{${methods.join(',')}}${extendedClasses}`; + const functionKind = this.getMethodKind(); + const methods: string = typeDetails.methods + .map(method => { + functionKind.getOrCreateFunctionType(method); // ensure, that the corresponding Type is existing in the type system + return functionKind.calculateIdentifier(method); // reuse the Identifier for Functions here! + }) + .sort() // the order of methods does not matter, therefore we need a stable order to make the identifiers comparable + .join(','); + // super classes (TODO oder strukturell per getAllSuperClassX lösen?!) + const superClasses: string = toArray(typeDetails.superClasses) + .map(selector => { + const type = resolveTypeSelector(this.services, selector); + assertType(type, isClassType); + return type.getIdentifier(); + }) + .sort() + .join(','); + // complete identifier (the name of the class does not matter for structural typing!) + return `${prefix}fields{${fields}}-methods{${methods}}-extends{${superClasses}}`; } else if (this.options.typing === 'Nominal') { - return `${prefix}-${typeDetails.className}`; + // only the name matters for nominal typing! + return `${prefix}${typeDetails.className}`; } else { assertUnreachable(this.options.typing); } } - getFunctionKind(): FunctionKind { - // ensure, that Typir uses the predefined 'function' kind + getMethodKind(): FunctionKind { + // ensure, that Typir uses the predefined 'function' kind for methods const kind = this.services.kinds.get(FunctionKindName); return isFunctionKind(kind) ? kind : new FunctionKind(this.services); } @@ -537,6 +531,145 @@ export function isClassKind(kind: unknown): kind is ClassKind { } +export class ClassTypeInitializer extends TypeInitializer implements TypeStateListener { + protected readonly typeDetails: CreateClassTypeDetails; + protected readonly kind: ClassKind; + + constructor(services: TypirServices, kind: ClassKind, typeDetails: CreateClassTypeDetails) { + super(services); + this.typeDetails = typeDetails; + this.kind = kind; + + // create the class type + const classType = new ClassType(kind, typeDetails as CreateClassTypeDetails); + if (kind.options.typing === 'Structural') { + // TODO Vorsicht Inference rules werden by default an den Identifier gebunden (ebenso Validations)! + this.services.graph.addNode(classType, kind.getIdentifierPrefix() + typeDetails.className); + // TODO hinterher wieder abmelden, wenn Type invalid geworden ist bzw. ein anderer Type gewonnen hat? bzw. gewinnt immer der erste Type? + } + + classType.addListener(this, true); // trigger directly, if some initialization states are already reached! + } + + switchedToIdentifiable(type: Type): void { + // TODO Vorsicht, dass hier nicht 2x derselbe Type angefangen wird zu erstellen und dann zwei Typen auf ihre Vervollständigung warten! + // 2x TypeResolver erstellen, beide müssen später denselben ClassType zurückliefern! + // bei Node { children: Node[] } muss der Zyklus erkannt und behandelt werden!! + this.producedType(type as ClassType); + } + + switchedToCompleted(classType: Type): void { + // register inference rules + // TODO or can this be done already after having the identifier? + registerInferenceRules(this.services, this.typeDetails, this.kind, classType as ClassType); + classType.removeListener(this); // the work of this initializer is done now + } + + switchedToInvalid(_type: Type): void { + // do nothing + } +} + + +function registerInferenceRules(services: TypirServices, typeDetails: CreateClassTypeDetails, classKind: ClassKind, classType: ClassType) { + if (typeDetails.inferenceRuleForDeclaration) { + services.inference.addInferenceRule({ + inferTypeWithoutChildren(domainElement, _typir) { + if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { + return classType; + } else { + return InferenceRuleNotApplicable; + } + }, + inferTypeWithChildrensTypes(_domainElement, _childrenTypes, _typir) { + // TODO check values for fields for nominal typing! + return classType; + }, + }, classType); + } + if (typeDetails.inferenceRuleForLiteral) { + registerInferenceRuleForLiteral(services, typeDetails.inferenceRuleForLiteral, classKind, classType); + } + if (typeDetails.inferenceRuleForReference) { + registerInferenceRuleForLiteral(services, typeDetails.inferenceRuleForReference, classKind, classType); + } + if (typeDetails.inferenceRuleForFieldAccess) { + services.inference.addInferenceRule((domainElement, _typir) => { + const result = typeDetails.inferenceRuleForFieldAccess!(domainElement); + if (result === InferenceRuleNotApplicable) { + return InferenceRuleNotApplicable; + } else if (typeof result === 'string') { + // get the type of the given field name + const fieldType = classType.getFields(true).get(result); + if (fieldType) { + return fieldType; + } + return { + $problem: InferenceProblem, + domainElement, + inferenceCandidate: classType, + location: `unknown field '${result}'`, + // rule: this, // this does not work with functions ... + subProblems: [], + }; + } else { + return result; // do the type inference for this element instead + } + }, classType); + } +} + +function registerInferenceRuleForLiteral(services: TypirServices, rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): void { + const mapListConverter = new MapListConverter(); + services.inference.addInferenceRule({ + inferTypeWithoutChildren(domainElement, _typir) { + const result = rule.filter(domainElement); + if (result) { + const matching = rule.matching(domainElement); + if (matching) { + const inputArguments = rule.inputValuesForFields(domainElement); + if (inputArguments.size >= 1) { + return mapListConverter.toList(inputArguments); + } else { + // there are no operands to check + return classType; // this case occurs only, if the current class has no fields (including fields of super types) or is nominally typed + } + } else { + // the domain element is slightly different + } + } else { + // the domain element has a completely different purpose + } + // does not match at all + return InferenceRuleNotApplicable; + }, + inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { + const allExpectedFields = classType.getFields(true); + // this class type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 + const checkedFieldsProblems = checkNameTypesMap( + mapListConverter.toMap(childrenTypes), + allExpectedFields, + createTypeCheckStrategy(classKind.options.subtypeFieldChecking, typir) + ); + if (checkedFieldsProblems.length >= 1) { + // (only) for overloaded functions, the types of the parameters need to be inferred in order to determine an exact match + return { + $problem: InferenceProblem, + domainElement, + inferenceCandidate: classType, + location: 'values for fields', + rule: this, + subProblems: checkedFieldsProblems, + }; + } else { + // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + return classType; + } + }, + }, classType); +} + + /** * Predefined validation to produce errors, if the same class is declared more than once. * This is often relevant for nominally typed classes. @@ -659,7 +792,7 @@ export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter * @returns a string key */ protected calculateMethodKey(clas: ClassType, func: FunctionType): string { - return `${clas.identifier}.${func.functionName}(${func.getInputs().map(param => param.type.identifier)})`; + return `${clas.getIdentifier()}.${func.functionName}(${func.getInputs().map(param => param.type.getIdentifier())})`; } afterValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { @@ -692,11 +825,11 @@ export class TopClassType extends Type { } override getName(): string { - return this.identifier; + return this.getIdentifier(); } override getUserRepresentation(): string { - return this.identifier; + return this.getIdentifier(); } override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { diff --git a/packages/typir/src/kinds/fixed-parameters-kind.ts b/packages/typir/src/kinds/fixed-parameters-kind.ts index da655d7..4cdae75 100644 --- a/packages/typir/src/kinds/fixed-parameters-kind.ts +++ b/packages/typir/src/kinds/fixed-parameters-kind.ts @@ -49,6 +49,7 @@ export class FixedParameterType extends Type { type: typeValues[i], }); } + this.completeInitialization({}); // TODO preconditions } getParameterTypes(): Type[] { @@ -149,7 +150,7 @@ export class FixedParameterKind implements Kind { readonly $name: `FixedParameterKind-${string}`; readonly services: TypirServices; readonly baseName: string; - readonly options: FixedParameterKindOptions; + readonly options: Readonly; readonly parameters: Parameter[]; // assumption: the parameters are in the correct order! constructor(typir: TypirServices, baseName: string, options?: Partial, ...parameterNames: string[]) { diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index 7086018..a363c95 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -51,6 +51,8 @@ export class FunctionType extends Type { type: resolveTypeSelector(this.kind.services, input.type), }; }); + + this.completeInitialization({}); // TODO preconditions } override getName(): string { @@ -251,7 +253,7 @@ export type InferFunctionCall = { export class FunctionKind implements Kind, TypeGraphListener { readonly $name: 'FunctionKind'; readonly services: TypirServices; - readonly options: FunctionKindOptions; + readonly options: Readonly; /** Limitations * - Works only, if function types are defined using the createFunctionType(...) function below! */ @@ -389,7 +391,7 @@ export class FunctionKind implements Kind, TypeGraphListener { const functionName = typeDetails.functionName; // check the input - assertTrue(this.getFunctionType(typeDetails) === undefined, `${functionName}`); // ensures, that no duplicated functions are created! + assertTrue(this.getFunctionType(typeDetails) === undefined, `The function '${functionName}' already exists!`); // ensures, that no duplicated functions are created! if (!typeDetails) { throw new Error('is undefined'); } @@ -546,10 +548,10 @@ export class FunctionKind implements Kind, TypeGraphListener { /* Get informed about deleted types in order to remove inference rules which are bound to them. */ - addedType(_newType: Type): void { + addedType(_newType: Type, _key: string): void { // do nothing } - removedType(type: Type): void { + removedType(type: Type, _key: string): void { if (isFunctionType(type)) { const overloads = this.mapNameTypes.get(type.functionName); if (overloads) { @@ -571,14 +573,15 @@ export class FunctionKind implements Kind, TypeGraphListener { calculateIdentifier(typeDetails: FunctionTypeDetails): string { - // this schema allows to identify duplicated functions! - const prefix = this.options.identifierPrefix; - // function name + const prefix = this.options.identifierPrefix ? this.options.identifierPrefix + '-' : ''; + // function name, if wanted const functionName = this.hasFunctionName(typeDetails.functionName) ? typeDetails.functionName : ''; - // inputs - const inputsString = typeDetails.inputParameters.map(input => resolveTypeSelector(this.services, input.type).getName()).join(','); + // inputs: type identifiers in defined order + const inputsString = typeDetails.inputParameters.map(input => resolveTypeSelector(this.services, input.type).getIdentifier()).join(','); + // output: type identifier + const outputString = typeDetails.outputParameter ? resolveTypeSelector(this.services, typeDetails.outputParameter.type).getIdentifier() : ''; // complete signature - return `${prefix}-${functionName}(${inputsString})`; + return `${prefix}${functionName}(${inputsString}):${outputString}`; } getParameterRepresentation(parameter: NameTypePair): string { @@ -620,7 +623,7 @@ export function isFunctionKind(kind: unknown): kind is FunctionKind { /** - * Predefined validation to produce errors, if the same function is declared more than once. + * Predefined validation to produce errors for those overloaded functions which cannot be distinguished when calling them. */ export class UniqueFunctionValidation implements ValidationRuleWithBeforeAfter { protected readonly foundDeclarations: Map = new Map(); @@ -662,7 +665,7 @@ export class UniqueFunctionValidation implements ValidationRuleWithBeforeAfter { * @returns a string key */ protected calculateFunctionKey(func: FunctionType): string { - return `${func.functionName}(${func.getInputs().map(param => param.type.identifier)})`; + return `${func.functionName}(${func.getInputs().map(param => param.type.getIdentifier())})`; } afterValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { diff --git a/packages/typir/src/kinds/multiplicity-kind.ts b/packages/typir/src/kinds/multiplicity-kind.ts index e682194..3815c37 100644 --- a/packages/typir/src/kinds/multiplicity-kind.ts +++ b/packages/typir/src/kinds/multiplicity-kind.ts @@ -26,10 +26,11 @@ export class MultiplicityType extends Type { this.constrainedType = constrainedType; this.lowerBound = lowerBound; this.upperBound = upperBound; + this.completeInitialization({}); // TODO preconditions } override getName(): string { - return this.kind.printType(this.getConstrainedType(), this.getLowerBound(), this.getUpperBound()); + return `${this.constrainedType.getName()}${this.kind.printRange(this.getLowerBound(), this.getUpperBound())}`; } override getUserRepresentation(): string { @@ -136,7 +137,7 @@ export const MultiplicityKindName = 'MultiplicityTypeKind'; export class MultiplicityKind implements Kind { readonly $name: 'MultiplicityTypeKind'; readonly services: TypirServices; - readonly options: MultiplicityKindOptions; + readonly options: Readonly; constructor(services: TypirServices, options?: Partial) { this.$name = MultiplicityKindName; @@ -185,7 +186,7 @@ export class MultiplicityKind implements Kind { } calculateIdentifier(typeDetails: MultiplicityTypeDetails): string { - return this.printType(typeDetails.constrainedType, typeDetails.lowerBound, typeDetails.upperBound); + return `${typeDetails.constrainedType.getIdentifier()}${this.printRange(typeDetails.lowerBound, typeDetails.upperBound)}`; } protected checkBounds(lowerBound: number, upperBound: number): boolean { @@ -200,10 +201,7 @@ export class MultiplicityKind implements Kind { return true; } - printType(constrainedType: Type, lowerBound: number, upperBound: number): string { - return `${constrainedType.getName()}${this.printRange(lowerBound, upperBound)}`; - } - protected printRange(lowerBound: number, upperBound: number): string { + printRange(lowerBound: number, upperBound: number): string { if (lowerBound === upperBound || (lowerBound === 0 && upperBound === MULTIPLICITY_UNLIMITED)) { // [2..2] => [2], [0..*] => [*] return `[${this.printBound(upperBound)}]`; diff --git a/packages/typir/src/kinds/primitive-kind.ts b/packages/typir/src/kinds/primitive-kind.ts index b9af4a7..f5625af 100644 --- a/packages/typir/src/kinds/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive-kind.ts @@ -20,19 +20,20 @@ export class PrimitiveType extends Type { constructor(kind: PrimitiveKind, identifier: string) { super(identifier); this.kind = kind; + this.completeInitialization({}); // no preconditions } override getName(): string { - return this.identifier; + return this.getIdentifier(); } override getUserRepresentation(): string { - return this.identifier; + return this.getIdentifier(); } override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { if (isPrimitiveType(otherType)) { - return checkValueForConflict(this.identifier, otherType.identifier, 'name'); + return checkValueForConflict(this.getIdentifier(), otherType.getIdentifier(), 'name'); } else { return [{ $problem: TypeEqualityProblem, diff --git a/packages/typir/src/kinds/top-kind.ts b/packages/typir/src/kinds/top-kind.ts index 50e5f25..8f49f8c 100644 --- a/packages/typir/src/kinds/top-kind.ts +++ b/packages/typir/src/kinds/top-kind.ts @@ -20,14 +20,15 @@ export class TopType extends Type { constructor(kind: TopKind, identifier: string) { super(identifier); this.kind = kind; + this.completeInitialization({}); // no preconditions } override getName(): string { - return this.identifier; + return this.getIdentifier(); } override getUserRepresentation(): string { - return this.identifier; + return this.getIdentifier(); } override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { @@ -86,7 +87,7 @@ export const TopKindName = 'TopKind'; export class TopKind implements Kind { readonly $name: 'TopKind'; readonly services: TypirServices; - readonly options: TopKindOptions; + readonly options: Readonly; protected instance: TopType | undefined; constructor(services: TypirServices, options?: Partial) { diff --git a/packages/typir/src/utils/type-initialization.ts b/packages/typir/src/utils/type-initialization.ts new file mode 100644 index 0000000..ec03514 --- /dev/null +++ b/packages/typir/src/utils/type-initialization.ts @@ -0,0 +1,50 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { Type } from '../graph/type-node.js'; +import { TypirServices } from '../typir.js'; + +export type TypeInitializerListener = (type: T) => void; + +export abstract class TypeInitializer { + protected readonly services: TypirServices; + protected typeToReturn: T | undefined; + protected listeners: Array> = []; + + constructor(services: TypirServices) { + this.services = services; + } + + protected producedType(newType: T): void { + const key = newType.getIdentifier(); + const existingType = this.services.graph.getType(key); + if (existingType) { + // ensure, that the same type is not duplicated! + this.typeToReturn = existingType as T; + // TODO: newType.invalidate() + } else { + this.typeToReturn = newType; + this.services.graph.addNode(newType); + } + + // inform and clear all listeners + this.listeners.forEach(listener => listener(this.typeToReturn!)); + this.listeners = []; + } + + getType(): T | undefined { + return this.typeToReturn; + } + + addListener(listener: TypeInitializerListener): void { + if (this.typeToReturn) { + // already resolved => call the listener directly + listener(this.typeToReturn); + } else { + this.listeners.push(listener); + } + } +} diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 43808ad..766c733 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -4,8 +4,14 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { isType, Type } from '../graph/type-node.js'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeEdge } from '../graph/type-edge.js'; +import { TypeGraphListener } from '../graph/type-graph.js'; +import { isType, Type, TypeInitializationState, TypeStateListener } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; +import { TypeInitializer } from './type-initialization.js'; +import { toArray } from './utils.js'; /** * Common interface of all problems/errors/messages which should be shown to users of DSLs which are type-checked with Typir. @@ -29,23 +35,385 @@ export function isNameTypePair(type: unknown): type is NameTypePair { return typeof type === 'object' && type !== null && typeof (type as NameTypePair).name === 'string' && isType((type as NameTypePair).type); } -// TODO this is a WIP sketch for managing the use of Types in properties/details of other Types (e.g. Types of fields of class Types) -export interface TypeReference { - readonly ref?: T; - readonly selector?: TypeSelector; - readonly error?: TypirProblem; -} + // This TypeScript type defines the possible ways to identify a wanted Typir type. -// TODO find better names +// TODO find better names: TypeSpecification, TypeDesignation/Designator, ... ? export type TypeSelector = - | Type // the instance of the wanted type - | string // identifier of the type (in the type graph/map) - | unknown // domain node to infer the final type from + | Type // the instance of the wanted type + | string // identifier of the type (in the type graph/map) + | TypeInitializer // delayed creation of types + | TypeReference // reference to a (maybe delayed) type + | unknown // domain node to infer the final type from ; -// TODO this is a sketch for delaying the type selection in the future export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); + +/* TODO ideas +- rekursive anlegen? mit TypeResolver verschmelzen? +- ClassTypeResolver extends TypeResolver? +- ClassType hat Properties - superClasses: TypeReference[] +- TypeReference VS TypeCreator/TypeInitializer +*/ + +export type WaitingForResolvedTypeReferencesListener = (waiter: WaitingForResolvedTypeReferences) => void; + +/** + * The purpose of this class is to inform some listeners, when all given TypeReferences reached their specified initialization state (or a later state). + * The listeners might be informed multiple times, if at least one of the TypeReferences was unresolved and later on again in the desired state. + */ +export class WaitingForResolvedTypeReferences { + protected informed: boolean = false; + + // All given TypeReferences must be (at least!) in the state Identifiable or Completed, before the listeners are informed. + protected readonly waitForRefsIdentified: Array> | undefined; + protected readonly waitForRefsCompleted: Array> | undefined; + + /** These listeners will be informed, when all TypeReferences are in the desired state. */ + protected readonly listeners: Array> = []; + + constructor( + waitForRefsToBeIdentified: Array> | undefined, + waitForRefsToBeCompleted: Array> | undefined, + ) { + + // remember the relevant TypeReferences + this.waitForRefsIdentified = waitForRefsToBeIdentified; + this.waitForRefsCompleted = waitForRefsToBeCompleted; + + // register to get updates for the relevant TypeReferences + toArray(this.waitForRefsIdentified).forEach(ref => { + ref.addReactionOnTypeIdentified(() => this.listeningForNextState(), false); + ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false); + }); + toArray(this.waitForRefsCompleted).forEach(ref => { + ref.addReactionOnTypeCompleted(() => this.listeningForNextState(), false); + ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false); + }); + + // everything might already be true + this.check(); + } + + addListener(newListener: WaitingForResolvedTypeReferencesListener, informIfAlreadyFulfilled: boolean): void { + this.listeners.push(newListener); + // inform new listener, if the state is already reached! + if (informIfAlreadyFulfilled && this.informed) { + newListener(this); + } + } + + removeListener(listenerToRemove: WaitingForResolvedTypeReferencesListener): void { + const index = this.listeners.indexOf(listenerToRemove); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } + + protected listeningForNextState(): void { + this.check(); + // TODO is a more performant solution possible, e.g. by counting or using "_type"?? + } + + protected listeningForReset(): void { + // since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again) + this.informed = false; + // TODO should listeners be informed about this invalidation? + } + + protected check() { + // already informed => do not inform again + if (this.informed) { + return; + } + + for (const ref of toArray(this.waitForRefsIdentified)) { + if (ref.isInStateOrLater('Identifiable')) { + // that is fine + } else { + return; + } + } + for (const ref of toArray(this.waitForRefsCompleted)) { + if (ref.isInStateOrLater('Completed')) { + // that is fine + } else { + return; + } + } + + // everything is fine now! => inform all listeners + this.informed = true; // don't inform the listeners again + this.listeners.forEach(listener => listener(this)); + } + + isFulfilled(): boolean { + return this.informed; + } +} + +export type WaitingForInvalidTypeReferencesListener = (waiter: WaitingForInvalidTypeReferences) => void; + +export class WaitingForInvalidTypeReferences { + protected counterInvalid: number; // just count the number of invalid TypeReferences + + // At least one of the given TypeReferences must be in the state Invalid. + protected readonly waitForRefsInvalid: Array>; + + /** These listeners will be informed, when all TypeReferences are in the desired state. */ + protected readonly listeners: Array> = []; + + constructor( + waitForRefsToBeInvalid: Array>, + ) { + + // remember the relevant TypeReferences + this.waitForRefsInvalid = waitForRefsToBeInvalid; + this.counterInvalid = this.waitForRefsInvalid.filter(ref => ref.isInState('Invalid')).length; + + // register to get updates for the relevant TypeReferences + this.waitForRefsInvalid.forEach(ref => { + ref.addReactionOnTypeIdentified(this.listeningForNextState, false); + ref.addReactionOnTypeUnresolved(this.listeningForReset, false); + }); + } + + addListener(newListener: WaitingForInvalidTypeReferencesListener, informIfAlreadyFulfilled: boolean): void { + this.listeners.push(newListener); + // inform new listener, if the state is already reached! + if (informIfAlreadyFulfilled && this.isFulfilled()) { + newListener(this); + } + } + + removeListener(listenerToRemove: WaitingForInvalidTypeReferencesListener): void { + const index = this.listeners.indexOf(listenerToRemove); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } + + protected listeningForNextState(_reference: TypeReference, _type: T): void { + this.counterInvalid--; + } + + protected listeningForReset(_reference: TypeReference, _type: T): void { + this.counterInvalid++; + if (this.isFulfilled()) { + this.listeners.forEach(listener => listener(this)); + } + } + + isFulfilled(): boolean { + return this.counterInvalid === this.waitForRefsInvalid.length && this.waitForRefsInvalid.length >= 1; + } +} + + + +// react on type found/identified/resolved/unresolved +export type TypeReferenceListener = (reference: TypeReference, type: T) => void; + +/** + * A TypeReference accepts a specification and resolves a type from this specification. + * Different TypeReferences might resolve to the same Type. + * + * The internal logic of a TypeReference is independent from the kind of the type to resolve. + * A TypeReference takes care of the lifecycle of the types. + */ +export class TypeReference implements TypeGraphListener, TypeStateListener { + protected readonly selector: TypeSelector; + protected readonly services: TypirServices; + protected resolvedType: T | undefined = undefined; + + // These listeners will be informed once and only about the transitions! + // Additionally, if the resolved type is already 'Completed', the listeners for 'Identifiable' will be informed as well. + protected readonly reactOnIdentified: Array> = []; + protected readonly reactOnCompleted: Array> = []; + protected readonly reactOnUnresolved: Array> = []; + + constructor(selector: TypeSelector, services: TypirServices) { + this.selector = selector; + this.services = services; + + this.startResolving(); + } + + protected startResolving(): void { + // discard the previously resolved type (if any) + this.resolvedType = undefined; + + // react on new types + this.services.graph.addListener(this); + // react on state changes of already existing types which are not (yet) completed + this.services.graph.getAllRegisteredTypes().forEach(type => { + if (type.getInitializationState() !== 'Completed') { + type.addListener(this, false); + } + }); + // TODO react on new inference rules + } + + protected stopResolving(): void { + // it is not required to listen to new types anymore, since the type is already resolved/found + this.services.graph.removeListener(this); + } + + getState(): TypeInitializationState | undefined { + if (this.resolvedType) { + return this.resolvedType.getInitializationState(); + } else { + return undefined; + } + } + + isInState(state: TypeInitializationState): boolean { + this.resolve(); // lazyly resolve on request + if (state === 'Invalid' && this.resolvedType === undefined) { + return true; + } + return this.resolvedType !== undefined && this.resolvedType.isInState(state); // resolved type is in the given state + } + isNotInState(state: TypeInitializationState): boolean { + return !this.isInState(state); + } + isInStateOrLater(state: TypeInitializationState): boolean { + this.resolve(); // lazyly resolve on request + switch (state) { + case 'Invalid': + return true; + default: + return this.resolvedType !== undefined && this.resolvedType.isInStateOrLater(state); + } + } + + getType(): T | undefined { + this.resolve(); // lazyly resolve on request + return this.resolvedType; + } + + protected resolve(): 'ALREADY_RESOLVED' | 'SUCCESSFULLY_RESOLVED' | 'RESOLVING_FAILED' { + if (this.resolvedType) { + // the type is already resolved => nothing to do + return 'ALREADY_RESOLVED'; + } + + // try to resolve the type + const resolvedType = this.tryToResolve(this.selector); + + if (resolvedType) { + // the type is successfully resolved! + this.resolvedType = resolvedType; + this.stopResolving(); + // notify observers + if (this.isInStateOrLater('Identifiable')) { + this.reactOnIdentified.forEach(listener => listener(this, resolvedType)); + } + if (this.isInStateOrLater('Completed')) { + this.reactOnCompleted.forEach(listener => listener(this, resolvedType)); + } + if (this.isNotInState('Completed')) { + // register to get updates for the resolved type in order to notify the observers of this TypeReference about the missing "identifiable" and "completed" cases above + resolvedType.addListener(this, false); // TODO or is this already done?? + } + return 'SUCCESSFULLY_RESOLVED'; + } else { + // the type is not resolved (yet) + return 'RESOLVING_FAILED'; + } + } + + protected tryToResolve(selector: TypeSelector): T | undefined { + // TODO is there a way to explicitly enfore/ensure "as T"? + if (isType(selector)) { + return selector as T; + } else if (typeof selector === 'string') { + return this.services.graph.getType(selector) as T; + } else if (selector instanceof TypeInitializer) { + return selector.getType(); + } else if (selector instanceof TypeReference) { + return selector.getType(); + } else if (typeof selector === 'function') { + return this.tryToResolve(selector()); // execute the function and try to recursively resolve the returned result again + } else { + const result = this.services.inference.inferType(selector); + if (isType(result)) { + return result as T; + } else { + return undefined; + } + } + } + + addReactionOnTypeIdentified(listener: TypeReferenceListener, informIfAlreadyIdentified: boolean): void { + this.reactOnIdentified.push(listener); + if (informIfAlreadyIdentified && this.isInStateOrLater('Identifiable')) { + listener(this, this.resolvedType!); + } + } + addReactionOnTypeCompleted(listener: TypeReferenceListener, informIfAlreadyCompleted: boolean): void { + this.reactOnCompleted.push(listener); + if (informIfAlreadyCompleted && this.isInStateOrLater('Completed')) { + listener(this, this.resolvedType!); + } + } + addReactionOnTypeUnresolved(listener: TypeReferenceListener, informIfInvalid: boolean): void { + this.reactOnUnresolved.push(listener); + if (informIfInvalid && this.isInState('Invalid')) { + listener(this, this.resolvedType!); + } + } + // TODO do we need corresponding "removeReactionOnTypeX(...)" methods? + + + addedType(addedType: Type, _key: string): void { + // after adding a new type, try to resolve the type + const result = this.resolve(); // is it possible to do this more performant by looking at the "addedType"? + if (result === 'RESOLVING_FAILED' && addedType.getInitializationState() !== 'Completed') { + // react on new states of this type as well, since the TypeSelector might depend on a particular state of the specified type + addedType.addListener(this, false); // the removal of this listener happens automatically! TODO doch nicht? + } + } + + removedType(removedType: Type, _key: string): void { + // the resolved type of this TypeReference is removed! + if (removedType === this.resolvedType) { + // notify observers, that the type reference is broken + this.reactOnUnresolved.forEach(listener => listener(this, this.resolvedType!)); + // start resolving the type again + this.startResolving(); + } + } + + addedEdge(_edge: TypeEdge): void { + // only types are relevant + } + removedEdge(_edge: TypeEdge): void { + // only types are relevant + } + + switchedToIdentifiable(type: Type): void { + const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? + if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { + // the type was already resolved, but some observers of this TypeReference still need to be informed + this.reactOnIdentified.forEach(listener => listener(this, this.resolvedType!)); + } + } + + switchedToCompleted(type: Type): void { + const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? + if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { + // the type was already resolved, but some observers of this TypeReference still need to be informed + this.reactOnCompleted.forEach(listener => listener(this, this.resolvedType!)); + } + } + + switchedToInvalid(_type: Type): void { + // TODO + } +} + + export function resolveTypeSelector(services: TypirServices, selector: TypeSelector): Type { /** TODO this is only a rough sketch: * - detect cycles/deadlocks during the resolving process @@ -61,6 +429,12 @@ export function resolveTypeSelector(services: TypirServices, selector: TypeSelec } else { throw new Error('TODO not-found problem'); } + } else if (selector instanceof TypeInitializer) { + return selector.getType(); + } else if (selector instanceof TypeReference) { + return selector.getType(); + } else if (typeof selector === 'function') { + return resolveTypeSelector(services, selector()); // execute the function and try to recursively resolve the returned result again } else { const result = services.inference.inferType(selector); if (isType(result)) { diff --git a/packages/typir/test/type-definitions.test.ts b/packages/typir/test/type-definitions.test.ts index 07986fe..1fc8b32 100644 --- a/packages/typir/test/type-definitions.test.ts +++ b/packages/typir/test/type-definitions.test.ts @@ -33,7 +33,7 @@ describe('Tests for Typir', () => { inferenceRules: domainElement => typeof domainElement === 'string'}); // combine type definition with a dedicated inference rule for it const typeBoolean = primitiveKind.createPrimitiveType({ primitiveName: 'Boolean' }); - // create class type Person with 1 firstName and 1..2 lastNames and a age properties + // create class type Person with 1 firstName and 1..2 lastNames and an age properties const typeOneOrTwoStrings = multiplicityKind.createMultiplicityType({ constrainedType: typeString, lowerBound: 1, upperBound: 2 }); const typePerson = classKind.createClassType({ className: 'Person', @@ -44,7 +44,7 @@ describe('Tests for Typir', () => { ], methods: [], }); - console.log(typePerson.getUserRepresentation()); + console.log(typePerson.getType()!.getUserRepresentation()); const typeStudent = classKind.createClassType({ className: 'Student', superClasses: typePerson, // a Student is a special Person @@ -57,7 +57,7 @@ describe('Tests for Typir', () => { // create some more types const typeListInt = listKind.createFixedParameterType({ parameterTypes: typeInt }); const typeListString = listKind.createFixedParameterType({ parameterTypes: typeString }); - const typeMapStringPerson = mapKind.createFixedParameterType({ parameterTypes: [typeString, typePerson] }); + // const typeMapStringPerson = mapKind.createFixedParameterType({ parameterTypes: [typeString, typePerson] }); const typeFunctionStringLength = functionKind.createFunctionType({ functionName: 'length', outputParameter: { name: NO_PARAMETER_NAME, type: typeInt }, @@ -99,14 +99,14 @@ describe('Tests for Typir', () => { expect(typir.assignability.isAssignable(typeInt, typeString)).toBe(true); expect(typir.assignability.isAssignable(typeString, typeInt)).not.toBe(true); // List, Map - expect(typir.assignability.isAssignable(typeListInt, typeMapStringPerson)).not.toBe(true); + // expect(typir.assignability.isAssignable(typeListInt, typeMapStringPerson)).not.toBe(true); expect(typir.assignability.isAssignable(typeListInt, typeListString)).not.toBe(true); expect(typir.assignability.isAssignable(typeListInt, typeListInt)).toBe(true); // classes - expect(typir.assignability.isAssignable(typeStudent, typePerson)).toBe(true); - const assignConflicts = typir.assignability.getAssignabilityProblem(typePerson, typeStudent); - expect(assignConflicts).not.toBe(undefined); - const msg = typir.printer.printAssignabilityProblem(assignConflicts as AssignabilityProblem); - console.log(msg); + // expect(typir.assignability.isAssignable(typeStudent, typePerson)).toBe(true); + // const assignConflicts = typir.assignability.getAssignabilityProblem(typePerson, typeStudent); + // expect(assignConflicts).not.toBe(undefined); + // const msg = typir.printer.printAssignabilityProblem(assignConflicts as AssignabilityProblem); + // console.log(msg); }); }); From bf1eb78894dac7b08ecfac96c24d1242ffd3e301 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 12 Nov 2024 22:33:32 +0100 Subject: [PATCH 02/19] listener for new inference rules, fixed several bugs --- examples/lox/test/lox-type-checking.test.ts | 4 +-- packages/typir/src/features/inference.ts | 28 +++++++++++++++- packages/typir/src/graph/type-node.ts | 23 +++++++++---- packages/typir/src/kinds/class-kind.ts | 32 +++++++++---------- packages/typir/src/utils/utils-definitions.ts | 23 +++++++++---- 5 files changed, 78 insertions(+), 32 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index a41125f..4cdca39 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -319,7 +319,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance describe('LOX', () => { // this test case will work after having the support for cyclic type definitions, since it will solve also issues with topological order of type definitions - test.only('complete with difficult order of classes', async () => await validate(` + test('complete with difficult order of classes', async () => await validate(` class SuperClass { a: number } @@ -340,7 +340,7 @@ describe('LOX', () => { var x = SubClass(); // Assigning nil to a class type var nilTest = SubClass(); - // nilTest = nil; // TODO failed + nilTest = nil; // Accessing members of a class var value = x.nested.method() + "wasd"; diff --git a/packages/typir/src/features/inference.ts b/packages/typir/src/features/inference.ts index 81bc725..ec279fc 100644 --- a/packages/typir/src/features/inference.ts +++ b/packages/typir/src/features/inference.ts @@ -84,6 +84,11 @@ export interface TypeInferenceRuleWithInferringChildren { } +export interface TypeInferenceCollectorListener { + addedInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void; + removedInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void; +} + /** * Collects an arbitrary number of inference rules * and allows to infer a type for a given domain element. @@ -103,6 +108,9 @@ export interface TypeInferenceCollector { * If the given type is removed from the type system, this rule will be automatically removed as well. */ addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void; + + addListener(listener: TypeInferenceCollectorListener): void; + removeListener(listener: TypeInferenceCollectorListener): void; } @@ -110,6 +118,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty protected readonly inferenceRules: Map = new Map(); // type identifier (otherwise '') -> inference rules protected readonly domainElementInference: DomainElementInferenceCaching; protected readonly services: TypirServices; + protected readonly listeners: TypeInferenceCollectorListener[] = []; constructor(services: TypirServices) { this.services = services; @@ -125,6 +134,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty this.inferenceRules.set(key, rules); } rules.push(rule); + this.listeners.forEach(listener => listener.addedInferenceRule(rule, boundToType)); } protected getBoundToTypeKey(boundToType?: Type): string { @@ -266,13 +276,29 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty } + addListener(listener: TypeInferenceCollectorListener): void { + this.listeners.push(listener); + } + removeListener(listener: TypeInferenceCollectorListener): void { + const index = this.listeners.indexOf(listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } + + /* Get informed about deleted types in order to remove inference rules which are bound to them. */ addedType(_newType: Type, _key: string): void { // do nothing } removedType(type: Type, _key: string): void { - this.inferenceRules.delete(this.getBoundToTypeKey(type)); + const key = this.getBoundToTypeKey(type); + const rulesToRemove = this.inferenceRules.get(key); + // remove the inference rules associated to the deleted type + this.inferenceRules.delete(key); + // inform listeners about removed inference rules + (rulesToRemove ?? []).forEach(rule => this.listeners.forEach(listener => listener.removedInferenceRule(rule, type))); } addedEdge(_edge: TypeEdge): void { // do nothing diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index e21a813..d0ffa2b 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -4,6 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { isClassType } from '../kinds/class-kind.js'; import { Kind, isKind } from '../kinds/kind.js'; import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForResolvedTypeReferences } from '../utils/utils-definitions.js'; import { assertTrue, assertUnreachable } from '../utils/utils.js'; @@ -166,7 +167,7 @@ export abstract class Type { preconditions.preconditionsForCompletion?.refsToBeIdentified, preconditions.preconditionsForCompletion?.refsToBeCompleted, ); - // completed --> invalid, TODO wie genau wird das realisiert?? triggert jetzt schon!! + // completed --> invalid const init3 = new WaitingForInvalidTypeReferences( preconditions.referencesRelevantForInvalidation ?? [], ); @@ -177,17 +178,25 @@ export abstract class Type { this.onInvalidation = preconditions.onInvalidation ?? (() => {}); // specify the transitions between the states: - init1.addListener(() => this.switchFromInvalidToIdentifiable(), true); + init1.addListener(() => { + this.switchFromInvalidToIdentifiable(); + if (init2.isFulfilled()) { + // this is required to ensure the stric order Identifiable --> Completed, since 'init2' might already be triggered + this.switchFromIdentifiableToCompleted(); + } + }, true); init2.addListener(() => { if (init1.isFulfilled()) { this.switchFromIdentifiableToCompleted(); } else { - // TODO ?? + // switching will be done later by 'init1' in order to conform to the stric order Identifiable --> Completed } - }, true); - init3.addListener(() => this.switchFromCompleteOrIdentifiableToInvalid(), false); // no initial trigger! - // TODO noch sicherstellen, dass keine Phasen übersprungen werden?? - // TODO trigger start?? + }, false); // not required, since init1 will switch to Completed as well! + init3.addListener(() => { + if (this.isNotInState('Invalid')) { + this.switchFromCompleteOrIdentifiableToInvalid(); + } + }, false); // no initial trigger! } protected onIdentification: () => void; // typical use cases: calculate the identifier diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 4f34b22..08c0d37 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -45,7 +45,7 @@ export class ClassType extends Type { superRef.addReactionOnTypeUnresolved((_ref, superType) => { // if the superType gets invalid, de-register this class as sub-class of the super-class superType.subClasses.splice(superType.subClasses.indexOf(this), 1); - }, true); + }, false); return superRef; }); @@ -75,21 +75,23 @@ export class ClassType extends Type { // calculate the Identifier, based on the resolved type references // const all: Array> = []; - const all: Array> = []; - all.push(...refFields); - all.push(...(refMethods as unknown as Array>)); // TODO dirty hack?! + const fieldsAndMethods: Array> = []; + fieldsAndMethods.push(...refFields); + fieldsAndMethods.push(...(refMethods as unknown as Array>)); // TODO dirty hack?! // all.push(...refMethods); // does not work this.completeInitialization({ preconditionsForInitialization: { - refsToBeIdentified: all, + refsToBeIdentified: fieldsAndMethods, }, preconditionsForCompletion: { refsToBeCompleted: this.superClasses as unknown as Array>, }, + referencesRelevantForInvalidation: [...fieldsAndMethods, ...(this.superClasses as unknown as Array>)], onIdentification: () => { - this.identifier = this.kind.calculateIdentifier(typeDetails); - // TODO identifier erst hier berechnen?! registering?? + // the identifier is calculated now + this.identifier = this.kind.calculateIdentifier(typeDetails); // TODO it is still not nice, that the type resolving is done again, since the TypeReferences here are not reused + // the registration of the type in the type graph is done by the TypeInitializer }, onCompletion: () => { // when all super classes are completely available, do the following checks: @@ -438,8 +440,7 @@ export class ClassKind implements Kind { // nominal typing return new TypeReference(typeDetails, this.services); } else { - // structural typing - // TODO does this case occur in practise? + // structural typing (does this case occur in practise?) return new TypeReference(() => this.calculateIdentifier(typeDetails), this.services); } } @@ -452,8 +453,6 @@ export class ClassKind implements Kind { * @returns an initializer which creates and returns the new class type, when all depending types are resolved */ createClassType(typeDetails: CreateClassTypeDetails): TypeInitializer { - // assertTrue(this.getClassType(typeDetails) === undefined, `The class '${typeDetails.className}' already exists!`); // ensures, that no duplicated classes are created! - return new ClassTypeInitializer(this.services, this, typeDetails); } @@ -463,6 +462,7 @@ export class ClassKind implements Kind { /** * TODO + * If some types for the properties of the class are missing, an exception will be thrown. * * Design decisions: * - This method is part of the ClassKind and not part of ClassType, since the ClassKind requires it for 'getClassType'! @@ -471,7 +471,7 @@ export class ClassKind implements Kind { * @param typeDetails the details * @returns the new identifier */ - calculateIdentifier(typeDetails: ClassTypeDetails): string { // TODO kann keinen Identifier liefern, wenn noch nicht resolved! + calculateIdentifier(typeDetails: ClassTypeDetails): string { // purpose of identifier: distinguish different types; NOT: not uniquely overloaded types const prefix = this.getIdentifierPrefix(); if (this.options.typing === 'Structural') { @@ -542,6 +542,7 @@ export class ClassTypeInitializer exten // create the class type const classType = new ClassType(kind, typeDetails as CreateClassTypeDetails); + // TODO erst nach Herausfiltern im Initializer darf der Type selbst sich registrieren! if (kind.options.typing === 'Structural') { // TODO Vorsicht Inference rules werden by default an den Identifier gebunden (ebenso Validations)! this.services.graph.addNode(classType, kind.getIdentifierPrefix() + typeDetails.className); @@ -551,17 +552,16 @@ export class ClassTypeInitializer exten classType.addListener(this, true); // trigger directly, if some initialization states are already reached! } - switchedToIdentifiable(type: Type): void { + switchedToIdentifiable(classType: Type): void { // TODO Vorsicht, dass hier nicht 2x derselbe Type angefangen wird zu erstellen und dann zwei Typen auf ihre Vervollständigung warten! // 2x TypeResolver erstellen, beide müssen später denselben ClassType zurückliefern! // bei Node { children: Node[] } muss der Zyklus erkannt und behandelt werden!! - this.producedType(type as ClassType); + this.producedType(classType as ClassType); + registerInferenceRules(this.services, this.typeDetails, this.kind, classType as ClassType); } switchedToCompleted(classType: Type): void { // register inference rules - // TODO or can this be done already after having the identifier? - registerInferenceRules(this.services, this.typeDetails, this.kind, classType as ClassType); classType.removeListener(this); // the work of this initializer is done now } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 766c733..3c97cf8 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -6,6 +6,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { TypeInferenceCollectorListener, TypeInferenceRule } from '../features/inference.js'; import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; import { isType, Type, TypeInitializationState, TypeStateListener } from '../graph/type-node.js'; @@ -173,8 +174,8 @@ export class WaitingForInvalidTypeReferences { // register to get updates for the relevant TypeReferences this.waitForRefsInvalid.forEach(ref => { - ref.addReactionOnTypeIdentified(this.listeningForNextState, false); - ref.addReactionOnTypeUnresolved(this.listeningForReset, false); + ref.addReactionOnTypeIdentified(() => this.listeningForNextState(), false); + ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false); }); } @@ -193,11 +194,11 @@ export class WaitingForInvalidTypeReferences { } } - protected listeningForNextState(_reference: TypeReference, _type: T): void { + protected listeningForNextState(): void { this.counterInvalid--; } - protected listeningForReset(_reference: TypeReference, _type: T): void { + protected listeningForReset(): void { this.counterInvalid++; if (this.isFulfilled()) { this.listeners.forEach(listener => listener(this)); @@ -221,7 +222,7 @@ export type TypeReferenceListener = (reference: TypeRefer * The internal logic of a TypeReference is independent from the kind of the type to resolve. * A TypeReference takes care of the lifecycle of the types. */ -export class TypeReference implements TypeGraphListener, TypeStateListener { +export class TypeReference implements TypeGraphListener, TypeStateListener, TypeInferenceCollectorListener { protected readonly selector: TypeSelector; protected readonly services: TypirServices; protected resolvedType: T | undefined = undefined; @@ -251,12 +252,14 @@ export class TypeReference implements TypeGraphListener, type.addListener(this, false); } }); - // TODO react on new inference rules + // react on new inference rules + this.services.inference.addListener(this); } protected stopResolving(): void { // it is not required to listen to new types anymore, since the type is already resolved/found this.services.graph.removeListener(this); + this.services.inference.removeListener(this); } getState(): TypeInitializationState | undefined { @@ -392,6 +395,14 @@ export class TypeReference implements TypeGraphListener, // only types are relevant } + addedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void { + // after adding a new inference rule, try to resolve the type + this.resolve(); + } + removedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void { + // empty + } + switchedToIdentifiable(type: Type): void { const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { From 36708f044a9c797325e9c1b7909de2e5fbca3c46 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 18 Nov 2024 20:02:22 +0100 Subject: [PATCH 03/19] fixed important bug --- packages/typir/src/utils/utils-definitions.ts | 70 +++++++++++++------ 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 3c97cf8..4090db2 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -64,32 +64,39 @@ export type WaitingForResolvedTypeReferencesListener = (w * The listeners might be informed multiple times, if at least one of the TypeReferences was unresolved and later on again in the desired state. */ export class WaitingForResolvedTypeReferences { - protected informed: boolean = false; + protected fulfilled: boolean = false; + protected typesForCycles: Type | undefined; // All given TypeReferences must be (at least!) in the state Identifiable or Completed, before the listeners are informed. protected readonly waitForRefsIdentified: Array> | undefined; protected readonly waitForRefsCompleted: Array> | undefined; + private readonly reactOnNextState: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextState(reference, resolvedType); + private readonly reactOnResetState: TypeReferenceListener = (_reference: TypeReference, _resolvedType: T) => this.listeningForReset(); - /** These listeners will be informed, when all TypeReferences are in the desired state. */ + /** These listeners will be informed once, when all TypeReferences are in the desired state. + * If some of these TypeReferences are invalid and later in the desired state again, the listeners will be informed again. */ protected readonly listeners: Array> = []; constructor( waitForRefsToBeIdentified: Array> | undefined, waitForRefsToBeCompleted: Array> | undefined, + typeCycle: Type | undefined, ) { // remember the relevant TypeReferences this.waitForRefsIdentified = waitForRefsToBeIdentified; this.waitForRefsCompleted = waitForRefsToBeCompleted; + this.typesForCycles = typeCycle; // register to get updates for the relevant TypeReferences toArray(this.waitForRefsIdentified).forEach(ref => { - ref.addReactionOnTypeIdentified(() => this.listeningForNextState(), false); - ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false); + ref.addReactionOnTypeIdentified(this.reactOnNextState, false); + ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); }); toArray(this.waitForRefsCompleted).forEach(ref => { - ref.addReactionOnTypeCompleted(() => this.listeningForNextState(), false); - ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false); + ref.addReactionOnTypeIdentified(this.reactOnNextState, false); + ref.addReactionOnTypeCompleted(this.reactOnNextState, false); + ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); }); // everything might already be true @@ -99,7 +106,7 @@ export class WaitingForResolvedTypeReferences { addListener(newListener: WaitingForResolvedTypeReferencesListener, informIfAlreadyFulfilled: boolean): void { this.listeners.push(newListener); // inform new listener, if the state is already reached! - if (informIfAlreadyFulfilled && this.informed) { + if (informIfAlreadyFulfilled && this.fulfilled) { newListener(this); } } @@ -111,32 +118,33 @@ export class WaitingForResolvedTypeReferences { } } - protected listeningForNextState(): void { + protected listeningForNextState(_reference: TypeReference, _resolvedType: T): void { + // check, whether all TypeReferences are resolved and the resolved types are in the expected state this.check(); - // TODO is a more performant solution possible, e.g. by counting or using "_type"?? + // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?? } protected listeningForReset(): void { // since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again) - this.informed = false; + this.fulfilled = false; // TODO should listeners be informed about this invalidation? } protected check() { // already informed => do not inform again - if (this.informed) { + if (this.fulfilled) { return; } for (const ref of toArray(this.waitForRefsIdentified)) { - if (ref.isInStateOrLater('Identifiable')) { + if (ref.isInStateOrLater('Identifiable') || ref.getType() === this.typesForCycles) { // that is fine } else { return; } } for (const ref of toArray(this.waitForRefsCompleted)) { - if (ref.isInStateOrLater('Completed')) { + if (ref.isInStateOrLater('Completed') || ref.getType() === this.typesForCycles) { // that is fine } else { return; @@ -144,12 +152,12 @@ export class WaitingForResolvedTypeReferences { } // everything is fine now! => inform all listeners - this.informed = true; // don't inform the listeners again - this.listeners.forEach(listener => listener(this)); + this.fulfilled = true; // don't inform the listeners again + this.listeners.slice().forEach(listener => listener(this)); // slice() prevents issues with removal of listeners during notifications } isFulfilled(): boolean { - return this.informed; + return this.fulfilled; } } @@ -201,7 +209,7 @@ export class WaitingForInvalidTypeReferences { protected listeningForReset(): void { this.counterInvalid++; if (this.isFulfilled()) { - this.listeners.forEach(listener => listener(this)); + this.listeners.slice().forEach(listener => listener(this)); } } @@ -213,7 +221,7 @@ export class WaitingForInvalidTypeReferences { // react on type found/identified/resolved/unresolved -export type TypeReferenceListener = (reference: TypeReference, type: T) => void; +export type TypeReferenceListener = (reference: TypeReference, resolvedType: T) => void; /** * A TypeReference accepts a specification and resolves a type from this specification. @@ -221,6 +229,8 @@ export type TypeReferenceListener = (reference: TypeRefer * * The internal logic of a TypeReference is independent from the kind of the type to resolve. * A TypeReference takes care of the lifecycle of the types. + * + * Once the type is resolved, listeners are notified about this and all following changes of its state. */ export class TypeReference implements TypeGraphListener, TypeStateListener, TypeInferenceCollectorListener { protected readonly selector: TypeSelector; @@ -366,7 +376,25 @@ export class TypeReference implements TypeGraphListener, listener(this, this.resolvedType!); } } - // TODO do we need corresponding "removeReactionOnTypeX(...)" methods? + + removeReactionOnTypeIdentified(listener: TypeReferenceListener): void { + const index = this.reactOnIdentified.indexOf(listener); + if (index >= 0) { + this.reactOnIdentified.splice(index, 1); + } + } + removeReactionOnTypeCompleted(listener: TypeReferenceListener): void { + const index = this.reactOnCompleted.indexOf(listener); + if (index >= 0) { + this.reactOnCompleted.splice(index, 1); + } + } + removeReactionOnTypeUnresolved(listener: TypeReferenceListener): void { + const index = this.reactOnUnresolved.indexOf(listener); + if (index >= 0) { + this.reactOnUnresolved.splice(index, 1); + } + } addedType(addedType: Type, _key: string): void { @@ -407,7 +435,7 @@ export class TypeReference implements TypeGraphListener, const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { // the type was already resolved, but some observers of this TypeReference still need to be informed - this.reactOnIdentified.forEach(listener => listener(this, this.resolvedType!)); + this.reactOnIdentified.slice().forEach(listener => listener(this, this.resolvedType!)); // slice() prevents issues with removal of listeners during notifications } } @@ -415,7 +443,7 @@ export class TypeReference implements TypeGraphListener, const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { // the type was already resolved, but some observers of this TypeReference still need to be informed - this.reactOnCompleted.forEach(listener => listener(this, this.resolvedType!)); + this.reactOnCompleted.slice().forEach(listener => listener(this, this.resolvedType!)); // slice() prevents issues with removal of listeners during notifications } } From c1f02ecd0645f163786bb9a09448eedd37243181 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 18 Nov 2024 20:03:06 +0100 Subject: [PATCH 04/19] refactorings, improved comments --- packages/typir/src/graph/type-node.ts | 33 +++++++++++-------- packages/typir/src/kinds/class-kind.ts | 28 +++++++++------- .../typir/src/utils/type-initialization.ts | 8 +++-- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index d0ffa2b..0be9623 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { isClassType } from '../kinds/class-kind.js'; import { Kind, isKind } from '../kinds/kind.js'; import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForResolvedTypeReferences } from '../utils/utils-definitions.js'; import { assertTrue, assertUnreachable } from '../utils/utils.js'; @@ -156,28 +155,32 @@ export abstract class Type { onCompletion?: () => void, onInvalidation?: () => void, }): void { - // specify the preconditions: - // invalid --> identifiable + // store the reactions + this.onIdentification = preconditions.onIdentification ?? (() => {}); + this.onCompletion = preconditions.onCompletion ?? (() => {}); + this.onInvalidation = preconditions.onInvalidation ?? (() => {}); + + if (this.kind.$name === 'ClassKind') { + console.log(''); + } + // preconditions for Identifiable const init1 = new WaitingForResolvedTypeReferences( preconditions.preconditionsForInitialization?.refsToBeIdentified, preconditions.preconditionsForInitialization?.refsToBeCompleted, + this, ); - // identifiable --> completed + // preconditions for Completed const init2 = new WaitingForResolvedTypeReferences( preconditions.preconditionsForCompletion?.refsToBeIdentified, preconditions.preconditionsForCompletion?.refsToBeCompleted, + this, ); - // completed --> invalid + // preconditions for Invalid const init3 = new WaitingForInvalidTypeReferences( preconditions.referencesRelevantForInvalidation ?? [], ); - // store the reactions - this.onIdentification = preconditions.onIdentification ?? (() => {}); - this.onCompletion = preconditions.onCompletion ?? (() => {}); - this.onInvalidation = preconditions.onInvalidation ?? (() => {}); - - // specify the transitions between the states: + // invalid --> identifiable init1.addListener(() => { this.switchFromInvalidToIdentifiable(); if (init2.isFulfilled()) { @@ -185,6 +188,7 @@ export abstract class Type { this.switchFromIdentifiableToCompleted(); } }, true); + // identifiable --> completed init2.addListener(() => { if (init1.isFulfilled()) { this.switchFromIdentifiableToCompleted(); @@ -192,6 +196,7 @@ export abstract class Type { // switching will be done later by 'init1' in order to conform to the stric order Identifiable --> Completed } }, false); // not required, since init1 will switch to Completed as well! + // identifiable/completed --> invalid init3.addListener(() => { if (this.isNotInState('Invalid')) { this.switchFromCompleteOrIdentifiableToInvalid(); @@ -207,21 +212,21 @@ export abstract class Type { this.assertState('Invalid'); this.onIdentification(); this.initialization = 'Identifiable'; - this.stateListeners.forEach(listener => listener.switchedToIdentifiable(this)); + this.stateListeners.slice().forEach(listener => listener.switchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications } protected switchFromIdentifiableToCompleted(): void { this.assertState('Identifiable'); this.onCompletion(); this.initialization = 'Completed'; - this.stateListeners.forEach(listener => listener.switchedToCompleted(this)); + this.stateListeners.slice().forEach(listener => listener.switchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications } protected switchFromCompleteOrIdentifiableToInvalid(): void { this.assertNotState('Invalid'); this.onInvalidation(); this.initialization = 'Invalid'; - this.stateListeners.forEach(listener => listener.switchedToInvalid(this)); + this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications } diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 08c0d37..338e236 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -73,7 +73,6 @@ export class ClassType extends Type { const refMethods = this.methods.map(m => m.type); // the uniqueness of methods can be checked with the predefined UniqueMethodValidation below - // calculate the Identifier, based on the resolved type references // const all: Array> = []; const fieldsAndMethods: Array> = []; fieldsAndMethods.push(...refFields); @@ -85,7 +84,7 @@ export class ClassType extends Type { refsToBeIdentified: fieldsAndMethods, }, preconditionsForCompletion: { - refsToBeCompleted: this.superClasses as unknown as Array>, + refsToBeCompleted: this.superClasses as unknown as Array>, // TODO here we are waiting for the same/current (identifiable) ClassType!! }, referencesRelevantForInvalidation: [...fieldsAndMethods, ...(this.superClasses as unknown as Array>)], onIdentification: () => { @@ -387,6 +386,7 @@ export interface ClassTypeDetails { } export interface CreateClassTypeDetails extends ClassTypeDetails { // TODO the generics look very bad! inferenceRuleForDeclaration?: (domainElement: unknown) => boolean, // TODO what is the purpose for this? what is the difference to literals? + // TODO rename to Constructor call?? inferenceRuleForLiteral?: InferClassLiteral, // InferClassLiteral | Array>, does not work: https://stackoverflow.com/questions/65129070/defining-an-array-of-differing-generic-types-in-typescript inferenceRuleForReference?: InferClassLiteral, inferenceRuleForFieldAccess?: (domainElement: unknown) => string | unknown | InferenceRuleNotApplicable, // name of the field | element to infer the type of the field (e.g. the type) | rule not applicable @@ -553,19 +553,25 @@ export class ClassTypeInitializer exten } switchedToIdentifiable(classType: Type): void { - // TODO Vorsicht, dass hier nicht 2x derselbe Type angefangen wird zu erstellen und dann zwei Typen auf ihre Vervollständigung warten! - // 2x TypeResolver erstellen, beide müssen später denselben ClassType zurückliefern! - // bei Node { children: Node[] } muss der Zyklus erkannt und behandelt werden!! - this.producedType(classType as ClassType); - registerInferenceRules(this.services, this.typeDetails, this.kind, classType as ClassType); - } + /* Important explanations: + * - This logic here (and 'producedType(...)') ensures, that the same ClassType is not registered twice in the type graph. + * - By waiting untile the new class has its identifier, 'producedType(...)' is able to check, whether this class type is already existing! + * - Accordingly, 'classType' and 'readyClassType' might have different values! + */ + const readyClassType = this.producedType(classType as ClassType); - switchedToCompleted(classType: Type): void { // register inference rules - classType.removeListener(this); // the work of this initializer is done now + registerInferenceRules(this.services, this.typeDetails, this.kind, readyClassType); + + // the work of this initializer is done now + classType.removeListener(this); + } + + switchedToCompleted(_classType: Type): void { + // do nothing } - switchedToInvalid(_type: Type): void { + switchedToInvalid(_previousClassType: Type): void { // do nothing } } diff --git a/packages/typir/src/utils/type-initialization.ts b/packages/typir/src/utils/type-initialization.ts index ec03514..8508f7b 100644 --- a/packages/typir/src/utils/type-initialization.ts +++ b/packages/typir/src/utils/type-initialization.ts @@ -18,8 +18,11 @@ export abstract class TypeInitializer { this.services = services; } - protected producedType(newType: T): void { + protected producedType(newType: T): T { const key = newType.getIdentifier(); + if (!key) { + throw new Error('missing identifier!'); + } const existingType = this.services.graph.getType(key); if (existingType) { // ensure, that the same type is not duplicated! @@ -31,8 +34,9 @@ export abstract class TypeInitializer { } // inform and clear all listeners - this.listeners.forEach(listener => listener(this.typeToReturn!)); + this.listeners.slice().forEach(listener => listener(this.typeToReturn!)); this.listeners = []; + return this.typeToReturn; } getType(): T | undefined { From 0c933fa1ba7070dfcca688500e895dfec10c7317 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 19 Nov 2024 15:03:43 +0100 Subject: [PATCH 05/19] validation (instead of exception) for super-sub-class cycles --- .../language/type-system/lox-type-checking.ts | 4 +- examples/lox/test/lox-type-checking.test.ts | 57 +++++++++++++----- packages/typir/src/kinds/class-kind.ts | 58 ++++++++++++++++--- 3 files changed, 98 insertions(+), 21 deletions(-) diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index b67cd99..79b0c88 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -6,7 +6,7 @@ import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; import { LangiumSharedServices } from 'langium/lsp'; -import { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, ParameterDetails, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation } from 'typir'; +import { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, ParameterDetails, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, createNoSuperClassCyclesValidation } from 'typir'; import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { ValidationMessageDetails } from '../../../../../packages/typir/lib/features/validation.js'; import { BinaryExpression, FunctionDeclaration, MemberCall, MethodMember, TypeReference, UnaryExpression, isBinaryExpression, isBooleanLiteral, isClass, isClassMember, isFieldMember, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from '../generated/ast.js'; @@ -196,6 +196,8 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueMethodValidation(this.typir, (node) => isMethodMember(node), // MethodMembers could have other $containers? (method, _type) => method.$container)); + // check for cycles in super-sub-type relationships + this.typir.validation.collector.addValidationRule(createNoSuperClassCyclesValidation(isClass)); } onNewAstNode(node: AstNode): void { diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 4cdca39..6be22e7 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -205,6 +205,8 @@ describe('Explicitly test type checking for LOX', () => { class MyClass1 {} class MyClass2 < MyClass1 {} `, 0); + }); + test('Class inheritance and the order of type definitions', async () => { // switching the order of super and sub class works in Langium, but not in Typir at the moment, TODO warum nicht mehr?? await validate(` class MyClass2 < MyClass1 {} @@ -290,30 +292,37 @@ describe('Explicitly test type checking for LOX', () => { } `, 2)); // both methods need to be marked + test.todo('Cyclic use of Classes: parent-children', async () => await validate(` + class Node { + children: Node + } + `, 0)); + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { - // note that inference problems occur here due to the order of class declarations! after fixing that issue, errors regarding cycles should occur! - - test.fails('3 involved classes', async () => { + test('3 involved classes', async () => { await validate(` class MyClass1 < MyClass3 { } class MyClass2 < MyClass1 { } class MyClass3 < MyClass2 { } - `, 0); + `, 3); }); - test.fails('2 involved classes', async () => { + test('2 involved classes', async () => { await validate(` class MyClass1 < MyClass2 { } class MyClass2 < MyClass1 { } - `, 0); + `, [ + 'Circles in super-sub-class-relationships are not allowed: MyClass1', + 'Circles in super-sub-class-relationships are not allowed: MyClass2', + ]); }); - test.fails('1 involved class', async () => { + test.only('1 involved class', async () => { await validate(` class MyClass1 < MyClass1 { } - `, 0); + `, 'Circles in super-sub-class-relationships are not allowed: MyClass1'); }); }); @@ -393,13 +402,35 @@ describe('LOX', () => { `, 0)); }); -async function validate(lox: string, errors: number, warnings: number = 0) { +async function validate(lox: string, errors: number | string | string[], warnings: number = 0) { const document = await parseDocument(loxServices, lox.trim()); const diagnostics: Diagnostic[] = await loxServices.validation.DocumentValidator.validateDocument(document); + // errors - const diagnosticsErrors = diagnostics.filter(d => d.severity === DiagnosticSeverity.Error); - expect(diagnosticsErrors, diagnosticsErrors.map(d => d.message).join('\n')).toHaveLength(errors); + const diagnosticsErrors = diagnostics.filter(d => d.severity === DiagnosticSeverity.Error).map(d => fixMessage(d.message)); + const msgError = diagnosticsErrors.join('\n'); + if (typeof errors === 'number') { + expect(diagnosticsErrors, msgError).toHaveLength(errors); + } else if (typeof errors === 'string') { + expect(diagnosticsErrors, msgError).toHaveLength(1); + expect(diagnosticsErrors[0]).toBe(errors); + } else { + expect(diagnosticsErrors, msgError).toHaveLength(errors.length); + for (const expected of errors) { + expect(diagnosticsErrors).includes(expected); + } + } + // warnings - const diagnosticsWarnings = diagnostics.filter(d => d.severity === DiagnosticSeverity.Warning); - expect(diagnosticsWarnings, diagnosticsWarnings.map(d => d.message).join('\n')).toHaveLength(warnings); + const diagnosticsWarnings = diagnostics.filter(d => d.severity === DiagnosticSeverity.Warning).map(d => fixMessage(d.message)); + const msgWarning = diagnosticsWarnings.join('\n'); + expect(diagnosticsWarnings, msgWarning).toHaveLength(warnings); +} + +function fixMessage(msg: string): string { + if (msg.startsWith('While validating the AstNode')) { + const inbetween = 'this error is found: '; + return msg.substring(msg.indexOf(inbetween) + inbetween.length); + } + return msg; } diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 338e236..8aa6ad3 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -8,7 +8,7 @@ import { assertUnreachable } from 'langium'; import { TypeEqualityProblem } from '../features/equality.js'; import { InferenceProblem, InferenceRuleNotApplicable } from '../features/inference.js'; import { SubTypeProblem } from '../features/subtype.js'; -import { ValidationProblem, ValidationRuleWithBeforeAfter } from '../features/validation.js'; +import { ValidationProblem, ValidationRule, ValidationRuleWithBeforeAfter } from '../features/validation.js'; import { Type, TypeStateListener, isType } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { TypeReference, TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; @@ -100,10 +100,6 @@ export class ClassType extends Type { throw new Error(`Only ${this.kind.options.maximumNumberOfSuperClasses} super-classes are allowed.`); } } - // check for cycles in sub-type-relationships - if (this.getAllSuperClasses(false).has(this)) { - throw new Error(`Circles in super-sub-class-relationships are not allowed: ${this.getName()}`); - } }, onInvalidation: () => { // TODO remove all listeners, ... @@ -301,11 +297,21 @@ export class ClassType extends Type { return result; } + hasSubSuperClassCycles(): boolean { + return this.getAllSuperClasses(false).has(this); + } + ensureNoCycles(): void { + if (this.hasSubSuperClassCycles()) { + throw new Error('This is not possible, since this class has cycles in its super-classes!'); + } + } + getFields(withSuperClassesFields: boolean): Map { // in case of conflicting field names, the type of the sub-class takes precedence! TODO check this const result = new Map(); // fields of super classes if (withSuperClassesFields) { + this.ensureNoCycles(); for (const superClass of this.getDeclaredSuperClasses()) { for (const [superName, superType] of superClass.getFields(true)) { result.set(superName, superType); @@ -336,6 +342,7 @@ export class ClassType extends Type { }); // methods of super classes if (withSuperClassMethods) { + this.ensureNoCycles(); for (const superClass of this.getDeclaredSuperClasses()) { for (const superMethod of superClass.getMethods(true)) { result.push(superMethod); @@ -390,6 +397,7 @@ export interface CreateClassTypeDetails inferenceRuleForLiteral?: InferClassLiteral, // InferClassLiteral | Array>, does not work: https://stackoverflow.com/questions/65129070/defining-an-array-of-differing-generic-types-in-typescript inferenceRuleForReference?: InferClassLiteral, inferenceRuleForFieldAccess?: (domainElement: unknown) => string | unknown | InferenceRuleNotApplicable, // name of the field | element to infer the type of the field (e.g. the type) | rule not applicable + // inference rules for Method calls are part of "methods: CreateFunctionTypeDetails[]" above! } // TODO nominal vs structural typing ?? @@ -567,8 +575,15 @@ export class ClassTypeInitializer exten classType.removeListener(this); } - switchedToCompleted(_classType: Type): void { - // do nothing + switchedToCompleted(classType: Type): void { + // If there is no inference rule for the declaration of a class, such a class is probably a library or builtIn class. + // Therefore, no validation errors can be shown for the classes and exceptions are thrown instead. + if (this.typeDetails.inferenceRuleForDeclaration === null) { + // check for cycles in sub-type-relationships of classes + if ((classType as ClassType).hasSubSuperClassCycles()) { + throw new Error(`Circles in super-sub-class-relationships are not allowed: ${classType.getName()}`); + } + } } switchedToInvalid(_previousClassType: Type): void { @@ -822,6 +837,35 @@ export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter } +/** + * Predefined validation to produce errors for all those class declarations, whose class type have cycles in their super-classes. + * @param isRelevant helps to filter out declarations of classes in the user AST, + * is parameter is the reasons, why this validation cannot be registered by default by Typir for classes, since this parameter is DSL-specific + * @returns a validation rule which checks for any class declaration/type, whether they have no cycles in their sub-super-class-relationships + */ +export function createNoSuperClassCyclesValidation(isRelevant: (domainElement: unknown) => boolean): ValidationRule { + return (domainElement: unknown, typir: TypirServices) => { + const result: ValidationProblem[] = []; + if (isRelevant(domainElement)) { // improves performance, since type inference need to be done only for relevant elements + const classType = typir.inference.inferType(domainElement); + if (isClassType(classType) && classType.isInStateOrLater('Completed')) { + // check for cycles in sub-type-relationships + if (classType.hasSubSuperClassCycles()) { + result.push({ + $problem: ValidationProblem, + domainElement, + severity: 'error', + message: `Circles in super-sub-class-relationships are not allowed: ${classType.getName()}`, + }); + } + } + } + return result; + }; +} + + + export class TopClassType extends Type { override readonly kind: TopClassKind; From ddda3241ce1b9685f7b94dc62590b25ebc7cb2ad Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 19 Nov 2024 16:24:02 +0100 Subject: [PATCH 06/19] propagate cyclic types to the direct children --- .../language/type-system/lox-type-checking.ts | 1 + examples/lox/test/lox-type-checking.test.ts | 10 +++-- packages/typir/src/graph/type-node.ts | 24 +++++++---- packages/typir/src/utils/utils-definitions.ts | 42 +++++++++++++++---- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 79b0c88..4ed6f4b 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -190,6 +190,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // check for unique function declarations this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); + // check for unique class declarations this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueClassValidation(this.typir, isClass)); // check for unique method declarations diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 6be22e7..8f5a931 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -306,10 +306,14 @@ describe('Test internal validation of Typir for cycles in the class inheritance class MyClass1 < MyClass3 { } class MyClass2 < MyClass1 { } class MyClass3 < MyClass2 { } - `, 3); + `, [ + 'Circles in super-sub-class-relationships are not allowed: MyClass1', + 'Circles in super-sub-class-relationships are not allowed: MyClass2', + 'Circles in super-sub-class-relationships are not allowed: MyClass3', + ]); }); - test('2 involved classes', async () => { + test.only('2 involved classes', async () => { await validate(` class MyClass1 < MyClass2 { } class MyClass2 < MyClass1 { } @@ -319,7 +323,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance ]); }); - test.only('1 involved class', async () => { + test('1 involved class', async () => { await validate(` class MyClass1 < MyClass1 { } `, 'Circles in super-sub-class-relationships are not allowed: MyClass1'); diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 0be9623..8236430 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -145,6 +145,9 @@ export abstract class Type { } } + protected waitForIdentifiable: WaitingForResolvedTypeReferences; + protected waitForCompleted: WaitingForResolvedTypeReferences; + protected waitForInvalid: WaitingForInvalidTypeReferences; // to be called at the end of the constructor of each specific Type implementation! protected completeInitialization(preconditions: { @@ -164,46 +167,51 @@ export abstract class Type { console.log(''); } // preconditions for Identifiable - const init1 = new WaitingForResolvedTypeReferences( + this.waitForIdentifiable = new WaitingForResolvedTypeReferences( preconditions.preconditionsForInitialization?.refsToBeIdentified, preconditions.preconditionsForInitialization?.refsToBeCompleted, this, ); // preconditions for Completed - const init2 = new WaitingForResolvedTypeReferences( + this.waitForCompleted = new WaitingForResolvedTypeReferences( preconditions.preconditionsForCompletion?.refsToBeIdentified, preconditions.preconditionsForCompletion?.refsToBeCompleted, this, ); // preconditions for Invalid - const init3 = new WaitingForInvalidTypeReferences( + this.waitForInvalid = new WaitingForInvalidTypeReferences( preconditions.referencesRelevantForInvalidation ?? [], ); // invalid --> identifiable - init1.addListener(() => { + this.waitForIdentifiable.addListener(() => { this.switchFromInvalidToIdentifiable(); - if (init2.isFulfilled()) { + if (this.waitForCompleted.isFulfilled()) { // this is required to ensure the stric order Identifiable --> Completed, since 'init2' might already be triggered this.switchFromIdentifiableToCompleted(); } }, true); // identifiable --> completed - init2.addListener(() => { - if (init1.isFulfilled()) { + this.waitForCompleted.addListener(() => { + if (this.waitForIdentifiable.isFulfilled()) { this.switchFromIdentifiableToCompleted(); } else { // switching will be done later by 'init1' in order to conform to the stric order Identifiable --> Completed } }, false); // not required, since init1 will switch to Completed as well! // identifiable/completed --> invalid - init3.addListener(() => { + this.waitForInvalid.addListener(() => { if (this.isNotInState('Invalid')) { this.switchFromCompleteOrIdentifiableToInvalid(); } }, false); // no initial trigger! } + ignoreDependingTypesDuringInitialization(additionalTypesToIgnore: Set): void { + this.waitForIdentifiable.addTypesToIgnoreForCycles(additionalTypesToIgnore); + this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore); + } + protected onIdentification: () => void; // typical use cases: calculate the identifier protected onCompletion: () => void; // typical use cases: determine all properties which depend on other types to be created protected onInvalidation: () => void; // TODO ist jetzt anders; typical use cases: register inference rules for the type object already now! diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 4090db2..1b878a9 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -65,12 +65,13 @@ export type WaitingForResolvedTypeReferencesListener = (w */ export class WaitingForResolvedTypeReferences { protected fulfilled: boolean = false; - protected typesForCycles: Type | undefined; + protected typesForCycles: Set = new Set(); // All given TypeReferences must be (at least!) in the state Identifiable or Completed, before the listeners are informed. protected readonly waitForRefsIdentified: Array> | undefined; protected readonly waitForRefsCompleted: Array> | undefined; - private readonly reactOnNextState: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextState(reference, resolvedType); + private readonly reactOnNextIdentified: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextIdentified(reference, resolvedType); + private readonly reactOnNextCompleted: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextCompleted(reference, resolvedType); private readonly reactOnResetState: TypeReferenceListener = (_reference: TypeReference, _resolvedType: T) => this.listeningForReset(); /** These listeners will be informed once, when all TypeReferences are in the desired state. @@ -86,16 +87,20 @@ export class WaitingForResolvedTypeReferences { // remember the relevant TypeReferences this.waitForRefsIdentified = waitForRefsToBeIdentified; this.waitForRefsCompleted = waitForRefsToBeCompleted; - this.typesForCycles = typeCycle; + + // set-up the set of types to not wait for them, in order to handle cycles + if (typeCycle) { + this.typesForCycles.add(typeCycle); + } // register to get updates for the relevant TypeReferences toArray(this.waitForRefsIdentified).forEach(ref => { - ref.addReactionOnTypeIdentified(this.reactOnNextState, false); + ref.addReactionOnTypeIdentified(this.reactOnNextIdentified, false); ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); }); toArray(this.waitForRefsCompleted).forEach(ref => { - ref.addReactionOnTypeIdentified(this.reactOnNextState, false); - ref.addReactionOnTypeCompleted(this.reactOnNextState, false); + ref.addReactionOnTypeIdentified(this.reactOnNextIdentified, false); + ref.addReactionOnTypeCompleted(this.reactOnNextCompleted, false); ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); }); @@ -118,7 +123,24 @@ export class WaitingForResolvedTypeReferences { } } - protected listeningForNextState(_reference: TypeReference, _resolvedType: T): void { + addTypesToIgnoreForCycles(moreTypes: Set): void { + // TODO wer überprüft, ob schon vorhanden?? recursiv weiter propagieren ?? + for (const anotherType of moreTypes) { + this.typesForCycles.add(anotherType); + // TODO noch rekursiv weiterpropagieren?? + } + this.check(); + } + + protected listeningForNextIdentified(_reference: TypeReference, resolvedType: T): void { + // inform the referenced type about the types to ignore for completion + resolvedType.ignoreDependingTypesDuringInitialization(this.typesForCycles); + // check, whether all TypeReferences are resolved and the resolved types are in the expected state + this.check(); + // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?? + } + + protected listeningForNextCompleted(_reference: TypeReference, _resolvedType: T): void { // check, whether all TypeReferences are resolved and the resolved types are in the expected state this.check(); // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?? @@ -137,14 +159,16 @@ export class WaitingForResolvedTypeReferences { } for (const ref of toArray(this.waitForRefsIdentified)) { - if (ref.isInStateOrLater('Identifiable') || ref.getType() === this.typesForCycles) { + const refType = ref.getType(); + if (refType && (refType.isInStateOrLater('Identifiable') || this.typesForCycles.has(refType))) { // that is fine } else { return; } } for (const ref of toArray(this.waitForRefsCompleted)) { - if (ref.isInStateOrLater('Completed') || ref.getType() === this.typesForCycles) { + const refType = ref.getType(); + if (refType && (refType.isInStateOrLater('Completed') || this.typesForCycles.has(refType))) { // that is fine } else { return; From 6c29f57f314714363e8bd4a38af8a782984a4849 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 19 Nov 2024 16:40:56 +0100 Subject: [PATCH 07/19] propagate types to ignore recursively to indirect dependencies as well --- examples/lox/test/lox-type-checking.test.ts | 2 +- packages/typir/src/utils/utils-definitions.ts | 34 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 8f5a931..6623ae2 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -313,7 +313,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance ]); }); - test.only('2 involved classes', async () => { + test('2 involved classes', async () => { await validate(` class MyClass1 < MyClass2 { } class MyClass2 < MyClass1 { } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 1b878a9..260fef6 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -124,12 +124,38 @@ export class WaitingForResolvedTypeReferences { } addTypesToIgnoreForCycles(moreTypes: Set): void { - // TODO wer überprüft, ob schon vorhanden?? recursiv weiter propagieren ?? + const newTypes: Set = new Set(); for (const anotherType of moreTypes) { - this.typesForCycles.add(anotherType); - // TODO noch rekursiv weiterpropagieren?? + if (this.typesForCycles.has(anotherType)) { + // ignore this additional type, required to break the propagation, since it becomes cyclic as well in case of cyclic types! + } else { + newTypes.add(anotherType); + this.typesForCycles.add(anotherType); + } + } + + if (newTypes.size >= 1) { + // propagate the new types to ignore recursively to indirect dependencies as well + for (const ref of (this.waitForRefsIdentified ?? [])) { + const refType = ref.getType(); + if (refType?.isInStateOrLater('Identifiable')) { + // already resolved (TODO is that correct?) + } else { + refType?.ignoreDependingTypesDuringInitialization(newTypes); + } + } + for (const ref of (this.waitForRefsCompleted ?? [])) { + const refType = ref.getType(); + if (refType?.isInStateOrLater('Completed')) { + // already resolved (TODO is that correct?) + } else { + refType?.ignoreDependingTypesDuringInitialization(newTypes); + } + } + + // since there are more types to ignore, check again + this.check(); } - this.check(); } protected listeningForNextIdentified(_reference: TypeReference, resolvedType: T): void { From 40bea255607194c865388bb0cf4a1f63f2a76a2f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 19 Nov 2024 17:00:04 +0100 Subject: [PATCH 08/19] added more expected error messages into existing test cases --- examples/lox/test/lox-type-checking.test.ts | 54 ++++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 6623ae2..f028eb4 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -169,20 +169,20 @@ describe('Explicitly test type checking for LOX', () => { await validate(` class MyClass { name: string age: number } var v1 = MyClass(); // constructor call - `, 0); + `, []); }); test('Class literals 2', async () => { await validate(` class MyClass { name: string age: number } var v1: MyClass = MyClass(); // constructor call - `, 0); + `, []); }); test('Class literals 3', async () => { await validate(` class MyClass1 {} class MyClass2 {} var v1: boolean = MyClass1() == MyClass2(); // comparing objects with each other - `, 0, 1); + `, [], 1); }); }); @@ -204,14 +204,14 @@ describe('Explicitly test type checking for LOX', () => { await validate(` class MyClass1 {} class MyClass2 < MyClass1 {} - `, 0); + `, []); }); test('Class inheritance and the order of type definitions', async () => { - // switching the order of super and sub class works in Langium, but not in Typir at the moment, TODO warum nicht mehr?? + // switching the order of super and sub class works in Langium and in Typir await validate(` class MyClass2 < MyClass1 {} class MyClass1 {} - `, 0); + `, []); }); test('Class fields', async () => { @@ -233,12 +233,19 @@ describe('Explicitly test type checking for LOX', () => { await validate(` class MyClass1 { } class MyClass1 { } - `, 2); + `, [ + 'Declared classes need to be unique (MyClass1).', + 'Declared classes need to be unique (MyClass1).', + ]); await validate(` class MyClass2 { } class MyClass2 { } class MyClass2 { } - `, 3); + `, [ + 'Declared classes need to be unique (MyClass2).', + 'Declared classes need to be unique (MyClass2).', + 'Declared classes need to be unique (MyClass2).', + ]); }); test('Class methods: OK', async () => await validate(` @@ -249,7 +256,7 @@ describe('Explicitly test type checking for LOX', () => { } var v1: MyClass1 = MyClass1(); var v2: number = v1.method1(456); - `, 0)); + `, [])); test('Class methods: wrong return value', async () => await validate(` class MyClass1 { @@ -290,18 +297,29 @@ describe('Explicitly test type checking for LOX', () => { return true; } } - `, 2)); // both methods need to be marked + `, [ // both methods need to be marked: + 'Declared methods need to be unique (class-MyClass1.method1(number)).', + 'Declared methods need to be unique (class-MyClass1.method1(number)).', + ])); + +}); - test.todo('Cyclic use of Classes: parent-children', async () => await validate(` +describe('Cyclic type definitions where a Class is declared and already used', () => { + test('Class with field', async () => await validate(` class Node { children: Node } - `, 0)); + `, [])); + test.todo('Class with method', async () => await validate(` + class Node { + myMethod(input: number): Class {} + } + `, [])); }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { - test('3 involved classes', async () => { + test('Three involved classes: 1 -> 2 -> 3 -> 1', async () => { await validate(` class MyClass1 < MyClass3 { } class MyClass2 < MyClass1 { } @@ -313,7 +331,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance ]); }); - test('2 involved classes', async () => { + test('Two involved classes: 1 -> 2 -> 1', async () => { await validate(` class MyClass1 < MyClass2 { } class MyClass2 < MyClass1 { } @@ -323,14 +341,14 @@ describe('Test internal validation of Typir for cycles in the class inheritance ]); }); - test('1 involved class', async () => { + test('One involved class: 1 -> 1', async () => { await validate(` class MyClass1 < MyClass1 { } `, 'Circles in super-sub-class-relationships are not allowed: MyClass1'); }); }); -describe('LOX', () => { +describe('longer LOX examples', () => { // this test case will work after having the support for cyclic type definitions, since it will solve also issues with topological order of type definitions test('complete with difficult order of classes', async () => await validate(` class SuperClass { @@ -366,7 +384,7 @@ describe('LOX', () => { // Assigning a subclass to a super class var superType: SuperClass = x; print superType.a; - `, 0)); + `, [])); test('complete with easy order of classes', async () => await validate(` class SuperClass { @@ -403,7 +421,7 @@ describe('LOX', () => { // Assigning a subclass to a super class var superType: SuperClass = x; print superType.a; - `, 0)); + `, [])); }); async function validate(lox: string, errors: number | string | string[], warnings: number = 0) { From d4b1cde533991db50ca064b8a031f1dff96a2131 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 20 Nov 2024 17:04:30 +0100 Subject: [PATCH 09/19] simplified code, improved comments, more test cases, renamings, some more little refactorings --- .../language/type-system/lox-type-checking.ts | 11 +- examples/lox/test/lox-type-checking.test.ts | 43 +++++ packages/typir/src/features/inference.ts | 3 + packages/typir/src/graph/type-node.ts | 87 +++++---- packages/typir/src/kinds/bottom-kind.ts | 2 +- packages/typir/src/kinds/class-kind.ts | 13 +- .../typir/src/kinds/fixed-parameters-kind.ts | 2 +- packages/typir/src/kinds/function-kind.ts | 2 +- packages/typir/src/kinds/multiplicity-kind.ts | 2 +- packages/typir/src/kinds/primitive-kind.ts | 2 +- packages/typir/src/kinds/top-kind.ts | 2 +- .../typir/src/utils/type-initialization.ts | 19 +- packages/typir/src/utils/utils-definitions.ts | 174 ++++++++++-------- 13 files changed, 233 insertions(+), 129 deletions(-) diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 4ed6f4b..023f48f 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -211,24 +211,17 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // TODO support lambda (type references)! - /** - * TODO Delayed: - * - (classType: Type) => Type(for output) - * - WANN werden sie aufgelöst? bei erster Verwendung? - * - WO wird das verwaltet? im Kind? im Type? im TypeGraph? - */ - // class types (nominal typing): if (isClass(node)) { const className = node.name; const classType = this.classKind.createClassType({ className, - superClasses: node.superClass?.ref, // note that type inference is used here; TODO delayed + superClasses: node.superClass?.ref, // note that type inference is used here fields: node.members .filter(isFieldMember) // only Fields, no Methods .map(f => { name: f.name, - type: f.type, // note that type inference is used here; TODO delayed + type: f.type, // note that type inference is used here }), methods: node.members .filter(isMethodMember) // only Methods, no Fields diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index f028eb4..d0aca0e 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -11,6 +11,7 @@ import type { Diagnostic } from 'vscode-languageserver-types'; import { DiagnosticSeverity } from 'vscode-languageserver-types'; import { createLoxServices } from '../src/language/lox-module.js'; import { deleteAllDocuments } from 'typir-langium'; +import { isClassType } from '../../../packages/typir/lib/kinds/class-kind.js'; const loxServices = createLoxServices(EmptyFileSystem).Lox; @@ -311,11 +312,53 @@ describe('Cyclic type definitions where a Class is declared and already used', ( } `, [])); + test('Two Classes with fields with the other Class as type', async () => await validate(` + class A { + prop1: B + } + class B { + prop2: A + } + `, [])); + + test.todo('Three Classes with fields with the other Class as type', async () => await validate(` + class A { + prop1: B + } + class B { + prop2: C + } + class C { + prop3: A + } + `, [])); + test.todo('Class with method', async () => await validate(` class Node { myMethod(input: number): Class {} } `, [])); + + test('Having two declarations for the delayed class A, but only one type A in the type system', async () => { + await validate(` + class A { + property1: B // needs to wait for B, since B is defined below + } + class A { + property2: B // needs to wait for B, since B is defined below + } + class B { } + `, [ // Typir works with this, but for LOX these validation errors are produced: + 'Declared classes need to be unique (A).', + 'Declared classes need to be unique (A).', + ]); + // check, that there is only one class type A in the type graph: + const classes = loxServices.graph.getAllRegisteredTypes().filter(t => isClassType(t)).map(t => t.getName()); + expect(classes).toHaveLength(2); + expect(classes).includes('A'); + expect(classes).includes('B'); + }); + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { diff --git a/packages/typir/src/features/inference.ts b/packages/typir/src/features/inference.ts index ec279fc..81e0fb8 100644 --- a/packages/typir/src/features/inference.ts +++ b/packages/typir/src/features/inference.ts @@ -49,6 +49,9 @@ type TypeInferenceResultWithInferringChildren = * i.e. only a single type (or no type at all) can be inferred for a given domain element. * There are inference rules which dependent on types of children of the given domain element (e.g. calls of overloaded functions depend on the types of the current arguments) * and there are inference rules without this need. + * + * Within inference rules, don't take the initialization state of the inferred type into account, + * since such inferrence rules might not work for cyclic type definitions. */ export type TypeInferenceRule = TypeInferenceRuleWithoutInferringChildren | TypeInferenceRuleWithInferringChildren; diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 8236430..c93f0cf 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { Kind, isKind } from '../kinds/kind.js'; -import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForResolvedTypeReferences } from '../utils/utils-definitions.js'; +import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForIdentifiableAndCompletedTypeReferences } from '../utils/utils-definitions.js'; import { assertTrue, assertUnreachable } from '../utils/utils.js'; import { TypeEdge } from './type-edge.js'; @@ -72,64 +72,67 @@ export abstract class Type { - protected initialization: TypeInitializationState = 'Invalid'; // TODO or Identifiable + // store the state of the initialization process of this type + + protected initializationState: TypeInitializationState = 'Invalid'; getInitializationState(): TypeInitializationState { - return this.initialization; + return this.initializationState; } protected assertState(expectedState: TypeInitializationState): void { if (this.isInState(expectedState) === false) { - throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but ${expectedState} is expected.`); + throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but ${expectedState} is expected.`); } } protected assertNotState(expectedState: TypeInitializationState): void { if (this.isNotInState(expectedState) === false) { - throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but this state is not expected.`); + throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`); } } protected assertStateOrLater(expectedState: TypeInitializationState): void { if (this.isInStateOrLater(expectedState) === false) { - throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but this state is not expected.`); + throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`); } } isInState(state: TypeInitializationState): boolean { - return this.initialization === state; + return this.initializationState === state; } isNotInState(state: TypeInitializationState): boolean { - return this.initialization !== state; + return this.initializationState !== state; } isInStateOrLater(state: TypeInitializationState): boolean { switch (state) { case 'Invalid': return true; case 'Identifiable': - return this.initialization !== 'Invalid'; + return this.initializationState !== 'Invalid'; case 'Completed': - return this.initialization === 'Completed'; + return this.initializationState === 'Completed'; default: assertUnreachable(state); } } - // manage listeners for updated state of the current type + + // manage listeners for updates of the initialization state protected stateListeners: TypeStateListener[] = []; - addListener(listener: TypeStateListener, informIfAlreadyFulfilled: boolean): void { + addListener(listener: TypeStateListener, informIfNotInvalidAnymore: boolean): void { this.stateListeners.push(listener); - if (informIfAlreadyFulfilled) { + if (informIfNotInvalidAnymore) { const currentState = this.getInitializationState(); switch (currentState) { case 'Invalid': - // TODO? + // don't inform about the Invalid state! break; case 'Identifiable': listener.switchedToIdentifiable(this); break; case 'Completed': - listener.switchedToIdentifiable(this); + listener.switchedToIdentifiable(this); // inform about both Identifiable and Completed! listener.switchedToCompleted(this); break; default: @@ -145,16 +148,32 @@ export abstract class Type { } } - protected waitForIdentifiable: WaitingForResolvedTypeReferences; - protected waitForCompleted: WaitingForResolvedTypeReferences; + // initialization logic which is specific for the type to initialize + protected onIdentification: () => void; + protected onCompletion: () => void; + protected onInvalidation: () => void; + + // internal helpers + protected waitForIdentifiable: WaitingForIdentifiableAndCompletedTypeReferences; + protected waitForCompleted: WaitingForIdentifiableAndCompletedTypeReferences; protected waitForInvalid: WaitingForInvalidTypeReferences; - // to be called at the end of the constructor of each specific Type implementation! - protected completeInitialization(preconditions: { + /** + * Use this method to specify, how THIS new type should be initialized. + * + * This method has(!) to be called at the end(!) of the constructor of each specific Type implementation, even if nothing has to be specified, + * since calling this method starts the initialization process! + * If you forget the call this method, the new type remains invalid and invisible for Typir and you will not be informed about this problem! + * + * @param preconditions all possible options for the initialization process + */ + protected defineTheInitializationProcessOfThisType(preconditions: { preconditionsForInitialization?: PreconditionsForInitializationState, preconditionsForCompletion?: PreconditionsForInitializationState, referencesRelevantForInvalidation?: TypeReference[], + /** typical use cases: calculate the identifier, register inference rules for the type object already now! */ onIdentification?: () => void, + /** typical use cases: do some internal checks for the completed properties */ onCompletion?: () => void, onInvalidation?: () => void, }): void { @@ -167,13 +186,13 @@ export abstract class Type { console.log(''); } // preconditions for Identifiable - this.waitForIdentifiable = new WaitingForResolvedTypeReferences( + this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForInitialization?.refsToBeIdentified, preconditions.preconditionsForInitialization?.refsToBeCompleted, this, ); // preconditions for Completed - this.waitForCompleted = new WaitingForResolvedTypeReferences( + this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForCompletion?.refsToBeIdentified, preconditions.preconditionsForCompletion?.refsToBeCompleted, this, @@ -187,53 +206,54 @@ export abstract class Type { this.waitForIdentifiable.addListener(() => { this.switchFromInvalidToIdentifiable(); if (this.waitForCompleted.isFulfilled()) { - // this is required to ensure the stric order Identifiable --> Completed, since 'init2' might already be triggered + // this is required to ensure the stric order Identifiable --> Completed, since 'waitForCompleted' might already be triggered this.switchFromIdentifiableToCompleted(); } - }, true); + }, true); // 'true' triggers the initialization process! // identifiable --> completed this.waitForCompleted.addListener(() => { if (this.waitForIdentifiable.isFulfilled()) { this.switchFromIdentifiableToCompleted(); } else { - // switching will be done later by 'init1' in order to conform to the stric order Identifiable --> Completed + // switching will be done later by 'waitForIdentifiable' in order to conform to the stric order Identifiable --> Completed } - }, false); // not required, since init1 will switch to Completed as well! + }, false); // not required, since 'waitForIdentifiable' will switch to Completed as well! // identifiable/completed --> invalid this.waitForInvalid.addListener(() => { if (this.isNotInState('Invalid')) { this.switchFromCompleteOrIdentifiableToInvalid(); } - }, false); // no initial trigger! + }, false); // no initial trigger, since 'Invalid' is the initial state } + /** + * This is an internal method to ignore some types during the initialization process in order to prevent dependency cycles. + * Usually there is no need to call this method on your own. + * @param additionalTypesToIgnore the new types to ignore during + */ ignoreDependingTypesDuringInitialization(additionalTypesToIgnore: Set): void { this.waitForIdentifiable.addTypesToIgnoreForCycles(additionalTypesToIgnore); this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore); } - protected onIdentification: () => void; // typical use cases: calculate the identifier - protected onCompletion: () => void; // typical use cases: determine all properties which depend on other types to be created - protected onInvalidation: () => void; // TODO ist jetzt anders; typical use cases: register inference rules for the type object already now! - protected switchFromInvalidToIdentifiable(): void { this.assertState('Invalid'); this.onIdentification(); - this.initialization = 'Identifiable'; + this.initializationState = 'Identifiable'; this.stateListeners.slice().forEach(listener => listener.switchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications } protected switchFromIdentifiableToCompleted(): void { this.assertState('Identifiable'); this.onCompletion(); - this.initialization = 'Completed'; + this.initializationState = 'Completed'; this.stateListeners.slice().forEach(listener => listener.switchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications } protected switchFromCompleteOrIdentifiableToInvalid(): void { this.assertNotState('Invalid'); this.onInvalidation(); - this.initialization = 'Invalid'; + this.initializationState = 'Invalid'; this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications } @@ -355,4 +375,3 @@ export interface TypeStateListener { switchedToIdentifiable(type: Type): void; switchedToCompleted(type: Type): void; } -// TODO brauchen wir das überhaupt? stattdessen in TypeReference direkt realisieren? diff --git a/packages/typir/src/kinds/bottom-kind.ts b/packages/typir/src/kinds/bottom-kind.ts index 5d9672b..a06e733 100644 --- a/packages/typir/src/kinds/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom-kind.ts @@ -20,7 +20,7 @@ export class BottomType extends Type { constructor(kind: BottomKind, identifier: string) { super(identifier); this.kind = kind; - this.completeInitialization({}); // no preconditions + this.defineTheInitializationProcessOfThisType({}); // no preconditions } override getName(): string { diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 8aa6ad3..3fcace8 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -20,7 +20,6 @@ import { TypeInitializer } from '../utils/type-initialization.js'; // TODO irgendwann die Dateien auseinander ziehen und Packages einführen! -// TODO wenn die Initialisierung von ClassType abgeschlossen ist, sollte darüber aktiv benachrichtigt werden! export class ClassType extends Type { override readonly kind: ClassKind; readonly className: string; @@ -79,12 +78,12 @@ export class ClassType extends Type { fieldsAndMethods.push(...(refMethods as unknown as Array>)); // TODO dirty hack?! // all.push(...refMethods); // does not work - this.completeInitialization({ + this.defineTheInitializationProcessOfThisType({ preconditionsForInitialization: { refsToBeIdentified: fieldsAndMethods, }, preconditionsForCompletion: { - refsToBeCompleted: this.superClasses as unknown as Array>, // TODO here we are waiting for the same/current (identifiable) ClassType!! + refsToBeCompleted: this.superClasses as unknown as Array>, }, referencesRelevantForInvalidation: [...fieldsAndMethods, ...(this.superClasses as unknown as Array>)], onIdentification: () => { @@ -554,7 +553,6 @@ export class ClassTypeInitializer exten if (kind.options.typing === 'Structural') { // TODO Vorsicht Inference rules werden by default an den Identifier gebunden (ebenso Validations)! this.services.graph.addNode(classType, kind.getIdentifierPrefix() + typeDetails.className); - // TODO hinterher wieder abmelden, wenn Type invalid geworden ist bzw. ein anderer Type gewonnen hat? bzw. gewinnt immer der erste Type? } classType.addListener(this, true); // trigger directly, if some initialization states are already reached! @@ -568,6 +566,13 @@ export class ClassTypeInitializer exten */ const readyClassType = this.producedType(classType as ClassType); + // remove/invalidate the duplicated and skipped class type now + if (readyClassType !== classType) { + if (this.kind.options.typing === 'Structural') { + this.services.graph.removeNode(classType, this.kind.getIdentifierPrefix() + this.typeDetails.className); + } + } + // register inference rules registerInferenceRules(this.services, this.typeDetails, this.kind, readyClassType); diff --git a/packages/typir/src/kinds/fixed-parameters-kind.ts b/packages/typir/src/kinds/fixed-parameters-kind.ts index 4cdae75..9f9dbea 100644 --- a/packages/typir/src/kinds/fixed-parameters-kind.ts +++ b/packages/typir/src/kinds/fixed-parameters-kind.ts @@ -49,7 +49,7 @@ export class FixedParameterType extends Type { type: typeValues[i], }); } - this.completeInitialization({}); // TODO preconditions + this.defineTheInitializationProcessOfThisType({}); // TODO preconditions } getParameterTypes(): Type[] { diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index a363c95..9bbcf35 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -52,7 +52,7 @@ export class FunctionType extends Type { }; }); - this.completeInitialization({}); // TODO preconditions + this.defineTheInitializationProcessOfThisType({}); // TODO preconditions } override getName(): string { diff --git a/packages/typir/src/kinds/multiplicity-kind.ts b/packages/typir/src/kinds/multiplicity-kind.ts index 3815c37..73c56c2 100644 --- a/packages/typir/src/kinds/multiplicity-kind.ts +++ b/packages/typir/src/kinds/multiplicity-kind.ts @@ -26,7 +26,7 @@ export class MultiplicityType extends Type { this.constrainedType = constrainedType; this.lowerBound = lowerBound; this.upperBound = upperBound; - this.completeInitialization({}); // TODO preconditions + this.defineTheInitializationProcessOfThisType({}); // TODO preconditions } override getName(): string { diff --git a/packages/typir/src/kinds/primitive-kind.ts b/packages/typir/src/kinds/primitive-kind.ts index f5625af..374be96 100644 --- a/packages/typir/src/kinds/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive-kind.ts @@ -20,7 +20,7 @@ export class PrimitiveType extends Type { constructor(kind: PrimitiveKind, identifier: string) { super(identifier); this.kind = kind; - this.completeInitialization({}); // no preconditions + this.defineTheInitializationProcessOfThisType({}); // no preconditions } override getName(): string { diff --git a/packages/typir/src/kinds/top-kind.ts b/packages/typir/src/kinds/top-kind.ts index 8f49f8c..c426cc9 100644 --- a/packages/typir/src/kinds/top-kind.ts +++ b/packages/typir/src/kinds/top-kind.ts @@ -20,7 +20,7 @@ export class TopType extends Type { constructor(kind: TopKind, identifier: string) { super(identifier); this.kind = kind; - this.completeInitialization({}); // no preconditions + this.defineTheInitializationProcessOfThisType({}); // no preconditions } override getName(): string { diff --git a/packages/typir/src/utils/type-initialization.ts b/packages/typir/src/utils/type-initialization.ts index 8508f7b..97f377c 100644 --- a/packages/typir/src/utils/type-initialization.ts +++ b/packages/typir/src/utils/type-initialization.ts @@ -9,6 +9,19 @@ import { TypirServices } from '../typir.js'; export type TypeInitializerListener = (type: T) => void; +/** + * The purpose of a TypeInitializer is to ensure, that the same type is created and registered only _once_ in the type system. + * This class is used during the use case, when a type declaration in the AST exists, + * for which a corresponding new Typir type needs to be established in the type system ("create new type"). + * + * Without checking for duplicates, the same type might be created twice, e.g. in the following scenario: + * If the creation of A is delayed, since a type B which is required for some properties of A is not yet created, A will be created not now, but later. + * During the "waiting time" for B, another declaration in the AST might be found with the same Typir type A. + * (The second declaration might be wrong, but the user expects to get a validation hint, and not Typir to crash, or the current DSL might allow duplicated type declarations.) + * Since the first Typir type is not yet in the type systems (since it still waits for B) and therefore remains unknown, + * it will be tried to create A a second time, again delayed, since B is still not yet available. + * When B is created, A is waiting twice and might be created twice, if no TypeInitializer is used. + */ export abstract class TypeInitializer { protected readonly services: TypirServices; protected typeToReturn: T | undefined; @@ -27,7 +40,7 @@ export abstract class TypeInitializer { if (existingType) { // ensure, that the same type is not duplicated! this.typeToReturn = existingType as T; - // TODO: newType.invalidate() + // TODO: newType.invalidate() ?? } else { this.typeToReturn = newType; this.services.graph.addNode(newType); @@ -35,7 +48,9 @@ export abstract class TypeInitializer { // inform and clear all listeners this.listeners.slice().forEach(listener => listener(this.typeToReturn!)); - this.listeners = []; + this.listeners = []; // clear the list of listeners, since they will not be informed again + + // return the created/identified type return this.typeToReturn; } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 260fef6..3bfba53 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -50,32 +50,37 @@ export type TypeSelector = export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); -/* TODO ideas -- rekursive anlegen? mit TypeResolver verschmelzen? -- ClassTypeResolver extends TypeResolver? -- ClassType hat Properties - superClasses: TypeReference[] -- TypeReference VS TypeCreator/TypeInitializer -*/ - -export type WaitingForResolvedTypeReferencesListener = (waiter: WaitingForResolvedTypeReferences) => void; +export type WaitingForResolvedTypeReferencesListener = (waiter: WaitingForIdentifiableAndCompletedTypeReferences) => void; /** - * The purpose of this class is to inform some listeners, when all given TypeReferences reached their specified initialization state (or a later state). - * The listeners might be informed multiple times, if at least one of the TypeReferences was unresolved and later on again in the desired state. + * The purpose of this class is to inform its listeners, when all given TypeReferences reached their specified initialization state (or a later state). + * After that, the listeners might be informed multiple times, + * if at least one of the TypeReferences was unresolved/invalid, but later on all TypeReferences are again in the desired state, and so on. */ -export class WaitingForResolvedTypeReferences { +export class WaitingForIdentifiableAndCompletedTypeReferences { + /** Remembers whether all TypeReferences are in the desired states (true) or not (false). */ protected fulfilled: boolean = false; + /** This is required for cyclic type definitions: + * In case of two types A, B which use each other for their properties (e.g. class A {p: B} and class B {p: A}), the case easily occurs, + * that the types A and B (respectively their WaitingFor... instances) are waiting for each other and therefore waiting for each other. + * In order to solve these cycles, types which are part of such "dependency cycles" should be ignored during waiting, + * e.g. A should not waiting B and B should not wait for A. + * These types to ignore are stored in the following Set. + */ protected typesForCycles: Set = new Set(); - // All given TypeReferences must be (at least!) in the state Identifiable or Completed, before the listeners are informed. + /** These TypeReferences must be in the states Identifiable or Completed, before the listeners are informed */ protected readonly waitForRefsIdentified: Array> | undefined; + /** These TypeReferences must be in the state Completed, before the listeners are informed */ protected readonly waitForRefsCompleted: Array> | undefined; + private readonly reactOnNextIdentified: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextIdentified(reference, resolvedType); private readonly reactOnNextCompleted: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextCompleted(reference, resolvedType); private readonly reactOnResetState: TypeReferenceListener = (_reference: TypeReference, _resolvedType: T) => this.listeningForReset(); /** These listeners will be informed once, when all TypeReferences are in the desired state. - * If some of these TypeReferences are invalid and later in the desired state again, the listeners will be informed again. */ + * If some of these TypeReferences are invalid (the listeners will not be informed about this) and later in the desired state again, + * the listeners will be informed again, and so on. */ protected readonly listeners: Array> = []; constructor( @@ -84,7 +89,7 @@ export class WaitingForResolvedTypeReferences { typeCycle: Type | undefined, ) { - // remember the relevant TypeReferences + // remember the relevant TypeReferences to wait for this.waitForRefsIdentified = waitForRefsToBeIdentified; this.waitForRefsCompleted = waitForRefsToBeCompleted; @@ -104,13 +109,13 @@ export class WaitingForResolvedTypeReferences { ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); }); - // everything might already be true + // everything might already be fulfilled this.check(); } addListener(newListener: WaitingForResolvedTypeReferencesListener, informIfAlreadyFulfilled: boolean): void { this.listeners.push(newListener); - // inform new listener, if the state is already reached! + // inform the new listener, if the state is already reached! if (informIfAlreadyFulfilled && this.fulfilled) { newListener(this); } @@ -123,33 +128,38 @@ export class WaitingForResolvedTypeReferences { } } - addTypesToIgnoreForCycles(moreTypes: Set): void { - const newTypes: Set = new Set(); - for (const anotherType of moreTypes) { - if (this.typesForCycles.has(anotherType)) { - // ignore this additional type, required to break the propagation, since it becomes cyclic as well in case of cyclic types! + addTypesToIgnoreForCycles(moreTypesToIgnore: Set): void { + // identify the actual new types to ignore (filtering out the types which are already ignored) + const newTypesToIgnore: Set = new Set(); + for (const typeToIgnore of moreTypesToIgnore) { + if (this.typesForCycles.has(typeToIgnore)) { + // ignore this additional type, required to break the propagation, since the propagation becomes cyclic as well in case of cyclic types! } else { - newTypes.add(anotherType); - this.typesForCycles.add(anotherType); + newTypesToIgnore.add(typeToIgnore); + this.typesForCycles.add(typeToIgnore); } } - if (newTypes.size >= 1) { - // propagate the new types to ignore recursively to indirect dependencies as well + if (newTypesToIgnore.size <= 0) { + // no new types to ignore => do nothing + } else { + // propagate the new types to ignore recursively to all direct and indirect referenced types ... + // ... which should be identifiable (or completed) for (const ref of (this.waitForRefsIdentified ?? [])) { const refType = ref.getType(); if (refType?.isInStateOrLater('Identifiable')) { - // already resolved (TODO is that correct?) + // this reference is already ready } else { - refType?.ignoreDependingTypesDuringInitialization(newTypes); + refType?.ignoreDependingTypesDuringInitialization(newTypesToIgnore); } } + // ... which should be completed for (const ref of (this.waitForRefsCompleted ?? [])) { const refType = ref.getType(); if (refType?.isInStateOrLater('Completed')) { - // already resolved (TODO is that correct?) + // this reference is already ready } else { - refType?.ignoreDependingTypesDuringInitialization(newTypes); + refType?.ignoreDependingTypesDuringInitialization(newTypesToIgnore); } } @@ -163,19 +173,19 @@ export class WaitingForResolvedTypeReferences { resolvedType.ignoreDependingTypesDuringInitialization(this.typesForCycles); // check, whether all TypeReferences are resolved and the resolved types are in the expected state this.check(); - // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?? + // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"? } protected listeningForNextCompleted(_reference: TypeReference, _resolvedType: T): void { // check, whether all TypeReferences are resolved and the resolved types are in the expected state this.check(); - // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?? + // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"? } protected listeningForReset(): void { // since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again) this.fulfilled = false; - // TODO should listeners be informed about this invalidation? + // the listeners are not informed about this invalidation; TODO might that help to get rid of WaitingForInvalidTypeReferences ?? } protected check() { @@ -270,12 +280,16 @@ export class WaitingForInvalidTypeReferences { -// react on type found/identified/resolved/unresolved +/** + * A listener for TypeReferences, who will be informed about the found/identified/resolved/unresolved type of the current TypeReference. + */ export type TypeReferenceListener = (reference: TypeReference, resolvedType: T) => void; /** - * A TypeReference accepts a specification and resolves a type from this specification. + * A TypeReference accepts a specification for a type and searches for the corresponding type in the type system according to this specification. * Different TypeReferences might resolve to the same Type. + * This class is used during the use case, when a Typir type uses other types for its properties, + * e.g. class types use other types from the type system for describing the types of its fields ("use existing type"). * * The internal logic of a TypeReference is independent from the kind of the type to resolve. * A TypeReference takes care of the lifecycle of the types. @@ -287,12 +301,23 @@ export class TypeReference implements TypeGraphListener, protected readonly services: TypirServices; protected resolvedType: T | undefined = undefined; - // These listeners will be informed once and only about the transitions! - // Additionally, if the resolved type is already 'Completed', the listeners for 'Identifiable' will be informed as well. + /* TODO Review zur Architektur (also zu "implements TypeStateListener"): + Alternativ könnte der Initialization-State komplett aus TypeReference rausgenommen werden und nur resolved/unresolved betrachtet werden? + Das würde die TypeReference vereinfachen und würde nicht zwei Sachen unnötigerweise vermengen. + Andererseits würde die Logik/Komplexität vielleicht auch nur woanders hinverschoben. + */ + + /** These listeners will be informed, whenever the type of this TypeReference was unresolved/unknown and it is known/resolved now. + * If the resolved type is already 'Completed', these listeners will be informed as well. */ protected readonly reactOnIdentified: Array> = []; + /** These listeners will be informed, whenever the state type of this TypeReference is 'Completed' now, + * since it was unresolved or identifiable before. */ protected readonly reactOnCompleted: Array> = []; + /** These listeners will be informed, whenever the state type of this TypeReference is invalid or unresolved now, + * since it was identifiable or completed before. */ protected readonly reactOnUnresolved: Array> = []; + // TODO introduce TypeReference factory service in order to replace the implementation? constructor(selector: TypeSelector, services: TypirServices) { this.selector = selector; this.services = services; @@ -306,14 +331,12 @@ export class TypeReference implements TypeGraphListener, // react on new types this.services.graph.addListener(this); - // react on state changes of already existing types which are not (yet) completed - this.services.graph.getAllRegisteredTypes().forEach(type => { - if (type.getInitializationState() !== 'Completed') { - type.addListener(this, false); - } - }); // react on new inference rules this.services.inference.addListener(this); + // don't react on state changes of already existing types which are not (yet) completed, since TypeSelectors don't care about the initialization state of types + + // try to resolve now + this.resolve(); } protected stopResolving(): void { @@ -331,7 +354,6 @@ export class TypeReference implements TypeGraphListener, } isInState(state: TypeInitializationState): boolean { - this.resolve(); // lazyly resolve on request if (state === 'Invalid' && this.resolvedType === undefined) { return true; } @@ -341,7 +363,6 @@ export class TypeReference implements TypeGraphListener, return !this.isInState(state); } isInStateOrLater(state: TypeInitializationState): boolean { - this.resolve(); // lazyly resolve on request switch (state) { case 'Invalid': return true; @@ -351,10 +372,14 @@ export class TypeReference implements TypeGraphListener, } getType(): T | undefined { - this.resolve(); // lazyly resolve on request return this.resolvedType; } + /** + * Resolves the referenced type, if the type is not yet resolved. + * Note that the resolved type might not be completed yet. + * @returns the result of the currently executed resolution + */ protected resolve(): 'ALREADY_RESOLVED' | 'SUCCESSFULLY_RESOLVED' | 'RESOLVING_FAILED' { if (this.resolvedType) { // the type is already resolved => nothing to do @@ -370,14 +395,13 @@ export class TypeReference implements TypeGraphListener, this.stopResolving(); // notify observers if (this.isInStateOrLater('Identifiable')) { - this.reactOnIdentified.forEach(listener => listener(this, resolvedType)); + this.reactOnIdentified.slice().forEach(listener => listener(this, resolvedType)); } if (this.isInStateOrLater('Completed')) { - this.reactOnCompleted.forEach(listener => listener(this, resolvedType)); - } - if (this.isNotInState('Completed')) { + this.reactOnCompleted.slice().forEach(listener => listener(this, resolvedType)); + } else { // register to get updates for the resolved type in order to notify the observers of this TypeReference about the missing "identifiable" and "completed" cases above - resolvedType.addListener(this, false); // TODO or is this already done?? + resolvedType.addListener(this, false); } return 'SUCCESSFULLY_RESOLVED'; } else { @@ -386,9 +410,16 @@ export class TypeReference implements TypeGraphListener, } } + /** + * Tries to find the specified type in the type system. + * This method does not care about the initialization state of the found type, + * this method is restricted to just search and find any type according to the given TypeSelector. + * @param selector the specification for the desired type + * @returns the found type or undefined, it there is no such type in the type system + */ protected tryToResolve(selector: TypeSelector): T | undefined { - // TODO is there a way to explicitly enfore/ensure "as T"? if (isType(selector)) { + // TODO is there a way to explicitly enforce/ensure "as T"? return selector as T; } else if (typeof selector === 'string') { return this.services.graph.getType(selector) as T; @@ -398,8 +429,9 @@ export class TypeReference implements TypeGraphListener, return selector.getType(); } else if (typeof selector === 'function') { return this.tryToResolve(selector()); // execute the function and try to recursively resolve the returned result again - } else { + } else { // the selector is of type 'known' => do type inference on it const result = this.services.inference.inferType(selector); + // TODO failures must not be cached, otherwise a type will never be found in the future!! if (isType(result)) { return result as T; } else { @@ -447,20 +479,16 @@ export class TypeReference implements TypeGraphListener, } - addedType(addedType: Type, _key: string): void { + addedType(_addedType: Type, _key: string): void { // after adding a new type, try to resolve the type - const result = this.resolve(); // is it possible to do this more performant by looking at the "addedType"? - if (result === 'RESOLVING_FAILED' && addedType.getInitializationState() !== 'Completed') { - // react on new states of this type as well, since the TypeSelector might depend on a particular state of the specified type - addedType.addListener(this, false); // the removal of this listener happens automatically! TODO doch nicht? - } + this.resolve(); // possible performance optimization: is it possible to do this more performant by looking at the "addedType"? } removedType(removedType: Type, _key: string): void { // the resolved type of this TypeReference is removed! if (removedType === this.resolvedType) { // notify observers, that the type reference is broken - this.reactOnUnresolved.forEach(listener => listener(this, this.resolvedType!)); + this.reactOnUnresolved.slice().forEach(listener => listener(this, this.resolvedType!)); // start resolving the type again this.startResolving(); } @@ -475,40 +503,38 @@ export class TypeReference implements TypeGraphListener, addedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void { // after adding a new inference rule, try to resolve the type - this.resolve(); + this.resolve(); // possible performance optimization: use only the new inference rule to resolve the type } removedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void { - // empty + // empty, since removed inference rules don't help to resolve a type } switchedToIdentifiable(type: Type): void { - const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? - if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { + if (type === this.resolvedType) { // the type was already resolved, but some observers of this TypeReference still need to be informed this.reactOnIdentified.slice().forEach(listener => listener(this, this.resolvedType!)); // slice() prevents issues with removal of listeners during notifications + } else { + throw new Error('This case should not happen, since this TypeReference is registered only for the resolved type.'); } } switchedToCompleted(type: Type): void { - const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"? - if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) { + if (type === this.resolvedType) { // the type was already resolved, but some observers of this TypeReference still need to be informed this.reactOnCompleted.slice().forEach(listener => listener(this, this.resolvedType!)); // slice() prevents issues with removal of listeners during notifications + type.removeListener(this); // all listeners are informed now, therefore no more notifications are needed + } else { + throw new Error('This case should not happen, since this TypeReference is registered only for the resolved type.'); } } - switchedToInvalid(_type: Type): void { - // TODO + switchedToInvalid(type: Type): void { + type.removeListener(this); } } export function resolveTypeSelector(services: TypirServices, selector: TypeSelector): Type { - /** TODO this is only a rough sketch: - * - detect cycles/deadlocks during the resolving process - * - make the resolving strategy exchangable - * - integrate it into TypeReference implementation? - */ if (isType(selector)) { return selector; } else if (typeof selector === 'string') { @@ -516,7 +542,7 @@ export function resolveTypeSelector(services: TypirServices, selector: TypeSelec if (result) { return result; } else { - throw new Error('TODO not-found problem'); + throw new Error(`A type with identifier '${selector}' as TypeSelector does not exist in the type graph.`); } } else if (selector instanceof TypeInitializer) { return selector.getType(); @@ -529,7 +555,7 @@ export function resolveTypeSelector(services: TypirServices, selector: TypeSelec if (isType(result)) { return result; } else { - throw new Error('TODO handle inference problem for ' + services.printer.printDomainElement(selector, false)); + throw new Error(`For '${services.printer.printDomainElement(selector, false)}' as TypeSelector, no type can be inferred.`); } } } From 005c37f7b1aa39244feaf54d8fb00972c41851ed Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 20 Nov 2024 19:53:52 +0100 Subject: [PATCH 10/19] fixed registration of inference rules of classes, new deconstruct logic for types, simplified TypeReference, reworked listeners, improved test cases, more comments --- examples/lox/test/lox-type-checking.test.ts | 92 ++++--- .../src/utils/typir-langium-utils.ts | 2 +- packages/typir/src/features/inference.ts | 12 + packages/typir/src/graph/type-node.ts | 66 +++-- packages/typir/src/index.ts | 1 + packages/typir/src/kinds/class-kind.ts | 81 ++++-- packages/typir/src/utils/test-utils.ts | 17 ++ .../typir/src/utils/type-initialization.ts | 2 +- packages/typir/src/utils/utils-definitions.ts | 243 +++++++----------- 9 files changed, 287 insertions(+), 229 deletions(-) create mode 100644 packages/typir/src/utils/test-utils.ts diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index d0aca0e..d682fa8 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -6,16 +6,23 @@ import { EmptyFileSystem } from 'langium'; import { parseDocument } from 'langium/test'; +import { deleteAllDocuments } from 'typir-langium'; import { afterEach, describe, expect, test } from 'vitest'; import type { Diagnostic } from 'vscode-languageserver-types'; import { DiagnosticSeverity } from 'vscode-languageserver-types'; -import { createLoxServices } from '../src/language/lox-module.js'; -import { deleteAllDocuments } from 'typir-langium'; import { isClassType } from '../../../packages/typir/lib/kinds/class-kind.js'; +import { isFunctionType } from '../../../packages/typir/lib/kinds/function-kind.js'; +import { createLoxServices } from '../src/language/lox-module.js'; +import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; const loxServices = createLoxServices(EmptyFileSystem).Lox; -afterEach(async () => await deleteAllDocuments(loxServices)); +afterEach(async () => { + await deleteAllDocuments(loxServices); + // check, that there are no user-defined classes and functions after clearing/invalidating all LOX documents + expectTypirTypes(loxServices, isClassType); + expectTypirTypes(loxServices, isFunctionType, '-', '*', '/', '+', '+', '+', '+', '<', '<=', '>', '>=', 'and', 'or', '==', '!=', '=', '!', '-'); +}); describe('Explicitly test type checking for LOX', () => { @@ -306,38 +313,51 @@ describe('Explicitly test type checking for LOX', () => { }); describe('Cyclic type definitions where a Class is declared and already used', () => { - test('Class with field', async () => await validate(` - class Node { - children: Node - } - `, [])); + test('Class with field', async () => { + await validate(` + class Node { + children: Node + } + `, []); + expectTypirTypes(loxServices, isClassType, 'Node'); + }); - test('Two Classes with fields with the other Class as type', async () => await validate(` - class A { - prop1: B - } - class B { - prop2: A - } - `, [])); + test('Two Classes with fields with the other Class as type', async () => { + await validate(` + class A { + prop1: B + } + class B { + prop2: A + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + }); - test.todo('Three Classes with fields with the other Class as type', async () => await validate(` - class A { - prop1: B - } - class B { - prop2: C - } - class C { - prop3: A - } - `, [])); + test('Three Classes with fields with the other Class as type', async () => { + await validate(` + class A { + prop1: B + } + class B { + prop2: C + } + class C { + prop3: A + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B', 'C'); + }); - test.todo('Class with method', async () => await validate(` - class Node { - myMethod(input: number): Class {} - } - `, [])); + test.todo('Class with method', async () => { + await validate(` + class Node { + myMethod(input: number): Node {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'Node'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod'); + }); test('Having two declarations for the delayed class A, but only one type A in the type system', async () => { await validate(` @@ -353,10 +373,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( 'Declared classes need to be unique (A).', ]); // check, that there is only one class type A in the type graph: - const classes = loxServices.graph.getAllRegisteredTypes().filter(t => isClassType(t)).map(t => t.getName()); - expect(classes).toHaveLength(2); - expect(classes).includes('A'); - expect(classes).includes('B'); + expectTypirTypes(loxServices, isClassType, 'A', 'B'); }); }); @@ -372,6 +389,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance 'Circles in super-sub-class-relationships are not allowed: MyClass2', 'Circles in super-sub-class-relationships are not allowed: MyClass3', ]); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2', 'MyClass3'); }); test('Two involved classes: 1 -> 2 -> 1', async () => { @@ -382,12 +400,14 @@ describe('Test internal validation of Typir for cycles in the class inheritance 'Circles in super-sub-class-relationships are not allowed: MyClass1', 'Circles in super-sub-class-relationships are not allowed: MyClass2', ]); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); }); test('One involved class: 1 -> 1', async () => { await validate(` class MyClass1 < MyClass1 { } `, 'Circles in super-sub-class-relationships are not allowed: MyClass1'); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); }); }); diff --git a/packages/typir-langium/src/utils/typir-langium-utils.ts b/packages/typir-langium/src/utils/typir-langium-utils.ts index f85442c..6fcab2e 100644 --- a/packages/typir-langium/src/utils/typir-langium-utils.ts +++ b/packages/typir-langium/src/utils/typir-langium-utils.ts @@ -25,6 +25,6 @@ export async function deleteAllDocuments(services: LangiumServices) { .toArray(); await services.shared.workspace.DocumentBuilder.update( [], // update no documents - docsToDelete + docsToDelete // delete all documents ); } diff --git a/packages/typir/src/features/inference.ts b/packages/typir/src/features/inference.ts index 81e0fb8..5afcb9c 100644 --- a/packages/typir/src/features/inference.ts +++ b/packages/typir/src/features/inference.ts @@ -111,6 +111,7 @@ export interface TypeInferenceCollector { * If the given type is removed from the type system, this rule will be automatically removed as well. */ addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void; + removeInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void; addListener(listener: TypeInferenceCollectorListener): void; removeListener(listener: TypeInferenceCollectorListener): void; @@ -140,6 +141,17 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty this.listeners.forEach(listener => listener.addedInferenceRule(rule, boundToType)); } + removeInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void { + const key = this.getBoundToTypeKey(boundToType); + const rules = this.inferenceRules.get(key); + if (rules) { + const index = rules.indexOf(rule); + if (index >= 0) { + rules.splice(index, 1); + } + } + } + protected getBoundToTypeKey(boundToType?: Type): string { return boundToType?.getIdentifier() ?? ''; } diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index c93f0cf..23f4f3d 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -168,8 +168,13 @@ export abstract class Type { * @param preconditions all possible options for the initialization process */ protected defineTheInitializationProcessOfThisType(preconditions: { + /** Contains only those TypeReferences which are required to do the initialization. */ preconditionsForInitialization?: PreconditionsForInitializationState, + /** Contains only those TypeReferences which are required to do the completion. + * TypeReferences which are required only for the initialization, but not for the completion, + * don't need to be repeated here, since the completion is done only after the initialization. */ preconditionsForCompletion?: PreconditionsForInitializationState, + /** Must contain all(!) TypeReferences of a type. */ referencesRelevantForInvalidation?: TypeReference[], /** typical use cases: calculate the identifier, register inference rules for the type object already now! */ onIdentification?: () => void, @@ -202,27 +207,38 @@ export abstract class Type { preconditions.referencesRelevantForInvalidation ?? [], ); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const thisType = this; + // invalid --> identifiable - this.waitForIdentifiable.addListener(() => { - this.switchFromInvalidToIdentifiable(); - if (this.waitForCompleted.isFulfilled()) { - // this is required to ensure the stric order Identifiable --> Completed, since 'waitForCompleted' might already be triggered - this.switchFromIdentifiableToCompleted(); - } + this.waitForIdentifiable.addListener({ + onFulfilled(_waiter) { + thisType.switchFromInvalidToIdentifiable(); + if (thisType.waitForCompleted.isFulfilled()) { + // this is required to ensure the stric order Identifiable --> Completed, since 'waitForCompleted' might already be triggered + thisType.switchFromIdentifiableToCompleted(); + } + }, + onInvalidated(_waiter) { + thisType.switchFromCompleteOrIdentifiableToInvalid(); + }, }, true); // 'true' triggers the initialization process! // identifiable --> completed - this.waitForCompleted.addListener(() => { - if (this.waitForIdentifiable.isFulfilled()) { - this.switchFromIdentifiableToCompleted(); - } else { - // switching will be done later by 'waitForIdentifiable' in order to conform to the stric order Identifiable --> Completed - } + this.waitForCompleted.addListener({ + onFulfilled(_waiter) { + if (thisType.waitForIdentifiable.isFulfilled()) { + thisType.switchFromIdentifiableToCompleted(); + } else { + // switching will be done later by 'waitForIdentifiable' in order to conform to the stric order Identifiable --> Completed + } + }, + onInvalidated(_waiter) { + thisType.switchFromCompleteOrIdentifiableToInvalid(); + }, }, false); // not required, since 'waitForIdentifiable' will switch to Completed as well! // identifiable/completed --> invalid this.waitForInvalid.addListener(() => { - if (this.isNotInState('Invalid')) { - this.switchFromCompleteOrIdentifiableToInvalid(); - } + this.switchFromCompleteOrIdentifiableToInvalid(); }, false); // no initial trigger, since 'Invalid' is the initial state } @@ -236,6 +252,15 @@ export abstract class Type { this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore); } + deconstruct(): void { + // clear everything + this.stateListeners.splice(0, this.stateListeners.length); + this.waitForInvalid.getWaitForRefsInvalid().forEach(ref => ref.deconstruct()); + this.waitForIdentifiable.deconstruct(); + this.waitForCompleted.deconstruct(); + this.waitForInvalid.deconstruct(); + } + protected switchFromInvalidToIdentifiable(): void { this.assertState('Invalid'); this.onIdentification(); @@ -251,10 +276,13 @@ export abstract class Type { } protected switchFromCompleteOrIdentifiableToInvalid(): void { - this.assertNotState('Invalid'); - this.onInvalidation(); - this.initializationState = 'Invalid'; - this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications + if (this.isNotInState('Invalid')) { + this.onInvalidation(); + this.initializationState = 'Invalid'; + this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications + } else { + // is already 'Invalid' => do nothing + } } diff --git a/packages/typir/src/index.ts b/packages/typir/src/index.ts index 71d0d3d..c608346 100644 --- a/packages/typir/src/index.ts +++ b/packages/typir/src/index.ts @@ -25,6 +25,7 @@ export * from './kinds/multiplicity-kind.js'; export * from './kinds/primitive-kind.js'; export * from './kinds/top-kind.js'; export * from './utils/dependency-injection.js'; +export * from './utils/test-utils.js'; export * from './utils/utils.js'; export * from './utils/type-initialization.js'; export * from './utils/utils-definitions.js'; diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 3fcace8..0e2c416 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -6,7 +6,7 @@ import { assertUnreachable } from 'langium'; import { TypeEqualityProblem } from '../features/equality.js'; -import { InferenceProblem, InferenceRuleNotApplicable } from '../features/inference.js'; +import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../features/inference.js'; import { SubTypeProblem } from '../features/subtype.js'; import { ValidationProblem, ValidationRule, ValidationRuleWithBeforeAfter } from '../features/validation.js'; import { Type, TypeStateListener, isType } from '../graph/type-node.js'; @@ -34,17 +34,26 @@ export class ClassType extends Type { this.kind = kind; this.className = typeDetails.className; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const thisType = this; + // resolve the super classes this.superClasses = toArray(typeDetails.superClasses).map(superr => { const superRef = new TypeReference(superr, kind.services); - superRef.addReactionOnTypeCompleted((_ref, superType) => { - // after the super-class is complete, register this class as sub-class for that super-class - superType.subClasses.push(this); + superRef.addListener({ + onTypeReferenceResolved(_reference, superType) { + // after the super-class is complete, register this class as sub-class for that super-class + superType.subClasses.push(thisType); + }, + onTypeReferenceInvalidated(_reference, superType) { + if (superType) { + // if the superType gets invalid, de-register this class as sub-class of the super-class + superType.subClasses.splice(superType.subClasses.indexOf(thisType), 1); + } else { + // initially do nothing + } + }, }, true); - superRef.addReactionOnTypeUnresolved((_ref, superType) => { - // if the superType gets invalid, de-register this class as sub-class of the super-class - superType.subClasses.splice(superType.subClasses.indexOf(this), 1); - }, false); return superRef; }); @@ -538,9 +547,11 @@ export function isClassKind(kind: unknown): kind is ClassKind { } +// TODO Review: Is it better to not have "extends TypeInitializer" and to merge TypeInitializer+ClassTypeInitializer into one class? export class ClassTypeInitializer extends TypeInitializer implements TypeStateListener { protected readonly typeDetails: CreateClassTypeDetails; protected readonly kind: ClassKind; + protected inferenceRules: TypeInferenceRule[]; constructor(services: TypirServices, kind: ClassKind, typeDetails: CreateClassTypeDetails) { super(services); @@ -549,12 +560,15 @@ export class ClassTypeInitializer exten // create the class type const classType = new ClassType(kind, typeDetails as CreateClassTypeDetails); - // TODO erst nach Herausfiltern im Initializer darf der Type selbst sich registrieren! if (kind.options.typing === 'Structural') { - // TODO Vorsicht Inference rules werden by default an den Identifier gebunden (ebenso Validations)! + // register structural classes also by their names, since these names are usually used for reference in the DSL/AST! this.services.graph.addNode(classType, kind.getIdentifierPrefix() + typeDetails.className); } + this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, classType); + // register all the inference rules already now to enable early type inference for this Class type + this.inferenceRules.forEach(rule => services.inference.addInferenceRule(rule, undefined)); // 'undefined', since the Identifier is still missing + classType.addListener(this, true); // trigger directly, if some initialization states are already reached! } @@ -568,16 +582,28 @@ export class ClassTypeInitializer exten // remove/invalidate the duplicated and skipped class type now if (readyClassType !== classType) { + // the class type changed, since the same type was already created earlier and is reused here (this is a special case) => skip the classType! + classType.removeListener(this); // since this ClassTypeInitializer initialized the invalid type, there is nothing to do anymore here! + if (this.kind.options.typing === 'Structural') { + // replace the type in the type graph this.services.graph.removeNode(classType, this.kind.getIdentifierPrefix() + this.typeDetails.className); + this.services.graph.addNode(readyClassType, this.kind.getIdentifierPrefix() + this.typeDetails.className); } - } - // register inference rules - registerInferenceRules(this.services, this.typeDetails, this.kind, readyClassType); + // remove the inference rules for the invalid type + this.inferenceRules.forEach(rule => this.services.inference.removeInferenceRule(rule, undefined)); + // but re-create the inference rules for the new type!! + // This is required, since inference rules for different declarations in the AST might be different, but should infer the same Typir type! + this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, readyClassType); + this.inferenceRules.forEach(rule => this.services.inference.addInferenceRule(rule, readyClassType)); + } else { + // the class type is unchanged (this is the usual case) - // the work of this initializer is done now - classType.removeListener(this); + // keep the existing inference rules, but register it for the unchanged class type + this.inferenceRules.forEach(rule => this.services.inference.removeInferenceRule(rule, undefined)); + this.inferenceRules.forEach(rule => this.services.inference.addInferenceRule(rule, readyClassType)); + } } switchedToCompleted(classType: Type): void { @@ -589,6 +615,9 @@ export class ClassTypeInitializer exten throw new Error(`Circles in super-sub-class-relationships are not allowed: ${classType.getName()}`); } } + + // the work of this initializer is done now + classType.removeListener(this); } switchedToInvalid(_previousClassType: Type): void { @@ -597,9 +626,10 @@ export class ClassTypeInitializer exten } -function registerInferenceRules(services: TypirServices, typeDetails: CreateClassTypeDetails, classKind: ClassKind, classType: ClassType) { +function createInferenceRules(typeDetails: CreateClassTypeDetails, classKind: ClassKind, classType: ClassType): TypeInferenceRule[] { + const result: TypeInferenceRule[] = []; if (typeDetails.inferenceRuleForDeclaration) { - services.inference.addInferenceRule({ + result.push({ inferTypeWithoutChildren(domainElement, _typir) { if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { return classType; @@ -611,16 +641,16 @@ function registerInferenceRules(services: TypirServices, typeDetails: // TODO check values for fields for nominal typing! return classType; }, - }, classType); + }); } if (typeDetails.inferenceRuleForLiteral) { - registerInferenceRuleForLiteral(services, typeDetails.inferenceRuleForLiteral, classKind, classType); + result.push(createInferenceRuleForLiteral(typeDetails.inferenceRuleForLiteral, classKind, classType)); } if (typeDetails.inferenceRuleForReference) { - registerInferenceRuleForLiteral(services, typeDetails.inferenceRuleForReference, classKind, classType); + result.push(createInferenceRuleForLiteral(typeDetails.inferenceRuleForReference, classKind, classType)); } if (typeDetails.inferenceRuleForFieldAccess) { - services.inference.addInferenceRule((domainElement, _typir) => { + result.push((domainElement, _typir) => { const result = typeDetails.inferenceRuleForFieldAccess!(domainElement); if (result === InferenceRuleNotApplicable) { return InferenceRuleNotApplicable; @@ -641,13 +671,14 @@ function registerInferenceRules(services: TypirServices, typeDetails: } else { return result; // do the type inference for this element instead } - }, classType); + }); } + return result; } -function registerInferenceRuleForLiteral(services: TypirServices, rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): void { +function createInferenceRuleForLiteral(rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): TypeInferenceRule { const mapListConverter = new MapListConverter(); - services.inference.addInferenceRule({ + return { inferTypeWithoutChildren(domainElement, _typir) { const result = rule.filter(domainElement); if (result) { @@ -692,7 +723,7 @@ function registerInferenceRuleForLiteral(services: TypirServices, rule: Infer return classType; } }, - }, classType); + }; } diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts new file mode 100644 index 0000000..b4fcc00 --- /dev/null +++ b/packages/typir/src/utils/test-utils.ts @@ -0,0 +1,17 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { expect } from 'vitest'; +import { Type } from '../graph/type-node.js'; +import { TypirServices } from '../typir.js'; + +export function expectTypirTypes(services: TypirServices, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): void { + const typeNames = services.graph.getAllRegisteredTypes().filter(filterTypes).map(t => t.getName()); + expect(typeNames, typeNames.join(', ')).toHaveLength(namesOfExpectedTypes.length); + for (const name of namesOfExpectedTypes) { + expect(typeNames).includes(name); + } +} diff --git a/packages/typir/src/utils/type-initialization.ts b/packages/typir/src/utils/type-initialization.ts index 97f377c..32ca40d 100644 --- a/packages/typir/src/utils/type-initialization.ts +++ b/packages/typir/src/utils/type-initialization.ts @@ -40,7 +40,7 @@ export abstract class TypeInitializer { if (existingType) { // ensure, that the same type is not duplicated! this.typeToReturn = existingType as T; - // TODO: newType.invalidate() ?? + newType.deconstruct(); } else { this.typeToReturn = newType; this.services.graph.addNode(newType); diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 3bfba53..1a47f9d 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -9,7 +9,7 @@ import { TypeInferenceCollectorListener, TypeInferenceRule } from '../features/inference.js'; import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; -import { isType, Type, TypeInitializationState, TypeStateListener } from '../graph/type-node.js'; +import { isType, Type, TypeStateListener } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { TypeInitializer } from './type-initialization.js'; import { toArray } from './utils.js'; @@ -50,14 +50,17 @@ export type TypeSelector = export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); -export type WaitingForResolvedTypeReferencesListener = (waiter: WaitingForIdentifiableAndCompletedTypeReferences) => void; +export interface WaitingForResolvedTypeReferencesListener { + onFulfilled(waiter: WaitingForIdentifiableAndCompletedTypeReferences): void; + onInvalidated(waiter: WaitingForIdentifiableAndCompletedTypeReferences): void; +} /** * The purpose of this class is to inform its listeners, when all given TypeReferences reached their specified initialization state (or a later state). * After that, the listeners might be informed multiple times, * if at least one of the TypeReferences was unresolved/invalid, but later on all TypeReferences are again in the desired state, and so on. */ -export class WaitingForIdentifiableAndCompletedTypeReferences { +export class WaitingForIdentifiableAndCompletedTypeReferences implements TypeReferenceListener, TypeStateListener { /** Remembers whether all TypeReferences are in the desired states (true) or not (false). */ protected fulfilled: boolean = false; /** This is required for cyclic type definitions: @@ -74,10 +77,6 @@ export class WaitingForIdentifiableAndCompletedTypeReferences> | undefined; - private readonly reactOnNextIdentified: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextIdentified(reference, resolvedType); - private readonly reactOnNextCompleted: TypeReferenceListener = (reference: TypeReference, resolvedType: T) => this.listeningForNextCompleted(reference, resolvedType); - private readonly reactOnResetState: TypeReferenceListener = (_reference: TypeReference, _resolvedType: T) => this.listeningForReset(); - /** These listeners will be informed once, when all TypeReferences are in the desired state. * If some of these TypeReferences are invalid (the listeners will not be informed about this) and later in the desired state again, * the listeners will be informed again, and so on. */ @@ -99,25 +98,29 @@ export class WaitingForIdentifiableAndCompletedTypeReferences { - ref.addReactionOnTypeIdentified(this.reactOnNextIdentified, false); - ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); - }); - toArray(this.waitForRefsCompleted).forEach(ref => { - ref.addReactionOnTypeIdentified(this.reactOnNextIdentified, false); - ref.addReactionOnTypeCompleted(this.reactOnNextCompleted, false); - ref.addReactionOnTypeUnresolved(this.reactOnResetState, false); - }); + toArray(this.waitForRefsIdentified).forEach(ref => ref.addListener(this, false)); + toArray(this.waitForRefsCompleted).forEach(ref => ref.addListener(this, false)); // everything might already be fulfilled - this.check(); + this.checkIfFulfilled(); } - addListener(newListener: WaitingForResolvedTypeReferencesListener, informIfAlreadyFulfilled: boolean): void { + deconstruct(): void { + this.listeners.splice(0, this.listeners.length); + this.waitForRefsIdentified?.forEach(ref => ref.removeListener(this)); + this.waitForRefsCompleted?.forEach(ref => ref.removeListener(this)); + this.typesForCycles.clear(); + } + + addListener(newListener: WaitingForResolvedTypeReferencesListener, informAboutCurrentState: boolean): void { this.listeners.push(newListener); - // inform the new listener, if the state is already reached! - if (informIfAlreadyFulfilled && this.fulfilled) { - newListener(this); + // inform the new listener + if (informAboutCurrentState) { + if (this.fulfilled) { + newListener.onFulfilled(this); + } else { + newListener.onInvalidated(this); + } } } @@ -164,31 +167,43 @@ export class WaitingForIdentifiableAndCompletedTypeReferences, resolvedType: T): void { + onTypeReferenceResolved(_reference: TypeReference, resolvedType: Type): void { // inform the referenced type about the types to ignore for completion resolvedType.ignoreDependingTypesDuringInitialization(this.typesForCycles); + resolvedType.addListener(this, false); // check, whether all TypeReferences are resolved and the resolved types are in the expected state - this.check(); + this.checkIfFulfilled(); // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"? } - protected listeningForNextCompleted(_reference: TypeReference, _resolvedType: T): void { + onTypeReferenceInvalidated(_reference: TypeReference, previousType: Type | undefined): void { + // since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again) + this.switchToNotFulfilled(); + if (previousType) { + previousType.removeListener(this); + } + } + + switchedToIdentifiable(_type: Type): void { // check, whether all TypeReferences are resolved and the resolved types are in the expected state - this.check(); + this.checkIfFulfilled(); // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"? } - - protected listeningForReset(): void { + switchedToCompleted(_type: Type): void { + // check, whether all TypeReferences are resolved and the resolved types are in the expected state + this.checkIfFulfilled(); + // TODO is a more performant solution possible, e.g. by counting or using "resolvedType"? + } + switchedToInvalid(_type: Type): void { // since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again) - this.fulfilled = false; - // the listeners are not informed about this invalidation; TODO might that help to get rid of WaitingForInvalidTypeReferences ?? + this.switchToNotFulfilled(); } - protected check() { + protected checkIfFulfilled(): void { // already informed => do not inform again if (this.fulfilled) { return; @@ -213,7 +228,17 @@ export class WaitingForIdentifiableAndCompletedTypeReferences inform all listeners this.fulfilled = true; // don't inform the listeners again - this.listeners.slice().forEach(listener => listener(this)); // slice() prevents issues with removal of listeners during notifications + this.listeners.slice().forEach(listener => listener.onFulfilled(this)); // slice() prevents issues with removal of listeners during notifications + } + + protected switchToNotFulfilled(): void { + // since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again) + if (this.fulfilled) { + this.fulfilled = false; + this.listeners.slice().forEach(listener => listener.onInvalidated(this)); // slice() prevents issues with removal of listeners during notifications + } else { + // already not fulfilled => nothing to do now + } } isFulfilled(): boolean { @@ -223,7 +248,7 @@ export class WaitingForIdentifiableAndCompletedTypeReferences = (waiter: WaitingForInvalidTypeReferences) => void; -export class WaitingForInvalidTypeReferences { +export class WaitingForInvalidTypeReferences implements TypeReferenceListener { protected counterInvalid: number; // just count the number of invalid TypeReferences // At least one of the given TypeReferences must be in the state Invalid. @@ -238,13 +263,15 @@ export class WaitingForInvalidTypeReferences { // remember the relevant TypeReferences this.waitForRefsInvalid = waitForRefsToBeInvalid; - this.counterInvalid = this.waitForRefsInvalid.filter(ref => ref.isInState('Invalid')).length; + this.counterInvalid = this.waitForRefsInvalid.filter(ref => ref.getType() === undefined || ref.getType()!.isInState('Invalid')).length; // register to get updates for the relevant TypeReferences - this.waitForRefsInvalid.forEach(ref => { - ref.addReactionOnTypeIdentified(() => this.listeningForNextState(), false); - ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false); - }); + this.waitForRefsInvalid.forEach(ref => ref.addListener(this, false)); + } + + deconstruct(): void { + this.listeners.splice(0, this.listeners.length); + this.waitForRefsInvalid.forEach(ref => ref.removeListener(this)); } addListener(newListener: WaitingForInvalidTypeReferencesListener, informIfAlreadyFulfilled: boolean): void { @@ -262,11 +289,11 @@ export class WaitingForInvalidTypeReferences { } } - protected listeningForNextState(): void { + onTypeReferenceResolved(_reference: TypeReference, _resolvedType: Type): void { this.counterInvalid--; } - protected listeningForReset(): void { + onTypeReferenceInvalidated(_reference: TypeReference, _previousType: Type | undefined): void { this.counterInvalid++; if (this.isFulfilled()) { this.listeners.slice().forEach(listener => listener(this)); @@ -276,6 +303,10 @@ export class WaitingForInvalidTypeReferences { isFulfilled(): boolean { return this.counterInvalid === this.waitForRefsInvalid.length && this.waitForRefsInvalid.length >= 1; } + + getWaitForRefsInvalid(): Array> { + return this.waitForRefsInvalid; + } } @@ -283,7 +314,10 @@ export class WaitingForInvalidTypeReferences { /** * A listener for TypeReferences, who will be informed about the found/identified/resolved/unresolved type of the current TypeReference. */ -export type TypeReferenceListener = (reference: TypeReference, resolvedType: T) => void; +export interface TypeReferenceListener { + onTypeReferenceResolved(reference: TypeReference, resolvedType: T): void; + onTypeReferenceInvalidated(reference: TypeReference, previousType: T | undefined): void; +} /** * A TypeReference accepts a specification for a type and searches for the corresponding type in the type system according to this specification. @@ -296,26 +330,13 @@ export type TypeReferenceListener = (reference: TypeRefer * * Once the type is resolved, listeners are notified about this and all following changes of its state. */ -export class TypeReference implements TypeGraphListener, TypeStateListener, TypeInferenceCollectorListener { +export class TypeReference implements TypeGraphListener, TypeInferenceCollectorListener { protected readonly selector: TypeSelector; protected readonly services: TypirServices; protected resolvedType: T | undefined = undefined; - /* TODO Review zur Architektur (also zu "implements TypeStateListener"): - Alternativ könnte der Initialization-State komplett aus TypeReference rausgenommen werden und nur resolved/unresolved betrachtet werden? - Das würde die TypeReference vereinfachen und würde nicht zwei Sachen unnötigerweise vermengen. - Andererseits würde die Logik/Komplexität vielleicht auch nur woanders hinverschoben. - */ - - /** These listeners will be informed, whenever the type of this TypeReference was unresolved/unknown and it is known/resolved now. - * If the resolved type is already 'Completed', these listeners will be informed as well. */ - protected readonly reactOnIdentified: Array> = []; - /** These listeners will be informed, whenever the state type of this TypeReference is 'Completed' now, - * since it was unresolved or identifiable before. */ - protected readonly reactOnCompleted: Array> = []; - /** These listeners will be informed, whenever the state type of this TypeReference is invalid or unresolved now, - * since it was identifiable or completed before. */ - protected readonly reactOnUnresolved: Array> = []; + /** These listeners will be informed, whenever the resolved state of this TypeReference switched from undefined to an actual type or from an actual type to undefined. */ + protected readonly listeners: Array> = []; // TODO introduce TypeReference factory service in order to replace the implementation? constructor(selector: TypeSelector, services: TypirServices) { @@ -325,6 +346,11 @@ export class TypeReference implements TypeGraphListener, this.startResolving(); } + deconstruct() { + this.stopResolving(); + this.listeners.splice(0, this.listeners.length); + } + protected startResolving(): void { // discard the previously resolved type (if any) this.resolvedType = undefined; @@ -345,32 +371,6 @@ export class TypeReference implements TypeGraphListener, this.services.inference.removeListener(this); } - getState(): TypeInitializationState | undefined { - if (this.resolvedType) { - return this.resolvedType.getInitializationState(); - } else { - return undefined; - } - } - - isInState(state: TypeInitializationState): boolean { - if (state === 'Invalid' && this.resolvedType === undefined) { - return true; - } - return this.resolvedType !== undefined && this.resolvedType.isInState(state); // resolved type is in the given state - } - isNotInState(state: TypeInitializationState): boolean { - return !this.isInState(state); - } - isInStateOrLater(state: TypeInitializationState): boolean { - switch (state) { - case 'Invalid': - return true; - default: - return this.resolvedType !== undefined && this.resolvedType.isInStateOrLater(state); - } - } - getType(): T | undefined { return this.resolvedType; } @@ -394,15 +394,7 @@ export class TypeReference implements TypeGraphListener, this.resolvedType = resolvedType; this.stopResolving(); // notify observers - if (this.isInStateOrLater('Identifiable')) { - this.reactOnIdentified.slice().forEach(listener => listener(this, resolvedType)); - } - if (this.isInStateOrLater('Completed')) { - this.reactOnCompleted.slice().forEach(listener => listener(this, resolvedType)); - } else { - // register to get updates for the resolved type in order to notify the observers of this TypeReference about the missing "identifiable" and "completed" cases above - resolvedType.addListener(this, false); - } + this.listeners.slice().forEach(listener => listener.onTypeReferenceResolved(this, resolvedType)); return 'SUCCESSFULLY_RESOLVED'; } else { // the type is not resolved (yet) @@ -440,41 +432,21 @@ export class TypeReference implements TypeGraphListener, } } - addReactionOnTypeIdentified(listener: TypeReferenceListener, informIfAlreadyIdentified: boolean): void { - this.reactOnIdentified.push(listener); - if (informIfAlreadyIdentified && this.isInStateOrLater('Identifiable')) { - listener(this, this.resolvedType!); - } - } - addReactionOnTypeCompleted(listener: TypeReferenceListener, informIfAlreadyCompleted: boolean): void { - this.reactOnCompleted.push(listener); - if (informIfAlreadyCompleted && this.isInStateOrLater('Completed')) { - listener(this, this.resolvedType!); - } - } - addReactionOnTypeUnresolved(listener: TypeReferenceListener, informIfInvalid: boolean): void { - this.reactOnUnresolved.push(listener); - if (informIfInvalid && this.isInState('Invalid')) { - listener(this, this.resolvedType!); + addListener(listener: TypeReferenceListener, informAboutCurrentState: boolean): void { + this.listeners.push(listener); + if (informAboutCurrentState) { + if (this.resolvedType) { + listener.onTypeReferenceResolved(this, this.resolvedType); + } else { + listener.onTypeReferenceInvalidated(this, undefined!); // hack, maybe remove this parameter? + } } } - removeReactionOnTypeIdentified(listener: TypeReferenceListener): void { - const index = this.reactOnIdentified.indexOf(listener); - if (index >= 0) { - this.reactOnIdentified.splice(index, 1); - } - } - removeReactionOnTypeCompleted(listener: TypeReferenceListener): void { - const index = this.reactOnCompleted.indexOf(listener); - if (index >= 0) { - this.reactOnCompleted.splice(index, 1); - } - } - removeReactionOnTypeUnresolved(listener: TypeReferenceListener): void { - const index = this.reactOnUnresolved.indexOf(listener); + removeListener(listener: TypeReferenceListener): void { + const index = this.listeners.indexOf(listener); if (index >= 0) { - this.reactOnUnresolved.splice(index, 1); + this.listeners.splice(index, 1); } } @@ -488,7 +460,7 @@ export class TypeReference implements TypeGraphListener, // the resolved type of this TypeReference is removed! if (removedType === this.resolvedType) { // notify observers, that the type reference is broken - this.reactOnUnresolved.slice().forEach(listener => listener(this, this.resolvedType!)); + this.listeners.slice().forEach(listener => listener.onTypeReferenceInvalidated(this, this.resolvedType!)); // start resolving the type again this.startResolving(); } @@ -508,29 +480,6 @@ export class TypeReference implements TypeGraphListener, removedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void { // empty, since removed inference rules don't help to resolve a type } - - switchedToIdentifiable(type: Type): void { - if (type === this.resolvedType) { - // the type was already resolved, but some observers of this TypeReference still need to be informed - this.reactOnIdentified.slice().forEach(listener => listener(this, this.resolvedType!)); // slice() prevents issues with removal of listeners during notifications - } else { - throw new Error('This case should not happen, since this TypeReference is registered only for the resolved type.'); - } - } - - switchedToCompleted(type: Type): void { - if (type === this.resolvedType) { - // the type was already resolved, but some observers of this TypeReference still need to be informed - this.reactOnCompleted.slice().forEach(listener => listener(this, this.resolvedType!)); // slice() prevents issues with removal of listeners during notifications - type.removeListener(this); // all listeners are informed now, therefore no more notifications are needed - } else { - throw new Error('This case should not happen, since this TypeReference is registered only for the resolved type.'); - } - } - - switchedToInvalid(type: Type): void { - type.removeListener(this); - } } From 072de086a1e36889dd3f29cff373552503679384 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 20 Nov 2024 20:11:07 +0100 Subject: [PATCH 11/19] more test cases for cyclic classes --- examples/lox/test/lox-type-checking.test.ts | 97 +++++++++++++++++++-- packages/typir/src/utils/test-utils.ts | 12 ++- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index d682fa8..7dd38a1 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -313,7 +313,7 @@ describe('Explicitly test type checking for LOX', () => { }); describe('Cyclic type definitions where a Class is declared and already used', () => { - test('Class with field', async () => { + test('Class with field of its own type', async () => { await validate(` class Node { children: Node @@ -334,7 +334,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isClassType, 'A', 'B'); }); - test('Three Classes with fields with the other Class as type', async () => { + test('Three Classes with fields with one of the other Classes as type', async () => { await validate(` class A { prop1: B @@ -349,14 +349,49 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isClassType, 'A', 'B', 'C'); }); - test.todo('Class with method', async () => { + test('Three Classes with fields with two of the other Classes as type', async () => { + await validate(` + class A { + prop1: B + prop2: C + } + class B { + prop3: C + prop4: A + } + class C { + prop5: A + prop6: B + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B', 'C'); + }); + + test('Class with field of its own type and another dependency', async () => { await validate(` class Node { - myMethod(input: number): Node {} + children: Node + other: Another + } + class Another { + children: Node } `, []); - expectTypirTypes(loxServices, isClassType, 'Node'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod'); + expectTypirTypes(loxServices, isClassType, 'Node', 'Another'); + }); + + test('Two Classes with a field of its own type and cyclic dependencies to each other', async () => { + await validate(` + class Node { + own: Node + other: Another + } + class Another { + own: Another + another: Node + } + `, []); + expectTypirTypes(loxServices, isClassType, 'Node', 'Another'); }); test('Having two declarations for the delayed class A, but only one type A in the type system', async () => { @@ -376,6 +411,56 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isClassType, 'A', 'B'); }); + test('Having three declarations for the delayed class A, but only one type A in the type system', async () => { + await validate(` + class A { + property1: B // needs to wait for B, since B is defined below + } + class A { + property2: B // needs to wait for B, since B is defined below + } + class A { + property3: B // needs to wait for B, since B is defined below + } + class B { } + `, [ // Typir works with this, but for LOX these validation errors are produced: + 'Declared classes need to be unique (A).', + 'Declared classes need to be unique (A).', + 'Declared classes need to be unique (A).', + ]); + // check, that there is only one class type A in the type graph: + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + }); + + test('Having two declarations for class A waiting for B, while B itself depends on A', async () => { + await validate(` + class A { + property1: B // needs to wait for B, since B is defined below + } + class A { + property2: B // needs to wait for B, since B is defined below + } + class B { + property3: A // should be the valid A and not the invalid A + } + `, [ // Typir works with this, but for LOX these validation errors are produced: + 'Declared classes need to be unique (A).', + 'Declared classes need to be unique (A).', + ]); + // check, that there is only one class type A in the type graph: + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + }); + + test.todo('Class with method', async () => { + await validate(` + class Node { + myMethod(input: number): Node {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'Node'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod'); + }); + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index b4fcc00..23f29a5 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -8,10 +8,16 @@ import { expect } from 'vitest'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; -export function expectTypirTypes(services: TypirServices, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): void { - const typeNames = services.graph.getAllRegisteredTypes().filter(filterTypes).map(t => t.getName()); +export function expectTypirTypes(services: TypirServices, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): Type[] { + const types = services.graph.getAllRegisteredTypes().filter(filterTypes); + types.forEach(type => expect(type.getInitializationState()).toBe('Completed')); + const typeNames = types.map(t => t.getName()); expect(typeNames, typeNames.join(', ')).toHaveLength(namesOfExpectedTypes.length); for (const name of namesOfExpectedTypes) { - expect(typeNames).includes(name); + const index = typeNames.indexOf(name); + expect(index >= 0).toBeTruthy(); + typeNames.splice(index, 1); // removing elements is needed to probably support duplicated entries } + expect(typeNames).toHaveLength(0); + return types; } From 704d81c6ddbf31783613bf09976391d7ef80844a Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Thu, 21 Nov 2024 15:13:16 +0100 Subject: [PATCH 12/19] small improvements for the lifecycle of types, renamings --- packages/typir/src/graph/type-graph.ts | 13 ++++---- packages/typir/src/graph/type-node.ts | 21 ++++++++++--- packages/typir/src/utils/utils-definitions.ts | 31 ++++++++----------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index 1eff152..a301834 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -50,18 +50,19 @@ export class TypeGraph { * Design decision: * This is the central API call to remove a type from the type system in case that it is no longer valid/existing/needed. * It is not required to directly inform the kind of the removed type yourself, since the kind itself will take care of removed types. - * @param type the type to remove + * @param typeToRemove the type to remove * @param key an optional key to register the type, since it is allowed to register the same type with different keys in the graph */ - removeNode(type: Type, key?: string): void { - const mapKey = key ?? type.getIdentifier(); + removeNode(typeToRemove: Type, key?: string): void { + const mapKey = key ?? typeToRemove.getIdentifier(); // remove all edges which are connected to the type to remove - type.getAllIncomingEdges().forEach(e => this.removeEdge(e)); - type.getAllOutgoingEdges().forEach(e => this.removeEdge(e)); + typeToRemove.getAllIncomingEdges().forEach(e => this.removeEdge(e)); + typeToRemove.getAllOutgoingEdges().forEach(e => this.removeEdge(e)); // remove the type itself const contained = this.nodes.delete(mapKey); if (contained) { - this.listeners.forEach(listener => listener.removedType(type, mapKey)); + this.listeners.slice().forEach(listener => listener.removedType(typeToRemove, mapKey)); + typeToRemove.deconstruct(); } else { throw new Error(`Type does not exist: ${mapKey}`); } diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 23f4f3d..e2840bb 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -9,7 +9,17 @@ import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingFo import { assertTrue, assertUnreachable } from '../utils/utils.js'; import { TypeEdge } from './type-edge.js'; -// export type TypeInitializationState = 'Created' | 'Identifiable' | 'Completed'; +/** + * The transitions between the states of a type are depicted as state machine: + * ```mermaid +stateDiagram-v2 + [*] --> Invalid + Invalid --> Identifiable + Identifiable --> Completed + Completed --> Invalid + Identifiable --> Invalid +``` + */ export type TypeInitializationState = 'Invalid' | 'Identifiable' | 'Completed'; export interface PreconditionsForInitializationState { @@ -20,7 +30,7 @@ export interface PreconditionsForInitializationState { /** * Design decisions: * - features of types are realized/determined by their kinds - * - Names of types must be unique! + * - Identifiers of types must be unique! */ export abstract class Type { readonly kind: Kind; // => $kind: string, required for isXType() checks @@ -194,14 +204,14 @@ export abstract class Type { this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForInitialization?.refsToBeIdentified, preconditions.preconditionsForInitialization?.refsToBeCompleted, - this, ); + this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); // preconditions for Completed this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForCompletion?.refsToBeIdentified, preconditions.preconditionsForCompletion?.refsToBeCompleted, - this, ); + this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); // preconditions for Invalid this.waitForInvalid = new WaitingForInvalidTypeReferences( preconditions.referencesRelevantForInvalidation ?? [], @@ -280,6 +290,9 @@ export abstract class Type { this.onInvalidation(); this.initializationState = 'Invalid'; this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications + // add the types again, since the initialization process started again + this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); + this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); } else { // is already 'Invalid' => do nothing } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 1a47f9d..a1d3ce8 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -50,7 +50,7 @@ export type TypeSelector = export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); -export interface WaitingForResolvedTypeReferencesListener { +export interface WaitingForIdentifiableAndCompletedTypeReferencesListener { onFulfilled(waiter: WaitingForIdentifiableAndCompletedTypeReferences): void; onInvalidated(waiter: WaitingForIdentifiableAndCompletedTypeReferences): void; } @@ -70,7 +70,7 @@ export class WaitingForIdentifiableAndCompletedTypeReferences = new Set(); + protected typesToIgnoreForCycles: Set = new Set(); /** These TypeReferences must be in the states Identifiable or Completed, before the listeners are informed */ protected readonly waitForRefsIdentified: Array> | undefined; @@ -80,23 +80,17 @@ export class WaitingForIdentifiableAndCompletedTypeReferences> = []; + protected readonly listeners: Array> = []; constructor( waitForRefsToBeIdentified: Array> | undefined, waitForRefsToBeCompleted: Array> | undefined, - typeCycle: Type | undefined, ) { // remember the relevant TypeReferences to wait for this.waitForRefsIdentified = waitForRefsToBeIdentified; this.waitForRefsCompleted = waitForRefsToBeCompleted; - // set-up the set of types to not wait for them, in order to handle cycles - if (typeCycle) { - this.typesForCycles.add(typeCycle); - } - // register to get updates for the relevant TypeReferences toArray(this.waitForRefsIdentified).forEach(ref => ref.addListener(this, false)); toArray(this.waitForRefsCompleted).forEach(ref => ref.addListener(this, false)); @@ -109,10 +103,10 @@ export class WaitingForIdentifiableAndCompletedTypeReferences ref.removeListener(this)); this.waitForRefsCompleted?.forEach(ref => ref.removeListener(this)); - this.typesForCycles.clear(); + this.typesToIgnoreForCycles.clear(); } - addListener(newListener: WaitingForResolvedTypeReferencesListener, informAboutCurrentState: boolean): void { + addListener(newListener: WaitingForIdentifiableAndCompletedTypeReferencesListener, informAboutCurrentState: boolean): void { this.listeners.push(newListener); // inform the new listener if (informAboutCurrentState) { @@ -124,7 +118,7 @@ export class WaitingForIdentifiableAndCompletedTypeReferences): void { + removeListener(listenerToRemove: WaitingForIdentifiableAndCompletedTypeReferencesListener): void { const index = this.listeners.indexOf(listenerToRemove); if (index >= 0) { this.listeners.splice(index, 1); @@ -135,11 +129,11 @@ export class WaitingForIdentifiableAndCompletedTypeReferences = new Set(); for (const typeToIgnore of moreTypesToIgnore) { - if (this.typesForCycles.has(typeToIgnore)) { - // ignore this additional type, required to break the propagation, since the propagation becomes cyclic as well in case of cyclic types! + if (this.typesToIgnoreForCycles.has(typeToIgnore)) { + // ignore this additional type, required to break the propagation, since the propagation itself becomes cyclic as well in case of cyclic types! } else { newTypesToIgnore.add(typeToIgnore); - this.typesForCycles.add(typeToIgnore); + this.typesToIgnoreForCycles.add(typeToIgnore); } } @@ -173,7 +167,7 @@ export class WaitingForIdentifiableAndCompletedTypeReferences, resolvedType: Type): void { // inform the referenced type about the types to ignore for completion - resolvedType.ignoreDependingTypesDuringInitialization(this.typesForCycles); + resolvedType.ignoreDependingTypesDuringInitialization(this.typesToIgnoreForCycles); resolvedType.addListener(this, false); // check, whether all TypeReferences are resolved and the resolved types are in the expected state this.checkIfFulfilled(); @@ -211,7 +205,7 @@ export class WaitingForIdentifiableAndCompletedTypeReferences inform all listeners this.fulfilled = true; // don't inform the listeners again this.listeners.slice().forEach(listener => listener.onFulfilled(this)); // slice() prevents issues with removal of listeners during notifications + this.typesToIgnoreForCycles.clear(); // otherwise deleted types remain in this Set forever } protected switchToNotFulfilled(): void { From a07614b616a480a42ffb34419bc1252c1d0e1e74 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 22 Nov 2024 00:10:20 +0100 Subject: [PATCH 13/19] realized cyclic types with Functions involved, slightly reworked the TypeInitializer --- .../language/type-system/lox-type-checking.ts | 6 +- examples/lox/test/lox-type-checking.test.ts | 52 +- examples/ox/src/language/ox-type-checking.ts | 6 +- packages/typir/src/features/operator.ts | 27 +- packages/typir/src/kinds/class-kind.ts | 21 +- packages/typir/src/kinds/function-kind.ts | 477 +++++++++++------- packages/typir/src/kinds/kind.ts | 8 + packages/typir/src/utils/test-utils.ts | 4 +- .../typir/src/utils/type-initialization.ts | 7 +- packages/typir/src/utils/utils-definitions.ts | 12 +- packages/typir/test/type-definitions.test.ts | 2 +- 11 files changed, 399 insertions(+), 223 deletions(-) diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 023f48f..3f463b6 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -6,7 +6,7 @@ import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; import { LangiumSharedServices } from 'langium/lsp'; -import { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, ParameterDetails, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, createNoSuperClassCyclesValidation } from 'typir'; +import { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, CreateParameterDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, createNoSuperClassCyclesValidation } from 'typir'; import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { ValidationMessageDetails } from '../../../../../packages/typir/lib/features/validation.js'; import { BinaryExpression, FunctionDeclaration, MemberCall, MethodMember, TypeReference, UnaryExpression, isBinaryExpression, isBooleanLiteral, isClass, isClassMember, isFieldMember, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from '../generated/ast.js'; @@ -206,7 +206,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // function types: they have to be updated after each change of the Langium document, since they are derived from FunctionDeclarations! if (isFunctionDeclaration(node)) { - this.functionKind.getOrCreateFunctionType(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar + this.functionKind.createFunctionType(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar } // TODO support lambda (type references)! @@ -259,7 +259,7 @@ function createFunctionDetails(node: FunctionDeclaration | MethodMember): Create return { functionName: callableName, outputParameter: { name: NO_PARAMETER_NAME, type: node.returnType }, - inputParameters: node.parameters.map(p => ({ name: p.name, type: p.type })), + inputParameters: node.parameters.map(p => ({ name: p.name, type: p.type })), // inference rule for function declaration: inferenceRuleForDeclaration: (domainElement: unknown) => domainElement === node, // only the current function/method declaration matches! /** inference rule for funtion/method calls: diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 7dd38a1..346f768 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -16,12 +16,13 @@ import { createLoxServices } from '../src/language/lox-module.js'; import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; const loxServices = createLoxServices(EmptyFileSystem).Lox; +const operatorNames = ['-', '*', '/', '+', '+', '+', '+', '<', '<=', '>', '>=', 'and', 'or', '==', '!=', '=', '!', '-']; afterEach(async () => { await deleteAllDocuments(loxServices); // check, that there are no user-defined classes and functions after clearing/invalidating all LOX documents expectTypirTypes(loxServices, isClassType); - expectTypirTypes(loxServices, isFunctionType, '-', '*', '/', '+', '+', '+', '+', '<', '<=', '>', '>=', 'and', 'or', '==', '!=', '=', '!', '-'); + expectTypirTypes(loxServices, isFunctionType, ...operatorNames); }); describe('Explicitly test type checking for LOX', () => { @@ -451,14 +452,59 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isClassType, 'A', 'B'); }); - test.todo('Class with method', async () => { + test('Class with method', async () => { await validate(` class Node { myMethod(input: number): Node {} } `, []); expectTypirTypes(loxServices, isClassType, 'Node'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + + test('Two different Classes with the same method (type) should result in only one method type', async () => { + await validate(` + class A { + prop1: boolean + myMethod(input: number): boolean {} + } + class B { + prop1: number + myMethod(input: number): boolean {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + + test('Two different Classes depend on each other regarding their methods return type', async () => { + await validate(` + class A { + prop1: boolean + myMethod(input: number): B {} + } + class B { + prop1: number + myMethod(input: number): A {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', 'myMethod', ...operatorNames); + }); + + test('Two different Classes with the same method which has on of these classes as return type', async () => { + await validate(` + class A { + prop1: boolean + myMethod(input: number): B {} + } + class B { + prop1: number + myMethod(input: number): B {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); }); }); diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 72b3cd9..b3fd926 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -6,7 +6,7 @@ import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; import { LangiumSharedServices } from 'langium/lsp'; -import { FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, ParameterDetails, PrimitiveKind, TypirServices, UniqueFunctionValidation } from 'typir'; +import { CreateParameterDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, PrimitiveKind, TypirServices, UniqueFunctionValidation } from 'typir'; import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { ValidationMessageDetails } from '../../../../packages/typir/lib/features/validation.js'; import { BinaryExpression, MemberCall, UnaryExpression, isAssignmentStatement, isBinaryExpression, isBooleanLiteral, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isNumberLiteral, isParameter, isReturnStatement, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from './generated/ast.js'; @@ -171,11 +171,11 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { if (isFunctionDeclaration(domainElement)) { const functionName = domainElement.name; // define function type - this.functionKind.getOrCreateFunctionType({ + this.functionKind.createFunctionType({ functionName, // note that the following two lines internally use type inference here in order to map language types to Typir types outputParameter: { name: NO_PARAMETER_NAME, type: domainElement.returnType }, - inputParameters: domainElement.parameters.map(p => ({ name: p.name, type: p.type })), + inputParameters: domainElement.parameters.map(p => ({ name: p.name, type: p.type })), // inference rule for function declaration: inferenceRuleForDeclaration: (node: unknown) => node === domainElement, // only the current function declaration matches! /** inference rule for funtion calls: diff --git a/packages/typir/src/features/operator.ts b/packages/typir/src/features/operator.ts index a900eb3..692e139 100644 --- a/packages/typir/src/features/operator.ts +++ b/packages/typir/src/features/operator.ts @@ -7,7 +7,8 @@ import { Type } from '../graph/type-node.js'; import { FunctionKind, FunctionKindName, isFunctionKind, NO_PARAMETER_NAME } from '../kinds/function-kind.js'; import { TypirServices } from '../typir.js'; -import { NameTypePair, Types } from '../utils/utils-definitions.js'; +import { TypeInitializer } from '../utils/type-initialization.js'; +import { NameTypePair, TypeInitializers } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; // export type InferOperatorWithSingleOperand = (domainElement: unknown, operatorName: string) => boolean | unknown; @@ -65,12 +66,12 @@ export interface GenericOperatorDetails { // TODO rename it to "OperatorFactory", when there are no more responsibilities! export interface OperatorManager { - createUnaryOperator(typeDetails: UnaryOperatorDetails): Types - createBinaryOperator(typeDetails: BinaryOperatorDetails): Types - createTernaryOperator(typeDetails: TernaryOperatorDetails): Types + createUnaryOperator(typeDetails: UnaryOperatorDetails): TypeInitializers + createBinaryOperator(typeDetails: BinaryOperatorDetails): TypeInitializers + createTernaryOperator(typeDetails: TernaryOperatorDetails): TypeInitializers /** This function allows to create a single operator with arbitrary input operands. */ - createGenericOperator(typeDetails: GenericOperatorDetails): Type; + createGenericOperator(typeDetails: GenericOperatorDetails): TypeInitializer; } /** @@ -98,9 +99,9 @@ export class DefaultOperatorManager implements OperatorManager { this.services = services; } - createUnaryOperator(typeDetails: UnaryOperatorDetails): Types { + createUnaryOperator(typeDetails: UnaryOperatorDetails): TypeInitializers { const signatures = toArray(typeDetails.signature); - const result: Type[] = []; + const result: Array> = []; for (const signature of signatures) { result.push(this.createGenericOperator({ name: typeDetails.name, @@ -114,9 +115,9 @@ export class DefaultOperatorManager implements OperatorManager { return result.length === 1 ? result[0] : result; } - createBinaryOperator(typeDetails: BinaryOperatorDetails): Types { + createBinaryOperator(typeDetails: BinaryOperatorDetails): TypeInitializers { const signatures = toArray(typeDetails.signature); - const result: Type[] = []; + const result: Array> = []; for (const signature of signatures) { result.push(this.createGenericOperator({ name: typeDetails.name, @@ -131,9 +132,9 @@ export class DefaultOperatorManager implements OperatorManager { return result.length === 1 ? result[0] : result; } - createTernaryOperator(typeDetails: TernaryOperatorDetails): Types { + createTernaryOperator(typeDetails: TernaryOperatorDetails): TypeInitializers { const signatures = toArray(typeDetails.signature); - const result: Type[] = []; + const result: Array> = []; for (const signature of signatures) { result.push(this.createGenericOperator({ name: typeDetails.name, @@ -149,7 +150,7 @@ export class DefaultOperatorManager implements OperatorManager { return result.length === 1 ? result[0] : result; } - createGenericOperator(typeDetails: GenericOperatorDetails): Type { + createGenericOperator(typeDetails: GenericOperatorDetails): TypeInitializer { // define/register the wanted operator as "special" function const functionKind = this.getFunctionKind(); @@ -170,7 +171,7 @@ export class DefaultOperatorManager implements OperatorManager { : undefined }); - return newOperatorType; + return newOperatorType as unknown as TypeInitializer; } protected getFunctionKind(): FunctionKind { diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 0e2c416..2e8fecd 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -499,9 +499,8 @@ export class ClassKind implements Kind { // methods const functionKind = this.getMethodKind(); const methods: string = typeDetails.methods - .map(method => { - functionKind.getOrCreateFunctionType(method); // ensure, that the corresponding Type is existing in the type system - return functionKind.calculateIdentifier(method); // reuse the Identifier for Functions here! + .map(createMethodDetails => { + return functionKind.calculateIdentifier(createMethodDetails); // reuse the Identifier for Functions here! }) .sort() // the order of methods does not matter, therefore we need a stable order to make the identifiers comparable .join(','); @@ -552,6 +551,7 @@ export class ClassTypeInitializer exten protected readonly typeDetails: CreateClassTypeDetails; protected readonly kind: ClassKind; protected inferenceRules: TypeInferenceRule[]; + protected initialClassType: ClassType; constructor(services: TypirServices, kind: ClassKind, typeDetails: CreateClassTypeDetails) { super(services); @@ -559,17 +559,17 @@ export class ClassTypeInitializer exten this.kind = kind; // create the class type - const classType = new ClassType(kind, typeDetails as CreateClassTypeDetails); + this.initialClassType = new ClassType(kind, typeDetails as CreateClassTypeDetails); if (kind.options.typing === 'Structural') { // register structural classes also by their names, since these names are usually used for reference in the DSL/AST! - this.services.graph.addNode(classType, kind.getIdentifierPrefix() + typeDetails.className); + this.services.graph.addNode(this.initialClassType, kind.getIdentifierPrefix() + typeDetails.className); } - this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, classType); + this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, this.initialClassType); // register all the inference rules already now to enable early type inference for this Class type this.inferenceRules.forEach(rule => services.inference.addInferenceRule(rule, undefined)); // 'undefined', since the Identifier is still missing - classType.addListener(this, true); // trigger directly, if some initialization states are already reached! + this.initialClassType.addListener(this, true); // trigger directly, if some initialization states are already reached! } switchedToIdentifiable(classType: Type): void { @@ -578,7 +578,8 @@ export class ClassTypeInitializer exten * - By waiting untile the new class has its identifier, 'producedType(...)' is able to check, whether this class type is already existing! * - Accordingly, 'classType' and 'readyClassType' might have different values! */ - const readyClassType = this.producedType(classType as ClassType); + assertType(classType, isClassType); + const readyClassType = this.producedType(classType); // remove/invalidate the duplicated and skipped class type now if (readyClassType !== classType) { @@ -623,6 +624,10 @@ export class ClassTypeInitializer exten switchedToInvalid(_previousClassType: Type): void { // do nothing } + + override getTypeInitial(): ClassType { + return this.initialClassType; + } } diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index 9bbcf35..02a49dd 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -5,32 +5,33 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../features/equality.js'; -import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable } from '../features/inference.js'; +import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../features/inference.js'; import { SubTypeProblem } from '../features/subtype.js'; import { ValidationProblem, ValidationRuleWithBeforeAfter } from '../features/validation.js'; import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; -import { Type, isType } from '../graph/type-node.js'; +import { Type, TypeStateListener, isType } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; -import { NameTypePair, TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; +import { TypeInitializer } from '../utils/type-initialization.js'; +import { NameTypePair, TypeReference, TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; import { TypeCheckStrategy, checkTypeArrays, checkTypes, checkValueForConflict, createKindConflict, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; -import { assertTrue } from '../utils/utils.js'; +import { assertTrue, assertType, assertUnreachable } from '../utils/utils.js'; import { Kind, isKind } from './kind.js'; export class FunctionType extends Type { override readonly kind: FunctionKind; readonly functionName: string; - readonly outputParameter: NameTypePair | undefined; - readonly inputParameters: NameTypePair[]; + readonly outputParameter: ParameterDetails | undefined; + readonly inputParameters: ParameterDetails[]; - constructor(kind: FunctionKind, identifier: string, typeDetails: FunctionTypeDetails) { - super(identifier); + constructor(kind: FunctionKind, typeDetails: FunctionTypeDetails) { + super(undefined); this.kind = kind; this.functionName = typeDetails.functionName; // output parameter - const outputType = typeDetails.outputParameter ? resolveTypeSelector(this.kind.services, typeDetails.outputParameter.type) : undefined; + const outputType = typeDetails.outputParameter ? new TypeReference(typeDetails.outputParameter.type, this.kind.services) : undefined; if (typeDetails.outputParameter) { assertTrue(outputType !== undefined); this.kind.enforceParameterName(typeDetails.outputParameter.name, this.kind.options.enforceOutputParameterName); @@ -46,13 +47,34 @@ export class FunctionType extends Type { // input parameters this.inputParameters = typeDetails.inputParameters.map(input => { this.kind.enforceParameterName(input.name, this.kind.options.enforceInputParameterNames); - return { + return { name: input.name, - type: resolveTypeSelector(this.kind.services, input.type), + type: new TypeReference(input.type, this.kind.services), }; }); - this.defineTheInitializationProcessOfThisType({}); // TODO preconditions + // define to wait for the parameter types + const allParameterRefs = this.inputParameters.map(p => p.type); + if (outputType) { + allParameterRefs.push(outputType); + } + this.defineTheInitializationProcessOfThisType({ + preconditionsForInitialization: { + refsToBeIdentified: allParameterRefs, + }, + referencesRelevantForInvalidation: allParameterRefs, + onIdentification: () => { + // the identifier is calculated now + this.identifier = this.kind.calculateIdentifier(typeDetails); // TODO it is still not nice, that the type resolving is done again, since the TypeReferences here are not reused + // the registration of the type in the type graph is done by the TypeInitializer + }, + onCompletion: () => { + // some check? + }, + onInvalidation: () => { + // ? + }, + }); } override getName(): string { @@ -143,12 +165,41 @@ export class FunctionType extends Type { return this.functionName; } - getOutput(): NameTypePair | undefined { - return this.outputParameter; + getOutput(notResolvedBehavior: 'EXCEPTION' | 'RETURN_UNDEFINED' = 'EXCEPTION'): NameTypePair | undefined { + if (this.outputParameter) { + const type = this.outputParameter.type.getType(); + if (type) { + return { + name: this.outputParameter.name, + type, + }; + } else { + switch (notResolvedBehavior) { + case 'EXCEPTION': + throw new Error(`Output parameter ${this.outputParameter.name} is not resolved.`); + case 'RETURN_UNDEFINED': + return undefined; + default: + assertUnreachable(notResolvedBehavior); + } + } + } else { + return undefined; + } } getInputs(): NameTypePair[] { - return this.inputParameters; + return this.inputParameters.map(param => { + const type = param.type.getType(); + if (type) { + return { + name: param.name, + type, + }; + } else { + throw new Error(`Input parameter ${param.name} is not resolved.`); + } + }); } } @@ -156,6 +207,11 @@ export function isFunctionType(type: unknown): type is FunctionType { return isType(type) && isFunctionKind(type.kind); } +export interface ParameterDetails { + name: string; + type: TypeReference; +} + export interface FunctionKindOptions { @@ -173,7 +229,7 @@ export interface FunctionKindOptions { export const FunctionKindName = 'FunctionKind'; -export interface ParameterDetails { +export interface CreateParameterDetails { name: string; type: TypeSelector; } @@ -181,8 +237,8 @@ export interface ParameterDetails { export interface FunctionTypeDetails { functionName: string, /** The order of parameters is important! */ - outputParameter: ParameterDetails | undefined, - inputParameters: ParameterDetails[], + outputParameter: CreateParameterDetails | undefined, + inputParameters: CreateParameterDetails[], } export interface CreateFunctionTypeDetails extends FunctionTypeDetails { /** for function declarations => returns the funtion type (the whole signature including all names) */ @@ -257,7 +313,7 @@ export class FunctionKind implements Kind, TypeGraphListener { /** Limitations * - Works only, if function types are defined using the createFunctionType(...) function below! */ - protected readonly mapNameTypes: Map = new Map(); // function name => all overloaded functions with this name/key + readonly mapNameTypes: Map = new Map(); // function name => all overloaded functions with this name/key // TODO try to replace this map with calculating the required identifier for the function constructor(services: TypirServices, options?: Partial) { @@ -372,172 +428,16 @@ export class FunctionKind implements Kind, TypeGraphListener { ); } - getFunctionType(typeDetails: FunctionTypeDetails): FunctionType | undefined { - const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as FunctionType; - } - - getOrCreateFunctionType(typeDetails: CreateFunctionTypeDetails): FunctionType { - const functionType = this.getFunctionType(typeDetails); - if (functionType) { - // register the additional inference rules for the same type! - this.registerInferenceRules(typeDetails, functionType); - return functionType; - } - return this.createFunctionType(typeDetails); - } - - createFunctionType(typeDetails: CreateFunctionTypeDetails): FunctionType { - const functionName = typeDetails.functionName; - - // check the input - assertTrue(this.getFunctionType(typeDetails) === undefined, `The function '${functionName}' already exists!`); // ensures, that no duplicated functions are created! - if (!typeDetails) { - throw new Error('is undefined'); - } - if (typeDetails.outputParameter === undefined && typeDetails.inferenceRuleForCalls) { - // no output parameter => no inference rule for calling this function - throw new Error(`A function '${functionName}' without output parameter cannot have an inferred type, when this function is called!`); - } - this.enforceFunctionName(functionName, this.options.enforceFunctionName); - - // create the function type - const functionType = new FunctionType(this, this.calculateIdentifier(typeDetails), typeDetails); - this.services.graph.addNode(functionType); - - // output parameter for function calls - const outputTypeForFunctionCalls = this.getOutputTypeForFunctionCalls(functionType); - - // remember the new function for later in order to enable overloaded functions! - let overloaded = this.mapNameTypes.get(functionName); - if (overloaded) { - // do nothing - } else { - overloaded = { - overloadedFunctions: [], - inference: new CompositeTypeInferenceRule(this.services), - sameOutputType: undefined, - }; - this.mapNameTypes.set(functionName, overloaded); - this.services.inference.addInferenceRule(overloaded.inference); - } - if (overloaded.overloadedFunctions.length <= 0) { - // remember the output type of the first function - overloaded.sameOutputType = outputTypeForFunctionCalls; - } else { - if (overloaded.sameOutputType && outputTypeForFunctionCalls && this.services.equality.areTypesEqual(overloaded.sameOutputType, outputTypeForFunctionCalls) === true) { - // the output types of all overloaded functions are the same for now - } else { - // there is a difference - overloaded.sameOutputType = undefined; - } - } - overloaded.overloadedFunctions.push({ - functionType, - inferenceRuleForCalls: typeDetails.inferenceRuleForCalls, - }); - - this.registerInferenceRules(typeDetails, functionType); - - return functionType; + getFunctionType(typeDetails: FunctionTypeDetails): TypeReference { + return new TypeReference(() => this.calculateIdentifier(typeDetails), this.services); } - protected registerInferenceRules(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType): void { - const functionName = typeDetails.functionName; - const mapNameTypes = this.mapNameTypes; - const overloaded = mapNameTypes.get(functionName)!; - const outputTypeForFunctionCalls = this.getOutputTypeForFunctionCalls(functionType); - if (typeDetails.inferenceRuleForCalls) { - /** Preconditions: - * - there is a rule which specifies how to infer the current function type - * - the current function has an output type/parameter, otherwise, this function could not provide any type (and throws an error), when it is called! - * (exception: the options contain a type to return in this special case) - */ - function check(returnType: Type | undefined): Type { - if (returnType) { - return returnType; - } else { - throw new Error(`The function ${functionName} is called, but has no output type to infer.`); - } - } - - // register inference rule for calls of the new function - // TODO what about the case, that multiple variants match?? after implicit conversion for example?! => overload with the lowest number of conversions wins! - overloaded.inference.addInferenceRule({ - inferTypeWithoutChildren(domainElement, _typir) { - const result = typeDetails.inferenceRuleForCalls!.filter(domainElement); - if (result) { - const matching = typeDetails.inferenceRuleForCalls!.matching(domainElement); - if (matching) { - const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(domainElement); - if (inputArguments && inputArguments.length >= 1) { - // this function type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 - const overloadInfos = mapNameTypes.get(functionName); - if (overloadInfos && overloadInfos.overloadedFunctions.length >= 2) { - // (only) for overloaded functions: - if (overloadInfos.sameOutputType) { - // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! - return overloadInfos.sameOutputType; - } else { - // otherwise: the types of the parameters need to be inferred in order to determine an exact match - return inputArguments; - } - } else { - // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors - return check(outputTypeForFunctionCalls); - } - } else { - // there are no operands to check - return check(outputTypeForFunctionCalls); - } - } else { - // the domain element is slightly different - } - } else { - // the domain element has a completely different purpose - } - // does not match at all - return InferenceRuleNotApplicable; - }, - inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { - const inputTypes = typeDetails.inputParameters.map(p => resolveTypeSelector(typir, p.type)); - // all operands need to be assignable(! not equal) to the required types - const comparisonConflicts = checkTypeArrays(childrenTypes, inputTypes, - (t1, t2) => typir.assignability.getAssignabilityProblem(t1, t2), true); - if (comparisonConflicts.length >= 1) { - // this function type does not match, due to assignability conflicts => return them as errors - return { - $problem: InferenceProblem, - domainElement, - inferenceCandidate: functionType, - location: 'input parameters', - rule: this, - subProblems: comparisonConflicts, - }; - // We have a dedicated validation for this case (see below), but a resulting error might be ignored by the user => return the problem during type-inference again - } else { - // matching => return the return type of the function for the case of a function call! - return check(outputTypeForFunctionCalls); - } - }, - }, functionType); - } - - // register inference rule for the declaration of the new function - // (regarding overloaded function, for now, it is assumed, that the given inference rule itself is concrete enough to handle overloaded functions itself!) - if (typeDetails.inferenceRuleForDeclaration) { - this.services.inference.addInferenceRule((domainElement, _typir) => { - if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { - return functionType; - } else { - return InferenceRuleNotApplicable; - } - }, functionType); - } + createFunctionType(typeDetails: CreateFunctionTypeDetails): TypeInitializer { + return new FunctionTypeInitializer(this.services, this, typeDetails); } - protected getOutputTypeForFunctionCalls(functionType: FunctionType): Type | undefined { - return functionType.getOutput()?.type ?? // by default, use the return type of the function ... + getOutputTypeForFunctionCalls(functionType: FunctionType): Type | undefined { + return functionType.getOutput('RETURN_UNDEFINED')?.type ?? // by default, use the return type of the function ... // ... if this type is missing, use the specified type for this case in the options: // 'THROW_ERROR': an error will be thrown later, when this case actually occurs! (this.options.typeToInferForCallsOfFunctionsWithoutOutput === 'THROW_ERROR' @@ -622,6 +522,221 @@ export function isFunctionKind(kind: unknown): kind is FunctionKind { } +export class FunctionTypeInitializer extends TypeInitializer implements TypeStateListener { + protected readonly typeDetails: CreateFunctionTypeDetails; + protected readonly kind: FunctionKind; + protected inferenceRules: FunctionInferenceRules; + protected initialFunctionType: FunctionType; + + constructor(services: TypirServices, kind: FunctionKind, typeDetails: CreateFunctionTypeDetails) { + super(services); + this.typeDetails = typeDetails; + this.kind = kind; + + const functionName = typeDetails.functionName; + + // check the input + if (typeDetails.outputParameter === undefined && typeDetails.inferenceRuleForCalls) { + // no output parameter => no inference rule for calling this function + throw new Error(`A function '${functionName}' without output parameter cannot have an inferred type, when this function is called!`); + } + kind.enforceFunctionName(functionName, kind.options.enforceFunctionName); + + // prepare the overloads + let overloaded = this.kind.mapNameTypes.get(functionName); + if (overloaded) { + // do nothing + } else { + overloaded = { + overloadedFunctions: [], + inference: new CompositeTypeInferenceRule(this.services), + sameOutputType: undefined, + }; + this.kind.mapNameTypes.set(functionName, overloaded); + this.services.inference.addInferenceRule(overloaded.inference); + } + + // create the new Function type + this.initialFunctionType = new FunctionType(kind, typeDetails); + + this.inferenceRules = createInferenceRules(typeDetails, kind, this.initialFunctionType); + registerInferenceRules(this.inferenceRules, kind, functionName, undefined); + + this.initialFunctionType.addListener(this, true); + } + + override getTypeInitial(): FunctionType { + return this.initialFunctionType; + } + + switchedToIdentifiable(functionType: Type): void { + const functionName = this.typeDetails.functionName; + assertType(functionType, isFunctionType); + const readyFunctionType = this.producedType(functionType); + if (readyFunctionType !== functionType) { + functionType.removeListener(this); + deregisterInferenceRules(this.inferenceRules, this.kind, functionName, undefined); + this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, readyFunctionType); + registerInferenceRules(this.inferenceRules, this.kind, functionName, readyFunctionType); + } else { + deregisterInferenceRules(this.inferenceRules, this.kind, functionName, undefined); + registerInferenceRules(this.inferenceRules, this.kind, functionName, readyFunctionType); + } + + // remember the new function for later in order to enable overloaded functions! + // const functionName = typeDetails.functionName; + const outputTypeForFunctionCalls = this.kind.getOutputTypeForFunctionCalls(readyFunctionType); // output parameter for function calls + const overloaded = this.kind.mapNameTypes.get(functionName)!; + if (overloaded.overloadedFunctions.length <= 0) { + // remember the output type of the first function + overloaded.sameOutputType = outputTypeForFunctionCalls; + } else { + if (overloaded.sameOutputType && outputTypeForFunctionCalls && this.services.equality.areTypesEqual(overloaded.sameOutputType, outputTypeForFunctionCalls) === true) { + // the output types of all overloaded functions are the same for now + } else { + // there is a difference + overloaded.sameOutputType = undefined; + } + } + overloaded.overloadedFunctions.push({ + functionType: readyFunctionType, + inferenceRuleForCalls: this.typeDetails.inferenceRuleForCalls, + }); + } + + switchedToCompleted(functionType: Type): void { + functionType.removeListener(this); + } + + switchedToInvalid(_functionType: Type): void { + // empty + } +} + +interface FunctionInferenceRules { + forCall?: TypeInferenceRule; + forDeclaration?: TypeInferenceRule; +} + +function registerInferenceRules(rules: FunctionInferenceRules, functionKind: FunctionKind, functionName: string, functionType: FunctionType | undefined): void { + if (rules.forCall) { + const overloaded = functionKind.mapNameTypes.get(functionName)!; + overloaded.inference.addInferenceRule(rules.forCall, functionType); + } + + if (rules.forDeclaration) { + functionKind.services.inference.addInferenceRule(rules.forDeclaration, functionType); + } +} + +function deregisterInferenceRules(rules: FunctionInferenceRules, functionKind: FunctionKind, functionName: string, functionType: FunctionType | undefined): void { + if (rules.forCall) { + const overloaded = functionKind.mapNameTypes.get(functionName); + overloaded?.inference.removeInferenceRule(rules.forCall, functionType); + } + + if (rules.forDeclaration) { + functionKind.services.inference.removeInferenceRule(rules.forDeclaration, functionType); + } +} + +function createInferenceRules(typeDetails: CreateFunctionTypeDetails, functionKind: FunctionKind, functionType: FunctionType): FunctionInferenceRules { + const result: FunctionInferenceRules = {}; + const functionName = typeDetails.functionName; + const mapNameTypes = functionKind.mapNameTypes; + const outputTypeForFunctionCalls = functionKind.getOutputTypeForFunctionCalls(functionType); + if (typeDetails.inferenceRuleForCalls) { // TODO warum wird hier nicht einfach "outputTypeForFunctionCalls !== undefined" überprüft?? + /** Preconditions: + * - there is a rule which specifies how to infer the current function type + * - the current function has an output type/parameter, otherwise, this function could not provide any type (and throws an error), when it is called! + * (exception: the options contain a type to return in this special case) + */ + function check(returnType: Type | undefined): Type { + if (returnType) { + return returnType; + } else { + throw new Error(`The function ${functionName} is called, but has no output type to infer.`); + } + } + + // register inference rule for calls of the new function + // TODO what about the case, that multiple variants match?? after implicit conversion for example?! => overload with the lowest number of conversions wins! + result.forCall = { + inferTypeWithoutChildren(domainElement, _typir) { + const result = typeDetails.inferenceRuleForCalls!.filter(domainElement); + if (result) { + const matching = typeDetails.inferenceRuleForCalls!.matching(domainElement); + if (matching) { + const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(domainElement); + if (inputArguments && inputArguments.length >= 1) { + // this function type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 + const overloadInfos = mapNameTypes.get(functionName); + if (overloadInfos && overloadInfos.overloadedFunctions.length >= 2) { + // (only) for overloaded functions: + if (overloadInfos.sameOutputType) { + // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! + return overloadInfos.sameOutputType; + } else { + // otherwise: the types of the parameters need to be inferred in order to determine an exact match + return inputArguments; + } + } else { + // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + return check(outputTypeForFunctionCalls); + } + } else { + // there are no operands to check + return check(outputTypeForFunctionCalls); + } + } else { + // the domain element is slightly different + } + } else { + // the domain element has a completely different purpose + } + // does not match at all + return InferenceRuleNotApplicable; + }, + inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { + const inputTypes = typeDetails.inputParameters.map(p => resolveTypeSelector(typir, p.type)); + // all operands need to be assignable(! not equal) to the required types + const comparisonConflicts = checkTypeArrays(childrenTypes, inputTypes, + (t1, t2) => typir.assignability.getAssignabilityProblem(t1, t2), true); + if (comparisonConflicts.length >= 1) { + // this function type does not match, due to assignability conflicts => return them as errors + return { + $problem: InferenceProblem, + domainElement, + inferenceCandidate: functionType, + location: 'input parameters', + rule: this, + subProblems: comparisonConflicts, + }; + // We have a dedicated validation for this case (see below), but a resulting error might be ignored by the user => return the problem during type-inference again + } else { + // matching => return the return type of the function for the case of a function call! + return check(outputTypeForFunctionCalls); + } + }, + }; + } + + // register inference rule for the declaration of the new function + // (regarding overloaded function, for now, it is assumed, that the given inference rule itself is concrete enough to handle overloaded functions itself!) + if (typeDetails.inferenceRuleForDeclaration) { + result.forDeclaration = (domainElement, _typir) => { + if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { + return functionType; + } else { + return InferenceRuleNotApplicable; + } + }; + } + + return result; +} + + /** * Predefined validation to produce errors for those overloaded functions which cannot be distinguished when calling them. */ diff --git a/packages/typir/src/kinds/kind.ts b/packages/typir/src/kinds/kind.ts index da8c16c..00fa097 100644 --- a/packages/typir/src/kinds/kind.ts +++ b/packages/typir/src/kinds/kind.ts @@ -11,6 +11,14 @@ */ export interface Kind { readonly $name: string; + + /* Each kind/type requires to have a method to calculate identifiers for new types: + calculateIdentifier(typeDetails: XTypeDetails): string { ... } + - used as key in the type graph-map + - used to uniquely identify same types! (not overloaded types) + - must be public in order to reuse it by other Kinds + */ + } export function isKind(kind: unknown): kind is Kind { diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index 23f29a5..e168eaf 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -10,13 +10,13 @@ import { TypirServices } from '../typir.js'; export function expectTypirTypes(services: TypirServices, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): Type[] { const types = services.graph.getAllRegisteredTypes().filter(filterTypes); - types.forEach(type => expect(type.getInitializationState()).toBe('Completed')); + types.forEach(type => expect(type.getInitializationState()).toBe('Completed')); // check that all types are 'Completed' const typeNames = types.map(t => t.getName()); expect(typeNames, typeNames.join(', ')).toHaveLength(namesOfExpectedTypes.length); for (const name of namesOfExpectedTypes) { const index = typeNames.indexOf(name); expect(index >= 0).toBeTruthy(); - typeNames.splice(index, 1); // removing elements is needed to probably support duplicated entries + typeNames.splice(index, 1); // removing elements is needed to work correctly with duplicated entries } expect(typeNames).toHaveLength(0); return types; diff --git a/packages/typir/src/utils/type-initialization.ts b/packages/typir/src/utils/type-initialization.ts index 32ca40d..e1715e0 100644 --- a/packages/typir/src/utils/type-initialization.ts +++ b/packages/typir/src/utils/type-initialization.ts @@ -43,7 +43,7 @@ export abstract class TypeInitializer { newType.deconstruct(); } else { this.typeToReturn = newType; - this.services.graph.addNode(newType); + this.services.graph.addNode(this.typeToReturn); } // inform and clear all listeners @@ -54,7 +54,10 @@ export abstract class TypeInitializer { return this.typeToReturn; } - getType(): T | undefined { + // TODO using this type feels wrong, but otherwise, it seems not to work ... + abstract getTypeInitial(): T + + getTypeFinal(): T | undefined { return this.typeToReturn; } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index a1d3ce8..723b0cb 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -27,6 +27,7 @@ export function isSpecificTypirProblem(problem: unknown, $problem: string): prob export type Types = Type | Type[]; export type Names = string | string[]; +export type TypeInitializers = TypeInitializer | Array>; export type NameTypePair = { name: string; @@ -92,11 +93,8 @@ export class WaitingForIdentifiableAndCompletedTypeReferences ref.addListener(this, false)); - toArray(this.waitForRefsCompleted).forEach(ref => ref.addListener(this, false)); - - // everything might already be fulfilled - this.checkIfFulfilled(); + toArray(this.waitForRefsIdentified).forEach(ref => ref.addListener(this, true)); // 'true' calls 'checkIfFulfilled()' to check, whether everything might already be fulfilled + toArray(this.waitForRefsCompleted).forEach(ref => ref.addListener(this, true)); } deconstruct(): void { @@ -411,7 +409,7 @@ export class TypeReference implements TypeGraphListener, } else if (typeof selector === 'string') { return this.services.graph.getType(selector) as T; } else if (selector instanceof TypeInitializer) { - return selector.getType(); + return selector.getTypeInitial(); } else if (selector instanceof TypeReference) { return selector.getType(); } else if (typeof selector === 'function') { @@ -489,7 +487,7 @@ export function resolveTypeSelector(services: TypirServices, selector: TypeSelec throw new Error(`A type with identifier '${selector}' as TypeSelector does not exist in the type graph.`); } } else if (selector instanceof TypeInitializer) { - return selector.getType(); + return selector.getTypeFinal(); } else if (selector instanceof TypeReference) { return selector.getType(); } else if (typeof selector === 'function') { diff --git a/packages/typir/test/type-definitions.test.ts b/packages/typir/test/type-definitions.test.ts index 1fc8b32..c6fb3d5 100644 --- a/packages/typir/test/type-definitions.test.ts +++ b/packages/typir/test/type-definitions.test.ts @@ -44,7 +44,7 @@ describe('Tests for Typir', () => { ], methods: [], }); - console.log(typePerson.getType()!.getUserRepresentation()); + console.log(typePerson.getTypeFinal()!.getUserRepresentation()); const typeStudent = classKind.createClassType({ className: 'Student', superClasses: typePerson, // a Student is a special Person From 14b21d5d7f6628f11da6e7067868270b4c4c175e Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 22 Nov 2024 10:16:45 +0100 Subject: [PATCH 14/19] fix for unique method validation, more test cases --- .../language/type-system/lox-type-checking.ts | 7 +++- examples/lox/test/lox-type-checking.test.ts | 30 ++++++++++++++ packages/typir/src/kinds/class-kind.ts | 41 +++++++++++++++---- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 3f463b6..0ed8871 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -192,11 +192,14 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); // check for unique class declarations - this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueClassValidation(this.typir, isClass)); + const uniqueClassValidator = new UniqueClassValidation(this.typir, isClass); // check for unique method declarations this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueMethodValidation(this.typir, (node) => isMethodMember(node), // MethodMembers could have other $containers? - (method, _type) => method.$container)); + (method, _type) => method.$container, + uniqueClassValidator, + )); + this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(uniqueClassValidator); // TODO this order is important, solve it in a different way! // check for cycles in super-sub-type relationships this.typir.validation.collector.addValidationRule(createNoSuperClassCyclesValidation(isClass)); } diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 346f768..c413ba1 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -507,6 +507,36 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); }); + test('Same delayed function type is used by a function declaration and a method declaration', async () => { + await validate(` + class A { + myMethod(input: number): B {} + } + fun myMethod(input: number): B {} + class B { } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + + test('Two class declarations A with the same delayed method which depends on the class B', async () => { + await validate(` + class A { + myMethod(input: number): B {} + } + class A { + myMethod(input: number): B {} + } + class B { } + `, [ // Typir works with this, but for LOX these validation errors are produced: + 'Declared classes need to be unique (A).', + 'Declared classes need to be unique (A).', + ]); + // check, that there is only one class type A in the type graph: + expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 2e8fecd..5f9e209 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -738,6 +738,7 @@ function createInferenceRuleForLiteral(rule: InferClassLiteral, classKind: */ export class UniqueClassValidation implements ValidationRuleWithBeforeAfter { protected readonly foundDeclarations: Map = new Map(); + protected readonly services: TypirServices; protected readonly isRelevant: (domainElement: unknown) => boolean; // using this check improves performance a lot @@ -798,25 +799,40 @@ export class UniqueClassValidation implements ValidationRuleWithBeforeAfter { this.foundDeclarations.clear(); return result; } + + isClassDuplicated(clas: ClassType): boolean { + const key = this.calculateClassKey(clas); + return this.foundDeclarations.has(key) && this.foundDeclarations.get(key)!.length >= 2; + } +} + +interface UniqueMethodValidationEntry { + domainElement: unknown; + classType: ClassType; } /** * Predefined validation to produce errors, if inside a class the same method is declared more than once. */ export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter { - protected readonly foundDeclarations: Map = new Map(); + protected readonly foundDeclarations: Map = new Map(); + protected readonly services: TypirServices; /** Determines domain elements which represent declared methods, improves performance a lot. */ protected readonly isMethodDeclaration: (domainElement: unknown) => domainElement is T; /** Determines the corresponding domain element of the class declaration, so that Typir can infer its ClassType */ protected readonly getClassOfMethod: (domainElement: T, methodType: FunctionType) => unknown; + protected readonly uniqueClassValidator: UniqueClassValidation | undefined; constructor(services: TypirServices, isMethodDeclaration: (domainElement: unknown) => domainElement is T, - getClassOfMethod: (domainElement: T, methodType: FunctionType) => unknown) { + getClassOfMethod: (domainElement: T, methodType: FunctionType) => unknown, + uniqueClassValidator?: UniqueClassValidation, + ) { this.services = services; this.isMethodDeclaration = isMethodDeclaration; this.getClassOfMethod = getClassOfMethod; + this.uniqueClassValidator = uniqueClassValidator; } beforeValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { @@ -837,7 +853,10 @@ export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter entries = []; this.foundDeclarations.set(key, entries); } - entries.push(domainElement); + entries.push({ + domainElement, + classType, + }); } } } @@ -862,12 +881,16 @@ export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter for (const [key, methods] of this.foundDeclarations.entries()) { if (methods.length >= 2) { for (const method of methods) { - result.push({ - $problem: ValidationProblem, - domainElement: method, - severity: 'error', - message: `Declared methods need to be unique (${key}).`, - }); + if (this.uniqueClassValidator?.isClassDuplicated(method.classType)) { + // ignore duplicated methods inside duplicated classes + } else { + result.push({ + $problem: ValidationProblem, + domainElement: method.domainElement, + severity: 'error', + message: `Declared methods need to be unique (${key}).`, + }); + } } } } From db66fba92c2e189f9f7efde63655a79943239331 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 22 Nov 2024 14:51:00 +0100 Subject: [PATCH 15/19] more comments, renamings --- packages/typir/src/graph/type-node.ts | 28 +++++++++---------- packages/typir/src/kinds/class-kind.ts | 16 +++++------ packages/typir/src/kinds/function-kind.ts | 16 +++++------ packages/typir/src/utils/utils-definitions.ts | 18 ++++++++++-- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index e2840bb..9aa712c 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -23,8 +23,8 @@ stateDiagram-v2 export type TypeInitializationState = 'Invalid' | 'Identifiable' | 'Completed'; export interface PreconditionsForInitializationState { - refsToBeIdentified?: TypeReference[]; // or later/more - refsToBeCompleted?: TypeReference[]; // or later/more + referencesToBeIdentifiable?: TypeReference[]; // or later/more + referencesToBeCompleted?: TypeReference[]; // or later/more } /** @@ -179,37 +179,37 @@ export abstract class Type { */ protected defineTheInitializationProcessOfThisType(preconditions: { /** Contains only those TypeReferences which are required to do the initialization. */ - preconditionsForInitialization?: PreconditionsForInitializationState, + preconditionsForIdentifiable?: PreconditionsForInitializationState, /** Contains only those TypeReferences which are required to do the completion. * TypeReferences which are required only for the initialization, but not for the completion, * don't need to be repeated here, since the completion is done only after the initialization. */ - preconditionsForCompletion?: PreconditionsForInitializationState, + preconditionsForCompleted?: PreconditionsForInitializationState, /** Must contain all(!) TypeReferences of a type. */ referencesRelevantForInvalidation?: TypeReference[], /** typical use cases: calculate the identifier, register inference rules for the type object already now! */ - onIdentification?: () => void, + onIdentifiable?: () => void, /** typical use cases: do some internal checks for the completed properties */ - onCompletion?: () => void, - onInvalidation?: () => void, + onCompleted?: () => void, + onInvalidated?: () => void, }): void { // store the reactions - this.onIdentification = preconditions.onIdentification ?? (() => {}); - this.onCompletion = preconditions.onCompletion ?? (() => {}); - this.onInvalidation = preconditions.onInvalidation ?? (() => {}); + this.onIdentification = preconditions.onIdentifiable ?? (() => {}); + this.onCompletion = preconditions.onCompleted ?? (() => {}); + this.onInvalidation = preconditions.onInvalidated ?? (() => {}); if (this.kind.$name === 'ClassKind') { console.log(''); } // preconditions for Identifiable this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences( - preconditions.preconditionsForInitialization?.refsToBeIdentified, - preconditions.preconditionsForInitialization?.refsToBeCompleted, + preconditions.preconditionsForIdentifiable?.referencesToBeIdentifiable, + preconditions.preconditionsForIdentifiable?.referencesToBeCompleted, ); this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); // preconditions for Completed this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences( - preconditions.preconditionsForCompletion?.refsToBeIdentified, - preconditions.preconditionsForCompletion?.refsToBeCompleted, + preconditions.preconditionsForCompleted?.referencesToBeIdentifiable, + preconditions.preconditionsForCompleted?.referencesToBeCompleted, ); this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); // preconditions for Invalid diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 5f9e209..9298db6 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -88,19 +88,19 @@ export class ClassType extends Type { // all.push(...refMethods); // does not work this.defineTheInitializationProcessOfThisType({ - preconditionsForInitialization: { - refsToBeIdentified: fieldsAndMethods, + preconditionsForIdentifiable: { + referencesToBeIdentifiable: fieldsAndMethods, }, - preconditionsForCompletion: { - refsToBeCompleted: this.superClasses as unknown as Array>, + preconditionsForCompleted: { + referencesToBeCompleted: this.superClasses as unknown as Array>, }, referencesRelevantForInvalidation: [...fieldsAndMethods, ...(this.superClasses as unknown as Array>)], - onIdentification: () => { + onIdentifiable: () => { // the identifier is calculated now this.identifier = this.kind.calculateIdentifier(typeDetails); // TODO it is still not nice, that the type resolving is done again, since the TypeReferences here are not reused // the registration of the type in the type graph is done by the TypeInitializer }, - onCompletion: () => { + onCompleted: () => { // when all super classes are completely available, do the following checks: // check number of allowed super classes if (this.kind.options.maximumNumberOfSuperClasses >= 0) { @@ -109,8 +109,8 @@ export class ClassType extends Type { } } }, - onInvalidation: () => { - // TODO remove all listeners, ... + onInvalidated: () => { + // nothing to do }, }); } diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index 02a49dd..f92a88e 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -59,20 +59,20 @@ export class FunctionType extends Type { allParameterRefs.push(outputType); } this.defineTheInitializationProcessOfThisType({ - preconditionsForInitialization: { - refsToBeIdentified: allParameterRefs, + preconditionsForIdentifiable: { + referencesToBeIdentifiable: allParameterRefs, }, referencesRelevantForInvalidation: allParameterRefs, - onIdentification: () => { + onIdentifiable: () => { // the identifier is calculated now - this.identifier = this.kind.calculateIdentifier(typeDetails); // TODO it is still not nice, that the type resolving is done again, since the TypeReferences here are not reused + this.identifier = this.kind.calculateIdentifier(typeDetails); // the registration of the type in the type graph is done by the TypeInitializer }, - onCompletion: () => { - // some check? + onCompleted: () => { + // no additional checks so far }, - onInvalidation: () => { - // ? + onInvalidated: () => { + // nothing to do }, }); } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 723b0cb..b55b859 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -48,7 +48,7 @@ export type TypeSelector = | TypeReference // reference to a (maybe delayed) type | unknown // domain node to infer the final type from ; -export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); +export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); // TODO export interface WaitingForIdentifiableAndCompletedTypeReferencesListener { @@ -305,10 +305,22 @@ export class WaitingForInvalidTypeReferences implements T /** - * A listener for TypeReferences, who will be informed about the found/identified/resolved/unresolved type of the current TypeReference. + * A listener for TypeReferences, who will be informed about the resolved/found type of the current TypeReference. */ export interface TypeReferenceListener { + /** + * Informs when the type of the reference is resolved/found. + * @param reference the currently resolved TypeReference + * @param resolvedType Usually the resolved type is either 'Identifiable' or 'Completed', + * in rare cases this type might be 'Invalid', e.g. if there are corresponding inference rules or TypeInitializers. + */ onTypeReferenceResolved(reference: TypeReference, resolvedType: T): void; + /** + * Informs when the type of the reference is invalidated/removed. + * @param reference the currently invalidate/unresolved TypeReference + * @param previousType undefined occurs in the special case, that the TypeReference never resolved a type so far, + * but new listeners already want to be informed about the (current) type. + */ onTypeReferenceInvalidated(reference: TypeReference, previousType: T | undefined): void; } @@ -431,7 +443,7 @@ export class TypeReference implements TypeGraphListener, if (this.resolvedType) { listener.onTypeReferenceResolved(this, this.resolvedType); } else { - listener.onTypeReferenceInvalidated(this, undefined!); // hack, maybe remove this parameter? + listener.onTypeReferenceInvalidated(this, undefined); } } } From 9f1b7033e2b9e407672a0fb52c65b6dc529206a5 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 26 Nov 2024 18:23:48 +0100 Subject: [PATCH 16/19] refactorings, more test cases and comments according to the review --- examples/lox/test/lox-type-checking.test.ts | 94 +++++++++++++++++-- packages/typir/src/graph/type-graph.ts | 8 +- packages/typir/src/graph/type-node.ts | 23 ++--- packages/typir/src/kinds/class-kind.ts | 15 +-- packages/typir/src/kinds/function-kind.ts | 2 +- .../typir/src/utils/type-initialization.ts | 8 +- packages/typir/src/utils/utils-definitions.ts | 7 +- 7 files changed, 125 insertions(+), 32 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index c413ba1..e378216 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -452,7 +452,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isClassType, 'A', 'B'); }); - test('Class with method', async () => { + test('Class with method: cycle with return type', async () => { await validate(` class Node { myMethod(input: number): Node {} @@ -462,6 +462,16 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); }); + test('Class with method: cycle with input parameter type', async () => { + await validate(` + class Node { + myMethod(input: Node): number {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'Node'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + test('Two different Classes with the same method (type) should result in only one method type', async () => { await validate(` class A { @@ -492,7 +502,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isFunctionType, 'myMethod', 'myMethod', ...operatorNames); }); - test('Two different Classes with the same method which has on of these classes as return type', async () => { + test('Two different Classes with the same method which has one of these classes as return type', async () => { await validate(` class A { prop1: boolean @@ -537,6 +547,74 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); }); + test('Mix of dependencies in classes: 1 method and 1 field', async () => { + await validate(` + class A { + myMethod(input: number): B1 {} + } + class B1 { + propB1: A + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B1'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + + test('Mix of dependencies in classes: 1 method and 2 fields (order 1)', async () => { + await validate(` + class B1 { + propB1: B2 + } + class B2 { + propB1: A + } + class A { + myMethod(input: number): B1 {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + + test.todo('Mix of dependencies in classes: 1 method and 2 fields (order 2)', async () => { + await validate(` + class A { + myMethod(input: number): B1 {} + } + class B1 { + propB1: B2 + } + class B2 { + propB1: A + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + }); + + test.todo('The same class is involved into two dependency cycles', async () => { + await validate(` + class A { + probA: C1 + myMethod(input: number): B1 {} + } + class B1 { + propB1: B2 + } + class B2 { + propB1: A + } + class C1 { + methodC1(p: C2) {} + } + class C2 { + methodC2(p: A) {} + } + `, []); + expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2', 'C1', 'C2'); + expectTypirTypes(loxServices, isFunctionType, 'myMethod', 'methodC1', 'methodC2', ...operatorNames); + }); + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { @@ -546,9 +624,9 @@ describe('Test internal validation of Typir for cycles in the class inheritance class MyClass2 < MyClass1 { } class MyClass3 < MyClass2 { } `, [ - 'Circles in super-sub-class-relationships are not allowed: MyClass1', - 'Circles in super-sub-class-relationships are not allowed: MyClass2', - 'Circles in super-sub-class-relationships are not allowed: MyClass3', + 'Cycles in super-sub-class-relationships are not allowed: MyClass1', + 'Cycles in super-sub-class-relationships are not allowed: MyClass2', + 'Cycles in super-sub-class-relationships are not allowed: MyClass3', ]); expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2', 'MyClass3'); }); @@ -558,8 +636,8 @@ describe('Test internal validation of Typir for cycles in the class inheritance class MyClass1 < MyClass2 { } class MyClass2 < MyClass1 { } `, [ - 'Circles in super-sub-class-relationships are not allowed: MyClass1', - 'Circles in super-sub-class-relationships are not allowed: MyClass2', + 'Cycles in super-sub-class-relationships are not allowed: MyClass1', + 'Cycles in super-sub-class-relationships are not allowed: MyClass2', ]); expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); }); @@ -567,7 +645,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance test('One involved class: 1 -> 1', async () => { await validate(` class MyClass1 < MyClass1 { } - `, 'Circles in super-sub-class-relationships are not allowed: MyClass1'); + `, 'Cycles in super-sub-class-relationships are not allowed: MyClass1'); expectTypirTypes(loxServices, isClassType, 'MyClass1'); }); }); diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index a301834..ffa0e5d 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -5,6 +5,7 @@ ******************************************************************************/ import { EdgeCachingInformation } from '../features/caching.js'; +import { assertTrue } from '../utils/utils.js'; import { TypeEdge } from './type-edge.js'; import { Type } from './type-node.js'; @@ -28,10 +29,11 @@ export class TypeGraph { * Therefore it is usually not needed to call this method in an other context. * @param type the new type * @param key an optional key to register the type, since it is allowed to register the same type with different keys in the graph - * TODO oder stattdessen einen ProxyType verwenden? wie funktioniert das mit isClassType und isSubType? wie funktioniert removeType? */ addNode(type: Type, key?: string): void { - // TODO überprüfen, dass Identifiable-State erreicht ist?? + if (!key) { + assertTrue(type.isInStateOrLater('Identifiable')); // the key of the type must be available! + } const mapKey = key ?? type.getIdentifier(); if (this.nodes.has(mapKey)) { if (this.nodes.get(mapKey) === type) { @@ -62,7 +64,7 @@ export class TypeGraph { const contained = this.nodes.delete(mapKey); if (contained) { this.listeners.slice().forEach(listener => listener.removedType(typeToRemove, mapKey)); - typeToRemove.deconstruct(); + typeToRemove.dispose(); } else { throw new Error(`Type does not exist: ${mapKey}`); } diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 9aa712c..801ec2d 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -19,6 +19,10 @@ stateDiagram-v2 Completed --> Invalid Identifiable --> Invalid ``` + * A state is 'Completed', when all its dependencies are available, i.e. the types of all its properties are available. + * A state is 'Identifiable', when all those dependencies are available which are required to calculate the identifier of the type. + * A state is 'Invalid' otherwise. + * 'Invalid' is made explicit, since it might require less dependencies than 'Completed' and therefore speed-ups the resolution of dependencies. */ export type TypeInitializationState = 'Invalid' | 'Identifiable' | 'Completed'; @@ -130,8 +134,8 @@ export abstract class Type { protected stateListeners: TypeStateListener[] = []; - addListener(listener: TypeStateListener, informIfNotInvalidAnymore: boolean): void { - this.stateListeners.push(listener); + addListener(newListeners: TypeStateListener, informIfNotInvalidAnymore: boolean): void { + this.stateListeners.push(newListeners); if (informIfNotInvalidAnymore) { const currentState = this.getInitializationState(); switch (currentState) { @@ -139,11 +143,11 @@ export abstract class Type { // don't inform about the Invalid state! break; case 'Identifiable': - listener.switchedToIdentifiable(this); + newListeners.switchedToIdentifiable(this); break; case 'Completed': - listener.switchedToIdentifiable(this); // inform about both Identifiable and Completed! - listener.switchedToCompleted(this); + newListeners.switchedToIdentifiable(this); // inform about both Identifiable and Completed! + newListeners.switchedToCompleted(this); break; default: assertUnreachable(currentState); @@ -197,21 +201,18 @@ export abstract class Type { this.onCompletion = preconditions.onCompleted ?? (() => {}); this.onInvalidation = preconditions.onInvalidated ?? (() => {}); - if (this.kind.$name === 'ClassKind') { - console.log(''); - } // preconditions for Identifiable this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForIdentifiable?.referencesToBeIdentifiable, preconditions.preconditionsForIdentifiable?.referencesToBeCompleted, ); - this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); + this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); // start of the principle: children don't need to wait for their parents // preconditions for Completed this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForCompleted?.referencesToBeIdentifiable, preconditions.preconditionsForCompleted?.referencesToBeCompleted, ); - this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); + this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); // start of the principle: children don't need to wait for their parents // preconditions for Invalid this.waitForInvalid = new WaitingForInvalidTypeReferences( preconditions.referencesRelevantForInvalidation ?? [], @@ -262,7 +263,7 @@ export abstract class Type { this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore); } - deconstruct(): void { + dispose(): void { // clear everything this.stateListeners.splice(0, this.stateListeners.length); this.waitForInvalid.getWaitForRefsInvalid().forEach(ref => ref.deconstruct()); diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 9298db6..a9c024e 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -388,6 +388,12 @@ export interface CreateFieldDetails { type: TypeSelector; } +/** + * Describes all properties of Methods of a Class. + * The final reason to describe methods with Function types was to have a simple solution and to reuse all the implementations for functions, + * since methods and functions are the same from a typing perspective. + * This interfaces makes annotating further properties to methods easier (which are not supported by functions). + */ export interface MethodDetails { type: TypeReference; // methods might have some more properties in the future @@ -444,8 +450,6 @@ export class ClassKind implements Kind { assertTrue(this.options.maximumNumberOfSuperClasses >= 0); // no negative values } - // zwei verschiedene Use cases für Calls: Reference/use (e.g. Var-Type) VS Creation (e.g. Class-Declaration) - /** * For the use case, that a type is used/referenced, e.g. to specify the type of a variable declaration. * @param typeDetails all information needed to identify the class @@ -546,7 +550,6 @@ export function isClassKind(kind: unknown): kind is ClassKind { } -// TODO Review: Is it better to not have "extends TypeInitializer" and to merge TypeInitializer+ClassTypeInitializer into one class? export class ClassTypeInitializer extends TypeInitializer implements TypeStateListener { protected readonly typeDetails: CreateClassTypeDetails; protected readonly kind: ClassKind; @@ -613,7 +616,7 @@ export class ClassTypeInitializer exten if (this.typeDetails.inferenceRuleForDeclaration === null) { // check for cycles in sub-type-relationships of classes if ((classType as ClassType).hasSubSuperClassCycles()) { - throw new Error(`Circles in super-sub-class-relationships are not allowed: ${classType.getName()}`); + throw new Error(`Cycles in super-sub-class-relationships are not allowed: ${classType.getName()}`); } } @@ -622,7 +625,7 @@ export class ClassTypeInitializer exten } switchedToInvalid(_previousClassType: Type): void { - // do nothing + // nothing specific needs to be done for Classes here, since the base implementation takes already care about all relevant stuff } override getTypeInitial(): ClassType { @@ -919,7 +922,7 @@ export function createNoSuperClassCyclesValidation(isRelevant: (domainElement: u $problem: ValidationProblem, domainElement, severity: 'error', - message: `Circles in super-sub-class-relationships are not allowed: ${classType.getName()}`, + message: `Cycles in super-sub-class-relationships are not allowed: ${classType.getName()}`, }); } } diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index f92a88e..06b7a46 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -609,7 +609,7 @@ export class FunctionTypeInitializer extends TypeInitializer im } switchedToInvalid(_functionType: Type): void { - // empty + // nothing specific needs to be done for Functions here, since the base implementation takes already care about all relevant stuff } } diff --git a/packages/typir/src/utils/type-initialization.ts b/packages/typir/src/utils/type-initialization.ts index e1715e0..e1ff512 100644 --- a/packages/typir/src/utils/type-initialization.ts +++ b/packages/typir/src/utils/type-initialization.ts @@ -21,6 +21,10 @@ export type TypeInitializerListener = (type: T) => void; * Since the first Typir type is not yet in the type systems (since it still waits for B) and therefore remains unknown, * it will be tried to create A a second time, again delayed, since B is still not yet available. * When B is created, A is waiting twice and might be created twice, if no TypeInitializer is used. + * + * Design decision: While this class does not provide some many default implementations, + * a common super class (or interface) of all type initializers is useful, + * since they all can be used as TypeSelector in an easy way. */ export abstract class TypeInitializer { protected readonly services: TypirServices; @@ -40,7 +44,7 @@ export abstract class TypeInitializer { if (existingType) { // ensure, that the same type is not duplicated! this.typeToReturn = existingType as T; - newType.deconstruct(); + newType.dispose(); } else { this.typeToReturn = newType; this.services.graph.addNode(this.typeToReturn); @@ -54,7 +58,7 @@ export abstract class TypeInitializer { return this.typeToReturn; } - // TODO using this type feels wrong, but otherwise, it seems not to work ... + // TODO using this type feels wrong, but without this approach, it seems not to work ... abstract getTypeInitial(): T getTypeFinal(): T | undefined { diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index b55b859..3505446 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -123,6 +123,11 @@ export class WaitingForIdentifiableAndCompletedTypeReferences): void { // identify the actual new types to ignore (filtering out the types which are already ignored) const newTypesToIgnore: Set = new Set(); @@ -164,7 +169,7 @@ export class WaitingForIdentifiableAndCompletedTypeReferences, resolvedType: Type): void { - // inform the referenced type about the types to ignore for completion + // inform the referenced type about the types to ignore for completion, so that the type could switch to its next phase (if needed) resolvedType.ignoreDependingTypesDuringInitialization(this.typesToIgnoreForCycles); resolvedType.addListener(this, false); // check, whether all TypeReferences are resolved and the resolved types are in the expected state From 2a6c18fc3cfefbe6540982bdd820f0130ac5563e Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 27 Nov 2024 10:37:40 +0100 Subject: [PATCH 17/19] added name as identifier for nominally typed classes earlier, refactoring --- examples/lox/test/lox-type-checking.test.ts | 8 ++++---- packages/typir/src/graph/type-node.ts | 2 +- packages/typir/src/kinds/class-kind.ts | 15 +++++++++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index e378216..b917b8f 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -576,7 +576,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); }); - test.todo('Mix of dependencies in classes: 1 method and 2 fields (order 2)', async () => { + test('Mix of dependencies in classes: 1 method and 2 fields (order 2)', async () => { await validate(` class A { myMethod(input: number): B1 {} @@ -592,7 +592,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); }); - test.todo('The same class is involved into two dependency cycles', async () => { + test('The same class is involved into two dependency cycles', async () => { await validate(` class A { probA: C1 @@ -605,10 +605,10 @@ describe('Cyclic type definitions where a Class is declared and already used', ( propB1: A } class C1 { - methodC1(p: C2) {} + methodC1(p: C2): void {} } class C2 { - methodC2(p: A) {} + methodC2(p: A): void {} } `, []); expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2', 'C1', 'C2'); diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 801ec2d..62369d7 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -61,7 +61,7 @@ export abstract class Type { * Identifiers might have a naming schema for calculatable values. */ getIdentifier(): string { - this.assertStateOrLater('Identifiable'); + // an Identifier must be available; note that the state might be 'Invalid' nevertheless, which is required to handle cyclic type definitions assertTrue(this.identifier !== undefined); return this.identifier; } diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index a9c024e..48c1eba 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -30,7 +30,9 @@ export class ClassType extends Type { protected methods: MethodDetails[]; // unordered constructor(kind: ClassKind, typeDetails: ClassTypeDetails) { - super(undefined); + super(kind.options.typing === 'Nominal' + ? kind.calculateNominalIdentifier(typeDetails) // use the name of the class as identifier already now + : undefined); // the identifier for structurally typed classes will be set later after resolving all fields and methods this.kind = kind; this.className = typeDetails.className; @@ -527,6 +529,10 @@ export class ClassKind implements Kind { } } + calculateNominalIdentifier(typeDetails: ClassTypeDetails): string { + return this.getIdentifierPrefix() + typeDetails.className; + } + getMethodKind(): FunctionKind { // ensure, that Typir uses the predefined 'function' kind for methods const kind = this.services.kinds.get(FunctionKindName); @@ -565,7 +571,7 @@ export class ClassTypeInitializer exten this.initialClassType = new ClassType(kind, typeDetails as CreateClassTypeDetails); if (kind.options.typing === 'Structural') { // register structural classes also by their names, since these names are usually used for reference in the DSL/AST! - this.services.graph.addNode(this.initialClassType, kind.getIdentifierPrefix() + typeDetails.className); + this.services.graph.addNode(this.initialClassType, kind.calculateNominalIdentifier(typeDetails)); } this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, this.initialClassType); @@ -591,8 +597,9 @@ export class ClassTypeInitializer exten if (this.kind.options.typing === 'Structural') { // replace the type in the type graph - this.services.graph.removeNode(classType, this.kind.getIdentifierPrefix() + this.typeDetails.className); - this.services.graph.addNode(readyClassType, this.kind.getIdentifierPrefix() + this.typeDetails.className); + const nominalIdentifier = this.kind.calculateNominalIdentifier(this.typeDetails); + this.services.graph.removeNode(classType, nominalIdentifier); + this.services.graph.addNode(readyClassType, nominalIdentifier); } // remove the inference rules for the invalid type From 3b471e2bf4d2a5c0e46d6089d50a8478a7e463b8 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 27 Nov 2024 10:53:42 +0100 Subject: [PATCH 18/19] improved existing test cases --- examples/lox/test/lox-type-checking.test.ts | 251 ++++++++++++-------- packages/typir/src/utils/test-utils.ts | 11 +- 2 files changed, 158 insertions(+), 104 deletions(-) diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index b917b8f..e28f866 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -78,6 +78,7 @@ describe('Explicitly test type checking for LOX', () => { await validate('fun myFunction2() : boolean { return 2; }', 1); await validate('fun myFunction3() : number { return 2; }', 0); await validate('fun myFunction4() : number { return true; }', 1); + expectTypirTypes(loxServices, isFunctionType, 'myFunction1', 'myFunction2', 'myFunction3', 'myFunction4', ...operatorNames); }); test('overloaded function: different return types are not enough', async () => { @@ -85,18 +86,21 @@ describe('Explicitly test type checking for LOX', () => { fun myFunction() : boolean { return true; } fun myFunction() : number { return 2; } `, 2); + expectTypirTypes(loxServices, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); // the types are different nevertheless! }); test('overloaded function: different parameter names are not enough', async () => { await validate(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(other: boolean) : boolean { return true; } `, 2); + expectTypirTypes(loxServices, isFunctionType, 'myFunction', ...operatorNames); // but both functions have the same type! }); test('overloaded function: but different parameter types are fine', async () => { await validate(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(input: number) : boolean { return true; } `, 0); + expectTypirTypes(loxServices, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); }); test('use overloaded operators: +', async () => { @@ -179,12 +183,14 @@ describe('Explicitly test type checking for LOX', () => { class MyClass { name: string age: number } var v1 = MyClass(); // constructor call `, []); + expectTypirTypes(loxServices, isClassType, 'MyClass'); }); test('Class literals 2', async () => { await validate(` class MyClass { name: string age: number } var v1: MyClass = MyClass(); // constructor call `, []); + expectTypirTypes(loxServices, isClassType, 'MyClass'); }); test('Class literals 3', async () => { await validate(` @@ -192,20 +198,26 @@ describe('Explicitly test type checking for LOX', () => { class MyClass2 {} var v1: boolean = MyClass1() == MyClass2(); // comparing objects with each other `, [], 1); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); }); }); - test('Class inheritance for assignments', async () => { + test('Class inheritance for assignments: correct', async () => { await validate(` class MyClass1 { name: string age: number } class MyClass2 < MyClass1 {} var v1: MyClass1 = MyClass2(); `, 0); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + }); + + test('Class inheritance for assignments: wrong', async () => { await validate(` class MyClass1 { name: string age: number } class MyClass2 < MyClass1 {} var v1: MyClass2 = MyClass1(); `, 1); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); }); test('Class inheritance and the order of type definitions', async () => { @@ -214,6 +226,7 @@ describe('Explicitly test type checking for LOX', () => { class MyClass1 {} class MyClass2 < MyClass1 {} `, []); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); }); test('Class inheritance and the order of type definitions', async () => { // switching the order of super and sub class works in Langium and in Typir @@ -221,24 +234,30 @@ describe('Explicitly test type checking for LOX', () => { class MyClass2 < MyClass1 {} class MyClass1 {} `, []); + expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); }); - test('Class fields', async () => { + test('Class fields: correct values', async () => { await validate(` class MyClass1 { name: string age: number } var v1: MyClass1 = MyClass1(); v1.name = "Bob"; v1.age = 42; `, 0); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); + + test('Class fields: wrong values', async () => { await validate(` class MyClass1 { name: string age: number } var v1: MyClass1 = MyClass1(); v1.name = 42; v1.age = "Bob"; `, 2); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); }); - test('Classes must be unique by name', async () => { + test('Classes must be unique by name 2', async () => { await validate(` class MyClass1 { } class MyClass1 { } @@ -246,6 +265,10 @@ describe('Explicitly test type checking for LOX', () => { 'Declared classes need to be unique (MyClass1).', 'Declared classes need to be unique (MyClass1).', ]); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); + + test('Classes must be unique by name 3', async () => { await validate(` class MyClass2 { } class MyClass2 { } @@ -255,61 +278,77 @@ describe('Explicitly test type checking for LOX', () => { 'Declared classes need to be unique (MyClass2).', 'Declared classes need to be unique (MyClass2).', ]); + expectTypirTypes(loxServices, isClassType, 'MyClass2'); }); - test('Class methods: OK', async () => await validate(` - class MyClass1 { - method1(input: number): number { - return 123; + test('Class methods: OK', async () => { + await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } } - } - var v1: MyClass1 = MyClass1(); - var v2: number = v1.method1(456); - `, [])); + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(456); + `, []); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); - test('Class methods: wrong return value', async () => await validate(` - class MyClass1 { - method1(input: number): number { - return true; + test('Class methods: wrong return value', async () => { + await validate(` + class MyClass1 { + method1(input: number): number { + return true; + } } - } - var v1: MyClass1 = MyClass1(); - var v2: number = v1.method1(456); - `, 1)); + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(456); + `, 1); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); - test('Class methods: method return type does not fit to variable type', async () => await validate(` - class MyClass1 { - method1(input: number): number { - return 123; + test('Class methods: method return type does not fit to variable type', async () => { + await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } } - } - var v1: MyClass1 = MyClass1(); - var v2: boolean = v1.method1(456); - `, 1)); + var v1: MyClass1 = MyClass1(); + var v2: boolean = v1.method1(456); + `, 1); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); - test('Class methods: value for input parameter does not fit to the type of the input parameter', async () => await validate(` - class MyClass1 { - method1(input: number): number { - return 123; + test('Class methods: value for input parameter does not fit to the type of the input parameter', async () => { + await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } } - } - var v1: MyClass1 = MyClass1(); - var v2: number = v1.method1(true); - `, 1)); + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(true); + `, 1); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); - test('Class methods: methods are not distinguishable', async () => await validate(` - class MyClass1 { - method1(input: number): number { - return 123; - } - method1(another: number): boolean { - return true; + test('Class methods: methods are not distinguishable', async () => { + await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } + method1(another: number): boolean { + return true; + } } - } - `, [ // both methods need to be marked: - 'Declared methods need to be unique (class-MyClass1.method1(number)).', - 'Declared methods need to be unique (class-MyClass1.method1(number)).', - ])); + `, [ // both methods need to be marked: + 'Declared methods need to be unique (class-MyClass1.method1(number)).', + 'Declared methods need to be unique (class-MyClass1.method1(number)).', + ]); + expectTypirTypes(loxServices, isClassType, 'MyClass1'); + }); }); @@ -652,78 +691,84 @@ describe('Test internal validation of Typir for cycles in the class inheritance describe('longer LOX examples', () => { // this test case will work after having the support for cyclic type definitions, since it will solve also issues with topological order of type definitions - test('complete with difficult order of classes', async () => await validate(` - class SuperClass { - a: number - } + test('complete with difficult order of classes', async () => { + await validate(` + class SuperClass { + a: number + } - class SubClass < SuperClass { - // Nested class - nested: NestedClass - } + class SubClass < SuperClass { + // Nested class + nested: NestedClass + } - class NestedClass { - field: string - method(): string { - return "execute this"; + class NestedClass { + field: string + method(): string { + return "execute this"; + } } - } - // Constructor call - var x = SubClass(); - // Assigning nil to a class type - var nilTest = SubClass(); - nilTest = nil; + // Constructor call + var x = SubClass(); + // Assigning nil to a class type + var nilTest = SubClass(); + nilTest = nil; - // Accessing members of a class - var value = x.nested.method() + "wasd"; - print value; + // Accessing members of a class + var value = x.nested.method() + "wasd"; + print value; - // Accessing members of a super class - var superValue = x.a; - print superValue; + // Accessing members of a super class + var superValue = x.a; + print superValue; - // Assigning a subclass to a super class - var superType: SuperClass = x; - print superType.a; - `, [])); + // Assigning a subclass to a super class + var superType: SuperClass = x; + print superType.a; + `, []); + expectTypirTypes(loxServices, isClassType, 'SuperClass', 'SubClass', 'NestedClass'); + }); - test('complete with easy order of classes', async () => await validate(` - class SuperClass { - a: number - } + test('complete with easy order of classes', async () => { + await validate(` + class SuperClass { + a: number + } - class NestedClass { - field: string - method(): string { - return "execute this"; + class NestedClass { + field: string + method(): string { + return "execute this"; + } } - } - class SubClass < SuperClass { - // Nested class - nested: NestedClass - } + class SubClass < SuperClass { + // Nested class + nested: NestedClass + } - // Constructor call - var x = SubClass(); - // Assigning nil to a class type - var nilTest = SubClass(); - nilTest = nil; + // Constructor call + var x = SubClass(); + // Assigning nil to a class type + var nilTest = SubClass(); + nilTest = nil; - // Accessing members of a class - var value = x.nested.method() + "wasd"; - print value; + // Accessing members of a class + var value = x.nested.method() + "wasd"; + print value; - // Accessing members of a super class - var superValue = x.a; - print superValue; + // Accessing members of a super class + var superValue = x.a; + print superValue; - // Assigning a subclass to a super class - var superType: SuperClass = x; - print superType.a; - `, [])); + // Assigning a subclass to a super class + var superType: SuperClass = x; + print superType.a; + `, []); + expectTypirTypes(loxServices, isClassType, 'SuperClass', 'SubClass', 'NestedClass'); + }); }); async function validate(lox: string, errors: number | string | string[], warnings: number = 0) { diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index e168eaf..698df97 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -8,6 +8,15 @@ import { expect } from 'vitest'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; +/** + * Testing utility to check, that exactly the expected types are in the type system. + * @param services the Typir services + * @param filterTypes used to identify the types of interest + * @param namesOfExpectedTypes the names (not the identifiers!) of the expected types; + * ensures that there are no more types; + * it is possible to specify names multiple times, if there are multiple types with the same name (e.g. for overloaded functions) + * @returns all the found types + */ export function expectTypirTypes(services: TypirServices, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): Type[] { const types = services.graph.getAllRegisteredTypes().filter(filterTypes); types.forEach(type => expect(type.getInitializationState()).toBe('Completed')); // check that all types are 'Completed' @@ -18,6 +27,6 @@ export function expectTypirTypes(services: TypirServices, filterTypes: (type: Ty expect(index >= 0).toBeTruthy(); typeNames.splice(index, 1); // removing elements is needed to work correctly with duplicated entries } - expect(typeNames).toHaveLength(0); + expect(typeNames, `There are more types than expected: ${typeNames.join(', ')}`).toHaveLength(0); return types; } From bc43521ec86f7167a7ee7ad2cd378131acca3d08 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 27 Nov 2024 13:51:42 +0100 Subject: [PATCH 19/19] renaming, refactoring, more comments --- packages/typir/src/kinds/class-kind.ts | 34 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 48c1eba..ff6bf61 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -31,7 +31,7 @@ export class ClassType extends Type { constructor(kind: ClassKind, typeDetails: ClassTypeDetails) { super(kind.options.typing === 'Nominal' - ? kind.calculateNominalIdentifier(typeDetails) // use the name of the class as identifier already now + ? kind.calculateIdentifierWithClassNameOnly(typeDetails) // use the name of the class as identifier already now : undefined); // the identifier for structurally typed classes will be set later after resolving all fields and methods this.kind = kind; this.className = typeDetails.className; @@ -483,19 +483,20 @@ export class ClassKind implements Kind { } /** - * TODO + * This method calculates the identifier of a class with the given details. + * Depending on structural or nominal typing of classes, the fields and methods or the name of the class will be used to compose the resulting identifier. * If some types for the properties of the class are missing, an exception will be thrown. * * Design decisions: * - This method is part of the ClassKind and not part of ClassType, since the ClassKind requires it for 'getClassType'! - * - The kind might use/add additional prefixes for the identifiers to make them "even more unique". + * - The kind might use/add additional prefixes for the identifiers to prevent collisions with types of other kinds, + * which might occur in some applications. * * @param typeDetails the details * @returns the new identifier */ calculateIdentifier(typeDetails: ClassTypeDetails): string { // purpose of identifier: distinguish different types; NOT: not uniquely overloaded types - const prefix = this.getIdentifierPrefix(); if (this.options.typing === 'Structural') { // fields const fields: string = typeDetails.fields @@ -520,17 +521,24 @@ export class ClassKind implements Kind { .sort() .join(','); // complete identifier (the name of the class does not matter for structural typing!) - return `${prefix}fields{${fields}}-methods{${methods}}-extends{${superClasses}}`; + return `${this.getIdentifierPrefix()}fields{${fields}}-methods{${methods}}-extends{${superClasses}}`; } else if (this.options.typing === 'Nominal') { - // only the name matters for nominal typing! - return `${prefix}${typeDetails.className}`; + // only the name of the class matters for nominal typing! + return this.calculateIdentifierWithClassNameOnly(typeDetails); } else { assertUnreachable(this.options.typing); } } - calculateNominalIdentifier(typeDetails: ClassTypeDetails): string { - return this.getIdentifierPrefix() + typeDetails.className; + /** + * Calculates an identifier for classes which takes only the name of the class into account, + * regardless of whether the class is structurally or nominally typed. + * For structurally typed classes, this identifier might be used as well, since these names are usually used for reference in the DSL/AST! + * @param typeDetails the details of the class + * @returns the identifier based on the class name + */ + calculateIdentifierWithClassNameOnly(typeDetails: ClassTypeDetails): string { + return `${this.getIdentifierPrefix()}${typeDetails.className}`; } getMethodKind(): FunctionKind { @@ -571,7 +579,7 @@ export class ClassTypeInitializer exten this.initialClassType = new ClassType(kind, typeDetails as CreateClassTypeDetails); if (kind.options.typing === 'Structural') { // register structural classes also by their names, since these names are usually used for reference in the DSL/AST! - this.services.graph.addNode(this.initialClassType, kind.calculateNominalIdentifier(typeDetails)); + this.services.graph.addNode(this.initialClassType, kind.calculateIdentifierWithClassNameOnly(typeDetails)); } this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, this.initialClassType); @@ -597,9 +605,9 @@ export class ClassTypeInitializer exten if (this.kind.options.typing === 'Structural') { // replace the type in the type graph - const nominalIdentifier = this.kind.calculateNominalIdentifier(this.typeDetails); - this.services.graph.removeNode(classType, nominalIdentifier); - this.services.graph.addNode(readyClassType, nominalIdentifier); + const nameBasedIdentifier = this.kind.calculateIdentifierWithClassNameOnly(this.typeDetails); + this.services.graph.removeNode(classType, nameBasedIdentifier); + this.services.graph.addNode(readyClassType, nameBasedIdentifier); } // remove the inference rules for the invalid type