Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworked the sub-type relationship infrastructure #58

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
22b305a
some minor fixes
JohannesMeierSE Jan 10, 2025
c6550d8
started with some predefined language elements for testing and test u…
JohannesMeierSE Jan 10, 2025
67e9dbe
assignability informs about the reason for successful assignability now
JohannesMeierSE Jan 10, 2025
92d7583
now sub-type relationships can be explicitly defined
JohannesMeierSE Jan 10, 2025
1652024
started implementation for finding best matches of overloaded functio…
JohannesMeierSE Jan 10, 2025
02b5a3d
fixed bug
JohannesMeierSE Jan 14, 2025
7437b23
more test cases
JohannesMeierSE Jan 14, 2025
20d7f41
new test case for conversions with cyclic rules
JohannesMeierSE Jan 14, 2025
cc8874d
extend conversion API to return all types to convert to
JohannesMeierSE Jan 17, 2025
bf7c90d
moved the existing graph algorithms into its own dedicated service in…
JohannesMeierSE Jan 17, 2025
0d0283c
reworked the management of sub-type relationships: use explicit edges…
JohannesMeierSE Jan 20, 2025
8bc6e71
make the whole assignability path in the type graph explicit, introdu…
JohannesMeierSE Jan 20, 2025
6862143
sub-type relationships: check for cycles, controlled by a new option
JohannesMeierSE Jan 20, 2025
e7434f3
improved README
JohannesMeierSE Jan 20, 2025
fe96e20
graph algorithms use only existing edges
JohannesMeierSE Jan 21, 2025
6ef3e51
removed methods of types to analyze their subType relationships
JohannesMeierSE Jan 21, 2025
03dc6b6
improved comments, fixed bug
JohannesMeierSE Jan 21, 2025
17449e5
improved test cases to explicitly check the resulting path
JohannesMeierSE Jan 21, 2025
95644b6
simplified APIs for sub-type and conversion, since users of Typir cou…
JohannesMeierSE Jan 27, 2025
a018854
improved the algorithm to select the best matches of overloading func…
JohannesMeierSE Jan 27, 2025
3d38164
introduced test fixtures for literals
JohannesMeierSE Jan 27, 2025
3605669
fixed typos
JohannesMeierSE Jan 27, 2025
fcbb582
prefixed methods in listeners with `on`
JohannesMeierSE Jan 27, 2025
8ee03f9
made methods in TypeGraphListener optional
JohannesMeierSE Jan 27, 2025
673f05f
refactoring: replaced nested ifs by chain of inverted ifs, improved c…
JohannesMeierSE Jan 27, 2025
7852b23
refactoring to enable the exchange of the predefined OverloadedFuncti…
JohannesMeierSE Jan 27, 2025
4417beb
added information about this PR into the CHANGELOG.md
JohannesMeierSE Jan 27, 2025
1882722
simplified assignability test cases
JohannesMeierSE Jan 28, 2025
145ac57
refactoring: split existing file into two more files
JohannesMeierSE Jan 28, 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
22 changes: 19 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,35 @@ We roughly follow the ideas of [semantic versioning](https://semver.org/).
Note that the versions "0.x.0" probably will include breaking changes.


## v0.1.2 (December 2024)
## v0.2.0 (upcoming)

### New features

- Users of Typir are able to explicitly define sub-type relationships via the `SubTypeService` (#58)
- Arbitrary paths of implicit conversion and sub-type relationships are considered for assignability now (#58)
- Control the behaviour in case of multiple matching overloads of functions (and operators) (#58)
- Moved the existing graph algorithms into its own dedicated service in order to reuse and to customize them (#58)

### Breaking changes

- `TypeConversion.markAsConvertible` accepts only one type for source and target now in order to simplify the API (#58)
- Methods in listeners (`TypeGraphListener`, `TypeStateListener`) are prefixed with `on` (#58)


## v0.1.2 (2024-12-20)

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


## v0.1.1 (December 2024)
## v0.1.1 (2024-12-20)

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


## v0.1.0 (December 2024)
## v0.1.0 (2024-12-20)

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!
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ Typir provides these core features:

Typir does intentionally _not_ include ...

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


## NPM workspace
Expand Down Expand Up @@ -109,7 +112,7 @@ typir.factory.Operators.createBinary({ name: '-', signatures: [{ left: numberTyp
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.

```typescript
typir.Conversion.markAsConvertible(numberType, stringType,'IMPLICIT_EXPLICIT');
typir.Conversion.markAsConvertible(numberType, stringType, 'IMPLICIT_EXPLICIT');
```

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.
Expand Down
6 changes: 3 additions & 3 deletions examples/lox/src/language/lox-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { LoxLinker } from './lox-linker.js';
*/
export type LoxAddedServices = {
validation: {
LoxValidator: LoxValidator
LoxValidator: LoxValidator,
},
typir: LangiumServicesForTypirBinding,
}
Expand All @@ -38,7 +38,7 @@ export function createLoxModule(shared: LangiumSharedCoreServices): Module<LoxSe
return {
validation: {
ValidationRegistry: (services) => new LoxValidationRegistry(services),
LoxValidator: () => new LoxValidator()
LoxValidator: () => new LoxValidator(),
},
// For type checking with Typir, inject and merge these modules:
typir: () => inject(Module.merge(
Expand Down Expand Up @@ -73,7 +73,7 @@ export function createLoxServices(context: DefaultSharedModuleContext): {
} {
const shared = inject(
createDefaultSharedModule(context),
LoxGeneratedSharedModule
LoxGeneratedSharedModule,
);
const Lox = inject(
createDefaultCoreModule({ shared }),
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 @@ -108,7 +108,7 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator,
this.documentTypesMap.delete(documentKey);
}

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

removedType(_type: Type): void {
onRemovedType(_type: Type): void {
// since this type creator actively removes types from the type graph itself, there is no need to react on removed types
}
addedEdge(_edge: TypeEdge): void {
onAddedEdge(_edge: TypeEdge): void {
// this type creator does not care about edges => do nothing
}
removedEdge(_edge: TypeEdge): void {
onRemovedEdge(_edge: TypeEdge): void {
// this type creator does not care about edges => do nothing
}
}
Expand Down
126 changes: 126 additions & 0 deletions packages/typir/src/graph/graph-algorithms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/******************************************************************************
* 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 { TypirServices } from '../typir.js';
import { TypeEdge } from './type-edge.js';
import { TypeGraph } from './type-graph.js';
import { Type } from './type-node.js';

/**
* Graph algorithms to do calculations on the type graph.
* All algorithms are robust regarding cycles.
*/
export interface GraphAlgorithms {
collectReachableTypes(from: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): Set<Type>;
existsEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): boolean;
getEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): TypeEdge[];
}

export class DefaultGraphAlgorithms implements GraphAlgorithms {
protected readonly graph: TypeGraph;

constructor(services: TypirServices) {
this.graph = services.infrastructure.Graph;
}

collectReachableTypes(from: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): Set<Type> {
const result: Set<Type> = new Set();
const remainingToCheck: Type[] = [from];

while (remainingToCheck.length > 0) {
const current = remainingToCheck.pop()!;
const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r));
for (const edge of outgoingEdges) {
if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) {
JohannesMeierSE marked this conversation as resolved.
Show resolved Hide resolved
if (result.has(edge.to)) {
// already checked
} else {
result.add(edge.to); // this type is reachable
remainingToCheck.push(edge.to); // check it for recursive conversions
}
}
}
}

return result;
}

existsEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): boolean {
const visited: Set<Type> = new Set();
const stack: Type[] = [from];

while (stack.length > 0) {
const current = stack.pop()!;
visited.add(current);

const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r));
for (const edge of outgoingEdges) {
if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) {
if (edge.to === to) {
/* It was possible to reach our goal type using this path.
* Base case that also catches the case in which start and end are the same
* (is there a cycle?). Therefore it is allowed to have been "visited".
* True will only be returned if there is a real path (cycle) made up of edges
*/
return true;
}
if (!visited.has(edge.to)) {
/* The target node of this edge has not been visited before and is also not our goal node
* Add it to the stack and investigate this path later.
*/
stack.push(edge.to);
}
}
}
}

// Fall through means that we could not reach the goal type
return false;
}

getEdgePath(from: Type, to: Type, $relations: Array<TypeEdge['$relation']>, filterEdges?: (edgr: TypeEdge) => boolean): TypeEdge[] {
const visited: Map<Type, TypeEdge|undefined> = new Map(); // the edge from the parent to the current node
visited.set(from, undefined);
const stack: Type[] = [from];

while (stack.length > 0) {
const current = stack.pop()!;

const outgoingEdges = $relations.flatMap(r => current.getOutgoingEdges(r));
for (const edge of outgoingEdges) {
if (edge.cachingInformation === 'LINK_EXISTS' && (filterEdges === undefined || filterEdges(edge))) {
if (edge.to === to) {
/* It was possible to reach our goal type using this path.
* Base case that also catches the case in which start and end are the same
* (is there a cycle?). Therefore it is allowed to have been "visited".
* True will only be returned if there is a real path (cycle) made up of edges
*/
const result: TypeEdge[] = [edge];
// collect the path of used edges, from "to" back to "from"
let backNode = edge.from;
while (backNode !== from) {
const backEdge = visited.get(backNode)!;
result.unshift(backEdge);
backNode = backEdge.from;
}
return result;
}
if (!visited.has(edge.to)) {
/* The target node of this edge has not been visited before and is also not our goal node
* Add it to the stack and investigate this path later.
*/
stack.push(edge.to);
visited.set(edge.to, edge);
}
}
}
}

// Fall through means that we could not reach the goal type
return [];
}

}
20 changes: 10 additions & 10 deletions packages/typir/src/graph/type-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class TypeGraph {
}
} else {
this.nodes.set(mapKey, type);
this.listeners.forEach(listener => listener.addedType(type, mapKey));
this.listeners.forEach(listener => listener.onAddedType?.call(listener, type, mapKey));
}
}

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

this.listeners.forEach(listener => listener.addedEdge(edge));
this.listeners.forEach(listener => listener.onAddedEdge?.call(listener, edge));
}

removeEdge(edge: TypeEdge): void {
Expand All @@ -105,7 +105,7 @@ export class TypeGraph {
const index = this.edges.indexOf(edge);
if (index >= 0) {
this.edges.splice(index, 1);
this.listeners.forEach(listener => listener.removedEdge(edge));
this.listeners.forEach(listener => listener.onRemovedEdge?.call(listener, edge));
} else {
throw new Error(`Edge does not exist: ${edge.$relation}`);
}
Expand Down Expand Up @@ -138,9 +138,9 @@ export class TypeGraph {

}

export interface TypeGraphListener {
addedType(type: Type, key: string): void;
removedType(type: Type, key: string): void;
addedEdge(edge: TypeEdge): void;
removedEdge(edge: TypeEdge): void;
}
export type TypeGraphListener = Partial<{
onAddedType(type: Type, key: string): void;
onRemovedType(type: Type, key: string): void;
onAddedEdge(edge: TypeEdge): void;
onRemovedEdge(edge: TypeEdge): void;
}>
40 changes: 9 additions & 31 deletions packages/typir/src/graph/type-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ export abstract class Type {
// don't inform about the Invalid state!
break;
case 'Identifiable':
newListeners.switchedToIdentifiable(this);
newListeners.onSwitchedToIdentifiable(this);
break;
case 'Completed':
newListeners.switchedToIdentifiable(this); // inform about both Identifiable and Completed!
newListeners.switchedToCompleted(this);
newListeners.onSwitchedToIdentifiable(this); // inform about both Identifiable and Completed!
newListeners.onSwitchedToCompleted(this);
break;
default:
assertUnreachable(currentState);
Expand Down Expand Up @@ -298,21 +298,21 @@ export abstract class Type {
this.assertState('Invalid');
this.onIdentification();
this.initializationState = 'Identifiable';
this.stateListeners.slice().forEach(listener => listener.switchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications
this.stateListeners.slice().forEach(listener => listener.onSwitchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications
}

protected switchFromIdentifiableToCompleted(): void {
this.assertState('Identifiable');
this.onCompletion();
this.initializationState = 'Completed';
this.stateListeners.slice().forEach(listener => listener.switchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications
this.stateListeners.slice().forEach(listener => listener.onSwitchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications
}

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

/**
* Analyzes, whether there is a sub type-relationship between two types.
* The difference between sub type-relationships and super type-relationships are only switched types.
* If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition).
*
* @param superType the super type, while the current type is the sub type
* @returns an empty array, if the relationship exists, otherwise some problems which might point to violations of the investigated relationship.
* These problems are presented to users in order to support them with useful information about the result of this analysis.
*/
abstract analyzeIsSubTypeOf(superType: Type): TypirProblem[];

/**
* Analyzes, whether there is a super type-relationship between two types.
* The difference between sub type-relationships and super type-relationships are only switched types.
* If both types are the same, no problems will be reported, since a type is considered as sub-type of itself (by definition).
*
* @param subType the sub type, while the current type is super type
* @returns an empty array, if the relationship exists, otherwise some problems which might point to violations of the investigated relationship.
* These problems are presented to users in order to support them with useful information about the result of this analysis.
*/
abstract analyzeIsSuperTypeOf(subType: Type): TypirProblem[];


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


export interface TypeStateListener {
switchedToInvalid(type: Type): void;
switchedToIdentifiable(type: Type): void;
switchedToCompleted(type: Type): void;
onSwitchedToInvalid(type: Type): void;
onSwitchedToIdentifiable(type: Type): void;
onSwitchedToCompleted(type: Type): void;
}
Loading