Skip to content

Commit

Permalink
listener for new inference rules, fixed several bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannesMeierSE committed Nov 12, 2024
1 parent a2bb2ee commit bf1eb78
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 32 deletions.
4 changes: 2 additions & 2 deletions examples/lox/test/lox-type-checking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance

describe('LOX', () => {
// this test case will work after having the support for cyclic type definitions, since it will solve also issues with topological order of type definitions
test.only('complete with difficult order of classes', async () => await validate(`
test('complete with difficult order of classes', async () => await validate(`
class SuperClass {
a: number
}
Expand All @@ -340,7 +340,7 @@ describe('LOX', () => {
var x = SubClass();
// Assigning nil to a class type
var nilTest = SubClass();
// nilTest = nil; // TODO failed
nilTest = nil;
// Accessing members of a class
var value = x.nested.method() + "wasd";
Expand Down
28 changes: 27 additions & 1 deletion packages/typir/src/features/inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export interface TypeInferenceRuleWithInferringChildren {
}


export interface TypeInferenceCollectorListener {
addedInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void;
removedInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void;
}

/**
* Collects an arbitrary number of inference rules
* and allows to infer a type for a given domain element.
Expand All @@ -103,13 +108,17 @@ export interface TypeInferenceCollector {
* If the given type is removed from the type system, this rule will be automatically removed as well.
*/
addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void;

addListener(listener: TypeInferenceCollectorListener): void;
removeListener(listener: TypeInferenceCollectorListener): void;
}


export class DefaultTypeInferenceCollector implements TypeInferenceCollector, TypeGraphListener {
protected readonly inferenceRules: Map<string, TypeInferenceRule[]> = new Map(); // type identifier (otherwise '') -> inference rules
protected readonly domainElementInference: DomainElementInferenceCaching;
protected readonly services: TypirServices;
protected readonly listeners: TypeInferenceCollectorListener[] = [];

constructor(services: TypirServices) {
this.services = services;
Expand All @@ -125,6 +134,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty
this.inferenceRules.set(key, rules);
}
rules.push(rule);
this.listeners.forEach(listener => listener.addedInferenceRule(rule, boundToType));
}

protected getBoundToTypeKey(boundToType?: Type): string {
Expand Down Expand Up @@ -266,13 +276,29 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty
}


addListener(listener: TypeInferenceCollectorListener): void {
this.listeners.push(listener);
}
removeListener(listener: TypeInferenceCollectorListener): void {
const index = this.listeners.indexOf(listener);
if (index >= 0) {
this.listeners.splice(index, 1);
}
}


/* Get informed about deleted types in order to remove inference rules which are bound to them. */

addedType(_newType: Type, _key: string): void {
// do nothing
}
removedType(type: Type, _key: string): void {
this.inferenceRules.delete(this.getBoundToTypeKey(type));
const key = this.getBoundToTypeKey(type);
const rulesToRemove = this.inferenceRules.get(key);
// remove the inference rules associated to the deleted type
this.inferenceRules.delete(key);
// inform listeners about removed inference rules
(rulesToRemove ?? []).forEach(rule => this.listeners.forEach(listener => listener.removedInferenceRule(rule, type)));
}
addedEdge(_edge: TypeEdge): void {
// do nothing
Expand Down
23 changes: 16 additions & 7 deletions packages/typir/src/graph/type-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import { isClassType } from '../kinds/class-kind.js';
import { Kind, isKind } from '../kinds/kind.js';
import { TypeReference, TypirProblem, WaitingForInvalidTypeReferences, WaitingForResolvedTypeReferences } from '../utils/utils-definitions.js';
import { assertTrue, assertUnreachable } from '../utils/utils.js';
Expand Down Expand Up @@ -166,7 +167,7 @@ export abstract class Type {
preconditions.preconditionsForCompletion?.refsToBeIdentified,
preconditions.preconditionsForCompletion?.refsToBeCompleted,
);
// completed --> invalid, TODO wie genau wird das realisiert?? triggert jetzt schon!!
// completed --> invalid
const init3 = new WaitingForInvalidTypeReferences(
preconditions.referencesRelevantForInvalidation ?? [],
);
Expand All @@ -177,17 +178,25 @@ export abstract class Type {
this.onInvalidation = preconditions.onInvalidation ?? (() => {});

// specify the transitions between the states:
init1.addListener(() => this.switchFromInvalidToIdentifiable(), true);
init1.addListener(() => {
this.switchFromInvalidToIdentifiable();
if (init2.isFulfilled()) {
// this is required to ensure the stric order Identifiable --> Completed, since 'init2' might already be triggered
this.switchFromIdentifiableToCompleted();
}
}, true);
init2.addListener(() => {
if (init1.isFulfilled()) {
this.switchFromIdentifiableToCompleted();
} else {
// TODO ??
// switching will be done later by 'init1' in order to conform to the stric order Identifiable --> Completed
}
}, true);
init3.addListener(() => this.switchFromCompleteOrIdentifiableToInvalid(), false); // no initial trigger!
// TODO noch sicherstellen, dass keine Phasen übersprungen werden??
// TODO trigger start??
}, false); // not required, since init1 will switch to Completed as well!
init3.addListener(() => {
if (this.isNotInState('Invalid')) {
this.switchFromCompleteOrIdentifiableToInvalid();
}
}, false); // no initial trigger!
}

protected onIdentification: () => void; // typical use cases: calculate the identifier
Expand Down
32 changes: 16 additions & 16 deletions packages/typir/src/kinds/class-kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ClassType extends Type {
superRef.addReactionOnTypeUnresolved((_ref, superType) => {
// if the superType gets invalid, de-register this class as sub-class of the super-class
superType.subClasses.splice(superType.subClasses.indexOf(this), 1);
}, true);
}, false);
return superRef;
});

Expand Down Expand Up @@ -75,21 +75,23 @@ export class ClassType extends Type {

// calculate the Identifier, based on the resolved type references
// const all: Array<TypeReference<Type | FunctionType>> = [];
const all: Array<TypeReference<Type>> = [];
all.push(...refFields);
all.push(...(refMethods as unknown as Array<TypeReference<Type>>)); // TODO dirty hack?!
const fieldsAndMethods: Array<TypeReference<Type>> = [];
fieldsAndMethods.push(...refFields);
fieldsAndMethods.push(...(refMethods as unknown as Array<TypeReference<Type>>)); // TODO dirty hack?!
// all.push(...refMethods); // does not work

this.completeInitialization({
preconditionsForInitialization: {
refsToBeIdentified: all,
refsToBeIdentified: fieldsAndMethods,
},
preconditionsForCompletion: {
refsToBeCompleted: this.superClasses as unknown as Array<TypeReference<Type>>,
},
referencesRelevantForInvalidation: [...fieldsAndMethods, ...(this.superClasses as unknown as Array<TypeReference<Type>>)],
onIdentification: () => {
this.identifier = this.kind.calculateIdentifier(typeDetails);
// TODO identifier erst hier berechnen?! registering??
// the identifier is calculated now
this.identifier = this.kind.calculateIdentifier(typeDetails); // TODO it is still not nice, that the type resolving is done again, since the TypeReferences here are not reused
// the registration of the type in the type graph is done by the TypeInitializer
},
onCompletion: () => {
// when all super classes are completely available, do the following checks:
Expand Down Expand Up @@ -438,8 +440,7 @@ export class ClassKind implements Kind {
// nominal typing
return new TypeReference(typeDetails, this.services);
} else {
// structural typing
// TODO does this case occur in practise?
// structural typing (does this case occur in practise?)
return new TypeReference(() => this.calculateIdentifier(typeDetails), this.services);
}
}
Expand All @@ -452,8 +453,6 @@ export class ClassKind implements Kind {
* @returns an initializer which creates and returns the new class type, when all depending types are resolved
*/
createClassType<T, T1, T2>(typeDetails: CreateClassTypeDetails<T, T1, T2>): TypeInitializer<ClassType> {
// assertTrue(this.getClassType(typeDetails) === undefined, `The class '${typeDetails.className}' already exists!`); // ensures, that no duplicated classes are created!

return new ClassTypeInitializer(this.services, this, typeDetails);
}

Expand All @@ -463,6 +462,7 @@ export class ClassKind implements Kind {

/**
* TODO
* If some types for the properties of the class are missing, an exception will be thrown.
*
* Design decisions:
* - This method is part of the ClassKind and not part of ClassType, since the ClassKind requires it for 'getClassType'!
Expand All @@ -471,7 +471,7 @@ export class ClassKind implements Kind {
* @param typeDetails the details
* @returns the new identifier
*/
calculateIdentifier<T>(typeDetails: ClassTypeDetails<T>): string { // TODO kann keinen Identifier liefern, wenn noch nicht resolved!
calculateIdentifier<T>(typeDetails: ClassTypeDetails<T>): string {
// purpose of identifier: distinguish different types; NOT: not uniquely overloaded types
const prefix = this.getIdentifierPrefix();
if (this.options.typing === 'Structural') {
Expand Down Expand Up @@ -542,6 +542,7 @@ export class ClassTypeInitializer<T = unknown, T1 = unknown, T2 = unknown> exten

// create the class type
const classType = new ClassType(kind, typeDetails as CreateClassTypeDetails);
// TODO erst nach Herausfiltern im Initializer darf der Type selbst sich registrieren!
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);
Expand All @@ -551,17 +552,16 @@ export class ClassTypeInitializer<T = unknown, T1 = unknown, T2 = unknown> exten
classType.addListener(this, true); // trigger directly, if some initialization states are already reached!
}

switchedToIdentifiable(type: Type): void {
switchedToIdentifiable(classType: Type): void {
// TODO Vorsicht, dass hier nicht 2x derselbe Type angefangen wird zu erstellen und dann zwei Typen auf ihre Vervollständigung warten!
// 2x TypeResolver erstellen, beide müssen später denselben ClassType zurückliefern!
// bei Node { children: Node[] } muss der Zyklus erkannt und behandelt werden!!
this.producedType(type as ClassType);
this.producedType(classType as ClassType);
registerInferenceRules<T, T1, T2>(this.services, this.typeDetails, this.kind, classType as ClassType);
}

switchedToCompleted(classType: Type): void {
// register inference rules
// TODO or can this be done already after having the identifier?
registerInferenceRules<T, T1, T2>(this.services, this.typeDetails, this.kind, classType as ClassType);
classType.removeListener(this); // the work of this initializer is done now
}

Expand Down
23 changes: 17 additions & 6 deletions packages/typir/src/utils/utils-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

/* eslint-disable @typescript-eslint/no-explicit-any */

import { TypeInferenceCollectorListener, TypeInferenceRule } from '../features/inference.js';
import { TypeEdge } from '../graph/type-edge.js';
import { TypeGraphListener } from '../graph/type-graph.js';
import { isType, Type, TypeInitializationState, TypeStateListener } from '../graph/type-node.js';
Expand Down Expand Up @@ -173,8 +174,8 @@ export class WaitingForInvalidTypeReferences<T extends Type = Type> {

// register to get updates for the relevant TypeReferences
this.waitForRefsInvalid.forEach(ref => {
ref.addReactionOnTypeIdentified(this.listeningForNextState, false);
ref.addReactionOnTypeUnresolved(this.listeningForReset, false);
ref.addReactionOnTypeIdentified(() => this.listeningForNextState(), false);
ref.addReactionOnTypeUnresolved(() => this.listeningForReset(), false);
});
}

Expand All @@ -193,11 +194,11 @@ export class WaitingForInvalidTypeReferences<T extends Type = Type> {
}
}

protected listeningForNextState(_reference: TypeReference<T>, _type: T): void {
protected listeningForNextState(): void {
this.counterInvalid--;
}

protected listeningForReset(_reference: TypeReference<T>, _type: T): void {
protected listeningForReset(): void {
this.counterInvalid++;
if (this.isFulfilled()) {
this.listeners.forEach(listener => listener(this));
Expand All @@ -221,7 +222,7 @@ export type TypeReferenceListener<T extends Type = Type> = (reference: TypeRefer
* The internal logic of a TypeReference is independent from the kind of the type to resolve.
* A TypeReference takes care of the lifecycle of the types.
*/
export class TypeReference<T extends Type = Type> implements TypeGraphListener, TypeStateListener {
export class TypeReference<T extends Type = Type> implements TypeGraphListener, TypeStateListener, TypeInferenceCollectorListener {
protected readonly selector: TypeSelector;
protected readonly services: TypirServices;
protected resolvedType: T | undefined = undefined;
Expand Down Expand Up @@ -251,12 +252,14 @@ export class TypeReference<T extends Type = Type> implements TypeGraphListener,
type.addListener(this, false);
}
});
// TODO react on new inference rules
// react on new inference rules
this.services.inference.addListener(this);
}

protected stopResolving(): void {
// it is not required to listen to new types anymore, since the type is already resolved/found
this.services.graph.removeListener(this);
this.services.inference.removeListener(this);
}

getState(): TypeInitializationState | undefined {
Expand Down Expand Up @@ -392,6 +395,14 @@ export class TypeReference<T extends Type = Type> implements TypeGraphListener,
// only types are relevant
}

addedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void {
// after adding a new inference rule, try to resolve the type
this.resolve();
}
removedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void {
// empty
}

switchedToIdentifiable(type: Type): void {
const result = this.resolve(); // is it possible to do this more performant by looking at the given "type"?
if (result === 'ALREADY_RESOLVED' && type === this.resolvedType) {
Expand Down

0 comments on commit bf1eb78

Please sign in to comment.