Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d37784b
first working version for defining simple custom types
JohannesMeierSE Mar 19, 2025
edd4a32
fixed some other smaller issues
JohannesMeierSE Mar 25, 2025
fa9e3b5
fixed generics, small issues
JohannesMeierSE Apr 4, 2025
9252fe4
no strings for TypeSelectors
JohannesMeierSE Apr 4, 2025
44f5b37
calculate identifiers only from custom properties
JohannesMeierSE Apr 4, 2025
f98b7c7
tests for custom types depending on custom types, no strings for Type…
JohannesMeierSE Apr 4, 2025
bedb16c
test cases for type inference
JohannesMeierSE Apr 6, 2025
f79d0df
fixed bug
JohannesMeierSE Apr 11, 2025
53b3ad3
register as listener and get informed about all existing types, if de…
JohannesMeierSE Apr 11, 2025
8b60ada
conversion and sub-types for custom types
JohannesMeierSE Apr 11, 2025
bdbe76a
test case for validation rules attached to custom type-specific infer…
JohannesMeierSE Apr 11, 2025
796630e
specify names and user representations once for all custom properties…
JohannesMeierSE Apr 11, 2025
0c4659d
test multiple custom types in parallel
JohannesMeierSE Apr 14, 2025
1ced5ca
fixed eslint rule
JohannesMeierSE Apr 15, 2025
74a6f90
support nesting of Properties for custom types
JohannesMeierSE Apr 15, 2025
2571d82
new property to skip type-specific inference rules for types which al…
JohannesMeierSE Apr 16, 2025
7b94761
removed testing stuff, fixed imports
JohannesMeierSE Apr 16, 2025
4e9e31f
more test cases for TypeSelectors with custom types
JohannesMeierSE Apr 16, 2025
d9ea8e1
first improvements according to the review
JohannesMeierSE Jul 4, 2025
98ec8e9
more improvements
JohannesMeierSE Jul 5, 2025
e50d304
improved comments according to the review
JohannesMeierSE Jul 8, 2025
1f63167
properties of custom types are readonly now, default implementation f…
JohannesMeierSE Jul 9, 2025
7003a61
added missing implementation (which will be improved in future PRs)
JohannesMeierSE Jul 9, 2025
7a98ce1
unified initialization design for classes, functions and custom types
JohannesMeierSE Jul 9, 2025
67de361
check typeName for uniqueness
JohannesMeierSE Jul 9, 2025
e551485
wrote some documentation for custom types
JohannesMeierSE Jul 9, 2025
6567a1e
fixes according to the review
JohannesMeierSE Jul 11, 2025
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
6 changes: 3 additions & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@
"@typescript-eslint/ban-types": "error", // bans types like String in favor of string
"@typescript-eslint/no-inferrable-types": "off", // don't blame decls like "index: number = 0", esp. in api signatures!
"@typescript-eslint/indent": "error", // consistent indentation
//"@typescript-eslint/no-explicit-any": "error", // don't use :any type
//"@typescript-eslint/no-explicit-any": "error", // don't use :any type
"@typescript-eslint/no-misused-new": "error", // no constructors for interfaces or new for classes
"@typescript-eslint/no-namespace": "off", // disallow the use of custom TypeScript modules and namespaces
"@typescript-eslint/no-namespace": "off", // disallow the use of custom TypeScript modules and namespaces
"@typescript-eslint/no-non-null-assertion": "off", // allow ! operator
"@typescript-eslint/parameter-properties": "error", // no property definitions in class constructors
"@typescript-eslint/parameter-properties": "off", // no property definitions in class constructors
"@typescript-eslint/no-unused-vars": ["error", { // disallow Unused Variables
"argsIgnorePattern": "^_"
}],
Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": [
"src/index.js",
],
}
34 changes: 33 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

We roughly follow the ideas of [semantic versioning](https://semver.org/).
Note that the versions "0.x.0" probably will include breaking changes.
For each minor and major version, there is a corresponding [milestone on GitHub](https://github.com/TypeFox/typir/milestones).

## v0.3.0 (2025-??-??)

Expand All @@ -14,13 +15,44 @@ Note that the versions "0.x.0" probably will include breaking changes.
- Fixed the implementation for merging modules for dependency injection, it is exactly the same fix from [Langium](https://github.com/eclipse-langium/langium/pull/1939), since we reused its DI implementation (#79).


## v0.3.0 (2025-??-??)

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

### New features

- New API to support custom types, i.e. types which are not predefined by Typir, but are created by users of Typir and tailored to the current language (#73):
- Supports custom properties with arrays, sets, maps, primitives and types
- Create a new `CustomKind` and use it to create corresponding `CustomType`s, which support the desired custom properties in TypeScript-safe way
- Type-specific names, user representations, inference rules and validation rules
- Specific rules for conversion and sub-type which are applied to all custom types
- Builtin support for dependencies between probably delayed (custom) types and unique custom types
- See some examples in `packages/typir/test/kinds/custom/custom-matrix.test.ts` and `packages/typir/test/kinds/custom/custom-restricted.test.ts`
- If you try to create a function type, class type or custom type a second time, the existing implementation already ensured, that the already existing type is reused and no new type is created (#73):
- For the type-specific inference rules, there is now an additional property `skipThisRuleIfThisTypeAlreadyExists` (in `InferCurrentTypeRule`) to control, whether these given inference rules for the "second new type" should be added to the existing type or whether they should be skipped.
- The default value is `false`, meaning that these type-specific inference (and validation) rules are attached to the existing type. That conforms to the behaviour before introducing this new property.
Comment thread
JohannesMeierSE marked this conversation as resolved.
- ...

### Breaking changes

- ...

### Fixed bugs

- Clear edges from invalid types, which are never added into the type graph (#73)
- The properties of all types are `readonly` now (#73)
- ...


## v0.2.1 (2025-04-09)

- Export `test-utils.ts` which are using `vitest` via the new namespace `'typir/test'` in order to not pollute production code with vitest dependencies (#68)


## v0.2.0 (2025-03-31)

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

### New features

- Users of Typir are able to explicitly define sub-type relationships via the `SubTypeService.markAsSubType(subType, superType)` now (#58)
Expand Down Expand Up @@ -91,7 +123,7 @@ Note that the versions "0.x.0" probably will include breaking changes.
This is the first official release of Typir.
It serves as first version to experiment with Typir and to gather feedback to guide and improve the upcoming versions. We are looking forward to your feedback!

- [Linked issues and PRs](https://github.com/TypeFox/typir/milestone/2)
- [Linked issues and PRs for v0.1.0](https://github.com/TypeFox/typir/milestone/2)
- Core implementations of the following [type-checking services](./packages/typir/src/services/):
- Assignability
- Equality
Expand Down
13 changes: 11 additions & 2 deletions documentation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ This describes the structure and the main content of the documentation for Typir
- [Type inference](./services/inference.md)
- ...


## Predefined types

- ...
The current set of predefined types:

- Top and bottom types
- Primitive types
- Structurally typed classes
- [Custom types](./kinds/custom-types.md)
- Function types
- Operators (are internally mappped to function types)


## Bindings

Expand Down Expand Up @@ -44,6 +53,6 @@ This repository contains the following stand-alone applications. Read their link

- [LOX](./examples/lox/README.md) - static type checking for LOX, implemented with Typir-Langium
- [OX](./examples/ox/README.md) - a reduced version of LOX, implemented with Typir-Langium
- Expressions - TODO
- [Expressions](./examples/expression.README.md) - static type checking for a hand-written reduced expression language, implemented with Typir (core)

Some of the internal test cases developed in [packages/typir/test/](../packages/typir/test/) demonstrate some features of Typir in more detail.
89 changes: 88 additions & 1 deletion documentation/kinds/custom-types.md

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.

Thanks!

Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,92 @@
Many languages contain features which cannot be easily described with the predefined types.
This describes how language-specific custom types can be defined and used in Typir.

TODO

## API by example

This section demonstrates the API to define custom types along the example extracted from `custom-example-matrix.test.ts`. Here a new mathematical matrix type is defined with `width` and `height`, which is similar to a two-dimensional array. The content of cells are primitive types.

First of all, you need to specify the properties of matrix types in Typir by creating a TypeScript type (note that `interface` instead of `type` does not work):

```typescript
type MatrixType = {
baseType: PrimitiveType;
width: number;
height: number;
};
```

Then you create a new factory for these matrix types:

```typescript
const matrixFactory = new CustomKind<MatrixType, TestLanguageNode>(typir, {
name: 'Matrix',
calculateTypeName: properties => `My${properties.width}x${properties.height}Matrix`,
// ... here you can specify additional rules for conversion, sub-types, ... for all matrix types ...
});
```

Now you can use this factory to create new matrix types:

```typescript
const matrix2x3 = matrixFactory
.create({ properties: { baseType: integerType, width: 2, height: 3 } })
.finish().getTypeFinal()!;
```

See `custom-example-restricted.test.ts` for another application example.


## Features

This sections describes the features of custom types in more detail.

### Custom properties

Custom types have custom properties ("data") including primitive values, Typir types and nesting/grouping with sets, arrays, and maps, and recursion.
See `custom-nested-properties.test.ts` for some examples.
When the initialization of the custom type is done, all its properties are read-only.

### Support by the TypeScript compiler

The API for custom types uses TypeScript generics to enable TypeScript-safe descriptions for these custom properties.
In the example above, calling `matrix2x3.properties.width` is supported by auto-completion in the IDE and will return the number `2`.

### Uniqueness

Typir ensures uniqueness for custom types.
Two custom types are unique, if their identifiers are the same (this counts for any type, not only for custom types).
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated
The default implementation calculates the identifier by concatenating the values of all properties.

### Circular dependencies

Cyclic dependencies of the given types for type properties are handled by Typir.
See some examples in `custom-cycles.test.ts` and `custom-selectors.test.ts`.
Therefore `getTypeFinal()` needs to be called after finishing a new custom type, e.g. `const myCustomType = customKind.create({...}).finish().getTypeFinal();`.
If the custom type is already available, you will get your `CustomType<Properties, LanguageType>`, otherwise `undefined`.
If the type is not yet available, you can register a callback, which is called, when the type is available:

```typescript
customKind.create({...}).finish().addListener(finishedType => {
// here the new custom type is available and can be used as usual
finishedType.getIdentifier();
});
```

### Behaviour

Specify rules for conversion, sub-type, names, identifiers, inference and validation (usually for all custom types OR for single ones).
See `custom-example-restricted.test.ts` for some examples.

### Multiple different custom types

You can use different factories for different custom types in parallel within the same Typir instance.
See `custom-independent.test.ts` for an example.


## Limitations

- You cannot use simple string values for `TypeSelector`s (in order to specify custom properties of type `Type`), since they cannot be distinguished from string values for primitive custom properties.
Therefore, only the restricted `TypeSelectorForCustomTypes` is supported by custom types instead of the usual `TypeSelector`.
- Even if your custom type does not depend on other types or if you know, that the types your custom type depends on are already available,
you need to call `getTypeFinal()`, e.g. `const myCustomType = customKind.create({...}).finish().getTypeFinal()!;`.
6 changes: 5 additions & 1 deletion packages/typir-langium/src/features/langium-language.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 { AbstractAstReflection, AstNode } from 'langium';
import { AbstractAstReflection, AstNode, isAstNode } from 'langium';
import { DefaultLanguageService, LanguageService, removeFromArray } from 'typir';

/**
Expand Down Expand Up @@ -53,4 +53,8 @@ export class LangiumLanguageService extends DefaultLanguageService<AstNode> impl
return this.superKeys.get(languageKey) ?? [];
}

override isLanguageNode(node: unknown): node is AstNode {
return isAstNode(node);
}

}
1 change: 1 addition & 0 deletions packages/typir-langium/src/typir-langium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type TypirLangiumServices<AstTypes extends LangiumAstTypes> = TypirServic

export type PartialTypirLangiumServices<AstTypes extends LangiumAstTypes> = DeepPartial<TypirLangiumServices<AstTypes>>


/**
* Creates a module that replaces some implementations of the core Typir services in order to be used with Langium.
* @param langiumServices Typir-Langium needs to interact with the Langium lifecycle
Expand Down
7 changes: 5 additions & 2 deletions packages/typir/src/graph/type-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class TypeGraph {
if (this.nodes.get(mapKey) === type) {
// this type is already registered => that is OK
} else {
throw new Error(`Names of types must be unique: ${mapKey}`);
throw new Error(`There is already a type with the identifier '${mapKey}'.`);
}
} else {
this.nodes.set(mapKey, type);
Expand Down Expand Up @@ -121,8 +121,11 @@ export class TypeGraph {

// register listeners for changed types/edges in the type graph

addListener(listener: TypeGraphListener): void {
addListener(listener: TypeGraphListener, options?: { callOnAddedForAllExisting: boolean }): void {
this.listeners.push(listener);
if (options?.callOnAddedForAllExisting && listener.onAddedType) {
this.nodes.forEach((type, key) => listener.onAddedType!.call(listener, type, key));
}
}
removeListener(listener: TypeGraphListener): void {
removeFromArray(listener, this.listeners);
Expand Down
5 changes: 5 additions & 0 deletions packages/typir/src/graph/type-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ export abstract class Type {
this.waitForIdentifiable.deconstruct();
this.waitForCompleted.deconstruct();
this.waitForInvalid.deconstruct();
// edges are already removed, when the type is removed from the graph,
// but in some cases, the type was not (yet) added to the graph, but got already edges => these edges need to be removed now
// e.g. "duplicated" types which are created and disposed by TypeInitializers
this.edgesIncoming.clear();
this.edgesOutgoing.clear();
}

protected switchFromInvalidToIdentifiable(): void {
Expand Down
4 changes: 4 additions & 0 deletions packages/typir/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export * from './kinds/class/class-type.js';
export * from './kinds/class/class-validation.js';
export * from './kinds/class/top-class-kind.js';
export * from './kinds/class/top-class-type.js';
export * from './kinds/custom/custom-definitions.js';
export * from './kinds/custom/custom-initializer.js';
export * from './kinds/custom/custom-kind.js';
export * from './kinds/custom/custom-type.js';
export * from './kinds/fixed-parameters/fixed-parameters-kind.js';
export * from './kinds/fixed-parameters/fixed-parameters-type.js';
export * from './kinds/function/function-initializer.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/typir/src/initialization/type-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { TypeReference } from './type-reference.js';
// TODO find better names: TypeSpecification, TypeDesignation/Designator, ... ?
export type BasicTypeSelector<T extends Type, LanguageType> =
| T // the wanted type
| string // identifier of the type (to be searched in the type graph/map)
| string // identifier of the type (to be searched in the type graph)
| TypeInitializer<T, LanguageType> // delayed creation of types
| TypeReference<T, LanguageType> // reference to a (maybe delayed) type
| LanguageType // language node to infer the final type from
Expand Down
2 changes: 1 addition & 1 deletion packages/typir/src/kinds/bottom/bottom-kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class BottomKind<LanguageType> implements Kind, BottomFactoryService<Lang
}

create(typeDetails: BottomTypeDetails<LanguageType>): BottomConfigurationChain<LanguageType> {
assertTrue(this.get(typeDetails) === undefined);
assertTrue(this.get(typeDetails) === undefined, 'The bottom type already exists.');
return new BottomConfigurationChainImpl(this.services, this, typeDetails);
}

Expand Down
10 changes: 3 additions & 7 deletions packages/typir/src/kinds/bottom/bottom-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,20 @@ export class BottomType extends Type implements TypeGraphListener {

// ensure, that this Bottom type is a sub-type of all (other) types:
const graph = kind.services.infrastructure.Graph;
graph.getAllRegisteredTypes().forEach(t => this.markAsSubType(t)); // the already existing types
graph.addListener(this); // all upcomping types
graph.addListener(this, { callOnAddedForAllExisting: true });
}

override dispose(): void {
this.kind.services.infrastructure.Graph.removeListener(this);
}

protected markAsSubType(type: Type): void {
onAddedType(type: Type, _key: string): void {
// this method is called for the already existing types and for all upcomping types
if (type !== this) {
this.kind.services.Subtype.markAsSubType(this, type, { checkForCycles: false });
}
}

onAddedType(type: Type, _key: string): void {
this.markAsSubType(type);
}

override getName(): string {
return this.getIdentifier();
}
Expand Down
Loading