diff --git a/.eslintrc.json b/.eslintrc.json index 906ee788..b4f8ee7f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -58,11 +58,11 @@ "@typescript-eslint/ban-types": "error", // bans types like String in favor of string "@typescript-eslint/no-inferrable-types": "off", // don't blame decls like "index: number = 0", esp. in api signatures! "@typescript-eslint/indent": "error", // consistent indentation - //"@typescript-eslint/no-explicit-any": "error", // don't use :any type + //"@typescript-eslint/no-explicit-any": "error", // don't use :any type "@typescript-eslint/no-misused-new": "error", // no constructors for interfaces or new for classes - "@typescript-eslint/no-namespace": "off", // disallow the use of custom TypeScript modules and namespaces + "@typescript-eslint/no-namespace": "off", // disallow the use of custom TypeScript modules and namespaces "@typescript-eslint/no-non-null-assertion": "off", // allow ! operator - "@typescript-eslint/parameter-properties": "error", // no property definitions in class constructors + "@typescript-eslint/parameter-properties": "off", // no property definitions in class constructors "@typescript-eslint/no-unused-vars": ["error", { // disallow Unused Variables "argsIgnorePattern": "^_" }], diff --git a/.vscode/settings.json b/.vscode/settings.json index 72446f43..bab8c938 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.autoImportFileExcludePatterns": [ + "src/index.js", + ], } diff --git a/CHANGELOG.md b/CHANGELOG.md index 08082f4d..5c55b36d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ We roughly follow the ideas of [semantic versioning](https://semver.org/). Note that the versions "0.x.0" probably will include breaking changes. +For each minor and major version, there is a corresponding [milestone on GitHub](https://github.com/TypeFox/typir/milestones). ## v0.3.0 (2025-??-??) @@ -14,6 +15,35 @@ Note that the versions "0.x.0" probably will include breaking changes. - Fixed the implementation for merging modules for dependency injection, it is exactly the same fix from [Langium](https://github.com/eclipse-langium/langium/pull/1939), since we reused its DI implementation (#79). +## v0.3.0 (2025-??-??) + +[Linked issues and PRs for v0.3.0](https://github.com/TypeFox/typir/milestone/4) + +### New features + +- New API to support custom types, i.e. types which are not predefined by Typir, but are created by users of Typir and tailored to the current language (#73): + - Supports custom properties with arrays, sets, maps, primitives and types + - Create a new `CustomKind` and use it to create corresponding `CustomType`s, which support the desired custom properties in TypeScript-safe way + - Type-specific names, user representations, inference rules and validation rules + - Specific rules for conversion and sub-type which are applied to all custom types + - Builtin support for dependencies between probably delayed (custom) types and unique custom types + - See some examples in `packages/typir/test/kinds/custom/custom-matrix.test.ts` and `packages/typir/test/kinds/custom/custom-restricted.test.ts` +- If you try to create a function type, class type or custom type a second time, the existing implementation already ensured, that the already existing type is reused and no new type is created (#73): + - For the type-specific inference rules, there is now an additional property `skipThisRuleIfThisTypeAlreadyExists` (in `InferCurrentTypeRule`) to control, whether these given inference rules for the "second new type" should be added to the existing type or whether they should be skipped. + - The default value is `false`, meaning that these type-specific inference (and validation) rules are attached to the existing type. That conforms to the behaviour before introducing this new property. +- ... + +### Breaking changes + +- ... + +### Fixed bugs + +- Clear edges from invalid types, which are never added into the type graph (#73) +- The properties of all types are `readonly` now (#73) +- ... + + ## v0.2.1 (2025-04-09) - Export `test-utils.ts` which are using `vitest` via the new namespace `'typir/test'` in order to not pollute production code with vitest dependencies (#68) @@ -21,6 +51,8 @@ Note that the versions "0.x.0" probably will include breaking changes. ## v0.2.0 (2025-03-31) +[Linked issues and PRs for v0.2.0](https://github.com/TypeFox/typir/milestone/3) + ### New features - Users of Typir are able to explicitly define sub-type relationships via the `SubTypeService.markAsSubType(subType, superType)` now (#58) @@ -91,7 +123,7 @@ Note that the versions "0.x.0" probably will include breaking changes. 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! -- [Linked issues and PRs](https://github.com/TypeFox/typir/milestone/2) +- [Linked issues and PRs for v0.1.0](https://github.com/TypeFox/typir/milestone/2) - Core implementations of the following [type-checking services](./packages/typir/src/services/): - Assignability - Equality diff --git a/documentation/index.md b/documentation/index.md index a8d36515..e21efc08 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -14,9 +14,18 @@ This describes the structure and the main content of the documentation for Typir - [Type inference](./services/inference.md) - ... + ## Predefined types -- ... +The current set of predefined types: + +- Top and bottom types +- Primitive types +- Structurally typed classes +- [Custom types](./kinds/custom-types.md) +- Function types +- Operators (are internally mappped to function types) + ## Bindings @@ -44,6 +53,6 @@ This repository contains the following stand-alone applications. Read their link - [LOX](./examples/lox/README.md) - static type checking for LOX, implemented with Typir-Langium - [OX](./examples/ox/README.md) - a reduced version of LOX, implemented with Typir-Langium -- Expressions - TODO +- [Expressions](./examples/expression.README.md) - static type checking for a hand-written reduced expression language, implemented with Typir (core) Some of the internal test cases developed in [packages/typir/test/](../packages/typir/test/) demonstrate some features of Typir in more detail. diff --git a/documentation/kinds/custom-types.md b/documentation/kinds/custom-types.md index e06096e2..f87cb0b8 100644 --- a/documentation/kinds/custom-types.md +++ b/documentation/kinds/custom-types.md @@ -3,5 +3,92 @@ Many languages contain features which cannot be easily described with the predefined types. This describes how language-specific custom types can be defined and used in Typir. -TODO +## API by example + +This section demonstrates the API to define custom types along the example extracted from `custom-example-matrix.test.ts`. Here a new mathematical matrix type is defined with `width` and `height`, which is similar to a two-dimensional array. The content of cells are primitive types. + +First of all, you need to specify the properties of matrix types in Typir by creating a TypeScript type (note that `interface` instead of `type` does not work): + +```typescript +type MatrixType = { + baseType: PrimitiveType; + width: number; + height: number; +}; +``` + +Then you create a new factory for these matrix types: + +```typescript +const matrixFactory = new CustomKind(typir, { + name: 'Matrix', + calculateTypeName: properties => `My${properties.width}x${properties.height}Matrix`, + // ... here you can specify additional rules for conversion, sub-types, ... for all matrix types ... +}); +``` + +Now you can use this factory to create new matrix types: + +```typescript +const matrix2x3 = matrixFactory + .create({ properties: { baseType: integerType, width: 2, height: 3 } }) + .finish().getTypeFinal()!; +``` + +See `custom-example-restricted.test.ts` for another application example. + + +## Features + +This sections describes the features of custom types in more detail. + +### Custom properties + +Custom types have custom properties ("data") including primitive values, Typir types and nesting/grouping with sets, arrays, and maps, and recursion. +See `custom-nested-properties.test.ts` for some examples. +When the initialization of the custom type is done, all its properties are read-only. + +### Support by the TypeScript compiler + +The API for custom types uses TypeScript generics to enable TypeScript-safe descriptions for these custom properties. +In the example above, calling `matrix2x3.properties.width` is supported by auto-completion in the IDE and will return the number `2`. + +### Uniqueness + +Typir ensures uniqueness for custom types. +Two custom types are identical, if their identifiers are the same (this counts for any type, not only for custom types). +The default implementation calculates the identifier by concatenating the values of all properties. + +### Circular dependencies + +Cyclic dependencies of the given types for type properties are handled by Typir. +See some examples in `custom-cycles.test.ts` and `custom-selectors.test.ts`. +Therefore `getTypeFinal()` needs to be called after finishing a new custom type, e.g. `const myCustomType = customKind.create({...}).finish().getTypeFinal();`. +If the custom type is already available, you will get your `CustomType`, otherwise `undefined`. +If the type is not yet available, you can register a callback, which is called, when the type is available: + +```typescript +customKind.create({...}).finish().addListener(finishedType => { + // here the new custom type is available and can be used as usual + finishedType.getIdentifier(); +}); +``` + +### Behaviour + +Specify rules for conversion, sub-type, names, identifiers, inference and validation (usually for all custom types OR for single ones). +See `custom-example-restricted.test.ts` for some examples. + +### Multiple different custom types + +You can use different factories for different custom types in parallel within the same Typir instance. +See `custom-independent.test.ts` for an example. + + +## Limitations + +- You cannot use simple string values for `TypeSelector`s (in order to specify custom properties of type `Type`), since they cannot be distinguished from string values for primitive custom properties. + Therefore, only the restricted `TypeSelectorForCustomTypes` is supported by custom types instead of the usual `TypeSelector`. +- Even if your custom type does not depend on other types or if you know, that the types your custom type depends on are already available, + you need to call `getTypeFinal()`, e.g. `const myCustomType = customKind.create({...}).finish().getTypeFinal()!;`. diff --git a/packages/typir-langium/src/features/langium-language.ts b/packages/typir-langium/src/features/langium-language.ts index 81251a22..9a5d9c61 100644 --- a/packages/typir-langium/src/features/langium-language.ts +++ b/packages/typir-langium/src/features/langium-language.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AbstractAstReflection, AstNode } from 'langium'; +import { AbstractAstReflection, AstNode, isAstNode } from 'langium'; import { DefaultLanguageService, LanguageService, removeFromArray } from 'typir'; /** @@ -53,4 +53,8 @@ export class LangiumLanguageService extends DefaultLanguageService impl return this.superKeys.get(languageKey) ?? []; } + override isLanguageNode(node: unknown): node is AstNode { + return isAstNode(node); + } + } diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index 370611f1..a30b24ab 100644 --- a/packages/typir-langium/src/typir-langium.ts +++ b/packages/typir-langium/src/typir-langium.ts @@ -34,6 +34,7 @@ export type TypirLangiumServices = TypirServic export type PartialTypirLangiumServices = DeepPartial> + /** * Creates a module that replaces some implementations of the core Typir services in order to be used with Langium. * @param langiumServices Typir-Langium needs to interact with the Langium lifecycle diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index e7599c75..b223a383 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -39,7 +39,7 @@ export class TypeGraph { if (this.nodes.get(mapKey) === type) { // this type is already registered => that is OK } else { - throw new Error(`Names of types must be unique: ${mapKey}`); + throw new Error(`There is already a type with the identifier '${mapKey}'.`); } } else { this.nodes.set(mapKey, type); @@ -121,8 +121,11 @@ export class TypeGraph { // register listeners for changed types/edges in the type graph - addListener(listener: TypeGraphListener): void { + addListener(listener: TypeGraphListener, options?: { callOnAddedForAllExisting: boolean }): void { this.listeners.push(listener); + if (options?.callOnAddedForAllExisting && listener.onAddedType) { + this.nodes.forEach((type, key) => listener.onAddedType!.call(listener, type, key)); + } } removeListener(listener: TypeGraphListener): void { removeFromArray(listener, this.listeners); diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index 89620b1d..dd02761b 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -289,6 +289,11 @@ export abstract class Type { this.waitForIdentifiable.deconstruct(); this.waitForCompleted.deconstruct(); this.waitForInvalid.deconstruct(); + // edges are already removed, when the type is removed from the graph, + // but in some cases, the type was not (yet) added to the graph, but got already edges => these edges need to be removed now + // e.g. "duplicated" types which are created and disposed by TypeInitializers + this.edgesIncoming.clear(); + this.edgesOutgoing.clear(); } protected switchFromInvalidToIdentifiable(): void { diff --git a/packages/typir/src/index.ts b/packages/typir/src/index.ts index 3c4023c5..bb72f81e 100644 --- a/packages/typir/src/index.ts +++ b/packages/typir/src/index.ts @@ -20,6 +20,10 @@ export * from './kinds/class/class-type.js'; export * from './kinds/class/class-validation.js'; export * from './kinds/class/top-class-kind.js'; export * from './kinds/class/top-class-type.js'; +export * from './kinds/custom/custom-definitions.js'; +export * from './kinds/custom/custom-initializer.js'; +export * from './kinds/custom/custom-kind.js'; +export * from './kinds/custom/custom-type.js'; export * from './kinds/fixed-parameters/fixed-parameters-kind.js'; export * from './kinds/fixed-parameters/fixed-parameters-type.js'; export * from './kinds/function/function-initializer.js'; diff --git a/packages/typir/src/initialization/type-selector.ts b/packages/typir/src/initialization/type-selector.ts index c67f2f58..656756b2 100644 --- a/packages/typir/src/initialization/type-selector.ts +++ b/packages/typir/src/initialization/type-selector.ts @@ -12,7 +12,7 @@ import { TypeReference } from './type-reference.js'; // TODO find better names: TypeSpecification, TypeDesignation/Designator, ... ? export type BasicTypeSelector = | T // the wanted type - | string // identifier of the type (to be searched in the type graph/map) + | string // identifier of the type (to be searched in the type graph) | TypeInitializer // delayed creation of types | TypeReference // reference to a (maybe delayed) type | LanguageType // language node to infer the final type from diff --git a/packages/typir/src/kinds/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index 754ee187..11ad254f 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -61,7 +61,7 @@ export class BottomKind implements Kind, BottomFactoryService): BottomConfigurationChain { - assertTrue(this.get(typeDetails) === undefined); + assertTrue(this.get(typeDetails) === undefined, 'The bottom type already exists.'); return new BottomConfigurationChainImpl(this.services, this, typeDetails); } diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index d28fc476..44945ceb 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -21,24 +21,20 @@ export class BottomType extends Type implements TypeGraphListener { // 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 + graph.addListener(this, { callOnAddedForAllExisting: true }); } override dispose(): void { this.kind.services.infrastructure.Graph.removeListener(this); } - protected markAsSubType(type: Type): void { + onAddedType(type: Type, _key: string): void { + // this method is called for the already existing types and for all upcomping types if (type !== this) { this.kind.services.Subtype.markAsSubType(this, type, { checkForCycles: false }); } } - onAddedType(type: Type, _key: string): void { - this.markAsSubType(type); - } - override getName(): string { return this.getIdentifier(); } diff --git a/packages/typir/src/kinds/class/class-initializer.ts b/packages/typir/src/kinds/class/class-initializer.ts index e264ecce..d016e1a9 100644 --- a/packages/typir/src/kinds/class/class-initializer.ts +++ b/packages/typir/src/kinds/class/class-initializer.ts @@ -8,7 +8,7 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; -import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; +import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; import { checkNameTypesMap, createTypeCheckStrategy, MapListConverter } from '../../utils/utils-type-comparison.js'; import { assertTypirType, toArray } from '../../utils/utils.js'; import { ClassKind, CreateClassTypeDetails, InferClassLiteral } from './class-kind.js'; @@ -17,9 +17,10 @@ import { ClassType, isClassType } from './class-type.js'; export class ClassTypeInitializer extends TypeInitializer implements TypeStateListener { protected readonly typeDetails: CreateClassTypeDetails; protected readonly kind: ClassKind; + protected readonly initialClassType: ClassType; + protected inferenceRules: Array> = []; protected validationRules: Array> = []; - protected initialClassType: ClassType; constructor(services: TypirServices, kind: ClassKind, typeDetails: CreateClassTypeDetails) { super(services); @@ -33,10 +34,9 @@ export class ClassTypeInitializer extends TypeInitializer services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, undefined))); - this.validationRules.forEach(rule => services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, undefined))); + this.registerRules(undefined); this.initialClassType.addListener(this, true); // trigger directly, if some initialization states are already reached! } @@ -63,23 +63,18 @@ export class ClassTypeInitializer extends TypeInitializer this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, undefined))); - this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, optionsBoundToType(rule.options, undefined))); + this.deregisterRules(undefined); // but re-create the inference rules for the new type!! // This is required, since inference rules for different declarations in the AST might be different, but should infer the same Typir type! - this.createInferenceAndValidationRules(this.typeDetails, readyClassType); + this.createRules(this.typeDetails, readyClassType); // add the new rules - this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, readyClassType))); - this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, readyClassType))); + this.registerRules(readyClassType); } else { // the class type is unchanged (this is the usual case) // keep the existing inference rules, but register it for the unchanged class type - this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, undefined))); - this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, optionsBoundToType(rule.options, undefined))); - - this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, readyClassType))); - this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, readyClassType))); + this.deregisterRules(undefined); + this.registerRules(readyClassType); } } @@ -105,37 +100,46 @@ export class ClassTypeInitializer extends TypeInitializer, classType: ClassType): void { + protected createRules(typeDetails: CreateClassTypeDetails, classType: ClassType): void { // clear the current list ... this.inferenceRules.splice(0, this.inferenceRules.length); this.validationRules.splice(0, this.validationRules.length); // ... and recreate all rules - for (const inferenceRulesForClassDeclaration of typeDetails.inferenceRulesForClassDeclaration) { - this.inferenceRules.push(bindInferCurrentTypeRule(inferenceRulesForClassDeclaration, classType)); + for (const inferenceRuleForClassDeclaration of typeDetails.inferenceRulesForClassDeclaration) { + if (skipInferenceRuleForExistingType(inferenceRuleForClassDeclaration, this.initialClassType, classType)) { + continue; + } + this.inferenceRules.push(bindInferCurrentTypeRule(inferenceRuleForClassDeclaration, classType)); // TODO check values for fields for structual typing! - const validationRule = bindValidateCurrentTypeRule(inferenceRulesForClassDeclaration, classType); + const validationRule = bindValidateCurrentTypeRule(inferenceRuleForClassDeclaration, classType); if (validationRule) { this.validationRules.push(validationRule); } } - for (const inferenceRulesForClassLiterals of typeDetails.inferenceRulesForClassLiterals) { - this.inferenceRules.push(this.createInferenceRuleForLiteral(inferenceRulesForClassLiterals, classType)); - const validationRule = this.createValidationRuleForLiteral(inferenceRulesForClassLiterals, classType); + for (const inferenceRuleForClassLiterals of typeDetails.inferenceRulesForClassLiterals) { + if (skipInferenceRuleForExistingType(inferenceRuleForClassLiterals, this.initialClassType, classType)) { + continue; + } + this.inferenceRules.push(this.createInferenceRuleForLiteral(inferenceRuleForClassLiterals, classType)); + const validationRule = this.createValidationRuleForLiteral(inferenceRuleForClassLiterals, classType); if (validationRule) { this.validationRules.push(validationRule); } } - for (const inferenceRulesForFieldAccess of typeDetails.inferenceRulesForFieldAccess) { + for (const inferenceRuleForFieldAccess of typeDetails.inferenceRulesForFieldAccess) { + if (skipInferenceRuleForExistingType(inferenceRuleForFieldAccess, this.initialClassType, classType)) { + continue; + } this.inferenceRules.push({ rule: (languageNode, _typir) => { - if (inferenceRulesForFieldAccess.filter !== undefined && inferenceRulesForFieldAccess.filter(languageNode) === false) { + if (inferenceRuleForFieldAccess.filter !== undefined && inferenceRuleForFieldAccess.filter(languageNode) === false) { return InferenceRuleNotApplicable; } - if (inferenceRulesForFieldAccess.matching !== undefined && inferenceRulesForFieldAccess.matching(languageNode, classType) === false) { + if (inferenceRuleForFieldAccess.matching !== undefined && inferenceRuleForFieldAccess.matching(languageNode, classType) === false) { return InferenceRuleNotApplicable; } - const result = inferenceRulesForFieldAccess.field(languageNode); + const result = inferenceRuleForFieldAccess.field(languageNode); if (result === InferenceRuleNotApplicable) { return InferenceRuleNotApplicable; } else if (typeof result === 'string') { @@ -157,21 +161,21 @@ export class ClassTypeInitializer extends TypeInitializer= 1) { this.validationRules.push({ rule: (languageNode, accept, typir) => { - if (inferenceRulesForFieldAccess.filter !== undefined && inferenceRulesForFieldAccess.filter(languageNode) === false) { + if (inferenceRuleForFieldAccess.filter !== undefined && inferenceRuleForFieldAccess.filter(languageNode) === false) { return; } - if (inferenceRulesForFieldAccess.matching !== undefined && inferenceRulesForFieldAccess.matching(languageNode, classType) === false) { + if (inferenceRuleForFieldAccess.matching !== undefined && inferenceRuleForFieldAccess.matching(languageNode, classType) === false) { return; } - const field = inferenceRulesForFieldAccess.field(languageNode); + const field = inferenceRuleForFieldAccess.field(languageNode); if (field === InferenceRuleNotApplicable) { return; } @@ -185,7 +189,7 @@ export class ClassTypeInitializer extends TypeInitializer rule(languageNode, classType, accept, typir)); }, options: { - languageKey: inferenceRulesForFieldAccess.languageKey, + languageKey: inferenceRuleForFieldAccess.languageKey, // boundToType: ... this property will be specified outside of this method }, }); @@ -298,4 +302,14 @@ export class ClassTypeInitializer extends TypeInitializer this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, classType))); + this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, classType))); + } + + protected deregisterRules(classType: ClassType | undefined): void { + this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, classType))); + this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, optionsBoundToType(rule.options, classType))); + } + } diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index ff210a93..a61d9784 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -14,8 +14,8 @@ import { FunctionType } from '../function/function-type.js'; import { ClassKind, ClassTypeDetails, isClassKind } from './class-kind.js'; export interface FieldDetails { - name: string; - type: TypeReference; + readonly name: string; + readonly type: TypeReference; } /** @@ -25,7 +25,7 @@ export interface FieldDetails { * This interfaces makes annotating further properties to methods easier (which are not supported by functions). */ export interface MethodDetails { - type: TypeReference; + readonly type: TypeReference; // methods might have some more properties in the future } @@ -36,7 +36,7 @@ export class ClassType extends Type { protected superClasses: Array>; // if necessary, the array could be replaced by Map: name/form -> ClassType, for faster look-ups protected readonly subClasses: ClassType[] = []; // additional sub classes might be added later on! protected readonly fields: Map = new Map(); // unordered - protected methods: MethodDetails[]; // unordered + protected readonly methods: MethodDetails[]; // unordered constructor(kind: ClassKind, typeDetails: ClassTypeDetails) { super(kind.options.typing === 'Nominal' diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 8c0a8d92..8bb5af11 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -22,24 +22,19 @@ export class TopClassType extends Type implements TypeGraphListener { // 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 + graph.addListener(this, { callOnAddedForAllExisting: true }); } override dispose(): void { this.kind.services.infrastructure.Graph.removeListener(this); } - protected markAsSubType(type: Type): void { + onAddedType(type: Type, _key: string): void { if (type !== this && isClassType(type)) { this.kind.services.Subtype.markAsSubType(type, this, { checkForCycles: false }); } } - onAddedType(type: Type, _key: string): void { - this.markAsSubType(type); - } - override getName(): string { return this.getIdentifier(); } diff --git a/packages/typir/src/kinds/custom/custom-definitions.ts b/packages/typir/src/kinds/custom/custom-definitions.ts new file mode 100644 index 00000000..89e54f9e --- /dev/null +++ b/packages/typir/src/kinds/custom/custom-definitions.ts @@ -0,0 +1,72 @@ +/****************************************************************************** + * 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. +******************************************************************************/ + +/* eslint-disable @typescript-eslint/indent */ + +import { Type } from '../../graph/type-node.js'; +import { TypeReference } from '../../initialization/type-reference.js'; +import { TypeSelector } from '../../initialization/type-selector.js'; + +/* Base properties */ + +export type CustomTypeProperties = { + [key: string]: CustomTypePropertyTypes +}; +// all properties might be optional or mandatory, this is kept in the derived types! + +export type CustomTypePropertyTypes = + | Type + | string | number | boolean | bigint | symbol + | CustomTypePropertyTypes[] | Map | Set + | CustomTypeProperties // recursive nesting + ; + + +/* Corresponding properties for specification during the initialization */ + +/** + * TypeSelectors for custom types don't support strings, since they shall by used as primitive properties (and uncertainty needs to be prevented!). + * As a workaround, encode the string value as a function, e.g. "() => 'MyIndentifer'". + */ +export type TypeSelectorForCustomTypes = Exclude, string>; + +export type CustomTypePropertyInitialization = + /* replace Type by a TypeSelector for it ... + * (Note this special case: If the LanguageType is set to "unknown", then the TypeSelector includes "unknown", + * which makes the TypeScript type-checking "useless" here, i.e. the TypeScript compiler allows you to use any value here (e.g. 'true') which does not work in general! + * Therefore "unknown" should not be used for LanguageType if possible.) */ + T extends Type ? TypeSelectorForCustomTypes : + // unchanged for the atomic cases: + T extends (string | number | boolean | bigint | symbol) ? T : + // ... in recursive way for the composites: + T extends Array ? (ValueType extends CustomTypePropertyTypes ? Array> : never) : + T extends Map ? (ValueType extends CustomTypePropertyTypes ? Map> : never) : + T extends Set ? (ValueType extends CustomTypePropertyTypes ? Set> : never) : + T extends CustomTypeProperties ? CustomTypeInitialization : + never; + +export type CustomTypeInitialization = { + [P in keyof T]: CustomTypePropertyInitialization; +}; + + +/* Corresponding read-only properties to store inside the type */ + +export type CustomTypePropertyStorage = + // replace Type by a TypeReference to it ... + T extends Type ? TypeReference : + // unchanged for the atomic cases: + T extends (string | number | boolean | bigint | symbol) ? T : + // ... in recursive way for the composites: + T extends Array ? (ValueType extends CustomTypePropertyTypes ? ReadonlyArray> : never) : + T extends Map ? (ValueType extends CustomTypePropertyTypes ? ReadonlyMap> : never) : + T extends Set ? (ContentType extends CustomTypePropertyTypes ? ReadonlySet> : never) : + T extends CustomTypeProperties ? CustomTypeStorage : + never; + +export type CustomTypeStorage = { + readonly [P in keyof T]: CustomTypePropertyStorage; +}; diff --git a/packages/typir/src/kinds/custom/custom-initializer.ts b/packages/typir/src/kinds/custom/custom-initializer.ts new file mode 100644 index 00000000..c0c40fd0 --- /dev/null +++ b/packages/typir/src/kinds/custom/custom-initializer.ts @@ -0,0 +1,167 @@ +/****************************************************************************** + * 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 { TypeGraphListener } from '../../graph/type-graph.js'; +import { Type, TypeStateListener } from '../../graph/type-node.js'; +import { TypeInitializer } from '../../initialization/type-initializer.js'; +import { MarkSubTypeOptions } from '../../services/subtype.js'; +import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; +import { assertTrue, assertTypirType } from '../../utils/utils.js'; +import { CustomTypeProperties } from './custom-definitions.js'; +import { CreateCustomTypeDetails, CustomKind } from './custom-kind.js'; +import { CustomType, isCustomType } from './custom-type.js'; + +export class CustomTypeInitializer + extends TypeInitializer, LanguageType> + implements TypeStateListener, TypeGraphListener +{ + protected readonly kind: CustomKind; + protected readonly typeDetails: CreateCustomTypeDetails; + protected readonly initialCustomType: CustomType; + + protected inferenceRules: Array> = []; + protected validationRules: Array> = []; + + constructor(kind: CustomKind, typeDetails: CreateCustomTypeDetails) { + super(kind.services); + this.kind = kind; + this.typeDetails = typeDetails; + + // create the new Custom type + this.initialCustomType = new CustomType(kind, typeDetails); + + // inference rules + this.createRules(this.initialCustomType); + // register all the inference rules already now to enable early type inference for this Custom type ('undefined', since its Identifier is still missing) + this.registerRules(undefined); + + this.initialCustomType.addListener(this, true); + } + + override getTypeInitial(): CustomType { + return this.initialCustomType; + } + + onSwitchedToIdentifiable(customType: Type): void { + assertTypirType(customType, type => isCustomType(type, this.initialCustomType.kind)); + assertTrue(customType === this.initialCustomType); + const readyCustomType = this.producedType(customType); + if (readyCustomType !== customType) { + // check some additional properties to be unique + if (readyCustomType.getName() !== customType.getName()) { + throw new Error(`There is already a custom type '${readyCustomType.getIdentifier()}' with name '${readyCustomType.getName()}', but now the name is '${customType.getName()}'!`); + } + if (readyCustomType.getUserRepresentation() !== customType.getUserRepresentation()) { + throw new Error(`There is already a custom type '${readyCustomType.getIdentifier()}' with user representation '${readyCustomType.getUserRepresentation()}', but now the user representation is '${customType.getUserRepresentation()}'!`); + } + customType.removeListener(this); + this.deregisterRules(undefined); + this.createRules(readyCustomType); + this.registerRules(readyCustomType); + } else { + this.deregisterRules(undefined); + this.registerRules(readyCustomType); + } + + // This logic could be called also after creating the type or after completing it instead. + // Benefit here: The final type is already produced and its identifier is usable, but it might not yet been completed! + this.handleEdgeRelationshipsOfNewType(); + } + + onSwitchedToCompleted(_customType: Type): void { + this.initialCustomType.removeListener(this); + this.services.infrastructure.Graph.removeListener(this); + } + + onSwitchedToInvalid(_customType: Type): void { + // nothing special required here + } + + protected handleEdgeRelationshipsOfNewType(): void { + // handle relationships of the new custom type to existing and known types + const newCustomType = this.getTypeFinal() ?? this.getTypeInitial(); + const options = this.kind.options; + + // sub-type + const subTypeOptions: Partial = { checkForCycles: false }; + (options?.getSubTypesOfNewCustomType?.call(options.getSubTypesOfNewCustomType, newCustomType) ?? []) + .forEach(subType => this.services.Subtype.markAsSubType(subType, newCustomType, subTypeOptions)); + (options?.getSuperTypesOfNewCustomType?.call(options.getSuperTypesOfNewCustomType, newCustomType) ?? []) + .forEach(superType => this.services.Subtype.markAsSubType(newCustomType, superType, subTypeOptions)); + + // conversion + (options?.getNewCustomTypeImplicitlyConvertibleToTypes?.call(options.getNewCustomTypeImplicitlyConvertibleToTypes, newCustomType) ?? []) + .forEach(to => this.services.Conversion.markAsConvertible(newCustomType, to, 'IMPLICIT_EXPLICIT')); + (options?.getNewCustomTypeExplicitlyConvertibleToTypes?.call(options.getNewCustomTypeExplicitlyConvertibleToTypes, newCustomType) ?? []) + .forEach(to => this.services.Conversion.markAsConvertible(newCustomType, to, 'EXPLICIT')); + (options?.getTypesImplicitlyConvertibleToNewCustomType?.call(options.getTypesImplicitlyConvertibleToNewCustomType, newCustomType) ?? []) + .forEach(from => this.services.Conversion.markAsConvertible(from, newCustomType, 'IMPLICIT_EXPLICIT')); + (options?.getTypesExplicitlyConvertibleToNewCustomType?.call(options.getTypesExplicitlyConvertibleToNewCustomType, newCustomType) ?? []) + .forEach(from => this.services.Conversion.markAsConvertible(from, newCustomType, 'EXPLICIT')); + + // handle relationships of the new custom type to types which are not known in advance + if (options.isNewCustomTypeSubTypeOf || options.isNewCustomTypeSuperTypeOf || + options.isNewCustomTypeConvertibleToType || options.isTypeConvertibleToNewCustomType + ) { + this.services.infrastructure.Graph.addListener(this, { callOnAddedForAllExisting: true }); + } + } + + onAddedType(newOtherType: Type, _key: string): void { + const newCustomType = this.getTypeFinal() ?? this.getTypeInitial(); + if (newOtherType !== newCustomType) { // don't relate the new custom type to itself + const options = this.kind.options; + + // sub-type + if (options.isNewCustomTypeSubTypeOf?.call(options.isNewCustomTypeSubTypeOf, newCustomType, newOtherType)) { + this.services.Subtype.markAsSubType(newCustomType, newOtherType, { checkForCycles: false }); + } + if (options.isNewCustomTypeSuperTypeOf?.call(options.isNewCustomTypeSuperTypeOf, newOtherType, newCustomType)) { + this.services.Subtype.markAsSubType(newOtherType, newCustomType, { checkForCycles: false }); + } + + // conversion + const convertCustomToOther = options.isNewCustomTypeConvertibleToType?.call(options.isNewCustomTypeConvertibleToType, newCustomType, newOtherType) ?? 'NONE'; + if (convertCustomToOther === 'IMPLICIT_EXPLICIT' || convertCustomToOther === 'EXPLICIT') { + this.services.Conversion.markAsConvertible(newCustomType, newOtherType, convertCustomToOther); + } + const convertOtherToCustom = options.isTypeConvertibleToNewCustomType?.call(options.isTypeConvertibleToNewCustomType, newOtherType, newCustomType) ?? 'NONE'; + if (convertOtherToCustom === 'IMPLICIT_EXPLICIT' || convertOtherToCustom === 'EXPLICIT') { + this.services.Conversion.markAsConvertible(newOtherType, newCustomType, convertOtherToCustom); + } + } + } + + protected createRules(customType: CustomType): void { + // clear the current list ... + this.inferenceRules.splice(0, this.inferenceRules.length); + this.validationRules.splice(0, this.validationRules.length); + + // ... and recreate all rules + for (const inferenceRule of this.typeDetails.inferenceRules) { + if (skipInferenceRuleForExistingType(inferenceRule, this.initialCustomType, customType)) { // this means: the 'initialCustomType' is the newly created type, the 'customType' is the already existing type + // don't create (additional) rules for the already existing type + continue; + } + this.inferenceRules.push(bindInferCurrentTypeRule(inferenceRule, customType)); + const validate = bindValidateCurrentTypeRule(inferenceRule, customType); + if (validate) { + this.validationRules.push(validate); + } + } + } + + protected registerRules(customType: CustomType | undefined): void { + this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, customType))); + this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, customType))); + } + + protected deregisterRules(customType: CustomType | undefined): void { + this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, customType))); + this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, optionsBoundToType(rule.options, customType))); + } + +} diff --git a/packages/typir/src/kinds/custom/custom-kind.ts b/packages/typir/src/kinds/custom/custom-kind.ts new file mode 100644 index 00000000..fe0f9322 --- /dev/null +++ b/packages/typir/src/kinds/custom/custom-kind.ts @@ -0,0 +1,177 @@ +/****************************************************************************** + * 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 { isMap, isSet } from 'util/types'; +import { Type, TypeDetails } from '../../graph/type-node.js'; +import { TypeInitializer } from '../../initialization/type-initializer.js'; +import { TypeReference } from '../../initialization/type-reference.js'; +import { ConversionMode } from '../../services/conversion.js'; +import { TypirServices } from '../../typir.js'; +import { InferCurrentTypeRule } from '../../utils/utils-definitions.js'; +import { Kind } from '../kind.js'; +import { CustomTypeInitialization, CustomTypeProperties, CustomTypePropertyInitialization, CustomTypePropertyTypes, CustomTypeStorage, TypeSelectorForCustomTypes } from './custom-definitions.js'; +import { CustomTypeInitializer } from './custom-initializer.js'; +import { CustomType } from './custom-type.js'; + +export interface CustomKindOptions { + /** Name for this custom kind. The names of custom kinds are unique. */ + name: string; + + /** This identifier needs to consider all properties which make the custom type unique. The identifiers are used to detect unique custom types. + * The default implementation considers all properties and their structure in a straight-forward way, + * but does not guarantee unique identifiers in all cases in general, since string properties might contain values looking like identifiers of other properties. + * The default implementation can be customized in order to overcome this limitation or to produce better readable identifiers. + * It is the responsibility of the user of Typir to consider all relevant properties and their structure/nesting. */ + calculateTypeIdentifier?: (properties: CustomTypeInitialization) => string; + + /** Define the name for each custom type; might be overridden by the custom type-specific name. + * If undefined, the identifier is used instead. */ + calculateTypeName?: (properties: CustomTypeStorage) => string; + /** Define the user representation for each custom type; might be overridden by the custom type-specific user representation. */ + calculateTypeUserRepresentation?: (properties: CustomTypeStorage) => string; + + // SubType + getSubTypesOfNewCustomType?: (superNewCustom: CustomType) => Type[]; + getSuperTypesOfNewCustomType?: (subNewCustom: CustomType) => Type[]; + isNewCustomTypeSubTypeOf?: (subNewCustom: CustomType, superOther: Type) => boolean; + isNewCustomTypeSuperTypeOf?: (subOther: Type, superNewCustom: CustomType) => boolean; + + // Conversion + getNewCustomTypeImplicitlyConvertibleToTypes?: (fromNewCustom: CustomType) => Type[]; + getTypesImplicitlyConvertibleToNewCustomType?: (toNewCustom: CustomType) => Type[]; + getNewCustomTypeExplicitlyConvertibleToTypes?: (fromNewCustom: CustomType) => Type[]; + getTypesExplicitlyConvertibleToNewCustomType?: (toNewCustom: CustomType) => Type[]; + isNewCustomTypeConvertibleToType?: (fromNewCustom: CustomType, toOther: Type) => ConversionMode; + isTypeConvertibleToNewCustomType?: (fromOther: Type, toNewCustom: CustomType) => ConversionMode; + // in order to have linear effort (instead of square effort), these methods are called only for the current, new CustomType (not for all existing types)! + + // TODO same for Equality in the future +} + +export interface CustomTypeDetails extends TypeDetails { + /** Values for all custom properties of the custom type. Note that TypeSelector are supported to initialize type properties of Type A. */ + properties: CustomTypeInitialization; + /** If specified, overrides the kind-specific name for custom types. */ + typeName?: string; + /** If specified, overrides the kind-specific user representation for custom types. */ + typeUserRepresentation?: string; +} + +export interface CreateCustomTypeDetails extends CustomTypeDetails { + inferenceRules: Array, LanguageType>>; +} + +export interface CustomFactoryService { + create(typeDetails: CustomTypeDetails): CustomTypeConfigurationChain; + get(properties: CustomTypeInitialization): TypeReference, LanguageType>; +} + +export interface CustomTypeConfigurationChain { + inferenceRule(rule: InferCurrentTypeRule, LanguageType, T>): CustomTypeConfigurationChain; + finish(): TypeInitializer, LanguageType>; +} + + +export class CustomKind implements Kind, CustomFactoryService { + readonly $name: `CustomKind-${string}`; + readonly services: TypirServices; + readonly options: CustomKindOptions; + + constructor(services: TypirServices, options: CustomKindOptions) { + this.$name = `CustomKind-${options.name}`; + this.services = services; + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + } + + protected collectOptions(options: CustomKindOptions): CustomKindOptions { + return { + // no default options required here + ...options, + }; + } + + get(properties: CustomTypeInitialization): TypeReference, LanguageType> { + return new TypeReference, LanguageType>(() => this.calculateIdentifier(properties), this.services); + } + + create(typeDetails: CustomTypeDetails): CustomTypeConfigurationChain { + return new CustomConfigurationChainImpl(this.services, this, typeDetails); + } + + calculateIdentifier(properties: CustomTypeInitialization): string { + if (this.options.calculateTypeIdentifier) { + return this.options.calculateTypeIdentifier(properties); + } else { + return `custom-${this.options.name/*is unique for all custom kinds*/}-${this.calculateIdentifierAll(properties)}`; + } + } + + protected calculateIdentifierAll(properties: CustomTypeInitialization): string { + return Object.entries(properties) + .map(entry => `${entry[0]}:${this.calculateIdentifierSingle(entry[1])}`) + .join(','); + } + protected calculateIdentifierSingle(value: CustomTypePropertyInitialization): string { + // all possible TypeSelectors + if (typeof value === 'function') { + return this.services.infrastructure.TypeResolver.resolve(value as TypeSelectorForCustomTypes).getIdentifier(); + } else if (value instanceof Type + || value instanceof TypeInitializer + || value instanceof TypeReference + || this.services.Language.isLanguageNode(value) + ) { + return this.services.infrastructure.TypeResolver.resolve(value).getIdentifier(); + } + // grouping with Array, Set, Map + else if (Array.isArray(value)) { + return `[${value.map(content => this.calculateIdentifierSingle(content)).join(',')}]`; + } else if (isSet(value)) { + return `(${Array.from(value.entries()).map(content => this.calculateIdentifierSingle(content)).sort().join(',')})`; // stable order of elements required + } else if (isMap(value)) { + return `{${Array.from(value.entries()).sort((c1, c2) => (c1[0] as string).localeCompare(c2[0])).map(content => `${content[0]}=${this.calculateIdentifierSingle(content[1])}`).join(',')}}`; // stable order of elements required + } + // primitives + else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' || typeof value === 'symbol') { + return String(value); + } + // composite with recursive object / index signature + else if (typeof value === 'object' && value !== null) { + return this.calculateIdentifierAll(value as CustomTypeInitialization); + } else { + throw new Error(`missing implementation for ${value}`); + } + } +} + +export function isCustomKind(kind: unknown): kind is CustomKind { + return kind instanceof CustomKind; +} + + +class CustomConfigurationChainImpl implements CustomConfigurationChainImpl { + protected readonly services: TypirServices; + protected readonly kind: CustomKind; + protected readonly typeDetails: CreateCustomTypeDetails; + + constructor(services: TypirServices, kind: CustomKind, typeDetails: CustomTypeDetails) { + this.services = services; + this.kind = kind; + this.typeDetails = { + ...typeDetails, + inferenceRules: [], + }; + } + + inferenceRule(rule: InferCurrentTypeRule, LanguageType, T>): CustomConfigurationChainImpl { + this.typeDetails.inferenceRules.push(rule as unknown as InferCurrentTypeRule, LanguageType>); + return this; + } + + finish(): TypeInitializer, LanguageType> { + return new CustomTypeInitializer(this.kind, this.typeDetails); + } +} diff --git a/packages/typir/src/kinds/custom/custom-type.ts b/packages/typir/src/kinds/custom/custom-type.ts new file mode 100644 index 00000000..bd1fa579 --- /dev/null +++ b/packages/typir/src/kinds/custom/custom-type.ts @@ -0,0 +1,240 @@ +/****************************************************************************** + * 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 { isMap, isSet } from 'util/types'; +import { Type } from '../../graph/type-node.js'; +import { TypeInitializer } from '../../initialization/type-initializer.js'; +import { TypeReference } from '../../initialization/type-reference.js'; +import { TypeEqualityProblem } from '../../services/equality.js'; +import { TypirProblem } from '../../utils/utils-definitions.js'; +import { checkTypes, checkValueForConflict, createKindConflict, createTypeCheckStrategy, ValueConflict } from '../../utils/utils-type-comparison.js'; +import { assertTrue } from '../../utils/utils.js'; +import { CustomTypeInitialization, CustomTypeProperties, CustomTypePropertyInitialization, CustomTypePropertyStorage, CustomTypePropertyTypes, CustomTypeStorage, TypeSelectorForCustomTypes } from './custom-definitions.js'; +import { CustomKind, CustomTypeDetails } from './custom-kind.js'; + +export class CustomType extends Type { + override readonly kind: CustomKind; + protected readonly typeName: string | undefined; + protected readonly typeUserRepresentation?: string; + readonly properties: CustomTypeStorage; + + constructor(kind: CustomKind, typeDetails: CustomTypeDetails) { + super(undefined, typeDetails); + this.kind = kind; + this.typeName = typeDetails.typeName; + this.typeUserRepresentation = typeDetails.typeUserRepresentation; + + const collectedReferences: Array> = []; + this.properties = this.replaceAllProperties(typeDetails.properties, collectedReferences) as CustomTypeStorage; + const allReferences: Array> = collectedReferences as Array>; // type-node.ts does not use + + this.defineTheInitializationProcessOfThisType({ + preconditionsForIdentifiable: { + referencesToBeIdentifiable: allReferences, + }, + referencesRelevantForInvalidation: allReferences, + onIdentifiable: () => { + this.identifier = this.kind.calculateIdentifier(typeDetails.properties); + } + }); + } + + protected replaceAllProperties(properties: CustomTypeInitialization, collectedReferences: Array>): CustomTypeStorage { + // const result: CustomTypeStorage = {}; // does not work, since the properties of CustomTypeStorage are defined as "readonly"! + const result: Record = {}; + for (const [key, value] of Object.entries(properties)) { + const transformed: CustomTypePropertyStorage = this.replaceSingleProperty(value, collectedReferences); + result[key] = transformed; + } + return result as CustomTypeStorage; + } + + protected replaceSingleProperty(value: CustomTypePropertyInitialization, collectedReferences: Array>): CustomTypePropertyStorage { + // TypeSelector --> TypeReference + // function + // Type + // (string) forbidden/not supported, since it is not unique, treat it as content/primitive property! + // TypeInitializer + // TypeReference + // LanguageType additional "Language"-Service required to distinguish it from object with index signature + // Array --> Array + // values: recursive transformation + // Map --> Map + // values: recursive transformation + // Set --> Set + // values: recursive transformation + // primitives --> primitives + // string + // number + // boolean + // bigint + // symbol + + // all possible TypeSelectors + if (typeof value === 'function') { + const result = new TypeReference(value as TypeSelectorForCustomTypes, this.kind.services); + collectedReferences.push(result); + return result as unknown as CustomTypePropertyStorage; + } else if (value instanceof Type + || value instanceof TypeInitializer + || value instanceof TypeReference + || this.kind.services.Language.isLanguageNode(value) + ) { + const result = new TypeReference(value, this.kind.services); + collectedReferences.push(result); + return result as unknown as CustomTypePropertyStorage; + } + // grouping with Array, Set, Map + else if (Array.isArray(value)) { + return value.map(content => this.replaceSingleProperty(content, collectedReferences)) as unknown as CustomTypePropertyStorage; + } else if (isSet(value)) { + const result = new Set>(); + for (const entry of value) { + result.add(this.replaceSingleProperty(entry, collectedReferences)); + } + return result as unknown as CustomTypePropertyStorage; + } else if (isMap(value)) { + const result: Map> = new Map(); + value.forEach((content, key) => result.set(key, this.replaceSingleProperty(content, collectedReferences))); + return result as unknown as CustomTypePropertyStorage; + } + // primitives + else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' || typeof value === 'symbol') { + return value as unknown as CustomTypePropertyStorage; + } + // composite with recursive object / index signature + else if (typeof value === 'object' && value !== null) { + return this.replaceAllProperties(value as CustomTypeInitialization, collectedReferences) as CustomTypePropertyStorage; + } else { + throw new Error(`missing implementation for ${value}`); + } + } + + override getName(): string { + return this.typeName // type-specific + ?? this.kind.options.calculateTypeName?.call(this.kind.options.calculateTypeName, this.properties) // kind-specific + ?? this.getIdentifier(); // fall-back + } + + override getUserRepresentation(): string { + return this.typeUserRepresentation // type-specific + ?? this.kind.options.calculateTypeUserRepresentation?.call(this.kind.options.calculateTypeUserRepresentation, this.properties) // kind-specific + ?? this.getName(); // fall-back + } + + override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { + if (isCustomType(otherType, this.kind)) { + const subProblems = this.analyzeTypeEqualityProblemsAll(this.properties, otherType.properties); + if (subProblems.length >= 1) { + return [{ + $problem: TypeEqualityProblem, + type1: this, + type2: otherType, + subProblems, + }]; + } else { + return []; + } + } else { + return [{ + $problem: TypeEqualityProblem, + type1: this, + type2: otherType, + subProblems: [createKindConflict(otherType, this)], + }]; + } + } + + protected analyzeTypeEqualityProblemsAll(properties1: CustomTypeStorage, properties2: CustomTypeStorage): TypirProblem[] { + const result: TypirProblem[] = []; + for (const [key, value1] of Object.entries(properties1)) { + const value2 = properties2[key]; + const subProblems = this.analyzeTypeEqualityProblemsSingle(value1, value2); + if (subProblems.length >= 1) { + result.push({ + $problem: ValueConflict, + location: key, + firstValue: value1, + secondValue: value2, + subProblems, + }); + } + } + return result; + } + protected analyzeTypeEqualityProblemsSingle(value1: CustomTypePropertyStorage, value2: CustomTypePropertyStorage): TypirProblem[] { + assertTrue(typeof value1 === typeof value2); + // a type is stored in a TypeReference! + if (value1 instanceof TypeReference) { + return checkTypes(value1.getType(), (value2 as TypeReference).getType(), createTypeCheckStrategy('EQUAL_TYPE', this.kind.services), false); + } + // grouping with Array, Set, Map + else if (Array.isArray(value1)) { + assertTrue(Array.isArray(value2)); + const sizeProblem = checkValueForConflict(value1.length, value2.length, 'length'); + if (sizeProblem.length >= 1) { + return sizeProblem; + } + const contentProblems: TypirProblem[] = []; + for (let i = 0; i < value1.length; i++) { + contentProblems.push(...this.analyzeTypeEqualityProblemsSingle(value1[i], value2[i])); + } + return contentProblems; + } else if (isSet(value1)) { + assertTrue(isSet(value2)); + const sizeProblem = checkValueForConflict(value1.size, value2.size, 'size'); + if (sizeProblem.length >= 1) { + return sizeProblem; + } + const contentProblems: TypirProblem[] = []; + for (const v1 of value1.entries()) { + let found = false; + for (const v2 of value2.entries()) { + if (this.analyzeTypeEqualityProblemsSingle(v1, v2).length === 0) { + found = true; + break; + } + } + if (found === false) { + contentProblems.push({ + $problem: ValueConflict, + firstValue: String(v1), + secondValue: undefined, + location: 'set entries', + subProblems: [], + }); + } + } + return contentProblems; + } else if (isMap(value1)) { + assertTrue(isMap(value2)); + const sizeProblem = checkValueForConflict(value1.size, value2.size, 'size'); + if (sizeProblem.length >= 1) { + return sizeProblem; + } + const contentProblems: TypirProblem[] = []; + for (const [key, v1] of value1.entries()) { + const v2 = value2.get(key); + contentProblems.push(...this.analyzeTypeEqualityProblemsSingle(v1, v2)); + } + return contentProblems; + } + // primitives + else if (typeof value1 === 'string' || typeof value1 === 'number' || typeof value1 === 'boolean' || typeof value1 === 'bigint' || typeof value1 === 'symbol') { + return checkValueForConflict(value1, value2, 'value'); + } + // composite with recursive object / index signature + else if (typeof value1 === 'object' && value1 !== null) { + return this.analyzeTypeEqualityProblemsAll(value1 as CustomTypeStorage, value2 as CustomTypeStorage); + } else { + throw new Error('missing implementation'); + } + } +} + +export function isCustomType(type: unknown, kind: string | CustomKind): type is CustomType { + return type instanceof CustomType && (typeof kind === 'string' ? type.kind.options.name === kind : type.kind === kind); +} diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 40fcb223..43c6a248 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -8,7 +8,7 @@ import { Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeInferenceRule } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; -import { bindInferCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType } from '../../utils/utils-definitions.js'; +import { bindInferCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, skipInferenceRuleForExistingType } from '../../utils/utils-definitions.js'; import { assertTypirType } from '../../utils/utils.js'; import { FunctionCallInferenceRule } from './function-inference-call.js'; import { CreateFunctionTypeDetails, FunctionKind, FunctionTypeDetails, InferFunctionCall } from './function-kind.js'; @@ -24,8 +24,10 @@ import { FunctionType, isFunctionType } from './function-type.js'; export class FunctionTypeInitializer extends TypeInitializer implements TypeStateListener { protected readonly typeDetails: CreateFunctionTypeDetails; protected readonly functions: AvailableFunctionsManager; - protected inferenceRules: FunctionInferenceRules; - protected initialFunctionType: FunctionType; + protected readonly initialFunctionType: FunctionType; + + protected inferenceForCall: Array> = []; + protected inferenceForDeclaration: Array> = []; constructor(services: TypirServices, kind: FunctionKind, typeDetails: CreateFunctionTypeDetails) { super(services); @@ -44,7 +46,7 @@ export class FunctionTypeInitializer extends TypeInitializer, typeDetails as FunctionTypeDetails); - this.inferenceRules = this.createInferenceRules(this.initialFunctionType); + this.createRules(this.initialFunctionType); this.registerRules(functionName, undefined); this.initialFunctionType.addListener(this, true); @@ -61,7 +63,7 @@ export class FunctionTypeInitializer extends TypeInitializer extends TypeInitializer { - const result: FunctionInferenceRules = { - inferenceForCall: [], - inferenceForDeclaration: [], - }; + protected createRules(functionType: FunctionType): void { + // clear the current list ... + this.inferenceForCall.splice(0, this.inferenceForCall.length); + this.inferenceForDeclaration.splice(0, this.inferenceForDeclaration.length); - for (const rule of this.typeDetails.inferenceRulesForCalls) { + // ... and recreate all rules + for (const inferenceRuleForCall of this.typeDetails.inferenceRulesForCalls) { + if (skipInferenceRuleForExistingType(inferenceRuleForCall, this.initialFunctionType, functionType)) { + continue; + } // create inference rule for calls of the new function - result.inferenceForCall.push({ - rule: this.createFunctionCallInferenceRule(rule, functionType), + this.inferenceForCall.push({ + rule: this.createFunctionCallInferenceRule(inferenceRuleForCall, functionType), options: { - languageKey: rule.languageKey, + languageKey: inferenceRuleForCall.languageKey, // boundToType: ... this property will be specified outside of this method, when this rule is registered }, }); @@ -119,19 +124,15 @@ export class FunctionTypeInitializer extends TypeInitializer, functionType: FunctionType): TypeInferenceRule { return new FunctionCallInferenceRule(this.typeDetails, rule, functionType, this.functions); } } - -interface FunctionInferenceRules { - inferenceForCall: Array>; - inferenceForDeclaration: Array>; -} diff --git a/packages/typir/src/kinds/function/function-type.ts b/packages/typir/src/kinds/function/function-type.ts index 5ca4126c..3a944c1e 100644 --- a/packages/typir/src/kinds/function/function-type.ts +++ b/packages/typir/src/kinds/function/function-type.ts @@ -13,8 +13,8 @@ import { assertTrue, assertUnreachable } from '../../utils/utils.js'; import { FunctionKind, FunctionTypeDetails, isFunctionKind } from './function-kind.js'; export interface ParameterDetails { - name: string; - type: TypeReference; + readonly name: string; + readonly type: TypeReference; } export class FunctionType extends Type { diff --git a/packages/typir/src/kinds/primitive/primitive-kind.ts b/packages/typir/src/kinds/primitive/primitive-kind.ts index 67161ecb..22c9479e 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -59,7 +59,7 @@ export class PrimitiveKind implements Kind, PrimitiveFactoryServic } create(typeDetails: PrimitiveTypeDetails): PrimitiveConfigurationChain { - assertTrue(this.get(typeDetails) === undefined); // ensure that the type is not created twice + assertTrue(this.get(typeDetails) === undefined, `There is already a primitive type with name '${typeDetails.primitiveName}'.`); // ensure that the type is not created twice return new PrimitiveConfigurationChainImpl(this.services, this, typeDetails); } diff --git a/packages/typir/src/kinds/top/top-kind.ts b/packages/typir/src/kinds/top/top-kind.ts index c99dffc5..1034a1cc 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -61,7 +61,7 @@ export class TopKind implements Kind, TopFactoryService): TopConfigurationChain { - assertTrue(this.get(typeDetails) === undefined); // ensure that the type is not created twice + assertTrue(this.get(typeDetails) === undefined, 'The top type already exists.'); // ensure that the type is not created twice return new TopConfigurationChainImpl(this.services, this, typeDetails); } diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index 96b2bf29..9a2c1373 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -21,24 +21,19 @@ export class TopType extends Type implements TypeGraphListener { // 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 + graph.addListener(this, { callOnAddedForAllExisting: true }); // all upcomping types } override dispose(): void { this.kind.services.infrastructure.Graph.removeListener(this); } - protected markAsSubType(type: Type): void { + onAddedType(type: Type, _key: string): void { if (type !== this) { this.kind.services.Subtype.markAsSubType(type, this, { checkForCycles: false }); } } - onAddedType(type: Type, _key: string): void { - this.markAsSubType(type); - } - override getName(): string { return this.getIdentifier(); } diff --git a/packages/typir/src/services/language.ts b/packages/typir/src/services/language.ts index 154c7def..3d352419 100644 --- a/packages/typir/src/services/language.ts +++ b/packages/typir/src/services/language.ts @@ -4,6 +4,11 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { Type } from '../graph/type-node.js'; +import { TypeInitializer } from '../initialization/type-initializer.js'; +import { TypeReference } from '../initialization/type-reference.js'; +import { isKind } from '../kinds/kind.js'; + /** * This services provides some static information about the language/DSL, for which the type system is created. * @@ -40,6 +45,8 @@ export interface LanguageService { * @returns the list does not contain the given language key itself */ getAllSuperKeys(languageKey: string): string[]; + + isLanguageNode(node: unknown): node is LanguageType; } @@ -60,4 +67,14 @@ export class DefaultLanguageService implements LanguageService implements ProblemPrinter< throw new Error(); } result = this.printIndentation(result, level); + result = this.printSubProblems(result, problem.subProblems, level); return result; } diff --git a/packages/typir/src/test/predefined-language-nodes.ts b/packages/typir/src/test/predefined-language-nodes.ts index 817ba315..8629eeac 100644 --- a/packages/typir/src/test/predefined-language-nodes.ts +++ b/packages/typir/src/test/predefined-language-nodes.ts @@ -8,8 +8,6 @@ import { DefaultLanguageService } from '../services/language.js'; 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. @@ -161,4 +159,8 @@ export class TestLanguageService extends DefaultLanguageService( customization2: Module, PartialTypirServices> = {}, customization3: Module, PartialTypirServices> = {}, ): TypirServices { - return inject(createDefaultTypirServicesModule(), customization1, customization2, customization3); + return inject( + createDefaultTypirServicesModule(), + customization1, + customization2, + customization3, + ); } /** diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index 37cfbe8c..0f588765 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -34,7 +34,7 @@ export function expectTypirTypes(services: TypirServices(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): asserts type is T { if (checkType(type)) { if (checkDetails(type)) { // everything is fine @@ -248,7 +248,7 @@ export function createTypirServicesForTesting( customizationForTesting: Module, PartialTypirServices> = {}, ): TypirServices { return createTypirServices( - createDefaultTypirServicesModule(), // all default core implementations + createDefaultTypirServicesModule(), // all default core implementations { // override some default implementations: Printer: () => new TestProblemPrinter(), // use the dedicated printer for TestLanguageNode's Language: () => new TestLanguageService(), // provide language keys for the TestLanguageNode's: they are just the names of the classes (without extends so far) diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 82414e5a..17a63baa 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -135,12 +135,25 @@ export interface InferCurrentTypeRule | Array>; + + skipThisRuleIfThisTypeAlreadyExists?: boolean | ((existingType: TypeType) => boolean); // default is false } export type InferCurrentTypeValidationRule = (languageNode: T, inferredType: TypeType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; +export function skipInferenceRuleForExistingType( + inferenceRule: InferCurrentTypeRule, newType: TypeType, existingType: TypeType +): boolean { + if (newType !== existingType) { + const skipRuleForExisting = inferenceRule.skipThisRuleIfThisTypeAlreadyExists; + // don't create (additional) rules for the already existing type + return skipRuleForExisting === true || (typeof skipRuleForExisting === 'function' && skipRuleForExisting(existingType) === true); + } + return false; +} + function checkRule(rule: InferCurrentTypeRule): void { if (rule.languageKey === undefined && rule.filter === undefined && rule.matching === undefined) { throw new Error('This inference rule has none of the properties "languageKey", "filter" and "matching" at all and therefore cannot infer any type!'); diff --git a/packages/typir/src/utils/utils-type-comparison.ts b/packages/typir/src/utils/utils-type-comparison.ts index 0d99fb2e..62e399f7 100644 --- a/packages/typir/src/utils/utils-type-comparison.ts +++ b/packages/typir/src/utils/utils-type-comparison.ts @@ -41,6 +41,7 @@ export interface ValueConflict extends TypirProblem { firstValue: string | undefined; secondValue: string | undefined; location: string; + subProblems?: TypirProblem[]; } export const ValueConflict = 'ValueConflict'; export function isValueConflict(problem: unknown): problem is ValueConflict { @@ -53,8 +54,8 @@ export function checkValueForConflict(first: T, second: T, location: string, if (relationToCheck(first, second) === false) { conflicts.push({ $problem: ValueConflict, - firstValue: `${first}`, - secondValue: `${second}`, + firstValue: String(first), + secondValue: String(second), location }); } diff --git a/packages/typir/test/api-example.test.ts b/packages/typir/test/api-example.test.ts index ffca37c0..c3aeb71d 100644 --- a/packages/typir/test/api-example.test.ts +++ b/packages/typir/test/api-example.test.ts @@ -4,8 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -/* eslint-disable @typescript-eslint/parameter-properties */ - import { describe, expect, test } from 'vitest'; import { InferenceRuleNotApplicable } from '../src/services/inference.js'; import { InferOperatorWithMultipleOperands } from '../src/services/operator.js'; diff --git a/packages/typir/test/kinds/custom/custom-cycles.test.ts b/packages/typir/test/kinds/custom/custom-cycles.test.ts new file mode 100644 index 00000000..afd0dce3 --- /dev/null +++ b/packages/typir/test/kinds/custom/custom-cycles.test.ts @@ -0,0 +1,110 @@ +/****************************************************************************** + * 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 { beforeEach, describe, expect, test } from 'vitest'; +import { Type } from '../../../src/graph/type-node.js'; +import { TypeInitializer } from '../../../src/initialization/type-initializer.js'; +import { TypeReference } from '../../../src/initialization/type-reference.js'; +import { CustomKind } from '../../../src/kinds/custom/custom-kind.js'; +import { isCustomType } from '../../../src/kinds/custom/custom-type.js'; +import { PrimitiveType } from '../../../src/kinds/primitive/primitive-type.js'; +import { TestLanguageNode } from '../../../src/test/predefined-language-nodes.js'; +import { TypirServices } from '../../../src/typir.js'; +import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; + +// These test cases test, that custom types might depend on other types including custom types +// and the creation of custom types is delayed, when those types are not yet existing. + +export type MyCustomType = { + dependsOnType: Type; + myProperty: number; +}; + + +describe('Check custom types depending on other types', () => { + let typir: TypirServices; + let integerType: PrimitiveType; + let customKind: CustomKind; + + beforeEach(() => { + typir = createTypirServicesForTesting(); + + integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + + customKind = new CustomKind(typir, { + name: 'MyCustom', + // determine which identifier is used to store and retrieve a custom type in the type graph (and to check its uniqueness) + calculateTypeIdentifier: properties => + `custom-mycustom-${typir.infrastructure.TypeResolver.resolve(properties.dependsOnType).getIdentifier()}-${properties.myProperty}`, + }); + }); + + test('Custom types depend on other custom types: in nice order', () => { + // custom1 depends on integer + const config1 = customKind.create({ typeName: 'C1', properties: { dependsOnType: integerType, myProperty: 1 } }).finish(); + const custom1 = config1.getTypeFinal(); + expectToBeType(custom1, type => isCustomType(type, customKind), type => type.properties.myProperty === 1 && type.properties.dependsOnType.getType() === integerType); + + // custom2 depends on custom1 + const config2 = customKind.create({ typeName: 'C2', properties: { dependsOnType: custom1, myProperty: 2 } }).finish(); + const custom2 = config2.getTypeFinal(); + expectToBeType(custom2, type => isCustomType(type, customKind), type => type.properties.myProperty === 2 && type.properties.dependsOnType.getType() === custom1); + + // custom3 depends on custom2 + const config3 = customKind.create({ typeName: 'C3', properties: { dependsOnType: custom2, myProperty: 3 } }).finish(); + const custom3 = config3.getTypeFinal(); + expectToBeType(custom3, type => isCustomType(type, customKind), type => type.properties.myProperty === 3 && type.properties.dependsOnType.getType() === custom2); + }); + + test('Custom types depend on other custom types: in difficult order', () => { + // custom2 depends on custom1, which is not defined yet + const config2 = customKind.create({ typeName: 'C2', properties: { + dependsOnType: customKind.get({ dependsOnType: integerType, myProperty: 1 }) as unknown as TypeReference, + myProperty: 2 } }).finish(); + let custom2 = config2.getTypeFinal(); + expect(custom2).toBeUndefined(); + + // custom1 depends on integer => directly available + const config1 = customKind.create({ typeName: 'C1', properties: { dependsOnType: integerType, myProperty: 1 } }).finish(); + const custom1 = config1.getTypeFinal(); + expectToBeType(custom1, type => isCustomType(type, customKind), type => type.properties.myProperty === 1 && type.properties.dependsOnType.getType() === integerType); + + // since custom1 is available now, custom2 is available as well + custom2 = config2.getTypeFinal(); + expectToBeType(custom2, type => isCustomType(type, customKind), type => type.properties.myProperty === 2 && type.properties.dependsOnType.getType() === custom1); + + // custom3 depends on custom2 + const config3 = customKind.create({ typeName: 'C3', properties: { dependsOnType: custom2, myProperty: 3 } }).finish(); + const custom3 = config3.getTypeFinal(); + expectToBeType(custom3, type => isCustomType(type, customKind), type => type.properties.myProperty === 3 && type.properties.dependsOnType.getType() === custom2); + }); + + test('Custom types depend on other custom types: in difficult order, transitive', () => { + // custom2 depends on custom1, which is not defined yet + const config2 = customKind.create({ typeName: 'C2', properties: { + dependsOnType: customKind.get({ dependsOnType: integerType, myProperty: 1 }) as unknown as TypeReference, + myProperty: 2 } }).finish(); + let custom2 = config2.getTypeFinal(); + expect(custom2).toBeUndefined(); + + // custom3 depends on custom2 + const config3 = customKind.create({ typeName: 'C3', properties: { dependsOnType: config2 as unknown as TypeInitializer, myProperty: 3 } }).finish(); + let custom3 = config3.getTypeFinal(); + expect(custom3).toBeUndefined(); + + // custom1 depends on integer => directly available + const config1 = customKind.create({ typeName: 'C1', properties: { dependsOnType: integerType, myProperty: 1 } }).finish(); + const custom1 = config1.getTypeFinal(); + expectToBeType(custom1, type => isCustomType(type, customKind), type => type.properties.myProperty === 1 && type.properties.dependsOnType.getType() === integerType); + + // since custom1 is available now, custom2 and custom3 are available as well + custom2 = config2.getTypeFinal(); + expectToBeType(custom2, type => isCustomType(type, customKind), type => type.properties.myProperty === 2 && type.properties.dependsOnType.getType() === custom1); + custom3 = config3.getTypeFinal(); + expectToBeType(custom3, type => isCustomType(type, customKind), type => type.properties.myProperty === 3 && type.properties.dependsOnType.getType() === custom2); + }); + +}); diff --git a/packages/typir/test/kinds/custom/custom-example-matrix.test.ts b/packages/typir/test/kinds/custom/custom-example-matrix.test.ts new file mode 100644 index 00000000..af32be3f --- /dev/null +++ b/packages/typir/test/kinds/custom/custom-example-matrix.test.ts @@ -0,0 +1,333 @@ +/****************************************************************************** + * 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 { beforeEach, describe, expect, test } from 'vitest'; +import { CustomKind } from '../../../src/kinds/custom/custom-kind.js'; +import { CustomType, isCustomType } from '../../../src/kinds/custom/custom-type.js'; +import { isPrimitiveType, PrimitiveType } from '../../../src/kinds/primitive/primitive-type.js'; +import { DefaultTypeInferenceCollector, InferenceRuleNotApplicable, TypeInferenceRule } from '../../../src/services/inference.js'; +import { ValidationProblemAcceptor } from '../../../src/services/validation.js'; +import { IntegerLiteral, TestExpressionNode, TestLanguageNode } from '../../../src/test/predefined-language-nodes.js'; +import { TypirServices } from '../../../src/typir.js'; +import { RuleRegistry } from '../../../src/utils/rule-registration.js'; +import { createTypirServicesForTesting, expectToBeType, expectTypirTypes, expectValidationIssuesNone, expectValidationIssuesStrict } from '../../../src/utils/test-utils.js'; +import { assertTypirType } from '../../../src/utils/utils.js'; + +/** + * The custom type called "Matrix" represents a two-dimensional array of primitive types. + * Known from mathematics, the "width" represents the number of columns and the "height" the number of row of a matrix. + * + * This TypeScript type specifies the properties of the Typir types which represent "matrices". + */ +export type MatrixType = { // "interface" instead of "type" does not work! + baseType: PrimitiveType; + width: number; + height: number; +}; + +describe('Tests simple custom types for Matrix types', () => { + + test('Matrix type', () => { + const typir = createTypirServicesForTesting(); + // TODO does not yet work: { factory: { Matrix: services => new CustomKind(services, { ... }) } } + const integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + + // create a custom kind to create custom types with dedicated properties, as defined in + const customKind = new CustomKind(typir, { + name: 'Matrix', + // determine which identifier is used to store and retrieve a custom type in the type graph + calculateTypeName: properties => `My${properties.width}x${properties.height}Matrix`, + // (and to check its uniqueness, i.e. if two types have the same identifier, they are the same and only one of it will be added to the type graph) + calculateTypeIdentifier: properties => + `custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + }); + + // now use this custom kind to create some custom types + const matrix2x2 = customKind // "lazy" to use matrix2x2 as 'baseType' => review ZOD, separate primitives and Typir-Types + .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) + .finish().getTypeFinal()!; // we know, that the new custom type depends only on types which are already available + expect(typir.Printer.printTypeUserRepresentation(matrix2x2)).toBe('My2x2MatrixType'); + assertTypirType(matrix2x2, type => isCustomType(type, customKind), 'My2x2MatrixType'); + expectTypirTypes(typir, type => isCustomType(type, customKind), 'My2x2MatrixType'); + expect(matrix2x2.properties.width).toBe(2); + expect(matrix2x2.properties.height).toBe(2); + expectToBeType(matrix2x2.properties.baseType.getType(), isPrimitiveType, type => type === integerType); + + const matrix3x3 = customKind + .create({ typeName: 'My3x3MatrixType', properties: { baseType: integerType, width: 3, height: 3 } }) + .finish().getTypeFinal()!; // we know, that the new custom type depends only on types which are already available + expect(typir.Printer.printTypeUserRepresentation(matrix3x3)).toBe('My3x3MatrixType'); + assertTypirType(matrix3x3, type => isCustomType(type, customKind), 'My3x3MatrixType'); + expectTypirTypes(typir, type => isCustomType(type, customKind), 'My2x2MatrixType', 'My3x3MatrixType'); + expect(matrix3x3.properties.width).toBe(3); + expect(matrix3x3.properties.height).toBe(3); + expectToBeType(matrix3x3.properties.baseType.getType(), isPrimitiveType, type => type === integerType); + }); + + test('Matrix type with very simple inference rules', () => { + const typir = createTypirServicesForTesting(); + const integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + const customKind = new CustomKind(typir, { + name: 'Matrix', + calculateTypeIdentifier: properties => + `custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + }); + + const matrix2x2 = customKind + .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) + .inferenceRule({ matching: node => node === matrixLiteral2x2 }) // very limited inference rule, only for testing + .finish().getTypeFinal()!; + const matrix3x3 = customKind + .create({ typeName: 'My3x3MatrixType', properties: { baseType: integerType, width: 3, height: 3 } }) + .inferenceRule({ matching: node => node === matrixLiteral3x3 }) // very limited inference rule, only for testing + .finish().getTypeFinal()!; + + expectToBeType(typir.Inference.inferType(matrixLiteral3x3), result => isCustomType(result, customKind), result => result === matrix3x3); + expectToBeType(typir.Inference.inferType(matrixLiteral2x2), result => isCustomType(result, customKind), result => result === matrix2x2); + + expect(typir.Inference.inferType(matrixLiteral1x1)).toHaveLength(1); // no type, but a problem, since there is no 1x1 matrix type! + }); + + test('Matrix type with very simple inference rules (+ validation rule)', () => { + const typir = createTypirServicesForTesting(); + const integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + const customKind = new CustomKind(typir, { + name: 'Matrix', + calculateTypeIdentifier: properties => + `custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + }); + + function checkCompleteness(node: MatrixLiteral, matrixType: CustomType, accept: ValidationProblemAcceptor): void { + const height = matrixType.properties.height; + if (node.elements.some(column => column.length !== height)) { + accept({ languageNode: node, severity: 'error', message: 'Incomplete content in matrix literal found' }); + } + } + + const matrix2x2 = customKind + .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) + .inferenceRule({ + matching: node => node === matrixLiteral2x2 || node === matrixLiteral2x2Incomplete, + validation: checkCompleteness }) // very limited inference rule, only for testing + .finish().getTypeFinal()!; + const matrix3x3 = customKind + .create({ typeName: 'My3x3MatrixType', properties: { baseType: integerType, width: 3, height: 3 } }) + .inferenceRule({ + matching: node => node === matrixLiteral3x3 || node === matrixLiteral3x3Incomplete, + validation: checkCompleteness }) // very limited inference rule, only for testing + .finish().getTypeFinal()!; + + expectToBeType(typir.Inference.inferType(matrixLiteral2x2), result => isCustomType(result, customKind), result => result === matrix2x2); + expectToBeType(typir.Inference.inferType(matrixLiteral2x2Incomplete), result => isCustomType(result, customKind), result => result === matrix2x2); + expectToBeType(typir.Inference.inferType(matrixLiteral3x3), result => isCustomType(result, customKind), result => result === matrix3x3); + expectToBeType(typir.Inference.inferType(matrixLiteral3x3Incomplete), result => isCustomType(result, customKind), result => result === matrix3x3); + + expectValidationIssuesNone(typir, matrixLiteral2x2); + expectValidationIssuesStrict(typir, matrixLiteral2x2Incomplete, ['Incomplete content in matrix literal found']); + expectValidationIssuesNone(typir, matrixLiteral3x3); + expectValidationIssuesStrict(typir, matrixLiteral3x3Incomplete, ['Incomplete content in matrix literal found']); + }); + + test('Matrix type with generic inference rule: only get', () => { + const typir = createTypirServicesForTesting(); + const integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + const customKind = new CustomKind(typir, { + name: 'Matrix', + calculateTypeIdentifier: properties => + `custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + }); + + const matrix2x2 = customKind + .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) + // no inference rule here + .finish().getTypeFinal()!; + const matrix3x3 = customKind + .create({ typeName: 'My3x3MatrixType', properties: { baseType: integerType, width: 3, height: 3 } }) + // no inference rule here + .finish().getTypeFinal()!; + + // ... but a single, generic inference rule here + typir.Inference.addInferenceRule(node => { + if (node instanceof MatrixLiteral) { + const width = node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0); // the number of cells in the longest row + const height = node.elements.length; // the number of rows + const type = customKind.get({ baseType: integerType, width, height }); + return type.getType() || InferenceRuleNotApplicable; + } + return InferenceRuleNotApplicable; + }); + + expectToBeType(typir.Inference.inferType(matrixLiteral3x3), result => isCustomType(result, customKind), result => result === matrix3x3); + expectToBeType(typir.Inference.inferType(matrixLiteral2x2), result => isCustomType(result, customKind), result => result === matrix2x2); + + expect(typir.Inference.inferType(matrixLiteral1x1)).toHaveLength(1); // no type, but a problem, since there is no 1x1 matrix type! + + const matrix1x1 = customKind + .create({ typeName: 'My1x1MatrixType', properties: { baseType: integerType, width: 1, height: 1 } }) + .finish().getTypeFinal()!; + // now the 1x1 matrix type exists and can be inferred! + expectToBeType(typir.Inference.inferType(matrixLiteral1x1), result => isCustomType(result, customKind), result => result === matrix1x1); + }); + + test('Matrix type with generic inference rule: get or create', () => { + const typir = createTypirServicesForTesting(); + const integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + const customKind = new CustomKind(typir, { + name: 'Matrix', + calculateTypeIdentifier: properties => + `custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + }); + // a single, generic inference rule + typir.Inference.addInferenceRule(node => { + if (node instanceof MatrixLiteral) { + const width = node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0); // the number of cells in the longest row + const height = node.elements.length; // the number of rows + return customKind.create({ typeName: `My${width}x${height}MatrixType`, properties: { baseType: integerType, width, height }}) + .finish().getTypeFinal()!; // we know, that the type can be created now, without delay + } + return InferenceRuleNotApplicable; + }); + + // we create some Matrix types in advance + const matrix2x2 = customKind + .create({ typeName: 'My2x2MatrixType', properties: { baseType: integerType, width: 2, height: 2 } }) + .finish().getTypeFinal()!; + const matrix3x3 = customKind + .create({ typeName: 'My3x3MatrixType', properties: { baseType: integerType, width: 3, height: 3 } }) + .finish().getTypeFinal()!; + + // the already created Matrix types are inferred + expectToBeType(typir.Inference.inferType(matrixLiteral3x3), result => isCustomType(result, customKind), result => result === matrix3x3); + expectToBeType(typir.Inference.inferType(matrixLiteral2x2), result => isCustomType(result, customKind), result => result === matrix2x2); + expectTypirTypes(typir, type => isCustomType(type, customKind), 'My2x2MatrixType', 'My3x3MatrixType'); // we have only 2 Matrix types in the type graph + + // a new Matrix type is created and inferred for the 1x1 matrix literal: + expectToBeType(typir.Inference.inferType(matrixLiteral1x1), result => isCustomType(result, customKind), + result => result.properties.height === 1 && result.properties.width === 1 && result.properties.baseType.getType() === integerType); + expectTypirTypes(typir, type => isCustomType(type, customKind), 'My2x2MatrixType', 'My3x3MatrixType', 'My1x1MatrixType'); // now we have 3 Matrix types + + // we try to explicitly create the 1x1 Matrix type ... + const matrix1x1 = customKind + .create({ typeName: 'My1x1MatrixType', properties: { baseType: integerType, width: 1, height: 1 } }) + .finish().getTypeFinal()!; // ... the already existing 1x1 Matrix type is returned: 'create' behaves like 'getOrCreate', since no duplicated types should be created + expectToBeType(typir.Inference.inferType(matrixLiteral1x1), result => isCustomType(result, customKind), result => result === matrix1x1); + // but we receive an error, if we specified a different 'typeName' + expect(() => customKind + .create({ typeName: 'AnotherName', properties: { baseType: integerType, width: 1, height: 1 } }) + .finish().getTypeFinal()).toThrowError("There is already a custom type 'custom-matrix-Integer-1-1' with name 'My1x1MatrixType', but now the name is 'AnotherName'!"); + }); + + describe('Matrix type with type-specific inference rules', () => { + let typir: TypirServices; + let integerType: PrimitiveType; + let customKind: CustomKind; + + // customize Typir in order to count the number of registered inference rules + class TestInferenceImpl extends DefaultTypeInferenceCollector { + override readonly ruleRegistry: RuleRegistry, TestLanguageNode>; + } + + beforeEach(() => { + typir = createTypirServicesForTesting({ + Inference: (services) => new TestInferenceImpl(services), + }); + + integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + + customKind = new CustomKind(typir, { + name: 'Matrix', + calculateTypeIdentifier: properties => + `custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + calculateTypeName: properties => + `${properties.width}x${properties.height}-Matrix`, + }); + }); + + function countInferenceRules(): number { + return (typir.Inference as TestInferenceImpl).ruleRegistry.getNumberUniqueRules(); + } + + function getOrCreateMatrixType(width: number, height: number, skipThisRuleIfThisTypeAlreadyExists: boolean): CustomType { + return customKind + .create({ properties: { baseType: integerType, width, height } }) + // each matrix type has its own custom inference rule + .inferenceRule({ + filter: node => node instanceof MatrixLiteral, + matching: (node, type) => node.elements.length === type.properties.width && node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0) === type.properties.height, + skipThisRuleIfThisTypeAlreadyExists, // control how to deal with this inference rule for an already existing custom type + }) + .finish() + .getTypeFinal()!; + } + + test('Additional inference rules for already existing types', () => { + const initialInferenceRuleSize = countInferenceRules(); + // create a new Matrix type + const matrix2x2 = getOrCreateMatrixType(2, 2, false); + expect(countInferenceRules()).toBe(initialInferenceRuleSize + 1); // new Matrix type with its own inference rule + // "create it again" => in the end, the existing Matrix type is reused + const matrix2x2Another = getOrCreateMatrixType(2, 2, false); + // both types are the same (since they have the same identifier, since it contains the same values for the primitive type, width and height), ... + expect(matrix2x2).toBe(matrix2x2Another); + // ... but we have another inference rule now, since the rules for the new type are moved to the existing type! + expect(countInferenceRules()).toBe(initialInferenceRuleSize + 2); + }); + + test('Dont create inference rules for already existing types', () => { + const initialInferenceRuleSize = countInferenceRules(); + // create a new Matrix type + const matrix2x2 = getOrCreateMatrixType(2, 2, true); + expect(countInferenceRules()).toBe(initialInferenceRuleSize + 1); // new Matrix type with its own inference rule + // "create it again" => in the end, the existing Matrix type is reused + const matrix2x2Another = getOrCreateMatrixType(2, 2, true); + // both types are the same, ... + expect(matrix2x2).toBe(matrix2x2Another); + // ... but there is no additional inference rule! + expect(countInferenceRules()).toBe(initialInferenceRuleSize + 1); + }); + + }); + +}); + + +/** + * Instances of this class represent literals for matrices in the AST, i.e. AST nodes. An example might be visualized like this: + * [ 1, 2, 3; + * 4, 5, 6 ] + * They are similar to array literals in usual programming languages. + */ +class MatrixLiteral extends TestExpressionNode { + constructor( + public elements: IntegerLiteral[][], + ) { super(); } +} + +// some predefined literals for matrices to be reused in test cases + +const matrixLiteral1x1 = new MatrixLiteral([ + [new IntegerLiteral(1)], +]); + +const matrixLiteral2x2 = new MatrixLiteral([ + [new IntegerLiteral(1), new IntegerLiteral(2)], + [new IntegerLiteral(3), new IntegerLiteral(4)], +]); +const matrixLiteral2x2Incomplete = new MatrixLiteral([ + [new IntegerLiteral(1), new IntegerLiteral(2)], + [new IntegerLiteral(3), /* incomplete here */], +]); + +const matrixLiteral3x3 = new MatrixLiteral([ + [new IntegerLiteral(1), new IntegerLiteral(2), new IntegerLiteral(3)], + [new IntegerLiteral(4), new IntegerLiteral(5), new IntegerLiteral(6)], + [new IntegerLiteral(7), new IntegerLiteral(8), new IntegerLiteral(9)], +]); +const matrixLiteral3x3Incomplete = new MatrixLiteral([ + [new IntegerLiteral(1), new IntegerLiteral(2), new IntegerLiteral(3)], + [new IntegerLiteral(4), new IntegerLiteral(5), new IntegerLiteral(6)], + [new IntegerLiteral(7), new IntegerLiteral(8), /* incomplete here */], +]); diff --git a/packages/typir/test/kinds/custom/custom-example-restricted.test.ts b/packages/typir/test/kinds/custom/custom-example-restricted.test.ts new file mode 100644 index 00000000..cf781e02 --- /dev/null +++ b/packages/typir/test/kinds/custom/custom-example-restricted.test.ts @@ -0,0 +1,148 @@ +/****************************************************************************** + * 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 { beforeEach, describe, expect, test } from 'vitest'; +import { Type } from '../../../src/graph/type-node.js'; +import { CustomKind } from '../../../src/kinds/custom/custom-kind.js'; +import { CustomType, isCustomType } from '../../../src/kinds/custom/custom-type.js'; +import { PrimitiveType } from '../../../src/kinds/primitive/primitive-type.js'; +import { InferenceRuleNotApplicable } from '../../../src/services/inference.js'; +import { IntegerLiteral, TestExpressionNode, TestLanguageNode } from '../../../src/test/predefined-language-nodes.js'; +import { TypirServices } from '../../../src/typir.js'; +import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; + +/** + * The custom type called "RestrictedInteger" represents a primitive integer type with an upper bound, + * i.e. it looks like usual integers, but integer values/literals higher than the upper bound cannot be assigned to the restricted integer. + * + * This TypeScript type specifies the properties of the Typir types which represent "restricted integers". + */ +export type RestrictedInteger = { + upperBound: number; +}; + +describe('Tests inference and assignability for Integers with an upper bound', () => { + let typir: TypirServices; + let integerType: PrimitiveType; + let customKind: CustomKind; + + beforeEach(() => { + typir = createTypirServicesForTesting(); + + integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + + customKind = new CustomKind(typir, { + name: 'RestrictedInteger', + // determine which identifier is used to store and retrieve a custom type in the type graph + // (and to check its uniqueness, i.e. if two types have the same identifier, they are the same and only one of it will be added to the type graph) + calculateTypeIdentifier: properties => `custom-restricted-integer-${properties.upperBound}`, + calculateTypeName: properties => `RI-${properties.upperBound}`, // the name for each RestrictedInteger type + // each RestrictedIntegerType is an IntegerType! + getSuperTypesOfNewCustomType: (_subNewCustom) => [integerType], + // For conversion of RestrictedIntegers, both directions need to be specified, since: + // - conversion is a directed relationship + // - this RestrictedInteger might be converted to another RestrictedIntger, or another RestrictedInteger might be converted to this RestrictedInteger => these are two (slightly) different cases + isNewCustomTypeConvertibleToType: (fromNewCustom, toOther) => isCustomType(toOther, fromNewCustom.kind) && fromNewCustom.properties.upperBound < toOther.properties.upperBound ? 'IMPLICIT_EXPLICIT' : 'NONE', + isTypeConvertibleToNewCustomType: (fromOther, toNewCustom) => isCustomType(fromOther, toNewCustom.kind) && fromOther.properties.upperBound < toNewCustom.properties.upperBound ? 'IMPLICIT_EXPLICIT' : 'NONE', + }); + + typir.Inference.addInferenceRule(node => { + if (node instanceof IntegerLiteral) { + return integerType; + } + if (node instanceof RestrictedIntegerLiteral) { + return restrictedType(node.upperBound); // creates (or gets) a corresponding RestrictedInteger type + } + return InferenceRuleNotApplicable; + }); + }); + + /** + * Utility function to create a new restricted type in the Typir type system. + * @param upperBound the desired upper bound + * @returns the restricted type which is part of the type system in the current Typir instance/services + */ + function restrictedType(upperBound: number): CustomType { + return customKind.create({ properties: { upperBound } }).finish().getTypeFinal()!; + } + + test('Check type inference', () => { + expectToBeType(typir.Inference.inferType(int2Limit10), result => isCustomType(result, customKind), result => result.properties.upperBound === 10); + expectToBeType(typir.Inference.inferType(int6Limit10), result => isCustomType(result, customKind), result => result.properties.upperBound === 10); + expect(typir.Inference.inferType(int2Limit10)).toBe(typir.Inference.inferType(int6Limit10)); // same type, as the upper bound is the same + }); + + + function expectAssignability(sourceType: Type, targetType: Type): void { + expect(typir.Assignability.isAssignable(sourceType, targetType)).toBe(true); + } + function expectAssignabilityProblem(sourceType: Type, targetType: Type, message: string): void { + const result = typir.Assignability.getAssignabilityProblem(sourceType, targetType); + expect(result).toBeTruthy(); + const resultPrinted = typir.Printer.printTypirProblem(result!); + expect(resultPrinted).includes(message); + } + + test('Assignability: same restricted types (2)', () => { + expectAssignability(restrictedType(2), restrictedType(2)); + }); + test('Assignability: same restricted types (3)', () => { + expectAssignability(restrictedType(3), restrictedType(3)); + }); + test('Assignability: same restricted types (10)', () => { + expectAssignability(restrictedType(10), restrictedType(10)); + }); + + test('Assignability 2 --> 3: works', () => { + const r2 = restrictedType(2); + const r3 = restrictedType(3); + expectAssignability(r2, r3); + }); + test('Assignability 2 --> 3: works (different order of type creation)', () => { + const r3 = restrictedType(3); + const r2 = restrictedType(2); + expectAssignability(r2, r3); + }); + + test('Assignability 3 --> 2: not supported', () => { + const r3 = restrictedType(3); + const r2 = restrictedType(2); + expectAssignabilityProblem(r3, r2, "The type 'RI-3' is not assignable to the type 'RI-2'."); + }); + test('Assignability 3 --> 2: not supported (different order of type creation)', () => { + const r2 = restrictedType(2); + const r3 = restrictedType(3); + expectAssignabilityProblem(r3, r2, "The type 'RI-3' is not assignable to the type 'RI-2'."); + }); + + test('Assignability 3 --> any integer: works', () => { + expectAssignability(restrictedType(3), integerType); + }); + + test('Assignability any integer --> 3: not supported', () => { + expectAssignabilityProblem(integerType, restrictedType(3), "The type 'Integer' is not assignable to the type 'RI-3'."); + }); + +}); + + +/** + * Instances of this class represent literals of restricted integers in the AST, i.e. AST nodes. + * In other words, RestrictedIntegerLiteral is for restricted integers, what IntegerLiteral (see predefined-language-nodes.ts) is for integers. + * + * While literals dedicated for restricted integers usually not occur in practical applications (only IntegerLiterals usually occur), + * they are used here for demonstration and to make test cases shorter. + */ +class RestrictedIntegerLiteral extends TestExpressionNode { + constructor( + public value: number, + public upperBound: number, + ) { super(); } +} + +// some predefined literals for restricted integers to be reused in test cases +const int2Limit10 = new RestrictedIntegerLiteral(2, 10); +const int6Limit10 = new RestrictedIntegerLiteral(6, 10); diff --git a/packages/typir/test/kinds/custom/custom-independent.test.ts b/packages/typir/test/kinds/custom/custom-independent.test.ts new file mode 100644 index 00000000..b6e3051f --- /dev/null +++ b/packages/typir/test/kinds/custom/custom-independent.test.ts @@ -0,0 +1,78 @@ +/****************************************************************************** + * 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 { beforeEach, describe, expect, test } from 'vitest'; +import { CustomKind } from '../../../src/kinds/custom/custom-kind.js'; +import { isCustomType } from '../../../src/kinds/custom/custom-type.js'; +import { TestLanguageNode } from '../../../src/test/predefined-language-nodes.js'; +import { TypirServices } from '../../../src/typir.js'; +import { createTypirServicesForTesting, expectTypirTypes } from '../../../src/utils/test-utils.js'; + +// These test cases test that it is possible to work with two different kinds of custom types independent from each other in the same Typir instance, +// even when these custom types/kinds have the same properties! + +export type MyCustomType1 = { + myNumber: number; + myString: string; +}; +export type MyCustomType2 = MyCustomType1; + + +describe('Check that different custom types can be used in parallel', () => { + let typir: TypirServices; + let customKind1: CustomKind; + let customKind2: CustomKind; + + beforeEach(() => { + typir = createTypirServicesForTesting(); + + customKind1 = new CustomKind(typir, { + name: 'MyCustom1', + // use the default 'calculateTypeIdentifier' implementation here + calculateTypeName: properties => `Custom1-${properties.myNumber}`, + }); + customKind2 = new CustomKind(typir, { + name: 'MyCustom2', + // use the default 'calculateTypeIdentifier' implementation here + calculateTypeName: properties => `Custom1-${properties.myNumber}`, // same names! but different identifiers! + }); + }); + + test('The name of CustomKinds needs to be unique', () => { + expect(() => new CustomKind(typir, { + name: 'MyCustom1', + calculateTypeIdentifier: () => 'does not matter', + })).toThrowError("duplicate kind named 'CustomKind-MyCustom1'"); + }); + + test('The name of CustomKinds needs to be unique: a different is not enough', () => { + expect(() => new CustomKind(typir, { + name: 'MyCustom2', + calculateTypeIdentifier: () => 'does not matter', + })).toThrowError("duplicate kind named 'CustomKind-MyCustom2'"); + }); + + test('The name of CustomKinds needs to be unique: it needs to be a different name', () => { + new CustomKind(typir, { + name: 'MyCustom', + calculateTypeIdentifier: () => 'does not matter', + }); + }); + + test('Same properties, but different types', () => { + const typeA1 = customKind1.create({ properties: { myNumber: 222, myString: 'Two' } }).finish().getTypeFinal()!; + const typeA2 = customKind2.create({ properties: { myNumber: 222, myString: 'Two' } }).finish().getTypeFinal()!; + expect(typeA1 === typeA2).toBe(false); // different types ... + expect(typeA1.kind).toBe(customKind1); // ... with different kinds + expect(typeA2.kind).toBe(customKind2); + expect(typeA1.getIdentifier()).not.toBe(typeA2.getIdentifier()); // different identifiers + expect(typeA1.getName()).toBe(typeA2.getName()); // same name (here for testing, in general, that usually does not make sense!) + // we have a single type for both kinds: + expectTypirTypes(typir, type => isCustomType(type, customKind1), 'Custom1-222'); + expectTypirTypes(typir, type => isCustomType(type, customKind2), 'Custom1-222'); + }); + +}); diff --git a/packages/typir/test/kinds/custom/custom-nested-properties.test.ts b/packages/typir/test/kinds/custom/custom-nested-properties.test.ts new file mode 100644 index 00000000..a7931653 --- /dev/null +++ b/packages/typir/test/kinds/custom/custom-nested-properties.test.ts @@ -0,0 +1,198 @@ +/****************************************************************************** + * 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 { beforeEach, describe, expect, test } from 'vitest'; +import { CustomTypeInitialization, CustomTypeStorage } from '../../../src/kinds/custom/custom-definitions.js'; +import { CustomKind } from '../../../src/kinds/custom/custom-kind.js'; +import { PrimitiveType } from '../../../src/kinds/primitive/primitive-type.js'; +import { TestLanguageNode } from '../../../src/test/predefined-language-nodes.js'; +import { TypirServices } from '../../../src/typir.js'; +import { createTypirServicesForTesting } from '../../../src/utils/test-utils.js'; + +// These test cases test that nesting of properties for custom types is possible (including recursion). + +export type NestedProperty = { + myBool: boolean; + myType: PrimitiveType; +}; + + +describe('Check that nested properties for custom types work', () => { + let typir: TypirServices; + let integerType: PrimitiveType; + + beforeEach(() => { + typir = createTypirServicesForTesting(); + integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); + }); + + test('Simple nesting', () => { + type Properties = { + nested: NestedProperty, + } + + const customKind = new CustomKind(typir, { + name: 'MyCustom', + calculateTypeIdentifier: properties => `mycustom-${properties.nested.myBool}-${typir.infrastructure.TypeResolver.resolve(properties.nested.myType).getIdentifier()}`, + calculateTypeName: properties => `Custom-${typir.infrastructure.TypeResolver.resolve(properties.nested.myType).getName()}`, + }); + + const customType = customKind.create({ properties: { nested: { myBool: true, myType: integerType }} }).finish().getTypeFinal()!; + + expect(customType.getIdentifier()).toBe('mycustom-true-Integer'); + expect(customType.getName()).toBe('Custom-Integer'); + expect(customType.properties.nested.myBool).toBe(true); + expect(customType.properties.nested.myType.getType()).toBe(integerType); + }); + + test('Deeper simple nesting', () => { + type Properties = { + nested: { + deeper: NestedProperty, + }, + } + + const customKind = new CustomKind(typir, { + name: 'MyCustom', + /** Compared with the test case above, this calculation creates the same identifiers, since it does not take the deeper nesting into account. + * For these independent test cases, this is no problem. + * But if both kinds are used together in the same Typir instance, the calculation of type identifiers need to be different for both kinds in order to produce unique identifiers. + */ + calculateTypeIdentifier: properties => `mycustom-${properties.nested.deeper.myBool}-${typir.infrastructure.TypeResolver.resolve(properties.nested.deeper.myType).getIdentifier()}`, + calculateTypeName: properties => `Custom-${typir.infrastructure.TypeResolver.resolve(properties.nested.deeper.myType).getName()}`, + }); + + const customType = customKind.create({ properties: { nested: { deeper: { myBool: true, myType: integerType } }} }).finish().getTypeFinal()!; + + expect(customType.getIdentifier()).toBe('mycustom-true-Integer'); + expect(customType.getName()).toBe('Custom-Integer'); + expect(customType.properties.nested.deeper.myBool).toBe(true); + expect(customType.properties.nested.deeper.myType.getType()).toBe(integerType); + }); + + test('Simple grouping with sets, arrays, maps', () => { + type Properties = { + mySet: Set, + myArray: NestedProperty[], + myMap: Map, + } + + const customKind = new CustomKind(typir, { + name: 'MyCustom', + calculateTypeIdentifier: properties => `mycustom-${properties.myArray.map(entry => `${entry.myBool}#${typir.infrastructure.TypeResolver.resolve(entry.myType).getIdentifier()}`).join(',')}`, + calculateTypeName: properties => `Custom-${properties.myArray.map(entry => `${entry.myBool}#${typir.infrastructure.TypeResolver.resolve(entry.myType).getName()}`).join(',')}`, + }); + + const mapValue: Map = new Map(); + mapValue.set('hello', { myBool: true, myType: integerType }); + mapValue.set('world', { myBool: false, myType: integerType }); + + const customType = customKind.create({ properties: { + myArray: [{ myBool: true, myType: integerType }, { myBool: false, myType: integerType }], + mySet: new Set([{ myBool: true, myType: integerType }, { myBool: false, myType: integerType }]), + myMap: mapValue, + } }).finish().getTypeFinal()!; + + expect(customType.getIdentifier()).toBe('mycustom-true#Integer,false#Integer'); + expect(customType.getName()).toBe('Custom-true#Integer,false#Integer'); + + // set + expect(customType.properties.mySet.size).toBe(2); + customType.properties.mySet.forEach(entry => expect(entry.myType.getType()).toBe(integerType)); + + // array + expect(customType.properties.myArray.length).toBe(2); + expect(customType.properties.myArray[0].myBool).toBe(true); + expect(customType.properties.myArray[0].myType.getType()).toBe(integerType); + expect(customType.properties.myArray[1].myBool).toBe(false); + expect(customType.properties.myArray[1].myType.getType()).toBe(integerType); + + // map + expect(customType.properties.myMap.size).toBe(2); + expect(customType.properties.myMap.get('hello')).toBeTruthy(); + expect(customType.properties.myMap.get('hello')!.myBool).toBe(true); + expect(customType.properties.myMap.get('hello')!.myType.getType()).toBe(integerType); + expect(customType.properties.myMap.get('world')).toBeTruthy(); + expect(customType.properties.myMap.get('world')!.myBool).toBe(false); + expect(customType.properties.myMap.get('world')!.myType.getType()).toBe(integerType); + expect(customType.properties.myMap.get('hello world')).toBeFalsy(); + }); + + test('More complex nesting', () => { + type Properties = { + myString: string, + nested: { + deepArray: NestedProperty[], + myNumber: number, + }, + } + + const customKind = new CustomKind(typir, { + name: 'MyCustom', + calculateTypeIdentifier: properties => `mycustom-${properties.nested.deepArray.map(entry => typir.infrastructure.TypeResolver.resolve(entry.myType).getIdentifier()).join(',')}`, + calculateTypeName: properties => `Custom-${properties.nested.deepArray.map(entry => typir.infrastructure.TypeResolver.resolve(entry.myType).getName()).join(',')}`, + }); + + const customType = customKind.create({ properties: { + nested: { + deepArray: [ + { myBool: true, myType: integerType }, + { myBool: false, myType: integerType }, + ], + myNumber: 123, + }, + myString: 'hello', + } }).finish().getTypeFinal()!; + + expect(customType.getIdentifier()).toBe('mycustom-Integer,Integer'); + expect(customType.getName()).toBe('Custom-Integer,Integer'); + customType.properties.nested.deepArray.forEach(entry => expect(entry.myType.getType()).toBe(integerType)); + }); + + test('Recursion in properties type', () => { + type Properties = { + myContent: NestedProperty, + myRecursion?: Properties, + } + + function calculate(properties: CustomTypeInitialization, desired: 'Identifier'|'Name'): string { + const own = desired === 'Identifier' + ? typir.infrastructure.TypeResolver.resolve(properties.myContent.myType).getIdentifier() + : typir.infrastructure.TypeResolver.resolve(properties.myContent.myType).getName(); + if (properties.myRecursion) { + return `${own}-${calculate(properties.myRecursion, desired)}`; + } else { + return own; + } + } + + const customKind = new CustomKind(typir, { + name: 'MyCustom', + calculateTypeIdentifier: properties => `mycustom-${calculate(properties, 'Identifier')}`, + calculateTypeName: properties => `Custom-${calculate(properties, 'Name')}`, + }); + + const customType = customKind.create({ properties: { + myContent: { myBool: true, myType: integerType }, + myRecursion: { + myContent: { myBool: false, myType: integerType }, + myRecursion: { + myContent: { myBool: true, myType: integerType }, + // no more entries + }, + }, + } }).finish().getTypeFinal()!; + + expect(customType.getIdentifier()).toBe('mycustom-Integer-Integer-Integer'); + expect(customType.getName()).toBe('Custom-Integer-Integer-Integer'); + let current: CustomTypeStorage | undefined = customType.properties; + while (current) { + expect(current.myContent.myType.getType()).toBe(integerType); + current = current.myRecursion; + } + }); + +}); diff --git a/packages/typir/test/kinds/custom/custom-selectors.test.ts b/packages/typir/test/kinds/custom/custom-selectors.test.ts new file mode 100644 index 00000000..56ec9699 --- /dev/null +++ b/packages/typir/test/kinds/custom/custom-selectors.test.ts @@ -0,0 +1,145 @@ +/****************************************************************************** + * 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 { beforeEach, describe, expect, test } from 'vitest'; +import { CustomKind } from '../../../src/kinds/custom/custom-kind.js'; +import { CustomType } from '../../../src/kinds/custom/custom-type.js'; +import { TestExpressionNode, TestLanguageNode } from '../../../src/test/predefined-language-nodes.js'; +import { TypirServices } from '../../../src/typir.js'; +import { createTypirServicesForTesting } from '../../../src/utils/test-utils.js'; + +// These test cases test that all possible TypeSelectors work for custom types. + +export type MyCustomProperties = { + dependsOnType?: CustomType; + myProperty: number; +}; + +describe('Test all possible TypeSelectors with custom types', () => { + let typir: TypirServices; + let customKind: CustomKind; + + beforeEach(() => { + typir = createTypirServicesForTesting(); + + customKind = new CustomKind(typir, { + name: 'MyCustom', + calculateTypeIdentifier: properties => + `custom-${properties.myProperty}-(${properties.dependsOnType ? typir.infrastructure.TypeResolver.resolve(properties.dependsOnType).getIdentifier() : ''})`, + }); + }); + + test('Type', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }).finish().getTypeFinal()!; + // custom2 depends on custom1 + const custom2 = customKind.create({ properties: { dependsOnType: custom1, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + }); + test('() => Type', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }).finish().getTypeFinal()!; + // custom2 depends on custom1 + const custom2 = customKind.create({ properties: { dependsOnType: () => custom1, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + }); + + // 'string' is not supported by design, since string values are used for string primitives! + test('() => string', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }).finish().getTypeFinal()!; + // custom2 depends on custom1, identified by its identifier + const custom2 = customKind.create({ properties: { dependsOnType: () => custom1.getIdentifier(), myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + }); + test('() => string (delayed)', () => { + // custom2 depends on custom1, identified by its identifier, but custom1 does not yet exist + const custom2 = customKind.create({ properties: { dependsOnType: () => 'custom-1-()', myProperty: 2 } }).finish(); + expect(custom2.getTypeFinal()).toBe(undefined); + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }).finish().getTypeFinal()!; + // now custom2 is complete + expect(custom2.getTypeFinal()).toBeTruthy(); + expect(custom2.getTypeFinal()!.properties.dependsOnType?.getType()).toBe(custom1); + }); + + test('TypeInitializer', () => { + // custom1 depends on nothing + const initializer1 = customKind.create({ properties: { myProperty: 1 } }).finish(); + const custom1 = initializer1.getTypeFinal()!; + // custom2 depends on custom1 + const custom2 = customKind.create({ properties: { dependsOnType: initializer1, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + }); + test('() => TypeInitializer', () => { + // custom1 depends on nothing + const initializer1 = customKind.create({ properties: { myProperty: 1 } }).finish(); + // custom2 depends on custom1 + const custom2 = customKind.create({ properties: { dependsOnType: () => initializer1, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe((initializer1.getTypeFinal())); + }); + + test('TypeReference', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }).finish().getTypeFinal()!; + // custom2 depends on custom1 + const custom2 = customKind.create({ properties: { dependsOnType: custom1, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + // custom3 depends on custom1, accessed via the TypeReference of custom2 to custom1 + const custom3 = customKind.create({ properties: { dependsOnType: custom2.properties.dependsOnType, myProperty: 3 } }).finish().getTypeFinal()!; + expect(custom3.properties.dependsOnType?.getType()).toBe(custom1); + }); + test('() => TypeReference', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }).finish().getTypeFinal()!; + // custom2 depends on custom1 + const custom2 = customKind.create({ properties: { dependsOnType: custom1, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + // custom3 depends on custom1, accessed via the TypeReference of custom2 to custom1 + const custom3 = customKind.create({ properties: { dependsOnType: custom2.properties.dependsOnType, myProperty: 3 } }).finish().getTypeFinal()!; + expect(custom3.properties.dependsOnType?.getType()).toBe(custom1); + }); + + + class CustomLiteral extends TestExpressionNode { + constructor( + public value: number, + ) { super(); } + } + const literalForTesting = new CustomLiteral(123); + + test('LanguageNode (type inference of TestLanguageNode)', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }) + .inferenceRule({ matching: node => node === literalForTesting }) // very simple rule, just for testing + .finish().getTypeFinal()!; + // custom2 depends on custom1, specified by 'literalForTesting' whose inferred type is used + const custom2 = customKind.create({ properties: { dependsOnType: literalForTesting, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + }); + test('() => LanguageNode (type inference of TestLanguageNode)', () => { + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }) + .inferenceRule({ matching: node => node === literalForTesting }) // very simple rule, just for testing + .finish().getTypeFinal()!; + // custom2 depends on custom1, specified by 'literalForTesting' whose inferred type is used + const custom2 = customKind.create({ properties: { dependsOnType: () => literalForTesting, myProperty: 2 } }).finish().getTypeFinal()!; + expect(custom2.properties.dependsOnType?.getType()).toBe(custom1); + }); + test('LanguageNode (type inference of TestLanguageNode) (delayed)', () => { + // custom2 depends on custom1, specified by 'literalForTesting' whose inferred type is used + const custom2 = customKind.create({ properties: { dependsOnType: literalForTesting, myProperty: 2 } }).finish(); + expect(custom2.getTypeFinal()).toBe(undefined); + // custom1 depends on nothing + const custom1 = customKind.create({ properties: { myProperty: 1 } }) + .inferenceRule({ matching: node => node === literalForTesting }) // very simple rule, just for testing + .finish().getTypeFinal()!; + // now, custom2 is complete + expect(custom2.getTypeFinal).toBeTruthy(); + expect(custom2.getTypeFinal()!.properties.dependsOnType?.getType()).toBe(custom1); + }); + +}); diff --git a/packages/typir/test/kinds/function/operator-overloaded.test.ts b/packages/typir/test/kinds/function/operator-overloaded.test.ts index cff237d0..2700be16 100644 --- a/packages/typir/test/kinds/function/operator-overloaded.test.ts +++ b/packages/typir/test/kinds/function/operator-overloaded.test.ts @@ -4,8 +4,6 @@ * 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 { Type } from '../../../src/graph/type-node.js'; import { isPrimitiveType, PrimitiveType } from '../../../src/kinds/primitive/primitive-type.js'; diff --git a/packages/typir/test/kinds/primitive/primitive.test.ts b/packages/typir/test/kinds/primitive/primitive.test.ts index b1d18170..6fba8ae3 100644 --- a/packages/typir/test/kinds/primitive/primitive.test.ts +++ b/packages/typir/test/kinds/primitive/primitive.test.ts @@ -30,7 +30,17 @@ describe('Tests some details for primitive types', () => { assertTypirType(integerType1, isPrimitiveType, 'integer'); // creating the 2nd integer will fail expect(() => typir.factory.Primitives.create({ primitiveName: 'integer' }).finish()) - .toThrowError(); + .toThrowError("There is already a primitive type with name 'integer'."); + }); + test('error when trying to create the same primitive twice (with delayed finish())', () => { + const typir = createTypirServicesForTesting(); + // start both type definitions + const integerType1 = typir.factory.Primitives.create({ primitiveName: 'integer' }); + const integerType2 = typir.factory.Primitives.create({ primitiveName: 'integer' }); + // now finish the types + assertTypirType(integerType1.finish(), isPrimitiveType, 'integer'); + expect(() => integerType2.finish()) + .toThrowError("There is already a type with the identifier 'integer'."); }); describe('Test validation for inference rule of a primitive type', () => { diff --git a/packages/typir/test/services/inference-registry.test.ts b/packages/typir/test/services/inference-registry.test.ts index 227605e5..f2fd758f 100644 --- a/packages/typir/test/services/inference-registry.test.ts +++ b/packages/typir/test/services/inference-registry.test.ts @@ -248,6 +248,6 @@ describe('Tests the logic for registering rules (applied to inference rules)', ( }); class TestInferenceImpl extends DefaultTypeInferenceCollector { - // make the public to access their details + // change its visibility to public to access their details override readonly ruleRegistry: RuleRegistry, TestLanguageNode>; }