From 22b305a174457915fb1b76cdbd36e13f13cfedd4 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 10 Jan 2025 14:11:40 +0100 Subject: [PATCH 01/29] some minor fixes --- examples/lox/src/language/lox-module.ts | 6 +++--- packages/typir/src/initialization/type-selector.ts | 4 ++-- packages/typir/src/typir.ts | 6 ++++-- packages/typir/src/utils/utils-type-comparison.ts | 10 +++++----- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/lox/src/language/lox-module.ts b/examples/lox/src/language/lox-module.ts index 5b9f410..c08ade9 100644 --- a/examples/lox/src/language/lox-module.ts +++ b/examples/lox/src/language/lox-module.ts @@ -18,7 +18,7 @@ import { LoxLinker } from './lox-linker.js'; */ export type LoxAddedServices = { validation: { - LoxValidator: LoxValidator + LoxValidator: LoxValidator, }, typir: LangiumServicesForTypirBinding, } @@ -38,7 +38,7 @@ export function createLoxModule(shared: LangiumSharedCoreServices): Module new LoxValidationRegistry(services), - LoxValidator: () => new LoxValidator() + LoxValidator: () => new LoxValidator(), }, // For type checking with Typir, inject and merge these modules: typir: () => inject(Module.merge( @@ -73,7 +73,7 @@ export function createLoxServices(context: DefaultSharedModuleContext): { } { const shared = inject( createDefaultSharedModule(context), - LoxGeneratedSharedModule + LoxGeneratedSharedModule, ); const Lox = inject( createDefaultCoreModule({ shared }), diff --git a/packages/typir/src/initialization/type-selector.ts b/packages/typir/src/initialization/type-selector.ts index 8100afd..cb6f29b 100644 --- a/packages/typir/src/initialization/type-selector.ts +++ b/packages/typir/src/initialization/type-selector.ts @@ -23,7 +23,7 @@ export type BasicTypeSelector = */ export type TypeSelector = | BasicTypeSelector // all base type selectors - | (() => BasicTypeSelector) // all type selectors might be given as functions as well, in order to ease delayed specifications + | (() => BasicTypeSelector) // all type selectors might be given as functions as well, in order to ease delayed specifications ; @@ -33,7 +33,7 @@ export interface TypeResolvingService { * 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 + * @returns the found type; or undefined, it there is no such type in the type system */ tryToResolve(selector: TypeSelector): T | undefined; diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index 8d035ad..204a9c3 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -104,13 +104,15 @@ export const DefaultTypirServiceModule: Module = { * Creates the TypirServices with the default module containing the default implements for Typir, which might be exchanged by the given optional customized modules. * @param customization1 optional Typir module with customizations * @param customization2 optional Typir module with customizations + * @param customization3 optional Typir module with customizations * @returns a Typir instance, i.e. the TypirServices with implementations */ export function createTypirServices( customization1: Module = {}, - customization2: Module = {} + customization2: Module = {}, + customization3: Module = {}, ): TypirServices { - return inject(DefaultTypirServiceModule, customization1, customization2); + return inject(DefaultTypirServiceModule, customization1, customization2, customization3); } /** diff --git a/packages/typir/src/utils/utils-type-comparison.ts b/packages/typir/src/utils/utils-type-comparison.ts index e52f27d..0cf4894 100644 --- a/packages/typir/src/utils/utils-type-comparison.ts +++ b/packages/typir/src/utils/utils-type-comparison.ts @@ -6,11 +6,11 @@ import { assertUnreachable } from 'langium'; import { isType, Type } from '../graph/type-node.js'; +import { Kind } from '../kinds/kind.js'; +import { InferenceProblem } from '../services/inference.js'; import { TypirServices } from '../typir.js'; import { assertTrue } from '../utils/utils.js'; -import { isSpecificTypirProblem, isNameTypePair, NameTypePair, TypirProblem } from './utils-definitions.js'; -import { InferenceProblem } from '../services/inference.js'; -import { Kind } from '../kinds/kind.js'; +import { isNameTypePair, isSpecificTypirProblem, NameTypePair, TypirProblem } from './utils-definitions.js'; export type TypeCheckStrategy = 'EQUAL_TYPE' | // the most strict checking @@ -164,11 +164,11 @@ export function checkTypes(left: TypeToCheck, right: TypeToCheck, } export function checkTypeArrays(leftTypes: TypeToCheck[], rightTypes: TypeToCheck[], - relationToCheck: (l: Type, r: Type) => (TypirProblem | undefined), checkNamesOfNameTypePairs: boolean): IndexedTypeConflict[] { + relationToCheck: (l: Type, r: Type, index: number) => (TypirProblem | undefined), checkNamesOfNameTypePairs: boolean): IndexedTypeConflict[] { const conflicts: IndexedTypeConflict[] = []; // check first common indices for (let i = 0; i < Math.min(leftTypes.length, rightTypes.length); i++) { - const currentProblems = checkTypes(leftTypes[i], rightTypes[i], relationToCheck, checkNamesOfNameTypePairs); + const currentProblems = checkTypes(leftTypes[i], rightTypes[i], (l, r) => relationToCheck(l, r, i), checkNamesOfNameTypePairs); currentProblems.forEach(p => p.propertyIndex = i); // add the index conflicts.push(...currentProblems); } From c6550d8c1db3f191b3336ba6ab2dd1c1cd6d5cd7 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 10 Jan 2025 14:13:46 +0100 Subject: [PATCH 02/29] started with some predefined language elements for testing and test utils --- .../src/test/predefined-language-nodes.ts | 122 ++++++++++++++++++ packages/typir/src/utils/test-utils.ts | 32 ++++- 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 packages/typir/src/test/predefined-language-nodes.ts diff --git a/packages/typir/src/test/predefined-language-nodes.ts b/packages/typir/src/test/predefined-language-nodes.ts new file mode 100644 index 0000000..ea6839f --- /dev/null +++ b/packages/typir/src/test/predefined-language-nodes.ts @@ -0,0 +1,122 @@ +/****************************************************************************** + * Copyright 2025 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 { InferOperatorWithMultipleOperands } from '../services/operator.js'; +import { DefaultTypeConflictPrinter } from '../services/printing.js'; + +/* eslint-disable @typescript-eslint/parameter-properties */ + +/** + * Base class for all language nodes, + * which are predefined for test cases. + */ +export abstract class TestLanguageNode { + + constructor() { + // empty + } + + print(): string { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const obj = this; + const properties = Object.entries(obj) + .map((key, value) => `${key}: ${this.printObject(value)}`) + .join(', '); + return `${this.constructor.name}(${properties})`; + } + + protected printObject(obj: unknown): string { + if (Array.isArray(obj)) { + const entries = obj.values().toArray().map(v => this.printObject(v)).join(', '); + return `[${entries}]`; + } + if (obj instanceof TestLanguageNode) { + return `${obj.print()}`; + } + return `${obj}`; + } + +} + +export abstract class TestExpressionNode extends TestLanguageNode { +} + +export abstract class TestStatementNode extends TestLanguageNode { +} + + +// TODO review: Should the following classes have "Test" as prefix for their names? + +export class IntegerLiteral extends TestExpressionNode { + constructor( + public value: number, + ) { super(); } +} +export class DoubleLiteral extends TestExpressionNode { + constructor( + public value: number, + ) { super(); } +} +export class BooleanLiteral extends TestExpressionNode { + constructor( + public value: boolean, + ) { super(); } +} +export class StringLiteral extends TestExpressionNode { + constructor( + public value: string, + ) { super(); } +} + +export class BinaryExpression extends TestExpressionNode { + constructor( + public left: TestExpressionNode, + public operator: string, + public right: TestExpressionNode, + ) { super(); } +} + + +export class Variable extends TestLanguageNode { + constructor( + public name: string, + public initialValue: TestExpressionNode, + ) { super(); } +} + + +export class AssignmentStatement extends TestStatementNode { + constructor( + public left: Variable, + public right: TestExpressionNode, + ) { super(); } +} + +export class StatementBlock extends TestStatementNode { + constructor( + public statements: TestLanguageNode[], + ) { super(); } +} + + +/* + * Some predefined utils for configuring Typir accordingly + */ + +export const InferenceRuleBinaryExpression: InferOperatorWithMultipleOperands = { + filter: node => node instanceof BinaryExpression, + matching: (node, operatorName) => node.operator === operatorName, + operands: node => [node.left, node.right], +}; + +export class TestProblemPrinter extends DefaultTypeConflictPrinter { + override printLanguageNode(languageNode: unknown, sentenceBegin?: boolean | undefined): string { + if (languageNode instanceof TestLanguageNode) { + return `${sentenceBegin ? 'T' : 't'}he language node '${languageNode.print()}'`; + } + return super.printLanguageNode(languageNode, sentenceBegin); + } +} diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index 93b1188..5ca0110 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -6,7 +6,9 @@ import { expect } from 'vitest'; import { Type } from '../graph/type-node.js'; -import { TypirServices } from '../typir.js'; +import { TestProblemPrinter } from '../test/predefined-language-nodes.js'; +import { createTypirServices, DefaultTypirServiceModule, PartialTypirServices, TypirServices } from '../typir.js'; +import { Module } from './dependency-injection.js'; /** * Testing utility to check, that exactly the expected types are in the type system. @@ -30,3 +32,31 @@ export function expectTypirTypes(services: TypirServices, filterTypes: (type: Ty expect(typeNames, `There are more types than expected: ${typeNames.join(', ')}`).toHaveLength(0); return types; } + +export function expectType(type: unknown, checkType: (t: unknown) => t is T, checkDetails: (t: T) => boolean): void { + if (checkType(type)) { + if (checkDetails(type)) { + // everything is fine + } else { + expect.fail(`'${type.getIdentifier()}' is the expected Typir type, but the details are wrong`); + } + } else { + expect.fail(`'${type}' is not the expected Typir type`); + } +} + +/** + * Creates TypirServices dedicated for testing purposes, + * with the default module containing the default implements for Typir, which might be exchanged by the given optional customized module. + * @param customizationForTesting specific customizations for the current test case + * @returns a Typir instance, i.e. the TypirServices with implementations + */ +export function createTypirServicesForTesting( + customizationForTesting: Module = {}, +): TypirServices { + return createTypirServices( + DefaultTypirServiceModule, // all default core implementations + { Printer: () => new TestProblemPrinter() }, // use the dedicated printer for TestLanguageNode's + customizationForTesting, // specific customizations for the current test case + ); +} From 67e9dbe4f642de62ef946fa70b9b94945ac6fec6 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 10 Jan 2025 14:14:40 +0100 Subject: [PATCH 03/29] assignability informs about the reason for successful assignability now --- packages/typir/src/services/assignability.ts | 22 ++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/typir/src/services/assignability.ts b/packages/typir/src/services/assignability.ts index 9a228d4..cc1dc66 100644 --- a/packages/typir/src/services/assignability.ts +++ b/packages/typir/src/services/assignability.ts @@ -22,10 +22,19 @@ export function isAssignabilityProblem(problem: unknown): problem is Assignabili return isSpecificTypirProblem(problem, AssignabilityProblem); } +export type AssignabilitySuccess = + | 'EQUAL' + | 'IMPLICIT_CONVERSION' + | 'SUB_TYPE' + ; + +export type AssignabilityResult = AssignabilitySuccess | AssignabilityProblem; + export interface TypeAssignability { // target := source; isAssignable(source: Type, target: Type): boolean; getAssignabilityProblem(source: Type, target: Type): AssignabilityProblem | undefined; + getAssignabilityResult(source: Type, target: Type): AssignabilityResult; } export class DefaultTypeAssignability implements TypeAssignability { @@ -40,24 +49,29 @@ export class DefaultTypeAssignability implements TypeAssignability { } isAssignable(source: Type, target: Type): boolean { - return this.getAssignabilityProblem(source, target) === undefined; + return isAssignabilityProblem(this.getAssignabilityProblem(source, target)) === false; } getAssignabilityProblem(source: Type, target: Type): AssignabilityProblem | undefined { + const result = this.getAssignabilityResult(source, target); + return isAssignabilityProblem(result) ? result : undefined; + } + + getAssignabilityResult(source: Type, target: Type): AssignabilityResult { // 1. are both types equal? if (this.equality.areTypesEqual(source, target)) { - return undefined; + return 'EQUAL'; } // 2. implicit conversion from source to target possible? if (this.conversion.isImplicitExplicitConvertible(source, target)) { - return undefined; + return 'IMPLICIT_CONVERSION'; } // 3. is the source a sub-type of the target? const subTypeResult = this.subtype.getSubTypeProblem(source, target); if (subTypeResult === undefined) { - return undefined; + return 'SUB_TYPE'; } else { // return the found sub-type issues return { From 92d7583a7b8a28062447f98c3bc52f2bc203fb34 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 10 Jan 2025 14:15:10 +0100 Subject: [PATCH 04/29] now sub-type relationships can be explicitly defined --- packages/typir/src/services/subtype.ts | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index aa6a45c..ab4d7b7 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -10,6 +10,7 @@ import { TypirServices } from '../typir.js'; import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; import { EdgeCachingInformation, TypeRelationshipCaching } from './caching.js'; import { TypeEdge, isTypeEdge } from '../graph/type-edge.js'; +import { toArray } from '../utils/utils.js'; export interface SubTypeProblem extends TypirProblem { $problem: 'SubTypeProblem'; @@ -44,6 +45,8 @@ export interface SubType { - terminology: "no sub-type" is not a problem in general ("it is a qualified NO"), it is just a property! This is a general issue of the current design! */ getSubTypeProblem(subType: Type, superType: Type): SubTypeProblem | undefined; + + markAsSubType(subType: Type | Type[], superType: Type | Type[]): void; } export class DefaultSubType implements SubType { @@ -131,6 +134,8 @@ export class DefaultSubType implements SubType { if (resultSuper.length <= 0) { return undefined; } + + // no sub-type relationship return { $problem: SubTypeProblem, superType, @@ -138,6 +143,33 @@ export class DefaultSubType implements SubType { subProblems: [...resultSuper, ...resultSub], // return the sub-type problems of both types }; } + + markAsSubType(subType: Type | Type[], superType: Type | Type[]): void { + const allSub = toArray(subType); + const allSuper = toArray(superType); + for (const subT of allSub) { + for (const superT of allSuper) { + this.markAsSubTypeSingle(subT, superT); + } + } + } + + protected markAsSubTypeSingle(subType: Type, superType: Type): void { + const cache = this.typeRelationships; + let edge = cache.getRelationshipUnidirectional(subType, superType, SubTypeEdge); + if (!edge) { + edge = { + $relation: SubTypeEdge, + from: subType, + to: superType, + cachingInformation: 'LINK_EXISTS', + error: undefined, + }; + } + cache.setOrUpdateUnidirectionalRelationship(edge, 'LINK_EXISTS'); + + // TODO check for cycles! + } } export interface SubTypeEdge extends TypeEdge { From 1652024a9025ce80e3cfc1d7e423f085742d139c Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 10 Jan 2025 14:16:10 +0100 Subject: [PATCH 05/29] started implementation for finding best matches of overloaded functions/operators, more test cases for assignability --- .../kinds/function/function-initializer.ts | 299 +++++++++++++----- .../typir/src/kinds/function/function-kind.ts | 14 +- packages/typir/src/services/inference.ts | 10 +- .../function/operator-overloaded.test.ts | 134 ++++++++ 4 files changed, 363 insertions(+), 94 deletions(-) create mode 100644 packages/typir/test/kinds/function/operator-overloaded.test.ts diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 13058f3..aa20386 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -6,12 +6,13 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; -import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; +import { AssignabilitySuccess, isAssignabilityProblem } from '../../services/assignability.js'; +import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule, TypeInferenceRuleWithInferringChildren } from '../../services/inference.js'; import { ValidationRule } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; import { assertType } from '../../utils/utils.js'; -import { CreateFunctionTypeDetails, FunctionKind } from './function-kind.js'; +import { CreateFunctionTypeDetails, FunctionKind, OverloadedFunctionDetails } from './function-kind.js'; import { FunctionType, isFunctionType } from './function-type.js'; export class FunctionTypeInitializer extends TypeInitializer implements TypeStateListener { @@ -38,13 +39,12 @@ export class FunctionTypeInitializer extends TypeInitializer im } // prepare the overloads - let overloaded = this.kind.mapNameTypes.get(functionName); - if (overloaded) { + if (this.kind.mapNameTypes.has(functionName)) { // do nothing } else { - overloaded = { + const overloaded: OverloadedFunctionDetails = { overloadedFunctions: [], - inference: new CompositeTypeInferenceRule(this.services), + inference: new OverloadedFunctionsTypeInferenceRule(this.services), sameOutputType: undefined, }; this.kind.mapNameTypes.set(functionName, overloaded); @@ -135,85 +135,14 @@ export class FunctionTypeInitializer extends TypeInitializer im protected createInferenceRules(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType): FunctionInferenceRules { const result: FunctionInferenceRules = {}; - const functionName = typeDetails.functionName; const mapNameTypes = this.kind.mapNameTypes; - const outputTypeForFunctionCalls = this.kind.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) { // this condition is checked here, since 'undefined' is OK, as long as it is not used; extracting this function is difficult due to TypeScripts strict rules for using 'this' - 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.inferenceForCall = { - inferTypeWithoutChildren(languageNode, _typir) { - const result = typeDetails.inferenceRuleForCalls!.filter(languageNode); - if (result) { - const matching = typeDetails.inferenceRuleForCalls!.matching(languageNode); - if (matching) { - const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); - 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 language node is slightly different - } - } else { - // the language node has a completely different purpose - } - // does not match at all - return InferenceRuleNotApplicable; - }, - inferTypeWithChildrensTypes(languageNode, actualInputTypes, typir) { - const expectedInputTypes = typeDetails.inputParameters.map(p => typir.infrastructure.TypeResolver.resolve(p.type)); - // all operands need to be assignable(! not equal) to the required types - const comparisonConflicts = checkTypeArrays(actualInputTypes, expectedInputTypes, - (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, - languageNode: languageNode, - 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); - } - }, - }; + // create inference rule for calls of the new function + if (typeDetails.inferenceRuleForCalls) { + result.inferenceForCall = new FunctionCallInferenceRule(typeDetails, functionType, mapNameTypes); } + // create validation for checking the assignability of arguments to input paramters if (typeDetails.validationForCall) { result.validationForCall = (languageNode, typir) => { if (typeDetails.inferenceRuleForCalls!.filter(languageNode) && typeDetails.inferenceRuleForCalls!.matching(languageNode)) { @@ -221,7 +150,7 @@ export class FunctionTypeInitializer extends TypeInitializer im const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); 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); + const overloadInfos = mapNameTypes.get(typeDetails.functionName); if (overloadInfos && overloadInfos.overloadedFunctions.length >= 2) { // for overloaded functions: the types of the parameters need to be inferred in order to determine an exact match // (Note that the short-cut for type inference for function calls, when all overloads return the same output type, does not work here, since the validation here is specific for this single variant!) @@ -253,7 +182,7 @@ export class FunctionTypeInitializer extends TypeInitializer im }; } - // register inference rule for the declaration of the new function + // create 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.inferenceForDeclaration = (languageNode, _typir) => { @@ -275,3 +204,207 @@ interface FunctionInferenceRules { validationForCall?: ValidationRule; inferenceForDeclaration?: TypeInferenceRule; } + + +/** 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) + */ +class FunctionCallInferenceRule implements TypeInferenceRuleWithInferringChildren { + protected readonly typeDetails: CreateFunctionTypeDetails; + protected readonly functionType: FunctionType; + protected readonly mapNameTypes: Map; + assignabilitySuccess: Array; + + constructor(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType, mapNameTypes: Map) { + this.typeDetails = typeDetails; + this.functionType = functionType; + this.mapNameTypes = mapNameTypes; + this.assignabilitySuccess = new Array(typeDetails.inputParameters.length); + } + + inferTypeWithoutChildren(languageNode: unknown, _typir: TypirServices): unknown { + this.assignabilitySuccess.fill(undefined); // reset the entries + const result = this.typeDetails.inferenceRuleForCalls!.filter(languageNode); + if (result) { + const matching = this.typeDetails.inferenceRuleForCalls!.matching(languageNode); + if (matching) { + const inputArguments = this.typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); + 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 = this.mapNameTypes.get(this.typeDetails.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 this.check(this.getOutputTypeForFunctionCalls()); + } + } else { + // there are no operands to check + return this.check(this.getOutputTypeForFunctionCalls()); + } + } else { + // the language node is slightly different + } + } else { + // the language node has a completely different purpose + } + // does not match at all + return InferenceRuleNotApplicable; + } + + inferTypeWithChildrensTypes(languageNode: unknown, actualInputTypes: Array, typir: TypirServices): Type | InferenceProblem { + const expectedInputTypes = this.typeDetails.inputParameters.map(p => typir.infrastructure.TypeResolver.resolve(p.type)); + // all operands need to be assignable(! not equal) to the required types + const comparisonConflicts = checkTypeArrays( + actualInputTypes, + expectedInputTypes, + (t1, t2, index) => { + const result = typir.Assignability.getAssignabilityResult(t1, t2); + if (isAssignabilityProblem(result)) { + return result; + } else { + // save the information equal/conversion/subtype for deciding "conflicts" of overloaded functions + this.assignabilitySuccess[index] = result; + return undefined; + } + }, + true, + ); + if (comparisonConflicts.length >= 1) { + // this function type does not match, due to assignability conflicts => return them as errors + return { + $problem: InferenceProblem, + languageNode: languageNode, + inferenceCandidate: this.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 this.check(this.getOutputTypeForFunctionCalls()); + } + } + + protected getOutputTypeForFunctionCalls(): Type | undefined { + return this.functionType.kind.getOutputTypeForFunctionCalls(this.functionType); + } + + protected check(returnType: Type | undefined): Type { + if (returnType) { // this condition is checked here, since 'undefined' is OK, as long as it is not used; extracting this function is difficult due to TypeScripts strict rules for using 'this' + return returnType; + } else { + throw new Error(`The function ${this.typeDetails.functionName} is called, but has no output type to infer.`); + } + } +} + + +export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInferenceRule { + + protected override inferTypeLogic(languageNode: unknown): Type | InferenceProblem[] { + this.checkForError(languageNode); + + // check all rules in order to search for the best-matching rule, not for the first-matching rule + const matchingOverloads: OverloadedMatch[] = []; + const collectedInferenceProblems: InferenceProblem[] = []; + for (const rules of this.inferenceRules.values()) { + for (const rule of rules) { + const result = this.executeSingleInferenceRuleLogic(rule, languageNode, collectedInferenceProblems); + if (result) { + matchingOverloads.push({ result, rule: rule as FunctionCallInferenceRule }); + } else { + // no result for this inference rule => check the next inference rules + } + } + } + + if (matchingOverloads.length <= 0) { + // no matches => return all the collected inference problems + if (collectedInferenceProblems.length <= 0) { + // document the reason, why neither a type nor inference problems are found + collectedInferenceProblems.push({ + $problem: InferenceProblem, + languageNode: languageNode, + location: 'found no applicable inference rules', + subProblems: [], + }); + } + return collectedInferenceProblems; + } else if (matchingOverloads.length === 1) { + // single match + return matchingOverloads[0].result; + } else { + // multiple matches => determine the one to return + + // 1. sort the found matches + matchingOverloads.sort((l, r) => this.compareMatchingOverloads(l, r)); + + // 2. identify the best matches at the beginning of the list + let index = 1; + while (index < matchingOverloads.length) { + if (this.compareMatchingOverloads(matchingOverloads[index - 1], matchingOverloads[index]) === 0) { + index++; // same priority + } else { + break; // lower priority => skip them + } + } + matchingOverloads.splice(index); // keep only the matches with the same highest priority + // TODO review: should we make this implementation more efficient? + + // 3. evaluate remaining best matches + if (matchingOverloads.length === 0) { + // return the single remaining match + return matchingOverloads[0].result; + } else { + // decide how to deal with multiple best matches + const result = this.handleMultipleBestMatches(matchingOverloads); + if (result) { + // return the chosen match + return result.result; + } else { + // no decision => inference is not possible + return [{ + $problem: InferenceProblem, + languageNode: languageNode, + location: `Found ${matchingOverloads.length} best matching overloads: ${matchingOverloads.map(m => m.result.getIdentifier()).join(', ')}`, + subProblems: [], // there are no real sub-problems, since the relevant overloads match ... + }]; + } + } + } + } + + protected handleMultipleBestMatches(matchingOverloads: OverloadedMatch[]): OverloadedMatch | undefined { + return matchingOverloads[0]; // by default, return the 1st best match + } + + // better matches are at the beginning of the list, i.e. better matches get values lower than zero + protected compareMatchingOverloads(match1: OverloadedMatch, match2: OverloadedMatch): number { + const cost1 = this.calculateCost(match1); + const cost2 = this.calculateCost(match2); + return cost1 === cost2 ? 0 : cost1 < cost2 ? -1 : +1; + } + + protected calculateCost(match: OverloadedMatch): number { + return match.rule.assignabilitySuccess + // equal types are better than sub-types, sub-types are better than conversions + .map(value => (value === 'EQUAL' ? 0 : value === 'SUB_TYPE' ? 1 : 2) as number) + .reduce((l, r) => l + r, 0); + } +} + +interface OverloadedMatch { + result: Type; + rule: FunctionCallInferenceRule; +} diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index 2a62629..5edbed1 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -10,13 +10,12 @@ import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference } from '../../initialization/type-reference.js'; import { TypeSelector } from '../../initialization/type-selector.js'; -import { CompositeTypeInferenceRule } from '../../services/inference.js'; import { ValidationProblem } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { NameTypePair } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy, checkTypes, checkValueForConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { Kind, isKind } from '../kind.js'; -import { FunctionTypeInitializer } from './function-initializer.js'; +import { FunctionTypeInitializer, OverloadedFunctionsTypeInferenceRule } from './function-initializer.js'; import { FunctionType, isFunctionType } from './function-type.js'; @@ -59,12 +58,15 @@ export interface CreateFunctionTypeDetails extends FunctionTypeDetails { validationForCall?: FunctionCallValidationRule, } -/** Collects all functions with the same name */ -interface OverloadedFunctionDetails { +/** + * Collects information about all functions with the same name. + * This is required to handle overloaded functions. + */ +export interface OverloadedFunctionDetails { // eslint-disable-next-line @typescript-eslint/no-explicit-any overloadedFunctions: Array>; - inference: CompositeTypeInferenceRule; // collects the inference rules for all functions with the same name - sameOutputType: Type | undefined; // if all overloaded functions with the same name have the same output/return type, this type is remembered here + inference: OverloadedFunctionsTypeInferenceRule; // collects the inference rules for all functions with the same name + sameOutputType: Type | undefined; // if all overloaded functions with the same name have the same output/return type, this type is remembered here (for performance optimization) } interface SingleFunctionDetails { diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index c544cce..2dba796 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -231,11 +231,11 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty // this rule might match => continue applying this rule // resolve the requested child types const childLanguageNodes = ruleResult; - const childTypes: Array = childLanguageNodes.map(child => this.services.Inference.inferType(child)); + const actualChildTypes: Array = childLanguageNodes.map(child => this.services.Inference.inferType(child)); // check, whether inferring the children resulted in some other inference problems const childTypeProblems: InferenceProblem[] = []; - for (let i = 0; i < childTypes.length; i++) { - const child = childTypes[i]; + for (let i = 0; i < actualChildTypes.length; i++) { + const child = actualChildTypes[i]; if (Array.isArray(child)) { childTypeProblems.push({ $problem: InferenceProblem, @@ -257,7 +257,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty return undefined; } else { // the types of all children are successfully inferred - const finalInferenceResult = rule.inferTypeWithChildrensTypes(languageNode, childTypes as Type[], this.services); + const finalInferenceResult = rule.inferTypeWithChildrensTypes(languageNode, actualChildTypes as Type[], this.services); if (isType(finalInferenceResult)) { // type is inferred! return finalInferenceResult; @@ -280,7 +280,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty // this rule is not applicable at all => ignore this rule return undefined; } else if (isType(result)) { - // the result type is already found! + // the result type is found! return result; } else if (isInferenceProblem(result)) { // found some inference problems diff --git a/packages/typir/test/kinds/function/operator-overloaded.test.ts b/packages/typir/test/kinds/function/operator-overloaded.test.ts new file mode 100644 index 0000000..d0eec16 --- /dev/null +++ b/packages/typir/test/kinds/function/operator-overloaded.test.ts @@ -0,0 +1,134 @@ +/****************************************************************************** + * 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/parameter-properties */ + +import { beforeAll, describe, expect, test } from 'vitest'; +import { AssignmentStatement, BinaryExpression, DoubleLiteral, InferenceRuleBinaryExpression, IntegerLiteral, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; +import { createTypirServicesForTesting, expectType } from '../../../src/utils/test-utils.js'; +import { InferenceRuleNotApplicable} from '../../../src/services/inference.js'; +import { ValidationMessageDetails } from '../../../src/services/validation.js'; +import { TypirServices } from '../../../src/typir.js'; +import { isPrimitiveType } from '../../../src/index.js'; + +describe('Multiple best matches for overloaded operators', () => { + let typir: TypirServices; + + beforeAll(() => { + typir = createTypirServicesForTesting(); + + // primitive types + const integerType = typir.factory.Primitives.create({ primitiveName: 'integer', inferenceRules: node => node instanceof IntegerLiteral }); + const doubleType = typir.factory.Primitives.create({ primitiveName: 'double', inferenceRules: node => node instanceof DoubleLiteral }); + const stringType = typir.factory.Primitives.create({ primitiveName: 'string', inferenceRules: node => node instanceof StringLiteral }); + + // operators + typir.factory.Operators.createBinary({ name: '+', signatures: [ // operator overloading + { left: integerType, right: integerType, return: integerType }, // 2 + 3 => 5 + { left: doubleType, right: doubleType, return: doubleType }, // 2.0 + 3.0 => 5.0 + { left: stringType, right: stringType, return: stringType }, // "2" + "3" => "23" + ], inferenceRule: InferenceRuleBinaryExpression }); + + // define relationships between types + typir.Conversion.markAsConvertible(doubleType, stringType, 'IMPLICIT_EXPLICIT'); // stringVariable := doubleValue; + typir.Subtype.markAsSubType(integerType, doubleType); // double <|--- integer + + // specify, how Typir can detect the type of a variable + typir.Inference.addInferenceRule(node => { + if (node instanceof Variable) { + return node.initialValue; // the type of the variable is the type of its initial value + } + return InferenceRuleNotApplicable; + }); + + // register a type-related validation + typir.validation.Collector.addValidationRule(node => { + if (node instanceof AssignmentStatement) { + return typir.validation.Constraints.ensureNodeIsAssignable(node.right, node.left, (actual, expected) => { message: + `The type '${actual.name}' is not assignable to the type '${expected.name}'.` }); + } + return []; + }); + }); + + test('2 + 3 => OK (both are integers)', () => { + expectOverload(new IntegerLiteral(2), new IntegerLiteral(3), 'integer'); + }); + + test('2.0 + 3.0 => OK (both are doubles)', () => { + expectOverload(new DoubleLiteral(2.0), new DoubleLiteral(3.0), 'double'); + }); + + test('"2" + "3" => OK (both are strings)', () => { + expectOverload(new StringLiteral('2'), new StringLiteral('3'), 'string'); + }); + + test('2.0 + 3 => OK (integers are doubles)', () => { + expectOverload(new DoubleLiteral(2.0), new IntegerLiteral(3), 'double'); + }); + + test('2.0 + "3" => OK (convert double to string)', () => { + expectOverload(new DoubleLiteral(2.0), new StringLiteral('3'), 'string'); + }); + + test('2 + "3" => OK (integer is sub-type of double, which is convertible to string)', () => { + expectOverload(new IntegerLiteral(2), new StringLiteral('3'), 'string'); + }); + + function expectOverload(left: TestExpressionNode, right: TestExpressionNode, typeName: 'string'|'integer'|'double'): void { + const example = new BinaryExpression(left, '+', right); + expect(typir.validation.Collector.validate(example)).toHaveLength(0); + expectType(typir.Inference.inferType(example), isPrimitiveType, type => type.getName() === typeName); + } + + + // tests all cases for assignability + + test('integer to integer', () => { + expectAssignmentValid(new IntegerLiteral(123), new IntegerLiteral(456)); + }); + test('double to integer', () => { + expectAssignmentError(new DoubleLiteral(123.0), new IntegerLiteral(456)); + }); + test('string to integer', () => { + expectAssignmentError(new StringLiteral('123'), new IntegerLiteral(456)); + }); + + test('integer to double', () => { + expectAssignmentValid(new IntegerLiteral(123), new DoubleLiteral(456.0)); + }); + test('double to double', () => { + expectAssignmentValid(new DoubleLiteral(123.0), new DoubleLiteral(456.0)); + }); + test('string to double', () => { + expectAssignmentError(new StringLiteral('123'), new DoubleLiteral(456.0)); + }); + + test('integer to string', () => { + expectAssignmentValid(new IntegerLiteral(123), new StringLiteral('456')); + }); + test('double to string', () => { + expectAssignmentValid(new DoubleLiteral(123.0), new StringLiteral('456')); + }); + test('string to string', () => { + expectAssignmentValid(new StringLiteral('123'), new StringLiteral('456')); + }); + + function expectAssignmentValid(value: TestExpressionNode, variableInitType: TestExpressionNode): void { + const variable = new Variable('v1', variableInitType); + const assignment = new AssignmentStatement(variable, value); + expect(typir.validation.Collector.validate(assignment)).toHaveLength(0); + } + + function expectAssignmentError(value: TestExpressionNode, variableInitType: TestExpressionNode): void { + const variable = new Variable('v1', variableInitType); + const assignment = new AssignmentStatement(variable, value); + const errors = typir.validation.Collector.validate(assignment); + expect(errors).toHaveLength(1); + expect(errors[0].message).includes('is not assignable to'); + } +}); + From 02b5a3d9b5760793faf0771a0ae8491bce49f4c4 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 14 Jan 2025 10:43:38 +0100 Subject: [PATCH 06/29] fixed bug --- packages/typir/src/test/predefined-language-nodes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typir/src/test/predefined-language-nodes.ts b/packages/typir/src/test/predefined-language-nodes.ts index ea6839f..0391561 100644 --- a/packages/typir/src/test/predefined-language-nodes.ts +++ b/packages/typir/src/test/predefined-language-nodes.ts @@ -30,7 +30,7 @@ export abstract class TestLanguageNode { protected printObject(obj: unknown): string { if (Array.isArray(obj)) { - const entries = obj.values().toArray().map(v => this.printObject(v)).join(', '); + const entries = Array.from(obj.values()).map(v => this.printObject(v)).join(', '); return `[${entries}]`; } if (obj instanceof TestLanguageNode) { From 7437b230d7caf95f5a8201c3269bcacfec23ad53 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 14 Jan 2025 10:44:20 +0100 Subject: [PATCH 07/29] more test cases --- packages/typir/src/utils/test-utils.ts | 2 +- .../function/operator-overloaded.test.ts | 168 +++++++++++------- 2 files changed, 105 insertions(+), 65 deletions(-) diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index 5ca0110..8b58df8 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -33,7 +33,7 @@ export function expectTypirTypes(services: TypirServices, filterTypes: (type: Ty return types; } -export function expectType(type: unknown, checkType: (t: unknown) => t is T, checkDetails: (t: T) => boolean): void { +export function expectToBeType(type: unknown, checkType: (t: unknown) => t is T, checkDetails: (t: T) => boolean): void { if (checkType(type)) { if (checkDetails(type)) { // everything is fine diff --git a/packages/typir/test/kinds/function/operator-overloaded.test.ts b/packages/typir/test/kinds/function/operator-overloaded.test.ts index d0eec16..61acde5 100644 --- a/packages/typir/test/kinds/function/operator-overloaded.test.ts +++ b/packages/typir/test/kinds/function/operator-overloaded.test.ts @@ -7,8 +7,8 @@ /* eslint-disable @typescript-eslint/parameter-properties */ import { beforeAll, describe, expect, test } from 'vitest'; -import { AssignmentStatement, BinaryExpression, DoubleLiteral, InferenceRuleBinaryExpression, IntegerLiteral, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; -import { createTypirServicesForTesting, expectType } from '../../../src/utils/test-utils.js'; +import { AssignmentStatement, BinaryExpression, BooleanLiteral, DoubleLiteral, InferenceRuleBinaryExpression, IntegerLiteral, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; +import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; import { InferenceRuleNotApplicable} from '../../../src/services/inference.js'; import { ValidationMessageDetails } from '../../../src/services/validation.js'; import { TypirServices } from '../../../src/typir.js'; @@ -24,16 +24,19 @@ describe('Multiple best matches for overloaded operators', () => { const integerType = typir.factory.Primitives.create({ primitiveName: 'integer', inferenceRules: node => node instanceof IntegerLiteral }); const doubleType = typir.factory.Primitives.create({ primitiveName: 'double', inferenceRules: node => node instanceof DoubleLiteral }); const stringType = typir.factory.Primitives.create({ primitiveName: 'string', inferenceRules: node => node instanceof StringLiteral }); + const booleanType = typir.factory.Primitives.create({ primitiveName: 'boolean', inferenceRules: node => node instanceof BooleanLiteral }); // operators typir.factory.Operators.createBinary({ name: '+', signatures: [ // operator overloading { left: integerType, right: integerType, return: integerType }, // 2 + 3 => 5 { left: doubleType, right: doubleType, return: doubleType }, // 2.0 + 3.0 => 5.0 { left: stringType, right: stringType, return: stringType }, // "2" + "3" => "23" + { left: booleanType, right: booleanType, return: booleanType }, // TRUE + TRUE => FALSE ], inferenceRule: InferenceRuleBinaryExpression }); // define relationships between types typir.Conversion.markAsConvertible(doubleType, stringType, 'IMPLICIT_EXPLICIT'); // stringVariable := doubleValue; + typir.Conversion.markAsConvertible(booleanType, integerType, 'IMPLICIT_EXPLICIT'); // integerVariable := booleanValue; typir.Subtype.markAsSubType(integerType, doubleType); // double <|--- integer // specify, how Typir can detect the type of a variable @@ -54,81 +57,118 @@ describe('Multiple best matches for overloaded operators', () => { }); }); - test('2 + 3 => OK (both are integers)', () => { - expectOverload(new IntegerLiteral(2), new IntegerLiteral(3), 'integer'); - }); - test('2.0 + 3.0 => OK (both are doubles)', () => { - expectOverload(new DoubleLiteral(2.0), new DoubleLiteral(3.0), 'double'); - }); + describe('tests all cases for assignability', () => { + test('integer to integer', () => { + expectAssignmentValid(new IntegerLiteral(123), new IntegerLiteral(456)); + }); + test('double to integer', () => { + expectAssignmentError(new DoubleLiteral(123.0), new IntegerLiteral(456)); + }); + test('string to integer', () => { + expectAssignmentError(new StringLiteral('123'), new IntegerLiteral(456)); + }); + test('boolean to integer', () => { + expectAssignmentValid(new BooleanLiteral(true), new IntegerLiteral(456)); + }); - test('"2" + "3" => OK (both are strings)', () => { - expectOverload(new StringLiteral('2'), new StringLiteral('3'), 'string'); - }); + test('integer to double', () => { + expectAssignmentValid(new IntegerLiteral(123), new DoubleLiteral(456.0)); + }); + test('double to double', () => { + expectAssignmentValid(new DoubleLiteral(123.0), new DoubleLiteral(456.0)); + }); + test('string to double', () => { + expectAssignmentError(new StringLiteral('123'), new DoubleLiteral(456.0)); + }); + test('boolean to double', () => { + expectAssignmentValid(new BooleanLiteral(true), new DoubleLiteral(456.0)); + }); - test('2.0 + 3 => OK (integers are doubles)', () => { - expectOverload(new DoubleLiteral(2.0), new IntegerLiteral(3), 'double'); - }); + test('integer to string', () => { + expectAssignmentValid(new IntegerLiteral(123), new StringLiteral('456')); + }); + test('double to string', () => { + expectAssignmentValid(new DoubleLiteral(123.0), new StringLiteral('456')); + }); + test('string to string', () => { + expectAssignmentValid(new StringLiteral('123'), new StringLiteral('456')); + }); + test('boolean to string', () => { + expectAssignmentValid(new BooleanLiteral(true), new StringLiteral('456')); + }); + + test('integer to boolean', () => { + expectAssignmentError(new IntegerLiteral(123), new BooleanLiteral(false)); + }); + test('double to boolean', () => { + expectAssignmentError(new DoubleLiteral(123.0), new BooleanLiteral(false)); + }); + test('string to boolean', () => { + expectAssignmentError(new StringLiteral('123'), new BooleanLiteral(false)); + }); + test('boolean to boolean', () => { + expectAssignmentValid(new BooleanLiteral(true), new BooleanLiteral(false)); + }); - test('2.0 + "3" => OK (convert double to string)', () => { - expectOverload(new DoubleLiteral(2.0), new StringLiteral('3'), 'string'); - }); - test('2 + "3" => OK (integer is sub-type of double, which is convertible to string)', () => { - expectOverload(new IntegerLiteral(2), new StringLiteral('3'), 'string'); + function expectAssignmentValid(value: TestExpressionNode, variableInitType: TestExpressionNode): void { + const variable = new Variable('v1', variableInitType); + const assignment = new AssignmentStatement(variable, value); + expect(typir.validation.Collector.validate(assignment)).toHaveLength(0); + } + + function expectAssignmentError(value: TestExpressionNode, variableInitType: TestExpressionNode): void { + const variable = new Variable('v1', variableInitType); + const assignment = new AssignmentStatement(variable, value); + const errors = typir.validation.Collector.validate(assignment); + expect(errors).toHaveLength(1); + expect(errors[0].message).includes('is not assignable to'); + } }); - function expectOverload(left: TestExpressionNode, right: TestExpressionNode, typeName: 'string'|'integer'|'double'): void { - const example = new BinaryExpression(left, '+', right); - expect(typir.validation.Collector.validate(example)).toHaveLength(0); - expectType(typir.Inference.inferType(example), isPrimitiveType, type => type.getName() === typeName); - } + describe('Test multiple matches for overloaded operators', () => { + test('2 + 3 => both are integers', () => { + expectOverload(new IntegerLiteral(2), new IntegerLiteral(3), 'integer'); + }); - // tests all cases for assignability + test('2.0 + 3.0 => both are doubles', () => { + expectOverload(new DoubleLiteral(2.0), new DoubleLiteral(3.0), 'double'); + }); - test('integer to integer', () => { - expectAssignmentValid(new IntegerLiteral(123), new IntegerLiteral(456)); - }); - test('double to integer', () => { - expectAssignmentError(new DoubleLiteral(123.0), new IntegerLiteral(456)); - }); - test('string to integer', () => { - expectAssignmentError(new StringLiteral('123'), new IntegerLiteral(456)); - }); + test('"2" + "3" => both are strings', () => { + expectOverload(new StringLiteral('2'), new StringLiteral('3'), 'string'); + }); - test('integer to double', () => { - expectAssignmentValid(new IntegerLiteral(123), new DoubleLiteral(456.0)); - }); - test('double to double', () => { - expectAssignmentValid(new DoubleLiteral(123.0), new DoubleLiteral(456.0)); - }); - test('string to double', () => { - expectAssignmentError(new StringLiteral('123'), new DoubleLiteral(456.0)); - }); + test('TRUE + FALSE => both are booleans', () => { + expectOverload(new BooleanLiteral(true), new BooleanLiteral(false), 'boolean'); + }); - test('integer to string', () => { - expectAssignmentValid(new IntegerLiteral(123), new StringLiteral('456')); - }); - test('double to string', () => { - expectAssignmentValid(new DoubleLiteral(123.0), new StringLiteral('456')); - }); - test('string to string', () => { - expectAssignmentValid(new StringLiteral('123'), new StringLiteral('456')); + test('2 + TRUE => convert boolean to integer', () => { + expectOverload(new IntegerLiteral(2), new BooleanLiteral(true), 'integer'); + }); + + test('2.0 + 3 => integers are doubles', () => { + expectOverload(new DoubleLiteral(2.0), new IntegerLiteral(3), 'double'); + }); + + test('2.0 + "3" => convert double to string', () => { + expectOverload(new DoubleLiteral(2.0), new StringLiteral('3'), 'string'); + }); + + test('2 + "3" => integer is sub-type of double, which is convertible to string', () => { + expectOverload(new IntegerLiteral(2), new StringLiteral('3'), 'string'); + }); + + + function expectOverload(left: TestExpressionNode, right: TestExpressionNode, typeName: 'string'|'integer'|'double'|'boolean'): void { + const example = new BinaryExpression(left, '+', right); + expect(typir.validation.Collector.validate(example)).toHaveLength(0); + const inferredType = typir.Inference.inferType(example); + expectToBeType(inferredType, isPrimitiveType, type => type.getName() === typeName); + } }); - function expectAssignmentValid(value: TestExpressionNode, variableInitType: TestExpressionNode): void { - const variable = new Variable('v1', variableInitType); - const assignment = new AssignmentStatement(variable, value); - expect(typir.validation.Collector.validate(assignment)).toHaveLength(0); - } - - function expectAssignmentError(value: TestExpressionNode, variableInitType: TestExpressionNode): void { - const variable = new Variable('v1', variableInitType); - const assignment = new AssignmentStatement(variable, value); - const errors = typir.validation.Collector.validate(assignment); - expect(errors).toHaveLength(1); - expect(errors[0].message).includes('is not assignable to'); - } }); From 20d7f4143d0dd79bc04d451e232af0e5f771c7fa Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 14 Jan 2025 11:04:58 +0100 Subject: [PATCH 08/29] new test case for conversions with cyclic rules --- .../typir/test/services/conversion.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/typir/test/services/conversion.test.ts diff --git a/packages/typir/test/services/conversion.test.ts b/packages/typir/test/services/conversion.test.ts new file mode 100644 index 0000000..007cdf2 --- /dev/null +++ b/packages/typir/test/services/conversion.test.ts @@ -0,0 +1,24 @@ +/****************************************************************************** + * Copyright 2025 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 { describe, expect, test } from 'vitest'; +import { createTypirServicesForTesting } from '../../src/index.js'; +import { TypirServices } from '../../src/typir.js'; + +describe('Testing conversion', () => { + + test('exception in case of cyclic conversion rules', () => { + const typir: TypirServices = createTypirServicesForTesting(); + const integerType = typir.factory.Primitives.create({ primitiveName: 'integer' }); + const doubleType = typir.factory.Primitives.create({ primitiveName: 'double' }); + + // define cyclic relationships between types + typir.Conversion.markAsConvertible(integerType, doubleType, 'IMPLICIT_EXPLICIT'); + expect(() => typir.Conversion.markAsConvertible(doubleType, integerType, 'IMPLICIT_EXPLICIT')) + .toThrowError('Adding the conversion from double to integer with mode IMPLICIT_EXPLICIT has introduced a cycle in the type graph.'); + }); + +}); From cc8874db5dd93c5f31ff00c4d1560975297d54af Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 17 Jan 2025 19:18:32 +0100 Subject: [PATCH 09/29] extend conversion API to return all types to convert to --- packages/typir/src/services/conversion.ts | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/typir/src/services/conversion.ts b/packages/typir/src/services/conversion.ts index d0226d6..ea61e9a 100644 --- a/packages/typir/src/services/conversion.ts +++ b/packages/typir/src/services/conversion.ts @@ -93,6 +93,14 @@ export interface TypeConversion { * @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; } /** @@ -176,6 +184,28 @@ export class DefaultTypeConversion implements TypeConversion { return 'NONE'; } + protected collectReachableTypes(from: Type, mode: ConversionModeForSpecification): Set { + const result: Set = new Set(); + const remainingToCheck: Type[] = [from]; + + while (remainingToCheck.length > 0) { + const current = remainingToCheck.pop()!; + const outgoingEdges = current.getOutgoingEdges(ConversionEdge); + for (const edge of outgoingEdges) { + if (edge.mode === mode) { + if (result.has(edge.to)) { + // already checked + } else { + result.add(edge.to); // this type is reachable + remainingToCheck.push(edge.to); // check it for recursive conversions + } + } + } + } + + return result; + } + protected existsEdgePath(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { const visited: Set = new Set(); const stack: Type[] = [from]; @@ -238,6 +268,10 @@ export class DefaultTypeConversion implements TypeConversion { protected getConversionEdge(from: Type, to: Type): ConversionEdge | undefined { return from.getOutgoingEdges(ConversionEdge).find(edge => edge.to === to); } + + getConvertibleTo(from: Type, mode: ConversionModeForSpecification): Set { + return this.collectReachableTypes(from, mode); + } } export interface ConversionEdge extends TypeEdge { From bf7c90d2ba08a6e9c9ea69b2f1396864b7d01589 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 17 Jan 2025 19:40:23 +0100 Subject: [PATCH 10/29] moved the existing graph algorithms into its own dedicated service in order to reuse and to customize them --- packages/typir/src/graph/graph-algorithms.ts | 79 ++++++++++++++++++++ packages/typir/src/services/conversion.ts | 54 ++----------- packages/typir/src/typir.ts | 3 + 3 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 packages/typir/src/graph/graph-algorithms.ts diff --git a/packages/typir/src/graph/graph-algorithms.ts b/packages/typir/src/graph/graph-algorithms.ts new file mode 100644 index 0000000..49ea2e2 --- /dev/null +++ b/packages/typir/src/graph/graph-algorithms.ts @@ -0,0 +1,79 @@ +/****************************************************************************** + * Copyright 2025 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 { TypirServices } from '../typir.js'; +import { TypeEdge } from './type-edge.js'; +import { TypeGraph } from './type-graph.js'; +import { Type } from './type-node.js'; + +export interface GraphAlgorithms { + collectReachableTypes(from: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): Set; + existsEdgePath(from: Type, to: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): boolean; +} + +export class DefaultGraphAlgorithms implements GraphAlgorithms { + protected readonly graph: TypeGraph; + + constructor(services: TypirServices) { + this.graph = services.infrastructure.Graph; + } + + collectReachableTypes(from: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): Set { + const result: Set = new Set(); + const remainingToCheck: Type[] = [from]; + + while (remainingToCheck.length > 0) { + const current = remainingToCheck.pop()!; + const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r)); + for (const edge of outgoingEdges) { + if (filterEdges === undefined || filterEdges(edge)) { + if (result.has(edge.to)) { + // already checked + } else { + result.add(edge.to); // this type is reachable + remainingToCheck.push(edge.to); // check it for recursive conversions + } + } + } + } + + return result; + } + + existsEdgePath(from: Type, to: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): boolean { + const visited: Set = new Set(); + const stack: Type[] = [from]; + + while (stack.length > 0) { + const current = stack.pop()!; + visited.add(current); + + const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r)); + for (const edge of outgoingEdges) { + if (filterEdges === undefined || filterEdges(edge)) { + if (edge.to === to) { + /* It was possible to reach our goal type using this path. + * Base case that also catches the case in which start and end are the same + * (is there a cycle?). Therefore it is allowed to have been "visited". + * True will only be returned if there is a real path (cycle) made up of edges + */ + return true; + } + if (!visited.has(edge.to)) { + /* The target node of this edge has not been visited before and is also not our goal node + * Add it to the stack and investigate this path later. + */ + stack.push(edge.to); + } + } + } + } + + // Fall through means that we could not reach the goal type + return false; + } + +} diff --git a/packages/typir/src/services/conversion.ts b/packages/typir/src/services/conversion.ts index ea61e9a..d7b5a74 100644 --- a/packages/typir/src/services/conversion.ts +++ b/packages/typir/src/services/conversion.ts @@ -4,6 +4,7 @@ * 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'; @@ -111,10 +112,12 @@ export interface TypeConversion { export class DefaultTypeConversion implements TypeConversion { protected readonly equality: TypeEquality; protected readonly graph: TypeGraph; + protected readonly algorithms: GraphAlgorithms; constructor(services: TypirServices) { this.equality = services.Equality; this.graph = services.infrastructure.Graph; + this.algorithms = services.infrastructure.GraphAlgorithms; } markAsConvertible(from: Type | Type[], to: Type | Type[], mode: ConversionModeForSpecification): void { @@ -185,58 +188,11 @@ export class DefaultTypeConversion implements TypeConversion { } protected collectReachableTypes(from: Type, mode: ConversionModeForSpecification): Set { - const result: Set = new Set(); - const remainingToCheck: Type[] = [from]; - - while (remainingToCheck.length > 0) { - const current = remainingToCheck.pop()!; - const outgoingEdges = current.getOutgoingEdges(ConversionEdge); - for (const edge of outgoingEdges) { - if (edge.mode === mode) { - if (result.has(edge.to)) { - // already checked - } else { - result.add(edge.to); // this type is reachable - remainingToCheck.push(edge.to); // check it for recursive conversions - } - } - } - } - - return result; + return this.algorithms.collectReachableTypes(from, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode); } protected existsEdgePath(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { - const visited: Set = new Set(); - const stack: Type[] = [from]; - - while (stack.length > 0) { - const current = stack.pop()!; - visited.add(current); - - const outgoingEdges = current.getOutgoingEdges(ConversionEdge); - for (const edge of outgoingEdges) { - if (edge.mode === mode) { - if (edge.to === to) { - /* It was possible to reach our goal type using this path. - * Base case that also catches the case in which start and end are the same - * (is there a cycle?). Therefore it is allowed to have been "visited". - * True will only be returned if there is a real path (cycle) made up of edges - */ - return true; - } - if (!visited.has(edge.to)) { - /* The target node of this edge has not been visited before and is also not our goal node - * Add it to the stack and investigate this path later. - */ - stack.push(edge.to); - } - } - } - } - - // Fall through means that we could not reach the goal type - return false; + return this.algorithms.existsEdgePath(from, to, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode); } protected isTransitivelyConvertable(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index 204a9c3..a69c52d 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -4,6 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { DefaultGraphAlgorithms, GraphAlgorithms } from './graph/graph-algorithms.js'; import { TypeGraph } from './graph/type-graph.js'; import { DefaultTypeResolver, TypeResolvingService } from './initialization/type-selector.js'; import { BottomFactoryService, BottomKind, BottomKindName } from './kinds/bottom/bottom-kind.js'; @@ -65,6 +66,7 @@ export type TypirServices = { }; readonly infrastructure: { readonly Graph: TypeGraph; + readonly GraphAlgorithms: GraphAlgorithms; readonly Kinds: KindRegistry; readonly TypeResolver: TypeResolvingService; }; @@ -95,6 +97,7 @@ export const DefaultTypirServiceModule: Module = { }, infrastructure: { Graph: () => new TypeGraph(), + GraphAlgorithms: (services) => new DefaultGraphAlgorithms(services), Kinds: (services) => new DefaultKindRegistry(services), TypeResolver: (services) => new DefaultTypeResolver(services), }, From 0d0283c12521d75fba78f1e8d96eaa13adc65f3e Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 20 Jan 2025 15:43:01 +0100 Subject: [PATCH 11/29] reworked the management of sub-type relationships: use explicit edges in the type graph, exploit them for advanced assignability checks (WIP) --- .../typir/src/kinds/bottom/bottom-type.ts | 36 ++++- packages/typir/src/kinds/class/class-type.ts | 16 +- .../typir/src/kinds/class/top-class-type.ts | 38 ++++- .../fixed-parameters/fixed-parameters-type.ts | 6 +- .../typir/src/kinds/function/function-type.ts | 6 +- .../kinds/multiplicity/multiplicity-type.ts | 10 +- .../src/kinds/primitive/primitive-type.ts | 6 +- packages/typir/src/kinds/top/top-type.ts | 36 ++++- packages/typir/src/services/assignability.ts | 21 ++- packages/typir/src/services/subtype.ts | 146 +++++++----------- 10 files changed, 214 insertions(+), 107 deletions(-) diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 4223b24..2203deb 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -5,19 +5,49 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; import { BottomKind, BottomTypeDetails, isBottomKind } from './bottom-kind.js'; +import { TypeGraphListener } from '../../graph/type-graph.js'; +import { TypeEdge } from '../../graph/type-edge.js'; -export class BottomType extends Type { +export class BottomType extends Type implements TypeGraphListener { override readonly kind: BottomKind; constructor(kind: BottomKind, identifier: string, typeDetails: BottomTypeDetails) { super(identifier, typeDetails); this.kind = kind; this.defineTheInitializationProcessOfThisType({}); // no preconditions + + // ensure, that this Bottom type is a sub-type of all (other) types: + const graph = kind.services.infrastructure.Graph; + graph.getAllRegisteredTypes().forEach(t => this.markAsSubType(t)); // the already existing types + graph.addListener(this); // all upcomping types + } + + override dispose(): void { + this.kind.services.infrastructure.Graph.removeListener(this); + } + + protected markAsSubType(type: Type): void { + if (type !== this) { + this.kind.services.Subtype.markAsSubType(this, type); + } + } + + addedType(type: Type, _key: string): void { + this.markAsSubType(type); + } + removedType(_type: Type, _key: string): void { + // empty + } + addedEdge(_edge: TypeEdge): void { + // empty + } + removedEdge(_edge: TypeEdge): void { + // empty } override getName(): string { @@ -53,8 +83,10 @@ export class BottomType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType: subType, + result: false, subProblems: [createKindConflict(this, subType)], }]; } diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index 2144b7b..2ddcb92 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypeReference } from '../../initialization/type-reference.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; @@ -55,13 +55,15 @@ export class ClassType extends Type { const superRef = new TypeReference(superr, kind.services); 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); + // after the super-class is complete ... + superType.subClasses.push(thisType); // register this class as sub-class for that super-class + kind.services.Subtype.markAsSubType(thisType, superType); // register the sub-type relationship in the type graph }, 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); + // if the superType gets invalid ... + superType.subClasses.splice(superType.subClasses.indexOf(thisType), 1); // de-register this class as sub-class of the super-class + // TODO unmark sub-type relationship (or already done automatically, since the type is removed from the graph?? gibt es noch andere Möglichkeiten eine Reference zu invalidieren außer dass der Type entfernt wurde??) } else { // initially do nothing } @@ -185,8 +187,10 @@ export class ClassType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(this, superType)], }]; } @@ -198,8 +202,10 @@ export class ClassType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType, + result: false, subProblems: [createKindConflict(subType, this)], }]; } diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 88be3b3..695af56 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -5,20 +5,50 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; import { TopClassKind, TopClassTypeDetails, isTopClassKind } from './top-class-kind.js'; import { isClassType } from './class-type.js'; +import { TypeGraphListener } from '../../graph/type-graph.js'; +import { TypeEdge } from '../../graph/type-edge.js'; -export class TopClassType extends Type { +export class TopClassType extends Type implements TypeGraphListener { override readonly kind: TopClassKind; constructor(kind: TopClassKind, identifier: string, typeDetails: TopClassTypeDetails) { super(identifier, typeDetails); this.kind = kind; this.defineTheInitializationProcessOfThisType({}); // no preconditions + + // ensure, that all (other) Class types are a sub-type of this TopClass type: + const graph = kind.services.infrastructure.Graph; + graph.getAllRegisteredTypes().forEach(t => this.markAsSubType(t)); // the already existing types + graph.addListener(this); // all upcomping types + } + + override dispose(): void { + this.kind.services.infrastructure.Graph.removeListener(this); + } + + protected markAsSubType(type: Type): void { + if (type !== this && isClassType(type)) { + this.kind.services.Subtype.markAsSubType(type, this); + } + } + + addedType(type: Type, _key: string): void { + this.markAsSubType(type); + } + removedType(_type: Type, _key: string): void { + // empty + } + addedEdge(_edge: TypeEdge): void { + // empty + } + removedEdge(_edge: TypeEdge): void { + // empty } override getName(): string { @@ -49,8 +79,10 @@ export class TopClassType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(superType, this)], }]; } @@ -63,8 +95,10 @@ export class TopClassType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType, + result: false, subProblems: [createKindConflict(this, subType)], }]; } diff --git a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts index 2f48e38..5f08a6c 100644 --- a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts +++ b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, checkTypeArrays, createKindConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; @@ -83,8 +83,10 @@ export class FixedParameterType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(this, superType)], }]; } @@ -96,8 +98,10 @@ export class FixedParameterType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType, + result: false, subProblems: [createKindConflict(subType, this)], }]; } diff --git a/packages/typir/src/kinds/function/function-type.ts b/packages/typir/src/kinds/function/function-type.ts index d9c3ad3..714530b 100644 --- a/packages/typir/src/kinds/function/function-type.ts +++ b/packages/typir/src/kinds/function/function-type.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { Type, isType } from '../../graph/type-node.js'; import { TypeReference } from '../../initialization/type-reference.js'; import { TypirProblem, NameTypePair } from '../../utils/utils-definitions.js'; @@ -131,8 +131,10 @@ export class FunctionType extends Type { } return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(this, superType)], }]; } @@ -143,8 +145,10 @@ export class FunctionType extends Type { } return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType, + result: false, subProblems: [createKindConflict(subType, this)], }]; } diff --git a/packages/typir/src/kinds/multiplicity/multiplicity-type.ts b/packages/typir/src/kinds/multiplicity/multiplicity-type.ts index 8bcf574..c180bc3 100644 --- a/packages/typir/src/kinds/multiplicity/multiplicity-type.ts +++ b/packages/typir/src/kinds/multiplicity/multiplicity-type.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { isSubTypeProblem, SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, createKindConflict } from '../../utils/utils-type-comparison.js'; @@ -62,8 +62,10 @@ export class MultiplicityType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(this, superType)], }]; } @@ -75,8 +77,10 @@ export class MultiplicityType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType, + result: false, subProblems: [createKindConflict(subType, this)], }]; } @@ -88,8 +92,8 @@ export class MultiplicityType extends Type { conflicts.push(...checkValueForConflict(subType.getLowerBound(), superType.getLowerBound(), 'lower bound', this.kind.isBoundGreaterEquals)); conflicts.push(...checkValueForConflict(subType.getUpperBound(), superType.getUpperBound(), 'upper bound', this.kind.isBoundGreaterEquals)); // check the constrained type - const constrainedTypeConflict = this.kind.services.Subtype.getSubTypeProblem(subType.getConstrainedType(), superType.getConstrainedType()); - if (constrainedTypeConflict !== undefined) { + const constrainedTypeConflict = this.kind.services.Subtype.getSubTypeResult(subType.getConstrainedType(), superType.getConstrainedType()); + if (isSubTypeProblem(constrainedTypeConflict)) { conflicts.push(constrainedTypeConflict); } return conflicts; diff --git a/packages/typir/src/kinds/primitive/primitive-type.ts b/packages/typir/src/kinds/primitive/primitive-type.ts index 840963d..b39ee32 100644 --- a/packages/typir/src/kinds/primitive/primitive-type.ts +++ b/packages/typir/src/kinds/primitive/primitive-type.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, createKindConflict } from '../../utils/utils-type-comparison.js'; @@ -47,8 +47,10 @@ export class PrimitiveType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(this, superType)], }]; } @@ -60,8 +62,10 @@ export class PrimitiveType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType: this, subType, + result: false, subProblems: [createKindConflict(subType, this)], }]; } diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index 13d1942..0df9af0 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -5,19 +5,49 @@ ******************************************************************************/ import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem } from '../../services/subtype.js'; +import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; import { TopKind, TopTypeDetails, isTopKind } from './top-kind.js'; +import { TypeGraphListener } from '../../graph/type-graph.js'; +import { TypeEdge } from '../../graph/type-edge.js'; -export class TopType extends Type { +export class TopType extends Type implements TypeGraphListener { override readonly kind: TopKind; constructor(kind: TopKind, identifier: string, typeDetails: TopTypeDetails) { super(identifier, typeDetails); this.kind = kind; this.defineTheInitializationProcessOfThisType({}); // no preconditions + + // ensure, that all (other) types are a sub-type of this Top type: + const graph = kind.services.infrastructure.Graph; + graph.getAllRegisteredTypes().forEach(t => this.markAsSubType(t)); // the already existing types + graph.addListener(this); // all upcomping types + } + + override dispose(): void { + this.kind.services.infrastructure.Graph.removeListener(this); + } + + protected markAsSubType(type: Type): void { + if (type !== this) { + this.kind.services.Subtype.markAsSubType(type, this); + } + } + + addedType(type: Type, _key: string): void { + this.markAsSubType(type); + } + removedType(_type: Type, _key: string): void { + // empty + } + addedEdge(_edge: TypeEdge): void { + // empty + } + removedEdge(_edge: TypeEdge): void { + // empty } override getName(): string { @@ -48,8 +78,10 @@ export class TopType extends Type { } else { return [{ $problem: SubTypeProblem, + $result: SubTypeResult, superType, subType: this, + result: false, subProblems: [createKindConflict(superType, this)], }]; } diff --git a/packages/typir/src/services/assignability.ts b/packages/typir/src/services/assignability.ts index cc1dc66..924ba25 100644 --- a/packages/typir/src/services/assignability.ts +++ b/packages/typir/src/services/assignability.ts @@ -4,12 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { GraphAlgorithms } from '../graph/graph-algorithms.js'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; -import { TypeConversion } from './conversion.js'; +import { ConversionEdge, TypeConversion } from './conversion.js'; import { TypeEquality } from './equality.js'; -import { SubType } from './subtype.js'; +import { isSubTypeSuccess, SubType, SubTypeEdge } from './subtype.js'; export interface AssignabilityProblem extends TypirProblem { $problem: 'AssignabilityProblem'; @@ -37,15 +38,21 @@ export interface TypeAssignability { getAssignabilityResult(source: Type, target: Type): AssignabilityResult; } + +/** + * This implementation for assignability checks step-by-step (1) equality, (2) implicit conversion, and (3) sub-type relationships of the source and target type. + */ export class DefaultTypeAssignability implements TypeAssignability { protected readonly conversion: TypeConversion; protected readonly subtype: SubType; protected readonly equality: TypeEquality; + protected readonly algorithms: GraphAlgorithms; constructor(services: TypirServices) { this.conversion = services.Conversion; this.subtype = services.Subtype; this.equality = services.Equality; + this.algorithms = services.infrastructure.GraphAlgorithms; } isAssignable(source: Type, target: Type): boolean { @@ -69,9 +76,15 @@ export class DefaultTypeAssignability implements TypeAssignability { } // 3. is the source a sub-type of the target? - const subTypeResult = this.subtype.getSubTypeProblem(source, target); - if (subTypeResult === undefined) { + const subTypeResult = this.subtype.getSubTypeResult(source, target); + if (isSubTypeSuccess(subTypeResult)) { return 'SUB_TYPE'; + } + + // 4. any path of implicit conversion and sub-type relationships + const graphSearch = this.algorithms.existsEdgePath(source, target, [ConversionEdge, SubTypeEdge]); + if (graphSearch) { + return 'SUB_TYPE'; // TODO path } else { // return the found sub-type issues return { diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index ab4d7b7..3b34a8e 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -4,26 +4,44 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { assertUnreachable } from 'langium'; +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 { TypirServices } from '../typir.js'; -import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; -import { EdgeCachingInformation, TypeRelationshipCaching } from './caching.js'; -import { TypeEdge, isTypeEdge } from '../graph/type-edge.js'; +import { TypirProblem } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; export interface SubTypeProblem extends TypirProblem { $problem: 'SubTypeProblem'; + $result: 'SubTypeResult'; superType: Type; subType: Type; + result: false; subProblems: TypirProblem[]; // might be empty } export const SubTypeProblem = 'SubTypeProblem'; -export function isSubTypeProblem(problem: unknown): problem is SubTypeProblem { - return isSpecificTypirProblem(problem, SubTypeProblem); +export function isSubTypeProblem(result: unknown): result is SubTypeProblem { + return isSubTypeResult(result) && result.result === false; +} + +export interface SubTypeSuccess { + $result: 'SubTypeResult'; + superType: Type; + subType: Type; + result: true; + path: SubTypeEdge[]; +} +export function isSubTypeSuccess(result: unknown): result is SubTypeSuccess { + return isSubTypeResult(result) && result.result === true; +} + +export type SubTypeResult = SubTypeSuccess | SubTypeProblem; +export const SubTypeResult = 'SubTypeResult'; +export function isSubTypeResult(result: unknown): result is SubTypeResult { + return typeof result === 'object' && result !== null && ((result as SubTypeResult).$result === SubTypeResult); } -// TODO new feature: allow to mark arbitrary types with a sub-type edge! (similar to conversion!) /** * Analyzes, whether there is a sub type-relationship between two types. @@ -45,103 +63,58 @@ export interface SubType { - terminology: "no sub-type" is not a problem in general ("it is a qualified NO"), it is just a property! This is a general issue of the current design! */ getSubTypeProblem(subType: Type, superType: Type): SubTypeProblem | undefined; + getSubTypeResult(subType: Type, superType: Type): SubTypeResult; markAsSubType(subType: Type | Type[], superType: Type | Type[]): void; } + +/** + * The default implementation for the SubType service. + * This implementation does not cache any computed sub-type-relationships. + */ export class DefaultSubType implements SubType { - protected readonly typeRelationships: TypeRelationshipCaching; + protected readonly graph: TypeGraph; + protected readonly algorithms: GraphAlgorithms; constructor(services: TypirServices) { - this.typeRelationships = services.caching.TypeRelationships; + this.graph = services.infrastructure.Graph; + this.algorithms = services.infrastructure.GraphAlgorithms; } isSubType(subType: Type, superType: Type): boolean { - return this.getSubTypeProblem(subType, superType) === undefined; + return isSubTypeSuccess(this.getSubTypeResult(subType, superType)); } getSubTypeProblem(subType: Type, superType: Type): SubTypeProblem | undefined { - const cache: TypeRelationshipCaching = this.typeRelationships; - const linkData = cache.getRelationshipUnidirectional(subType, superType, SubTypeEdge); - const subTypeCaching = linkData?.cachingInformation ?? 'UNKNOWN'; + const result = this.getSubTypeResult(subType, superType); + return isSubTypeProblem(result) ? result : undefined; + } - function save(subTypeCaching: EdgeCachingInformation, error: SubTypeProblem | undefined): void { - const newEdge: SubTypeEdge = { - $relation: SubTypeEdge, - from: subType, - to: superType, - cachingInformation: 'LINK_EXISTS', - error, + getSubTypeResult(subType: Type, superType: Type): SubTypeResult { + // search for a transitive sub-type relationship + if (this.algorithms.existsEdgePath(subType, superType, [SubTypeEdge])) { + return { + $result: SubTypeResult, + result: true, + subType, + superType, + path: [], // TODO insert the path here }; - cache.setOrUpdateUnidirectionalRelationship(newEdge, subTypeCaching); - } - - // skip recursive checking - if (subTypeCaching === 'PENDING') { - /** 'undefined' should be correct here ... - * - since this relationship will be checked earlier/higher/upper in the call stack again - * - since this values is not cached and therefore NOT reused in the earlier call! */ - return undefined; - } - - // the result is already known - if (subTypeCaching === 'LINK_EXISTS') { - return undefined; - } - if (subTypeCaching === 'NO_LINK') { - return { + } else { + return { + $result: SubTypeResult, $problem: SubTypeProblem, - superType, + result: false, subType, - subProblems: linkData?.error ? [linkData.error] : [], + superType, + subProblems: [], // TODO ? }; } - - // do the expensive calculation now - if (subTypeCaching === 'UNKNOWN') { - // mark the current relationship as PENDING to detect and resolve cycling checks - save('PENDING', undefined); - - // do the actual calculation - const result = this.calculateSubType(subType, superType); - - // this allows to cache results (and to re-set the PENDING state) - if (result === undefined) { - save('LINK_EXISTS', undefined); - } else { - save('NO_LINK', result); - } - return result; - } - assertUnreachable(subTypeCaching); } - protected calculateSubType(subType: Type, superType: Type): SubTypeProblem | undefined { - /** Approach: - * - delegate the calculation to the types, since they realize type-specific sub-type checking - * - Therefore, it is not necessary to add special cases for TopType and BottomType here (e.g. if (isTopType(superType)) { return undefined; }). - * - Additionally, this allows users of Typir to implement top/bottom types on their own without changing this implementation here! - */ - - // 1st delegate to the kind of the sub type - const resultSub = subType.analyzeIsSubTypeOf(superType); - if (resultSub.length <= 0) { - return undefined; - } - - // 2nd delegate to the kind of the super type - const resultSuper = superType.analyzeIsSuperTypeOf(subType); - if (resultSuper.length <= 0) { - return undefined; - } - - // no sub-type relationship - return { - $problem: SubTypeProblem, - superType, - subType, - subProblems: [...resultSuper, ...resultSub], // return the sub-type problems of both types - }; + protected getSubTypeEdge(from: Type, to: Type): SubTypeEdge | undefined { + return from.getOutgoingEdges(SubTypeEdge).find(edge => edge.to === to); } markAsSubType(subType: Type | Type[], superType: Type | Type[]): void { @@ -155,8 +128,7 @@ export class DefaultSubType implements SubType { } protected markAsSubTypeSingle(subType: Type, superType: Type): void { - const cache = this.typeRelationships; - let edge = cache.getRelationshipUnidirectional(subType, superType, SubTypeEdge); + let edge = this.getSubTypeEdge(subType, superType); if (!edge) { edge = { $relation: SubTypeEdge, @@ -165,8 +137,10 @@ export class DefaultSubType implements SubType { cachingInformation: 'LINK_EXISTS', error: undefined, }; + this.graph.addEdge(edge); + } else { + edge.cachingInformation = 'LINK_EXISTS'; } - cache.setOrUpdateUnidirectionalRelationship(edge, 'LINK_EXISTS'); // TODO check for cycles! } From 8bc6e71eadc1e4508e37b20f359f312b709caccd Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 20 Jan 2025 17:18:35 +0100 Subject: [PATCH 12/29] make the whole assignability path in the type graph explicit, introduced assignability result+success(+problem) --- packages/typir/src/graph/graph-algorithms.ts | 43 +++++++++++ .../kinds/function/function-initializer.ts | 9 ++- packages/typir/src/services/assignability.ts | 73 +++++++++++-------- packages/typir/src/services/conversion.ts | 2 +- packages/typir/src/services/subtype.ts | 24 +++--- 5 files changed, 107 insertions(+), 44 deletions(-) diff --git a/packages/typir/src/graph/graph-algorithms.ts b/packages/typir/src/graph/graph-algorithms.ts index 49ea2e2..680f73e 100644 --- a/packages/typir/src/graph/graph-algorithms.ts +++ b/packages/typir/src/graph/graph-algorithms.ts @@ -12,6 +12,7 @@ import { Type } from './type-node.js'; export interface GraphAlgorithms { collectReachableTypes(from: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): Set; existsEdgePath(from: Type, to: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): boolean; + getEdgePath(from: Type, to: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): TypeEdge[]; } export class DefaultGraphAlgorithms implements GraphAlgorithms { @@ -76,4 +77,46 @@ export class DefaultGraphAlgorithms implements GraphAlgorithms { return false; } + getEdgePath(from: Type, to: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): TypeEdge[] { + const visited: Map = new Map(); // the edge from the parent to the current node + visited.set(from, undefined); + const stack: Type[] = [from]; + + while (stack.length > 0) { + const current = stack.pop()!; + + const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r)); + for (const edge of outgoingEdges) { + if (filterEdges === undefined || filterEdges(edge)) { + if (edge.to === to) { + /* It was possible to reach our goal type using this path. + * Base case that also catches the case in which start and end are the same + * (is there a cycle?). Therefore it is allowed to have been "visited". + * True will only be returned if there is a real path (cycle) made up of edges + */ + const result: TypeEdge[] = [edge]; + // collect the path of used edges, from "to" back to "from" + let backNode = edge.from; + while (backNode !== from) { + const backEdge = visited.get(backNode)!; + result.unshift(backEdge); + backNode = backEdge.from; + } + return result; + } + if (!visited.has(edge.to)) { + /* The target node of this edge has not been visited before and is also not our goal node + * Add it to the stack and investigate this path later. + */ + stack.push(edge.to); + visited.set(edge.to, edge); + } + } + } + } + + // Fall through means that we could not reach the goal type + return []; + } + } diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index aa20386..1586e6f 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -7,11 +7,13 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { AssignabilitySuccess, isAssignabilityProblem } from '../../services/assignability.js'; +import { isConversionEdge } from '../../services/conversion.js'; import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule, TypeInferenceRuleWithInferringChildren } from '../../services/inference.js'; +import { isSubTypeEdge } from '../../services/subtype.js'; import { ValidationRule } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; -import { assertType } from '../../utils/utils.js'; +import { assertType, assertUnreachable } from '../../utils/utils.js'; import { CreateFunctionTypeDetails, FunctionKind, OverloadedFunctionDetails } from './function-kind.js'; import { FunctionType, isFunctionType } from './function-type.js'; @@ -398,8 +400,9 @@ export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInference protected calculateCost(match: OverloadedMatch): number { return match.rule.assignabilitySuccess - // equal types are better than sub-types, sub-types are better than conversions - .map(value => (value === 'EQUAL' ? 0 : value === 'SUB_TYPE' ? 1 : 2) as number) + .flatMap(s => s?.path ?? []) // collect all conversion/sub-type edges which are required to map actual types to the expected types of the parameters + // equal types (i.e. an empty path) are better than sub-types, sub-types are better than conversions + .map(edge => (isSubTypeEdge(edge) ? 1 : isConversionEdge(edge) ? 2 : assertUnreachable(edge)) as number) .reduce((l, r) => l + r, 0); } } diff --git a/packages/typir/src/services/assignability.ts b/packages/typir/src/services/assignability.ts index 924ba25..4f5cd01 100644 --- a/packages/typir/src/services/assignability.ts +++ b/packages/typir/src/services/assignability.ts @@ -7,29 +7,41 @@ import { GraphAlgorithms } from '../graph/graph-algorithms.js'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; -import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; +import { TypirProblem } from '../utils/utils-definitions.js'; import { ConversionEdge, TypeConversion } from './conversion.js'; import { TypeEquality } from './equality.js'; -import { isSubTypeSuccess, SubType, SubTypeEdge } from './subtype.js'; +import { SubType, SubTypeEdge } from './subtype.js'; export interface AssignabilityProblem extends TypirProblem { $problem: 'AssignabilityProblem'; + $result: 'AssignabilityResult'; source: Type; target: Type; + result: false; subProblems: TypirProblem[]; } export const AssignabilityProblem = 'AssignabilityProblem'; export function isAssignabilityProblem(problem: unknown): problem is AssignabilityProblem { - return isSpecificTypirProblem(problem, AssignabilityProblem); + return isAssignabilityResult(problem) && problem.result === false; } -export type AssignabilitySuccess = - | 'EQUAL' - | 'IMPLICIT_CONVERSION' - | 'SUB_TYPE' - ; +export interface AssignabilitySuccess { + $result: 'AssignabilityResult'; + source: Type; + target: Type; + result: true; + path: Array; +} +export function isAssignabilitySuccess(success: unknown): success is AssignabilitySuccess { + return isAssignabilityResult(success) && success.result === true; +} export type AssignabilityResult = AssignabilitySuccess | AssignabilityProblem; +export const AssignabilityResult = 'AssignabilityResult'; +export function isAssignabilityResult(result: unknown): result is AssignabilityResult { + return typeof result === 'object' && result !== null && ((result as AssignabilityResult).$result === AssignabilityResult); +} + export interface TypeAssignability { // target := source; @@ -67,32 +79,35 @@ export class DefaultTypeAssignability implements TypeAssignability { getAssignabilityResult(source: Type, target: Type): AssignabilityResult { // 1. are both types equal? if (this.equality.areTypesEqual(source, target)) { - return 'EQUAL'; - } - - // 2. implicit conversion from source to target possible? - if (this.conversion.isImplicitExplicitConvertible(source, target)) { - return 'IMPLICIT_CONVERSION'; - } - - // 3. is the source a sub-type of the target? - const subTypeResult = this.subtype.getSubTypeResult(source, target); - if (isSubTypeSuccess(subTypeResult)) { - return 'SUB_TYPE'; + return { + $result: AssignabilityResult, + source, + target, + result: true, + path: [], + }; } - // 4. any path of implicit conversion and sub-type relationships - const graphSearch = this.algorithms.existsEdgePath(source, target, [ConversionEdge, SubTypeEdge]); - if (graphSearch) { - return 'SUB_TYPE'; // TODO path - } else { - // return the found sub-type issues - return { - $problem: AssignabilityProblem, + // 2. any path of implicit conversion and sub-type relationships + const path = this.algorithms.getEdgePath(source, target, [ConversionEdge, SubTypeEdge]); + if (path.length >= 1) { + return { + $result: AssignabilityResult, source, target, - subProblems: [subTypeResult] + result: true, + path, // report the found path in the graph }; } + + // return the found sub-type issues + return { + $problem: AssignabilityProblem, + $result: AssignabilityResult, + source, + target, + result: false, + subProblems: [], // TODO + }; } } diff --git a/packages/typir/src/services/conversion.ts b/packages/typir/src/services/conversion.ts index d7b5a74..79fd816 100644 --- a/packages/typir/src/services/conversion.ts +++ b/packages/typir/src/services/conversion.ts @@ -192,7 +192,7 @@ export class DefaultTypeConversion implements TypeConversion { } protected existsEdgePath(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { - return this.algorithms.existsEdgePath(from, to, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode); + return this.algorithms.existsEdgePath(from, to, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode && edge.cachingInformation === 'LINK_EXISTS'); } protected isTransitivelyConvertable(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index 3b34a8e..41ebf38 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -21,8 +21,8 @@ export interface SubTypeProblem extends TypirProblem { subProblems: TypirProblem[]; // might be empty } export const SubTypeProblem = 'SubTypeProblem'; -export function isSubTypeProblem(result: unknown): result is SubTypeProblem { - return isSubTypeResult(result) && result.result === false; +export function isSubTypeProblem(problem: unknown): problem is SubTypeProblem { + return isSubTypeResult(problem) && problem.result === false; } export interface SubTypeSuccess { @@ -32,8 +32,8 @@ export interface SubTypeSuccess { result: true; path: SubTypeEdge[]; } -export function isSubTypeSuccess(result: unknown): result is SubTypeSuccess { - return isSubTypeResult(result) && result.result === true; +export function isSubTypeSuccess(success: unknown): success is SubTypeSuccess { + return isSubTypeResult(success) && success.result === true; } export type SubTypeResult = SubTypeSuccess | SubTypeProblem; @@ -58,10 +58,6 @@ export function isSubTypeResult(result: unknown): result is SubTypeResult { */ export interface SubType { isSubType(subType: Type, superType: Type): boolean; - /* TODO: - - no problem ==> sub-type relationship exists - - terminology: "no sub-type" is not a problem in general ("it is a qualified NO"), it is just a property! This is a general issue of the current design! - */ getSubTypeProblem(subType: Type, superType: Type): SubTypeProblem | undefined; getSubTypeResult(subType: Type, superType: Type): SubTypeResult; @@ -93,13 +89,14 @@ export class DefaultSubType implements SubType { getSubTypeResult(subType: Type, superType: Type): SubTypeResult { // search for a transitive sub-type relationship - if (this.algorithms.existsEdgePath(subType, superType, [SubTypeEdge])) { + const path = this.algorithms.getEdgePath(subType, superType, [SubTypeEdge]); + if (path.length >= 1) { return { $result: SubTypeResult, result: true, subType, superType, - path: [], // TODO insert the path here + path, // return the found path }; } else { return { @@ -142,8 +139,13 @@ export class DefaultSubType implements SubType { edge.cachingInformation = 'LINK_EXISTS'; } - // TODO check for cycles! + // TODO check for cycles: invalidates existing test cases + // const hasIntroducedCycle = this.algorithms.existsEdgePath(subType, subType, [SubTypeEdge], edge => edge.cachingInformation === 'LINK_EXISTS'); + // if (hasIntroducedCycle) { + // throw new Error(`Adding the sub-type relationship from ${subType.getIdentifier()} to ${superType.getIdentifier()} has introduced a cycle in the type graph.`); + // } } + } export interface SubTypeEdge extends TypeEdge { From 6862143e1c0b9574e81d6852054e9e9ad9e2e69f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 20 Jan 2025 17:56:57 +0100 Subject: [PATCH 13/29] sub-type relationships: check for cycles, controlled by a new option --- .../typir/src/kinds/bottom/bottom-type.ts | 2 +- packages/typir/src/kinds/class/class-type.ts | 3 +- .../typir/src/kinds/class/class-validation.ts | 2 +- .../typir/src/kinds/class/top-class-type.ts | 2 +- packages/typir/src/kinds/top/top-type.ts | 2 +- packages/typir/src/services/subtype.ts | 38 ++++++++++++++----- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 2203deb..948f821 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -33,7 +33,7 @@ export class BottomType extends Type implements TypeGraphListener { protected markAsSubType(type: Type): void { if (type !== this) { - this.kind.services.Subtype.markAsSubType(this, type); + this.kind.services.Subtype.markAsSubType(this, type, { checkForCycles: false }); } } diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index 2ddcb92..da6ff74 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -57,7 +57,8 @@ export class ClassType extends Type { onTypeReferenceResolved(_reference, superType) { // after the super-class is complete ... superType.subClasses.push(thisType); // register this class as sub-class for that super-class - kind.services.Subtype.markAsSubType(thisType, superType); // register the sub-type relationship in the type graph + kind.services.Subtype.markAsSubType(thisType, superType, // register the sub-type relationship in the type graph + { checkForCycles: false }); // ignore cycles in sub-super-class relationships for now, since they are reported with a dedicated validation for the user }, onTypeReferenceInvalidated(_reference, superType) { if (superType) { diff --git a/packages/typir/src/kinds/class/class-validation.ts b/packages/typir/src/kinds/class/class-validation.ts index cc54ada..e5fd00a 100644 --- a/packages/typir/src/kinds/class/class-validation.ts +++ b/packages/typir/src/kinds/class/class-validation.ts @@ -181,7 +181,7 @@ 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 + * this 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: (languageNode: unknown) => boolean): ValidationRule { diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 695af56..d605286 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -34,7 +34,7 @@ export class TopClassType extends Type implements TypeGraphListener { protected markAsSubType(type: Type): void { if (type !== this && isClassType(type)) { - this.kind.services.Subtype.markAsSubType(type, this); + this.kind.services.Subtype.markAsSubType(type, this, { checkForCycles: false }); } } diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index 0df9af0..f8ea570 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -33,7 +33,7 @@ export class TopType extends Type implements TypeGraphListener { protected markAsSubType(type: Type): void { if (type !== this) { - this.kind.services.Subtype.markAsSubType(type, this); + this.kind.services.Subtype.markAsSubType(type, this, { checkForCycles: false }); } } diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index 41ebf38..77c1b18 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -43,6 +43,14 @@ export function isSubTypeResult(result: unknown): result is SubTypeResult { } +export interface MarkSubTypeOptions { + /** If selected, it will be checked, whether cycles in sub-type relationships exists now at the involved types. + * Types which internally manage their sub-type relationships themselves usually don't check for cycles, + * since there might be already (other) cycles by user-defined types (e.g. classes with sub-super classes) and which are reported with dedicated validations. + */ + checkForCycles: boolean; +} + /** * Analyzes, whether there is a sub type-relationship between two types. * @@ -61,7 +69,7 @@ export interface SubType { getSubTypeProblem(subType: Type, superType: Type): SubTypeProblem | undefined; getSubTypeResult(subType: Type, superType: Type): SubTypeResult; - markAsSubType(subType: Type | Type[], superType: Type | Type[]): void; + markAsSubType(subType: Type | Type[], superType: Type | Type[], options?: Partial): void; } @@ -114,17 +122,27 @@ export class DefaultSubType implements SubType { return from.getOutgoingEdges(SubTypeEdge).find(edge => edge.to === to); } - markAsSubType(subType: Type | Type[], superType: Type | Type[]): void { + markAsSubType(subType: Type | Type[], superType: Type | Type[], options?: Partial): void { const allSub = toArray(subType); const allSuper = toArray(superType); + const actualOptions = this.collectMarkSubTypeOptions(options); for (const subT of allSub) { for (const superT of allSuper) { - this.markAsSubTypeSingle(subT, superT); + this.markAsSubTypeSingle(subT, superT, actualOptions); } } } - protected markAsSubTypeSingle(subType: Type, superType: Type): void { + protected collectMarkSubTypeOptions(options?: Partial): MarkSubTypeOptions { + return { + // the default values: + checkForCycles: true, + // the actually overriden values: + ...options + }; + } + + protected markAsSubTypeSingle(subType: Type, superType: Type, options: MarkSubTypeOptions): void { let edge = this.getSubTypeEdge(subType, superType); if (!edge) { edge = { @@ -139,11 +157,13 @@ export class DefaultSubType implements SubType { edge.cachingInformation = 'LINK_EXISTS'; } - // TODO check for cycles: invalidates existing test cases - // const hasIntroducedCycle = this.algorithms.existsEdgePath(subType, subType, [SubTypeEdge], edge => edge.cachingInformation === 'LINK_EXISTS'); - // if (hasIntroducedCycle) { - // throw new Error(`Adding the sub-type relationship from ${subType.getIdentifier()} to ${superType.getIdentifier()} has introduced a cycle in the type graph.`); - // } + // check for cycles + if (options.checkForCycles) { + const hasIntroducedCycle = this.algorithms.existsEdgePath(subType, subType, [SubTypeEdge], edge => edge.cachingInformation === 'LINK_EXISTS'); + if (hasIntroducedCycle) { + throw new Error(`Adding the sub-type relationship from ${subType.getIdentifier()} to ${superType.getIdentifier()} has introduced a cycle in the type graph.`); + } + } } } From e7434f3d13b59b328e797de4907d215b6cdf47e2 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 20 Jan 2025 18:00:15 +0100 Subject: [PATCH 14/29] improved README --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b4c2f8..7878d6c 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,12 @@ Typir provides these core features: Typir does intentionally _not_ include ... -- Rule engines and constraint solving +- Rule engines and constraint solving, + since type inference is calculate in a recursive manner and does not use unification/substitution - Formal proofs - External DSLs for formalizing types +- Support for dynamic type systems, which do typing during the execution of the DSL. + Typir aims at static type systems, which do typing during the writing of the DSL. ## NPM workspace @@ -109,7 +112,7 @@ typir.factory.Operators.createBinary({ name: '-', signatures: [{ left: numberTyp As we'd like to be able to convert numbers to strings implicitly, we add the following line. Note that this will for example make it possible to concatenate numbers and strings with the `+` operator, though it has no signature for a number and a string parameter in the operator definition above. ```typescript -typir.Conversion.markAsConvertible(numberType, stringType,'IMPLICIT_EXPLICIT'); +typir.Conversion.markAsConvertible(numberType, stringType, 'IMPLICIT_EXPLICIT'); ``` Furthermore we can specify how Typir should infer the variable type. We decided that the type of the variable should be the type of its initial value. Typir internally considers the inference rules for primitives and operators as well, when recursively inferring the given AstElement. From fe96e20594facba079fb666dac5724e43234f15a Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 21 Jan 2025 20:42:15 +0100 Subject: [PATCH 15/29] graph algorithms use only existing edges --- packages/typir/src/graph/graph-algorithms.ts | 10 +++++++--- packages/typir/src/services/conversion.ts | 3 ++- packages/typir/src/services/subtype.ts | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/typir/src/graph/graph-algorithms.ts b/packages/typir/src/graph/graph-algorithms.ts index 680f73e..efa8d6f 100644 --- a/packages/typir/src/graph/graph-algorithms.ts +++ b/packages/typir/src/graph/graph-algorithms.ts @@ -9,6 +9,10 @@ import { TypeEdge } from './type-edge.js'; import { TypeGraph } from './type-graph.js'; import { Type } from './type-node.js'; +/** + * Graph algorithms to do calculations on the type graph. + * All algorithms are robust regarding cycles. + */ export interface GraphAlgorithms { collectReachableTypes(from: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): Set; existsEdgePath(from: Type, to: Type, $relations: Array, filterEdges?: (edgr: TypeEdge) => boolean): boolean; @@ -30,7 +34,7 @@ export class DefaultGraphAlgorithms implements GraphAlgorithms { const current = remainingToCheck.pop()!; const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r)); for (const edge of outgoingEdges) { - if (filterEdges === undefined || filterEdges(edge)) { + if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) { if (result.has(edge.to)) { // already checked } else { @@ -54,7 +58,7 @@ export class DefaultGraphAlgorithms implements GraphAlgorithms { const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r)); for (const edge of outgoingEdges) { - if (filterEdges === undefined || filterEdges(edge)) { + if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) { if (edge.to === to) { /* It was possible to reach our goal type using this path. * Base case that also catches the case in which start and end are the same @@ -87,7 +91,7 @@ export class DefaultGraphAlgorithms implements GraphAlgorithms { const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r)); for (const edge of outgoingEdges) { - if (filterEdges === undefined || filterEdges(edge)) { + if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) { if (edge.to === to) { /* It was possible to reach our goal type using this path. * Base case that also catches the case in which start and end are the same diff --git a/packages/typir/src/services/conversion.ts b/packages/typir/src/services/conversion.ts index 79fd816..b5bf924 100644 --- a/packages/typir/src/services/conversion.ts +++ b/packages/typir/src/services/conversion.ts @@ -48,6 +48,7 @@ export interface TypeConversion { * @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 | Type[], to: Type | Type[], mode: ConversionModeForSpecification): void; @@ -192,7 +193,7 @@ export class DefaultTypeConversion implements TypeConversion { } protected existsEdgePath(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { - return this.algorithms.existsEdgePath(from, to, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode && edge.cachingInformation === 'LINK_EXISTS'); + return this.algorithms.existsEdgePath(from, to, [ConversionEdge], edge => (edge as ConversionEdge).mode === mode); } protected isTransitivelyConvertable(from: Type, to: Type, mode: ConversionModeForSpecification): boolean { diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index 77c1b18..34bc09f 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -159,7 +159,7 @@ export class DefaultSubType implements SubType { // check for cycles if (options.checkForCycles) { - const hasIntroducedCycle = this.algorithms.existsEdgePath(subType, subType, [SubTypeEdge], edge => edge.cachingInformation === 'LINK_EXISTS'); + const hasIntroducedCycle = this.algorithms.existsEdgePath(subType, subType, [SubTypeEdge]); if (hasIntroducedCycle) { throw new Error(`Adding the sub-type relationship from ${subType.getIdentifier()} to ${superType.getIdentifier()} has introduced a cycle in the type graph.`); } From 6ef3e5167e3adb1fd7e5efefe394ad294cfbeb15 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 21 Jan 2025 20:47:09 +0100 Subject: [PATCH 16/29] removed methods of types to analyze their subType relationships --- packages/typir/src/graph/type-node.ts | 22 ---------- .../typir/src/kinds/bottom/bottom-type.ts | 28 ++----------- packages/typir/src/kinds/class/class-type.ts | 37 ++--------------- .../typir/src/kinds/class/top-class-type.ts | 41 ++----------------- .../fixed-parameters/fixed-parameters-type.ts | 37 ++--------------- .../typir/src/kinds/function/function-type.ts | 35 ++-------------- .../kinds/multiplicity/multiplicity-type.ts | 36 ++-------------- .../src/kinds/primitive/primitive-type.ts | 35 +--------------- packages/typir/src/kinds/top/top-type.ts | 30 ++------------ 9 files changed, 25 insertions(+), 276 deletions(-) diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index d7b0757..624640c 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -331,28 +331,6 @@ export abstract class Type { */ abstract analyzeTypeEqualityProblems(otherType: Type): TypirProblem[]; - /** - * Analyzes, whether there is a sub type-relationship between two types. - * The difference between sub type-relationships and super type-relationships are only switched types. - * If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition). - * - * @param superType the super type, while the current type is the sub type - * @returns an empty array, if the relationship exists, otherwise some problems which might point to violations of the investigated relationship. - * These problems are presented to users in order to support them with useful information about the result of this analysis. - */ - abstract analyzeIsSubTypeOf(superType: Type): TypirProblem[]; - - /** - * Analyzes, whether there is a super type-relationship between two types. - * The difference between sub type-relationships and super type-relationships are only switched types. - * If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition). - * - * @param subType the sub type, while the current type is super type - * @returns an empty array, if the relationship exists, otherwise some problems which might point to violations of the investigated relationship. - * These problems are presented to users in order to support them with useful information about the result of this analysis. - */ - abstract analyzeIsSuperTypeOf(subType: Type): TypirProblem[]; - addIncomingEdge(edge: TypeEdge): void { const key = edge.$relation; diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 948f821..b48fef7 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -4,14 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; +import { TypeEdge } from '../../graph/type-edge.js'; +import { TypeGraphListener } from '../../graph/type-graph.js'; import { isType, Type } from '../../graph/type-node.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; import { BottomKind, BottomTypeDetails, isBottomKind } from './bottom-kind.js'; -import { TypeGraphListener } from '../../graph/type-graph.js'; -import { TypeEdge } from '../../graph/type-edge.js'; export class BottomType extends Type implements TypeGraphListener { override readonly kind: BottomKind; @@ -71,27 +70,6 @@ export class BottomType extends Type implements TypeGraphListener { } } - override analyzeIsSubTypeOf(_superType: Type): TypirProblem[] { - // a BottomType is the sub type of all types! - return []; - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - if (isBottomType(subType)) { - // special case by definition: BottomType is sub-type of BottomType - return []; - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType: subType, - result: false, - subProblems: [createKindConflict(this, subType)], - }]; - } - } - } export function isBottomType(type: unknown): type is BottomType { diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index da6ff74..afc2daf 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -4,13 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypeReference } from '../../initialization/type-reference.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; -import { checkNameTypesMap, checkValueForConflict, createKindConflict, IndexedTypeConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; -import { toArray, assertUnreachable } from '../../utils/utils.js'; +import { checkNameTypesMap, checkValueForConflict, createKindConflict, createTypeCheckStrategy, IndexedTypeConflict } from '../../utils/utils-type-comparison.js'; +import { assertUnreachable, toArray } from '../../utils/utils.js'; import { FunctionType } from '../function/function-type.js'; import { ClassKind, ClassTypeDetails, isClassKind } from './class-kind.js'; @@ -182,36 +181,6 @@ export class ClassType extends Type { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isClassType(superType)) { - return this.analyzeSubTypeProblems(this, superType); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(this, superType)], - }]; - } - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - if (isClassType(subType)) { - return this.analyzeSubTypeProblems(subType, this); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType, - result: false, - subProblems: [createKindConflict(subType, this)], - }]; - } - } - protected analyzeSubTypeProblems(subType: ClassType, superType: ClassType): TypirProblem[] { if (this.kind.options.typing === 'Structural') { // for structural typing, the sub type needs to have all fields of the super type with assignable types (including fields of all super classes): diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index d605286..a6cc73f 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -4,15 +4,14 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; +import { TypeEdge } from '../../graph/type-edge.js'; +import { TypeGraphListener } from '../../graph/type-graph.js'; import { isType, Type } from '../../graph/type-node.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; -import { TopClassKind, TopClassTypeDetails, isTopClassKind } from './top-class-kind.js'; import { isClassType } from './class-type.js'; -import { TypeGraphListener } from '../../graph/type-graph.js'; -import { TypeEdge } from '../../graph/type-edge.js'; +import { isTopClassKind, TopClassKind, TopClassTypeDetails } from './top-class-kind.js'; export class TopClassType extends Type implements TypeGraphListener { override readonly kind: TopClassKind; @@ -72,38 +71,6 @@ export class TopClassType extends Type implements TypeGraphListener { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isTopClassType(superType)) { - // special case by definition: TopClassType is sub-type of TopClassType - return []; - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(superType, this)], - }]; - } - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - // an TopClassType is the super type of all ClassTypes! - if (isClassType(subType)) { - return []; - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType, - result: false, - subProblems: [createKindConflict(this, subType)], - }]; - } - } - } export function isTopClassType(type: unknown): type is TopClassType { diff --git a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts index 5f08a6c..97a97e7 100644 --- a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts +++ b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts @@ -4,13 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; -import { checkValueForConflict, checkTypeArrays, createKindConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; +import { checkTypeArrays, checkValueForConflict, createKindConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, toArray } from '../../utils/utils.js'; -import { Parameter, FixedParameterKind, isFixedParametersKind, FixedParameterTypeDetails } from './fixed-parameters-kind.js'; +import { FixedParameterKind, FixedParameterTypeDetails, isFixedParametersKind, Parameter } from './fixed-parameters-kind.js'; export class ParameterValue { readonly parameter: Parameter; @@ -77,36 +76,6 @@ export class FixedParameterType extends Type { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isFixedParameterType(superType)) { - return this.analyzeSubTypeProblems(this, superType); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(this, superType)], - }]; - } - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - if (isFixedParameterType(subType)) { - return this.analyzeSubTypeProblems(subType, this); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType, - result: false, - subProblems: [createKindConflict(subType, this)], - }]; - } - } - protected analyzeSubTypeProblems(subType: FixedParameterType, superType: FixedParameterType): TypirProblem[] { // same name, e.g. both need to be Map, Set, Array, ... const baseTypeCheck = checkValueForConflict(subType.kind.baseName, superType.kind.baseName, 'base type'); diff --git a/packages/typir/src/kinds/function/function-type.ts b/packages/typir/src/kinds/function/function-type.ts index 714530b..bebd5fe 100644 --- a/packages/typir/src/kinds/function/function-type.ts +++ b/packages/typir/src/kinds/function/function-type.ts @@ -4,12 +4,11 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { Type, isType } from '../../graph/type-node.js'; import { TypeReference } from '../../initialization/type-reference.js'; -import { TypirProblem, NameTypePair } from '../../utils/utils-definitions.js'; -import { checkValueForConflict, checkTypes, checkTypeArrays, createKindConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; +import { NameTypePair, TypirProblem } from '../../utils/utils-definitions.js'; +import { checkTypeArrays, checkTypes, checkValueForConflict, createKindConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, assertUnreachable } from '../../utils/utils.js'; import { FunctionKind, FunctionTypeDetails, isFunctionKind } from './function-kind.js'; @@ -125,34 +124,6 @@ export class FunctionType extends Type { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isFunctionType(superType)) { - return this.analyzeSubTypeProblems(this, superType); - } - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(this, superType)], - }]; - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - if (isFunctionType(subType)) { - return this.analyzeSubTypeProblems(subType, this); - } - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType, - result: false, - subProblems: [createKindConflict(subType, this)], - }]; - } - protected analyzeSubTypeProblems(subType: FunctionType, superType: FunctionType): TypirProblem[] { const conflicts: TypirProblem[] = []; const strategy = createTypeCheckStrategy(this.kind.options.subtypeParameterChecking, this.kind.services); diff --git a/packages/typir/src/kinds/multiplicity/multiplicity-type.ts b/packages/typir/src/kinds/multiplicity/multiplicity-type.ts index c180bc3..9a68da9 100644 --- a/packages/typir/src/kinds/multiplicity/multiplicity-type.ts +++ b/packages/typir/src/kinds/multiplicity/multiplicity-type.ts @@ -4,12 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { isSubTypeProblem, SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; +import { isSubTypeProblem } from '../../services/subtype.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, createKindConflict } from '../../utils/utils-type-comparison.js'; -import { MultiplicityKind, MultiplicityTypeDetails, isMultiplicityKind } from './multiplicity-kind.js'; +import { isMultiplicityKind, MultiplicityKind, MultiplicityTypeDetails } from './multiplicity-kind.js'; export class MultiplicityType extends Type { override readonly kind: MultiplicityKind; @@ -56,36 +56,6 @@ export class MultiplicityType extends Type { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isMultiplicityType(superType)) { - return this.analyzeSubTypeProblems(this, superType); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(this, superType)], - }]; - } - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - if (isMultiplicityType(subType)) { - return this.analyzeSubTypeProblems(subType, this); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType, - result: false, - subProblems: [createKindConflict(subType, this)], - }]; - } - } - protected analyzeSubTypeProblems(subType: MultiplicityType, superType: MultiplicityType): TypirProblem[] { const conflicts: TypirProblem[] = []; // check the multiplicities diff --git a/packages/typir/src/kinds/primitive/primitive-type.ts b/packages/typir/src/kinds/primitive/primitive-type.ts index b39ee32..14e0d24 100644 --- a/packages/typir/src/kinds/primitive/primitive-type.ts +++ b/packages/typir/src/kinds/primitive/primitive-type.ts @@ -4,12 +4,11 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, createKindConflict } from '../../utils/utils-type-comparison.js'; -import { PrimitiveKind, PrimitiveTypeDetails, isPrimitiveKind } from './primitive-kind.js'; +import { isPrimitiveKind, PrimitiveKind, PrimitiveTypeDetails } from './primitive-kind.js'; export class PrimitiveType extends Type { override readonly kind: PrimitiveKind; @@ -41,36 +40,6 @@ export class PrimitiveType extends Type { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isPrimitiveType(superType)) { - return this.analyzeSubTypeProblems(this, superType); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(this, superType)], - }]; - } - } - - override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { - if (isPrimitiveType(subType)) { - return this.analyzeSubTypeProblems(subType, this); - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType: this, - subType, - result: false, - subProblems: [createKindConflict(subType, this)], - }]; - } - } - protected analyzeSubTypeProblems(subType: PrimitiveType, superType: PrimitiveType): TypirProblem[] { return subType.analyzeTypeEqualityProblems(superType); } diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index f8ea570..ec70a09 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -4,14 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEqualityProblem } from '../../services/equality.js'; -import { SubTypeProblem, SubTypeResult } from '../../services/subtype.js'; +import { TypeEdge } from '../../graph/type-edge.js'; +import { TypeGraphListener } from '../../graph/type-graph.js'; import { isType, Type } from '../../graph/type-node.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; -import { TopKind, TopTypeDetails, isTopKind } from './top-kind.js'; -import { TypeGraphListener } from '../../graph/type-graph.js'; -import { TypeEdge } from '../../graph/type-edge.js'; +import { isTopKind, TopKind, TopTypeDetails } from './top-kind.js'; export class TopType extends Type implements TypeGraphListener { override readonly kind: TopKind; @@ -71,27 +70,6 @@ export class TopType extends Type implements TypeGraphListener { } } - override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { - if (isTopType(superType)) { - // special case by definition: TopType is sub-type of TopType - return []; - } else { - return [{ - $problem: SubTypeProblem, - $result: SubTypeResult, - superType, - subType: this, - result: false, - subProblems: [createKindConflict(superType, this)], - }]; - } - } - - override analyzeIsSuperTypeOf(_subType: Type): TypirProblem[] { - // a TopType is the super type of all types! - return []; - } - } export function isTopType(type: unknown): type is TopType { From 03dc6b6ebc4d63808bd30eb7cf4f741212446a1f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 21 Jan 2025 20:52:03 +0100 Subject: [PATCH 17/29] improved comments, fixed bug --- packages/typir/src/services/assignability.ts | 5 +++-- packages/typir/src/services/subtype.ts | 10 +++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/typir/src/services/assignability.ts b/packages/typir/src/services/assignability.ts index 4f5cd01..6a58ec9 100644 --- a/packages/typir/src/services/assignability.ts +++ b/packages/typir/src/services/assignability.ts @@ -8,7 +8,7 @@ import { GraphAlgorithms } from '../graph/graph-algorithms.js'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { TypirProblem } from '../utils/utils-definitions.js'; -import { ConversionEdge, TypeConversion } from './conversion.js'; +import { ConversionEdge, isConversionEdge, TypeConversion } from './conversion.js'; import { TypeEquality } from './equality.js'; import { SubType, SubTypeEdge } from './subtype.js'; @@ -89,7 +89,8 @@ export class DefaultTypeAssignability implements TypeAssignability { } // 2. any path of implicit conversion and sub-type relationships - const path = this.algorithms.getEdgePath(source, target, [ConversionEdge, SubTypeEdge]); + const path = this.algorithms.getEdgePath(source, target, [ConversionEdge, SubTypeEdge], + edge => isConversionEdge(edge) ? edge.mode === 'IMPLICIT_EXPLICIT' : true); // no explicit conversion if (path.length >= 1) { return { $result: AssignabilityResult, diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index 34bc09f..e644ff4 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -56,13 +56,6 @@ export interface MarkSubTypeOptions { * * The sub-type relationship might be direct or indirect (transitive). * If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition). - * - * In theory, the difference between sub type-relationships and super type-relationships are only switched types. - * But in practise, the default implementation will ask both involved types (if they have different kinds), - * whether there is a sub type-relationship respectively a super type-relationship. - * If at least one type reports a relationship, a sub type-relationship is assumed. - * This simplifies the implementation of TopTypes and the implementation of new types (or customization of existing types), - * since unchanged types don't need to be customized to report sub type-relationships accordingly. */ export interface SubType { isSubType(subType: Type, superType: Type): boolean; @@ -75,6 +68,9 @@ export interface SubType { /** * The default implementation for the SubType service. + * It assumes that all known types and all their sub-type relationships are explicitly encoded in the type graph. + * Cycles in the sub-type relationships are supported, + * so that DSL users might accidentally define e.g. classes with cyclic sub-super classes, resulting in validation errors shown to them. * This implementation does not cache any computed sub-type-relationships. */ export class DefaultSubType implements SubType { From 17449e5eac8180f3973cccede8bcda6e114e78dd Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 21 Jan 2025 20:57:07 +0100 Subject: [PATCH 18/29] improved test cases to explicitly check the resulting path --- .../function/operator-overloaded.test.ts | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/typir/test/kinds/function/operator-overloaded.test.ts b/packages/typir/test/kinds/function/operator-overloaded.test.ts index 61acde5..0359b90 100644 --- a/packages/typir/test/kinds/function/operator-overloaded.test.ts +++ b/packages/typir/test/kinds/function/operator-overloaded.test.ts @@ -7,12 +7,12 @@ /* eslint-disable @typescript-eslint/parameter-properties */ import { beforeAll, describe, expect, test } from 'vitest'; -import { AssignmentStatement, BinaryExpression, BooleanLiteral, DoubleLiteral, InferenceRuleBinaryExpression, IntegerLiteral, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; -import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; -import { InferenceRuleNotApplicable} from '../../../src/services/inference.js'; +import { assertTrue, ConversionEdge, isAssignabilitySuccess, isPrimitiveType, isType, SubTypeEdge } from '../../../src/index.js'; +import { InferenceRuleNotApplicable } from '../../../src/services/inference.js'; import { ValidationMessageDetails } from '../../../src/services/validation.js'; +import { AssignmentStatement, BinaryExpression, BooleanLiteral, DoubleLiteral, InferenceRuleBinaryExpression, IntegerLiteral, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; import { TypirServices } from '../../../src/typir.js'; -import { isPrimitiveType } from '../../../src/index.js'; +import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; describe('Multiple best matches for overloaded operators', () => { let typir: TypirServices; @@ -35,9 +35,9 @@ describe('Multiple best matches for overloaded operators', () => { ], inferenceRule: InferenceRuleBinaryExpression }); // define relationships between types - typir.Conversion.markAsConvertible(doubleType, stringType, 'IMPLICIT_EXPLICIT'); // stringVariable := doubleValue; typir.Conversion.markAsConvertible(booleanType, integerType, 'IMPLICIT_EXPLICIT'); // integerVariable := booleanValue; typir.Subtype.markAsSubType(integerType, doubleType); // double <|--- integer + typir.Conversion.markAsConvertible(doubleType, stringType, 'IMPLICIT_EXPLICIT'); // stringVariable := doubleValue; // specify, how Typir can detect the type of a variable typir.Inference.addInferenceRule(node => { @@ -58,7 +58,7 @@ describe('Multiple best matches for overloaded operators', () => { }); - describe('tests all cases for assignability', () => { + describe('tests all cases for assignability and the checks the found assignability paths', () => { test('integer to integer', () => { expectAssignmentValid(new IntegerLiteral(123), new IntegerLiteral(456)); }); @@ -69,11 +69,11 @@ describe('Multiple best matches for overloaded operators', () => { expectAssignmentError(new StringLiteral('123'), new IntegerLiteral(456)); }); test('boolean to integer', () => { - expectAssignmentValid(new BooleanLiteral(true), new IntegerLiteral(456)); + expectAssignmentValid(new BooleanLiteral(true), new IntegerLiteral(456), 'ConversionEdge'); }); test('integer to double', () => { - expectAssignmentValid(new IntegerLiteral(123), new DoubleLiteral(456.0)); + expectAssignmentValid(new IntegerLiteral(123), new DoubleLiteral(456.0), 'SubTypeEdge'); }); test('double to double', () => { expectAssignmentValid(new DoubleLiteral(123.0), new DoubleLiteral(456.0)); @@ -82,20 +82,20 @@ describe('Multiple best matches for overloaded operators', () => { expectAssignmentError(new StringLiteral('123'), new DoubleLiteral(456.0)); }); test('boolean to double', () => { - expectAssignmentValid(new BooleanLiteral(true), new DoubleLiteral(456.0)); + expectAssignmentValid(new BooleanLiteral(true), new DoubleLiteral(456.0), 'ConversionEdge', 'SubTypeEdge'); }); test('integer to string', () => { - expectAssignmentValid(new IntegerLiteral(123), new StringLiteral('456')); + expectAssignmentValid(new IntegerLiteral(123), new StringLiteral('456'), 'SubTypeEdge', 'ConversionEdge'); }); test('double to string', () => { - expectAssignmentValid(new DoubleLiteral(123.0), new StringLiteral('456')); + expectAssignmentValid(new DoubleLiteral(123.0), new StringLiteral('456'), 'ConversionEdge'); }); test('string to string', () => { expectAssignmentValid(new StringLiteral('123'), new StringLiteral('456')); }); test('boolean to string', () => { - expectAssignmentValid(new BooleanLiteral(true), new StringLiteral('456')); + expectAssignmentValid(new BooleanLiteral(true), new StringLiteral('456'), 'ConversionEdge', 'SubTypeEdge', 'ConversionEdge'); }); test('integer to boolean', () => { @@ -112,10 +112,34 @@ describe('Multiple best matches for overloaded operators', () => { }); - function expectAssignmentValid(value: TestExpressionNode, variableInitType: TestExpressionNode): void { + function expectAssignmentValid(value: TestExpressionNode, variableInitType: TestExpressionNode, ...expectedPath: Array): void { const variable = new Variable('v1', variableInitType); const assignment = new AssignmentStatement(variable, value); expect(typir.validation.Collector.validate(assignment)).toHaveLength(0); + + // do type inference + const valueType = typir.Inference.inferType(value); + assertTrue(isType(valueType)); + const variableType = typir.Inference.inferType(variable); + assertTrue(isType(variableType)); + // check the resulting assignability path + const assignabilityResult = typir.Assignability.getAssignabilityResult(valueType, variableType); + assertTrue(isAssignabilitySuccess(assignabilityResult)); + const actualPath = assignabilityResult.path; + const msg = `Actual assignability path is ${actualPath.map(e => e.$relation).join(' --> ')}.`; + expect(actualPath.length, msg).toBe(expectedPath.length); + for (let i = 0; i < actualPath.length; i++) { + expect(actualPath[i].$relation, msg).toBe(expectedPath[i]); + if (i >= 1) { + // the edges are connected with each other + expect(actualPath[i - 1].to).toBe(actualPath[i].from); + } + } + // check beginning and end of the path + if (actualPath.length >= 1) { + expect(actualPath[0].from).toBe(valueType); + expect(actualPath[actualPath.length - 1].to).toBe(variableType); + } } function expectAssignmentError(value: TestExpressionNode, variableInitType: TestExpressionNode): void { @@ -128,7 +152,7 @@ describe('Multiple best matches for overloaded operators', () => { }); - describe('Test multiple matches for overloaded operators', () => { + describe('Test multiple matches for overloaded operators and ensures that the best match is chosen', () => { test('2 + 3 => both are integers', () => { expectOverload(new IntegerLiteral(2), new IntegerLiteral(3), 'integer'); }); From 95644b63f2ea974960567cf7a749dc6c1e9d1311 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 08:36:49 +0100 Subject: [PATCH 19/29] simplified APIs for sub-type and conversion, since users of Typir could use loops to define multiple relationships at once --- packages/typir/src/services/conversion.ts | 15 ++------------- packages/typir/src/services/subtype.ts | 19 ++++--------------- packages/typir/test/type-definitions.test.ts | 3 +-- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/packages/typir/src/services/conversion.ts b/packages/typir/src/services/conversion.ts index b5bf924..c768320 100644 --- a/packages/typir/src/services/conversion.ts +++ b/packages/typir/src/services/conversion.ts @@ -9,7 +9,6 @@ import { isTypeEdge, TypeEdge } from '../graph/type-edge.js'; import { TypeGraph } from '../graph/type-graph.js'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; -import { toArray } from '../utils/utils.js'; import { TypeEquality } from './equality.js'; /** @@ -50,7 +49,7 @@ export interface TypeConversion { * @param mode the desired conversion relationship between the two given types * @throws an error, if a cycle was introduced */ - markAsConvertible(from: Type | Type[], to: Type | Type[], mode: ConversionModeForSpecification): void; + markAsConvertible(from: Type, to: Type, mode: ConversionModeForSpecification): void; /** * Identifies the existing conversion relationship between two given types. @@ -121,17 +120,7 @@ export class DefaultTypeConversion implements TypeConversion { this.algorithms = services.infrastructure.GraphAlgorithms; } - markAsConvertible(from: Type | Type[], to: Type | Type[], mode: ConversionModeForSpecification): void { - const allFrom = toArray(from); - const allTo = toArray(to); - for (const f of allFrom) { - for (const t of allTo) { - this.markAsConvertibleSingle(f, t, mode); - } - } - } - - protected markAsConvertibleSingle(from: Type, to: Type, mode: ConversionModeForSpecification): void { + markAsConvertible(from: Type, to: Type, mode: ConversionModeForSpecification): void { let edge = this.getConversionEdge(from, to); if (!edge) { // create a missing edge (with the desired mode) diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index e644ff4..08b2435 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -10,7 +10,6 @@ import { TypeGraph } from '../graph/type-graph.js'; import { Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { TypirProblem } from '../utils/utils-definitions.js'; -import { toArray } from '../utils/utils.js'; export interface SubTypeProblem extends TypirProblem { $problem: 'SubTypeProblem'; @@ -62,7 +61,7 @@ export interface SubType { getSubTypeProblem(subType: Type, superType: Type): SubTypeProblem | undefined; getSubTypeResult(subType: Type, superType: Type): SubTypeResult; - markAsSubType(subType: Type | Type[], superType: Type | Type[], options?: Partial): void; + markAsSubType(subType: Type, superType: Type, options?: Partial): void; } @@ -118,17 +117,6 @@ export class DefaultSubType implements SubType { return from.getOutgoingEdges(SubTypeEdge).find(edge => edge.to === to); } - markAsSubType(subType: Type | Type[], superType: Type | Type[], options?: Partial): void { - const allSub = toArray(subType); - const allSuper = toArray(superType); - const actualOptions = this.collectMarkSubTypeOptions(options); - for (const subT of allSub) { - for (const superT of allSuper) { - this.markAsSubTypeSingle(subT, superT, actualOptions); - } - } - } - protected collectMarkSubTypeOptions(options?: Partial): MarkSubTypeOptions { return { // the default values: @@ -138,7 +126,8 @@ export class DefaultSubType implements SubType { }; } - protected markAsSubTypeSingle(subType: Type, superType: Type, options: MarkSubTypeOptions): void { + markAsSubType(subType: Type, superType: Type, options: MarkSubTypeOptions): void { + const actualOptions = this.collectMarkSubTypeOptions(options); let edge = this.getSubTypeEdge(subType, superType); if (!edge) { edge = { @@ -154,7 +143,7 @@ export class DefaultSubType implements SubType { } // check for cycles - if (options.checkForCycles) { + if (actualOptions.checkForCycles) { const hasIntroducedCycle = this.algorithms.existsEdgePath(subType, subType, [SubTypeEdge]); if (hasIntroducedCycle) { throw new Error(`Adding the sub-type relationship from ${subType.getIdentifier()} to ${superType.getIdentifier()} has introduced a cycle in the type graph.`); diff --git a/packages/typir/test/type-definitions.test.ts b/packages/typir/test/type-definitions.test.ts index 3586dd6..fe11ba8 100644 --- a/packages/typir/test/type-definitions.test.ts +++ b/packages/typir/test/type-definitions.test.ts @@ -89,8 +89,7 @@ describe('Tests for Typir', () => { const opTernaryIf = typir.factory.Operators.createTernary({ name: 'if', signature: { first: typeBoolean, second: typeInt, third: typeInt, return: typeInt } }); // automated conversion from int to string - // it is possible to define multiple sources and/or targets at the same time: - typir.Conversion.markAsConvertible([typeInt, typeInt], [typeString, typeString, typeString], 'EXPLICIT'); + typir.Conversion.markAsConvertible(typeInt, typeString, 'EXPLICIT'); // single relationships are possible as well typir.Conversion.markAsConvertible(typeInt, typeString, 'IMPLICIT_EXPLICIT'); From a01885485498ff5188c664a4676a48a7cdc6523f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 08:56:29 +0100 Subject: [PATCH 20/29] improved the algorithm to select the best matches of overloading functions --- .../kinds/function/function-initializer.ts | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 1586e6f..b3cdfe6 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -349,28 +349,29 @@ export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInference } else { // multiple matches => determine the one to return - // 1. sort the found matches - matchingOverloads.sort((l, r) => this.compareMatchingOverloads(l, r)); - - // 2. identify the best matches at the beginning of the list - let index = 1; - while (index < matchingOverloads.length) { - if (this.compareMatchingOverloads(matchingOverloads[index - 1], matchingOverloads[index]) === 0) { - index++; // same priority + // 1. identify and collect the best matches + const bestMatches: OverloadedMatch[] = [ matchingOverloads[0] ]; + for (let i = 1; i < matchingOverloads.length; i++) { + const currentMatch = matchingOverloads[i]; + const comparison = this.compareMatchingOverloads(bestMatches[0], currentMatch); + if (comparison < 0) { + // the existing matches are better than the current one => keep the existing best matches + } else if (comparison > 0) { + // the current match is better than the already collect ones => replace the existing best matches by the current one + bestMatches.splice(0, bestMatches.length, currentMatch); } else { - break; // lower priority => skip them + // the current and the existing matches are both good => collect both + bestMatches.push(currentMatch); } } - matchingOverloads.splice(index); // keep only the matches with the same highest priority - // TODO review: should we make this implementation more efficient? - // 3. evaluate remaining best matches - if (matchingOverloads.length === 0) { + // 2. evaluate the remaining best matches + if (bestMatches.length === 0) { // return the single remaining match - return matchingOverloads[0].result; + return bestMatches[0].result; } else { // decide how to deal with multiple best matches - const result = this.handleMultipleBestMatches(matchingOverloads); + const result = this.handleMultipleBestMatches(bestMatches); if (result) { // return the chosen match return result.result; @@ -379,7 +380,7 @@ export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInference return [{ $problem: InferenceProblem, languageNode: languageNode, - location: `Found ${matchingOverloads.length} best matching overloads: ${matchingOverloads.map(m => m.result.getIdentifier()).join(', ')}`, + location: `Found ${bestMatches.length} best matching overloads: ${bestMatches.map(m => m.result.getIdentifier()).join(', ')}`, subProblems: [], // there are no real sub-problems, since the relevant overloads match ... }]; } From 3d3816423c0e3ff9679b48262df62ba91383277c Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 09:41:38 +0100 Subject: [PATCH 21/29] introduced test fixtures for literals --- .../src/test/predefined-language-nodes.ts | 22 +++++++- .../function/operator-overloaded.test.ts | 50 +++++++++---------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/packages/typir/src/test/predefined-language-nodes.ts b/packages/typir/src/test/predefined-language-nodes.ts index 0391561..6744770 100644 --- a/packages/typir/src/test/predefined-language-nodes.ts +++ b/packages/typir/src/test/predefined-language-nodes.ts @@ -71,6 +71,26 @@ export class StringLiteral extends TestExpressionNode { ) { super(); } } +// some predefined literals +export const integer2 = new IntegerLiteral(2); +export const integer3 = new IntegerLiteral(3); +export const integer123 = new IntegerLiteral(123); +export const integer456 = new IntegerLiteral(456); + +export const double123_0 = new DoubleLiteral(123.0); +export const double456_0 = new DoubleLiteral(456.0); +export const double2_0 = new DoubleLiteral(2.0); +export const double3_0 = new DoubleLiteral(3.0); + +export const booleanTrue = new BooleanLiteral(true); +export const booleanFalse = new BooleanLiteral(false); + +export const string123 = new StringLiteral('123'); +export const string456 = new StringLiteral('456'); +export const string2 = new StringLiteral('2'); +export const string3 = new StringLiteral('3'); + + export class BinaryExpression extends TestExpressionNode { constructor( public left: TestExpressionNode, @@ -83,7 +103,7 @@ export class BinaryExpression extends TestExpressionNode { export class Variable extends TestLanguageNode { constructor( public name: string, - public initialValue: TestExpressionNode, + public initialValue: TestExpressionNode, // the type of this initialization expression is used as type of the variable ) { super(); } } diff --git a/packages/typir/test/kinds/function/operator-overloaded.test.ts b/packages/typir/test/kinds/function/operator-overloaded.test.ts index 0359b90..287dcb5 100644 --- a/packages/typir/test/kinds/function/operator-overloaded.test.ts +++ b/packages/typir/test/kinds/function/operator-overloaded.test.ts @@ -10,7 +10,7 @@ import { beforeAll, describe, expect, test } from 'vitest'; import { assertTrue, ConversionEdge, isAssignabilitySuccess, isPrimitiveType, isType, SubTypeEdge } from '../../../src/index.js'; import { InferenceRuleNotApplicable } from '../../../src/services/inference.js'; import { ValidationMessageDetails } from '../../../src/services/validation.js'; -import { AssignmentStatement, BinaryExpression, BooleanLiteral, DoubleLiteral, InferenceRuleBinaryExpression, IntegerLiteral, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; +import { AssignmentStatement, BinaryExpression, booleanFalse, BooleanLiteral, booleanTrue, double123_0, double2_0, double3_0, double456_0, DoubleLiteral, InferenceRuleBinaryExpression, integer123, integer2, integer3, integer456, IntegerLiteral, string123, string2, string3, string456, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; import { TypirServices } from '../../../src/typir.js'; import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; @@ -60,55 +60,55 @@ describe('Multiple best matches for overloaded operators', () => { describe('tests all cases for assignability and the checks the found assignability paths', () => { test('integer to integer', () => { - expectAssignmentValid(new IntegerLiteral(123), new IntegerLiteral(456)); + expectAssignmentValid(integer123, integer456); }); test('double to integer', () => { - expectAssignmentError(new DoubleLiteral(123.0), new IntegerLiteral(456)); + expectAssignmentError(double123_0, integer456); }); test('string to integer', () => { - expectAssignmentError(new StringLiteral('123'), new IntegerLiteral(456)); + expectAssignmentError(string123, integer456); }); test('boolean to integer', () => { - expectAssignmentValid(new BooleanLiteral(true), new IntegerLiteral(456), 'ConversionEdge'); + expectAssignmentValid(booleanTrue, integer456, 'ConversionEdge'); }); test('integer to double', () => { - expectAssignmentValid(new IntegerLiteral(123), new DoubleLiteral(456.0), 'SubTypeEdge'); + expectAssignmentValid(integer123, double456_0, 'SubTypeEdge'); }); test('double to double', () => { - expectAssignmentValid(new DoubleLiteral(123.0), new DoubleLiteral(456.0)); + expectAssignmentValid(double123_0, double456_0); }); test('string to double', () => { - expectAssignmentError(new StringLiteral('123'), new DoubleLiteral(456.0)); + expectAssignmentError(string123, double456_0); }); test('boolean to double', () => { - expectAssignmentValid(new BooleanLiteral(true), new DoubleLiteral(456.0), 'ConversionEdge', 'SubTypeEdge'); + expectAssignmentValid(booleanTrue, double456_0, 'ConversionEdge', 'SubTypeEdge'); }); test('integer to string', () => { - expectAssignmentValid(new IntegerLiteral(123), new StringLiteral('456'), 'SubTypeEdge', 'ConversionEdge'); + expectAssignmentValid(integer123, string456, 'SubTypeEdge', 'ConversionEdge'); }); test('double to string', () => { - expectAssignmentValid(new DoubleLiteral(123.0), new StringLiteral('456'), 'ConversionEdge'); + expectAssignmentValid(double123_0, string456, 'ConversionEdge'); }); test('string to string', () => { - expectAssignmentValid(new StringLiteral('123'), new StringLiteral('456')); + expectAssignmentValid(string123, string456); }); test('boolean to string', () => { - expectAssignmentValid(new BooleanLiteral(true), new StringLiteral('456'), 'ConversionEdge', 'SubTypeEdge', 'ConversionEdge'); + expectAssignmentValid(booleanTrue, string456, 'ConversionEdge', 'SubTypeEdge', 'ConversionEdge'); }); test('integer to boolean', () => { - expectAssignmentError(new IntegerLiteral(123), new BooleanLiteral(false)); + expectAssignmentError(integer123, booleanFalse); }); test('double to boolean', () => { - expectAssignmentError(new DoubleLiteral(123.0), new BooleanLiteral(false)); + expectAssignmentError(double123_0, booleanFalse); }); test('string to boolean', () => { - expectAssignmentError(new StringLiteral('123'), new BooleanLiteral(false)); + expectAssignmentError(string123, booleanFalse); }); test('boolean to boolean', () => { - expectAssignmentValid(new BooleanLiteral(true), new BooleanLiteral(false)); + expectAssignmentValid(booleanTrue, booleanFalse); }); @@ -154,35 +154,35 @@ describe('Multiple best matches for overloaded operators', () => { describe('Test multiple matches for overloaded operators and ensures that the best match is chosen', () => { test('2 + 3 => both are integers', () => { - expectOverload(new IntegerLiteral(2), new IntegerLiteral(3), 'integer'); + expectOverload(integer2, integer3, 'integer'); }); test('2.0 + 3.0 => both are doubles', () => { - expectOverload(new DoubleLiteral(2.0), new DoubleLiteral(3.0), 'double'); + expectOverload(double2_0, double3_0, 'double'); }); test('"2" + "3" => both are strings', () => { - expectOverload(new StringLiteral('2'), new StringLiteral('3'), 'string'); + expectOverload(string2, string3, 'string'); }); test('TRUE + FALSE => both are booleans', () => { - expectOverload(new BooleanLiteral(true), new BooleanLiteral(false), 'boolean'); + expectOverload(booleanTrue, booleanFalse, 'boolean'); }); test('2 + TRUE => convert boolean to integer', () => { - expectOverload(new IntegerLiteral(2), new BooleanLiteral(true), 'integer'); + expectOverload(integer2, booleanTrue, 'integer'); }); test('2.0 + 3 => integers are doubles', () => { - expectOverload(new DoubleLiteral(2.0), new IntegerLiteral(3), 'double'); + expectOverload(double2_0, integer3, 'double'); }); test('2.0 + "3" => convert double to string', () => { - expectOverload(new DoubleLiteral(2.0), new StringLiteral('3'), 'string'); + expectOverload(double2_0, string3, 'string'); }); test('2 + "3" => integer is sub-type of double, which is convertible to string', () => { - expectOverload(new IntegerLiteral(2), new StringLiteral('3'), 'string'); + expectOverload(integer2, string3, 'string'); }); From 3605669aa0d38dcae7deac445baa73c46745946f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 09:49:16 +0100 Subject: [PATCH 22/29] fixed typos --- README.md | 2 +- packages/typir/src/initialization/type-selector.ts | 2 +- packages/typir/src/kinds/class/class-validation.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7878d6c..cafc4bd 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Typir provides these core features: Typir does intentionally _not_ include ... - Rule engines and constraint solving, - since type inference is calculate in a recursive manner and does not use unification/substitution + since type inference is calculated in a recursive manner and does not use unification/substitution - Formal proofs - External DSLs for formalizing types - Support for dynamic type systems, which do typing during the execution of the DSL. diff --git a/packages/typir/src/initialization/type-selector.ts b/packages/typir/src/initialization/type-selector.ts index cb6f29b..11e606b 100644 --- a/packages/typir/src/initialization/type-selector.ts +++ b/packages/typir/src/initialization/type-selector.ts @@ -33,7 +33,7 @@ export interface TypeResolvingService { * 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 + * @returns the found type; or undefined, if there is no such type in the type system */ tryToResolve(selector: TypeSelector): T | undefined; diff --git a/packages/typir/src/kinds/class/class-validation.ts b/packages/typir/src/kinds/class/class-validation.ts index e5fd00a..912c64a 100644 --- a/packages/typir/src/kinds/class/class-validation.ts +++ b/packages/typir/src/kinds/class/class-validation.ts @@ -181,7 +181,7 @@ 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, - * this parameter is the reasons, why this validation cannot be registered by default by Typir for classes, since this parameter is DSL-specific + * this parameter is the reason, 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: (languageNode: unknown) => boolean): ValidationRule { From fcbb582891f4a665cd9cec7656e61181ac8da9eb Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 10:11:33 +0100 Subject: [PATCH 23/29] prefixed methods in listeners with `on` --- .../src/features/langium-type-creator.ts | 8 ++++---- packages/typir/src/graph/type-graph.ts | 16 ++++++++-------- packages/typir/src/graph/type-node.ts | 18 +++++++++--------- .../typir/src/initialization/type-reference.ts | 8 ++++---- .../typir/src/initialization/type-waiting.ts | 6 +++--- packages/typir/src/kinds/bottom/bottom-type.ts | 8 ++++---- .../typir/src/kinds/class/class-initializer.ts | 6 +++--- .../typir/src/kinds/class/top-class-type.ts | 8 ++++---- .../src/kinds/function/function-initializer.ts | 6 +++--- .../typir/src/kinds/function/function-kind.ts | 8 ++++---- packages/typir/src/kinds/top/top-type.ts | 8 ++++---- packages/typir/src/services/inference.ts | 8 ++++---- packages/typir/src/services/validation.ts | 8 ++++---- 13 files changed, 58 insertions(+), 58 deletions(-) diff --git a/packages/typir-langium/src/features/langium-type-creator.ts b/packages/typir-langium/src/features/langium-type-creator.ts index 1ab811c..f89f954 100644 --- a/packages/typir-langium/src/features/langium-type-creator.ts +++ b/packages/typir-langium/src/features/langium-type-creator.ts @@ -108,7 +108,7 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator, this.documentTypesMap.delete(documentKey); } - addedType(newType: Type): void { + onAddedType(newType: Type): void { // the TypeGraph notifies about newly created Types if (this.currentDocumentKey) { // associate the new type with the current Langium document! @@ -123,13 +123,13 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator, } } - removedType(_type: Type): void { + onRemovedType(_type: Type): void { // since this type creator actively removes types from the type graph itself, there is no need to react on removed types } - addedEdge(_edge: TypeEdge): void { + onAddedEdge(_edge: TypeEdge): void { // this type creator does not care about edges => do nothing } - removedEdge(_edge: TypeEdge): void { + onRemovedEdge(_edge: TypeEdge): void { // this type creator does not care about edges => do nothing } } diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index 75a56a5..f0e250d 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -43,7 +43,7 @@ export class TypeGraph { } } else { this.nodes.set(mapKey, type); - this.listeners.forEach(listener => listener.addedType(type, mapKey)); + this.listeners.forEach(listener => listener.onAddedType(type, mapKey)); } } @@ -63,7 +63,7 @@ export class TypeGraph { // remove the type itself const contained = this.nodes.delete(mapKey); if (contained) { - this.listeners.slice().forEach(listener => listener.removedType(typeToRemove, mapKey)); + this.listeners.slice().forEach(listener => listener.onRemovedType(typeToRemove, mapKey)); typeToRemove.dispose(); } else { throw new Error(`Type does not exist: ${mapKey}`); @@ -94,7 +94,7 @@ export class TypeGraph { edge.to.addIncomingEdge(edge); edge.from.addOutgoingEdge(edge); - this.listeners.forEach(listener => listener.addedEdge(edge)); + this.listeners.forEach(listener => listener.onAddedEdge(edge)); } removeEdge(edge: TypeEdge): void { @@ -105,7 +105,7 @@ export class TypeGraph { const index = this.edges.indexOf(edge); if (index >= 0) { this.edges.splice(index, 1); - this.listeners.forEach(listener => listener.removedEdge(edge)); + this.listeners.forEach(listener => listener.onRemovedEdge(edge)); } else { throw new Error(`Edge does not exist: ${edge.$relation}`); } @@ -139,8 +139,8 @@ export class TypeGraph { } export interface TypeGraphListener { - addedType(type: Type, key: string): void; - removedType(type: Type, key: string): void; - addedEdge(edge: TypeEdge): void; - removedEdge(edge: TypeEdge): void; + onAddedType(type: Type, key: string): void; + onRemovedType(type: Type, key: string): void; + onAddedEdge(edge: TypeEdge): void; + onRemovedEdge(edge: TypeEdge): void; } diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 624640c..6f34ecf 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -165,11 +165,11 @@ export abstract class Type { // don't inform about the Invalid state! break; case 'Identifiable': - newListeners.switchedToIdentifiable(this); + newListeners.onSwitchedToIdentifiable(this); break; case 'Completed': - newListeners.switchedToIdentifiable(this); // inform about both Identifiable and Completed! - newListeners.switchedToCompleted(this); + newListeners.onSwitchedToIdentifiable(this); // inform about both Identifiable and Completed! + newListeners.onSwitchedToCompleted(this); break; default: assertUnreachable(currentState); @@ -298,21 +298,21 @@ export abstract class Type { this.assertState('Invalid'); this.onIdentification(); this.initializationState = 'Identifiable'; - this.stateListeners.slice().forEach(listener => listener.switchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications + 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.switchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications + 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.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications + 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])); @@ -413,7 +413,7 @@ export function isType(type: unknown): type is Type { export interface TypeStateListener { - switchedToInvalid(type: Type): void; - switchedToIdentifiable(type: Type): void; - switchedToCompleted(type: Type): void; + onSwitchedToInvalid(type: Type): void; + onSwitchedToIdentifiable(type: Type): void; + onSwitchedToCompleted(type: Type): void; } diff --git a/packages/typir/src/initialization/type-reference.ts b/packages/typir/src/initialization/type-reference.ts index 29a8d77..44e4a3e 100644 --- a/packages/typir/src/initialization/type-reference.ts +++ b/packages/typir/src/initialization/type-reference.ts @@ -132,12 +132,12 @@ export class TypeReference implements TypeGraphListener, } - addedType(_addedType: Type, _key: string): void { + onAddedType(_addedType: Type, _key: string): void { // after adding a new type, try to resolve the type this.resolve(); // possible performance optimization: is it possible to do this more performant by looking at the "addedType"? } - removedType(removedType: Type, _key: string): void { + onRemovedType(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 @@ -147,10 +147,10 @@ export class TypeReference implements TypeGraphListener, } } - addedEdge(_edge: TypeEdge): void { + onAddedEdge(_edge: TypeEdge): void { // only types are relevant } - removedEdge(_edge: TypeEdge): void { + onRemovedEdge(_edge: TypeEdge): void { // only types are relevant } diff --git a/packages/typir/src/initialization/type-waiting.ts b/packages/typir/src/initialization/type-waiting.ts index f8aca1b..0f4e903 100644 --- a/packages/typir/src/initialization/type-waiting.ts +++ b/packages/typir/src/initialization/type-waiting.ts @@ -142,17 +142,17 @@ export class WaitingForIdentifiableAndCompletedTypeReferences exten this.initialClassType.addListener(this, true); // trigger directly, if some initialization states are already reached! } - switchedToIdentifiable(classType: Type): void { + onSwitchedToIdentifiable(classType: Type): void { /* 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! @@ -74,7 +74,7 @@ export class ClassTypeInitializer exten } } - switchedToCompleted(classType: Type): void { + onSwitchedToCompleted(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) { @@ -88,7 +88,7 @@ export class ClassTypeInitializer exten classType.removeListener(this); } - switchedToInvalid(_previousClassType: Type): void { + onSwitchedToInvalid(_previousClassType: Type): void { // nothing specific needs to be done for Classes here, since the base implementation takes already care about all relevant stuff } diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index a6cc73f..3d71c4f 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -37,16 +37,16 @@ export class TopClassType extends Type implements TypeGraphListener { } } - addedType(type: Type, _key: string): void { + onAddedType(type: Type, _key: string): void { this.markAsSubType(type); } - removedType(_type: Type, _key: string): void { + onRemovedType(_type: Type, _key: string): void { // empty } - addedEdge(_edge: TypeEdge): void { + onAddedEdge(_edge: TypeEdge): void { // empty } - removedEdge(_edge: TypeEdge): void { + onRemovedEdge(_edge: TypeEdge): void { // empty } diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index b3cdfe6..4b4fff5 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -66,7 +66,7 @@ export class FunctionTypeInitializer extends TypeInitializer im return this.initialFunctionType; } - switchedToIdentifiable(functionType: Type): void { + onSwitchedToIdentifiable(functionType: Type): void { const functionName = this.typeDetails.functionName; assertType(functionType, isFunctionType); const readyFunctionType = this.producedType(functionType); @@ -101,11 +101,11 @@ export class FunctionTypeInitializer extends TypeInitializer im }); } - switchedToCompleted(functionType: Type): void { + onSwitchedToCompleted(functionType: Type): void { functionType.removeListener(this); } - switchedToInvalid(_functionType: Type): void { + onSwitchedToInvalid(_functionType: Type): void { // 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/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index 5edbed1..27d5f2e 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -271,10 +271,10 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer /* Get informed about deleted types in order to remove inference rules which are bound to them. */ - addedType(_newType: Type, _key: string): void { + onAddedType(_newType: Type, _key: string): void { // do nothing } - removedType(type: Type, _key: string): void { + onRemovedType(type: Type, _key: string): void { if (isFunctionType(type)) { const overloads = this.mapNameTypes.get(type.functionName); if (overloads) { @@ -287,10 +287,10 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer } } } - addedEdge(_edge: TypeEdge): void { + onAddedEdge(_edge: TypeEdge): void { // do nothing } - removedEdge(_edge: TypeEdge): void { + onRemovedEdge(_edge: TypeEdge): void { // do nothing } diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index ec70a09..a814298 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -36,16 +36,16 @@ export class TopType extends Type implements TypeGraphListener { } } - addedType(type: Type, _key: string): void { + onAddedType(type: Type, _key: string): void { this.markAsSubType(type); } - removedType(_type: Type, _key: string): void { + onRemovedType(_type: Type, _key: string): void { // empty } - addedEdge(_edge: TypeEdge): void { + onAddedEdge(_edge: TypeEdge): void { // empty } - removedEdge(_edge: TypeEdge): void { + onRemovedEdge(_edge: TypeEdge): void { // empty } diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 2dba796..03393c5 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -312,10 +312,10 @@ 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, _key: string): void { + onAddedType(_newType: Type, _key: string): void { // do nothing } - removedType(type: Type, _key: string): void { + onRemovedType(type: Type, _key: string): void { const key = this.getBoundToTypeKey(type); const rulesToRemove = this.inferenceRules.get(key); // remove the inference rules associated to the deleted type @@ -323,10 +323,10 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty // inform listeners about removed inference rules (rulesToRemove ?? []).forEach(rule => this.listeners.forEach(listener => listener.removedInferenceRule(rule, type))); } - addedEdge(_edge: TypeEdge): void { + onAddedEdge(_edge: TypeEdge): void { // do nothing } - removedEdge(_edge: TypeEdge): void { + onRemovedEdge(_edge: TypeEdge): void { // do nothing } diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index adfd1d7..452b1fd 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -259,17 +259,17 @@ export class DefaultValidationCollector Date: Mon, 27 Jan 2025 10:30:35 +0100 Subject: [PATCH 24/29] made methods in TypeGraphListener optional --- packages/typir/src/graph/type-graph.ts | 12 ++++++------ packages/typir/src/initialization/type-reference.ts | 8 -------- packages/typir/src/kinds/bottom/bottom-type.ts | 10 ---------- packages/typir/src/kinds/class/top-class-type.ts | 10 ---------- packages/typir/src/kinds/function/function-kind.ts | 11 ----------- packages/typir/src/kinds/top/top-type.ts | 10 ---------- packages/typir/src/services/inference.ts | 11 ----------- packages/typir/src/services/validation.ts | 11 ----------- 8 files changed, 6 insertions(+), 77 deletions(-) diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index f0e250d..321a428 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -43,7 +43,7 @@ export class TypeGraph { } } else { this.nodes.set(mapKey, type); - this.listeners.forEach(listener => listener.onAddedType(type, mapKey)); + this.listeners.forEach(listener => listener.onAddedType?.call(listener, type, mapKey)); } } @@ -63,7 +63,7 @@ export class TypeGraph { // remove the type itself const contained = this.nodes.delete(mapKey); if (contained) { - this.listeners.slice().forEach(listener => listener.onRemovedType(typeToRemove, mapKey)); + this.listeners.slice().forEach(listener => listener.onRemovedType?.call(listener, typeToRemove, mapKey)); typeToRemove.dispose(); } else { throw new Error(`Type does not exist: ${mapKey}`); @@ -94,7 +94,7 @@ export class TypeGraph { edge.to.addIncomingEdge(edge); edge.from.addOutgoingEdge(edge); - this.listeners.forEach(listener => listener.onAddedEdge(edge)); + this.listeners.forEach(listener => listener.onAddedEdge?.call(listener, edge)); } removeEdge(edge: TypeEdge): void { @@ -105,7 +105,7 @@ export class TypeGraph { const index = this.edges.indexOf(edge); if (index >= 0) { this.edges.splice(index, 1); - this.listeners.forEach(listener => listener.onRemovedEdge(edge)); + this.listeners.forEach(listener => listener.onRemovedEdge?.call(listener, edge)); } else { throw new Error(`Edge does not exist: ${edge.$relation}`); } @@ -138,9 +138,9 @@ export class TypeGraph { } -export interface TypeGraphListener { +export type TypeGraphListener = Partial<{ onAddedType(type: Type, key: string): void; onRemovedType(type: Type, key: string): void; onAddedEdge(edge: TypeEdge): void; onRemovedEdge(edge: TypeEdge): void; -} +}> diff --git a/packages/typir/src/initialization/type-reference.ts b/packages/typir/src/initialization/type-reference.ts index 44e4a3e..4319d9d 100644 --- a/packages/typir/src/initialization/type-reference.ts +++ b/packages/typir/src/initialization/type-reference.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; import { Type } from '../graph/type-node.js'; import { TypeInferenceCollectorListener, TypeInferenceRule } from '../services/inference.js'; @@ -147,13 +146,6 @@ export class TypeReference implements TypeGraphListener, } } - onAddedEdge(_edge: TypeEdge): void { - // only types are relevant - } - onRemovedEdge(_edge: TypeEdge): void { - // only types are relevant - } - addedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void { // after adding a new inference rule, try to resolve the type this.resolve(); // possible performance optimization: use only the new inference rule to resolve the type diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 9ddb76b..3c79517 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEdge } from '../../graph/type-edge.js'; import { TypeGraphListener } from '../../graph/type-graph.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypeEqualityProblem } from '../../services/equality.js'; @@ -39,15 +38,6 @@ export class BottomType extends Type implements TypeGraphListener { onAddedType(type: Type, _key: string): void { this.markAsSubType(type); } - onRemovedType(_type: Type, _key: string): void { - // empty - } - onAddedEdge(_edge: TypeEdge): void { - // empty - } - onRemovedEdge(_edge: TypeEdge): void { - // empty - } override getName(): string { return this.getIdentifier(); diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 3d71c4f..dcfa41e 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEdge } from '../../graph/type-edge.js'; import { TypeGraphListener } from '../../graph/type-graph.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypeEqualityProblem } from '../../services/equality.js'; @@ -40,15 +39,6 @@ export class TopClassType extends Type implements TypeGraphListener { onAddedType(type: Type, _key: string): void { this.markAsSubType(type); } - onRemovedType(_type: Type, _key: string): void { - // empty - } - onAddedEdge(_edge: TypeEdge): void { - // empty - } - onRemovedEdge(_edge: TypeEdge): void { - // empty - } override getName(): string { return this.getIdentifier(); diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index 27d5f2e..b235432 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEdge } from '../../graph/type-edge.js'; import { TypeGraphListener } from '../../graph/type-graph.js'; import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; @@ -270,10 +269,6 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer /* Get informed about deleted types in order to remove inference rules which are bound to them. */ - - onAddedType(_newType: Type, _key: string): void { - // do nothing - } onRemovedType(type: Type, _key: string): void { if (isFunctionType(type)) { const overloads = this.mapNameTypes.get(type.functionName); @@ -287,12 +282,6 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer } } } - onAddedEdge(_edge: TypeEdge): void { - // do nothing - } - onRemovedEdge(_edge: TypeEdge): void { - // do nothing - } calculateIdentifier(typeDetails: FunctionTypeDetails): string { diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index a814298..b1dd4f4 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEdge } from '../../graph/type-edge.js'; import { TypeGraphListener } from '../../graph/type-graph.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypeEqualityProblem } from '../../services/equality.js'; @@ -39,15 +38,6 @@ export class TopType extends Type implements TypeGraphListener { onAddedType(type: Type, _key: string): void { this.markAsSubType(type); } - onRemovedType(_type: Type, _key: string): void { - // empty - } - onAddedEdge(_edge: TypeEdge): void { - // empty - } - onRemovedEdge(_edge: TypeEdge): void { - // empty - } override getName(): string { return this.getIdentifier(); diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 03393c5..e3dac1d 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -5,7 +5,6 @@ ******************************************************************************/ import { assertUnreachable } from 'langium'; -import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; import { isType, Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; @@ -311,10 +310,6 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty /* Get informed about deleted types in order to remove inference rules which are bound to them. */ - - onAddedType(_newType: Type, _key: string): void { - // do nothing - } onRemovedType(type: Type, _key: string): void { const key = this.getBoundToTypeKey(type); const rulesToRemove = this.inferenceRules.get(key); @@ -323,12 +318,6 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty // inform listeners about removed inference rules (rulesToRemove ?? []).forEach(rule => this.listeners.forEach(listener => listener.removedInferenceRule(rule, type))); } - onAddedEdge(_edge: TypeEdge): void { - // do nothing - } - onRemovedEdge(_edge: TypeEdge): void { - // do nothing - } /* By default, the central cache of Typir is used. */ diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 452b1fd..243205f 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; import { Type, isType } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; @@ -258,18 +257,8 @@ export class DefaultValidationCollector Date: Mon, 27 Jan 2025 10:50:04 +0100 Subject: [PATCH 25/29] refactoring: replaced nested ifs by chain of inverted ifs, improved comments --- .../kinds/function/function-initializer.ts | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 4b4fff5..9a14a1a 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -228,39 +228,37 @@ class FunctionCallInferenceRule implements TypeInferenceRuleWithInferringChil inferTypeWithoutChildren(languageNode: unknown, _typir: TypirServices): unknown { this.assignabilitySuccess.fill(undefined); // reset the entries + // 1. Does the filter of the inference rule accept the current language node? const result = this.typeDetails.inferenceRuleForCalls!.filter(languageNode); - if (result) { - const matching = this.typeDetails.inferenceRuleForCalls!.matching(languageNode); - if (matching) { - const inputArguments = this.typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); - 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 = this.mapNameTypes.get(this.typeDetails.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 this.check(this.getOutputTypeForFunctionCalls()); - } - } else { - // there are no operands to check - return this.check(this.getOutputTypeForFunctionCalls()); - } - } else { - // the language node is slightly different - } - } else { + if (!result) { // the language node has a completely different purpose + return InferenceRuleNotApplicable; + } + // 2. Does the inference rule match this language node? + const matching = this.typeDetails.inferenceRuleForCalls!.matching(languageNode); + if (!matching) { + // the language node is slightly different + return InferenceRuleNotApplicable; + } + // 3. Check whether the current arguments fit to the expected parameter types + const inputArguments = this.typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); + if (inputArguments.length <= 0) { + // there are no operands to check + return this.check(this.getOutputTypeForFunctionCalls()); + } + // at least one operand => this function type might match, to be sure, resolve the types of the values for the parameters + const overloadInfos = this.mapNameTypes.get(this.typeDetails.functionName); + if (overloadInfos === undefined || overloadInfos.overloadedFunctions.length <= 1) { + // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + return this.check(this.getOutputTypeForFunctionCalls()); + } + // two or more overloaded functions + if (overloadInfos.sameOutputType) { + // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! + return overloadInfos.sameOutputType; } - // does not match at all - return InferenceRuleNotApplicable; + // the types of the parameters need to be inferred in order to determine an exact match + return inputArguments; } inferTypeWithChildrensTypes(languageNode: unknown, actualInputTypes: Array, typir: TypirServices): Type | InferenceProblem { From 7852b236f952faaaa20b1ba5b64a9943e78473fe Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 11:04:39 +0100 Subject: [PATCH 26/29] refactoring to enable the exchange of the predefined OverloadedFunctionsTypeInferenceRule, improved comments --- .../src/kinds/function/function-initializer.ts | 18 ++++++++++++++++-- .../typir/src/kinds/function/function-kind.ts | 5 +++-- packages/typir/src/utils/utils.ts | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 9a14a1a..6128d03 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -46,7 +46,7 @@ export class FunctionTypeInitializer extends TypeInitializer im } else { const overloaded: OverloadedFunctionDetails = { overloadedFunctions: [], - inference: new OverloadedFunctionsTypeInferenceRule(this.services), + inference: this.createInferenceRuleForOverloads(), sameOutputType: undefined, }; this.kind.mapNameTypes.set(functionName, overloaded); @@ -62,6 +62,10 @@ export class FunctionTypeInitializer extends TypeInitializer im this.initialFunctionType.addListener(this, true); } + protected createInferenceRuleForOverloads(): CompositeTypeInferenceRule { + return new OverloadedFunctionsTypeInferenceRule(this.services); + } + override getTypeInitial(): FunctionType { return this.initialFunctionType; } @@ -208,7 +212,11 @@ interface FunctionInferenceRules { } -/** Preconditions: +/** + * Dedicated inference rule for calls of a single function signature. + * It takes into account, that all parameters match and provides information, how parameters are matching ('assignabilitySuccess'). + * + * 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) @@ -310,6 +318,12 @@ class FunctionCallInferenceRule implements TypeInferenceRuleWithInferringChil } +/** + * Custom inference rule for functions, which consists of one inference rule for each overload/signature for a function with same name. + * When deadling with multiple inference rules, usually the first successful inference rule is applied and following inference rules are ignored. + * In order to deal with multiple matching inference rules for overloaded functions, + * all available inference rules need to be executed and all successful inference rules need to be collected. + */ export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInferenceRule { protected override inferTypeLogic(languageNode: unknown): Type | InferenceProblem[] { diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index b235432..7272182 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -9,12 +9,13 @@ import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference } from '../../initialization/type-reference.js'; import { TypeSelector } from '../../initialization/type-selector.js'; +import { CompositeTypeInferenceRule } from '../../services/inference.js'; import { ValidationProblem } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { NameTypePair } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy, checkTypes, checkValueForConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { Kind, isKind } from '../kind.js'; -import { FunctionTypeInitializer, OverloadedFunctionsTypeInferenceRule } from './function-initializer.js'; +import { FunctionTypeInitializer } from './function-initializer.js'; import { FunctionType, isFunctionType } from './function-type.js'; @@ -64,7 +65,7 @@ export interface CreateFunctionTypeDetails extends FunctionTypeDetails { export interface OverloadedFunctionDetails { // eslint-disable-next-line @typescript-eslint/no-explicit-any overloadedFunctions: Array>; - inference: OverloadedFunctionsTypeInferenceRule; // collects the inference rules for all functions with the same name + inference: CompositeTypeInferenceRule; // collects the inference rules for all functions with the same name sameOutputType: Type | undefined; // if all overloaded functions with the same name have the same output/return type, this type is remembered here (for performance optimization) } diff --git a/packages/typir/src/utils/utils.ts b/packages/typir/src/utils/utils.ts index e0a5422..649e219 100644 --- a/packages/typir/src/utils/utils.ts +++ b/packages/typir/src/utils/utils.ts @@ -14,7 +14,7 @@ export function assertTrue(condition: boolean, msg?: string): asserts condition } export function toArray(value: undefined | T | T[]): T[] { - if (!value) { + if (value === undefined) { return []; } if (Array.isArray(value)) { From 4417beb47d1af17b88f4d945b96bbcb44020b287 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 27 Jan 2025 11:06:00 +0100 Subject: [PATCH 27/29] added information about this PR into the CHANGELOG.md --- CHANGELOG.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a9b46..dc8e2cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,35 @@ We roughly follow the ideas of [semantic versioning](https://semver.org/). Note that the versions "0.x.0" probably will include breaking changes. -## v0.1.2 (December 2024) +## v0.2.0 (upcoming) + +### New features + +- Users of Typir are able to explicitly define sub-type relationships via the `SubTypeService` (#58) +- Arbitrary paths of implicit conversion and sub-type relationships are considered for assignability now (#58) +- Control the behaviour in case of multiple matching overloads of functions (and operators) (#58) +- Moved the existing graph algorithms into its own dedicated service in order to reuse and to customize them (#58) + +### Breaking changes + +- `TypeConversion.markAsConvertible` accepts only one type for source and target now in order to simplify the API (#58) +- Methods in listeners (`TypeGraphListener`, `TypeStateListener`) are prefixed with `on` (#58) + + +## v0.1.2 (2024-12-20) - Replaced absolute paths in READMEs by relative paths, which is a requirement for correct links on NPM +- Edit: Note that the tag for this release was accidentally added on the branch `jm/v0.1.2`, not on the `main` branch. -## v0.1.1 (December 2024) +## v0.1.1 (2024-12-20) - Improved the READMEs in the packages `typir` and `typir-langium`. - Improved the CONTRIBUTING.md. - Improved source code for Tiny Typir in `api-example.test.ts`. -## v0.1.0 (December 2024) +## v0.1.0 (2024-12-20) This is the first official release of Typir. It serves as first version to experiment with Typir and to gather feedback to guide and improve the upcoming versions. We are looking forward to your feedback! From 1882722f960d6bc5c8e2154a71d1f31cc9a4a7ee Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 28 Jan 2025 16:12:04 +0100 Subject: [PATCH 28/29] simplified assignability test cases --- .../function/operator-overloaded.test.ts | 74 +++++++++---------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/packages/typir/test/kinds/function/operator-overloaded.test.ts b/packages/typir/test/kinds/function/operator-overloaded.test.ts index 287dcb5..9d126cf 100644 --- a/packages/typir/test/kinds/function/operator-overloaded.test.ts +++ b/packages/typir/test/kinds/function/operator-overloaded.test.ts @@ -7,24 +7,28 @@ /* eslint-disable @typescript-eslint/parameter-properties */ import { beforeAll, describe, expect, test } from 'vitest'; -import { assertTrue, ConversionEdge, isAssignabilitySuccess, isPrimitiveType, isType, SubTypeEdge } from '../../../src/index.js'; +import { assertTrue, ConversionEdge, isAssignabilityProblem, isAssignabilitySuccess, isPrimitiveType, PrimitiveType, SubTypeEdge, Type } from '../../../src/index.js'; import { InferenceRuleNotApplicable } from '../../../src/services/inference.js'; import { ValidationMessageDetails } from '../../../src/services/validation.js'; -import { AssignmentStatement, BinaryExpression, booleanFalse, BooleanLiteral, booleanTrue, double123_0, double2_0, double3_0, double456_0, DoubleLiteral, InferenceRuleBinaryExpression, integer123, integer2, integer3, integer456, IntegerLiteral, string123, string2, string3, string456, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; +import { AssignmentStatement, BinaryExpression, booleanFalse, BooleanLiteral, booleanTrue, double2_0, double3_0, DoubleLiteral, InferenceRuleBinaryExpression, integer2, integer3, IntegerLiteral, string2, string3, StringLiteral, TestExpressionNode, Variable } from '../../../src/test/predefined-language-nodes.js'; import { TypirServices } from '../../../src/typir.js'; import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; describe('Multiple best matches for overloaded operators', () => { let typir: TypirServices; + let integerType: PrimitiveType; + let doubleType: PrimitiveType; + let stringType: PrimitiveType; + let booleanType: PrimitiveType; beforeAll(() => { typir = createTypirServicesForTesting(); // primitive types - const integerType = typir.factory.Primitives.create({ primitiveName: 'integer', inferenceRules: node => node instanceof IntegerLiteral }); - const doubleType = typir.factory.Primitives.create({ primitiveName: 'double', inferenceRules: node => node instanceof DoubleLiteral }); - const stringType = typir.factory.Primitives.create({ primitiveName: 'string', inferenceRules: node => node instanceof StringLiteral }); - const booleanType = typir.factory.Primitives.create({ primitiveName: 'boolean', inferenceRules: node => node instanceof BooleanLiteral }); + integerType = typir.factory.Primitives.create({ primitiveName: 'integer', inferenceRules: node => node instanceof IntegerLiteral }); + doubleType = typir.factory.Primitives.create({ primitiveName: 'double', inferenceRules: node => node instanceof DoubleLiteral }); + stringType = typir.factory.Primitives.create({ primitiveName: 'string', inferenceRules: node => node instanceof StringLiteral }); + booleanType = typir.factory.Primitives.create({ primitiveName: 'boolean', inferenceRules: node => node instanceof BooleanLiteral }); // operators typir.factory.Operators.createBinary({ name: '+', signatures: [ // operator overloading @@ -60,70 +64,61 @@ describe('Multiple best matches for overloaded operators', () => { describe('tests all cases for assignability and the checks the found assignability paths', () => { test('integer to integer', () => { - expectAssignmentValid(integer123, integer456); + expectAssignmentValid(integerType, integerType); }); test('double to integer', () => { - expectAssignmentError(double123_0, integer456); + expectAssignmentError(doubleType, integerType); }); test('string to integer', () => { - expectAssignmentError(string123, integer456); + expectAssignmentError(stringType, integerType); }); test('boolean to integer', () => { - expectAssignmentValid(booleanTrue, integer456, 'ConversionEdge'); + expectAssignmentValid(booleanType, integerType, 'ConversionEdge'); }); test('integer to double', () => { - expectAssignmentValid(integer123, double456_0, 'SubTypeEdge'); + expectAssignmentValid(integerType, doubleType, 'SubTypeEdge'); }); test('double to double', () => { - expectAssignmentValid(double123_0, double456_0); + expectAssignmentValid(doubleType, doubleType); }); test('string to double', () => { - expectAssignmentError(string123, double456_0); + expectAssignmentError(stringType, doubleType); }); test('boolean to double', () => { - expectAssignmentValid(booleanTrue, double456_0, 'ConversionEdge', 'SubTypeEdge'); + expectAssignmentValid(booleanType, doubleType, 'ConversionEdge', 'SubTypeEdge'); }); test('integer to string', () => { - expectAssignmentValid(integer123, string456, 'SubTypeEdge', 'ConversionEdge'); + expectAssignmentValid(integerType, stringType, 'SubTypeEdge', 'ConversionEdge'); }); test('double to string', () => { - expectAssignmentValid(double123_0, string456, 'ConversionEdge'); + expectAssignmentValid(doubleType, stringType, 'ConversionEdge'); }); test('string to string', () => { - expectAssignmentValid(string123, string456); + expectAssignmentValid(stringType, stringType); }); test('boolean to string', () => { - expectAssignmentValid(booleanTrue, string456, 'ConversionEdge', 'SubTypeEdge', 'ConversionEdge'); + expectAssignmentValid(booleanType, stringType, 'ConversionEdge', 'SubTypeEdge', 'ConversionEdge'); }); test('integer to boolean', () => { - expectAssignmentError(integer123, booleanFalse); + expectAssignmentError(integerType, booleanType); }); test('double to boolean', () => { - expectAssignmentError(double123_0, booleanFalse); + expectAssignmentError(doubleType, booleanType); }); test('string to boolean', () => { - expectAssignmentError(string123, booleanFalse); + expectAssignmentError(stringType, booleanType); }); test('boolean to boolean', () => { - expectAssignmentValid(booleanTrue, booleanFalse); + expectAssignmentValid(booleanType, booleanType); }); - function expectAssignmentValid(value: TestExpressionNode, variableInitType: TestExpressionNode, ...expectedPath: Array): void { - const variable = new Variable('v1', variableInitType); - const assignment = new AssignmentStatement(variable, value); - expect(typir.validation.Collector.validate(assignment)).toHaveLength(0); - - // do type inference - const valueType = typir.Inference.inferType(value); - assertTrue(isType(valueType)); - const variableType = typir.Inference.inferType(variable); - assertTrue(isType(variableType)); + function expectAssignmentValid(sourceType: Type, targetType: Type, ...expectedPath: Array): void { // check the resulting assignability path - const assignabilityResult = typir.Assignability.getAssignabilityResult(valueType, variableType); + const assignabilityResult = typir.Assignability.getAssignabilityResult(sourceType, targetType); assertTrue(isAssignabilitySuccess(assignabilityResult)); const actualPath = assignabilityResult.path; const msg = `Actual assignability path is ${actualPath.map(e => e.$relation).join(' --> ')}.`; @@ -137,17 +132,14 @@ describe('Multiple best matches for overloaded operators', () => { } // check beginning and end of the path if (actualPath.length >= 1) { - expect(actualPath[0].from).toBe(valueType); - expect(actualPath[actualPath.length - 1].to).toBe(variableType); + expect(actualPath[0].from).toBe(sourceType); + expect(actualPath[actualPath.length - 1].to).toBe(targetType); } } - function expectAssignmentError(value: TestExpressionNode, variableInitType: TestExpressionNode): void { - const variable = new Variable('v1', variableInitType); - const assignment = new AssignmentStatement(variable, value); - const errors = typir.validation.Collector.validate(assignment); - expect(errors).toHaveLength(1); - expect(errors[0].message).includes('is not assignable to'); + function expectAssignmentError(sourceType: Type, targetType: Type): void { + const assignabilityResult = typir.Assignability.getAssignabilityResult(sourceType, targetType); + assertTrue(isAssignabilityProblem(assignabilityResult)); } }); From 145ac57285709ed40959b4ef993166b22c1989fe Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Tue, 28 Jan 2025 16:18:39 +0100 Subject: [PATCH 29/29] refactoring: split existing file into two more files --- .../kinds/function/function-call-inference.ts | 118 +++++++++ .../kinds/function/function-initializer.ts | 223 +----------------- .../function/function-overloaded-inference.ts | 119 ++++++++++ 3 files changed, 241 insertions(+), 219 deletions(-) create mode 100644 packages/typir/src/kinds/function/function-call-inference.ts create mode 100644 packages/typir/src/kinds/function/function-overloaded-inference.ts diff --git a/packages/typir/src/kinds/function/function-call-inference.ts b/packages/typir/src/kinds/function/function-call-inference.ts new file mode 100644 index 0000000..a3e73f4 --- /dev/null +++ b/packages/typir/src/kinds/function/function-call-inference.ts @@ -0,0 +1,118 @@ +/****************************************************************************** + * Copyright 2025 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 { AssignabilitySuccess, isAssignabilityProblem } from '../../services/assignability.js'; +import { TypeInferenceRuleWithInferringChildren, InferenceRuleNotApplicable, InferenceProblem } from '../../services/inference.js'; +import { TypirServices } from '../../typir.js'; +import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; +import { CreateFunctionTypeDetails, OverloadedFunctionDetails } from './function-kind.js'; +import { FunctionType } from './function-type.js'; + +/** + * Dedicated inference rule for calls of a single function signature. + * It takes into account, that all parameters match and provides information, how parameters are matching ('assignabilitySuccess'). + * + * 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) + */ +export class FunctionCallInferenceRule implements TypeInferenceRuleWithInferringChildren { + protected readonly typeDetails: CreateFunctionTypeDetails; + protected readonly functionType: FunctionType; + protected readonly mapNameTypes: Map; + assignabilitySuccess: Array; + + constructor(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType, mapNameTypes: Map) { + this.typeDetails = typeDetails; + this.functionType = functionType; + this.mapNameTypes = mapNameTypes; + this.assignabilitySuccess = new Array(typeDetails.inputParameters.length); + } + + inferTypeWithoutChildren(languageNode: unknown, _typir: TypirServices): unknown { + this.assignabilitySuccess.fill(undefined); // reset the entries + // 1. Does the filter of the inference rule accept the current language node? + const result = this.typeDetails.inferenceRuleForCalls!.filter(languageNode); + if (!result) { + // the language node has a completely different purpose + return InferenceRuleNotApplicable; + } + // 2. Does the inference rule match this language node? + const matching = this.typeDetails.inferenceRuleForCalls!.matching(languageNode); + if (!matching) { + // the language node is slightly different + return InferenceRuleNotApplicable; + } + // 3. Check whether the current arguments fit to the expected parameter types + const inputArguments = this.typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); + if (inputArguments.length <= 0) { + // there are no operands to check + return this.check(this.getOutputTypeForFunctionCalls()); + } + // at least one operand => this function type might match, to be sure, resolve the types of the values for the parameters + const overloadInfos = this.mapNameTypes.get(this.typeDetails.functionName); + if (overloadInfos === undefined || overloadInfos.overloadedFunctions.length <= 1) { + // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + return this.check(this.getOutputTypeForFunctionCalls()); + } + // two or more overloaded functions + if (overloadInfos.sameOutputType) { + // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! + return overloadInfos.sameOutputType; + } + // the types of the parameters need to be inferred in order to determine an exact match + return inputArguments; + } + + inferTypeWithChildrensTypes(languageNode: unknown, actualInputTypes: Array, typir: TypirServices): Type | InferenceProblem { + const expectedInputTypes = this.typeDetails.inputParameters.map(p => typir.infrastructure.TypeResolver.resolve(p.type)); + // all operands need to be assignable(! not equal) to the required types + const comparisonConflicts = checkTypeArrays( + actualInputTypes, + expectedInputTypes, + (t1, t2, index) => { + const result = typir.Assignability.getAssignabilityResult(t1, t2); + if (isAssignabilityProblem(result)) { + return result; + } else { + // save the information equal/conversion/subtype for deciding "conflicts" of overloaded functions + this.assignabilitySuccess[index] = result; + return undefined; + } + }, + true, + ); + if (comparisonConflicts.length >= 1) { + // this function type does not match, due to assignability conflicts => return them as errors + return { + $problem: InferenceProblem, + languageNode: languageNode, + inferenceCandidate: this.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 this.check(this.getOutputTypeForFunctionCalls()); + } + } + + protected getOutputTypeForFunctionCalls(): Type | undefined { + return this.functionType.kind.getOutputTypeForFunctionCalls(this.functionType); + } + + protected check(returnType: Type | undefined): Type { + if (returnType) { // this condition is checked here, since 'undefined' is OK, as long as it is not used; extracting this function is difficult due to TypeScripts strict rules for using 'this' + return returnType; + } else { + throw new Error(`The function ${this.typeDetails.functionName} is called, but has no output type to infer.`); + } + } +} diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 6128d03..5b3ee73 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -6,15 +6,14 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; -import { AssignabilitySuccess, isAssignabilityProblem } from '../../services/assignability.js'; -import { isConversionEdge } from '../../services/conversion.js'; -import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule, TypeInferenceRuleWithInferringChildren } from '../../services/inference.js'; -import { isSubTypeEdge } from '../../services/subtype.js'; +import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; import { ValidationRule } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; -import { assertType, assertUnreachable } from '../../utils/utils.js'; +import { assertType } from '../../utils/utils.js'; +import { FunctionCallInferenceRule } from './function-call-inference.js'; import { CreateFunctionTypeDetails, FunctionKind, OverloadedFunctionDetails } from './function-kind.js'; +import { OverloadedFunctionsTypeInferenceRule } from './function-overloaded-inference.js'; import { FunctionType, isFunctionType } from './function-type.js'; export class FunctionTypeInitializer extends TypeInitializer implements TypeStateListener { @@ -210,217 +209,3 @@ interface FunctionInferenceRules { validationForCall?: ValidationRule; inferenceForDeclaration?: TypeInferenceRule; } - - -/** - * Dedicated inference rule for calls of a single function signature. - * It takes into account, that all parameters match and provides information, how parameters are matching ('assignabilitySuccess'). - * - * 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) - */ -class FunctionCallInferenceRule implements TypeInferenceRuleWithInferringChildren { - protected readonly typeDetails: CreateFunctionTypeDetails; - protected readonly functionType: FunctionType; - protected readonly mapNameTypes: Map; - assignabilitySuccess: Array; - - constructor(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType, mapNameTypes: Map) { - this.typeDetails = typeDetails; - this.functionType = functionType; - this.mapNameTypes = mapNameTypes; - this.assignabilitySuccess = new Array(typeDetails.inputParameters.length); - } - - inferTypeWithoutChildren(languageNode: unknown, _typir: TypirServices): unknown { - this.assignabilitySuccess.fill(undefined); // reset the entries - // 1. Does the filter of the inference rule accept the current language node? - const result = this.typeDetails.inferenceRuleForCalls!.filter(languageNode); - if (!result) { - // the language node has a completely different purpose - return InferenceRuleNotApplicable; - } - // 2. Does the inference rule match this language node? - const matching = this.typeDetails.inferenceRuleForCalls!.matching(languageNode); - if (!matching) { - // the language node is slightly different - return InferenceRuleNotApplicable; - } - // 3. Check whether the current arguments fit to the expected parameter types - const inputArguments = this.typeDetails.inferenceRuleForCalls!.inputArguments(languageNode); - if (inputArguments.length <= 0) { - // there are no operands to check - return this.check(this.getOutputTypeForFunctionCalls()); - } - // at least one operand => this function type might match, to be sure, resolve the types of the values for the parameters - const overloadInfos = this.mapNameTypes.get(this.typeDetails.functionName); - if (overloadInfos === undefined || overloadInfos.overloadedFunctions.length <= 1) { - // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors - return this.check(this.getOutputTypeForFunctionCalls()); - } - // two or more overloaded functions - if (overloadInfos.sameOutputType) { - // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! - return overloadInfos.sameOutputType; - } - // the types of the parameters need to be inferred in order to determine an exact match - return inputArguments; - } - - inferTypeWithChildrensTypes(languageNode: unknown, actualInputTypes: Array, typir: TypirServices): Type | InferenceProblem { - const expectedInputTypes = this.typeDetails.inputParameters.map(p => typir.infrastructure.TypeResolver.resolve(p.type)); - // all operands need to be assignable(! not equal) to the required types - const comparisonConflicts = checkTypeArrays( - actualInputTypes, - expectedInputTypes, - (t1, t2, index) => { - const result = typir.Assignability.getAssignabilityResult(t1, t2); - if (isAssignabilityProblem(result)) { - return result; - } else { - // save the information equal/conversion/subtype for deciding "conflicts" of overloaded functions - this.assignabilitySuccess[index] = result; - return undefined; - } - }, - true, - ); - if (comparisonConflicts.length >= 1) { - // this function type does not match, due to assignability conflicts => return them as errors - return { - $problem: InferenceProblem, - languageNode: languageNode, - inferenceCandidate: this.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 this.check(this.getOutputTypeForFunctionCalls()); - } - } - - protected getOutputTypeForFunctionCalls(): Type | undefined { - return this.functionType.kind.getOutputTypeForFunctionCalls(this.functionType); - } - - protected check(returnType: Type | undefined): Type { - if (returnType) { // this condition is checked here, since 'undefined' is OK, as long as it is not used; extracting this function is difficult due to TypeScripts strict rules for using 'this' - return returnType; - } else { - throw new Error(`The function ${this.typeDetails.functionName} is called, but has no output type to infer.`); - } - } -} - - -/** - * Custom inference rule for functions, which consists of one inference rule for each overload/signature for a function with same name. - * When deadling with multiple inference rules, usually the first successful inference rule is applied and following inference rules are ignored. - * In order to deal with multiple matching inference rules for overloaded functions, - * all available inference rules need to be executed and all successful inference rules need to be collected. - */ -export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInferenceRule { - - protected override inferTypeLogic(languageNode: unknown): Type | InferenceProblem[] { - this.checkForError(languageNode); - - // check all rules in order to search for the best-matching rule, not for the first-matching rule - const matchingOverloads: OverloadedMatch[] = []; - const collectedInferenceProblems: InferenceProblem[] = []; - for (const rules of this.inferenceRules.values()) { - for (const rule of rules) { - const result = this.executeSingleInferenceRuleLogic(rule, languageNode, collectedInferenceProblems); - if (result) { - matchingOverloads.push({ result, rule: rule as FunctionCallInferenceRule }); - } else { - // no result for this inference rule => check the next inference rules - } - } - } - - if (matchingOverloads.length <= 0) { - // no matches => return all the collected inference problems - if (collectedInferenceProblems.length <= 0) { - // document the reason, why neither a type nor inference problems are found - collectedInferenceProblems.push({ - $problem: InferenceProblem, - languageNode: languageNode, - location: 'found no applicable inference rules', - subProblems: [], - }); - } - return collectedInferenceProblems; - } else if (matchingOverloads.length === 1) { - // single match - return matchingOverloads[0].result; - } else { - // multiple matches => determine the one to return - - // 1. identify and collect the best matches - const bestMatches: OverloadedMatch[] = [ matchingOverloads[0] ]; - for (let i = 1; i < matchingOverloads.length; i++) { - const currentMatch = matchingOverloads[i]; - const comparison = this.compareMatchingOverloads(bestMatches[0], currentMatch); - if (comparison < 0) { - // the existing matches are better than the current one => keep the existing best matches - } else if (comparison > 0) { - // the current match is better than the already collect ones => replace the existing best matches by the current one - bestMatches.splice(0, bestMatches.length, currentMatch); - } else { - // the current and the existing matches are both good => collect both - bestMatches.push(currentMatch); - } - } - - // 2. evaluate the remaining best matches - if (bestMatches.length === 0) { - // return the single remaining match - return bestMatches[0].result; - } else { - // decide how to deal with multiple best matches - const result = this.handleMultipleBestMatches(bestMatches); - if (result) { - // return the chosen match - return result.result; - } else { - // no decision => inference is not possible - return [{ - $problem: InferenceProblem, - languageNode: languageNode, - location: `Found ${bestMatches.length} best matching overloads: ${bestMatches.map(m => m.result.getIdentifier()).join(', ')}`, - subProblems: [], // there are no real sub-problems, since the relevant overloads match ... - }]; - } - } - } - } - - protected handleMultipleBestMatches(matchingOverloads: OverloadedMatch[]): OverloadedMatch | undefined { - return matchingOverloads[0]; // by default, return the 1st best match - } - - // better matches are at the beginning of the list, i.e. better matches get values lower than zero - protected compareMatchingOverloads(match1: OverloadedMatch, match2: OverloadedMatch): number { - const cost1 = this.calculateCost(match1); - const cost2 = this.calculateCost(match2); - return cost1 === cost2 ? 0 : cost1 < cost2 ? -1 : +1; - } - - protected calculateCost(match: OverloadedMatch): number { - return match.rule.assignabilitySuccess - .flatMap(s => s?.path ?? []) // collect all conversion/sub-type edges which are required to map actual types to the expected types of the parameters - // equal types (i.e. an empty path) are better than sub-types, sub-types are better than conversions - .map(edge => (isSubTypeEdge(edge) ? 1 : isConversionEdge(edge) ? 2 : assertUnreachable(edge)) as number) - .reduce((l, r) => l + r, 0); - } -} - -interface OverloadedMatch { - result: Type; - rule: FunctionCallInferenceRule; -} diff --git a/packages/typir/src/kinds/function/function-overloaded-inference.ts b/packages/typir/src/kinds/function/function-overloaded-inference.ts new file mode 100644 index 0000000..8a8bce4 --- /dev/null +++ b/packages/typir/src/kinds/function/function-overloaded-inference.ts @@ -0,0 +1,119 @@ +/****************************************************************************** + * Copyright 2025 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 { isConversionEdge } from '../../services/conversion.js'; +import { CompositeTypeInferenceRule, InferenceProblem } from '../../services/inference.js'; +import { isSubTypeEdge } from '../../services/subtype.js'; +import { assertUnreachable } from '../../utils/utils.js'; +import { FunctionCallInferenceRule } from './function-call-inference.js'; + +/** + * Custom inference rule for functions, which consists of one inference rule for each overload/signature for a function with same name. + * When deadling with multiple inference rules, usually the first successful inference rule is applied and following inference rules are ignored. + * In order to deal with multiple matching inference rules for overloaded functions, + * all available inference rules need to be executed and all successful inference rules need to be collected. + */ +export class OverloadedFunctionsTypeInferenceRule extends CompositeTypeInferenceRule { + + protected override inferTypeLogic(languageNode: unknown): Type | InferenceProblem[] { + this.checkForError(languageNode); + + // check all rules in order to search for the best-matching rule, not for the first-matching rule + const matchingOverloads: OverloadedMatch[] = []; + const collectedInferenceProblems: InferenceProblem[] = []; + for (const rules of this.inferenceRules.values()) { + for (const rule of rules) { + const result = this.executeSingleInferenceRuleLogic(rule, languageNode, collectedInferenceProblems); + if (result) { + matchingOverloads.push({ result, rule: rule as FunctionCallInferenceRule }); + } else { + // no result for this inference rule => check the next inference rules + } + } + } + + if (matchingOverloads.length <= 0) { + // no matches => return all the collected inference problems + if (collectedInferenceProblems.length <= 0) { + // document the reason, why neither a type nor inference problems are found + collectedInferenceProblems.push({ + $problem: InferenceProblem, + languageNode: languageNode, + location: 'found no applicable inference rules', + subProblems: [], + }); + } + return collectedInferenceProblems; + } else if (matchingOverloads.length === 1) { + // single match + return matchingOverloads[0].result; + } else { + // multiple matches => determine the one to return + + // 1. identify and collect the best matches + const bestMatches: OverloadedMatch[] = [ matchingOverloads[0] ]; + for (let i = 1; i < matchingOverloads.length; i++) { + const currentMatch = matchingOverloads[i]; + const comparison = this.compareMatchingOverloads(bestMatches[0], currentMatch); + if (comparison < 0) { + // the existing matches are better than the current one => keep the existing best matches + } else if (comparison > 0) { + // the current match is better than the already collect ones => replace the existing best matches by the current one + bestMatches.splice(0, bestMatches.length, currentMatch); + } else { + // the current and the existing matches are both good => collect both + bestMatches.push(currentMatch); + } + } + + // 2. evaluate the remaining best matches + if (bestMatches.length === 0) { + // return the single remaining match + return bestMatches[0].result; + } else { + // decide how to deal with multiple best matches + const result = this.handleMultipleBestMatches(bestMatches); + if (result) { + // return the chosen match + return result.result; + } else { + // no decision => inference is not possible + return [{ + $problem: InferenceProblem, + languageNode: languageNode, + location: `Found ${bestMatches.length} best matching overloads: ${bestMatches.map(m => m.result.getIdentifier()).join(', ')}`, + subProblems: [], // there are no real sub-problems, since the relevant overloads match ... + }]; + } + } + } + } + + protected handleMultipleBestMatches(matchingOverloads: OverloadedMatch[]): OverloadedMatch | undefined { + return matchingOverloads[0]; // by default, return the 1st best match + } + + // better matches are at the beginning of the list, i.e. better matches get values lower than zero + protected compareMatchingOverloads(match1: OverloadedMatch, match2: OverloadedMatch): number { + const cost1 = this.calculateCost(match1); + const cost2 = this.calculateCost(match2); + return cost1 === cost2 ? 0 : cost1 < cost2 ? -1 : +1; + } + + protected calculateCost(match: OverloadedMatch): number { + return match.rule.assignabilitySuccess + .flatMap(s => s?.path ?? []) // collect all conversion/sub-type edges which are required to map actual types to the expected types of the parameters + // equal types (i.e. an empty path) are better than sub-types, sub-types are better than conversions + .map(edge => (isSubTypeEdge(edge) ? 1 : isConversionEdge(edge) ? 2 : assertUnreachable(edge)) as number) + .reduce((l, r) => l + r, 0); + } +} + +interface OverloadedMatch { + result: Type; + rule: FunctionCallInferenceRule; +}