UNPKG

typir

Version:

General purpose type checking library

423 lines (376 loc) 19.3 kB
/****************************************************************************** * 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 { TypeReference } from '../initialization/type-reference.js'; import { WaitingForIdentifiableAndCompletedTypeReferences, WaitingForInvalidTypeReferences } from '../initialization/type-waiting.js'; import { Kind } from '../kinds/kind.js'; import { TypirSpecifics } from '../typir.js'; import { TypirProblem } from '../utils/utils-definitions.js'; import { assertTrue, assertUnreachable, removeFromArray } from '../utils/utils.js'; import { TypeEdge } from './type-edge.js'; /** * 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 ``` * 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'; export interface PreconditionsForInitializationState { referencesToBeIdentifiable?: Array<TypeReference<Type>>; // or later/more referencesToBeCompleted?: Array<TypeReference<Type>>; // or later/more } /** * Contains properties which are be relevant for all types to create, * i.e. it is used for specifying details of all types to create. */ export interface TypeDetails<Specifics extends TypirSpecifics> { /** A node from the language might be associated with the new type to create, * e.g. the declaration node in the AST (e.g. a FunctionDeclarationNode is associated with the corresponding FunctionType). */ associatedLanguageNode?: Specifics['LanguageType']; } /** * Design decisions: * - features of types are realized/determined by their kinds * - Identifiers of types must be unique! */ export abstract class Type { readonly kind: Kind; // => $kind: string, required for isXType() checks /* 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. */ 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<string, TypeEdge[]> = new Map(); protected readonly edgesOutgoing: Map<string, TypeEdge[]> = new Map(); /** * A node from the language might be associated with the current type, * e.g. the declaration node in the AST (e.g. a FunctionDeclarationNode is associated with the corresponding FunctionType) * This language node is _not_ used for managing the lifecycles of this type, * since it should be usable for any domain-specific purpose. * Therefore, the use and update of this feature is under the responsibility of the user of Typir. */ readonly associatedLanguageNode: unknown | undefined; // TODO 'unknown' is not replaced by Specifics['LanguageType'], since this generic is not used by Type constructor(identifier: string | undefined, typeDetails: TypeDetails<TypirSpecifics>) { this.identifier = identifier; this.associatedLanguageNode = typeDetails.associatedLanguageNode; } /** * 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 { // 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; } /** * Returns a string value containing a short representation of the type to be shown to users of the type-checked language nodes. * This value don't need to be unique for all types. * This name should be quite short. * Services should not call this function directly, but typir.printer.printTypeName(...) instead. * @returns a short string value to show to the user */ abstract getName(): string; /** * Calculates a string value which might be shown to users of the type-checked language nodes. * This value don't need to be unique for all types. * This representation might be longer and show lots of details of the type. * Services should not call this function directly, but typir.printer.printTypeUserRepresentation(...) instead. * @returns a longer string value to show to the user */ abstract getUserRepresentation(): string; // store the state of the initialization process of this type protected initializationState: TypeInitializationState = 'Invalid'; getInitializationState(): TypeInitializationState { 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.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.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.initializationState}, but this state is not expected.`); } } isInState(state: TypeInitializationState): boolean { return this.initializationState === state; } isNotInState(state: TypeInitializationState): boolean { return this.initializationState !== state; } isInStateOrLater(state: TypeInitializationState): boolean { switch (state) { case 'Invalid': return true; case 'Identifiable': return this.initializationState !== 'Invalid'; case 'Completed': return this.initializationState === 'Completed'; default: assertUnreachable(state); } } // manage listeners for updates of the initialization state protected stateListeners: TypeStateListener[] = []; addListener(newListeners: TypeStateListener, informIfNotInvalidAnymore: boolean): void { this.stateListeners.push(newListeners); if (informIfNotInvalidAnymore) { const currentState = this.getInitializationState(); switch (currentState) { case 'Invalid': // don't inform about the Invalid state! break; case 'Identifiable': newListeners.onSwitchedToIdentifiable(this); break; case 'Completed': newListeners.onSwitchedToIdentifiable(this); // inform about both Identifiable and Completed! newListeners.onSwitchedToCompleted(this); break; default: assertUnreachable(currentState); } } } removeListener(listener: TypeStateListener): void { removeFromArray(listener, this.stateListeners); } // initialization logic which is specific for the type to initialize protected onIdentification: () => void; protected onCompletion: () => void; protected onInvalidation: () => void; // internal helpers protected waitForIdentifiable: WaitingForIdentifiableAndCompletedTypeReferences<Type>; protected waitForCompleted: WaitingForIdentifiableAndCompletedTypeReferences<Type>; protected waitForInvalid: WaitingForInvalidTypeReferences<Type>; /** * 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: { /** Contains only those TypeReferences which are required to do the initialization. */ 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. */ preconditionsForCompleted?: PreconditionsForInitializationState, /** Must contain all(!) TypeReferences of a type. */ referencesRelevantForInvalidation?: Array<TypeReference<Type>>, /** typical use cases: calculate the identifier, register inference rules for the type object already now! */ onIdentifiable?: () => void, /** typical use cases: do some internal checks for the completed properties */ onCompleted?: () => void, onInvalidated?: () => void, }): void { // store the reactions this.onIdentification = preconditions.onIdentifiable ?? (() => {}); this.onCompletion = preconditions.onCompleted ?? (() => {}); this.onInvalidation = preconditions.onInvalidated ?? (() => {}); // preconditions for Identifiable this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences( preconditions.preconditionsForIdentifiable?.referencesToBeIdentifiable, preconditions.preconditionsForIdentifiable?.referencesToBeCompleted, ); 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])); // start of the principle: children don't need to wait for their parents // preconditions for Invalid this.waitForInvalid = new WaitingForInvalidTypeReferences( preconditions.referencesRelevantForInvalidation ?? [], ); // eslint-disable-next-line @typescript-eslint/no-this-alias const thisType = this; // invalid --> identifiable 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({ 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(() => { this.switchFromCompleteOrIdentifiableToInvalid(); }, 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<Type>): void { this.waitForIdentifiable.addTypesToIgnoreForCycles(additionalTypesToIgnore); this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore); } dispose(): void { // clear everything this.stateListeners.splice(0, this.stateListeners.length); this.waitForInvalid.getWaitForRefsInvalid().forEach(ref => ref.dispose()); this.waitForIdentifiable.deconstruct(); this.waitForCompleted.deconstruct(); this.waitForInvalid.deconstruct(); // edges are already removed, when the type is removed from the graph, // but in some cases, the type was not (yet) added to the graph, but got already edges => these edges need to be removed now // e.g. "duplicated" types which are created and disposed by TypeInitializers this.edgesIncoming.clear(); this.edgesOutgoing.clear(); } protected switchFromInvalidToIdentifiable(): void { this.assertState('Invalid'); this.onIdentification(); this.initializationState = 'Identifiable'; this.stateListeners.slice().forEach(listener => listener.onSwitchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications } protected switchFromIdentifiableToCompleted(): void { this.assertState('Identifiable'); this.onCompletion(); this.initializationState = 'Completed'; this.stateListeners.slice().forEach(listener => listener.onSwitchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications } protected switchFromCompleteOrIdentifiableToInvalid(): void { if (this.isNotInState('Invalid')) { this.onInvalidation(); this.initializationState = 'Invalid'; this.stateListeners.slice().forEach(listener => listener.onSwitchedToInvalid(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 } } /** * Analyzes, whether two types are equal. * @param otherType to be compared with the current type * @returns an empty array, if both types are equal, otherwise some problems which might point to found differences/conflicts between the two types. * These problems are presented to users in order to support them with useful information about the result of this analysis. */ abstract analyzeTypeEqualityProblems(otherType: Type): TypirProblem[]; addIncomingEdge(edge: TypeEdge): void { const key = edge.$relation; if (this.edgesIncoming.has(key)) { this.edgesIncoming.get(key)!.push(edge); } else { this.edgesIncoming.set(key, [edge]); } } addOutgoingEdge(edge: TypeEdge): void { const key = edge.$relation; if (this.edgesOutgoing.has(key)) { this.edgesOutgoing.get(key)!.push(edge); } else { this.edgesOutgoing.set(key, [edge]); } } removeIncomingEdge(edge: TypeEdge): boolean { const key = edge.$relation; const list = this.edgesIncoming.get(key); if (list) { const index = list.indexOf(edge); if (index >= 0) { list.splice(index, 1); if (list.length <= 0) { this.edgesIncoming.delete(key); } return true; } } return false; } removeOutgoingEdge(edge: TypeEdge): boolean { const key = edge.$relation; const list = this.edgesOutgoing.get(key); if (list) { const index = list.indexOf(edge); if (index >= 0) { list.splice(index, 1); if (list.length <= 0) { this.edgesOutgoing.delete(key); } return true; } } return false; } getIncomingEdges<T extends TypeEdge>($relation: T['$relation']): T[] { return this.edgesIncoming.get($relation) as T[] ?? []; } getOutgoingEdges<T extends TypeEdge>($relation: T['$relation']): T[] { return this.edgesOutgoing.get($relation) as T[] ?? []; } getEdges<T extends TypeEdge>($relation: T['$relation']): T[] { return [ ...this.getIncomingEdges($relation), ...this.getOutgoingEdges($relation), ]; } getAllIncomingEdges(): TypeEdge[] { return Array.from(this.edgesIncoming.values()).flat(); } getAllOutgoingEdges(): TypeEdge[] { return Array.from(this.edgesOutgoing.values()).flat(); } getAllEdges(): TypeEdge[] { return [ ...this.getAllIncomingEdges(), ...this.getAllOutgoingEdges(), ]; } } export function isType(type: unknown): type is Type { return typeof type === 'object' && type !== null && typeof (type as Type).getIdentifier === 'function' && typeof (type as Type).kind === 'object'; } export interface TypeStateListener { onSwitchedToInvalid(type: Type): void; onSwitchedToIdentifiable(type: Type): void; onSwitchedToCompleted(type: Type): void; }