Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub]

### New features

- Introduced new function `createTypirServicesWithAdditionalServices<AdditionalTypirServices>(Module<AdditionalTypirServices>, ...)` to create Typir services with additional services, which are specific for the current application. See `customization-example.test.ts` for examples and explanations.
- The `$name`s of kinds are configurable now.

### Fixed bugs
Expand Down
13 changes: 8 additions & 5 deletions packages/typir-langium/src/typir-langium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,19 @@ export function createDefaultTypirLangiumServicesModule<AstTypes extends Langium
* @param typeSystemDefinition the actual definition of the type system
* @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
* @returns the Typir services configured for the current Langium-based language
*/
export function createTypirLangiumServices<AstTypes extends LangiumAstTypes>(
langiumServices: LangiumSharedCoreServices, reflection: AbstractAstReflection, typeSystemDefinition: LangiumTypeSystemDefinition<AstTypes>,
customization1: Module<PartialTypirLangiumServices<AstTypes>> = {},
customization2: Module<PartialTypirLangiumServices<AstTypes>> = {},
customization3: Module<PartialTypirLangiumServices<AstTypes>> = {},
langiumServices: LangiumSharedCoreServices,
reflection: AbstractAstReflection,
typeSystemDefinition: LangiumTypeSystemDefinition<AstTypes>,
customization1?: Module<PartialTypirLangiumServices<AstTypes>>,
customization2?: Module<PartialTypirLangiumServices<AstTypes>>,
customization3?: Module<PartialTypirLangiumServices<AstTypes>>,
): TypirLangiumServices<AstTypes> {
return inject(
// use all core Typir services ...
// use the default implementations for all core Typir services ...
createDefaultTypirServicesModule<AstNode>(),
// ... with adapted implementations for Typir-Langium
createLangiumSpecificTypirServicesModule(langiumServices),
Expand Down
50 changes: 42 additions & 8 deletions packages/typir/src/typir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,56 @@ export function createDefaultTypirServicesModule<LanguageType>(): Module<TypirSe
}

/**
* Creates the TypirServices with the default module containing the default implements for Typir, which might be exchanged by the given optional customized modules.
* Creates the TypirServices with the default module containing the default implements for Typir,
* which might be exchanged by the given optional customized modules.
* @param customization1 optional Typir module with customizations
* @param customization2 optional Typir module with customizations
* @param customization3 optional Typir module with customizations
* @returns a Typir instance, i.e. the TypirServices with implementations
* @returns a Typir instance, i.e. the TypirServices with implementations for all services
*/
export function createTypirServices<LanguageType>(
customization1: Module<TypirServices<LanguageType>, PartialTypirServices<LanguageType>> = {},
customization2: Module<TypirServices<LanguageType>, PartialTypirServices<LanguageType>> = {},
customization3: Module<TypirServices<LanguageType>, PartialTypirServices<LanguageType>> = {},
customization1?: Module<TypirServices<LanguageType>, PartialTypirServices<LanguageType>>,
customization2?: Module<TypirServices<LanguageType>, PartialTypirServices<LanguageType>>,
customization3?: Module<TypirServices<LanguageType>, PartialTypirServices<LanguageType>>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was three before, but right now I am a bit bewildered. Why three? Is it just an exemplary choice?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the end, three is an arbitrary number here. I am fine to add even more optional customizations. For me, it sounds like a good idea to some more in order to make it even more flexible. inject in dependency-injection.ts from Langium has even 8 optional customizations ...

): TypirServices<LanguageType> {
return inject(
// use the default implementations for all core Typir services
createDefaultTypirServicesModule<LanguageType>(),
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)
);
}

// TODO Review: Is it possible to merge/unify these two functions in a nice way?
Comment thread
insafuhrmann marked this conversation as resolved.
Outdated

/**
* Creates the TypirServices with the default module containing the default implements for Typir,
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated
* 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 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)
* @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<LanguageType, AdditionalServices>(
moduleForAdditionalServices: Module<TypirServices<LanguageType> & AdditionalServices, AdditionalServices>,
customization1?: Module<TypirServices<LanguageType> & AdditionalServices, DeepPartial<TypirServices<LanguageType> & AdditionalServices>>,
customization2?: Module<TypirServices<LanguageType> & AdditionalServices, DeepPartial<TypirServices<LanguageType> & AdditionalServices>>,
customization3?: Module<TypirServices<LanguageType> & AdditionalServices, DeepPartial<TypirServices<LanguageType> & AdditionalServices>>,
): TypirServices<LanguageType> & AdditionalServices {
return inject(
// use the default implementations for all core Typir services
createDefaultTypirServicesModule<LanguageType>(),
// 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)
);
}

Expand Down
200 changes: 200 additions & 0 deletions packages/typir/test/customization-example.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/******************************************************************************
* 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<TestLanguageNode>();
// 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 are 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<TestLanguageNode>({
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<TestLanguageNode>;
},
};
type ExampleTypirServices = TypirServices<TestLanguageNode> & AdditionalExampleTypirServices;

// Instantiate the services and provide implementations for all added services.
const typir: ExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({
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');
Comment thread
JohannesMeierSE marked this conversation as resolved.
});

test('Newly added services are usable by all other services', async () => {
// new service
interface TestService {
doSomething(): string;
}
type AdditionalExampleTypirServices = {
TestService: TestService;
};
// Defining this TypeScript type is not mandatory, but makes the customization with additional services easier (search for the use of this type!)
Comment thread
insafuhrmann marked this conversation as resolved.
Outdated
type ExampleTypirServices = TypirServices<TestLanguageNode> & 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<TestLanguageNode> {
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<TestLanguageNode, AdditionalExampleTypirServices>(
// 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<TestLanguageNode>;
},
};
type ExampleTypirServices = TypirServices<TestLanguageNode> & AdditionalExampleTypirServices;

// Reusing the following default implementation causes some issues with unique names ...
let typir: ExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({
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<TestLanguageNode, AdditionalExampleTypirServices>({
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("A new type with identifier 'class-A' and kind 'OtherClass' (implemented in ClassKind) shall be created, but there is already a type with identifier 'class-A' and kind 'ClassKind' (implemented in ClassKind) in the type graph.");
Comment thread
insafuhrmann marked this conversation as resolved.
Outdated
typir = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({
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<TestLanguageNode>({
factory: {
Classes: () => { throw new Error('Do not use classes!'); },
},
});
expect(() => typir.factory.Classes).toThrowError('Do not use classes!');
});

});