typir
Version:
General purpose type checking library
233 lines (205 loc) • 11.1 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.
******************************************************************************/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { isType, Type } from '../graph/type-node.js';
import { TypeInitializer } from '../initialization/type-initializer.js';
import { InferenceRuleNotApplicable, TypeInferenceRule, TypeInferenceRuleOptions } from '../services/inference.js';
import { ValidationProblemAcceptor, ValidationRule, ValidationRuleOptions } from '../services/validation.js';
import { TypirSpecifics, TypirServices } from '../typir.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.
* This approach makes it easier to introduce additional errors by users of Typir, compared to a union type, e.g. export type TypirProblem = ValueConflict | IndexedTypeConflict | ...
*/
export interface TypirProblem {
readonly $problem: string;
}
export function isSpecificTypirProblem(problem: unknown, $problem: string): problem is TypirProblem {
return typeof problem === 'object' && problem !== null && ((problem as TypirProblem).$problem === $problem);
}
export type Types = Type | Type[];
export type Names = string | string[];
export type TypeInitializers<T extends Type, Specifics extends TypirSpecifics> = TypeInitializer<T, Specifics> | Array<TypeInitializer<T, Specifics>>;
export type NameTypePair = {
name: string;
type: Type;
}
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);
}
//
// Utilities for validations
//
/** A pair of a rule for type inference with its additional options. */
export interface ValidationRuleWithOptions<Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType']> {
rule: ValidationRule<Specifics, T>;
options: Partial<ValidationRuleOptions>;
}
export function bindValidateCurrentTypeRule<TypeType extends Type, Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType']>(
rule: InferCurrentTypeRule<TypeType, Specifics, T>, type: TypeType
): ValidationRuleWithOptions<Specifics, T> | undefined {
// check the given rule
checkRule(rule); // fail early
if (toArray(rule.validation).length <= 0) { // there are no checks => don't create a validation rule!
return undefined;
}
// create a single validation rule with options
// (This is more efficient than having one validation rule for each check, since 'filter' and 'match' are checked multiple times in that case.)
return {
rule: (languageNode, accept, typir) => {
// when this validation rule is executed, it is already ensured, that the (non-undefined) language key of rule and language node fit!
if (rule.filter !== undefined && rule.filter(languageNode) === false) {
return; // if specified, the filter needs to accept the current language node
}
if (rule.matching !== undefined && rule.matching(languageNode, type) === false) {
return; // if specified, the current language node needs to match the condition of the inference rule
}
// since the current language node fits to this inference rule, validate it according
for (const validationRule of toArray(rule.validation)) {
validationRule(languageNode, type, accept, typir);
}
},
options: {
languageKey: rule.languageKey,
boundToType: type,
}
};
}
/**
* These options are used for pre-defined valiations in order to enable the user to decide,
* how the created pre-defined valiation should be registered.
*/
export interface RegistrationOptions {
/**
* 'MYSELF' indicates, that the caller is responsible to register the validation rule,
* otherwise the given options are used to register the return validation rule now.
*/
registration: 'MYSELF' | Partial<ValidationRuleOptions>;
}
//
// Utilities for type inference
//
/** A pair of a rule for type inference with its additional options. */
export interface InferenceRuleWithOptions<Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType']> {
rule: TypeInferenceRule<Specifics, T>;
options: Partial<TypeInferenceRuleOptions>;
}
export function optionsBoundToType<T extends Partial<TypeInferenceRuleOptions> | Partial<ValidationRuleOptions>>(options: T, type: Type | undefined): T {
return {
...options,
boundToType: type,
};
}
export function ruleWithOptionsBoundToType<
Specifics extends TypirSpecifics,
T extends Specifics['LanguageType'] = Specifics['LanguageType'],
>(rule: InferenceRuleWithOptions<Specifics, T>, type: Type | undefined): InferenceRuleWithOptions<Specifics, T> {
return {
rule: rule.rule,
options: optionsBoundToType(rule.options, type),
};
}
/**
* An inference rule which is dedicated for inferrring a certain type.
* This utility type is often used for inference rules which are annotated to the declaration of a type.
* At least one of the properties needs to be specified.
*/
export interface InferCurrentTypeRule<
TypeType extends Type,
Specifics extends TypirSpecifics,
T extends Specifics['LanguageType'] = Specifics['LanguageType'],
> {
languageKey?: string | string[];
filter?: (languageNode: Specifics['LanguageType']) => languageNode is T;
matching?: (languageNode: T, typeToInfer: TypeType) => boolean;
/**
* This validation will be applied to all language nodes for which the current type is inferred according to this inference rule.
* This validation is specific for this inference rule and this inferred type.
*/
validation?: InferCurrentTypeValidationRule<TypeType, Specifics, T> | Array<InferCurrentTypeValidationRule<TypeType, Specifics, T>>;
skipThisRuleIfThisTypeAlreadyExists?: boolean | ((existingType: TypeType) => boolean); // default is false
}
export type InferCurrentTypeValidationRule<
TypeType extends Type,
Specifics extends TypirSpecifics,
T extends Specifics['LanguageType'] = Specifics['LanguageType'],
> =
(languageNode: T, inferredType: TypeType, accept: ValidationProblemAcceptor<Specifics>, typir: TypirServices<Specifics>) => void;
export function skipInferenceRuleForExistingType<TypeType extends Type, Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType']>(
inferenceRule: InferCurrentTypeRule<TypeType, Specifics, T>, newType: TypeType, existingType: TypeType
): boolean {
if (newType !== existingType) {
const skipRuleForExisting = inferenceRule.skipThisRuleIfThisTypeAlreadyExists;
// don't create (additional) rules for the already existing type
return skipRuleForExisting === true || (typeof skipRuleForExisting === 'function' && skipRuleForExisting(existingType) === true);
}
return false;
}
function checkRule<TypeType extends Type, Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType']>(
rule: InferCurrentTypeRule<TypeType, Specifics, T>
): void {
if (rule.languageKey === undefined && rule.filter === undefined && rule.matching === undefined) {
throw new Error('This inference rule has none of the properties "languageKey", "filter" and "matching" at all and therefore cannot infer any type!');
}
}
export function bindInferCurrentTypeRule<TypeType extends Type, Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType']>(
rule: InferCurrentTypeRule<TypeType, Specifics, T>, type: TypeType
): InferenceRuleWithOptions<Specifics, T> {
checkRule(rule); // fail early
return {
rule: (languageNode, _typir) => {
// when this inference rule is executed, it is already ensured, that the (non-undefined) language key of rule and language node fit!
if (rule.filter !== undefined) {
if (rule.filter(languageNode)) {
if (rule.matching !== undefined) {
if (rule.matching(languageNode, type)) {
return type;
} else {
return InferenceRuleNotApplicable; // TODO or an InferenceProblem?
}
} else {
return type; // the filter was successful and there is no additional matching
}
} else {
return InferenceRuleNotApplicable; // TODO or an InferenceProblem?
}
}
if (rule.matching !== undefined) {
if (rule.matching(languageNode as T, type)) {
return type;
} else {
return InferenceRuleNotApplicable; // TODO or an InferenceProblem?
}
}
// Usually the 'languageKey' will be used only to register the inference rule, not during its execution. Therefore it is checked only here at the end:
if (rule.languageKey !== undefined) {
return type; // sometimes it is enough to filter only by the language key, e.g. in case of dedicated "IntegerLiteral"s which always have an "Integer" type
} else {
throw new Error('This inference rule has none of the properties "languageKey", "filter" and "matching" at all and therefore cannot infer any type!');
}
},
options: {
languageKey: rule.languageKey,
boundToType: type,
}
};
}
export function registerInferCurrentTypeRules<TypeType extends Type, Specifics extends TypirSpecifics>(
rules: InferCurrentTypeRule<TypeType, Specifics> | Array<InferCurrentTypeRule<TypeType, Specifics>> | undefined, type: TypeType, services: TypirServices<Specifics>
): void {
for (const ruleSingle of toArray(rules)) {
// inference
const {rule: ruleInfer, options: optionsInfer} = bindInferCurrentTypeRule(ruleSingle, type);
services.Inference.addInferenceRule(ruleInfer, optionsInfer);
// validation
const validate = bindValidateCurrentTypeRule(ruleSingle, type);
if (validate) {
services.validation.Collector.addValidationRule(validate.rule, validate.options);
}
}
// In theory, there is a small performance optimization possible:
// Register all inference rules (with the same languageKey) within a single generic inference rule (in order to keep the number of "global" inference rules small)
}