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

Cyclic type definitions #33

Merged
merged 19 commits into from
Nov 27, 2024
Merged
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a2bb2ee
first working version of type definitions in "wrong" order for classe…
JohannesMeierSE Nov 12, 2024
bf1eb78
listener for new inference rules, fixed several bugs
JohannesMeierSE Nov 12, 2024
36708f0
fixed important bug
JohannesMeierSE Nov 18, 2024
c1f02ec
refactorings, improved comments
JohannesMeierSE Nov 18, 2024
0c933fa
validation (instead of exception) for super-sub-class cycles
JohannesMeierSE Nov 19, 2024
ddda324
propagate cyclic types to the direct children
JohannesMeierSE Nov 19, 2024
6c29f57
propagate types to ignore recursively to indirect dependencies as well
JohannesMeierSE Nov 19, 2024
40bea25
added more expected error messages into existing test cases
JohannesMeierSE Nov 19, 2024
d4b1cde
simplified code, improved comments, more test cases, renamings, some …
JohannesMeierSE Nov 20, 2024
005c37f
fixed registration of inference rules of classes, new deconstruct log…
JohannesMeierSE Nov 20, 2024
072de08
more test cases for cyclic classes
JohannesMeierSE Nov 20, 2024
704d81c
small improvements for the lifecycle of types, renamings
JohannesMeierSE Nov 21, 2024
a07614b
realized cyclic types with Functions involved, slightly reworked the …
JohannesMeierSE Nov 21, 2024
14b21d5
fix for unique method validation, more test cases
JohannesMeierSE Nov 22, 2024
db66fba
more comments, renamings
JohannesMeierSE Nov 22, 2024
9f1b703
refactorings, more test cases and comments according to the review
JohannesMeierSE Nov 26, 2024
2a6c18f
added name as identifier for nominally typed classes earlier, refacto…
JohannesMeierSE Nov 27, 2024
3b471e2
improved existing test cases
JohannesMeierSE Nov 27, 2024
bc43521
renaming, refactoring, more comments
JohannesMeierSE Nov 27, 2024
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
Prev Previous commit
Next Next commit
simplified code, improved comments, more test cases, renamings, some …
…more little refactorings
JohannesMeierSE committed Nov 20, 2024
commit d4b1cde533991db50ca064b8a031f1dff96a2131
11 changes: 2 additions & 9 deletions examples/lox/src/language/type-system/lox-type-checking.ts
Original file line number Diff line number Diff line change
@@ -211,24 +211,17 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator {

// TODO support lambda (type references)!

/**
* TODO Delayed:
* - (classType: Type) => Type(for output)
* - WANN werden sie aufgelöst? bei erster Verwendung?
* - WO wird das verwaltet? im Kind? im Type? im TypeGraph?
*/

// class types (nominal typing):
if (isClass(node)) {
const className = node.name;
const classType = this.classKind.createClassType({
className,
superClasses: node.superClass?.ref, // note that type inference is used here; TODO delayed
superClasses: node.superClass?.ref, // note that type inference is used here
fields: node.members
.filter(isFieldMember) // only Fields, no Methods
.map(f => <CreateFieldDetails>{
name: f.name,
type: f.type, // note that type inference is used here; TODO delayed
type: f.type, // note that type inference is used here
}),
methods: node.members
.filter(isMethodMember) // only Methods, no Fields
43 changes: 43 additions & 0 deletions examples/lox/test/lox-type-checking.test.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import type { Diagnostic } from 'vscode-languageserver-types';
import { DiagnosticSeverity } from 'vscode-languageserver-types';
import { createLoxServices } from '../src/language/lox-module.js';
import { deleteAllDocuments } from 'typir-langium';
import { isClassType } from '../../../packages/typir/lib/kinds/class-kind.js';

const loxServices = createLoxServices(EmptyFileSystem).Lox;

@@ -311,11 +312,53 @@ describe('Cyclic type definitions where a Class is declared and already used', (
}
`, []));

test('Two Classes with fields with the other Class as type', async () => await validate(`
class A {
prop1: B
}
class B {
prop2: A
}
`, []));

test.todo('Three Classes with fields with the other Class as type', async () => await validate(`
class A {
prop1: B
}
class B {
prop2: C
}
class C {
prop3: A
}
`, []));

test.todo('Class with method', async () => await validate(`
class Node {
myMethod(input: number): Class {}
}
`, []));

test('Having two declarations for the delayed class A, but only one type A in the type system', async () => {
await validate(`
class A {
property1: B // needs to wait for B, since B is defined below
}
class A {
property2: B // needs to wait for B, since B is defined below
}
class B { }
`, [ // Typir works with this, but for LOX these validation errors are produced:
'Declared classes need to be unique (A).',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you thought of this case and the possibility of duplicates happening during the wait. I wonder about a validation like this and its relationship to the type initialiser that ensures a type is registered only once. Do we simply have both? In the sense of: Should a language normally have this validation, but whether it has or not, we make sure in the background that there is no double entry?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a language normally have this validation, but whether it has or not, we make sure in the background that there is no double entry?

Yes, exactly!

Typir ensures, that types are existing only once in the type graph.

The (predefined) validations check, that there are not multiple AstNodes which declare the same Typir type.

'Declared classes need to be unique (A).',
]);
// check, that there is only one class type A in the type graph:
const classes = loxServices.graph.getAllRegisteredTypes().filter(t => isClassType(t)).map(t => t.getName());
expect(classes).toHaveLength(2);
expect(classes).includes('A');
expect(classes).includes('B');
});

});

describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => {
3 changes: 3 additions & 0 deletions packages/typir/src/features/inference.ts
Original file line number Diff line number Diff line change
@@ -49,6 +49,9 @@ type TypeInferenceResultWithInferringChildren =
* i.e. only a single type (or no type at all) can be inferred for a given domain element.
* There are inference rules which dependent on types of children of the given domain element (e.g. calls of overloaded functions depend on the types of the current arguments)
* and there are inference rules without this need.
*
* Within inference rules, don't take the initialization state of the inferred type into account,
* since such inferrence rules might not work for cyclic type definitions.
*/
export type TypeInferenceRule = TypeInferenceRuleWithoutInferringChildren | TypeInferenceRuleWithInferringChildren;

87 changes: 53 additions & 34 deletions packages/typir/src/graph/type-node.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
******************************************************************************/

import { Kind, isKind } from '../kinds/kind.js';
import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForResolvedTypeReferences } from '../utils/utils-definitions.js';
import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForIdentifiableAndCompletedTypeReferences } from '../utils/utils-definitions.js';
import { assertTrue, assertUnreachable } from '../utils/utils.js';
import { TypeEdge } from './type-edge.js';

@@ -72,64 +72,67 @@ export abstract class Type {



protected initialization: TypeInitializationState = 'Invalid'; // TODO or Identifiable
// store the state of the initialization process of this type

protected initializationState: TypeInitializationState = 'Invalid';

getInitializationState(): TypeInitializationState {
return this.initialization;
return this.initializationState;
}

protected assertState(expectedState: TypeInitializationState): void {
if (this.isInState(expectedState) === false) {
throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but ${expectedState} is expected.`);
throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but ${expectedState} is expected.`);
}
}
protected assertNotState(expectedState: TypeInitializationState): void {
if (this.isNotInState(expectedState) === false) {
throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but this state is not expected.`);
throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`);
}
}
protected assertStateOrLater(expectedState: TypeInitializationState): void {
if (this.isInStateOrLater(expectedState) === false) {
throw new Error(`The current state of type '${this.identifier}' is ${this.initialization}, but this state is not expected.`);
throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`);
}
}

isInState(state: TypeInitializationState): boolean {
return this.initialization === state;
return this.initializationState === state;
}
isNotInState(state: TypeInitializationState): boolean {
return this.initialization !== state;
return this.initializationState !== state;
}
isInStateOrLater(state: TypeInitializationState): boolean {
switch (state) {
case 'Invalid':
return true;
case 'Identifiable':
return this.initialization !== 'Invalid';
return this.initializationState !== 'Invalid';
case 'Completed':
return this.initialization === 'Completed';
return this.initializationState === 'Completed';
default:
assertUnreachable(state);
}
}

// manage listeners for updated state of the current type

// manage listeners for updates of the initialization state

protected stateListeners: TypeStateListener[] = [];

addListener(listener: TypeStateListener, informIfAlreadyFulfilled: boolean): void {
addListener(listener: TypeStateListener, informIfNotInvalidAnymore: boolean): void {
this.stateListeners.push(listener);
if (informIfAlreadyFulfilled) {
if (informIfNotInvalidAnymore) {
const currentState = this.getInitializationState();
switch (currentState) {
case 'Invalid':
// TODO?
// don't inform about the Invalid state!
break;
case 'Identifiable':
listener.switchedToIdentifiable(this);
break;
case 'Completed':
listener.switchedToIdentifiable(this);
listener.switchedToIdentifiable(this); // inform about both Identifiable and Completed!
listener.switchedToCompleted(this);
break;
default:
@@ -145,16 +148,32 @@ export abstract class Type {
}
}

protected waitForIdentifiable: WaitingForResolvedTypeReferences;
protected waitForCompleted: WaitingForResolvedTypeReferences;
// initialization logic which is specific for the type to initialize
protected onIdentification: () => void;
protected onCompletion: () => void;
protected onInvalidation: () => void;

// internal helpers
protected waitForIdentifiable: WaitingForIdentifiableAndCompletedTypeReferences;
protected waitForCompleted: WaitingForIdentifiableAndCompletedTypeReferences;
protected waitForInvalid: WaitingForInvalidTypeReferences;

// to be called at the end of the constructor of each specific Type implementation!
protected completeInitialization(preconditions: {
/**
* Use this method to specify, how THIS new type should be initialized.
*
* This method has(!) to be called at the end(!) of the constructor of each specific Type implementation, even if nothing has to be specified,
* since calling this method starts the initialization process!
* If you forget the call this method, the new type remains invalid and invisible for Typir and you will not be informed about this problem!
*
* @param preconditions all possible options for the initialization process
*/
protected defineTheInitializationProcessOfThisType(preconditions: {
preconditionsForInitialization?: PreconditionsForInitializationState,
preconditionsForCompletion?: PreconditionsForInitializationState,
referencesRelevantForInvalidation?: TypeReference[],
/** typical use cases: calculate the identifier, register inference rules for the type object already now! */
onIdentification?: () => void,
/** typical use cases: do some internal checks for the completed properties */
onCompletion?: () => void,
onInvalidation?: () => void,
}): void {
@@ -167,13 +186,13 @@ export abstract class Type {
console.log('');
}
// preconditions for Identifiable
this.waitForIdentifiable = new WaitingForResolvedTypeReferences(
this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences(
preconditions.preconditionsForInitialization?.refsToBeIdentified,
preconditions.preconditionsForInitialization?.refsToBeCompleted,
this,
);
// preconditions for Completed
this.waitForCompleted = new WaitingForResolvedTypeReferences(
this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences(
preconditions.preconditionsForCompletion?.refsToBeIdentified,
preconditions.preconditionsForCompletion?.refsToBeCompleted,
this,
@@ -187,53 +206,54 @@ export abstract class Type {
this.waitForIdentifiable.addListener(() => {
this.switchFromInvalidToIdentifiable();
if (this.waitForCompleted.isFulfilled()) {
// this is required to ensure the stric order Identifiable --> Completed, since 'init2' might already be triggered
// this is required to ensure the stric order Identifiable --> Completed, since 'waitForCompleted' might already be triggered
this.switchFromIdentifiableToCompleted();
}
}, true);
}, true); // 'true' triggers the initialization process!
// identifiable --> completed
this.waitForCompleted.addListener(() => {
if (this.waitForIdentifiable.isFulfilled()) {
this.switchFromIdentifiableToCompleted();
} else {
// switching will be done later by 'init1' in order to conform to the stric order Identifiable --> Completed
// switching will be done later by 'waitForIdentifiable' in order to conform to the stric order Identifiable --> Completed
}
}, false); // not required, since init1 will switch to Completed as well!
}, false); // not required, since 'waitForIdentifiable' will switch to Completed as well!
// identifiable/completed --> invalid
this.waitForInvalid.addListener(() => {
if (this.isNotInState('Invalid')) {
this.switchFromCompleteOrIdentifiableToInvalid();
}
}, false); // no initial trigger!
}, false); // no initial trigger, since 'Invalid' is the initial state
}

/**
* This is an internal method to ignore some types during the initialization process in order to prevent dependency cycles.
* Usually there is no need to call this method on your own.
* @param additionalTypesToIgnore the new types to ignore during
*/
ignoreDependingTypesDuringInitialization(additionalTypesToIgnore: Set<Type>): void {
this.waitForIdentifiable.addTypesToIgnoreForCycles(additionalTypesToIgnore);
this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore);
}

protected onIdentification: () => void; // typical use cases: calculate the identifier
protected onCompletion: () => void; // typical use cases: determine all properties which depend on other types to be created
protected onInvalidation: () => void; // TODO ist jetzt anders; typical use cases: register inference rules for the type object already now!

protected switchFromInvalidToIdentifiable(): void {
this.assertState('Invalid');
this.onIdentification();
this.initialization = 'Identifiable';
this.initializationState = 'Identifiable';
this.stateListeners.slice().forEach(listener => listener.switchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications
}

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

protected switchFromCompleteOrIdentifiableToInvalid(): void {
this.assertNotState('Invalid');
this.onInvalidation();
this.initialization = 'Invalid';
this.initializationState = 'Invalid';
this.stateListeners.slice().forEach(listener => listener.switchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications
}

@@ -355,4 +375,3 @@ export interface TypeStateListener {
switchedToIdentifiable(type: Type): void;
switchedToCompleted(type: Type): void;
}
// TODO brauchen wir das überhaupt? stattdessen in TypeReference direkt realisieren?
2 changes: 1 addition & 1 deletion packages/typir/src/kinds/bottom-kind.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export class BottomType extends Type {
constructor(kind: BottomKind, identifier: string) {
super(identifier);
this.kind = kind;
this.completeInitialization({}); // no preconditions
this.defineTheInitializationProcessOfThisType({}); // no preconditions
}

override getName(): string {
13 changes: 9 additions & 4 deletions packages/typir/src/kinds/class-kind.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,6 @@ import { TypeInitializer } from '../utils/type-initialization.js';

// TODO irgendwann die Dateien auseinander ziehen und Packages einführen!

// TODO wenn die Initialisierung von ClassType abgeschlossen ist, sollte darüber aktiv benachrichtigt werden!
export class ClassType extends Type {
override readonly kind: ClassKind;
readonly className: string;
@@ -79,12 +78,12 @@ export class ClassType extends Type {
fieldsAndMethods.push(...(refMethods as unknown as Array<TypeReference<Type>>)); // TODO dirty hack?!
// all.push(...refMethods); // does not work

this.completeInitialization({
this.defineTheInitializationProcessOfThisType({
preconditionsForInitialization: {
refsToBeIdentified: fieldsAndMethods,
},
preconditionsForCompletion: {
refsToBeCompleted: this.superClasses as unknown as Array<TypeReference<Type>>, // TODO here we are waiting for the same/current (identifiable) ClassType!!
refsToBeCompleted: this.superClasses as unknown as Array<TypeReference<Type>>,
},
referencesRelevantForInvalidation: [...fieldsAndMethods, ...(this.superClasses as unknown as Array<TypeReference<Type>>)],
onIdentification: () => {
@@ -554,7 +553,6 @@ export class ClassTypeInitializer<T = unknown, T1 = unknown, T2 = unknown> exten
if (kind.options.typing === 'Structural') {
// TODO Vorsicht Inference rules werden by default an den Identifier gebunden (ebenso Validations)!
this.services.graph.addNode(classType, kind.getIdentifierPrefix() + typeDetails.className);
// TODO hinterher wieder abmelden, wenn Type invalid geworden ist bzw. ein anderer Type gewonnen hat? bzw. gewinnt immer der erste Type?
}

classType.addListener(this, true); // trigger directly, if some initialization states are already reached!
@@ -568,6 +566,13 @@ export class ClassTypeInitializer<T = unknown, T1 = unknown, T2 = unknown> exten
*/
const readyClassType = this.producedType(classType as ClassType);

// remove/invalidate the duplicated and skipped class type now
if (readyClassType !== classType) {
if (this.kind.options.typing === 'Structural') {
this.services.graph.removeNode(classType, this.kind.getIdentifierPrefix() + this.typeDetails.className);
}
}

// register inference rules
registerInferenceRules<T, T1, T2>(this.services, this.typeDetails, this.kind, readyClassType);

2 changes: 1 addition & 1 deletion packages/typir/src/kinds/fixed-parameters-kind.ts
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ export class FixedParameterType extends Type {
type: typeValues[i],
});
}
this.completeInitialization({}); // TODO preconditions
this.defineTheInitializationProcessOfThisType({}); // TODO preconditions
}

getParameterTypes(): Type[] {
2 changes: 1 addition & 1 deletion packages/typir/src/kinds/function-kind.ts
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ export class FunctionType extends Type {
};
});

this.completeInitialization({}); // TODO preconditions
this.defineTheInitializationProcessOfThisType({}); // TODO preconditions
}

override getName(): string {
2 changes: 1 addition & 1 deletion packages/typir/src/kinds/multiplicity-kind.ts
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ export class MultiplicityType extends Type {
this.constrainedType = constrainedType;
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.completeInitialization({}); // TODO preconditions
this.defineTheInitializationProcessOfThisType({}); // TODO preconditions
}

override getName(): string {
Loading