diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 9c1c999..bdd311f 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -6,60 +6,47 @@ import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; import { LangiumSharedServices } from 'langium/lsp'; -import { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, CreateParameterDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, createNoSuperClassCyclesValidation, ValidationMessageDetails } from 'typir'; +import { CreateFieldDetails, CreateFunctionTypeDetails, CreateParameterDetails, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, ValidationMessageDetails, createNoSuperClassCyclesValidation } from 'typir'; import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { BinaryExpression, FunctionDeclaration, MemberCall, MethodMember, TypeReference, UnaryExpression, isBinaryExpression, isBooleanLiteral, isClass, isClassMember, isFieldMember, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from '../generated/ast.js'; /* eslint-disable @typescript-eslint/no-unused-vars */ export class LoxTypeCreator extends AbstractLangiumTypeCreator { protected readonly typir: TypirServices; - protected readonly primitiveKind: PrimitiveKind; - protected readonly functionKind: FunctionKind; - protected readonly classKind: ClassKind; - protected readonly anyKind: TopKind; - protected readonly operators: OperatorManager; constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { super(typirServices, langiumServices); this.typir = typirServices; - - this.primitiveKind = new PrimitiveKind(this.typir); - this.functionKind = new FunctionKind(this.typir); - this.classKind = new ClassKind(this.typir, { - typing: 'Nominal', - }); - this.anyKind = new TopKind(this.typir); - this.operators = this.typir.operators; } onInitialize(): void { // primitive types // typeBool, typeNumber and typeVoid are specific types for OX, ... - const typeBool = this.primitiveKind.createPrimitiveType({ primitiveName: 'boolean', + const typeBool = this.typir.factory.primitives.create({ primitiveName: 'boolean', inferenceRules: [ isBooleanLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'boolean' ]}); // ... but their primitive kind is provided/preset by Typir - const typeNumber = this.primitiveKind.createPrimitiveType({ primitiveName: 'number', + const typeNumber = this.typir.factory.primitives.create({ primitiveName: 'number', inferenceRules: [ isNumberLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'number' ]}); - const typeString = this.primitiveKind.createPrimitiveType({ primitiveName: 'string', + const typeString = this.typir.factory.primitives.create({ primitiveName: 'string', inferenceRules: [ isStringLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'string' ]}); - const typeVoid = this.primitiveKind.createPrimitiveType({ primitiveName: 'void', + const typeVoid = this.typir.factory.primitives.create({ primitiveName: 'void', inferenceRules: [ (node: unknown) => isTypeReference(node) && node.primitive === 'void', isPrintStatement, (node: unknown) => isReturnStatement(node) && node.value === undefined ] }); - const typeNil = this.primitiveKind.createPrimitiveType({ primitiveName: 'nil', + const typeNil = this.typir.factory.primitives.create({ primitiveName: 'nil', inferenceRules: isNilLiteral }); // From "Crafting Interpreters" no value, like null in other languages. Uninitialised variables default to nil. When the execution reaches the end of the block of a function body without hitting a return, nil is implicitly returned. - const typeAny = this.anyKind.createTopType({}); + const typeAny = this.typir.factory.top.create({}); // extract inference rules, which is possible here thanks to the unified structure of the Langium grammar (but this is not possible in general!) const binaryInferenceRule: InferOperatorWithMultipleOperands = { @@ -75,9 +62,9 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // binary operators: numbers => number for (const operator of ['-', '*', '/']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); } - this.operators.createBinaryOperator({ name: '+', signature: [ + this.typir.factory.operators.createBinary({ name: '+', signature: [ { left: typeNumber, right: typeNumber, return: typeNumber }, { left: typeString, right: typeString, return: typeString }, { left: typeNumber, right: typeString, return: typeString }, @@ -86,24 +73,24 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // binary operators: numbers => boolean for (const operator of ['<', '<=', '>', '>=']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); } // binary operators: booleans => boolean for (const operator of ['and', 'or']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); } // ==, != for all data types (the warning for different types is realized below) for (const operator of ['==', '!=']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeAny, right: typeAny, return: typeBool }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeAny, right: typeAny, return: typeBool }, inferenceRule: binaryInferenceRule }); } // = for SuperType = SubType (TODO integrate the validation here? should be replaced!) - this.operators.createBinaryOperator({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule }); // unary operators - this.operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); - this.operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + this.typir.factory.operators.createUnary({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); + this.typir.factory.operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); // additional inference rules for ... this.typir.inference.addInferenceRule((domainElement: unknown) => { @@ -208,7 +195,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // function types: they have to be updated after each change of the Langium document, since they are derived from FunctionDeclarations! if (isFunctionDeclaration(node)) { - this.functionKind.createFunctionType(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar + this.typir.factory.functions.create(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar } // TODO support lambda (type references)! @@ -216,7 +203,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // class types (nominal typing): if (isClass(node)) { const className = node.name; - const classType = this.classKind.createClassType({ + const classType = this.typir.factory.classes.create({ className, superClasses: node.superClass?.ref, // note that type inference is used here fields: node.members @@ -250,7 +237,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // any class !== all classes; here we want to say, that 'nil' is assignable to each concrete Class type! // this.typir.conversion.markAsConvertible(typeNil, this.classKind.getOrCreateTopClassType({}), 'IMPLICIT_EXPLICIT'); classType.addListener(type => { - this.typir.conversion.markAsConvertible(this.primitiveKind.getPrimitiveType({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT'); + this.typir.conversion.markAsConvertible(this.typir.factory.primitives.get({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT'); }); } } diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index ba7a6e7..271988a 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -6,39 +6,32 @@ import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; import { LangiumSharedServices } from 'langium/lsp'; -import { CreateParameterDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, PrimitiveKind, TypirServices, UniqueFunctionValidation } from 'typir'; +import { CreateParameterDetails, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, TypirServices, UniqueFunctionValidation } from 'typir'; import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { ValidationMessageDetails } from '../../../../packages/typir/lib/services/validation.js'; import { BinaryExpression, MemberCall, UnaryExpression, isAssignmentStatement, isBinaryExpression, isBooleanLiteral, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isNumberLiteral, isParameter, isReturnStatement, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from './generated/ast.js'; export class OxTypeCreator extends AbstractLangiumTypeCreator { protected readonly typir: TypirServices; - protected readonly primitiveKind: PrimitiveKind; - protected readonly functionKind: FunctionKind; - protected readonly operators: OperatorManager; constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { super(typirServices, langiumServices); this.typir = typirServices; - - this.primitiveKind = new PrimitiveKind(this.typir); - this.functionKind = new FunctionKind(this.typir); - this.operators = this.typir.operators; } onInitialize(): void { // define primitive types // typeBool, typeNumber and typeVoid are specific types for OX, ... - const typeBool = this.primitiveKind.createPrimitiveType({ primitiveName: 'boolean', inferenceRules: [ + const typeBool = this.typir.factory.primitives.create({ primitiveName: 'boolean', inferenceRules: [ isBooleanLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'boolean', ]}); // ... but their primitive kind is provided/preset by Typir - const typeNumber = this.primitiveKind.createPrimitiveType({ primitiveName: 'number', inferenceRules: [ + const typeNumber = this.typir.factory.primitives.create({ primitiveName: 'number', inferenceRules: [ isNumberLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'number', ]}); - const typeVoid = this.primitiveKind.createPrimitiveType({ primitiveName: 'void', inferenceRules: + const typeVoid = this.typir.factory.primitives.create({ primitiveName: 'void', inferenceRules: (node: unknown) => isTypeReference(node) && node.primitive === 'void' }); @@ -57,29 +50,29 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { // define operators // binary operators: numbers => number for (const operator of ['+', '-', '*', '/']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); } // TODO better name: overloads, overloadRules, selectors, signatures // TODO better name for "inferenceRule": astSelectors // binary operators: numbers => boolean for (const operator of ['<', '<=', '>', '>=']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); } // binary operators: booleans => boolean for (const operator of ['and', 'or']) { - this.operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); + this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); } // ==, != for booleans and numbers for (const operator of ['==', '!=']) { - this.operators.createBinaryOperator({ name: operator, signature: [ + this.typir.factory.operators.createBinary({ name: operator, signature: [ { left: typeNumber, right: typeNumber, return: typeBool }, { left: typeBool, right: typeBool, return: typeBool }, ], inferenceRule: binaryInferenceRule }); } // unary operators - this.operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); - this.operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + this.typir.factory.operators.createUnary({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); + this.typir.factory.operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); /** Hints regarding the order of Typir configurations for OX: * - In general, Typir aims to not depend on the order of configurations. @@ -171,7 +164,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { if (isFunctionDeclaration(domainElement)) { const functionName = domainElement.name; // define function type - this.functionKind.createFunctionType({ + this.typir.factory.functions.create({ functionName, // note that the following two lines internally use type inference here in order to map language types to Typir types outputParameter: { name: NO_PARAMETER_NAME, type: domainElement.returnType }, diff --git a/packages/typir/src/kinds/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index eb59b4c..97366cb 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -23,7 +23,12 @@ export type InferBottomType = (domainElement: unknown) => boolean; export const BottomKindName = 'BottomKind'; -export class BottomKind implements Kind { +export interface BottomFactoryService { + create(typeDetails: BottomTypeDetails): BottomType; + get(typeDetails: BottomTypeDetails): BottomType | undefined; +} + +export class BottomKind implements Kind, BottomFactoryService { readonly $name: 'BottomKind'; readonly services: TypirServices; readonly options: Readonly; @@ -41,22 +46,13 @@ export class BottomKind implements Kind { }; } - getBottomType(typeDetails: BottomTypeDetails): BottomType | undefined { + get(typeDetails: BottomTypeDetails): BottomType | undefined { const key = this.calculateIdentifier(typeDetails); return this.services.graph.getType(key) as BottomType; } - getOrCreateBottomType(typeDetails: BottomTypeDetails): BottomType { - const bottomType = this.getBottomType(typeDetails); - if (bottomType) { - this.registerInferenceRules(typeDetails, bottomType); - return bottomType; - } - return this.createBottomType(typeDetails); - } - - createBottomType(typeDetails: BottomTypeDetails): BottomType { - assertTrue(this.getBottomType(typeDetails) === undefined); + create(typeDetails: BottomTypeDetails): BottomType { + assertTrue(this.get(typeDetails) === undefined); // create the bottom type (singleton) if (this.instance) { // note, that the given inference rules are ignored in this case! diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index 7ca61f6..415efd3 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -5,19 +5,19 @@ ******************************************************************************/ import { assertUnreachable } from 'langium'; -import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference, resolveTypeSelector } from '../../initialization/type-reference.js'; import { TypeSelector } from '../../initialization/type-selector.js'; +import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, assertType, toArray } from '../../utils/utils.js'; -import { CreateFunctionTypeDetails, FunctionKind, FunctionKindName, isFunctionKind } from '../function/function-kind.js'; +import { CreateFunctionTypeDetails, FunctionFactoryService } from '../function/function-kind.js'; import { Kind, isKind } from '../kind.js'; +import { ClassTypeInitializer } from './class-initializer.js'; +import { ClassType, isClassType } from './class-type.js'; import { TopClassKind, TopClassKindName, TopClassTypeDetails, isTopClassKind } from './top-class-kind.js'; import { TopClassType } from './top-class-type.js'; -import { ClassType, isClassType } from './class-type.js'; -import { ClassTypeInitializer } from './class-initializer.js'; export interface ClassKindOptions { typing: 'Structural' | 'Nominal', // JS classes are nominal, TS structures are structural @@ -58,6 +58,11 @@ export type InferClassLiteral = { }; +export interface ClassFactoryService { + create(typeDetails: CreateClassTypeDetails): TypeInitializer; + get(typeDetails: ClassTypeDetails | string): TypeReference; +} + /** * Classes have a name and have an arbitrary number of fields, consisting of a name and a type, and an arbitrary number of super-classes. * Fields have exactly one type and no multiplicity (which can be realized with a type of kind 'MultiplicityKind'). @@ -65,7 +70,7 @@ export type InferClassLiteral = { * The field name is used to identify fields of classes. * The order of fields is not defined, i.e. there is no order of fields. */ -export class ClassKind implements Kind { +export class ClassKind implements Kind, ClassFactoryService { readonly $name: 'ClassKind'; readonly services: TypirServices; readonly options: Readonly; @@ -91,7 +96,7 @@ export class ClassKind implements Kind { * @param typeDetails all information needed to identify the class * @returns a reference to the class type, which might be resolved in the future, if the class type does not yet exist */ - getClassType(typeDetails: ClassTypeDetails | string): TypeReference { // string for nominal typing + get(typeDetails: ClassTypeDetails | string): TypeReference { // string for nominal typing if (typeof typeDetails === 'string') { // nominal typing return new TypeReference(typeDetails, this.services); @@ -108,7 +113,7 @@ export class ClassKind implements Kind { * @param typeDetails all information needed to create a new class * @returns an initializer which creates and returns the new class type, when all depending types are resolved */ - createClassType(typeDetails: CreateClassTypeDetails): TypeInitializer { + create(typeDetails: CreateClassTypeDetails): TypeInitializer { return new ClassTypeInitializer(this.services, this, typeDetails); } @@ -138,10 +143,10 @@ export class ClassKind implements Kind { .sort() // the order of fields does not matter, therefore we need a stable order to make the identifiers comparable .join(','); // methods - const functionKind = this.getMethodKind(); + const functionFactory = this.getMethodFactory(); const methods: string = typeDetails.methods .map(createMethodDetails => { - return functionKind.calculateIdentifier(createMethodDetails); // reuse the Identifier for Functions here! + return functionFactory.calculateIdentifier(createMethodDetails); // reuse the Identifier for Functions here! }) .sort() // the order of methods does not matter, therefore we need a stable order to make the identifiers comparable .join(','); @@ -175,10 +180,8 @@ export class ClassKind implements Kind { return `${this.getIdentifierPrefix()}${typeDetails.className}`; } - getMethodKind(): FunctionKind { - // ensure, that Typir uses the predefined 'function' kind for methods - const kind = this.services.kinds.get(FunctionKindName); - return isFunctionKind(kind) ? kind : new FunctionKind(this.services); + getMethodFactory(): FunctionFactoryService { + return this.services.factory.functions; } getOrCreateTopClassType(typeDetails: TopClassTypeDetails): TopClassType { diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index 8384fde..cc98223 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -88,7 +88,7 @@ export class ClassType extends Type { // resolve methods this.methods = typeDetails.methods.map(method => { - type: new TypeReference(kind.getMethodKind().createFunctionType(method), kind.services), + type: new TypeReference(kind.getMethodFactory().create(method), kind.services), }); const refMethods = this.methods.map(m => m.type); // the uniqueness of methods can be checked with the predefined UniqueMethodValidation below diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index fd430e2..1720dfc 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -4,14 +4,14 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { CompositeTypeInferenceRule } from '../../services/inference.js'; -import { ValidationProblem } from '../../services/validation.js'; import { TypeEdge } from '../../graph/type-edge.js'; import { TypeGraphListener } from '../../graph/type-graph.js'; import { Type } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference, resolveTypeSelector } from '../../initialization/type-reference.js'; import { TypeSelector } from '../../initialization/type-selector.js'; +import { CompositeTypeInferenceRule } from '../../services/inference.js'; +import { ValidationProblem } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { NameTypePair } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy, checkTypes, checkValueForConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; @@ -96,6 +96,12 @@ export type InferFunctionCall = { */ +export interface FunctionFactoryService { + create(typeDetails: CreateFunctionTypeDetails): TypeInitializer; + get(typeDetails: FunctionTypeDetails): TypeReference; + calculateIdentifier(typeDetails: FunctionTypeDetails): string; +} + /** * Represents signatures of executable code. * @@ -112,7 +118,7 @@ export type InferFunctionCall = { * - optional parameters * - parameters which are used for output AND input */ -export class FunctionKind implements Kind, TypeGraphListener { +export class FunctionKind implements Kind, TypeGraphListener, FunctionFactoryService { readonly $name: 'FunctionKind'; readonly services: TypirServices; readonly options: Readonly; @@ -234,11 +240,11 @@ export class FunctionKind implements Kind, TypeGraphListener { ); } - getFunctionType(typeDetails: FunctionTypeDetails): TypeReference { + get(typeDetails: FunctionTypeDetails): TypeReference { return new TypeReference(() => this.calculateIdentifier(typeDetails), this.services); } - createFunctionType(typeDetails: CreateFunctionTypeDetails): TypeInitializer { + create(typeDetails: CreateFunctionTypeDetails): TypeInitializer { return new FunctionTypeInitializer(this.services, this, typeDetails); } diff --git a/packages/typir/src/kinds/primitive/primitive-kind.ts b/packages/typir/src/kinds/primitive/primitive-kind.ts index 7009157..74f73f1 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -20,7 +20,12 @@ export type InferPrimitiveType = (domainElement: unknown) => boolean; export const PrimitiveKindName = 'PrimitiveKind'; -export class PrimitiveKind implements Kind { +export interface PrimitiveFactoryService { + create(typeDetails: PrimitiveTypeDetails): PrimitiveType; + get(typeDetails: PrimitiveTypeDetails): PrimitiveType | undefined; +} + +export class PrimitiveKind implements Kind, PrimitiveFactoryService { readonly $name: 'PrimitiveKind'; readonly services: TypirServices; @@ -30,22 +35,13 @@ export class PrimitiveKind implements Kind { this.services.kinds.register(this); } - getPrimitiveType(typeDetails: PrimitiveTypeDetails): PrimitiveType | undefined { + get(typeDetails: PrimitiveTypeDetails): PrimitiveType | undefined { const key = this.calculateIdentifier(typeDetails); return this.services.graph.getType(key) as PrimitiveType; } - getOrCreatePrimitiveType(typeDetails: PrimitiveTypeDetails): PrimitiveType { - const primitiveType = this.getPrimitiveType(typeDetails); - if (primitiveType) { - this.registerInferenceRules(typeDetails, primitiveType); - return primitiveType; - } - return this.createPrimitiveType(typeDetails); - } - - createPrimitiveType(typeDetails: PrimitiveTypeDetails): PrimitiveType { - assertTrue(this.getPrimitiveType(typeDetails) === undefined); + create(typeDetails: PrimitiveTypeDetails): PrimitiveType { + assertTrue(this.get(typeDetails) === undefined); // create the primitive type const primitiveType = new PrimitiveType(this, this.calculateIdentifier(typeDetails)); diff --git a/packages/typir/src/kinds/top/top-kind.ts b/packages/typir/src/kinds/top/top-kind.ts index b5660d6..f9a387b 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -23,7 +23,12 @@ export type InferTopType = (domainElement: unknown) => boolean; export const TopKindName = 'TopKind'; -export class TopKind implements Kind { +export interface TopFactoryService { + create(typeDetails: TopTypeDetails): TopType; + get(typeDetails: TopTypeDetails): TopType | undefined; +} + +export class TopKind implements Kind, TopFactoryService { readonly $name: 'TopKind'; readonly services: TypirServices; readonly options: Readonly; @@ -41,22 +46,13 @@ export class TopKind implements Kind { }; } - getTopType(typeDetails: TopTypeDetails): TopType | undefined { + get(typeDetails: TopTypeDetails): TopType | undefined { const key = this.calculateIdentifier(typeDetails); return this.services.graph.getType(key) as TopType; } - getOrCreateTopType(typeDetails: TopTypeDetails): TopType { - const topType = this.getTopType(typeDetails); - if (topType) { - this.registerInferenceRules(typeDetails, topType); - return topType; - } - return this.createTopType(typeDetails); - } - - createTopType(typeDetails: TopTypeDetails): TopType { - assertTrue(this.getTopType(typeDetails) === undefined); + create(typeDetails: TopTypeDetails): TopType { + assertTrue(this.get(typeDetails) === undefined); // create the top type (singleton) if (this.instance) { diff --git a/packages/typir/src/services/operator.ts b/packages/typir/src/services/operator.ts index 8848135..83fc502 100644 --- a/packages/typir/src/services/operator.ts +++ b/packages/typir/src/services/operator.ts @@ -5,9 +5,9 @@ ******************************************************************************/ import { Type } from '../graph/type-node.js'; -import { FunctionKind, FunctionKindName, isFunctionKind, NO_PARAMETER_NAME } from '../kinds/function/function-kind.js'; -import { TypirServices } from '../typir.js'; import { TypeInitializer } from '../initialization/type-initializer.js'; +import { FunctionFactoryService, NO_PARAMETER_NAME } from '../kinds/function/function-kind.js'; +import { TypirServices } from '../typir.js'; import { NameTypePair, TypeInitializers } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; @@ -66,12 +66,12 @@ export interface GenericOperatorDetails { // TODO rename it to "OperatorFactory", when there are no more responsibilities! export interface OperatorManager { - createUnaryOperator(typeDetails: UnaryOperatorDetails): TypeInitializers - createBinaryOperator(typeDetails: BinaryOperatorDetails): TypeInitializers - createTernaryOperator(typeDetails: TernaryOperatorDetails): TypeInitializers + createUnary(typeDetails: UnaryOperatorDetails): TypeInitializers + createBinary(typeDetails: BinaryOperatorDetails): TypeInitializers + createTernary(typeDetails: TernaryOperatorDetails): TypeInitializers /** This function allows to create a single operator with arbitrary input operands. */ - createGenericOperator(typeDetails: GenericOperatorDetails): TypeInitializer; + createGeneric(typeDetails: GenericOperatorDetails): TypeInitializer; } /** @@ -99,11 +99,11 @@ export class DefaultOperatorManager implements OperatorManager { this.services = services; } - createUnaryOperator(typeDetails: UnaryOperatorDetails): TypeInitializers { + createUnary(typeDetails: UnaryOperatorDetails): TypeInitializers { const signatures = toArray(typeDetails.signature); const result: Array> = []; for (const signature of signatures) { - result.push(this.createGenericOperator({ + result.push(this.createGeneric({ name: typeDetails.name, outputType: signature.return, inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! @@ -115,11 +115,11 @@ export class DefaultOperatorManager implements OperatorManager { return result.length === 1 ? result[0] : result; } - createBinaryOperator(typeDetails: BinaryOperatorDetails): TypeInitializers { + createBinary(typeDetails: BinaryOperatorDetails): TypeInitializers { const signatures = toArray(typeDetails.signature); const result: Array> = []; for (const signature of signatures) { - result.push(this.createGenericOperator({ + result.push(this.createGeneric({ name: typeDetails.name, outputType: signature.return, inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! @@ -132,11 +132,11 @@ export class DefaultOperatorManager implements OperatorManager { return result.length === 1 ? result[0] : result; } - createTernaryOperator(typeDetails: TernaryOperatorDetails): TypeInitializers { + createTernary(typeDetails: TernaryOperatorDetails): TypeInitializers { const signatures = toArray(typeDetails.signature); const result: Array> = []; for (const signature of signatures) { - result.push(this.createGenericOperator({ + result.push(this.createGeneric({ name: typeDetails.name, outputType: signature.return, inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! @@ -150,12 +150,12 @@ export class DefaultOperatorManager implements OperatorManager { return result.length === 1 ? result[0] : result; } - createGenericOperator(typeDetails: GenericOperatorDetails): TypeInitializer { + createGeneric(typeDetails: GenericOperatorDetails): TypeInitializer { // define/register the wanted operator as "special" function - const functionKind = this.getFunctionKind(); + const functionFactory = this.getFunctionFactory(); // create the operator as type of kind 'function' - const newOperatorType = functionKind.createFunctionType({ + const newOperatorType = functionFactory.create({ functionName: typeDetails.name, outputParameter: { name: NO_PARAMETER_NAME, type: typeDetails.outputType }, inputParameters: typeDetails.inputParameter, @@ -174,9 +174,7 @@ export class DefaultOperatorManager implements OperatorManager { return newOperatorType as unknown as TypeInitializer; } - protected getFunctionKind(): FunctionKind { - // ensure, that Typir uses the predefined 'function' kind - const kind = this.services.kinds.get(FunctionKindName); - return isFunctionKind(kind) ? kind : new FunctionKind(this.services); + protected getFunctionFactory(): FunctionFactoryService { + return this.services.factory.functions; } } diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index c0fdb83..ee10ee4 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -4,17 +4,22 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { TypeGraph } from './graph/type-graph.js'; +import { BottomFactoryService, BottomKind } from './kinds/bottom/bottom-kind.js'; +import { ClassFactoryService, ClassKind } from './kinds/class/class-kind.js'; +import { FunctionKind, FunctionFactoryService } from './kinds/function/function-kind.js'; +import { PrimitiveFactoryService, PrimitiveKind } from './kinds/primitive/primitive-kind.js'; +import { TopFactoryService, TopKind } from './kinds/top/top-kind.js'; import { DefaultTypeAssignability, TypeAssignability } from './services/assignability.js'; import { DefaultDomainElementInferenceCaching, DefaultTypeRelationshipCaching, DomainElementInferenceCaching, TypeRelationshipCaching } from './services/caching.js'; import { DefaultTypeConversion, TypeConversion } from './services/conversion.js'; import { DefaultTypeEquality, TypeEquality } from './services/equality.js'; import { DefaultTypeInferenceCollector, TypeInferenceCollector } from './services/inference.js'; +import { DefaultKindRegistry, KindRegistry } from './services/kind-registry.js'; import { DefaultOperatorManager, OperatorManager } from './services/operator.js'; import { DefaultTypeConflictPrinter, ProblemPrinter } from './services/printing.js'; import { DefaultSubType, SubType } from './services/subtype.js'; import { DefaultValidationCollector, DefaultValidationConstraints, ValidationCollector, ValidationConstraints } from './services/validation.js'; -import { TypeGraph } from './graph/type-graph.js'; -import { DefaultKindRegistry, KindRegistry } from './services/kind-registry.js'; import { inject, Module } from './utils/dependency-injection.js'; /** @@ -48,12 +53,19 @@ export type TypirServices = { }; readonly graph: TypeGraph; readonly kinds: KindRegistry; - readonly operators: OperatorManager; readonly printer: ProblemPrinter; readonly validation: { readonly collector: ValidationCollector; readonly constraints: ValidationConstraints; }; + readonly factory: { + readonly primitives: PrimitiveFactoryService; + readonly functions: FunctionFactoryService; + readonly classes: ClassFactoryService; + readonly top: TopFactoryService; + readonly bottom: BottomFactoryService; + readonly operators: OperatorManager; + }; }; export const DefaultTypirServiceModule: Module = { @@ -67,13 +79,20 @@ export const DefaultTypirServiceModule: Module = { typeRelationships: (services) => new DefaultTypeRelationshipCaching(services), domainElementInference: () => new DefaultDomainElementInferenceCaching() }, - operators: (services) => new DefaultOperatorManager(services), kinds: () => new DefaultKindRegistry(), printer: () => new DefaultTypeConflictPrinter(), validation: { collector: (services) => new DefaultValidationCollector(services), constraints: (services) => new DefaultValidationConstraints(services), }, + factory: { + primitives: (services) => new PrimitiveKind(services), + functions: (services) => new FunctionKind(services), + classes: (services) => new ClassKind(services, { typing: 'Nominal' }), + top: (services) => new TopKind(services), + bottom: (services) => new BottomKind(services), + operators: (services) => new DefaultOperatorManager(services), + }, }; /** diff --git a/packages/typir/test/api-example.test.ts b/packages/typir/test/api-example.test.ts new file mode 100644 index 0000000..9b2548a --- /dev/null +++ b/packages/typir/test/api-example.test.ts @@ -0,0 +1,145 @@ +/****************************************************************************** + * Copyright 2024 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 { describe, expect, test } from 'vitest'; +import { InferenceRuleNotApplicable, InferOperatorWithMultipleOperands, ValidationMessageDetails } from '../src/index.js'; +import { createTypirServices } from '../src/typir.js'; + +describe('Tests for the new API', () => { + test('Experiments', async () => { + const typir = createTypirServices(); + + const booleanType = typir.factory.primitives.create({ primitiveName: 'boolean' }); + expect(booleanType).toBeTruthy(); + const getBool = typir.factory.primitives.get({ primitiveName: 'boolean' }); + expect(getBool).toBe(booleanType); + + typir.factory.functions.create({ functionName: 'myFunction', inputParameters: [], outputParameter: undefined }); + + // operators + typir.factory.operators.createBinary({ name: '&&', signature: [{ left: booleanType, right: booleanType, return: booleanType }] }); + // typir.operators.createBinary({ name: '&&', signature: [{ left: booleanType, right: booleanType, return: booleanType }] }); // TODO entfernen! + }); + + + test('Tiny Typir', async () => { + const typir = createTypirServices(); // set-up the type system + + // primitive types + const numberType = typir.factory.primitives.create({ primitiveName: 'number', inferenceRules: node => node instanceof NumberLiteral }); + const stringType = typir.factory.primitives.create({ primitiveName: 'string', inferenceRules: node => node instanceof StringLiteral }); + + // operators + const inferenceRule: InferOperatorWithMultipleOperands = { + filter: node => node instanceof BinaryExpression, + matching: (node, operatorName) => node.operator === operatorName, + operands: node => [node.left, node.right], + }; + typir.factory.operators.createBinary({ name: '+', signature: [ // operator overloading + { left: numberType, right: numberType, return: numberType }, // 2 + 3 + { left: stringType, right: stringType, return: stringType }, // "2" + "3" + ], inferenceRule }); + typir.factory.operators.createBinary({ name: '-', signature: [{ left: numberType, right: numberType, return: numberType }], inferenceRule }); // 2 - 3 + + // numbers are implicitly convertable to strings + typir.conversion.markAsConvertible(numberType, stringType, 'IMPLICIT_EXPLICIT'); + + // specify, how Typir can detect the type of a variable + typir.inference.addInferenceRule(node => { + if (node instanceof Variable) { + return node.initialValue; // the type of the variable is the type of its initial value + } + return InferenceRuleNotApplicable; + }); + + // register a type-related validation + typir.validation.collector.addValidationRule(node => { + if (node instanceof AssignmentStatement) { + return typir.validation.constraints.ensureNodeIsAssignable(node.right, node.left, (actual, expected) => { message: + `The type '${actual.name}' is not assignable to the type '${expected.name}'.` }); + } + return []; + }); + + // 2 + 3 => OK + const example1 = new BinaryExpression(new NumberLiteral(2), '+', new NumberLiteral(3)); + expect(typir.validation.collector.validate(example1)).toHaveLength(0); + + // 2 + "3" => OK + const example2 = new BinaryExpression(new NumberLiteral(2), '+', new StringLiteral('3')); + expect(typir.validation.collector.validate(example2)).toHaveLength(0); + + // 2 - "3" => wrong + const example3 = new BinaryExpression(new NumberLiteral(2), '-', new StringLiteral('3')); + const errors1 = typir.validation.collector.validate(example3); + const errorStack = typir.printer.printTypirProblem(errors1[0]); // the problem comes with "sub-problems" to describe the reasons in more detail + expect(errorStack).includes("The parameter 'right' at index 1 got a value with a wrong type."); + expect(errorStack).includes("For property 'right', the types 'string' and 'number' do not match."); + + // 123 is assignable to a string variable + const varString = new Variable('v1', new StringLiteral('Hello')); + const assignNumberToString = new AssignmentStatement(varString, new NumberLiteral(123)); + expect(typir.validation.collector.validate(assignNumberToString)).toHaveLength(0); + + // "123" is not assignable to a number variable + const varNumber = new Variable('v2', new NumberLiteral(456)); + const assignStringToNumber = new AssignmentStatement(varNumber, new StringLiteral('123')); + const errors2 = typir.validation.collector.validate(assignStringToNumber); + expect(errors2[0].message).toBe("The type 'string' is not assignable to the type 'number'."); + }); + +}); + +abstract class AstElement { + // empty +} + +class NumberLiteral extends AstElement { + value: number; + constructor(value: number) { + super(); + this.value = value; + } +} +class StringLiteral extends AstElement { + value: string; + constructor(value: string) { + super(); + this.value = value; + } +} + +class BinaryExpression extends AstElement { + left: AstElement; + operator: string; + right: AstElement; + constructor(left: AstElement, operator: string, right: AstElement) { + super(); + this.left = left; + this.operator = operator; + this.right = right; + } +} + +class Variable extends AstElement { + name: string; + initialValue: AstElement; + constructor(name: string, initialValue: AstElement) { + super(); + this.name = name; + this.initialValue = initialValue; + } +} + +class AssignmentStatement extends AstElement { + left: Variable; + right: AstElement; + constructor(left: Variable, right: AstElement) { + super(); + this.left = left; + this.right = right; + } +} diff --git a/packages/typir/test/type-definitions.test.ts b/packages/typir/test/type-definitions.test.ts index b746c29..3d135a4 100644 --- a/packages/typir/test/type-definitions.test.ts +++ b/packages/typir/test/type-definitions.test.ts @@ -16,25 +16,27 @@ import { FixedParameterKind } from '../src/kinds/fixed-parameters/fixed-paramete describe('Tests for Typir', () => { test('Define some types', async () => { // start the type system - const typir = createTypirServices(); + const typir = createTypirServices({ + // customize some default factories for predefined types + factory: { + classes: (services) =>new ClassKind(typir, { typing: 'Structural', maximumNumberOfSuperClasses: 1, subtypeFieldChecking: 'SUB_TYPE' }), + }, + }); // reuse predefined kinds - const primitiveKind = new PrimitiveKind(typir); const multiplicityKind = new MultiplicityKind(typir, { symbolForUnlimited: '*' }); - const classKind = new ClassKind(typir, { typing: 'Structural', maximumNumberOfSuperClasses: 1, subtypeFieldChecking: 'SUB_TYPE' }); const listKind = new FixedParameterKind(typir, 'List', { parameterSubtypeCheckingStrategy: 'EQUAL_TYPE' }, 'entry'); const mapKind = new FixedParameterKind(typir, 'Map', { parameterSubtypeCheckingStrategy: 'EQUAL_TYPE' }, 'key', 'value'); - const functionKind = new FunctionKind(typir); // create some primitive types - const typeInt = primitiveKind.createPrimitiveType({ primitiveName: 'Integer' }); - const typeString = primitiveKind.createPrimitiveType({ primitiveName: 'String', + const typeInt = typir.factory.primitives.create({ primitiveName: 'Integer' }); + const typeString = typir.factory.primitives.create({ primitiveName: 'String', inferenceRules: domainElement => typeof domainElement === 'string'}); // combine type definition with a dedicated inference rule for it - const typeBoolean = primitiveKind.createPrimitiveType({ primitiveName: 'Boolean' }); + const typeBoolean = typir.factory.primitives.create({ primitiveName: 'Boolean' }); // create class type Person with 1 firstName and 1..2 lastNames and an age properties const typeOneOrTwoStrings = multiplicityKind.createMultiplicityType({ constrainedType: typeString, lowerBound: 1, upperBound: 2 }); - const typePerson = classKind.createClassType({ + const typePerson = typir.factory.classes.create({ className: 'Person', fields: [ { name: 'firstName', type: typeString }, @@ -44,7 +46,7 @@ describe('Tests for Typir', () => { methods: [], }); console.log(typePerson.getTypeFinal()!.getUserRepresentation()); - const typeStudent = classKind.createClassType({ + const typeStudent = typir.factory.classes.create({ className: 'Student', superClasses: typePerson, // a Student is a special Person fields: [ @@ -57,34 +59,34 @@ describe('Tests for Typir', () => { const typeListInt = listKind.createFixedParameterType({ parameterTypes: typeInt }); const typeListString = listKind.createFixedParameterType({ parameterTypes: typeString }); // const typeMapStringPerson = mapKind.createFixedParameterType({ parameterTypes: [typeString, typePerson] }); - const typeFunctionStringLength = functionKind.createFunctionType({ + const typeFunctionStringLength = typir.factory.functions.create({ functionName: 'length', outputParameter: { name: NO_PARAMETER_NAME, type: typeInt }, inputParameters: [{ name: 'value', type: typeString }] }); // binary operators on Integers - const opAdd = typir.operators.createBinaryOperator({ name: '+', signature: { left: typeInt, right: typeInt, return: typeInt } }); - const opMinus = typir.operators.createBinaryOperator({ name: '-', signature: { left: typeInt, right: typeInt, return: typeInt } }); - const opLess = typir.operators.createBinaryOperator({ name: '<', signature: { left: typeInt, right: typeInt, return: typeBoolean } }); - const opEqualInt = typir.operators.createBinaryOperator({ name: '==', signature: { left: typeInt, right: typeInt, return: typeBoolean }, + const opAdd = typir.factory.operators.createBinary({ name: '+', signature: { left: typeInt, right: typeInt, return: typeInt } }); + const opMinus = typir.factory.operators.createBinary({ name: '-', signature: { left: typeInt, right: typeInt, return: typeInt } }); + const opLess = typir.factory.operators.createBinary({ name: '<', signature: { left: typeInt, right: typeInt, return: typeBoolean } }); + const opEqualInt = typir.factory.operators.createBinary({ name: '==', signature: { left: typeInt, right: typeInt, return: typeBoolean }, inferenceRule: { filter: (domainElement): domainElement is string => typeof domainElement === 'string', matching: domainElement => domainElement.includes('=='), operands: domainElement => [] }}); // binary operators on Booleans - const opEqualBool = typir.operators.createBinaryOperator({ name: '==', signature: { left: typeBoolean, right: typeBoolean, return: typeBoolean } }); - const opAnd = typir.operators.createBinaryOperator({ name: '&&', signature: { left: typeBoolean, right: typeBoolean, return: typeBoolean } }); + const opEqualBool = typir.factory.operators.createBinary({ name: '==', signature: { left: typeBoolean, right: typeBoolean, return: typeBoolean } }); + const opAnd = typir.factory.operators.createBinary({ name: '&&', signature: { left: typeBoolean, right: typeBoolean, return: typeBoolean } }); // unary operators - const opNotBool = typir.operators.createUnaryOperator({ name: '!', signature: { operand: typeBoolean, return: typeBoolean }, + const opNotBool = typir.factory.operators.createUnary({ name: '!', signature: { operand: typeBoolean, return: typeBoolean }, inferenceRule: { filter: (domainElement): domainElement is string => typeof domainElement === 'string', matching: domainElement => domainElement.includes('NOT'), operand: domainElement => [] }}); // ternary operator - const opTernaryIf = typir.operators.createTernaryOperator({ name: 'if', signature: { first: typeBoolean, second: typeInt, third: typeInt, return: typeInt } }); + const opTernaryIf = typir.factory.operators.createTernary({ name: 'if', signature: { first: typeBoolean, second: typeInt, third: typeInt, return: typeInt } }); // automated conversion from int to string // it is possible to define multiple sources and/or targets at the same time: