diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c55b36d..a5d1302c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,6 @@ 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-??-??) - -### New features - -- New example how to use Typir (core) for a simple expression language with a handwritten parser (#59) - -## Fixed bugs - -- 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-??-??) @@ -21,6 +11,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] ### New features +- New example how to use Typir (core) for a simple expression language with a handwritten parser (#59) - 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 @@ -31,17 +22,24 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - 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. -- ... +- Create Typir services with additional services, which are specific for the current application (#78): + - Typir core: `createTypirServicesWithAdditionalServices<..., AdditionalServices>(Module, ...)`, see `customization-example.test.ts` for examples and explanations + - Typir-Langium: `createTypirLangiumServicesWithAdditionalServices<..., AdditionalServices>(..., Module, ...)` works in the same way + - Internal testing in Typir (core): `createTypirServicesForTestingWithAdditionalServices(Module, ...)` +- The `$name`s of kinds/factories are configurable now (#78). +- Typir-Langium: The Langium services are stored in the `TypirLangiumAddedServices` now as `services.langium.LangiumServices` in order to make them available for all Typir services (#78). ### Breaking changes -- ... +- Typir-Langium: `LangiumLanguageNodeInferenceCaching` and `DefaultLangiumTypeCreator` use the `TypirLangiumServices` parameter to retrieve the `LangiumSharedCoreServices` now (#78). ### 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) -- ... +- The logic to ensure that types are not created multiple times needs to check that the kind of the types is the same. Otherwise a collision of duplicated identifiers of types needs to be reported (#78). +- Specified sub-super-relationships of language keys for the predefined test fixtures in `predefined-language-nodes.ts` (#78) +- Fixed the implementation for merging modules for dependency injection (DI), it is exactly the same fix from [Langium](https://github.com/eclipse-langium/langium/pull/1939), since we reused its DI implementation (#79). ## v0.2.1 (2025-04-09) diff --git a/documentation/customization.md b/documentation/customization.md index fe5086e3..6f3a826f 100644 --- a/documentation/customization.md +++ b/documentation/customization.md @@ -1,11 +1,78 @@ -# Customize default implementations +# Customize Typir -This describes how the default implementations of Typir can be customized. +This describes how the default behaviour of Typir can be customized. How to use custom types in Typir is described [in this section](./kinds/custom-types.md). If you are already familar with Langium and its [strategies for customization](https://langium.org/docs/reference/configuration-services/#customization), feel free to skip this section, since the strategies and even the implementation are nearly the same. As described in the [design section](./design.md), nearly all features of Typir are exposed by APIs in form of interfaces, -for which Typir provides classes implementing these interfaces as default implementations. These interfaces and implementations are composed in `typir.ts`. +for which Typir provides classes implementing these interfaces as default implementations. These interfaces and implementations are composed in ... -TODO +- `typir.ts` for Typir (core) +- `typir-langium.ts` for Typir-Langium + +Some examples how to customize existing services and how to add new services are sketched in `customization-example.test.ts`. + + +## Customize the implementation of existing services + +To customize or replace the default implementation for an existing Typir service, just provide another implementation when initializing the Typir services. +As an example, the existing factory to create classes is replaced to allow two super classes now (default is one super class only): + +```typescript +const customizedTypir = createTypirServices({ + factory: { + Classes: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2 }), + }, + // ... customize as many existing services as you like ... +}); +``` + +## Add additional services + +Additional services need to be explicitly specified. +In general, you can add an arbitrary number of services, which might be deeply grouped. +It is even possible to add new services to already existing groups. +In the following example, an additional factory for classes is exposed as service: + +```typescript +type AdditionalExampleTypirServices = { + readonly factory: { + readonly OtherClasses: ClassFactoryService; + }, +}; +``` + +Mark new services with the keyword `readonly` to prevent changing them at runtime, since they are instantiated only once when they are used for the first time. +Specify implementations for all added services which are considered when you instantiate the Typir services. +Instead of `createTypirServices`, use `createTypirServicesWithAdditionalServices` instead and specify the new services as generic (here `<..., AdditionalExampleTypirServices>`): + +```typescript +const customizedTypir: TypirServices & AdditionalExampleTypirServices = createTypirServicesWithAdditionalServices({ + factory: { + OtherClasses: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2, $name: 'OtherClass' }), + }, +}); +``` + +TypeScript doesn't force you to write `TypirServices & AdditionalExampleTypirServices` in the code snipped above, but makes explicit what is going on here: +You get the `TypirServices` as usual, but they are combined with your defined `AdditionalExampleTypirServices`, i.e. you get only one object back containing default and custom services. Additionally, both default and custom services are correctly TypeScript-typed. + +To simplify the code, it is recommended (but not mandatory) to introduce a TypeScript type like the following and to use it instead, since it makes explicit that the current Typir services are customized: + +```typescript +type ExampleTypirServices = TypirServices & AdditionalExampleTypirServices; +``` + +Newly added services are usable by all other services, including new services and existing services. +The latter is important when customizing default implementations, when the custom implementation depends on the new services. + +It is possible to provide implementations for new services together with customizations for existing services: + +```typescript +const customizedTypir: ExampleTypirServices = createTypirServicesWithAdditionalServices({ + // 1st mandatory argument: implementations for all new services +}, { + // 2nd optional argument: customize some (existing or new) services here +}, /* even more arguments for even more customizations for services are possible here */); +``` diff --git a/documentation/design.md b/documentation/design.md index 34646f9f..4e7c4b36 100644 --- a/documentation/design.md +++ b/documentation/design.md @@ -6,8 +6,16 @@ This describes the main design principles of Typir. ### Type -- identifier -- name +All types need to have *unique identifiers* in order to identifier duplicate types and to access types by their identifier. +If Typir reports errors regarding non-unique identifiers, check the following possibles reasons for colliding identifiers: + +- The calculation for identifiers does not encode all relevant properties in the identifier. In that case, for types with different properties, the same identifier is calculated, which leads to collisions. +- Two different type factories produce colliding identifiers, e.g. since the same type factory is instantiated multiple times with the same prefix for identifiers or the same calculation of identifiers. + +Types also have a *name*, which is used as a short name for types, e.g. used to be shown in error messages to users. Names don't need to be unique. + +TODO: + - single instances - kind @@ -30,6 +38,8 @@ Each type system, i.e. each instance of the `TypirServices`, has one type graph: - services - (default) implementations - Typir module in `typir.ts`: assembles services and implementations + - It is possible to group services + - Names of services start with an uppercase letter, names of groups start with a lowercase letter - Dependency injection (DI) diff --git a/documentation/kinds/custom-types.md b/documentation/kinds/custom-types.md index f87cb0b8..954dbb35 100644 --- a/documentation/kinds/custom-types.md +++ b/documentation/kinds/custom-types.md @@ -1,7 +1,7 @@ # Custom types 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. +This section describes how language-specific custom types can be defined and used in Typir. ## API by example @@ -23,8 +23,8 @@ Then you create a new factory for these matrix types: ```typescript const matrixFactory = new CustomKind(typir, { name: 'Matrix', + // ... here you can specify some optional rules for type names, conversion, sub-types, ... for all matrix types: calculateTypeName: properties => `My${properties.width}x${properties.height}Matrix`, - // ... here you can specify additional rules for conversion, sub-types, ... for all matrix types ... }); ``` @@ -38,6 +38,28 @@ const matrix2x3 = matrixFactory See `custom-example-restricted.test.ts` for another application example. +In order to provide the matrix factory like the other predefined factories for primitives, functions and so on, +read the [section about customization](../customization.md), summarized as follows: + +```typescript +// define your custom factory as additional Typir service +type AdditionalMatrixTypirServices = { + readonly factory: { + readonly Matrix: CustomKind; + } +} + +// specify the additional services as TypeScript generic when initializing the Typir services and provide the custom factory +const typir = createTypirServicesWithAdditionalServices({ + factory: { + Matrix: services => new CustomKind(services, { ... }) + } +}); + +// now the custom matrix factory is usable like the predefined factories +typir.factory.Matrix.create({ ... }).finish().getTypeFinal()!; +``` + ## Features @@ -57,8 +79,9 @@ In the example above, calling `matrix2x3.properties.width` is supported by auto- ### 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. +Two custom types are identical, if their identifiers are the same (this counts for any type, not only for custom types, see the [general design](../design.md) for types). +The default implementation calculates the identifier by concatenating the values of all properties and therefore provides a sufficient default solution. +Nevertheless, it is possible to customize the calculation of identifiers (`calculateTypeIdentifier`), e.g. to improve their readability. ### Circular dependencies @@ -90,5 +113,6 @@ See `custom-independent.test.ts` for an example. - 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`. + As a workaround for the identifier `'MyIdentifier'`, use `() => 'MyIdentifier'` instead. - 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-caching.ts b/packages/typir-langium/src/features/langium-caching.ts index f2742bdc..e03489e4 100644 --- a/packages/typir-langium/src/features/langium-caching.ts +++ b/packages/typir-langium/src/features/langium-caching.ts @@ -4,16 +4,17 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, DocumentCache, DocumentState, LangiumSharedCoreServices } from 'langium'; +import { AstNode, DocumentCache, DocumentState } from 'langium'; import { CachePending, LanguageNodeInferenceCaching, Type } from 'typir'; -import { getDocumentKey } from '../utils/typir-langium-utils.js'; +import { TypirLangiumServices } from '../typir-langium.js'; +import { getDocumentKey, LangiumAstTypes } from '../utils/typir-langium-utils.js'; // cache AstNodes -export class LangiumLanguageNodeInferenceCaching implements LanguageNodeInferenceCaching { +export class LangiumLanguageNodeInferenceCaching implements LanguageNodeInferenceCaching { protected readonly cache: DocumentCache; // removes cached AstNodes, if their underlying LangiumDocuments are invalidated - constructor(langiumServices: LangiumSharedCoreServices) { - this.cache = new DocumentCache(langiumServices, DocumentState.IndexedReferences); + constructor(typirServices: TypirLangiumServices) { + this.cache = new DocumentCache(typirServices.langium.LangiumServices, DocumentState.IndexedReferences); } cacheSet(languageNode: AstNode, type: Type): void { diff --git a/packages/typir-langium/src/features/langium-type-creator.ts b/packages/typir-langium/src/features/langium-type-creator.ts index 3ecdd556..bb5ec4a0 100644 --- a/packages/typir-langium/src/features/langium-type-creator.ts +++ b/packages/typir-langium/src/features/langium-type-creator.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, DocumentState, interruptAndCheck, LangiumDocument, LangiumSharedCoreServices } from 'langium'; +import { AstNode, AstUtils, DocumentState, interruptAndCheck, LangiumDocument } from 'langium'; import { Type, TypeGraph, TypeGraphListener } from 'typir'; import { TypirLangiumServices } from '../typir-langium.js'; import { getDocumentKeyForDocument, getDocumentKeyForURI, LangiumAstTypes } from '../utils/typir-langium-utils.js'; @@ -55,14 +55,14 @@ export class DefaultLangiumTypeCreator impleme protected readonly typeGraph: TypeGraph; protected readonly typeSystemDefinition: LangiumTypeSystemDefinition; - constructor(typirServices: TypirLangiumServices, langiumServices: LangiumSharedCoreServices) { + constructor(typirServices: TypirLangiumServices) { this.typir = typirServices; this.typeGraph = typirServices.infrastructure.Graph; this.typeSystemDefinition = typirServices.langium.TypeSystemDefinition; // for new and updated documents: // Create Typir types after completing the Langium 'ComputedScopes' phase, since they need to be available for the following Linking phase - langiumServices.workspace.DocumentBuilder.onBuildPhase(DocumentState.ComputedScopes, async (documents, cancelToken) => { + typirServices.langium.LangiumServices.workspace.DocumentBuilder.onBuildPhase(DocumentState.ComputedScopes, async (documents, cancelToken) => { for (const document of documents) { await interruptAndCheck(cancelToken); @@ -73,7 +73,7 @@ export class DefaultLangiumTypeCreator impleme // for deleted documents: // Delete Typir types which are derived from AstNodes of deleted documents - langiumServices.workspace.DocumentBuilder.onUpdate((_changed, deleted) => { + typirServices.langium.LangiumServices.workspace.DocumentBuilder.onUpdate((_changed, deleted) => { deleted .map(del => getDocumentKeyForURI(del)) .forEach(del => this.invalidateTypesOfDocument(del)); diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index a30b24ab..9c0f8336 100644 --- a/packages/typir-langium/src/typir-langium.ts +++ b/packages/typir-langium/src/typir-langium.ts @@ -20,7 +20,8 @@ import { LangiumAstTypes } from './utils/typir-langium-utils.js'; */ export type TypirLangiumAddedServices = { readonly Inference: LangiumTypeInferenceCollector; // concretizes the TypeInferenceCollector for Langium - readonly langium: { // new services which are specific for Langium + readonly langium: { // all new services which are specific for Langium + readonly LangiumServices: LangiumSharedCoreServices; // store the Langium services to make them available for all Typir services readonly TypeCreator: LangiumTypeCreator; readonly TypeSystemDefinition: LangiumTypeSystemDefinition; }; @@ -40,12 +41,12 @@ export type PartialTypirLangiumServices = Deep * @param langiumServices Typir-Langium needs to interact with the Langium lifecycle * @returns (only) the replaced implementations */ -export function createLangiumSpecificTypirServicesModule(langiumServices: LangiumSharedCoreServices): Module> { +export function createLangiumSpecificTypirServicesModule(_langiumServices: LangiumSharedCoreServices): Module, PartialTypirServices> { return { Printer: () => new LangiumProblemPrinter(), Language: () => { throw new Error('Use new LangiumLanguageService(undefined), and replace "undefined" by the generated XXXAstReflection!'); }, // to be replaced later caching: { - LanguageNodeInference: () => new LangiumLanguageNodeInferenceCaching(langiumServices), + LanguageNodeInference: (typirServices) => new LangiumLanguageNodeInferenceCaching(typirServices), }, }; } @@ -59,7 +60,8 @@ export function createDefaultTypirLangiumServicesModule new DefaultLangiumTypeInferenceCollector(typirServices), langium: { - TypeCreator: (typirServices) => new DefaultLangiumTypeCreator(typirServices, langiumServices), + LangiumServices: () => langiumServices, + TypeCreator: (typirServices) => new DefaultLangiumTypeCreator(typirServices), TypeSystemDefinition: () => { throw new Error('The type system needs to be specified!'); }, // to be replaced later }, validation: { @@ -78,19 +80,24 @@ export function createDefaultTypirLangiumServicesModule( - langiumServices: LangiumSharedCoreServices, reflection: AbstractAstReflection, typeSystemDefinition: LangiumTypeSystemDefinition, - customization1: Module> = {}, - customization2: Module> = {}, - customization3: Module> = {}, + langiumServices: LangiumSharedCoreServices, + reflection: AbstractAstReflection, + typeSystemDefinition: LangiumTypeSystemDefinition, + customization1?: Module>, + customization2?: Module>, + customization3?: Module>, + customization4?: Module>, ): TypirLangiumServices { return inject( - // use all core Typir services ... + // use the default implementations for all core Typir services ... createDefaultTypirServicesModule(), // ... with adapted implementations for Typir-Langium - createLangiumSpecificTypirServicesModule(langiumServices), + createLangiumSpecificTypirServicesModule(langiumServices), // add the additional services for the Typir-Langium binding createDefaultTypirLangiumServicesModule(langiumServices), // add the language-specific parts provided by Langium into the Typir-Services @@ -104,6 +111,55 @@ export function createTypirLangiumServices( customization1, // ... production customization2, // ... testing (in order to replace some customizations of production) customization3, // ... testing (e.g. to have customizations for all test cases and for single test cases) + customization4, // ... for even more flexibility + ); +} + +/** + * This is the entry point to create Typir-Langium services to simplify type checking for DSLs developed with Langium, + * the language workbench for textual domain-specific languages (DSLs) in the web (https://langium.org/). + * Additionally, some new services are defined, and implementations for them are registered. + * @param langiumServices Typir-Langium needs to interact with the Langium lifecycle + * @param reflection Typir-Langium needs to know the existing AstNode$.types in order to do some performance optimizations + * @param typeSystemDefinition the actual definition of the type system + * @param moduleForAdditionalServices contains the configurations for all added services + * @param customization1 some optional customizations of the Typir-Langium and Typir(-core) services, e.g. for production + * @param customization2 some optional customizations of the Typir-Langium and Typir(-core) services, e.g. for testing + * @param customization3 some optional customizations of the Typir-Langium and Typir(-core) services, e.g. for testing + * @param customization4 some optional customizations of the Typir-Langium and Typir(-core) services + * @returns the Typir services configured for the current Langium-based language + */ +export function createTypirLangiumServicesWithAdditionalServices( + langiumServices: LangiumSharedCoreServices, + reflection: AbstractAstReflection, + typeSystemDefinition: LangiumTypeSystemDefinition, + moduleForAdditionalServices: Module & AdditionalServices, AdditionalServices>, + customization1?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, + customization2?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, + customization3?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, + customization4?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, +): TypirLangiumServices & AdditionalServices { + return inject( + // use the default implementations for all core Typir services ... + createDefaultTypirServicesModule(), + // ... with adapted implementations for Typir-Langium + createLangiumSpecificTypirServicesModule(langiumServices), + // add the additional services for the Typir-Langium binding + createDefaultTypirLangiumServicesModule(langiumServices), + // add the language-specific parts provided by Langium into the Typir-Services + >>{ + langium: { + TypeSystemDefinition: () => typeSystemDefinition, + }, + Language: () => new LangiumLanguageService(reflection), + }, + // add implementations for all additional services + moduleForAdditionalServices, + // optionally add some more language-specific customization, e.g. for ... + customization1, // ... production + customization2, // ... testing (in order to replace some customizations of production) + customization3, // ... testing (e.g. to have customizations for all test cases and for single test cases) + customization4, // ... for even more flexibility ); } diff --git a/packages/typir-langium/src/utils/typir-langium-utils.ts b/packages/typir-langium/src/utils/typir-langium-utils.ts index f0ca9349..2842f406 100644 --- a/packages/typir-langium/src/utils/typir-langium-utils.ts +++ b/packages/typir-langium/src/utils/typir-langium-utils.ts @@ -25,8 +25,8 @@ export async function deleteAllDocuments(services: LangiumSharedCoreServices) { .map((x) => x.uri) .toArray(); await services.workspace.DocumentBuilder.update( - [], // update no documents - docsToDelete // delete all documents + [], // update no documents, but ... + docsToDelete // delete all documents ); } diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index dd02761b..a91c9472 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -6,7 +6,7 @@ import { TypeReference } from '../initialization/type-reference.js'; import { WaitingForIdentifiableAndCompletedTypeReferences, WaitingForInvalidTypeReferences } from '../initialization/type-waiting.js'; -import { Kind, isKind } from '../kinds/kind.js'; +import { Kind } from '../kinds/kind.js'; import { TypirProblem } from '../utils/utils-definitions.js'; import { assertTrue, assertUnreachable, removeFromArray } from '../utils/utils.js'; import { TypeEdge } from './type-edge.js'; @@ -410,7 +410,7 @@ export abstract class Type { } export function isType(type: unknown): type is Type { - return typeof type === 'object' && type !== null && typeof (type as Type).getIdentifier === 'function' && isKind((type as Type).kind); + return typeof type === 'object' && type !== null && typeof (type as Type).getIdentifier === 'function' && typeof (type as Type).kind === 'object'; } diff --git a/packages/typir/src/initialization/type-initializer.ts b/packages/typir/src/initialization/type-initializer.ts index e14e3dd1..8055b2e6 100644 --- a/packages/typir/src/initialization/type-initializer.ts +++ b/packages/typir/src/initialization/type-initializer.ts @@ -42,6 +42,9 @@ export abstract class TypeInitializer { } const existingType = this.services.infrastructure.Graph.getType(key); if (existingType) { + if (newType.kind !== existingType.kind) { + throw new Error(`The identifier '${key}' for the new type of kind '${newType.kind.$name}' (implemented in ${newType.kind.constructor.name}) collides with the identifier '${key}' of an existing type of kind '${existingType.kind.$name}' (implemented in ${existingType.kind.constructor.name}).`); + } // ensure, that the same type is not duplicated! this.typeToReturn = existingType as T; newType.dispose(); diff --git a/packages/typir/src/kinds/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index 11ad254f..b51e6592 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -8,7 +8,7 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; -import { isKind, Kind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { BottomType } from './bottom-type.js'; export interface BottomTypeDetails extends TypeDetails { @@ -18,7 +18,7 @@ export interface CreateBottomTypeDetails extends BottomTypeDetails inferenceRules: Array>; } -export interface BottomKindOptions { +export interface BottomKindOptions extends KindOptions { name: string; } @@ -35,20 +35,21 @@ interface BottomConfigurationChain { } export class BottomKind implements Kind, BottomFactoryService { - readonly $name: 'BottomKind'; + readonly $name: string; readonly services: TypirServices; readonly options: Readonly; constructor(services: TypirServices, options?: Partial) { - this.$name = BottomKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); } protected collectOptions(options?: Partial): BottomKindOptions { return { // the default values: + $name: BottomKindName, name: 'never', // the actually overriden values: ...options @@ -72,7 +73,7 @@ export class BottomKind implements Kind, BottomFactoryService(kind: unknown): kind is BottomKind { - return isKind(kind) && kind.$name === BottomKindName; + return kind instanceof BottomKind; } diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index 9f85043c..68c1d873 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -16,13 +16,13 @@ import { InferCurrentTypeRule, RegistrationOptions } from '../../utils/utils-def import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, assertTypirType, toArray } from '../../utils/utils.js'; import { FunctionType } from '../function/function-type.js'; -import { Kind, isKind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { ClassTypeInitializer } from './class-initializer.js'; import { ClassType, isClassType } from './class-type.js'; import { NoSuperClassCyclesValidationOptions, UniqueClassValidation, UniqueMethodValidation, UniqueMethodValidationOptions, createNoSuperClassCyclesValidation } from './class-validation.js'; -import { TopClassKind, TopClassKindName, isTopClassKind } from './top-class-kind.js'; +import { TopClassKind, TopClassKindName } from './top-class-kind.js'; -export interface ClassKindOptions { +export interface ClassKindOptions extends KindOptions { typing: 'Structural' | 'Nominal', // JS classes are nominal, TS structures are structural /** Values < 0 indicate an arbitrary number of super classes. */ maximumNumberOfSuperClasses: number, @@ -101,21 +101,22 @@ export interface ClassConfigurationChain { * The order of fields is not defined, i.e. there is no order of fields. */ export class ClassKind implements Kind, ClassFactoryService { - readonly $name: 'ClassKind'; + readonly $name: string; readonly services: TypirServices; readonly options: Readonly; constructor(services: TypirServices, options?: Partial) { - this.$name = ClassKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); assertTrue(this.options.maximumNumberOfSuperClasses >= 0); // no negative values } protected collectOptions(options?: Partial): ClassKindOptions { return { // the default values: + $name: ClassKindName, typing: 'Nominal', maximumNumberOfSuperClasses: 1, subtypeFieldChecking: 'EQUAL_TYPE', @@ -214,8 +215,7 @@ export class ClassKind implements Kind, ClassFactoryService { // ensure, that Typir uses the predefined 'TopClass' kind - const kind = this.services.infrastructure.Kinds.get(TopClassKindName); - return isTopClassKind(kind) ? kind : new TopClassKind(this.services); + return this.services.infrastructure.Kinds.getOrCreateKind(TopClassKindName, services => new TopClassKind(services)); } createUniqueClassValidation(options: RegistrationOptions): UniqueClassValidation { @@ -250,7 +250,7 @@ export class ClassKind implements Kind, ClassFactoryService(kind: unknown): kind is ClassKind { - return isKind(kind) && kind.$name === ClassKindName; + return kind instanceof ClassKind; } diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index a61d9784..e2aac996 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -119,7 +119,10 @@ export class ClassType extends Type { // check number of allowed super classes if (this.kind.options.maximumNumberOfSuperClasses >= 0) { if (this.kind.options.maximumNumberOfSuperClasses < this.getDeclaredSuperClasses().length) { - throw new Error(`Only ${this.kind.options.maximumNumberOfSuperClasses} super-classes are allowed.`); + throw new Error(this.kind.options.maximumNumberOfSuperClasses === 1 + ? 'Only 1 super-class is allowed.' + : `Only ${this.kind.options.maximumNumberOfSuperClasses} super-classes are allowed.` + ); } } }, diff --git a/packages/typir/src/kinds/class/top-class-kind.ts b/packages/typir/src/kinds/class/top-class-kind.ts index 5368c9b7..44446420 100644 --- a/packages/typir/src/kinds/class/top-class-kind.ts +++ b/packages/typir/src/kinds/class/top-class-kind.ts @@ -8,35 +8,36 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; -import { isKind, Kind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { TopClassType } from './top-class-type.js'; export interface TopClassTypeDetails extends TypeDetails { inferenceRules?: InferCurrentTypeRule | Array> } -export interface TopClassKindOptions { +export interface TopClassKindOptions extends KindOptions { name: string; } export const TopClassKindName = 'TopClassKind'; export class TopClassKind implements Kind { - readonly $name: 'TopClassKind'; + readonly $name: string; readonly services: TypirServices; readonly options: TopClassKindOptions; protected instance: TopClassType | undefined; constructor(services: TypirServices, options?: Partial) { - this.$name = TopClassKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); } protected collectOptions(options?: Partial): TopClassKindOptions { return { // the default values: + $name: TopClassKindName, name: 'TopClass', // the actually overriden values: ...options @@ -72,5 +73,5 @@ export class TopClassKind implements Kind { } export function isTopClassKind(kind: unknown): kind is TopClassKind { - return isKind(kind) && kind.$name === TopClassKindName; + return kind instanceof TopClassKind; } diff --git a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts index 83f53047..0d5d31f7 100644 --- a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts +++ b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts @@ -8,7 +8,7 @@ import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, toArray } from '../../utils/utils.js'; -import { Kind, isKind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { FixedParameterType } from './fixed-parameters-type.js'; export class Parameter { @@ -25,7 +25,7 @@ export interface FixedParameterTypeDetails extends TypeDetails, List, Array, Map, ..., i.e. types with a fixed number of arbitrary parameter types */ export class FixedParameterKind implements Kind { - readonly $name: `FixedParameterKind-${string}`; + readonly $name: string; readonly services: TypirServices; readonly baseName: string; readonly options: Readonly; readonly parameters: Parameter[]; // assumption: the parameters are in the correct order! constructor(typir: TypirServices, baseName: string, options?: Partial, ...parameterNames: string[]) { - this.$name = `${FixedParameterKindName}-${baseName}`; + this.options = this.collectOptions(options); + this.$name = `${this.options.$name}-${baseName}`; this.services = typir; this.services.infrastructure.Kinds.register(this); this.baseName = baseName; - this.options = this.collectOptions(options); this.parameters = parameterNames.map((name, index) => { name, index }); // check input @@ -56,6 +56,7 @@ export class FixedParameterKind implements Kind { protected collectOptions(options?: Partial): FixedParameterKindOptions { return { // the default values: + $name: FixedParameterKindName, parameterSubtypeCheckingStrategy: 'EQUAL_TYPE', // the actually overriden values: ...options @@ -95,5 +96,5 @@ export class FixedParameterKind implements Kind { } export function isFixedParametersKind(kind: unknown): kind is FixedParameterKind { - return isKind(kind) && kind.$name.startsWith('FixedParameterKind-'); + return kind instanceof FixedParameterKind; } diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index d3eabee2..a95a7c44 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -12,14 +12,14 @@ import { ValidationRule } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { InferCurrentTypeRule, NameTypePair, RegistrationOptions } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; -import { isKind, Kind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { FunctionTypeInitializer } from './function-initializer.js'; import { AvailableFunctionsManager } from './function-overloading.js'; import { FunctionType } from './function-type.js'; import { UniqueFunctionValidation } from './function-validation-unique.js'; -export interface FunctionKindOptions { +export interface FunctionKindOptions extends KindOptions { // these three options controls structural vs nominal typing somehow ... enforceFunctionName: boolean, enforceInputParameterNames: boolean, @@ -147,22 +147,23 @@ export interface FunctionConfigurationChain { * - parameters which are used for output AND input */ export class FunctionKind implements Kind, FunctionFactoryService { - readonly $name: 'FunctionKind'; + readonly $name: string; readonly services: TypirServices; readonly options: Readonly>; readonly functions: AvailableFunctionsManager; constructor(services: TypirServices, options?: Partial>) { - this.$name = FunctionKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); this.functions = this.createFunctionManager(); } protected collectOptions(options?: Partial>): FunctionKindOptions { return { // the default values: + $name: FunctionKindName, enforceFunctionName: false, enforceInputParameterNames: false, enforceOutputParameterName: false, @@ -246,7 +247,7 @@ export class FunctionKind implements Kind, FunctionFactoryService< } export function isFunctionKind(kind: unknown): kind is FunctionKind { - return isKind(kind) && kind.$name === FunctionKindName; + return kind instanceof FunctionKind; } diff --git a/packages/typir/src/kinds/kind.ts b/packages/typir/src/kinds/kind.ts index e42bc6e0..fd77a9af 100644 --- a/packages/typir/src/kinds/kind.ts +++ b/packages/typir/src/kinds/kind.ts @@ -30,6 +30,10 @@ export interface Kind { } -export function isKind(kind: unknown): kind is Kind { - return typeof kind === 'object' && kind !== null && typeof (kind as Kind).$name === 'string'; +/** + * Options which are relevant for all kinds. + */ +export interface KindOptions { + /** Customize the name which is used to register the kind in the kind registry. */ + $name: string; } diff --git a/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts b/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts index 3a655c59..2c387ad7 100644 --- a/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts +++ b/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts @@ -7,7 +7,7 @@ import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { assertTrue } from '../../utils/utils.js'; -import { Kind, isKind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { MultiplicityType } from './multiplicity-type.js'; export interface MultiplicityTypeDetails extends TypeDetails { @@ -16,7 +16,7 @@ export interface MultiplicityTypeDetails extends TypeDetails implements Kind { - readonly $name: 'MultiplicityTypeKind'; + readonly $name: string; readonly services: TypirServices; readonly options: Readonly; constructor(services: TypirServices, options?: Partial) { - this.$name = MultiplicityKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); } protected collectOptions(options?: Partial): MultiplicityKindOptions { return { // the default values: + $name: MultiplicityKindName, symbolForUnlimited: '*', // the actually overriden values: ...options @@ -115,5 +116,5 @@ export class MultiplicityKind implements Kind { } export function isMultiplicityKind(kind: unknown): kind is MultiplicityKind { - return isKind(kind) && kind.$name === MultiplicityKindName; + return kind instanceof MultiplicityKind; } diff --git a/packages/typir/src/kinds/primitive/primitive-kind.ts b/packages/typir/src/kinds/primitive/primitive-kind.ts index 22c9479e..8f10c74a 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -8,10 +8,10 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; -import { isKind, Kind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { PrimitiveType } from './primitive-type.js'; -export interface PrimitiveKindOptions { +export interface PrimitiveKindOptions extends KindOptions { // empty for now } @@ -36,19 +36,22 @@ export interface PrimitiveConfigurationChain { } export class PrimitiveKind implements Kind, PrimitiveFactoryService { - readonly $name: 'PrimitiveKind'; + readonly $name: string; readonly services: TypirServices; readonly options: PrimitiveKindOptions; constructor(services: TypirServices, options?: Partial) { - this.$name = PrimitiveKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); } protected collectOptions(options?: Partial): PrimitiveKindOptions { return { + // the default values: + $name: PrimitiveKindName, + // the actually overriden values: ...options, }; } @@ -69,7 +72,7 @@ export class PrimitiveKind implements Kind, PrimitiveFactoryServic } export function isPrimitiveKind(kind: unknown): kind is PrimitiveKind { - return isKind(kind) && kind.$name === PrimitiveKindName; + return kind instanceof PrimitiveKind; } diff --git a/packages/typir/src/kinds/top/top-kind.ts b/packages/typir/src/kinds/top/top-kind.ts index 1034a1cc..f4380b91 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -8,7 +8,7 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; -import { isKind, Kind } from '../kind.js'; +import { Kind, KindOptions } from '../kind.js'; import { TopType } from './top-type.js'; export interface TopTypeDetails extends TypeDetails { @@ -18,7 +18,7 @@ interface CreateTopTypeDetails extends TopTypeDetails>; } -export interface TopKindOptions { +export interface TopKindOptions extends KindOptions { name: string; } @@ -35,20 +35,21 @@ export interface TopConfigurationChain { } export class TopKind implements Kind, TopFactoryService { - readonly $name: 'TopKind'; + readonly $name: string; readonly services: TypirServices; readonly options: Readonly; constructor(services: TypirServices, options?: Partial) { - this.$name = TopKindName; + this.options = this.collectOptions(options); + this.$name = this.options.$name; this.services = services; this.services.infrastructure.Kinds.register(this); - this.options = this.collectOptions(options); } protected collectOptions(options?: Partial): TopKindOptions { return { // the default values: + $name: TopKindName, name: 'any', // the actually overriden values: ...options @@ -72,7 +73,7 @@ export class TopKind implements Kind, TopFactoryService(kind: unknown): kind is TopKind { - return isKind(kind) && kind.$name === TopKindName; + return kind instanceof TopKind; } diff --git a/packages/typir/src/services/kind-registry.ts b/packages/typir/src/services/kind-registry.ts index f401d7fc..4e8b439d 100644 --- a/packages/typir/src/services/kind-registry.ts +++ b/packages/typir/src/services/kind-registry.ts @@ -9,8 +9,8 @@ import { TypirServices } from '../typir.js'; export interface KindRegistry { register(kind: Kind): void; - get(type: T['$name']): T | undefined; - getOrCreateKind(type: T['$name'], factory: (services: TypirServices) => T): T; + get($name: string): T | undefined; + getOrCreateKind($name: string, factory: (services: TypirServices) => T): T; } export class DefaultKindRegistry implements KindRegistry { @@ -34,14 +34,14 @@ export class DefaultKindRegistry implements KindRegistry(type: T['$name']): T | undefined { - return this.kinds.get(type) as (T | undefined); + get($name: string): T | undefined { + return this.kinds.get($name) as (T | undefined); } - getOrCreateKind(type: T['$name'], factory: (services: TypirServices) => T): T { - const existing = this.get(type); + getOrCreateKind($name: string, factory: (services: TypirServices) => T): T { + const existing = this.get($name); if (existing) { - return existing; + return existing as T; } return factory(this.services); } diff --git a/packages/typir/src/services/language.ts b/packages/typir/src/services/language.ts index 3d352419..76035299 100644 --- a/packages/typir/src/services/language.ts +++ b/packages/typir/src/services/language.ts @@ -7,7 +7,6 @@ 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. @@ -72,7 +71,7 @@ export class DefaultLanguageService implements LanguageService `${key}: ${this.printObject(value)}`) .join(', '); - return `${this.constructor.name}(${properties})`; + return `${this}(${properties})`; } protected printObject(obj: unknown): string { @@ -156,10 +156,68 @@ export class TestProblemPrinter extends DefaultTypeConflictPrinter { + protected subKeys: Map = new Map(); // key => all its direct sub-keys + protected superKeys: Map = new Map(); // key => all its direct super-keys + + constructor(subSuper: Array<{ superKey: string, subKey: string}> = []) { + super(); + this.registerSubSuperRelationship('TestLanguageNode', 'Variable'); + this.registerSubSuperRelationship('TestLanguageNode', 'TestExpressionNode'); + this.registerSubSuperRelationship('TestLanguageNode', 'TestStatementNode'); + this.registerSubSuperRelationship('TestExpressionNode', 'IntegerLiteral'); + this.registerSubSuperRelationship('TestExpressionNode', 'DoubleLiteral'); + this.registerSubSuperRelationship('TestExpressionNode', 'BooleanLiteral'); + this.registerSubSuperRelationship('TestExpressionNode', 'StringLiteral'); + this.registerSubSuperRelationship('TestExpressionNode', 'ClassConstructorCall'); + this.registerSubSuperRelationship('TestExpressionNode', 'ClassFieldAccess'); + this.registerSubSuperRelationship('TestExpressionNode', 'BinaryExpression'); + this.registerSubSuperRelationship('TestStatementNode', 'AssignmentStatement'); + this.registerSubSuperRelationship('TestStatementNode', 'StatementBlock'); + subSuper.forEach(entry => this.registerSubSuperRelationship(entry.superKey, entry.subKey)); + } + override getLanguageNodeKey(languageNode: TestLanguageNode): string | undefined { return languageNode.constructor.name; } + protected registerSubSuperRelationship(superKey: string, subKey: string): void { + this.addKeyValue(subKey, superKey, this.superKeys); + this.addKeyValue(superKey, subKey, this.subKeys); + } + protected addKeyValue(key: string, value: string, map: Map): void { + let entries = map.get(key); + if (entries === undefined) { + entries = []; + map.set(key, entries); + } + entries.push(value); + } + + override getAllSubKeys(languageKey: string): string[] { + return this.getTransitiveKeys(languageKey, this.subKeys); + } + + override getAllSuperKeys(languageKey: string): string[] { + return this.getTransitiveKeys(languageKey, this.superKeys); + } + + protected getTransitiveKeys(languageKey: string, map: Map): string[] { + const result: Set = new Set(); + const toCheck: string[] = [languageKey]; + while (toCheck.length >= 1) { + const current = toCheck.splice(0, 1)[0]; + for (const next of map.get(current) ?? []) { + if (result.has(next)) { + // already collected => nothing to do + } else { + result.add(next); + toCheck.push(next); + } + } + } + return Array.from(result); + } + override isLanguageNode(node: TestLanguageNode): node is TestLanguageNode { return node instanceof TestLanguageNode; } diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index bc5e8aa7..ad0b8c2f 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -108,25 +108,64 @@ export function createDefaultTypirServicesModule(): Module( - customization1: Module, PartialTypirServices> = {}, - customization2: Module, PartialTypirServices> = {}, - customization3: Module, PartialTypirServices> = {}, + customization1?: Module, PartialTypirServices>, + customization2?: Module, PartialTypirServices>, + customization3?: Module, PartialTypirServices>, + customization4?: Module, PartialTypirServices>, ): TypirServices { return inject( + // use the default implementations for all core Typir services createDefaultTypirServicesModule(), - customization1, - customization2, - customization3, + // optionally add some more language-specific customization, e.g. for ... + customization1, // ... production + customization2, // ... testing (in order to replace some customizations of production) + customization3, // ... testing (e.g. to have customizations for all test cases and for single test cases) + customization4, // ... for even more flexibility ); } +/** + * Creates the TypirServices with the default module containing the default implementations for Typir, + * which might be exchanged by the given optional customized modules. + * Additionally, some new services are defined, and implementations for them are registered. + * @param moduleForAdditionalServices contains the configurations for all added services + * @param customization1 optional Typir module with customizations (for new and existing services) + * @param customization2 optional Typir module with customizations (for new and existing services) + * @param customization3 optional Typir module with customizations (for new and existing services) + * @param customization4 optional Typir module with customizations (for new and existing services) + * @returns a Typir instance, i.e. the TypirServices consisting of the default services and the added services, + * with implementations for all services + */ +export function createTypirServicesWithAdditionalServices( + moduleForAdditionalServices: Module & AdditionalServices, AdditionalServices>, + customization1?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, + customization2?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, + customization3?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, + customization4?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, +): TypirServices & AdditionalServices { + return inject( + // use the default implementations for all core Typir services + createDefaultTypirServicesModule(), + // add implementations for all additional services + moduleForAdditionalServices, + // optionally add some more language-specific customization, e.g. for ... + customization1, // ... production + customization2, // ... testing (in order to replace some customizations of production) + customization3, // ... testing (e.g. to have customizations for all test cases and for single test cases) + customization4, // ... for even more flexibility + ); +} + + /** * A deep partial type definition for services. We look into T to see whether its type definition contains * any methods. If it does, it's one of our services and therefore should not be partialized. diff --git a/packages/typir/src/utils/dependency-injection.ts b/packages/typir/src/utils/dependency-injection.ts index 059318d3..280084e2 100644 --- a/packages/typir/src/utils/dependency-injection.ts +++ b/packages/typir/src/utils/dependency-injection.ts @@ -4,6 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +// Copied from Langium + /* eslint-disable @typescript-eslint/no-explicit-any */ /** diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index 0f588765..b3901156 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -6,10 +6,10 @@ import { expect } from 'vitest'; import { Type } from '../graph/type-node.js'; -import { TestLanguageNode, TestLanguageService, TestProblemPrinter } from '../test/predefined-language-nodes.js'; -import { createDefaultTypirServicesModule, createTypirServices, PartialTypirServices, TypirServices } from '../typir.js'; -import { Module } from './dependency-injection.js'; import { Severity } from '../services/validation.js'; +import { TestLanguageNode, TestLanguageService, TestProblemPrinter } from '../test/predefined-language-nodes.js'; +import { createDefaultTypirServicesModule, createTypirServices, DeepPartial, PartialTypirServices, TypirServices } from '../typir.js'; +import { inject, Module } from './dependency-injection.js'; /** * Testing utility to check, that exactly the expected types are in the type system. @@ -34,9 +34,9 @@ export function expectTypirTypes(services: TypirServices(type: unknown, checkType: (t: unknown) => t is T, checkDetails: (t: T) => boolean): asserts type is T { +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)) { + if (checkDetails === undefined || checkDetails(type)) { // everything is fine } else { expect.fail(`'${type.getIdentifier()}' is the actual Typir type, but the details are wrong`); @@ -248,7 +248,6 @@ export function createTypirServicesForTesting( customizationForTesting: Module, PartialTypirServices> = {}, ): TypirServices { return createTypirServices( - 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) @@ -256,3 +255,25 @@ export function createTypirServicesForTesting( customizationForTesting, // specific customizations for the current test case ); } + +/** + * Creates TypirServices dedicated for testing purposes, + * with the default module containing the default implements for Typir, which might be exchanged by the given optional customized module. + * @param moduleForAdditionalServices required implementations for the additional services + * @param customizationForTesting specific customizations for the current test case + * @returns a Typir instance, i.e. the TypirServices with implementations + */ +export function createTypirServicesForTestingWithAdditionalServices( + moduleForAdditionalServices: Module & AdditionalServices, AdditionalServices>, + customizationForTesting?: Module & AdditionalServices, DeepPartial & AdditionalServices>>, +): TypirServices & AdditionalServices { + return inject( + createDefaultTypirServicesModule(), // all default core implementations + moduleForAdditionalServices, + { // 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) + }, + customizationForTesting, // specific customizations for the current test case + ); +} diff --git a/packages/typir/test/customization-example.test.ts b/packages/typir/test/customization-example.test.ts new file mode 100644 index 00000000..fc51da44 --- /dev/null +++ b/packages/typir/test/customization-example.test.ts @@ -0,0 +1,201 @@ +/****************************************************************************** + * Copyright 2025 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { describe, expect, test } from 'vitest'; +import { ClassFactoryService, ClassKind } from '../src/kinds/class/class-kind.js'; +import { TestLanguageNode } from '../src/test/predefined-language-nodes.js'; +import { createTypirServices, createTypirServicesWithAdditionalServices, TypirServices } from '../src/typir.js'; +import { expectToBeType } from '../src/index-test.js'; +import { DefaultTypeConflictPrinter, isClassType, Type } from '../src/index.js'; + +describe('Some examples how to customize the Typir services, focusing on adding another type factory', () => { + + test('Demonstrate the default behaviour of classes', async () => { + // Use the default configuration of Typir + const typir = createTypirServices(); + // Create some classes + const classA = typir.factory.Classes.create({ className: 'A', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + const classB = typir.factory.Classes.create({ className: 'B', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + expectToBeType(classA, isClassType, type => type.className === 'A'); + expectToBeType(classB, isClassType, type => type.className === 'B'); + // Not more than 1 super-class is allowed: + expect(() => typir.factory.Classes.create({ className: 'C', fields: [], methods: [], superClasses: [classA, classB] }).finish()) + .toThrowError('Only 1 super-class is allowed.'); + }); + + test('Update an existing type factory', async () => { + // The service for creating classes already exists in the Typir services, but its implementation is configured: + // - Here, only an option of the existing implementation is changed. + // - But in general you could add a completely new implementation here. + const typir = createTypirServices({ + factory: { + Classes: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2 }), + }, + }); + // Create some classes + const classA = typir.factory.Classes.create({ className: 'A', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + const classB = typir.factory.Classes.create({ className: 'B', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + expectToBeType(classA, isClassType, type => type.className === 'A'); + expectToBeType(classB, isClassType, type => type.className === 'B'); + // 2 super-classes are fine now: + const classC = typir.factory.Classes.create({ className: 'C', fields: [], methods: [], superClasses: [classA, classB] }).finish().getTypeFinal()!; + expect(classC).toBeTruthy(); + }); + + test('Add another type factory', async () => { + // Make the additional service explicit: + // In general, you can add an arbitrary number of services, which might be deeply nested + type AdditionalExampleTypirServices = { + readonly factory: { + readonly OtherClasses: ClassFactoryService; + }, + }; + type ExampleTypirServices = TypirServices & AdditionalExampleTypirServices; + + // Instantiate the services and provide implementations for all added services. + const typir: ExampleTypirServices = createTypirServicesWithAdditionalServices({ + factory: { + // Here we reuse the existing class kind implementation, but with a different configuration to demonstrate types with a different behaviour: + OtherClasses: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2, $name: 'OtherClass' }), + }, + }); + + // Default classes: not more than 1 super-class + const classA = typir.factory.Classes.create({ className: 'A', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + const classB = typir.factory.Classes.create({ className: 'B', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + expectToBeType(classA, isClassType, type => type.className === 'A'); + expectToBeType(classB, isClassType, type => type.className === 'B'); + expect(() => typir.factory.Classes.create({ className: 'C', fields: [], methods: [], superClasses: [classA, classB] }).finish()) + .toThrowError('Only 1 super-class is allowed.'); + + // New classes: 2 super-classes are fine now + const classD = typir.factory.OtherClasses.create({ className: 'D', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + const classE = typir.factory.OtherClasses.create({ className: 'E', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; + const classF = typir.factory.OtherClasses.create({ className: 'F', fields: [], methods: [], superClasses: [classD, classE] }).finish().getTypeFinal()!; + expect(classF).toBeTruthy(); + expectToBeType(classD, isClassType, type => type.className === 'D'); + expectToBeType(classE, isClassType, type => type.className === 'E'); + expectToBeType(classF, isClassType, type => type.className === 'F'); + }); + + test('Newly added services are usable by all other services', async () => { + // new service + interface TestService { + doSomething(): string; + } + type AdditionalExampleTypirServices = { + TestService: TestService; + }; + // Defining the following TypeScript type "ExampleTypirServices" is not mandatory, but makes the customization with additional services easier. + // Without this type "ExampleTypirServices", you would need to replace all its occurrances by "TypirServices & AdditionalExampleTypirServices". + type ExampleTypirServices = TypirServices & AdditionalExampleTypirServices; + + // implementation for the new service + class TestServiceImpl implements TestService { + readonly services: ExampleTypirServices; + constructor(services: ExampleTypirServices) { + this.services = services; + } + doSomething(): string { + // all services are usable here! + this.services.Assignability; // existing service + this.services.TestService; // new service + return 'something'; + } + } + + // adapted implementation for an existing service + class ExamplePrinter extends DefaultTypeConflictPrinter { + readonly services: ExampleTypirServices; + constructor(services: ExampleTypirServices) { + super(); + this.services = services; + } + override printTypeName(type: Type): string { + // new services are usable in (adapted) implementations for existing services + return `${this.services.TestService.doSomething()}--${super.printTypeName(type)}`; + } + } + + // Instantiate the Typir services and provide implementations for all added and customized services: + const typir: ExampleTypirServices = createTypirServicesWithAdditionalServices( + // 1st argument: Specify implementations for all new services + { + TestService: services => new TestServiceImpl(services), + }, + // 2nd argument: Customize some existing services here + // In general, the following optional arguments might customize all services (default and added ones) + { + Printer: services => new ExamplePrinter(services), + }, + // some more optional customizations might be added here for convenience + ); + + // Create a type and check the new prefix + const type = typir.factory.Primitives.create({ primitiveName: 'ABC' }).finish(); + expect(typir.Printer.printTypeName(type)).toBe('something--ABC'); + }); + + test('Ensure unique names/identifiers when using different instances of the same kind class in parallel', async () => { + // This test case demonstrates some issues and how to solve them for the Classes case. + // Depending on the kind, not all of theses issues occur or occur in a different way. + // This test case aims to point to these issues in general. + type AdditionalExampleTypirServices = { + readonly factory: { + readonly OtherClasses: ClassFactoryService; + }, + }; + type ExampleTypirServices = TypirServices & AdditionalExampleTypirServices; + + // Reusing the following default implementation causes some issues with unique names ... + let typir: ExampleTypirServices = createTypirServicesWithAdditionalServices({ + factory: { + OtherClasses: services => new ClassKind(services), + }, + }); + + // Each kind needs to have a unique $name + expect(typir.factory.Classes).toBeTypeOf('object'); // trigger to create the default class factory, since they are created lazily + expect(() => typir.factory.OtherClasses).toThrowError("duplicate kind named 'ClassKind'"); + typir = createTypirServicesWithAdditionalServices({ + factory: { + OtherClasses: services => new ClassKind(services, { + $name: 'OtherClass', // specify another $name for the new kind + }), + }, + }); + expect(typir.factory.Classes).toBeTypeOf('object'); + expect(typir.factory.OtherClasses).toBeTypeOf('object'); // now both kinds are available and have different $names + + // Types need to have unique identifiers: this is ensured by having unique prefixes + expectToBeType(typir.factory.Classes.create({ className: 'A', fields: [], methods: [] }).finish().getTypeFinal(), isClassType, type => type.className === 'A'); + expect(() => typir.factory.OtherClasses.create({ className: 'A', fields: [], methods: [] }).finish()) + .toThrowError("The identifier 'class-A' for the new type of kind 'OtherClass' (implemented in ClassKind) collides with the identifier 'class-A' of an existing type of kind 'ClassKind' (implemented in ClassKind)."); + typir = createTypirServicesWithAdditionalServices({ + factory: { + OtherClasses: services => new ClassKind(services, { + $name: 'OtherClass', + identifierPrefix: 'other-class', // unique prefix for types of this kind + }), + }, + }); + expectToBeType(typir.factory.Classes.create({ className: 'A', fields: [], methods: [] }).finish().getTypeFinal(), isClassType, type => type.className === 'A'); + expectToBeType(typir.factory.OtherClasses.create({ className: 'A', fields: [], methods: [] }).finish().getTypeFinal(), isClassType, type => type.className === 'A'); + }); + + test('Removing an existing type factory', async () => { + // Removing an existing type factory is not possible and does not make sense, since other default services might use this service. + // - Simple approach: Just don't use this service anymore. + // - More explicit approach: Throw an exception whenever this service is used, as demonstrated here: + const typir = createTypirServices({ + factory: { + Classes: () => { throw new Error('Do not use classes!'); }, + }, + }); + expect(() => typir.factory.Classes).toThrowError('Do not use classes!'); + }); + +}); diff --git a/packages/typir/test/kinds/custom/custom-example-matrix.test.ts b/packages/typir/test/kinds/custom/custom-example-matrix.test.ts index af32be3f..cb7e9f4e 100644 --- a/packages/typir/test/kinds/custom/custom-example-matrix.test.ts +++ b/packages/typir/test/kinds/custom/custom-example-matrix.test.ts @@ -13,7 +13,7 @@ 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 { createTypirServicesForTesting, createTypirServicesForTestingWithAdditionalServices, expectToBeType, expectTypirTypes, expectValidationIssuesNone, expectValidationIssuesStrict } from '../../../src/utils/test-utils.js'; import { assertTypirType } from '../../../src/utils/utils.js'; /** @@ -30,38 +30,44 @@ export type MatrixType = { // "interface" instead of "type" does not work! 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}`, + test('Matrix type with exposed factory', () => { + type AdditionalMatrixTypirServices = { + readonly factory: { + readonly Matrix: CustomKind; + }, + }; + const typir = createTypirServicesForTestingWithAdditionalServices({ + factory: { + // create a custom kind to create custom types with dedicated properties (as defined in ) and provide it as additional Typir service + Matrix: services => new CustomKind(services, { + 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-${services.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`, + }), + }, }); + const integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); - // 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 + // now use this custom factory to create some custom types + const matrix2x2 = typir.factory.Matrix // "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'); + assertTypirType(matrix2x2, type => isCustomType(type, typir.factory.Matrix), 'My2x2MatrixType'); + expectTypirTypes(typir, type => isCustomType(type, typir.factory.Matrix), '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 + const matrix3x3 = typir.factory.Matrix .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'); + assertTypirType(matrix3x3, type => isCustomType(type, typir.factory.Matrix), 'My3x3MatrixType'); + expectTypirTypes(typir, type => isCustomType(type, typir.factory.Matrix), 'My2x2MatrixType', 'My3x3MatrixType'); expect(matrix3x3.properties.width).toBe(3); expect(matrix3x3.properties.height).toBe(3); expectToBeType(matrix3x3.properties.baseType.getType(), isPrimitiveType, type => type === integerType); @@ -299,6 +305,8 @@ describe('Tests simple custom types for Matrix types', () => { * [ 1, 2, 3; * 4, 5, 6 ] * They are similar to array literals in usual programming languages. + * + * To keep the example more clear, this new literal is not registered in the TestLanguageService (see custom-example-restricted.test.ts for a corresponding example). */ class MatrixLiteral extends TestExpressionNode { constructor( diff --git a/packages/typir/test/kinds/custom/custom-example-restricted.test.ts b/packages/typir/test/kinds/custom/custom-example-restricted.test.ts index cf781e02..deae26a0 100644 --- a/packages/typir/test/kinds/custom/custom-example-restricted.test.ts +++ b/packages/typir/test/kinds/custom/custom-example-restricted.test.ts @@ -10,7 +10,7 @@ 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 { IntegerLiteral, TestExpressionNode, TestLanguageNode, TestLanguageService } from '../../../src/test/predefined-language-nodes.js'; import { TypirServices } from '../../../src/typir.js'; import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js'; @@ -30,7 +30,9 @@ describe('Tests inference and assignability for Integers with an upper bound', ( let customKind: CustomKind; beforeEach(() => { - typir = createTypirServicesForTesting(); + typir = createTypirServicesForTesting({ + Language: () => new TestLanguageService([{ superKey: 'TestExpressionNode', subKey: 'RestrictedIntegerLiteral' }]), // register the language key of the new RestrictedIntegerLiteral + }); integerType = typir.factory.Primitives.create({ primitiveName: 'Integer' }).finish(); diff --git a/packages/typir/test/kinds/custom/custom-selectors.test.ts b/packages/typir/test/kinds/custom/custom-selectors.test.ts index 56ec9699..01ae4d31 100644 --- a/packages/typir/test/kinds/custom/custom-selectors.test.ts +++ b/packages/typir/test/kinds/custom/custom-selectors.test.ts @@ -7,7 +7,7 @@ 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 { TestExpressionNode, TestLanguageNode, TestLanguageService } from '../../../src/test/predefined-language-nodes.js'; import { TypirServices } from '../../../src/typir.js'; import { createTypirServicesForTesting } from '../../../src/utils/test-utils.js'; @@ -23,7 +23,7 @@ describe('Test all possible TypeSelectors with custom types', () => { let customKind: CustomKind; beforeEach(() => { - typir = createTypirServicesForTesting(); + typir = createTypirServicesForTesting({ Language: () => new TestLanguageService([{ superKey: 'TestExpressionNode', subKey: 'CustomLiteral' }])}); customKind = new CustomKind(typir, { name: 'MyCustom',