typir
Version:
General purpose type checking library
232 lines (207 loc) • 9.79 kB
text/typescript
/******************************************************************************
* 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 { GraphAlgorithms } from '../graph/graph-algorithms.js';
import { isTypeEdge, TypeEdge } from '../graph/type-edge.js';
import { TypeGraph } from '../graph/type-graph.js';
import { Type } from '../graph/type-node.js';
import { TypirSpecifics, TypirServices } from '../typir.js';
import { TypeEquality } from './equality.js';
/**
* Describes the possible conversion modes.
*
* IMPLICIT means coercion,
* e.g. in "3 + 'three'" the int value 3 is implicitly converted to the string value '3'.
* By default, this relation is transitive (this could be configured).
* Cycles are not allowed for this relation.
*
* EXPLICIT means casting,
* e.g. in "myValue as MyType" the value stored in the variable myValue is explicitly casted to MyType.
* By default, this relation is not transitive (this could be configured).
* Cycles are allowed for this relation.
*/
export type ConversionModeForSpecification =
/** The conversion is implicitly possible. In this case, the explicit conversion is possible as well (IMPLICIT => EXPLICIT). */
'IMPLICIT_EXPLICIT' |
/** The conversion is only explicitly possible */
'EXPLICIT';
export type ConversionMode =
ConversionModeForSpecification |
/** no conversion possible at all (this is the default mode) */
'NONE' |
/** a type is always self-convertible to itself (implicitly or explicitly), in this case no conversion is necessary */
'SELF';
/**
* Manages conversions between different types.
* A conversion is a directed relationship between two types.
* If a source type can be converted to a target type, the source type could be assignable to the target type (depending on the conversion mode: target := source).
*/
export interface TypeConversion {
/**
* Defines the conversion relationship between two types.
* @param from the from/source type
* @param to the to/target type
* @param mode the desired conversion relationship between the two given types
* @throws an error, if a cycle was introduced
*/
markAsConvertible(from: Type, to: Type, mode: ConversionModeForSpecification): void;
/**
* Identifies the existing conversion relationship between two given types.
* @param from the from/source type
* @param to the to/target type
* @returns the existing conversion relationship between the two given types
*/
getConversion(from: Type, to: Type): ConversionMode;
/**
* Checks whether the given two types are implicitly (or explicitly) convertible.
* @param from the from/source type
* @param to the to/target type
* @returns true if the conversion is possible, false otherwise
*/
isImplicitExplicitConvertible(from: Type, to: Type): boolean;
/**
* Checks whether the given two types are (only) explicitly convertible.
* @param from the from/source type
* @param to the to/target type
* @returns true if the conversion is possible, false otherwise
*/
isExplicitConvertible(from: Type, to: Type): boolean;
/**
* Checks whether the given two types are not convertible (and are not equals).
* @param from the from/source type
* @param to the to/target type
* @returns true if the conversion is not possible, false otherwise
*/
isNoneConvertible(from: Type, to: Type): boolean;
/**
* Checks whether the given two types are (implicitly or explicitly) convertible, since they are equal.
* @param from the from/source type
* @param to the to/target type
* @returns true if the types are equal, false otherwise
*/
isSelfConvertible(from: Type, to: Type): boolean;
/**
* Checks whether the given two types are convertible (EXPLICIT or IMPLICIT or SELF).
* @param from the from/source type
* @param to the to/target type
* @returns true if the implicit or explicit conversion is possible or the types are equal, false otherwise
*/
isConvertible(from: Type, to: Type): boolean;
/**
* Returns all other types to which the given type can be recursively converted.
* @param from the source type, which is convertible to the returned types
* @param mode only conversion rules with the given conversion mode are considered
* @returns the set of recursively reachable types for conversion ("conversion targets")
*/
getConvertibleTo(from: Type, mode: ConversionModeForSpecification): Set<Type>;
}
/**
* Design decisions:
* - Do not store transitive relationships, since they must be removed, when types of the corresponding path are removed!
* - Store only EXPLICIT and IMPLICIT relationships, since this is not required, missing edges means NONE/SELF.
*/
export class DefaultTypeConversion<Specifics extends TypirSpecifics> implements TypeConversion {
protected readonly equality: TypeEquality;
protected readonly graph: TypeGraph;
protected readonly algorithms: GraphAlgorithms;
constructor(services: TypirServices<Specifics>) {
this.equality = services.Equality;
this.graph = services.infrastructure.Graph;
this.algorithms = services.infrastructure.GraphAlgorithms;
}
markAsConvertible(from: Type, to: Type, mode: ConversionModeForSpecification): void {
let edge = this.getConversionEdge(from, to);
if (!edge) {
// create a missing edge (with the desired mode)
edge = {
$relation: ConversionEdge,
from,
to,
cachingInformation: 'LINK_EXISTS',
mode,
};
this.graph.addEdge(edge);
} else {
// update the mode
edge.mode = mode;
}
if (mode === 'IMPLICIT_EXPLICIT') {
/* check that the new edges did not introduce cycles
* if it did, the from node will be reachable via a cycle path
*/
const hasIntroducedCycle = this.existsEdgePath(from, from, mode);
if (hasIntroducedCycle) {
throw new Error(`Adding the conversion from ${from.getIdentifier()} to ${to.getIdentifier()} with mode ${mode} has introduced a cycle in the type graph.`);
}
}
}
protected isTransitive(mode: ConversionModeForSpecification): boolean {
// by default, only IMPLICIT is transitive!
return mode === 'IMPLICIT_EXPLICIT';
}
getConversion(from: Type, to: Type): ConversionMode {
// check whether the direct conversion is stored in the type graph (this is quite fast)
const edge = this.getConversionEdge(from, to);
if (edge) {
return edge.mode;
}
// special case: if both types are equal, no conversion is needed (often this check is quite fast)
if (this.equality.areTypesEqual(from, to)) {
return 'SELF';
}
// check whether there is a transitive relationship (in general, these checks are expensive)
if (this.isTransitive('EXPLICIT') && this.isTransitivelyConvertable(from, to, 'EXPLICIT')) {
return 'EXPLICIT';
}
if (this.isTransitive('IMPLICIT_EXPLICIT') && this.isTransitivelyConvertable(from, to, 'IMPLICIT_EXPLICIT')) {
return 'IMPLICIT_EXPLICIT';
}
// the default case
return 'NONE';
}
protected collectReachableTypes(from: Type, mode: ConversionModeForSpecification): Set<Type> {
return this.algorithms.collectReachableTypes(from, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode);
}
protected existsEdgePath(from: Type, to: Type, mode: ConversionModeForSpecification): boolean {
return this.algorithms.existsEdgePath(from, to, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode);
}
protected isTransitivelyConvertable(from: Type, to: Type, mode: ConversionModeForSpecification): boolean {
if (from === to) {
return true;
} else {
return(this.existsEdgePath(from, to, mode));
}
}
isImplicitExplicitConvertible(from: Type, to: Type): boolean {
return this.getConversion(from, to) === 'IMPLICIT_EXPLICIT';
}
isExplicitConvertible(from: Type, to: Type): boolean {
return this.getConversion(from, to) === 'EXPLICIT';
}
isNoneConvertible(from: Type, to: Type): boolean {
return this.getConversion(from, to) === 'NONE';
}
isSelfConvertible(from: Type, to: Type): boolean {
return this.getConversion(from, to) === 'SELF';
}
isConvertible(from: Type, to: Type): boolean {
const currentMode = this.getConversion(from, to);
return currentMode === 'IMPLICIT_EXPLICIT' || currentMode === 'EXPLICIT' || currentMode === 'SELF';
}
protected getConversionEdge(from: Type, to: Type): ConversionEdge | undefined {
return from.getOutgoingEdges<ConversionEdge>(ConversionEdge).find(edge => edge.to === to);
}
getConvertibleTo(from: Type, mode: ConversionModeForSpecification): Set<Type> {
return this.collectReachableTypes(from, mode);
}
}
export interface ConversionEdge extends TypeEdge {
readonly $relation: 'ConversionEdge';
mode: ConversionMode;
}
export const ConversionEdge = 'ConversionEdge';
export function isConversionEdge(edge: unknown): edge is ConversionEdge {
return isTypeEdge(edge) && edge.$relation === ConversionEdge;
}