Skip to content

Commit 8a04e85

Browse files
Reworked the sub-type relationship infrastructure (#58)
- Sub-type relationships are now explicitly stored in the type graph - They are not calculated by the asymmetric `analyzeIsSubTypeOf` and `analyzeIsSuperTypeOf` anymore - Types store their sub-type-reationships themselves in the type graph - no caching of (transitive) sub-type relationships anymore - new feature: now sub-type relationships can be explicitly defined by the user of Typir - This is the precondition to improve assignability checks: - arbitrary paths of implicit conversion and sub-type relationships can be exploited now for advanced assignability checks, by path search in the type graph - implementation for finding and controlling best matches of overloaded functions/operators - moved the existing graph algorithms into its own dedicated service in order to reuse and to customize them (+ created variants of the existing algorithms) - I started to tackle the terminology issue with "problems" as result of e.g. sub-type checking by introducing `SubTypeProblem` and `SubTypeSuccess` interfaces, combined as `type SubTypeResult = SubTypeSuccess | SubTypeProblem` - provided some predefined language elements for reuse in test cases inside `typir` in `packages/typir/src/test/predefined-language-nodes.ts` - more test utilities - more test cases
1 parent c49355a commit 8a04e85

37 files changed

+1134
-590
lines changed

CHANGELOG.md

+19-3
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,35 @@ We roughly follow the ideas of [semantic versioning](https://semver.org/).
44
Note that the versions "0.x.0" probably will include breaking changes.
55

66

7-
## v0.1.2 (December 2024)
7+
## v0.2.0 (upcoming)
8+
9+
### New features
10+
11+
- Users of Typir are able to explicitly define sub-type relationships via the `SubTypeService` (#58)
12+
- Arbitrary paths of implicit conversion and sub-type relationships are considered for assignability now (#58)
13+
- Control the behaviour in case of multiple matching overloads of functions (and operators) (#58)
14+
- Moved the existing graph algorithms into its own dedicated service in order to reuse and to customize them (#58)
15+
16+
### Breaking changes
17+
18+
- `TypeConversion.markAsConvertible` accepts only one type for source and target now in order to simplify the API (#58)
19+
- Methods in listeners (`TypeGraphListener`, `TypeStateListener`) are prefixed with `on` (#58)
20+
21+
22+
## v0.1.2 (2024-12-20)
823

924
- Replaced absolute paths in READMEs by relative paths, which is a requirement for correct links on NPM
25+
- Edit: Note that the tag for this release was accidentally added on the branch `jm/v0.1.2`, not on the `main` branch.
1026

1127

12-
## v0.1.1 (December 2024)
28+
## v0.1.1 (2024-12-20)
1329

1430
- Improved the READMEs in the packages `typir` and `typir-langium`.
1531
- Improved the CONTRIBUTING.md.
1632
- Improved source code for Tiny Typir in `api-example.test.ts`.
1733

1834

19-
## v0.1.0 (December 2024)
35+
## v0.1.0 (2024-12-20)
2036

2137
This is the first official release of Typir.
2238
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!

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@ Typir provides these core features:
4040

4141
Typir does intentionally _not_ include ...
4242

43-
- Rule engines and constraint solving
43+
- Rule engines and constraint solving,
44+
since type inference is calculated in a recursive manner and does not use unification/substitution
4445
- Formal proofs
4546
- External DSLs for formalizing types
47+
- Support for dynamic type systems, which do typing during the execution of the DSL.
48+
Typir aims at static type systems, which do typing during the writing of the DSL.
4649

4750

4851
## NPM workspace
@@ -109,7 +112,7 @@ typir.factory.Operators.createBinary({ name: '-', signatures: [{ left: numberTyp
109112
As we'd like to be able to convert numbers to strings implicitly, we add the following line. Note that this will for example make it possible to concatenate numbers and strings with the `+` operator, though it has no signature for a number and a string parameter in the operator definition above.
110113

111114
```typescript
112-
typir.Conversion.markAsConvertible(numberType, stringType,'IMPLICIT_EXPLICIT');
115+
typir.Conversion.markAsConvertible(numberType, stringType, 'IMPLICIT_EXPLICIT');
113116
```
114117

115118
Furthermore we can specify how Typir should infer the variable type. We decided that the type of the variable should be the type of its initial value. Typir internally considers the inference rules for primitives and operators as well, when recursively inferring the given AstElement.

examples/lox/src/language/lox-module.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { LoxLinker } from './lox-linker.js';
1818
*/
1919
export type LoxAddedServices = {
2020
validation: {
21-
LoxValidator: LoxValidator
21+
LoxValidator: LoxValidator,
2222
},
2323
typir: LangiumServicesForTypirBinding,
2424
}
@@ -38,7 +38,7 @@ export function createLoxModule(shared: LangiumSharedCoreServices): Module<LoxSe
3838
return {
3939
validation: {
4040
ValidationRegistry: (services) => new LoxValidationRegistry(services),
41-
LoxValidator: () => new LoxValidator()
41+
LoxValidator: () => new LoxValidator(),
4242
},
4343
// For type checking with Typir, inject and merge these modules:
4444
typir: () => inject(Module.merge(
@@ -73,7 +73,7 @@ export function createLoxServices(context: DefaultSharedModuleContext): {
7373
} {
7474
const shared = inject(
7575
createDefaultSharedModule(context),
76-
LoxGeneratedSharedModule
76+
LoxGeneratedSharedModule,
7777
);
7878
const Lox = inject(
7979
createDefaultCoreModule({ shared }),

packages/typir-langium/src/features/langium-type-creator.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator,
108108
this.documentTypesMap.delete(documentKey);
109109
}
110110

111-
addedType(newType: Type): void {
111+
onAddedType(newType: Type): void {
112112
// the TypeGraph notifies about newly created Types
113113
if (this.currentDocumentKey) {
114114
// associate the new type with the current Langium document!
@@ -123,13 +123,13 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator,
123123
}
124124
}
125125

126-
removedType(_type: Type): void {
126+
onRemovedType(_type: Type): void {
127127
// since this type creator actively removes types from the type graph itself, there is no need to react on removed types
128128
}
129-
addedEdge(_edge: TypeEdge): void {
129+
onAddedEdge(_edge: TypeEdge): void {
130130
// this type creator does not care about edges => do nothing
131131
}
132-
removedEdge(_edge: TypeEdge): void {
132+
onRemovedEdge(_edge: TypeEdge): void {
133133
// this type creator does not care about edges => do nothing
134134
}
135135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/******************************************************************************
2+
* Copyright 2025 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import { TypirServices } from '../typir.js';
8+
import { TypeEdge } from './type-edge.js';
9+
import { TypeGraph } from './type-graph.js';
10+
import { Type } from './type-node.js';
11+
12+
/**
13+
* Graph algorithms to do calculations on the type graph.
14+
* All algorithms are robust regarding cycles.
15+
*/
16+
export interface GraphAlgorithms {
17+
collectReachableTypes(from: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): Set<Type>;
18+
existsEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): boolean;
19+
getEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): TypeEdge[];
20+
}
21+
22+
export class DefaultGraphAlgorithms implements GraphAlgorithms {
23+
protected readonly graph: TypeGraph;
24+
25+
constructor(services: TypirServices) {
26+
this.graph = services.infrastructure.Graph;
27+
}
28+
29+
collectReachableTypes(from: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): Set<Type> {
30+
const result: Set<Type> = new Set();
31+
const remainingToCheck: Type[] = [from];
32+
33+
while (remainingToCheck.length > 0) {
34+
const current = remainingToCheck.pop()!;
35+
const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r));
36+
for (const edge of outgoingEdges) {
37+
if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) {
38+
if (result.has(edge.to)) {
39+
// already checked
40+
} else {
41+
result.add(edge.to); // this type is reachable
42+
remainingToCheck.push(edge.to); // check it for recursive conversions
43+
}
44+
}
45+
}
46+
}
47+
48+
return result;
49+
}
50+
51+
existsEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): boolean {
52+
const visited: Set<Type> = new Set();
53+
const stack: Type[] = [from];
54+
55+
while (stack.length > 0) {
56+
const current = stack.pop()!;
57+
visited.add(current);
58+
59+
const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r));
60+
for (const edge of outgoingEdges) {
61+
if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) {
62+
if (edge.to === to) {
63+
/* It was possible to reach our goal type using this path.
64+
* Base case that also catches the case in which start and end are the same
65+
* (is there a cycle?). Therefore it is allowed to have been "visited".
66+
* True will only be returned if there is a real path (cycle) made up of edges
67+
*/
68+
return true;
69+
}
70+
if (!visited.has(edge.to)) {
71+
/* The target node of this edge has not been visited before and is also not our goal node
72+
* Add it to the stack and investigate this path later.
73+
*/
74+
stack.push(edge.to);
75+
}
76+
}
77+
}
78+
}
79+
80+
// Fall through means that we could not reach the goal type
81+
return false;
82+
}
83+
84+
getEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): TypeEdge[] {
85+
const visited: Map<Type, TypeEdge|undefined> = new Map(); // the edge from the parent to the current node
86+
visited.set(from, undefined);
87+
const stack: Type[] = [from];
88+
89+
while (stack.length > 0) {
90+
const current = stack.pop()!;
91+
92+
const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r));
93+
for (const edge of outgoingEdges) {
94+
if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) {
95+
if (edge.to === to) {
96+
/* It was possible to reach our goal type using this path.
97+
* Base case that also catches the case in which start and end are the same
98+
* (is there a cycle?). Therefore it is allowed to have been "visited".
99+
* True will only be returned if there is a real path (cycle) made up of edges
100+
*/
101+
const result: TypeEdge[] = [edge];
102+
// collect the path of used edges, from "to" back to "from"
103+
let backNode = edge.from;
104+
while (backNode !== from) {
105+
const backEdge = visited.get(backNode)!;
106+
result.unshift(backEdge);
107+
backNode = backEdge.from;
108+
}
109+
return result;
110+
}
111+
if (!visited.has(edge.to)) {
112+
/* The target node of this edge has not been visited before and is also not our goal node
113+
* Add it to the stack and investigate this path later.
114+
*/
115+
stack.push(edge.to);
116+
visited.set(edge.to, edge);
117+
}
118+
}
119+
}
120+
}
121+
122+
// Fall through means that we could not reach the goal type
123+
return [];
124+
}
125+
126+
}

packages/typir/src/graph/type-graph.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class TypeGraph {
4343
}
4444
} else {
4545
this.nodes.set(mapKey, type);
46-
this.listeners.forEach(listener => listener.addedType(type, mapKey));
46+
this.listeners.forEach(listener => listener.onAddedType?.call(listener, type, mapKey));
4747
}
4848
}
4949

@@ -63,7 +63,7 @@ export class TypeGraph {
6363
// remove the type itself
6464
const contained = this.nodes.delete(mapKey);
6565
if (contained) {
66-
this.listeners.slice().forEach(listener => listener.removedType(typeToRemove, mapKey));
66+
this.listeners.slice().forEach(listener => listener.onRemovedType?.call(listener, typeToRemove, mapKey));
6767
typeToRemove.dispose();
6868
} else {
6969
throw new Error(`Type does not exist: ${mapKey}`);
@@ -94,7 +94,7 @@ export class TypeGraph {
9494
edge.to.addIncomingEdge(edge);
9595
edge.from.addOutgoingEdge(edge);
9696

97-
this.listeners.forEach(listener => listener.addedEdge(edge));
97+
this.listeners.forEach(listener => listener.onAddedEdge?.call(listener, edge));
9898
}
9999

100100
removeEdge(edge: TypeEdge): void {
@@ -105,7 +105,7 @@ export class TypeGraph {
105105
const index = this.edges.indexOf(edge);
106106
if (index >= 0) {
107107
this.edges.splice(index, 1);
108-
this.listeners.forEach(listener => listener.removedEdge(edge));
108+
this.listeners.forEach(listener => listener.onRemovedEdge?.call(listener, edge));
109109
} else {
110110
throw new Error(`Edge does not exist: ${edge.$relation}`);
111111
}
@@ -138,9 +138,9 @@ export class TypeGraph {
138138

139139
}
140140

141-
export interface TypeGraphListener {
142-
addedType(type: Type, key: string): void;
143-
removedType(type: Type, key: string): void;
144-
addedEdge(edge: TypeEdge): void;
145-
removedEdge(edge: TypeEdge): void;
146-
}
141+
export type TypeGraphListener = Partial<{
142+
onAddedType(type: Type, key: string): void;
143+
onRemovedType(type: Type, key: string): void;
144+
onAddedEdge(edge: TypeEdge): void;
145+
onRemovedEdge(edge: TypeEdge): void;
146+
}>

packages/typir/src/graph/type-node.ts

+9-31
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@ export abstract class Type {
165165
// don't inform about the Invalid state!
166166
break;
167167
case 'Identifiable':
168-
newListeners.switchedToIdentifiable(this);
168+
newListeners.onSwitchedToIdentifiable(this);
169169
break;
170170
case 'Completed':
171-
newListeners.switchedToIdentifiable(this); // inform about both Identifiable and Completed!
172-
newListeners.switchedToCompleted(this);
171+
newListeners.onSwitchedToIdentifiable(this); // inform about both Identifiable and Completed!
172+
newListeners.onSwitchedToCompleted(this);
173173
break;
174174
default:
175175
assertUnreachable(currentState);
@@ -298,21 +298,21 @@ export abstract class Type {
298298
this.assertState('Invalid');
299299
this.onIdentification();
300300
this.initializationState = 'Identifiable';
301-
this.stateListeners.slice().forEach(listener => listener.switchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications
301+
this.stateListeners.slice().forEach(listener => listener.onSwitchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications
302302
}
303303

304304
protected switchFromIdentifiableToCompleted(): void {
305305
this.assertState('Identifiable');
306306
this.onCompletion();
307307
this.initializationState = 'Completed';
308-
this.stateListeners.slice().forEach(listener => listener.switchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications
308+
this.stateListeners.slice().forEach(listener => listener.onSwitchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications
309309
}
310310

311311
protected switchFromCompleteOrIdentifiableToInvalid(): void {
312312
if (this.isNotInState('Invalid')) {
313313
this.onInvalidation();
314314
this.initializationState = 'Invalid';
315-
this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications
315+
this.stateListeners.slice().forEach(listener => listener.onSwitchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications
316316
// add the types again, since the initialization process started again
317317
this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this]));
318318
this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this]));
@@ -331,28 +331,6 @@ export abstract class Type {
331331
*/
332332
abstract analyzeTypeEqualityProblems(otherType: Type): TypirProblem[];
333333

334-
/**
335-
* Analyzes, whether there is a sub type-relationship between two types.
336-
* The difference between sub type-relationships and super type-relationships are only switched types.
337-
* If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition).
338-
*
339-
* @param superType the super type, while the current type is the sub type
340-
* @returns an empty array, if the relationship exists, otherwise some problems which might point to violations of the investigated relationship.
341-
* These problems are presented to users in order to support them with useful information about the result of this analysis.
342-
*/
343-
abstract analyzeIsSubTypeOf(superType: Type): TypirProblem[];
344-
345-
/**
346-
* Analyzes, whether there is a super type-relationship between two types.
347-
* The difference between sub type-relationships and super type-relationships are only switched types.
348-
* If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition).
349-
*
350-
* @param subType the sub type, while the current type is super type
351-
* @returns an empty array, if the relationship exists, otherwise some problems which might point to violations of the investigated relationship.
352-
* These problems are presented to users in order to support them with useful information about the result of this analysis.
353-
*/
354-
abstract analyzeIsSuperTypeOf(subType: Type): TypirProblem[];
355-
356334

357335
addIncomingEdge(edge: TypeEdge): void {
358336
const key = edge.$relation;
@@ -435,7 +413,7 @@ export function isType(type: unknown): type is Type {
435413

436414

437415
export interface TypeStateListener {
438-
switchedToInvalid(type: Type): void;
439-
switchedToIdentifiable(type: Type): void;
440-
switchedToCompleted(type: Type): void;
416+
onSwitchedToInvalid(type: Type): void;
417+
onSwitchedToIdentifiable(type: Type): void;
418+
onSwitchedToCompleted(type: Type): void;
441419
}

0 commit comments

Comments
 (0)