Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 11 additions & 13 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,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<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

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

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<TestLanguageNode> & AdditionalExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({
factory: {
OtherClasses: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2, $name: 'OtherClass' }),
},
});
```

TypeScript doesn't force you to write `TypirServices<TestLanguageNode> & 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<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 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 */);
```
14 changes: 12 additions & 2 deletions documentation/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)


Expand Down
32 changes: 28 additions & 4 deletions documentation/kinds/custom-types.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 All @@ -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

Expand Down Expand Up @@ -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()!;`.
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