Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
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
22 changes: 10 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,14 @@ 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-??-??)

[Linked issues and PRs for v0.3.0](https://github.com/TypeFox/typir/milestone/4)

### 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
Expand All @@ -31,7 +22,12 @@ 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<AdditionalServices>, ...)`, see `customization-example.test.ts` for examples and explanations
- Typir-Langium: `createTypirLangiumServicesWithAdditionalServices<..., AdditionalServices>(..., Module<AdditionalServices>, ...)` works in the same way
- Internal testing in Typir (core): `createTypirServicesForTestingWithAdditionalServices<AdditionalServices>(Module<AdditionalServices>, ...)`
- 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

Expand All @@ -41,7 +37,9 @@ For each minor and major version, there is a corresponding [milestone on GitHub]

- 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)
Expand Down
72 changes: 68 additions & 4 deletions documentation/customization.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,75 @@
# 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<TestLanguageNode>;
},
};
```

Provide implementations for all added services when you instantiate the Typir services.
Instead of `createTypirServices`, use `createTypirServicesWithAdditionalServices` instead:

```typescript
const customizedTypir: TypirServices<TestLanguageNode> & AdditionalExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({
factory: {
OtherClasses: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2, $name: 'OtherClass' }),
},
});
```

TypeScript don't force you to write `TypirServices<TestLanguageNode> & AdditionalExampleTypirServices` in the code snipped above, but makes explicit what is going on here.
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated
To simplify the code, it is possible (but not mandatory) to introduce a TypeScript type like the following and to use it instead:

```typescript
type ExampleTypirServices = TypirServices<TestLanguageNode> & 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<TestLanguageNode, AdditionalExampleTypirServices>({
// 1st argument: mandatory implementations for all new services
}, {
// 2nd argument: customize some existing services here
});
```
2 changes: 2 additions & 0 deletions documentation/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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)


Expand Down
25 changes: 24 additions & 1 deletion documentation/kinds/custom-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Then you create a new factory for these matrix types:
```typescript
const matrixFactory = new CustomKind<MatrixType, TestLanguageNode>(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 ...
});
```

Expand All @@ -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<MatrixType, TestLanguageNode>;
}
}

// specify the additional services as TypeScript generic when initializing the Typir services and provide the custom factory
const typir = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalMatrixTypirServices>({
factory: {
Matrix: services => new CustomKind<MatrixType, TestLanguageNode>(services, { ... })
}
});

// now the custom matrix factory is usable like the predefined factories
typir.factory.Matrix.create({ ... }).finish().getTypeFinal()!;
```


## Features

Expand Down Expand Up @@ -90,5 +112,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()!;`.
11 changes: 6 additions & 5 deletions packages/typir-langium/src/features/langium-caching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AstTypes extends LangiumAstTypes> implements LanguageNodeInferenceCaching {
protected readonly cache: DocumentCache<unknown, Type | CachePending>; // removes cached AstNodes, if their underlying LangiumDocuments are invalidated

constructor(langiumServices: LangiumSharedCoreServices) {
this.cache = new DocumentCache(langiumServices, DocumentState.IndexedReferences);
constructor(typirServices: TypirLangiumServices<AstTypes>) {
this.cache = new DocumentCache(typirServices.langium.LangiumServices, DocumentState.IndexedReferences);
Comment thread
insafuhrmann marked this conversation as resolved.
}

cacheSet(languageNode: AstNode, type: Type): void {
Expand Down
8 changes: 4 additions & 4 deletions packages/typir-langium/src/features/langium-type-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,14 +55,14 @@ export class DefaultLangiumTypeCreator<AstTypes extends LangiumAstTypes> impleme
protected readonly typeGraph: TypeGraph;
protected readonly typeSystemDefinition: LangiumTypeSystemDefinition<AstTypes>;

constructor(typirServices: TypirLangiumServices<AstTypes>, langiumServices: LangiumSharedCoreServices) {
constructor(typirServices: TypirLangiumServices<AstTypes>) {
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);

Expand All @@ -73,7 +73,7 @@ export class DefaultLangiumTypeCreator<AstTypes extends LangiumAstTypes> 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));
Expand Down
Loading