diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7f42758 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Typir Change Log + +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.0 (December 2024) + +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! + +- [Linked issues and PRs](https://github.com/TypeFox/typir/milestone/2) +- Core implementations of the following [type-checking services](/packages/typir/src/services/): + - Assignability + - Equality + - Conversion (implicit/coercion and explicit/casting) + - Type inference + - Sub-typing + - Validation + - Caching +- [Predefined types](/packages/typir/src/kinds/) to reuse: + - Primitives + - Functions (with overloading) + - Classes (nominally typed) + - Top, bottom + - (some more are under development) + - Operators (which are mapped to Functions, with overloading) +- Application examples: + - LOX (without lambdas) + - OX diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00adde6..b5f9041 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,20 @@ # Contributing TODO + + +## Communication + +The following communication channels are available: + +- [GitHub issues](https://github.com/TypeFox/typir/issues) - for bug reports, feature requests, etc. +- [GitHub discussions](https://github.com/TypeFox/typir/discussions) - for questions, ideas, announcements, etc. +- [Weekly Langium dev meeting](https://github.com/eclipse-langium/langium/discussions/564?sort=new) - While Typir is independent from Langium in general, you might meet some Typir developers at the Langium dev meetings. + +In case you have a question, please look into the provided resources and documentations first. +If you don't find any answer there, feel free to use the discussions to get help. + + +## Release Process + +The release process for Typir is described in [RELEASE.md](./RELEASE.md). diff --git a/README.md b/README.md index 7cc3d26..7c7b070 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,82 @@ # Typir -Engineering types for software languages in the web. +
+ + [![npm](https://img.shields.io/npm/v/typir)](https://www.npmjs.com/package/typir) + [![Build](https://github.com/TypeFox/typir/actions/workflows/actions.yml/badge.svg)](https://github.com/TypeFox/typir/actions/workflows/actions.yml) + [![Github Discussions](https://img.shields.io/badge/github-discussions-blue?logo=github)](https://github.com/TypeFox/typir/discussions) + [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-FFAE33?logo=gitpod)](https://gitpod.io/#https://github.com/TypeFox/typir) + +
+ +--- + +Typir is a library for type systems and type checking for software languages in the web. + +Typir is OpenSource, written in TypeScript, and follows pragmatic approaches for easing type checking in practical language engineering projects by providing default implementations for recurring problems. +As a stand-alone library, Typir provides a TypeScript-API for language engineers without an additional, external DSL for formalizing types. + + +## Core Features + +Typir provides these core features: + +- Predefined types: + - primitives + - functions (with overloading) + - classes + - top, bottom + - (more are planned) +- Solutions for: circular type definitions, caching, operators +- Meaningful and customizable error messages +- The provided default implementations are customizable by dependency injection + +Typir does intentionally _not_ include ... + +- rules engines and constraint solving +- formal proofs +- external DSLs for formalizing types + ## NPM workspace This repository is a NPM workspace. It contains the following packages: -- [Typir](./packages/typir/README.md) - the core package of Typir -- [Typir-Langium](./packages/typir-langium/README.md) - a integration of Typir for [Langium](https://github.com/eclipse-langium/langium) +- [Typir](./packages/typir/README.md) - the core package of Typir with default implementations for type checking services and some predefined types +- [Typir-Langium](./packages/typir-langium/README.md) - a binding of Typir for [Langium](https://github.com/eclipse-langium/langium), a language workbench for developing textual DSLs in the web, +in order to ease type checking for Langium-based languages + +This repository contains the following stand-alone applications, which demonstrate how to use Typir for type checking: + +- [LOX](./examples/lox/README.md) - static type checking for LOX, implemented with Typir-Langium +- [OX](./examples/ox/README.md) - a reduced version of LOX, implemented with Typir-Langium + + +## Tiny Typir Example + +[TODO](/packages/typir/test/api-example.test.ts) + + +## Resources + +Typir is presented in these talks: + +- [LangDev'24](https://langdevcon.org/2024/program#26): [Video](https://www.youtube.com/watch?v=CL8EbJYeyTE), [slides](/resources/talks/2024-10-17-LangDev.pdf) (2024-10-17) +- [OCX/EclipseCon'24](https://www.ocxconf.org/event/778b82cc-6834-48a4-a58e-f883c5a7b8c9/agenda?session=23b97df9-0435-4fab-8a01-e0a9cf3e3831&shareLink=true): [Video](https://www.youtube.com/watch?v=WLzXAhcl-aY&list=PLy7t4z5SYNaRRGVdF83feN-_uHLwvGvgw&index=23), [slides](/resources/talks/2024-10-24-EclipseCon.pdf) (2024-10-24) + + +## Roadmap + +The roadmap of Typir is organized with [milestones in GitHub](https://github.com/TypeFox/typir/milestones). + +The roadmap include, among other, these features: + +- More predefined types: structurally typed classes, lambdas, generics, constrained primitive types (e.g. numbers with upper and lower bound), ... +- Calculate types, e.g. operators whose return types depend on their current input types +- Optimized APIs to register rules for inference and validation + +For the released versions of Typir, see the [CHANGELOG.md](/CHANGELOG.md). + ## Contributing @@ -15,6 +84,7 @@ Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of We also have a release process described in [RELEASE.md](./RELEASE.md). + ## License -[MIT License](/LICENSE) +Typir is fully [MIT licensed](/LICENSE). diff --git a/examples/lox/README.md b/examples/lox/README.md new file mode 100644 index 0000000..1b7de36 --- /dev/null +++ b/examples/lox/README.md @@ -0,0 +1,17 @@ +# Typir applied to LOX + +This package contains an adapted version of [LOX](https://craftinginterpreters.com/the-lox-language.html), [realized with Langium](https://github.com/TypeFox/langium-lox) and statically type-checked with [Typir](https://typir.org/). + +Typir is used here to make LOX a statically typed language: + +- Variables have one type, which is either explicitly declared (e.g. `var v1: string`) or derived from the initial value (e.g. `var v2: 2 <= 3`). +- Lox supports these types here: + - primitives: boolean, string, number, void + - Classes (nominally typed) + - Lambdas (not yet supported) +- We keep `nil`, but it can be assigned only to variables with a class or lambda as type. + Variables with primitive type and without explicit initial value have the primitive types default value. + +For examples written in LOX, look at some [collected examples](./examples/) or the [test cases](./test/). + +To compare the current implementation for type checking with Typir with an implementation without Typir, have a look into [this repository](https://github.com/TypeFox/langium-lox/tree/main/langium/src/language-server/type-system). diff --git a/examples/lox/examples/basic.lox b/examples/lox/examples/basic.lox index 9cd76e6..c1401af 100644 --- a/examples/lox/examples/basic.lox +++ b/examples/lox/examples/basic.lox @@ -88,34 +88,6 @@ fun returnSum(a: number, b: number): number { return a + b; } -// Closures - -fun identity(a: (number, number) => number): (number, number) => number { - return a; -} - -print identity(returnSum)(1, 2); // prints "3"; - -fun outerFunction(): void { - fun localFunction(): void { - print "I'm local!"; - } - localFunction(); -} - -fun returnFunction(): () => void { - var outside = "outside"; - - fun inner(): void { - print outside; - } - - return inner; -} - -var fn = returnFunction(); -fn(); - // Classes WIP class SuperClass { diff --git a/examples/lox/package.json b/examples/lox/package.json index bb34d7d..f3a072d 100644 --- a/examples/lox/package.json +++ b/examples/lox/package.json @@ -29,14 +29,14 @@ }, "dependencies": { "commander": "~12.1.0", - "langium": "~3.2.0", + "langium": "~3.3.0", "typir-langium": "~0.0.2", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" }, "devDependencies": { "@types/vscode": "~1.94.0", - "langium-cli": "~3.2.0" + "langium-cli": "~3.3.0" }, "files": [ "bin", diff --git a/examples/lox/src/language/lox-linker.ts b/examples/lox/src/language/lox-linker.ts new file mode 100644 index 0000000..ff4727e --- /dev/null +++ b/examples/lox/src/language/lox-linker.ts @@ -0,0 +1,63 @@ +/****************************************************************************** + * 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 { AstNodeDescription, DefaultLinker, LinkingError, ReferenceInfo } from 'langium'; +import { isType } from '../../../../packages/typir/lib/graph/type-node.js'; +import { TypirServices } from '../../../../packages/typir/lib/typir.js'; +import { isClass, isFunctionDeclaration, isMemberCall, isMethodMember } from './generated/ast.js'; +import { LoxServices } from './lox-module.js'; + +export class LoxLinker extends DefaultLinker { + protected readonly typir: TypirServices; + + constructor(services: LoxServices) { + super(services); + this.typir = services.typir; + } + + override getCandidate(refInfo: ReferenceInfo): AstNodeDescription | LinkingError { + const container = refInfo.container; + if (isMemberCall(container) && container.explicitOperationCall) { + // handle overloaded functions/methods + const scope = this.scopeProvider.getScope(refInfo); + const calledDescriptions = scope.getAllElements().filter(d => d.name === refInfo.reference.$refText).toArray(); // same name + if (calledDescriptions.length === 1) { + return calledDescriptions[0]; // no overloaded functions/methods + } if (calledDescriptions.length >= 2) { + // in case of overloaded functions/methods, do type inference for given arguments + const argumentTypes = container.arguments.map(arg => this.typir.Inference.inferType(arg)).filter(isType); + if (argumentTypes.length === container.arguments.length) { // for all given arguments, a type is inferred + for (const calledDescription of calledDescriptions) { + const called = this.loadAstNode(calledDescription); + if (isClass(called)) { + // special case: call of the constructur, without any arguments/parameters + return calledDescription; // there is only one constructor without any parameters + } + if ((isMethodMember(called) || isFunctionDeclaration(called)) && called.parameters.length === container.arguments.length) { // same number of arguments + // infer expected types of parameters + const parameterTypes = called.parameters.map(p => this.typir.Inference.inferType(p)).filter(isType); + if (parameterTypes.length === called.parameters.length) { // for all parameters, a type is inferred + if (argumentTypes.every((arg, index) => this.typir.Assignability.isAssignable(arg, parameterTypes[index]))) { + return calledDescription; + } + } + } + } + } + // no matching method is found, return the first found method => linking works + validation issues regarding the wrong parameter values can be shown! + return calledDescriptions[0]; + + // the following approach does not work, since the container's cross-references are required for type inference, but they are not yet resolved + // const type = this.typir.Inference.inferType(container); + // if (isFunctionType(type)) { + // return type.associatedDomainElement; + // } + } + return this.createLinkingError(refInfo); + } + return super.getCandidate(refInfo); + } +} diff --git a/examples/lox/src/language/lox-module.ts b/examples/lox/src/language/lox-module.ts index 3254ccf..5b9f410 100644 --- a/examples/lox/src/language/lox-module.ts +++ b/examples/lox/src/language/lox-module.ts @@ -4,13 +4,14 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { Module, PartialLangiumCoreServices, createDefaultCoreModule, inject } from 'langium'; +import { LangiumSharedCoreServices, Module, PartialLangiumCoreServices, createDefaultCoreModule, inject } from 'langium'; import { DefaultSharedModuleContext, LangiumServices, LangiumSharedServices, createDefaultSharedModule } from 'langium/lsp'; import { LangiumServicesForTypirBinding, createLangiumModuleForTypirBinding, initializeLangiumTypirServices } from 'typir-langium'; import { LoxGeneratedModule, LoxGeneratedSharedModule } from './generated/module.js'; import { LoxScopeProvider } from './lox-scope.js'; import { LoxValidationRegistry, LoxValidator } from './lox-validator.js'; -import { createLoxTypirModule } from './type-system/lox-type-checking.js'; +import { createLoxTypirModule } from './lox-type-checking.js'; +import { LoxLinker } from './lox-linker.js'; /** * Declaration of custom services - add your own service classes here. @@ -18,29 +19,38 @@ import { createLoxTypirModule } from './type-system/lox-type-checking.js'; export type LoxAddedServices = { validation: { LoxValidator: LoxValidator - } + }, + typir: LangiumServicesForTypirBinding, } /** * Union of Langium default services and your custom services - use this as constructor parameter * of custom service classes. */ -export type LoxServices = LangiumServices & LoxAddedServices & LangiumServicesForTypirBinding +export type LoxServices = LangiumServices & LoxAddedServices /** * Dependency injection module that overrides Langium default services and contributes the * declared custom services. The Langium defaults can be partially specified to override only * selected services, while the custom services must be fully specified. */ -export const LoxModule: Module = { - validation: { - ValidationRegistry: (services) => new LoxValidationRegistry(services), - LoxValidator: () => new LoxValidator() - }, - references: { - ScopeProvider: (services) => new LoxScopeProvider(services) - } -}; +export function createLoxModule(shared: LangiumSharedCoreServices): Module { + return { + validation: { + ValidationRegistry: (services) => new LoxValidationRegistry(services), + LoxValidator: () => new LoxValidator() + }, + // For type checking with Typir, inject and merge these modules: + typir: () => inject(Module.merge( + createLangiumModuleForTypirBinding(shared), // the Typir default services + createLoxTypirModule(shared), // custom Typir services for LOX + )), + references: { + ScopeProvider: (services) => new LoxScopeProvider(services), + Linker: (services) => new LoxLinker(services), + }, + }; +} /** * Create the full set of services required by Langium. @@ -53,9 +63,6 @@ export const LoxModule: Module isTypeReference(node) && node.primitive === 'boolean' ]}); // ... but their primitive kind is provided/preset by Typir - const typeNumber = this.typir.factory.primitives.create({ primitiveName: 'number', + const typeNumber = this.typir.factory.Primitives.create({ primitiveName: 'number', inferenceRules: [ isNumberLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'number' ]}); - const typeString = this.typir.factory.primitives.create({ primitiveName: 'string', + const typeString = this.typir.factory.Primitives.create({ primitiveName: 'string', inferenceRules: [ isStringLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'string' ]}); - const typeVoid = this.typir.factory.primitives.create({ 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.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.typir.factory.top.create({}); + const typeNil = this.typir.factory.Primitives.create({ primitiveName: 'nil', + inferenceRules: isNilLiteral }); // 'nil' is only assignable to variables with a class as type in the LOX implementation here + 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 = { @@ -62,9 +61,9 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // binary operators: numbers => number for (const operator of ['-', '*', '/']) { - this.typir.factory.operators.createBinary({ 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.typir.factory.operators.createBinary({ name: '+', signature: [ + this.typir.factory.Operators.createBinary({ name: '+', signatures: [ { left: typeNumber, right: typeNumber, return: typeNumber }, { left: typeString, right: typeString, return: typeString }, { left: typeNumber, right: typeString, return: typeString }, @@ -73,47 +72,58 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // binary operators: numbers => boolean for (const operator of ['<', '<=', '>', '>=']) { - this.typir.factory.operators.createBinary({ 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.typir.factory.operators.createBinary({ 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.typir.factory.operators.createBinary({ 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, + // show a warning to the user, if something like "3 == false" is compared, since different types already indicate, that the IF condition will be evaluated to false + validationRule: (node, _operatorName, _operatorType, typir) => typir.validation.Constraints.ensureNodeIsEquals(node.left, node.right, (actual, expected) => { + message: `This comparison will always return '${node.operator === '==' ? 'false' : 'true'}' as '${node.left.$cstNode?.text}' and '${node.right.$cstNode?.text}' have the different types '${actual.name}' and '${expected.name}'.`, + domainElement: node, // inside the BinaryExpression ... + domainProperty: 'operator', // ... mark the '==' or '!=' token, i.e. the 'operator' property + severity: 'warning' }), + // (The use of "node.right" and "node.left" without casting is possible, since the type checks of the given 'inferenceRule' are reused for the 'validationRule'. + // This approach saves the duplication of checks for inference and validation, but makes the validation rules depending on the inference rule.) + }); } - // = for SuperType = SubType (TODO integrate the validation here? should be replaced!) - this.typir.factory.operators.createBinary({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule }); + // = for SuperType = SubType (Note that this implementation of LOX realized assignments as operators!) + this.typir.factory.Operators.createBinary({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule, + // this validation will be checked for each call of this operator! + validationRule: (node, _opName, _opType, typir) => typir.validation.Constraints.ensureNodeIsAssignable(node.right, node.left, (actual, expected) => { + message: `The expression '${node.right.$cstNode?.text}' of type '${actual.name}' is not assignable to '${node.left.$cstNode?.text}' with type '${expected.name}'`, + domainProperty: 'value' }), + }); // unary operators - 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 }); + 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) => { + this.typir.Inference.addInferenceRule((domainElement: unknown) => { // ... member calls if (isMemberCall(domainElement)) { const ref = domainElement.element?.ref; if (isClass(ref)) { return InferenceRuleNotApplicable; // not required anymore - } else if (isClassMember(ref)) { - return InferenceRuleNotApplicable; // TODO + } else if (isFieldMember(ref)) { + return InferenceRuleNotApplicable; // inference rule is registered directly at the Fields } else if (isMethodMember(ref)) { - return InferenceRuleNotApplicable; // TODO + return InferenceRuleNotApplicable; // inference rule is registered directly at the method } else if (isVariableDeclaration(ref)) { - // use variables inside expressions! - return ref; // infer the Typir type from the variable, see the case below + return ref; // use variables inside expressions: infer the Typir type from the variable, see the case below } else if (isParameter(ref)) { - // use parameters inside expressions - return ref.type; + return ref.type; // use parameters inside expressions } else if (isFunctionDeclaration(ref)) { - // there is already an inference rule for function calls - return InferenceRuleNotApplicable; + return InferenceRuleNotApplicable; // there is already an inference rule for function calls } else if (ref === undefined) { - return InferenceRuleNotApplicable; + return InferenceRuleNotApplicable; // unresolved cross-reference: syntactic issues must be fixed before type checking can be applied } else { assertUnreachable(ref); } @@ -121,51 +131,41 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // ... variable declarations if (isVariableDeclaration(domainElement)) { if (domainElement.type) { - // the user declared this variable with a type - return domainElement.type; + return domainElement.type; // the user declared this variable with a type } else if (domainElement.value) { - // the didn't declared a type for this variable => do type inference of the assigned value instead! - return domainElement.value; + return domainElement.value; // the user didn't declare a type for this variable => do type inference of the assigned value instead! } else { return InferenceRuleNotApplicable; // this case is impossible, there is a validation in the Langium LOX validator for this case } } + // ... parameters + if (isParameter(domainElement)) { + return domainElement.type; + } return InferenceRuleNotApplicable; }); // some explicit validations for typing issues with Typir (replaces corresponding functions in the OxValidator!) - this.typir.validation.collector.addValidationRule( + this.typir.validation.Collector.addValidationRule( (node: unknown, typir: TypirServices) => { if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { - return typir.validation.constraints.ensureNodeIsAssignable(node.condition, typeBool, + return typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool, () => { message: "Conditions need to be evaluated to 'boolean'.", domainProperty: 'condition' }); } if (isVariableDeclaration(node)) { return [ - ...typir.validation.constraints.ensureNodeHasNotType(node, typeVoid, + ...typir.validation.Constraints.ensureNodeHasNotType(node, typeVoid, () => { message: "Variable can't be declared with a type 'void'.", domainProperty: 'type' }), - ...typir.validation.constraints.ensureNodeIsAssignable(node.value, node, (actual, expected) => { + ...typir.validation.Constraints.ensureNodeIsAssignable(node.value, node, (actual, expected) => { message: `The expression '${node.value?.$cstNode?.text}' of type '${actual.name}' is not assignable to '${node.name}' with type '${expected.name}'`, domainProperty: 'value' }), ]; } - if (isBinaryExpression(node) && node.operator === '=') { - return typir.validation.constraints.ensureNodeIsAssignable(node.right, node.left, (actual, expected) => { - message: `The expression '${node.right.$cstNode?.text}' of type '${actual.name}' is not assignable to '${node.left.$cstNode?.text}' with type '${expected.name}'`, - domainProperty: 'value' }); - } - if (isBinaryExpression(node) && (node.operator === '==' || node.operator === '!=')) { - return typir.validation.constraints.ensureNodeIsEquals(node.left, node.right, (actual, expected) => { - message: `This comparison will always return '${node.operator === '==' ? 'false' : 'true'}' as '${node.left.$cstNode?.text}' and '${node.right.$cstNode?.text}' have the different types '${actual.name}' and '${expected.name}'.`, - domainElement: node, // mark the 'operator' property! (note that "node.right" and "node.left" are the input for Typir) - domainProperty: 'operator', - severity: 'warning' }); - } if (isReturnStatement(node)) { const callableDeclaration: FunctionDeclaration | MethodMember | undefined = AstUtils.getContainerOfType(node, node => isFunctionDeclaration(node) || isMethodMember(node)); if (callableDeclaration && callableDeclaration.returnType.primitive && callableDeclaration.returnType.primitive !== 'void' && node.value) { // the return value must fit to the return type of the function / method - return typir.validation.constraints.ensureNodeIsAssignable(node.value, callableDeclaration.returnType, (actual, expected) => { + return typir.validation.Constraints.ensureNodeIsAssignable(node.value, callableDeclaration.returnType, (actual, expected) => { message: `The expression '${node.value!.$cstNode?.text}' of type '${actual.name}' is not usable as return value for the function '${callableDeclaration.name}' with return type '${expected.name}'.`, domainProperty: 'value' }); } @@ -175,19 +175,19 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { ); // check for unique function declarations - this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); + this.typir.validation.Collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); // check for unique class declarations const uniqueClassValidator = new UniqueClassValidation(this.typir, isClass); // check for unique method declarations - this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueMethodValidation(this.typir, + this.typir.validation.Collector.addValidationRuleWithBeforeAndAfter(new UniqueMethodValidation(this.typir, (node) => isMethodMember(node), // MethodMembers could have other $containers? (method, _type) => method.$container, uniqueClassValidator, )); - this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(uniqueClassValidator); // TODO this order is important, solve it in a different way! + this.typir.validation.Collector.addValidationRuleWithBeforeAndAfter(uniqueClassValidator); // TODO this order is important, solve it in a different way! // check for cycles in super-sub-type relationships - this.typir.validation.collector.addValidationRule(createNoSuperClassCyclesValidation(isClass)); + this.typir.validation.Collector.addValidationRule(createNoSuperClassCyclesValidation(isClass)); } onNewAstNode(node: AstNode): void { @@ -195,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.typir.factory.functions.create(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)! @@ -203,7 +203,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // class types (nominal typing): if (isClass(node)) { const className = node.name; - const classType = this.typir.factory.classes.create({ + const classType = this.typir.factory.Classes.create({ className, superClasses: node.superClass?.ref, // note that type inference is used here fields: node.members @@ -217,10 +217,10 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { .map(member => createFunctionDetails(member)), // same logic as for functions, since the LOX grammar defines them very similar // inference rule for declaration inferenceRuleForDeclaration: (domainElement: unknown) => domainElement === node, - // inference ruleS(?) for objects/class literals conforming to the current class - inferenceRuleForLiteral: { // > + // inference rule for constructor calls (i.e. class literals) conforming to the current class + inferenceRuleForConstructor: { // > filter: isMemberCall, - matching: (domainElement: MemberCall) => isClass(domainElement.element?.ref) && domainElement.element!.ref.name === className, + matching: (domainElement: MemberCall) => isClass(domainElement.element?.ref) && domainElement.element!.ref.name === className && domainElement.explicitOperationCall, inputValuesForFields: (_domainElement: MemberCall) => new Map(), // values for fields don't matter for nominal typing }, inferenceRuleForReference: { // > @@ -229,16 +229,17 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { inputValuesForFields: (_domainElement: TypeReference) => new Map(), // values for fields don't matter for nominal typing }, // inference rule for accessing fields - inferenceRuleForFieldAccess: (domainElement: unknown) => isMemberCall(domainElement) && isFieldMember(domainElement.element?.ref) && domainElement.element!.ref.$container === node + inferenceRuleForFieldAccess: (domainElement: unknown) => isMemberCall(domainElement) && isFieldMember(domainElement.element?.ref) && domainElement.element!.ref.$container === node && !domainElement.explicitOperationCall ? domainElement.element!.ref.name : InferenceRuleNotApplicable, + associatedDomainElement: node, }); - // TODO conversion 'nil' to classes ('TopClass')! - // 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'); + // explicitly declare, that 'nil' can be assigned to any Class variable classType.addListener(type => { - this.typir.conversion.markAsConvertible(this.typir.factory.primitives.get({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT'); + this.typir.Conversion.markAsConvertible(this.typir.factory.Primitives.get({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT'); }); + // The following idea does not work, since variables in LOX have a concrete class type and not an "any class" type: + // this.typir.conversion.markAsConvertible(typeNil, this.classKind.getOrCreateTopClassType({}), 'IMPLICIT_EXPLICIT'); } } } @@ -258,13 +259,14 @@ function createFunctionDetails(node: FunctionDeclaration | MethodMember): Create inferenceRuleForCalls: { filter: isMemberCall, matching: (domainElement: MemberCall) => (isFunctionDeclaration(domainElement.element?.ref) || isMethodMember(domainElement.element?.ref)) - && domainElement.element!.ref.name === callableName, + && domainElement.explicitOperationCall && domainElement.element!.ref.name === callableName, inputArguments: (domainElement: MemberCall) => domainElement.arguments }, + associatedDomainElement: node, }; } -export function createLoxTypirModule(langiumServices: LangiumSharedServices): Module { +export function createLoxTypirModule(langiumServices: LangiumSharedCoreServices): Module { return { // specific configurations for LOX TypeCreator: (typirServices) => new LoxTypeCreator(typirServices, langiumServices), diff --git a/examples/lox/src/language/lox-utils.ts b/examples/lox/src/language/lox-utils.ts new file mode 100644 index 0000000..7bf93cc --- /dev/null +++ b/examples/lox/src/language/lox-utils.ts @@ -0,0 +1,18 @@ +/****************************************************************************** + * 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 { Class } from './generated/ast.js'; + +export function getClassChain(classItem: Class): Class[] { + const set = new Set(); + let value: Class | undefined = classItem; + while (value && !set.has(value)) { + set.add(value); + value = value.superClass?.ref; + } + // Sets preserve insertion order + return Array.from(set); +} diff --git a/examples/lox/src/language/lox.langium b/examples/lox/src/language/lox.langium index cca302b..d19e864 100644 --- a/examples/lox/src/language/lox.langium +++ b/examples/lox/src/language/lox.langium @@ -3,20 +3,20 @@ grammar Lox entry LoxProgram: elements+=LoxElement*; -LoxElement: +LoxElement: Class | - ExpressionBlock | + ExpressionBlock | IfStatement | WhileStatement | ForStatement | FunctionDeclaration | - VariableDeclaration ';' | - PrintStatement ';' | - ReturnStatement ';' | + VariableDeclaration ';' | + PrintStatement ';' | + ReturnStatement ';' | Expression ';' ; -IfStatement: +IfStatement: 'if' '(' condition=Expression ')' block=ExpressionBlock ('else' elseBlock=ExpressionBlock)? ; @@ -61,7 +61,7 @@ Comparison infers Expression: MemberCall infers Expression: Primary ( - {infer MemberCall.previous=current} + {infer MemberCall.previous=current} // Member call with function call ( "." element=[NamedElement:ID] @@ -93,7 +93,6 @@ Primary infers Expression: NilLiteral | FeatureCall; -// TODO "FeatureCall" is never used ?! FeatureCall infers Expression: {infer MemberCall} (element=[NamedElement:ID] | element=[NamedElement:'this'] | element=[NamedElement:'super']) @@ -130,8 +129,8 @@ FieldMember: name=ID ':' type=TypeReference; TypeReference: - reference=[Class:ID] - | primitive=("string" | "number" | "boolean" | "void") + reference=[Class:ID] + | primitive=("string" | "number" | "boolean" | "void") | '(' ( parameters+=LambdaParameter (',' parameters+=LambdaParameter)*)? ')' '=>' returnType=TypeReference; LambdaParameter: (name=ID ':')? type=TypeReference; diff --git a/examples/lox/src/language/type-system/assignment.ts b/examples/lox/src/language/type-system/assignment.ts deleted file mode 100644 index 0bad4b8..0000000 --- a/examples/lox/src/language/type-system/assignment.ts +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * 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 { isClassType, isFunctionType, isNilType, TypeDescription } from './descriptions.js'; -import { getClassChain } from './infer.js'; - -export function isAssignable(from: TypeDescription, to: TypeDescription): boolean { - if (isClassType(from)) { - if (!isClassType(to)) { - return false; - } - const fromLit = from.literal; - const fromChain = getClassChain(fromLit); - const toClass = to.literal; - for (const fromClass of fromChain) { - if (fromClass === toClass) { - return true; - } - } - return false; - } - if (isNilType(from)) { - return isClassType(to); - } - if (isFunctionType(from)) { - if (!isFunctionType(to)) { - return false; - } - if (!isAssignable(from.returnType, to.returnType)) { - return false; - } - if (from.parameters.length !== to.parameters.length) { - return false; - } - for (let i = 0; i < from.parameters.length; i++) { - const fromParam = from.parameters[i]; - const toParam = to.parameters[i]; - if (!isAssignable(fromParam.type, toParam.type)) { - return false; - } - } - return true; - } - return from.$type === to.$type; -} diff --git a/examples/lox/src/language/type-system/descriptions.ts b/examples/lox/src/language/type-system/descriptions.ts deleted file mode 100644 index 93a8720..0000000 --- a/examples/lox/src/language/type-system/descriptions.ts +++ /dev/null @@ -1,162 +0,0 @@ -/****************************************************************************** - * 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 { AstNode } from 'langium'; -import { BooleanLiteral, Class, NumberLiteral, StringLiteral } from '../generated/ast.js'; - -export type TypeDescription = - | NilTypeDescription - | VoidTypeDescription - | BooleanTypeDescription - | StringTypeDescription - | NumberTypeDescription - | FunctionTypeDescription - | ClassTypeDescription - | ErrorType; - -export interface NilTypeDescription { - readonly $type: 'nil' -} - -export function createNilType(): NilTypeDescription { - return { - $type: 'nil' - }; -} - -export function isNilType(item: TypeDescription): item is NilTypeDescription { - return item.$type === 'nil'; -} - -export interface VoidTypeDescription { - readonly $type: 'void' -} - -export function createVoidType(): VoidTypeDescription { - return { - $type: 'void' - }; -} - -export function isVoidType(item: TypeDescription): item is VoidTypeDescription { - return item.$type === 'void'; -} - -export interface BooleanTypeDescription { - readonly $type: 'boolean' - readonly literal?: BooleanLiteral -} - -export function createBooleanType(literal?: BooleanLiteral): BooleanTypeDescription { - return { - $type: 'boolean', - literal - }; -} - -export function isBooleanType(item: TypeDescription): item is BooleanTypeDescription { - return item.$type === 'boolean'; -} - -export interface StringTypeDescription { - readonly $type: 'string' - readonly literal?: StringLiteral -} - -export function createStringType(literal?: StringLiteral): StringTypeDescription { - return { - $type: 'string', - literal - }; -} - -export function isStringType(item: TypeDescription): item is StringTypeDescription { - return item.$type === 'string'; -} - -export interface NumberTypeDescription { - readonly $type: 'number', - readonly literal?: NumberLiteral -} - -export function createNumberType(literal?: NumberLiteral): NumberTypeDescription { - return { - $type: 'number', - literal - }; -} - -export function isNumberType(item: TypeDescription): item is NumberTypeDescription { - return item.$type === 'number'; -} - -export interface FunctionTypeDescription { - readonly $type: 'function' - readonly returnType: TypeDescription - readonly parameters: FunctionParameter[] -} - -export interface FunctionParameter { - name: string - type: TypeDescription -} - -export function createFunctionType(returnType: TypeDescription, parameters: FunctionParameter[]): FunctionTypeDescription { - return { - $type: 'function', - parameters, - returnType - }; -} - -export function isFunctionType(item: TypeDescription): item is FunctionTypeDescription { - return item.$type === 'function'; -} - -export interface ClassTypeDescription { - readonly $type: 'class' - readonly literal: Class -} - -export function createClassType(literal: Class): ClassTypeDescription { - return { - $type: 'class', - literal - }; -} - -export function isClassType(item: TypeDescription): item is ClassTypeDescription { - return item.$type === 'class'; -} - -export interface ErrorType { - readonly $type: 'error' - readonly source?: AstNode - readonly message: string -} - -export function createErrorType(message: string, source?: AstNode): ErrorType { - return { - $type: 'error', - message, - source - }; -} - -export function isErrorType(item: TypeDescription): item is ErrorType { - return item.$type === 'error'; -} - -export function typeToString(item: TypeDescription): string { - if (isClassType(item)) { - return item.literal.name; - } else if (isFunctionType(item)) { - const params = item.parameters.map(e => `${e.name}: ${typeToString(e.type)}`).join(', '); - return `(${params}) => ${typeToString(item.returnType)}`; - } else { - return item.$type; - } -} diff --git a/examples/lox/src/language/type-system/infer.ts b/examples/lox/src/language/type-system/infer.ts deleted file mode 100644 index 8d80eb4..0000000 --- a/examples/lox/src/language/type-system/infer.ts +++ /dev/null @@ -1,154 +0,0 @@ -/****************************************************************************** - * 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 { AstNode } from 'langium'; -import { BinaryExpression, Class, isBinaryExpression, isBooleanLiteral, isClass, isFieldMember, isFunctionDeclaration, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, MemberCall, TypeReference } from '../generated/ast.js'; -import { createBooleanType, createClassType, createErrorType, createFunctionType, createNilType, createNumberType, createStringType, createVoidType, isFunctionType, isStringType, TypeDescription } from './descriptions.js'; - -export function inferType(node: AstNode | undefined, cache: Map): TypeDescription { - let type: TypeDescription | undefined; - if (!node) { - return createErrorType('Could not infer type for undefined', node); - } - const existing = cache.get(node); - if (existing) { - return existing; - } - // Prevent recursive inference errors - cache.set(node, createErrorType('Recursive definition', node)); - if (isStringLiteral(node)) { - type = createStringType(node); - } else if (isNumberLiteral(node)) { - type = createNumberType(node); - } else if (isBooleanLiteral(node)) { - type = createBooleanType(node); - } else if (isNilLiteral(node)) { - type = createNilType(); - } else if (isFunctionDeclaration(node) || isMethodMember(node)) { - const returnType = inferType(node.returnType, cache); - const parameters = node.parameters.map(e => ({ - name: e.name, - type: inferType(e.type, cache) - })); - type = createFunctionType(returnType, parameters); - } else if (isTypeReference(node)) { - type = inferTypeRef(node, cache); - } else if (isMemberCall(node)) { - type = inferMemberCall(node, cache); - if (node.explicitOperationCall) { - if (isFunctionType(type)) { - type = type.returnType; - } - } - } else if (isVariableDeclaration(node)) { - if (node.type) { - type = inferType(node.type, cache); - } else if (node.value) { - type = inferType(node.value, cache); - } else { - type = createErrorType('No type hint for this element', node); - } - } else if (isParameter(node)) { - type = inferType(node.type, cache); - } else if (isFieldMember(node)) { - type = inferType(node.type, cache); - } else if (isClass(node)) { - type = createClassType(node); - } else if (isBinaryExpression(node)) { - type = inferBinaryExpression(node, cache); - } else if (isUnaryExpression(node)) { - if (node.operator === '!') { - type = createBooleanType(); - } else { - type = createNumberType(); - } - } else if (isPrintStatement(node)) { - type = createVoidType(); - } else if (isReturnStatement(node)) { - if (!node.value) { - type = createVoidType(); - } else { - type = inferType(node.value, cache); - } - } - if (!type) { - type = createErrorType('Could not infer type for ' + node.$type, node); - } - - cache.set(node, type); - return type; -} - -function inferTypeRef(node: TypeReference, cache: Map): TypeDescription { - if (node.primitive) { - if (node.primitive === 'number') { - return createNumberType(); - } else if (node.primitive === 'string') { - return createStringType(); - } else if (node.primitive === 'boolean') { - return createBooleanType(); - } else if (node.primitive === 'void') { - return createVoidType(); - } - } else if (node.reference) { - if (node.reference.ref) { - return createClassType(node.reference.ref); - } - } else if (node.returnType) { - const returnType = inferType(node.returnType, cache); - const parameters = node.parameters.map((e, i) => ({ - name: e.name ?? `$${i}`, - type: inferType(e.type, cache) - })); - return createFunctionType(returnType, parameters); - } - return createErrorType('Could not infer type for this reference', node); -} - -function inferMemberCall(node: MemberCall, cache: Map): TypeDescription { - const element = node.element?.ref; - if (element) { - return inferType(element, cache); - } else if (node.explicitOperationCall && node.previous) { - const previousType = inferType(node.previous, cache); - if (isFunctionType(previousType)) { - return previousType.returnType; - } - return createErrorType('Cannot call operation on non-function type', node); - } - return createErrorType('Could not infer type for element ' + (node.element?.$refText ?? 'undefined'), node); -} - -function inferBinaryExpression(expr: BinaryExpression, cache: Map): TypeDescription { - if (['-', '*', '/', '%'].includes(expr.operator)) { - return createNumberType(); - } else if (['and', 'or', '<', '<=', '>', '>=', '==', '!='].includes(expr.operator)) { - return createBooleanType(); - } - const left = inferType(expr.left, cache); - const right = inferType(expr.right, cache); - if (expr.operator === '+') { - if (isStringType(left) || isStringType(right)) { - return createStringType(); - } else { - return createNumberType(); - } - } else if (expr.operator === '=') { - return right; - } - return createErrorType('Could not infer type from binary expression', expr); -} - -export function getClassChain(classItem: Class): Class[] { - const set = new Set(); - let value: Class | undefined = classItem; - while (value && !set.has(value)) { - set.add(value); - value = value.superClass?.ref; - } - // Sets preserve insertion order - return Array.from(set); -} diff --git a/examples/lox/src/language/type-system/operator.ts b/examples/lox/src/language/type-system/operator.ts deleted file mode 100644 index 01e4a98..0000000 --- a/examples/lox/src/language/type-system/operator.ts +++ /dev/null @@ -1,27 +0,0 @@ -/****************************************************************************** - * 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 { TypeDescription } from './descriptions.js'; - -export function isLegalOperation(operator: string, left: TypeDescription, right?: TypeDescription): boolean { - if (operator === '+') { - if (!right) { - return left.$type === 'number'; - } - return (left.$type === 'number' || left.$type === 'string') - && (right.$type === 'number' || right.$type === 'string'); - } else if (['-', '/', '*', '%', '<', '<=', '>', '>='].includes(operator)) { - if (!right) { - return left.$type === 'number'; - } - return left.$type === 'number' && right.$type === 'number'; - } else if (['and', 'or'].includes(operator)) { - return left.$type === 'boolean' && right?.$type === 'boolean'; - } else if (operator === '!') { - return left.$type === 'boolean'; - } - return true; -} diff --git a/examples/lox/test/lox-type-checking-classes.test.ts b/examples/lox/test/lox-type-checking-classes.test.ts index f6102ec..7bbff27 100644 --- a/examples/lox/test/lox-type-checking-classes.test.ts +++ b/examples/lox/test/lox-type-checking-classes.test.ts @@ -17,7 +17,7 @@ describe('Test type checking for classes', () => { class MyClass2 < MyClass1 {} var v1: MyClass1 = MyClass2(); `, 0); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); }); test('Class inheritance for assignments: wrong', async () => { @@ -26,7 +26,7 @@ describe('Test type checking for classes', () => { class MyClass2 < MyClass1 {} var v1: MyClass2 = MyClass1(); `, 1); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); }); test('Class fields: correct values', async () => { @@ -36,7 +36,7 @@ describe('Test type checking for classes', () => { v1.name = "Bob"; v1.age = 42; `, 0); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); }); test('Class fields: wrong values', async () => { @@ -46,7 +46,7 @@ describe('Test type checking for classes', () => { v1.name = 42; v1.age = "Bob"; `, 2); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); }); test('Classes must be unique by name 2', async () => { @@ -57,7 +57,7 @@ describe('Test type checking for classes', () => { 'Declared classes need to be unique (MyClass1).', 'Declared classes need to be unique (MyClass1).', ]); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); }); test('Classes must be unique by name 3', async () => { @@ -70,76 +70,7 @@ describe('Test type checking for classes', () => { 'Declared classes need to be unique (MyClass2).', 'Declared classes need to be unique (MyClass2).', ]); - expectTypirTypes(loxServices, isClassType, 'MyClass2'); - }); - - test('Class methods: OK', async () => { - await validateLox(` - class MyClass1 { - method1(input: number): number { - return 123; - } - } - var v1: MyClass1 = MyClass1(); - var v2: number = v1.method1(456); - `, []); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); - }); - - test('Class methods: wrong return value', async () => { - await validateLox(` - class MyClass1 { - method1(input: number): number { - return true; - } - } - var v1: MyClass1 = MyClass1(); - var v2: number = v1.method1(456); - `, 1); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); - }); - - test('Class methods: method return type does not fit to variable type', async () => { - await validateLox(` - class MyClass1 { - method1(input: number): number { - return 123; - } - } - var v1: MyClass1 = MyClass1(); - var v2: boolean = v1.method1(456); - `, 1); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); - }); - - test('Class methods: value for input parameter does not fit to the type of the input parameter', async () => { - await validateLox(` - class MyClass1 { - method1(input: number): number { - return 123; - } - } - var v1: MyClass1 = MyClass1(); - var v2: number = v1.method1(true); - `, 1); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); - }); - - test('Class methods: methods are not distinguishable', async () => { - await validateLox(` - class MyClass1 { - method1(input: number): number { - return 123; - } - method1(another: number): boolean { - return true; - } - } - `, [ // both methods need to be marked: - 'Declared methods need to be unique (class-MyClass1.method1(number)).', - 'Declared methods need to be unique (class-MyClass1.method1(number)).', - ]); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass2'); }); }); @@ -151,7 +82,7 @@ describe('Class literals', () => { class MyClass { name: string age: number } var v1 = MyClass(); // constructor call `, []); - expectTypirTypes(loxServices, isClassType, 'MyClass'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass'); }); test('Class literals 2', async () => { @@ -159,7 +90,7 @@ describe('Class literals', () => { class MyClass { name: string age: number } var v1: MyClass = MyClass(); // constructor call `, []); - expectTypirTypes(loxServices, isClassType, 'MyClass'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass'); }); test('Class literals 3', async () => { @@ -168,7 +99,19 @@ describe('Class literals', () => { class MyClass2 {} var v1: boolean = MyClass1() == MyClass2(); // comparing objects with each other `, [], 1); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); + }); + + test('nil is assignable to any Class', async () => { + await validateLox(` + class MyClass1 {} + class MyClass2 {} + var v1 = MyClass1(); + var v2: MyClass2 = MyClass2(); + v1 = nil; + v2 = nil; + `, []); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); }); }); diff --git a/examples/lox/test/lox-type-checking-cycles.test.ts b/examples/lox/test/lox-type-checking-cycles.test.ts index b87455d..9aae69e 100644 --- a/examples/lox/test/lox-type-checking-cycles.test.ts +++ b/examples/lox/test/lox-type-checking-cycles.test.ts @@ -17,7 +17,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( children: Node } `, []); - expectTypirTypes(loxServices, isClassType, 'Node'); + expectTypirTypes(loxServices.typir, isClassType, 'Node'); }); test('Two Classes with fields with the other Class as type', async () => { @@ -29,7 +29,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( prop2: A } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); }); test('Three Classes with fields with one of the other Classes as type', async () => { @@ -44,7 +44,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( prop3: A } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B', 'C'); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B', 'C'); }); test('Three Classes with fields with two of the other Classes as type', async () => { @@ -62,7 +62,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( prop6: B } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B', 'C'); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B', 'C'); }); test('Class with field of its own type and another dependency', async () => { @@ -75,7 +75,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( children: Node } `, []); - expectTypirTypes(loxServices, isClassType, 'Node', 'Another'); + expectTypirTypes(loxServices.typir, isClassType, 'Node', 'Another'); }); test('Two Classes with a field of its own type and cyclic dependencies to each other', async () => { @@ -89,7 +89,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( another: Node } `, []); - expectTypirTypes(loxServices, isClassType, 'Node', 'Another'); + expectTypirTypes(loxServices.typir, isClassType, 'Node', 'Another'); }); test('Having two declarations for the delayed class A, but only one type A in the type system', async () => { @@ -106,7 +106,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( 'Declared classes need to be unique (A).', ]); // check, that there is only one class type A in the type graph: - expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); }); test('Having three declarations for the delayed class A, but only one type A in the type system', async () => { @@ -127,7 +127,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( 'Declared classes need to be unique (A).', ]); // check, that there is only one class type A in the type graph: - expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); }); test('Having two declarations for class A waiting for B, while B itself depends on A', async () => { @@ -146,7 +146,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( 'Declared classes need to be unique (A).', ]); // check, that there is only one class type A in the type graph: - expectTypirTypes(loxServices, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); }); test('Class with method: cycle with return type', async () => { @@ -155,8 +155,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( myMethod(input: number): Node {} } `, []); - expectTypirTypes(loxServices, isClassType, 'Node'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'Node'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Class with method: cycle with input parameter type', async () => { @@ -165,8 +165,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( myMethod(input: Node): number {} } `, []); - expectTypirTypes(loxServices, isClassType, 'Node'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'Node'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Two different Classes with the same method (type) should result in only one method type', async () => { @@ -180,8 +180,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( myMethod(input: number): boolean {} } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Two different Classes depend on each other regarding their methods return type', async () => { @@ -195,8 +195,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( myMethod(input: number): A {} } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', 'myMethod', ...operatorNames); }); test('Two different Classes with the same method which has one of these classes as return type', async () => { @@ -210,8 +210,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( myMethod(input: number): B {} } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Same delayed function type is used by a function declaration and a method declaration', async () => { @@ -222,8 +222,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( fun myMethod(input: number): B {} class B { } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Two class declarations A with the same delayed method which depends on the class B', async () => { @@ -240,8 +240,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( 'Declared classes need to be unique (A).', ]); // check, that there is only one class type A in the type graph: - expectTypirTypes(loxServices, isClassType, 'A', 'B'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Mix of dependencies in classes: 1 method and 1 field', async () => { @@ -253,8 +253,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( propB1: A } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B1'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B1'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Mix of dependencies in classes: 1 method and 2 fields (order 1)', async () => { @@ -269,8 +269,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( myMethod(input: number): B1 {} } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B1', 'B2'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('Mix of dependencies in classes: 1 method and 2 fields (order 2)', async () => { @@ -285,8 +285,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( propB1: A } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B1', 'B2'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', ...operatorNames); }); test('The same class is involved into two dependency cycles', async () => { @@ -308,8 +308,8 @@ describe('Cyclic type definitions where a Class is declared and already used', ( methodC2(p: A): void {} } `, []); - expectTypirTypes(loxServices, isClassType, 'A', 'B1', 'B2', 'C1', 'C2'); - expectTypirTypes(loxServices, isFunctionType, 'myMethod', 'methodC1', 'methodC2', ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType, 'A', 'B1', 'B2', 'C1', 'C2'); + expectTypirTypes(loxServices.typir, isFunctionType, 'myMethod', 'methodC1', 'methodC2', ...operatorNames); }); test('Class inheritance and the order of type definitions', async () => { @@ -318,7 +318,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( class MyClass1 {} class MyClass2 < MyClass1 {} `, []); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); }); test('Class inheritance and the order of type definitions', async () => { @@ -327,7 +327,7 @@ describe('Cyclic type definitions where a Class is declared and already used', ( class MyClass2 < MyClass1 {} class MyClass1 {} `, []); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); }); }); @@ -342,7 +342,7 @@ describe('Test internal validation of Typir for cycles in the class inheritance 'Cycles in super-sub-class-relationships are not allowed: MyClass2', 'Cycles in super-sub-class-relationships are not allowed: MyClass3', ]); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2', 'MyClass3'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2', 'MyClass3'); }); test('Two involved classes: 1 -> 2 -> 1', async () => { @@ -353,14 +353,14 @@ describe('Test internal validation of Typir for cycles in the class inheritance 'Cycles in super-sub-class-relationships are not allowed: MyClass1', 'Cycles in super-sub-class-relationships are not allowed: MyClass2', ]); - expectTypirTypes(loxServices, isClassType, 'MyClass1', 'MyClass2'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1', 'MyClass2'); }); test('One involved class: 1 -> 1', async () => { await validateLox(` class MyClass1 < MyClass1 { } `, 'Cycles in super-sub-class-relationships are not allowed: MyClass1'); - expectTypirTypes(loxServices, isClassType, 'MyClass1'); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); }); }); @@ -402,7 +402,7 @@ describe('longer LOX examples with classes regarding ordering', () => { var superType: SuperClass = x; print superType.a; `, []); - expectTypirTypes(loxServices, isClassType, 'SuperClass', 'SubClass', 'NestedClass'); + expectTypirTypes(loxServices.typir, isClassType, 'SuperClass', 'SubClass', 'NestedClass'); }); test('complete with easy order of classes', async () => { @@ -442,6 +442,6 @@ describe('longer LOX examples with classes regarding ordering', () => { var superType: SuperClass = x; print superType.a; `, []); - expectTypirTypes(loxServices, isClassType, 'SuperClass', 'SubClass', 'NestedClass'); + expectTypirTypes(loxServices.typir, isClassType, 'SuperClass', 'SubClass', 'NestedClass'); }); }); diff --git a/examples/lox/test/lox-type-checking-functions.test.ts b/examples/lox/test/lox-type-checking-functions.test.ts index 44193bb..c6ac429 100644 --- a/examples/lox/test/lox-type-checking-functions.test.ts +++ b/examples/lox/test/lox-type-checking-functions.test.ts @@ -4,43 +4,94 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { describe, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { loxServices, operatorNames, validateLox } from './lox-type-checking-utils.js'; import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; import { isFunctionType } from '../../../packages/typir/lib/kinds/function/function-type.js'; +import { isFunctionDeclaration, isMemberCall, LoxProgram } from '../src/language/generated/ast.js'; +import { assertTrue, assertType } from '../../../packages/typir/lib/utils/utils.js'; +import { isType } from '../../../packages/typir/lib/graph/type-node.js'; +import { isPrimitiveType } from '../../../packages/typir/lib/kinds/primitive/primitive-type.js'; describe('Test type checking for user-defined functions', () => { test('function: return value and return type must match', async () => { await validateLox('fun myFunction1() : boolean { return true; }', 0); - await validateLox('fun myFunction2() : boolean { return 2; }', 1); + await validateLox('fun myFunction2() : boolean { return 2; }', + "The expression '2' of type 'number' is not usable as return value for the function 'myFunction2' with return type 'boolean'."); await validateLox('fun myFunction3() : number { return 2; }', 0); - await validateLox('fun myFunction4() : number { return true; }', 1); - expectTypirTypes(loxServices, isFunctionType, 'myFunction1', 'myFunction2', 'myFunction3', 'myFunction4', ...operatorNames); + await validateLox('fun myFunction4() : number { return true; }', + "The expression 'true' of type 'boolean' is not usable as return value for the function 'myFunction4' with return type 'number'."); + expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction1', 'myFunction2', 'myFunction3', 'myFunction4', ...operatorNames); }); test('overloaded function: different return types are not enough', async () => { await validateLox(` fun myFunction() : boolean { return true; } fun myFunction() : number { return 2; } - `, 2); - expectTypirTypes(loxServices, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); // the types are different nevertheless! + `, [ + 'Declared functions need to be unique (myFunction()).', + 'Declared functions need to be unique (myFunction()).', + ]); + expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); // the types are different nevertheless! }); test('overloaded function: different parameter names are not enough', async () => { await validateLox(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(other: boolean) : boolean { return true; } - `, 2); - expectTypirTypes(loxServices, isFunctionType, 'myFunction', ...operatorNames); // but both functions have the same type! + `, [ + 'Declared functions need to be unique (myFunction(boolean)).', + 'Declared functions need to be unique (myFunction(boolean)).', + ]); + expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', ...operatorNames); // but both functions have the same type! }); test('overloaded function: but different parameter types are fine', async () => { await validateLox(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(input: number) : boolean { return true; } - `, 0); - expectTypirTypes(loxServices, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); + `, []); + expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); + }); + + test('overloaded function: check correct type inference and cross-references', async () => { + const rootNode = (await validateLox(` + fun myFunction(input: number) : number { return 987; } + fun myFunction(input: boolean) : boolean { return true; } + myFunction(123); + myFunction(false); + `, [])).parseResult.value as LoxProgram; + expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); + + // check type inference + cross-reference of the two method calls + expect(rootNode.elements).toHaveLength(4); + + // Call 1 should be number + const call1Node = rootNode.elements[2]; + // check cross-reference + assertTrue(isMemberCall(call1Node)); + const method1 = call1Node.element?.ref; + assertTrue(isFunctionDeclaration(method1)); + expect(method1.returnType.primitive).toBe('number'); + // check type inference + const call1Type = loxServices.typir.Inference.inferType(call1Node); + expect(isType(call1Type)).toBeTruthy(); + assertType(call1Type, isPrimitiveType); + expect(call1Type.getName()).toBe('number'); + + // Call 2 should be boolean + const call2Node = rootNode.elements[3]; + // check cross-reference + assertTrue(isMemberCall(call2Node)); + const method2 = call2Node.element?.ref; + assertTrue(isFunctionDeclaration(method2)); + expect(method2.returnType.primitive).toBe('boolean'); + // check type inference + const call2Type = loxServices.typir.Inference.inferType(call2Node); + expect(isType(call2Type)).toBeTruthy(); + assertType(call2Type, isPrimitiveType); + expect(call2Type.getName()).toBe('boolean'); }); }); diff --git a/examples/lox/test/lox-type-checking-method.test.ts b/examples/lox/test/lox-type-checking-method.test.ts new file mode 100644 index 0000000..6b71468 --- /dev/null +++ b/examples/lox/test/lox-type-checking-method.test.ts @@ -0,0 +1,150 @@ +/****************************************************************************** + * 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 { isType } from '../../../packages/typir/lib/graph/type-node.js'; +import { isClassType } from '../../../packages/typir/lib/kinds/class/class-type.js'; +import { isFunctionType } from '../../../packages/typir/lib/kinds/function/function-type.js'; +import { isPrimitiveType } from '../../../packages/typir/lib/kinds/primitive/primitive-type.js'; +import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; +import { assertTrue, assertType } from '../../../packages/typir/lib/utils/utils.js'; +import { isMemberCall, isMethodMember, LoxProgram } from '../src/language/generated/ast.js'; +import { loxServices, operatorNames, validateLox } from './lox-type-checking-utils.js'; + +describe('Test type checking for methods of classes', () => { + + test('Class methods: OK', async () => { + await validateLox(` + class MyClass1 { + method1(input: number): number { + return 123; + } + } + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(456); + `, []); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); + }); + + test('Class methods: wrong return value', async () => { + await validateLox(` + class MyClass1 { + method1(input: number): number { + return true; + } + } + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(456); + `, 1); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); + }); + + test('Class methods: method return type does not fit to variable type', async () => { + await validateLox(` + class MyClass1 { + method1(input: number): number { + return 123; + } + } + var v1: MyClass1 = MyClass1(); + var v2: boolean = v1.method1(456); + `, 1); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); + }); + + test('Class methods: value for input parameter does not fit to the type of the input parameter', async () => { + await validateLox(` + class MyClass1 { + method1(input: number): number { + return 123; + } + } + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(true); + `, 1); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); + }); + + test('Class methods: methods are not distinguishable', async () => { + await validateLox(` + class MyClass1 { + method1(input: number): number { + return 123; + } + method1(another: number): boolean { + return true; + } + } + `, [ // both methods need to be marked: + 'Declared methods need to be unique (class-MyClass1.method1(number)).', + 'Declared methods need to be unique (class-MyClass1.method1(number)).', + ]); + expectTypirTypes(loxServices.typir, isClassType, 'MyClass1'); + }); + +}); + +describe('Test overloaded methods', () => { + const methodDeclaration = ` + class MyClass { + method1(input: number): number { + return 987; + } + method1(input: boolean): boolean { + return true; + } + } + `; + + test('Calls with correct arguments', async () => { + const rootNode = (await validateLox(`${methodDeclaration} + var v = MyClass(); + v.method1(123); + v.method1(false); + `, [])).parseResult.value as LoxProgram; + expectTypirTypes(loxServices.typir, isClassType, 'MyClass'); + expectTypirTypes(loxServices.typir, isFunctionType, 'method1', 'method1', ...operatorNames); + + // check type inference + cross-reference of the two method calls + expect(rootNode.elements).toHaveLength(4); + + // Call 1 should be number + const call1Node = rootNode.elements[2]; + // check cross-reference + assertTrue(isMemberCall(call1Node)); + const method1 = call1Node.element?.ref; + assertTrue(isMethodMember(method1)); + expect(method1.returnType.primitive).toBe('number'); + // check type inference + const call1Type = loxServices.typir.Inference.inferType(call1Node); + expect(isType(call1Type)).toBeTruthy(); + assertType(call1Type, isPrimitiveType); + expect(call1Type.getName()).toBe('number'); + + // Call 2 should be boolean + const call2Node = rootNode.elements[3]; + // check cross-reference + assertTrue(isMemberCall(call2Node)); + const method2 = call2Node.element?.ref; + assertTrue(isMethodMember(method2)); + expect(method2.returnType.primitive).toBe('boolean'); + // check type inference + const call2Type = loxServices.typir.Inference.inferType(call2Node); + expect(isType(call2Type)).toBeTruthy(); + assertType(call2Type, isPrimitiveType); + expect(call2Type.getName()).toBe('boolean'); + }); + + test('Call with wrong argument', async () => { + await validateLox(`${methodDeclaration} + var v = MyClass(); + v.method1("wrong"); // the linker provides an Method here, but the arguments don't match + `, [ + "The given operands for the overloaded function 'method1' match the expected types only partially.", + ]); + }); + +}); diff --git a/examples/lox/test/lox-type-checking-operators.test.ts b/examples/lox/test/lox-type-checking-operators.test.ts index 13c12ab..69348c4 100644 --- a/examples/lox/test/lox-type-checking-operators.test.ts +++ b/examples/lox/test/lox-type-checking-operators.test.ts @@ -9,6 +9,43 @@ import { validateLox } from './lox-type-checking-utils.js'; describe('Test type checking for operators', () => { + test('binary operators', async () => { + await validateLox('var myResult: number = 2 + 3;', 0); + await validateLox('var myResult: number = 2 - 3;', 0); + await validateLox('var myResult: number = 2 * 3;', 0); + await validateLox('var myResult: number = 2 / 3;', 0); + + await validateLox('var myResult: boolean = 2 < 3;', 0); + await validateLox('var myResult: boolean = 2 <= 3;', 0); + await validateLox('var myResult: boolean = 2 > 3;', 0); + await validateLox('var myResult: boolean = 2 >= 3;', 0); + + await validateLox('var myResult: boolean = true and false;', 0); + await validateLox('var myResult: boolean = true or false;', 0); + + await validateLox('var myResult: boolean = 2 == 3;', 0); + await validateLox('var myResult: boolean = 2 != 3;', 0); + await validateLox('var myResult: boolean = true == false;', 0); + await validateLox('var myResult: boolean = true != false;', 0); + + await validateLox('var myResult: boolean = true == 3;', 0, + "This comparison will always return 'false' as 'true' and '3' have the different types 'boolean' and 'number'."); + await validateLox('var myResult: boolean = 2 != false;', 0, + "This comparison will always return 'true' as '2' and 'false' have the different types 'number' and 'boolean'."); + }); + + test('unary operator: !', async () => { + await validateLox('var myResult: boolean = !true;', 0); + await validateLox('var myResult: boolean = !!true;', 0); + await validateLox('var myResult: boolean = !!!true;', 0); + }); + + test('unary operator: -', async () => { + await validateLox('var myResult: number = -2;', 0); + await validateLox('var myResult: number = --2;', 0); + await validateLox('var myResult: number = ---2;', 0); + }); + test('overloaded operator "+"', async () => { await validateLox('var myResult: number = 1 + 2;', 0); await validateLox('var myResult: string = "a" + "b";', 0); @@ -32,8 +69,19 @@ describe('Test type checking for operators', () => { await validateLox('var myVar : boolean = 2 == false;', 0, 1); }); - test('Only a single problem with the inner expression, since the type of "+" is always number!', async () => { - await validateLox('var myVar : number = 2 + (2 * false);', 1); + test('Only a single problem with the inner expression, since the type of "*" is always number!', async () => { + await validateLox('var myVar : number = 2 * (2 * false);', [ + "While validating the AstNode '(2 * false)', this error is found: The given operands for the function '*' match the expected types only partially.", + ]); + }); + + test('Two issues in nested expressions, since "*" expects always numbers, while "and" returns always booleans!', async () => { + await validateLox('var myVar : number = 2 * (2 and false);', [ + // this is obvious: left and right need to have the same type + "While validating the AstNode '(2 and false)', this error is found: The given operands for the function 'and' match the expected types only partially.", + // '*' supports only numbers for left and right, but the right operand is always boolean as result of the 'and' operator + "While validating the AstNode '2 * (2 and false)', this error is found: The given operands for the function '*' match the expected types only partially.", + ]); }); }); diff --git a/examples/lox/test/lox-type-checking-utils.ts b/examples/lox/test/lox-type-checking-utils.ts index 509862d..d4a6fbf 100644 --- a/examples/lox/test/lox-type-checking-utils.ts +++ b/examples/lox/test/lox-type-checking-utils.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, LangiumDocument } from 'langium'; import { parseDocument } from 'langium/test'; import { expectTypirTypes, isClassType, isFunctionType } from 'typir'; import { deleteAllDocuments } from 'typir-langium'; @@ -12,46 +12,46 @@ import { afterEach, expect } from 'vitest'; import type { Diagnostic } from 'vscode-languageserver-types'; import { DiagnosticSeverity } from 'vscode-languageserver-types'; import { createLoxServices } from '../src/language/lox-module.js'; +import { fail } from 'assert'; export const loxServices = createLoxServices(EmptyFileSystem).Lox; export const operatorNames = ['-', '*', '/', '+', '+', '+', '+', '<', '<=', '>', '>=', 'and', 'or', '==', '!=', '=', '!', '-']; afterEach(async () => { - await deleteAllDocuments(loxServices); + await deleteAllDocuments(loxServices.shared); // check, that there are no user-defined classes and functions after clearing/invalidating all LOX documents - expectTypirTypes(loxServices, isClassType); - expectTypirTypes(loxServices, isFunctionType, ...operatorNames); + expectTypirTypes(loxServices.typir, isClassType); + expectTypirTypes(loxServices.typir, isFunctionType, ...operatorNames); }); -export async function validateLox(lox: string, errors: number | string | string[], warnings: number = 0) { +export async function validateLox(lox: string, errors: number | string | string[], warnings: number | string | string[] = 0): Promise { const document = await parseDocument(loxServices, lox.trim()); const diagnostics: Diagnostic[] = await loxServices.validation.DocumentValidator.validateDocument(document); // errors - const diagnosticsErrors = diagnostics.filter(d => d.severity === DiagnosticSeverity.Error).map(d => fixMessage(d.message)); + const diagnosticsErrors: string[] = diagnostics.filter(d => d.severity === DiagnosticSeverity.Error).map(d => d.message); + checkIssues(diagnosticsErrors, errors); + + // warnings + const diagnosticsWarnings: string[] = diagnostics.filter(d => d.severity === DiagnosticSeverity.Warning).map(d => d.message); + checkIssues(diagnosticsWarnings, warnings); + + return document; +} + +function checkIssues(diagnosticsErrors: string[], errors: number | string | string[]): void { const msgError = diagnosticsErrors.join('\n'); if (typeof errors === 'number') { expect(diagnosticsErrors, msgError).toHaveLength(errors); } else if (typeof errors === 'string') { expect(diagnosticsErrors, msgError).toHaveLength(1); - expect(diagnosticsErrors[0]).toBe(errors); + expect(diagnosticsErrors[0], msgError).includes(errors); } else { expect(diagnosticsErrors, msgError).toHaveLength(errors.length); for (const expected of errors) { - expect(diagnosticsErrors).includes(expected); + if (diagnosticsErrors.some(dia => dia.includes(expected)) === false) { + fail(`This issue is expected:\n${expected}\n... but it is not contained in these found issues:\n${msgError}`); + } } } - - // warnings - const diagnosticsWarnings = diagnostics.filter(d => d.severity === DiagnosticSeverity.Warning).map(d => fixMessage(d.message)); - const msgWarning = diagnosticsWarnings.join('\n'); - expect(diagnosticsWarnings, msgWarning).toHaveLength(warnings); -} - -function fixMessage(msg: string): string { - if (msg.startsWith('While validating the AstNode')) { - const inbetween = 'this error is found: '; - return msg.substring(msg.indexOf(inbetween) + inbetween.length); - } - return msg; } diff --git a/examples/ox/README.md b/examples/ox/README.md new file mode 100644 index 0000000..752f6ad --- /dev/null +++ b/examples/ox/README.md @@ -0,0 +1,7 @@ +# Typir applied to OX + +OX is a reduced version of [LOX](/examples/lox/) in order to demonstrate type checking with [Typir](https://typir.org/) for primitives, functions and operators only. + +For examples written in OX, look at some [collected examples](./examples/) or the [test cases](./test/). + +Start experimenting with OX by executing the "Run Extension (OX)" launch configuration. diff --git a/examples/ox/package.json b/examples/ox/package.json index ddd4404..c54a83f 100644 --- a/examples/ox/package.json +++ b/examples/ox/package.json @@ -29,14 +29,14 @@ }, "dependencies": { "commander": "~12.1.0", - "langium": "~3.2.0", + "langium": "~3.3.0", "typir-langium": "~0.0.2", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" }, "devDependencies": { "@types/vscode": "~1.94.0", - "langium-cli": "~3.2.0" + "langium-cli": "~3.3.0" }, "files": [ "bin", diff --git a/examples/ox/src/language/ox-module.ts b/examples/ox/src/language/ox-module.ts index 7861bef..f4d6715 100644 --- a/examples/ox/src/language/ox-module.ts +++ b/examples/ox/src/language/ox-module.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { Module, inject } from 'langium'; +import { LangiumSharedCoreServices, Module, inject } from 'langium'; import { DefaultSharedModuleContext, LangiumServices, LangiumSharedServices, PartialLangiumServices, createDefaultModule, createDefaultSharedModule } from 'langium/lsp'; import { LangiumServicesForTypirBinding, createLangiumModuleForTypirBinding, initializeLangiumTypirServices } from 'typir-langium'; import { OxGeneratedModule, OxGeneratedSharedModule } from './generated/module.js'; @@ -17,25 +17,33 @@ import { OxValidator, registerValidationChecks } from './ox-validator.js'; export type OxAddedServices = { validation: { OxValidator: OxValidator - } + }, + typir: LangiumServicesForTypirBinding, } /** * Union of Langium default services and your custom services - use this as constructor parameter * of custom service classes. */ -export type OxServices = LangiumServices & OxAddedServices & LangiumServicesForTypirBinding +export type OxServices = LangiumServices & OxAddedServices /** * Dependency injection module that overrides Langium default services and contributes the * declared custom services. The Langium defaults can be partially specified to override only * selected services, while the custom services must be fully specified. */ -export const OxModule: Module = { - validation: { - OxValidator: () => new OxValidator() - } -}; +export function createOxModule(shared: LangiumSharedCoreServices): Module { + return { + validation: { + OxValidator: () => new OxValidator() + }, + // For type checking with Typir, inject and merge these modules: + typir: () => inject(Module.merge( + createLangiumModuleForTypirBinding(shared), // the Typir default services + createOxTypirModule(shared), // custom Typir services for LOX + )), + }; +} /** * Create the full set of services required by Langium. @@ -48,9 +56,6 @@ export const OxModule: Module isTypeReference(node) && node.primitive === 'boolean', ]}); // ... but their primitive kind is provided/preset by Typir - const typeNumber = this.typir.factory.primitives.create({ primitiveName: 'number', inferenceRules: [ + const typeNumber = this.typir.factory.Primitives.create({ primitiveName: 'number', inferenceRules: [ isNumberLiteral, (node: unknown) => isTypeReference(node) && node.primitive === 'number', ]}); - const typeVoid = this.typir.factory.primitives.create({ primitiveName: 'void', inferenceRules: + const typeVoid = this.typir.factory.Primitives.create({ primitiveName: 'void', inferenceRules: (node: unknown) => isTypeReference(node) && node.primitive === 'void' }); @@ -50,29 +49,28 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { // define operators // binary operators: numbers => number for (const operator of ['+', '-', '*', '/']) { - this.typir.factory.operators.createBinary({ 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.typir.factory.operators.createBinary({ 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.typir.factory.operators.createBinary({ 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.typir.factory.operators.createBinary({ name: operator, signature: [ + this.typir.factory.Operators.createBinary({ name: operator, signatures: [ { left: typeNumber, right: typeNumber, return: typeBool }, { left: typeBool, right: typeBool, return: typeBool }, ], inferenceRule: binaryInferenceRule }); } // unary operators - 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 }); + 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. @@ -86,7 +84,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { */ // additional inference rules ... - this.typir.inference.addInferenceRule((domainElement: unknown) => { + this.typir.Inference.addInferenceRule((domainElement: unknown) => { // ... for member calls (which are used in expressions) if (isMemberCall(domainElement)) { const ref = domainElement.element.ref; @@ -121,22 +119,22 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { }); // explicit validations for typing issues, realized with Typir (which replaced corresponding functions in the OxValidator!) - this.typir.validation.collector.addValidationRule( + this.typir.validation.Collector.addValidationRule( (node: unknown, typir: TypirServices) => { if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { - return typir.validation.constraints.ensureNodeIsAssignable(node.condition, typeBool, + return typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool, () => { message: "Conditions need to be evaluated to 'boolean'.", domainProperty: 'condition' }); } if (isVariableDeclaration(node)) { return [ - ...typir.validation.constraints.ensureNodeHasNotType(node, typeVoid, + ...typir.validation.Constraints.ensureNodeHasNotType(node, typeVoid, () => { message: "Variables can't be declared with the type 'void'.", domainProperty: 'type' }), - ...typir.validation.constraints.ensureNodeIsAssignable(node.value, node, + ...typir.validation.Constraints.ensureNodeIsAssignable(node.value, node, (actual, expected) => { message: `The initialization expression '${node.value?.$cstNode?.text}' of type '${actual.name}' is not assignable to the variable '${node.name}' with type '${expected.name}'.`, domainProperty: 'value' }) ]; } if (isAssignmentStatement(node) && node.varRef.ref) { - return typir.validation.constraints.ensureNodeIsAssignable(node.value, node.varRef.ref, + return typir.validation.Constraints.ensureNodeIsAssignable(node.value, node.varRef.ref, (actual, expected) => { message: `The expression '${node.value.$cstNode?.text}' of type '${actual.name}' is not assignable to the variable '${node.varRef.ref!.name}' with type '${expected.name}'.`, domainProperty: 'value', @@ -146,7 +144,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { const functionDeclaration = AstUtils.getContainerOfType(node, isFunctionDeclaration); if (functionDeclaration && functionDeclaration.returnType.primitive !== 'void' && node.value) { // the return value must fit to the return type of the function - return typir.validation.constraints.ensureNodeIsAssignable(node.value, functionDeclaration.returnType, + return typir.validation.Constraints.ensureNodeIsAssignable(node.value, functionDeclaration.returnType, () => { message: `The expression '${node.value!.$cstNode?.text}' is not usable as return value for the function '${functionDeclaration.name}'.`, domainProperty: 'value' }); } } @@ -155,7 +153,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { ); // check for unique function declarations - this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); + this.typir.validation.Collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); } onNewAstNode(domainElement: AstNode): void { @@ -164,7 +162,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { if (isFunctionDeclaration(domainElement)) { const functionName = domainElement.name; // define function type - this.typir.factory.functions.create({ + 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 }, @@ -177,17 +175,18 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { * - additionally, validations for the assigned values to the expected parameter( type)s are derived */ inferenceRuleForCalls: { filter: isMemberCall, - matching: (call: MemberCall) => isFunctionDeclaration(call.element.ref) && call.element.ref.name === functionName, - inputArguments: (call: MemberCall) => call.arguments - // TODO does OX support overloaded function declarations? add a scope provider for that ... - } + matching: (call: MemberCall) => isFunctionDeclaration(call.element.ref) && call.explicitOperationCall && call.element.ref.name === functionName, + inputArguments: (call: MemberCall) => call.arguments // they are needed to validate, that the given arguments are assignable to the parameters + // Note that OX does not support overloaded function declarations for simplicity: Look into LOX to see how to handle overloaded functions and methods! + }, + associatedDomainElement: domainElement, }); } } } -export function createOxTypirModule(langiumServices: LangiumSharedServices): Module { +export function createOxTypirModule(langiumServices: LangiumSharedCoreServices): Module { return { // specific configurations for OX TypeCreator: (typirServices) => new OxTypeCreator(typirServices, langiumServices), diff --git a/examples/ox/test/ox-type-checking-operators.test.ts b/examples/ox/test/ox-type-checking-operators.test.ts index b7cb65f..92472ac 100644 --- a/examples/ox/test/ox-type-checking-operators.test.ts +++ b/examples/ox/test/ox-type-checking-operators.test.ts @@ -9,7 +9,6 @@ import { validateOx } from './ox-type-checking-utils.js'; describe('Test type checking for statements and variables in OX', () => { - // TODO add new test cases to LOX as well test('binary operators', async () => { await validateOx('var myResult: number = 2 + 3;', 0); await validateOx('var myResult: number = 2 - 3;', 0); @@ -52,8 +51,19 @@ describe('Test type checking for statements and variables in OX', () => { await validateOx('var myVar : boolean = 2 == false;', 1); }); - test('Only a single problem with the inner expression, since the type of "+" is always number!', async () => { - await validateOx('var myVar : number = 2 + (2 == false);', 2); // TODO should be only 1 problem ... + test('Only a single problem with the inner expression, since the type of "*" is always number!', async () => { + await validateOx('var myVar : number = 2 * (2 * false);', [ + "While validating the AstNode '(2 * false)', this error is found: The given operands for the function '*' match the expected types only partially.", + ]); + }); + + test('Two issues in nested expressions, since "*" expects always numbers, while "and" returns always booleans!', async () => { + await validateOx('var myVar : number = 2 * (2 and false);', [ + // this is obvious: left and right need to have the same type + "While validating the AstNode '(2 and false)', this error is found: The given operands for the function 'and' match the expected types only partially.", + // '*' supports only numbers for left and right, but the right operand is always boolean as result of the 'and' operator + "While validating the AstNode '2 * (2 and false)', this error is found: The given operands for the function '*' match the expected types only partially.", + ]); }); }); diff --git a/examples/ox/test/ox-type-checking-utils.ts b/examples/ox/test/ox-type-checking-utils.ts index 604311f..1fa4c3d 100644 --- a/examples/ox/test/ox-type-checking-utils.ts +++ b/examples/ox/test/ox-type-checking-utils.ts @@ -8,22 +8,35 @@ import { EmptyFileSystem } from 'langium'; import { parseDocument } from 'langium/test'; import { deleteAllDocuments } from 'typir-langium'; import { afterEach, expect } from 'vitest'; -import type { Diagnostic } from 'vscode-languageserver-types'; -import { createOxServices } from '../src/language/ox-module.js'; -import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; import { isFunctionType } from '../../../packages/typir/lib/kinds/function/function-type.js'; +import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; +import { createOxServices } from '../src/language/ox-module.js'; +import { fail } from 'assert'; export const oxServices = createOxServices(EmptyFileSystem).Ox; export const operatorNames = ['-', '*', '/', '+', '<', '<=', '>', '>=', 'and', 'or', '==', '==', '!=', '!=', '!', '-']; afterEach(async () => { - await deleteAllDocuments(oxServices); + await deleteAllDocuments(oxServices.shared); // check, that there are no user-defined classes and functions after clearing/invalidating all LOX documents - expectTypirTypes(oxServices, isFunctionType, ...operatorNames); + expectTypirTypes(oxServices.typir, isFunctionType, ...operatorNames); }); -export async function validateOx(ox: string, errors: number) { +export async function validateOx(ox: string, errors: number | string | string[]) { const document = await parseDocument(oxServices, ox.trim()); - const diagnostics: Diagnostic[] = await oxServices.validation.DocumentValidator.validateDocument(document); - expect(diagnostics, diagnostics.map(d => d.message).join('\n')).toHaveLength(errors); + const diagnostics: string[] = (await oxServices.validation.DocumentValidator.validateDocument(document)).map(d => d.message); + const msgError = diagnostics.join('\n'); + if (typeof errors === 'number') { + expect(diagnostics, msgError).toHaveLength(errors); + } else if (typeof errors === 'string') { + expect(diagnostics, msgError).toHaveLength(1); + expect(diagnostics[0], msgError).includes(errors); + } else { + expect(diagnostics, msgError).toHaveLength(errors.length); + for (const expected of errors) { + if (diagnostics.some(dia => dia.includes(expected)) === false) { + fail(`This issue is expected:\n${expected}\n... but it is not contained in these found issues:\n${msgError}`); + } + } + } } diff --git a/package-lock.json b/package-lock.json index 6c5283d..66f1fba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "license": "MIT", "dependencies": { "commander": "~12.1.0", - "langium": "~3.2.0", + "langium": "~3.3.0", "typir-langium": "~0.0.2", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" @@ -50,7 +50,7 @@ }, "devDependencies": { "@types/vscode": "~1.94.0", - "langium-cli": "~3.2.0" + "langium-cli": "~3.3.0" }, "engines": { "vscode": "^1.67.0" @@ -70,7 +70,7 @@ "license": "MIT", "dependencies": { "commander": "~12.1.0", - "langium": "~3.2.0", + "langium": "~3.3.0", "typir-langium": "~0.0.2", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" @@ -80,7 +80,7 @@ }, "devDependencies": { "@types/vscode": "~1.94.0", - "langium-cli": "~3.2.0" + "langium-cli": "~3.3.0" }, "engines": { "vscode": "^1.67.0" @@ -2381,9 +2381,9 @@ } }, "node_modules/langium": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.2.0.tgz", - "integrity": "sha512-HxAPgCVC7X+dCN99QKlZMEoaLW4s/mt0IImYrP6ooEBOMh8lJYdFNNSpJ5NIOE+WFwQd3xa2phTJDmJhOWVR7A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.0.tgz", + "integrity": "sha512-y1n1MxeHtXvE0Ksi/HjLCvesHm3/Vr0pyyuA1fn+vmGSYR81NkxnC3bU4kd2o0CrZaz8xekz9rhm+lGfRgNFzw==", "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", @@ -2396,17 +2396,17 @@ } }, "node_modules/langium-cli": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/langium-cli/-/langium-cli-3.2.0.tgz", - "integrity": "sha512-4JWeCMuTHyFO+GCnOVT8+jygdob4KnU0uh/26cMxgZ1FlenAk8zrOnrXbuUzIm0FAIetCqrR6GUXqeko+Vg5og==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/langium-cli/-/langium-cli-3.3.0.tgz", + "integrity": "sha512-QWvlOYdLbso8/lv6Ma+SBtvMN9k70JrplLx6VSIcV7gJNDTXeS+tjwC/f6T0aco1fg8uLL8GiAcaMovd1FnneA==", "dev": true, "dependencies": { "chalk": "~5.3.0", "commander": "~11.0.0", "fs-extra": "~11.1.1", "jsonschema": "~1.4.1", - "langium": "~3.2.0", - "langium-railroad": "~3.2.0", + "langium": "~3.3.0", + "langium-railroad": "~3.3.0", "lodash": "~4.17.21" }, "bin": { @@ -2431,12 +2431,12 @@ } }, "node_modules/langium-railroad": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/langium-railroad/-/langium-railroad-3.2.0.tgz", - "integrity": "sha512-8wJqRid1udSH9PKo8AkRrJCUNHQ6Xu9tGi+//bLdHGDdlK9gpps1AwO71ufE864/so77K4ZmqBuLnBnxPcGs/Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/langium-railroad/-/langium-railroad-3.3.0.tgz", + "integrity": "sha512-x56CU0KnLoqYLkHEPDJjFoekFoCVbbZbmHduldiXjKD8owt6t5aqgWfg31OeMeR+7XgONZTtmsO76yl6GvEkzQ==", "dev": true, "dependencies": { - "langium": "~3.2.0", + "langium": "~3.3.0", "railroad-diagrams": "~1.0.0" } }, @@ -4079,7 +4079,7 @@ "version": "0.0.2", "license": "MIT", "dependencies": { - "langium": "~3.2.0", + "langium": "~3.3.0", "typir": "~0.0.2" }, "engines": { diff --git a/packages/typir-langium/README.md b/packages/typir-langium/README.md index 5659869..b88f2cf 100644 --- a/packages/typir-langium/README.md +++ b/packages/typir-langium/README.md @@ -1,6 +1,7 @@ # Typir integration for Langium -Typir-Langium is a framework for type checking of languages developed with [Langium](https://langium.org). +Typir-Langium is a framework for type checking of languages developed with [Langium](https://langium.org), +the language workbench for developing textual domain-specific languages (DSLs) in the web. ## Installation @@ -12,6 +13,23 @@ npm install typir-langium Will follow! +Important design decisions: + +- Typir-Langium does not depend on `langium/lsp`, i.e. Typir-Langium can be used even for Langium-based DSLs which don't use LSP. + +Integrate Typir as additional Langium service into your DSL. + +```typescript +export type MyDSLAddedServices = { + // ... + typir: LangiumServicesForTypirBinding, + // ... +} +``` + +In case of a [multi-language project](https://langium.org/docs/recipes/multiple-languages/), this approach enables you to manage multiple type systems in parallel by having `typir1: LangiumServicesForTypirBinding`, `typir2: LangiumServicesForTypirBinding` and so on. + + ## Examples Look at the examples in the `examples` folder of the repo ([here](../../examples)). There we have some demo projects for you to get started. diff --git a/packages/typir-langium/package.json b/packages/typir-langium/package.json index 3717ddd..fbf68de 100644 --- a/packages/typir-langium/package.json +++ b/packages/typir-langium/package.json @@ -48,7 +48,7 @@ }, "bugs": "https://github.com/TypeFox/typir/issues", "dependencies": { - "langium": "~3.2.0", + "langium": "~3.3.0", "typir": "~0.0.2" } } diff --git a/packages/typir-langium/src/features/langium-caching.ts b/packages/typir-langium/src/features/langium-caching.ts index f44215d..17207bf 100644 --- a/packages/typir-langium/src/features/langium-caching.ts +++ b/packages/typir-langium/src/features/langium-caching.ts @@ -4,8 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, ContextCache, DocumentState, LangiumSharedCoreServices, URI } from 'langium'; -import { LangiumSharedServices } from 'langium/lsp'; +import { AstNode, DocumentCache, DocumentState, LangiumSharedCoreServices } from 'langium'; import { CachePending, DomainElementInferenceCaching, Type } from 'typir'; import { getDocumentKey } from '../utils/typir-langium-utils.js'; @@ -13,7 +12,7 @@ import { getDocumentKey } from '../utils/typir-langium-utils.js'; export class LangiumDomainElementInferenceCaching implements DomainElementInferenceCaching { protected readonly cache: DocumentCache; // removes cached AstNodes, if their underlying LangiumDocuments are invalidated - constructor(langiumServices: LangiumSharedServices) { + constructor(langiumServices: LangiumSharedCoreServices) { this.cache = new DocumentCache(langiumServices, DocumentState.IndexedReferences); } @@ -48,45 +47,3 @@ export class LangiumDomainElementInferenceCaching implements DomainElementInfere return this.cache.has(key, domainElement) && this.cache.get(key, domainElement) === CachePending; } } - - -// TODO this is copied from Langium, since the introducing PR #1659 will be included in the upcoming Langium version 3.3 (+ PR #1712), after releasing v3.3 this class can be removed completely! -/** - * Every key/value pair in this cache is scoped to a document. - * If this document is changed or deleted, all associated key/value pairs are deleted. - */ -export class DocumentCache extends ContextCache { - - /** - * Creates a new document cache. - * - * @param sharedServices Service container instance to hook into document lifecycle events. - * @param state Optional document state on which the cache should evict. - * If not provided, the cache will evict on `DocumentBuilder#onUpdate`. - * *Deleted* documents are considered in both cases. - * - * Providing a state here will use `DocumentBuilder#onDocumentPhase` instead, - * which triggers on all documents that have been affected by this change, assuming that the - * state is `DocumentState.Linked` or a later state. - */ - constructor(sharedServices: LangiumSharedCoreServices, state?: DocumentState) { - super(uri => uri.toString()); - if (state) { - this.toDispose.push(sharedServices.workspace.DocumentBuilder.onDocumentPhase(state, document => { - this.clear(document.uri.toString()); - })); - this.toDispose.push(sharedServices.workspace.DocumentBuilder.onUpdate((_changed, deleted) => { - for (const uri of deleted) { // react only on deleted documents - this.clear(uri); - } - })); - } else { - this.toDispose.push(sharedServices.workspace.DocumentBuilder.onUpdate((changed, deleted) => { - const allUris = changed.concat(deleted); // react on both changed and deleted documents - for (const uri of allUris) { - this.clear(uri); - } - })); - } - } -} diff --git a/packages/typir-langium/src/features/langium-type-creator.ts b/packages/typir-langium/src/features/langium-type-creator.ts index 63587b5..dcf87ea 100644 --- a/packages/typir-langium/src/features/langium-type-creator.ts +++ b/packages/typir-langium/src/features/langium-type-creator.ts @@ -4,22 +4,34 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, DocumentState, interruptAndCheck, LangiumDocument } from 'langium'; -import { LangiumSharedServices } from 'langium/lsp'; +import { AstNode, AstUtils, DocumentState, interruptAndCheck, LangiumDocument, LangiumSharedCoreServices } from 'langium'; import { Type, TypeEdge, TypeGraph, TypeGraphListener, TypirServices } from 'typir'; import { getDocumentKeyForDocument, getDocumentKeyForURI } from '../utils/typir-langium-utils.js'; -export interface LangiumTypeCreator { // TODO Registry instead? +/** + * This service provides the API to define the actual types, inference rules and validation rules + * for a textual DSL developed with Langium in order to include them into the Langium lifecycle. + */ +export interface LangiumTypeCreator { + /** + * This function needs to be called once to trigger the initialization process. + * Depending on the implemention, it might or might not call onInitialize(). + */ triggerInitialization(): void; /** - * For the initialization of the type system, e.g. to register primitive types and operators, inference rules and validation rules. - * This method will be executed once before the 1st added/updated/removed domain element. + * For the initialization of the type system, e.g. to register primitive types and operators, inference rules and validation rules, + * which are constant and don't depend on the actual domain elements. + * This method will be executed once before the first added/updated/removed domain element. */ onInitialize(): void; - /** React on updates of the AST in order to add/remove corresponding types from the type system, e.g. user-definied functions. */ - onNewAstNode(domainElement: unknown): void; + /** + * React on updates of the AST in order to add/remove corresponding types from the type system, + * e.g. for user-definied functions to create corresponding function types in the type graph. + * @param domainElement an AstNode of the current AST + */ + onNewAstNode(domainElement: AstNode): void; } export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator, TypeGraphListener { @@ -28,26 +40,29 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator, protected readonly documentTypesMap: Map = new Map(); protected readonly typeGraph: TypeGraph; - constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { - this.typeGraph = typirServices.graph; + constructor(typirServices: TypirServices, langiumServices: LangiumSharedCoreServices) { + this.typeGraph = typirServices.infrastructure.Graph; - // for new and updated documents - langiumServices.workspace.DocumentBuilder.onBuildPhase(DocumentState.IndexedReferences, async (documents, cancelToken) => { + // for new and updated documents: + // Create Typir types after completing the Langium 'ComputedScopes' phase, since they need to be available for the following Linking phase + langiumServices.workspace.DocumentBuilder.onBuildPhase(DocumentState.ComputedScopes, async (documents, cancelToken) => { for (const document of documents) { await interruptAndCheck(cancelToken); // notify Typir about each contained node of the processed document - this.handleProcessedDocument(document); + this.handleProcessedDocument(document); // takes care about the invalid AstNodes as well } }); - // for deleted documents + + // for deleted documents: + // Delete Typir types which are derived from AstNodes of deleted documents langiumServices.workspace.DocumentBuilder.onUpdate((_changed, deleted) => { deleted .map(del => getDocumentKeyForURI(del)) .forEach(del => this.invalidateTypesOfDocument(del)); }); - // get informed about added/removed types + // get informed about added/removed types in Typir's type graph this.typeGraph.addListener(this); } @@ -120,7 +135,7 @@ export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator, } export class PlaceholderLangiumTypeCreator extends AbstractLangiumTypeCreator { - constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { + constructor(typirServices: TypirServices, langiumServices: LangiumSharedCoreServices) { super(typirServices, langiumServices); } override onInitialize(): void { diff --git a/packages/typir-langium/src/features/langium-validation.ts b/packages/typir-langium/src/features/langium-validation.ts index dc58b19..df70e09 100644 --- a/packages/typir-langium/src/features/langium-validation.ts +++ b/packages/typir-langium/src/features/langium-validation.ts @@ -4,22 +4,23 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, ValidationAcceptor, ValidationChecks } from 'langium'; -import { LangiumServices } from 'langium/lsp'; +import { AstNode, LangiumDefaultCoreServices, ValidationAcceptor, ValidationChecks } from 'langium'; import { TypirServices, ValidationProblem } from 'typir'; import { LangiumServicesForTypirBinding } from '../typir-langium.js'; -export function registerTypirValidationChecks(services: LangiumServices & LangiumServicesForTypirBinding) { - const registry = services.validation.ValidationRegistry; - const validator = services.TypeValidation; +export function registerTypirValidationChecks(langiumServices: LangiumDefaultCoreServices, typirServices: LangiumServicesForTypirBinding) { + const registry = langiumServices.validation.ValidationRegistry; + const validator = typirServices.TypeValidation; + registry.registerBeforeDocument(validator.checkTypingProblemsWithTypirBeforeDocument, validator); const checks: ValidationChecks = { AstNode: validator.checkTypingProblemsWithTypir, // checking each node is not performant, improve the API, see below! }; registry.register(checks, validator); + registry.registerAfterDocument(validator.checkTypingProblemsWithTypirAfterDocument, validator); } /* -* TODO validation with Typir for Langium +* TODO Ideas and features for the validation with Typir for Langium * * What to validate: * - Is it possible to infer a type at all? Type vs undefined @@ -38,36 +39,53 @@ export function registerTypirValidationChecks(services: LangiumServices & Langiu * Apply the same ideas for InferenceRules as well! */ -export class LangiumTypirValidator { - protected readonly services: TypirServices; - - constructor(services: LangiumServicesForTypirBinding) { - this.services = services; - } +export interface LangiumTypirValidator { + /** + * Will be called once before starting the validation of a LangiumDocument. + * @param rootNode the root node of the current document + * @param accept receives the found validation hints + */ + checkTypingProblemsWithTypirBeforeDocument(rootNode: AstNode, accept: ValidationAcceptor): void; /** * Executes all checks, which are directly derived from the current Typir configuration, - * i.e. arguments fit to parameters for function calls (including operands for operators). + * i.e. checks that arguments fit to parameters for function calls (including operands for operators). * @param node the current AST node to check regarding typing issues * @param accept receives the found validation hints */ - checkTypingProblemsWithTypir(node: AstNode, accept: ValidationAcceptor) { - // TODO use the new validation registry API in Langium v3.3 instead! - if (node.$container === undefined) { - this.report(this.services.validation.collector.validateBefore(node), node, accept); + checkTypingProblemsWithTypir(node: AstNode, accept: ValidationAcceptor): void; - AstUtils.streamAst(node).forEach(child => { - this.report(this.services.validation.collector.validate(child), child, accept); - }); + /** + * Will be called once after finishing the validation of a LangiumDocument. + * @param rootNode the root node of the current document + * @param accept receives the found validation hints + */ + checkTypingProblemsWithTypirAfterDocument(rootNode: AstNode, accept: ValidationAcceptor): void; +} - this.report(this.services.validation.collector.validateAfter(node), node, accept); - } +export class DefaultLangiumTypirValidator implements LangiumTypirValidator { + protected readonly services: TypirServices; + + constructor(services: LangiumServicesForTypirBinding) { + this.services = services; + } + + checkTypingProblemsWithTypirBeforeDocument(rootNode: AstNode, accept: ValidationAcceptor): void { + this.report(this.services.validation.Collector.validateBefore(rootNode), rootNode, accept); + } + + checkTypingProblemsWithTypir(node: AstNode, accept: ValidationAcceptor) { + this.report(this.services.validation.Collector.validate(node), node, accept); + } + + checkTypingProblemsWithTypirAfterDocument(rootNode: AstNode, accept: ValidationAcceptor): void { + this.report(this.services.validation.Collector.validateAfter(rootNode), rootNode, accept); } protected report(problems: ValidationProblem[], node: AstNode, accept: ValidationAcceptor): void { // print all found problems for the given AST node for (const problem of problems) { - const message = this.services.printer.printValidationProblem(problem); + const message = this.services.Printer.printValidationProblem(problem); accept(problem.severity, message, { node, property: problem.domainProperty, index: problem.domainIndex }); } } diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index e4abbed..7bd79a9 100644 --- a/packages/typir-langium/src/typir-langium.ts +++ b/packages/typir-langium/src/typir-langium.ts @@ -4,12 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { LangiumServices, LangiumSharedServices } from 'langium/lsp'; -import { DeepPartial, DefaultTypeRelationshipCaching, DefaultTypirServiceModule, Module, TypirServices } from 'typir'; +import { LangiumDefaultCoreServices, LangiumSharedCoreServices } from 'langium'; +import { DeepPartial, DefaultTypirServiceModule, Module, PartialTypirServices, TypirServices } from 'typir'; import { LangiumDomainElementInferenceCaching } from './features/langium-caching.js'; import { LangiumProblemPrinter } from './features/langium-printing.js'; -import { PlaceholderLangiumTypeCreator, LangiumTypeCreator } from './features/langium-type-creator.js'; -import { LangiumTypirValidator, registerTypirValidationChecks } from './features/langium-validation.js'; +import { LangiumTypeCreator, PlaceholderLangiumTypeCreator } from './features/langium-type-creator.js'; +import { DefaultLangiumTypirValidator, LangiumTypirValidator, registerTypirValidationChecks } from './features/langium-validation.js'; /** * Additional Typir-Langium services to manage the Typir services @@ -24,36 +24,58 @@ export type LangiumServicesForTypirBinding = TypirServices & TypirLangiumService export type PartialTypirLangiumServices = DeepPartial -/** - * Contains all customizations of Typir to simplify type checking for DSLs developed with Langium, - * the language workbench for textual domain-specific languages (DSLs) in the web (https://langium.org/). - */ -export function createLangiumModuleForTypirBinding(langiumServices: LangiumSharedServices): Module { - return { - // use all core Typir services: - ...DefaultTypirServiceModule, - // replace some of the core Typir default implementations for Langium: - printer: () => new LangiumProblemPrinter(), +export function createLangiumSpecificTypirServicesModule(langiumServices: LangiumSharedCoreServices): Module { + // replace some implementations for the core Typir services + const LangiumSpecifics: Module = { + Printer: () => new LangiumProblemPrinter(), caching: { - typeRelationships: (services) => new DefaultTypeRelationshipCaching(services), // this is the same implementation as in core Typir, since all edges of removed types are removed as well - domainElementInference: () => new LangiumDomainElementInferenceCaching(langiumServices), + DomainElementInference: () => new LangiumDomainElementInferenceCaching(langiumServices), }, - // provide implementations for the additional services for the Typir-Langium-binding: - TypeValidation: (typirServices) => new LangiumTypirValidator(typirServices), + }; + return Module.merge( + // use all core Typir services: + DefaultTypirServiceModule, + // replace some of the core Typir default implementations for Langium: + LangiumSpecifics + ); +} + +export function createDefaultTypirLangiumServices(langiumServices: LangiumSharedCoreServices): Module { + return { + TypeValidation: (typirServices) => new DefaultLangiumTypirValidator(typirServices), TypeCreator: (typirServices) => new PlaceholderLangiumTypeCreator(typirServices, langiumServices), }; } -export function initializeLangiumTypirServices(services: LangiumServices & LangiumServicesForTypirBinding): void { +/** + * Contains all customizations of Typir to simplify type checking for DSLs developed with Langium, + * the language workbench for textual domain-specific languages (DSLs) in the web (https://langium.org/). + */ +export function createLangiumModuleForTypirBinding(langiumServices: LangiumSharedCoreServices): Module { + return Module.merge( + // the core Typir services (with adapted implementations for Typir-Langium) + createLangiumSpecificTypirServicesModule(langiumServices), + // the additional services for the Typir-Langium binding (with implementations) + createDefaultTypirLangiumServices(langiumServices), + ); +} + +export function initializeLangiumTypirServices(langiumServices: LangiumDefaultCoreServices, typirServices: LangiumServicesForTypirBinding): void { // register the type-related validations of Typir at the Langium validation registry - registerTypirValidationChecks(services); + registerTypirValidationChecks(langiumServices, typirServices); // initialize the type creation (this is not done automatically by dependency injection!) - services.TypeCreator.triggerInitialization(); - // TODO This does not work, if there is no Language Server used, e.g. in test cases! - // services.shared.lsp.LanguageServer.onInitialized(_params => { - // services.TypeCreator.triggerInitialization(); - // }); + typirServices.TypeCreator.triggerInitialization(); + + /* + Don't use the following code ... + services.shared.lsp.LanguageServer.onInitialized(_params => { + services.TypeCreator.triggerInitialization(); + }); + ... since it requires a Language Server, which is not true in all cases, e.g. in test cases. + Without this approach, the parameter "services: LangiumServices" can be relaxed to "services: LangiumDefaultCoreServices" to support non-LSP scenarios! + */ + // maybe using services.shared.workspace.WorkspaceManager.initializeWorkspace/loadAdditionalDocuments // another idea is to use eagerLoad(inject(...)) when creating the services } diff --git a/packages/typir-langium/src/utils/typir-langium-utils.ts b/packages/typir-langium/src/utils/typir-langium-utils.ts index 6fcab2e..bf13111 100644 --- a/packages/typir-langium/src/utils/typir-langium-utils.ts +++ b/packages/typir-langium/src/utils/typir-langium-utils.ts @@ -4,8 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, LangiumDocument, URI } from 'langium'; -import { LangiumServices } from 'langium/lsp'; +import { AstNode, AstUtils, LangiumDocument, LangiumSharedCoreServices, URI } from 'langium'; export function getDocumentKeyForURI(document: URI): string { return document.toString(); @@ -19,11 +18,11 @@ export function getDocumentKey(node: AstNode): string { return getDocumentKeyForDocument(AstUtils.getDocument(node)); } -export async function deleteAllDocuments(services: LangiumServices) { - const docsToDelete = services.shared.workspace.LangiumDocuments.all +export async function deleteAllDocuments(services: LangiumSharedCoreServices) { + const docsToDelete = services.workspace.LangiumDocuments.all .map((x) => x.uri) .toArray(); - await services.shared.workspace.DocumentBuilder.update( + await services.workspace.DocumentBuilder.update( [], // update no documents docsToDelete // delete all documents ); diff --git a/packages/typir/README.md b/packages/typir/README.md index e0beee0..9de7f98 100644 --- a/packages/typir/README.md +++ b/packages/typir/README.md @@ -12,6 +12,11 @@ npm install typir Will follow! +Important design decisions: + +- Typir is a stand-alone library and has no dependencies to any existing language workbench. + + ## Examples Look at the examples in the `examples` folder of the repo ([here](../../examples)). There we have some demo projects for you to get started. diff --git a/packages/typir/src/graph/type-node.ts b/packages/typir/src/graph/type-node.ts index d4b6264..1e0e13f 100644 --- a/packages/typir/src/graph/type-node.ts +++ b/packages/typir/src/graph/type-node.ts @@ -33,6 +33,16 @@ export interface PreconditionsForInitializationState { referencesToBeCompleted?: TypeReference[]; // or later/more } +/** + * Contains properties which are be relevant for all types to create, + * i.e. it is used for specifying details of all types to create. + */ +export interface TypeDetails { + /** An element from the domain might be associated with the new type to create, + * e.g. the declaration node in the AST (e.g. a FunctionDeclarationNode is associated with the corresponding FunctionType). */ + associatedDomainElement?: unknown; +} + /** * Design decisions: * - features of types are realized/determined by their kinds @@ -53,8 +63,17 @@ export abstract class Type { protected readonly edgesIncoming: Map = new Map(); protected readonly edgesOutgoing: Map = new Map(); - constructor(identifier: string | undefined) { + /** + * The current type might be associated with an element from the domain, e.g. the corresponding declaration node in the AST. + * This domain element is _not_ used for managing the lifecycles of this type, + * since it should be usable for any domain-specific purpose. + * Therefore, the use and update of this feature is under the responsibility of the user of Typir. + */ + readonly associatedDomainElement: unknown | undefined; + + constructor(identifier: string | undefined, typeDetails: TypeDetails) { this.identifier = identifier; + this.associatedDomainElement = typeDetails.associatedDomainElement; } diff --git a/packages/typir/src/initialization/type-initializer.ts b/packages/typir/src/initialization/type-initializer.ts index e1ff512..a1b2c6b 100644 --- a/packages/typir/src/initialization/type-initializer.ts +++ b/packages/typir/src/initialization/type-initializer.ts @@ -40,14 +40,14 @@ export abstract class TypeInitializer { if (!key) { throw new Error('missing identifier!'); } - const existingType = this.services.graph.getType(key); + const existingType = this.services.infrastructure.Graph.getType(key); if (existingType) { // ensure, that the same type is not duplicated! this.typeToReturn = existingType as T; newType.dispose(); } else { this.typeToReturn = newType; - this.services.graph.addNode(this.typeToReturn); + this.services.infrastructure.Graph.addNode(this.typeToReturn); } // inform and clear all listeners diff --git a/packages/typir/src/initialization/type-reference.ts b/packages/typir/src/initialization/type-reference.ts index 26fda18..29a8d77 100644 --- a/packages/typir/src/initialization/type-reference.ts +++ b/packages/typir/src/initialization/type-reference.ts @@ -4,12 +4,11 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeInferenceCollectorListener, TypeInferenceRule } from '../services/inference.js'; import { TypeEdge } from '../graph/type-edge.js'; import { TypeGraphListener } from '../graph/type-graph.js'; -import { isType, Type } from '../graph/type-node.js'; +import { Type } from '../graph/type-node.js'; +import { TypeInferenceCollectorListener, TypeInferenceRule } from '../services/inference.js'; import { TypirServices } from '../typir.js'; -import { TypeInitializer } from './type-initializer.js'; import { TypeSelector } from './type-selector.js'; /** @@ -51,7 +50,6 @@ export class TypeReference implements TypeGraphListener, /** These listeners will be informed, whenever the resolved state of this TypeReference switched from undefined to an actual type or from an actual type to undefined. */ protected readonly listeners: Array> = []; - // TODO introduce TypeReference factory service in order to replace the implementation? constructor(selector: TypeSelector, services: TypirServices) { this.selector = selector; this.services = services; @@ -69,9 +67,9 @@ export class TypeReference implements TypeGraphListener, this.resolvedType = undefined; // react on new types - this.services.graph.addListener(this); + this.services.infrastructure.Graph.addListener(this); // react on new inference rules - this.services.inference.addListener(this); + this.services.Inference.addListener(this); // don't react on state changes of already existing types which are not (yet) completed, since TypeSelectors don't care about the initialization state of types // try to resolve now @@ -80,8 +78,8 @@ export class TypeReference implements TypeGraphListener, 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); + this.services.infrastructure.Graph.removeListener(this); + this.services.Inference.removeListener(this); } getType(): T | undefined { @@ -100,7 +98,7 @@ export class TypeReference implements TypeGraphListener, } // try to resolve the type - const resolvedType = this.tryToResolve(this.selector); + const resolvedType = this.services.infrastructure.TypeResolver.tryToResolve(this.selector); if (resolvedType) { // the type is successfully resolved! @@ -115,36 +113,6 @@ export class TypeReference implements TypeGraphListener, } } - /** - * Tries to find the specified type in the type system. - * This method does not care about the initialization state of the found type, - * this method is restricted to just search and find any type according to the given TypeSelector. - * @param selector the specification for the desired type - * @returns the found type or undefined, it there is no such type in the type system - */ - protected tryToResolve(selector: TypeSelector): T | undefined { - if (isType(selector)) { - // TODO is there a way to explicitly enforce/ensure "as T"? - return selector as T; - } else if (typeof selector === 'string') { - return this.services.graph.getType(selector) as T; - } else if (selector instanceof TypeInitializer) { - return selector.getTypeInitial(); - } else if (selector instanceof TypeReference) { - return selector.getType(); - } else if (typeof selector === 'function') { - return this.tryToResolve(selector()); // execute the function and try to recursively resolve the returned result again - } else { // the selector is of type 'known' => do type inference on it - const result = this.services.inference.inferType(selector); - // TODO failures must not be cached, otherwise a type will never be found in the future!! - if (isType(result)) { - return result as T; - } else { - return undefined; - } - } - } - addListener(listener: TypeReferenceListener, informAboutCurrentState: boolean): void { this.listeners.push(listener); if (informAboutCurrentState) { @@ -194,30 +162,3 @@ export class TypeReference implements TypeGraphListener, // empty, since removed inference rules don't help to resolve a type } } - - -export function resolveTypeSelector(services: TypirServices, selector: TypeSelector): Type { - if (isType(selector)) { - return selector; - } else if (typeof selector === 'string') { - const result = services.graph.getType(selector); - if (result) { - return result; - } else { - throw new Error(`A type with identifier '${selector}' as TypeSelector does not exist in the type graph.`); - } - } else if (selector instanceof TypeInitializer) { - return selector.getTypeFinal(); - } else if (selector instanceof TypeReference) { - return selector.getType(); - } else if (typeof selector === 'function') { - return resolveTypeSelector(services, selector()); // execute the function and try to recursively resolve the returned result again - } else { - const result = services.inference.inferType(selector); - if (isType(result)) { - return result; - } else { - throw new Error(`For '${services.printer.printDomainElement(selector, false)}' as TypeSelector, no type can be inferred.`); - } - } -} diff --git a/packages/typir/src/initialization/type-selector.ts b/packages/typir/src/initialization/type-selector.ts index cfcd8cb..18b95a1 100644 --- a/packages/typir/src/initialization/type-selector.ts +++ b/packages/typir/src/initialization/type-selector.ts @@ -4,17 +4,102 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { Type } from '../graph/type-node.js'; +import { isType, Type } from '../graph/type-node.js'; +import { TypirServices } from '../typir.js'; import { TypeInitializer } from './type-initializer.js'; import { TypeReference } from './type-reference.js'; -// This TypeScript type defines the possible ways to identify a wanted Typir type. // TODO find better names: TypeSpecification, TypeDesignation/Designator, ... ? -export type TypeSelector = +export type BasicTypeSelector = | Type // the instance of the wanted type | string // identifier of the type (in the type graph/map) | TypeInitializer // delayed creation of types | TypeReference // reference to a (maybe delayed) type | unknown // domain node to infer the final type from ; -export type DelayedTypeSelector = TypeSelector | (() => TypeSelector); // TODO + +/** + * This TypeScript type defines the possible ways to identify a desired Typir type. + */ +export type TypeSelector = + | BasicTypeSelector // all base type selectors + | (() => BasicTypeSelector) // all type selectors might be given as functions as well, in order to ease delayed specifications + ; + + +export interface TypeResolvingService { + /** + * Tries to find the specified type in the type system. + * This method does not care about the initialization state of the found type, + * this method is restricted to just search and find any type according to the given TypeSelector. + * @param selector the specification for the desired type + * @returns the found type or undefined, it there is no such type in the type system + */ + tryToResolve(selector: TypeSelector): T | undefined; + + /** + * Finds the specified type in the type system. + * This method does not care about the initialization state of the found type, + * this method is restricted to just search and find any type according to the given TypeSelector. + * @param selector the specification for the desired type + * @returns the found type; or an exception, if the type cannot be resolved + */ + resolve(selector: TypeSelector): T; +} + +export class DefaultTypeResolver implements TypeResolvingService { + protected readonly services: TypirServices; + + constructor(services: TypirServices) { + this.services = services; + } + + tryToResolve(selector: TypeSelector): T | undefined { + if (isType(selector)) { + // TODO is there a way to explicitly enforce/ensure "as T"? + return selector as T; + } else if (typeof selector === 'string') { + return this.services.infrastructure.Graph.getType(selector) as T; + } else if (selector instanceof TypeInitializer) { + return selector.getTypeInitial(); + } else if (selector instanceof TypeReference) { + return selector.getType(); + } else if (typeof selector === 'function') { + return this.tryToResolve(selector()); // execute the function and try to recursively resolve the returned result again + } else { // the selector is of type 'known' => do type inference on it + const result = this.services.Inference.inferType(selector); + // TODO failures must not be cached, otherwise a type will never be found in the future!! + if (isType(result)) { + return result as T; + } else { + return undefined; + } + } + } + + resolve(selector: TypeSelector): T { + if (isType(selector)) { + return selector as T; + } else if (typeof selector === 'string') { + const result = this.services.infrastructure.Graph.getType(selector); + if (result) { + return result as T; + } else { + throw new Error(`A type with identifier '${selector}' as TypeSelector does not exist in the type graph.`); + } + } else if (selector instanceof TypeInitializer) { + return selector.getTypeFinal(); + } else if (selector instanceof TypeReference) { + return selector.getType(); + } else if (typeof selector === 'function') { + return this.resolve(selector()); // execute the function and try to recursively resolve the returned result again + } else { + const result = this.services.Inference.inferType(selector); + if (isType(result)) { + return result as T; + } else { + throw new Error(`For '${this.services.Printer.printDomainElement(selector, false)}' as TypeSelector, no type can be inferred.`); + } + } + } +} diff --git a/packages/typir/src/kinds/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index 97366cb..a58835f 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -9,8 +9,9 @@ import { TypirServices } from '../../typir.js'; import { assertTrue, toArray } from '../../utils/utils.js'; import { BottomType } from './bottom-type.js'; import { isKind, Kind } from '../kind.js'; +import { TypeDetails } from '../../graph/type-node.js'; -export interface BottomTypeDetails { +export interface BottomTypeDetails extends TypeDetails { /** In case of multiple inference rules, later rules are not evaluated anymore, if an earler rule already matched. */ inferenceRules?: InferBottomType | InferBottomType[] } @@ -37,8 +38,12 @@ export class BottomKind implements Kind, BottomFactoryService { constructor(services: TypirServices, options?: Partial) { this.$name = BottomKindName; this.services = services; - this.services.kinds.register(this); - this.options = { + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + } + + protected collectOptions(options?: Partial): BottomKindOptions { + return { // the default values: name: 'never', // the actually overriden values: @@ -48,7 +53,7 @@ export class BottomKind implements Kind, BottomFactoryService { get(typeDetails: BottomTypeDetails): BottomType | undefined { const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as BottomType; + return this.services.infrastructure.Graph.getType(key) as BottomType; } create(typeDetails: BottomTypeDetails): BottomType { @@ -58,9 +63,9 @@ export class BottomKind implements Kind, BottomFactoryService { // note, that the given inference rules are ignored in this case! return this.instance; } - const bottomType = new BottomType(this, this.calculateIdentifier(typeDetails)); + const bottomType = new BottomType(this, this.calculateIdentifier(typeDetails), typeDetails); this.instance = bottomType; - this.services.graph.addNode(bottomType); + this.services.infrastructure.Graph.addNode(bottomType); // register all inference rules for primitives within a single generic inference rule (in order to keep the number of "global" inference rules small) this.registerInferenceRules(typeDetails, bottomType); @@ -71,7 +76,7 @@ export class BottomKind implements Kind, BottomFactoryService { protected registerInferenceRules(typeDetails: BottomTypeDetails, bottomType: BottomType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { - this.services.inference.addInferenceRule((domainElement, _typir) => { + this.services.Inference.addInferenceRule((domainElement, _typir) => { for (const inferenceRule of rules) { if (inferenceRule(domainElement)) { return bottomType; diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 97a3dd1..4223b24 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -9,13 +9,13 @@ import { SubTypeProblem } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; -import { BottomKind, isBottomKind } from './bottom-kind.js'; +import { BottomKind, BottomTypeDetails, isBottomKind } from './bottom-kind.js'; export class BottomType extends Type { override readonly kind: BottomKind; - constructor(kind: BottomKind, identifier: string) { - super(identifier); + constructor(kind: BottomKind, identifier: string, typeDetails: BottomTypeDetails) { + super(identifier, typeDetails); this.kind = kind; this.defineTheInitializationProcessOfThisType({}); // no preconditions } diff --git a/packages/typir/src/kinds/class/class-initializer.ts b/packages/typir/src/kinds/class/class-initializer.ts index 7ebccd6..68f108a 100644 --- a/packages/typir/src/kinds/class/class-initializer.ts +++ b/packages/typir/src/kinds/class/class-initializer.ts @@ -28,12 +28,12 @@ export class ClassTypeInitializer exten this.initialClassType = new ClassType(kind, typeDetails as CreateClassTypeDetails); if (kind.options.typing === 'Structural') { // register structural classes also by their names, since these names are usually used for reference in the DSL/AST! - this.services.graph.addNode(this.initialClassType, kind.calculateIdentifierWithClassNameOnly(typeDetails)); + this.services.infrastructure.Graph.addNode(this.initialClassType, kind.calculateIdentifierWithClassNameOnly(typeDetails)); } - this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, this.initialClassType); + this.inferenceRules = this.createInferenceRules(this.typeDetails, this.initialClassType); // register all the inference rules already now to enable early type inference for this Class type - this.inferenceRules.forEach(rule => services.inference.addInferenceRule(rule, undefined)); // 'undefined', since the Identifier is still missing + this.inferenceRules.forEach(rule => services.Inference.addInferenceRule(rule, undefined)); // 'undefined', since the Identifier is still missing this.initialClassType.addListener(this, true); // trigger directly, if some initialization states are already reached! } @@ -55,22 +55,22 @@ export class ClassTypeInitializer exten if (this.kind.options.typing === 'Structural') { // replace the type in the type graph const nameBasedIdentifier = this.kind.calculateIdentifierWithClassNameOnly(this.typeDetails); - this.services.graph.removeNode(classType, nameBasedIdentifier); - this.services.graph.addNode(readyClassType, nameBasedIdentifier); + this.services.infrastructure.Graph.removeNode(classType, nameBasedIdentifier); + this.services.infrastructure.Graph.addNode(readyClassType, nameBasedIdentifier); } // remove the inference rules for the invalid type - this.inferenceRules.forEach(rule => this.services.inference.removeInferenceRule(rule, undefined)); + this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule, undefined)); // but re-create the inference rules for the new type!! // This is required, since inference rules for different declarations in the AST might be different, but should infer the same Typir type! - this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, readyClassType); - this.inferenceRules.forEach(rule => this.services.inference.addInferenceRule(rule, readyClassType)); + this.inferenceRules = this.createInferenceRules(this.typeDetails, readyClassType); + this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule, readyClassType)); } else { // the class type is unchanged (this is the usual case) // keep the existing inference rules, but register it for the unchanged class type - this.inferenceRules.forEach(rule => this.services.inference.removeInferenceRule(rule, undefined)); - this.inferenceRules.forEach(rule => this.services.inference.addInferenceRule(rule, readyClassType)); + this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule, undefined)); + this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule, readyClassType)); } } @@ -95,105 +95,106 @@ export class ClassTypeInitializer exten override getTypeInitial(): ClassType { return this.initialClassType; } -} - -function createInferenceRules(typeDetails: CreateClassTypeDetails, classKind: ClassKind, classType: ClassType): TypeInferenceRule[] { - const result: TypeInferenceRule[] = []; - if (typeDetails.inferenceRuleForDeclaration) { - result.push({ - inferTypeWithoutChildren(domainElement, _typir) { - if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { + protected createInferenceRules(typeDetails: CreateClassTypeDetails, classType: ClassType): TypeInferenceRule[] { + const result: TypeInferenceRule[] = []; + if (typeDetails.inferenceRuleForDeclaration) { + result.push({ + inferTypeWithoutChildren(domainElement, _typir) { + if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { + return classType; + } else { + return InferenceRuleNotApplicable; + } + }, + inferTypeWithChildrensTypes(_domainElement, _childrenTypes, _typir) { + // TODO check values for fields for structual typing! return classType; - } else { + }, + }); + } + if (typeDetails.inferenceRuleForConstructor) { + result.push(this.createInferenceRuleForLiteral(typeDetails.inferenceRuleForConstructor, classType)); + } + if (typeDetails.inferenceRuleForReference) { + result.push(this.createInferenceRuleForLiteral(typeDetails.inferenceRuleForReference, classType)); + } + if (typeDetails.inferenceRuleForFieldAccess) { + result.push((domainElement, _typir) => { + const result = typeDetails.inferenceRuleForFieldAccess!(domainElement); + if (result === InferenceRuleNotApplicable) { return InferenceRuleNotApplicable; + } else if (typeof result === 'string') { + // get the type of the given field name + const fieldType = classType.getFields(true).get(result); + if (fieldType) { + return fieldType; + } + return { + $problem: InferenceProblem, + domainElement, + inferenceCandidate: classType, + location: `unknown field '${result}'`, + // rule: this, // this does not work with functions ... + subProblems: [], + }; + } else { + return result; // do the type inference for this element instead } - }, - inferTypeWithChildrensTypes(_domainElement, _childrenTypes, _typir) { - // TODO check values for fields for nominal typing! - return classType; - }, - }); - } - if (typeDetails.inferenceRuleForLiteral) { - result.push(createInferenceRuleForLiteral(typeDetails.inferenceRuleForLiteral, classKind, classType)); - } - if (typeDetails.inferenceRuleForReference) { - result.push(createInferenceRuleForLiteral(typeDetails.inferenceRuleForReference, classKind, classType)); - } - if (typeDetails.inferenceRuleForFieldAccess) { - result.push((domainElement, _typir) => { - const result = typeDetails.inferenceRuleForFieldAccess!(domainElement); - if (result === InferenceRuleNotApplicable) { - return InferenceRuleNotApplicable; - } else if (typeof result === 'string') { - // get the type of the given field name - const fieldType = classType.getFields(true).get(result); - if (fieldType) { - return fieldType; - } - return { - $problem: InferenceProblem, - domainElement, - inferenceCandidate: classType, - location: `unknown field '${result}'`, - // rule: this, // this does not work with functions ... - subProblems: [], - }; - } else { - return result; // do the type inference for this element instead - } - }); + }); + } + return result; } - return result; -} -function createInferenceRuleForLiteral(rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): TypeInferenceRule { - const mapListConverter = new MapListConverter(); - return { - inferTypeWithoutChildren(domainElement, _typir) { - const result = rule.filter(domainElement); - if (result) { - const matching = rule.matching(domainElement); - if (matching) { - const inputArguments = rule.inputValuesForFields(domainElement); - if (inputArguments.size >= 1) { - return mapListConverter.toList(inputArguments); + protected createInferenceRuleForLiteral(rule: InferClassLiteral, classType: ClassType): TypeInferenceRule { + const mapListConverter = new MapListConverter(); + const kind = this.kind; + return { + inferTypeWithoutChildren(domainElement, _typir) { + const result = rule.filter(domainElement); + if (result) { + const matching = rule.matching(domainElement); + if (matching) { + const inputArguments = rule.inputValuesForFields(domainElement); + if (inputArguments.size >= 1) { + return mapListConverter.toList(inputArguments); + } else { + // there are no operands to check + return classType; // this case occurs only, if the current class has no fields (including fields of super types) or is nominally typed + } } else { - // there are no operands to check - return classType; // this case occurs only, if the current class has no fields (including fields of super types) or is nominally typed + // the domain element is slightly different } } else { - // the domain element is slightly different + // the domain element has a completely different purpose } - } else { - // the domain element has a completely different purpose - } - // does not match at all - return InferenceRuleNotApplicable; - }, - inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { - const allExpectedFields = classType.getFields(true); - // this class type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 - const checkedFieldsProblems = checkNameTypesMap( - mapListConverter.toMap(childrenTypes), - allExpectedFields, - createTypeCheckStrategy(classKind.options.subtypeFieldChecking, typir) - ); - if (checkedFieldsProblems.length >= 1) { - // (only) for overloaded functions, the types of the parameters need to be inferred in order to determine an exact match - return { - $problem: InferenceProblem, - domainElement, - inferenceCandidate: classType, - location: 'values for fields', - rule: this, - subProblems: checkedFieldsProblems, - }; - } else { - // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors - return classType; - } - }, - }; + // does not match at all + return InferenceRuleNotApplicable; + }, + inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { + const allExpectedFields = classType.getFields(true); + // this class type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 + const checkedFieldsProblems = checkNameTypesMap( + mapListConverter.toMap(childrenTypes), + allExpectedFields, + createTypeCheckStrategy(kind.options.subtypeFieldChecking, typir) + ); + if (checkedFieldsProblems.length >= 1) { + // (only) for overloaded functions, the types of the parameters need to be inferred in order to determine an exact match + return { + $problem: InferenceProblem, + domainElement, + inferenceCandidate: classType, + location: 'values for fields', + rule: this, + subProblems: checkedFieldsProblems, + }; + } else { + // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + return classType; + } + }, + }; + } + } diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index 415efd3..53abdfc 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -5,8 +5,9 @@ ******************************************************************************/ import { assertUnreachable } from 'langium'; +import { TypeDetails } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; -import { TypeReference, resolveTypeSelector } from '../../initialization/type-reference.js'; +import { TypeReference } from '../../initialization/type-reference.js'; import { TypeSelector } from '../../initialization/type-selector.js'; import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; @@ -16,8 +17,7 @@ import { CreateFunctionTypeDetails, FunctionFactoryService } from '../function/f 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 { TopClassKind, TopClassKindName, isTopClassKind } from './top-class-kind.js'; export interface ClassKindOptions { typing: 'Structural' | 'Nominal', // JS classes are nominal, TS structures are structural @@ -35,26 +35,28 @@ export interface CreateFieldDetails { type: TypeSelector; } -export interface ClassTypeDetails { +export interface ClassTypeDetails extends TypeDetails { className: string, superClasses?: TypeSelector | TypeSelector[], fields: CreateFieldDetails[], methods: Array>, // all details of functions can be configured for methods as well, in particular, inference rules for function/method calls! } export interface CreateClassTypeDetails extends ClassTypeDetails { // TODO the generics look very bad! - inferenceRuleForDeclaration?: (domainElement: unknown) => boolean, // TODO what is the purpose for this? what is the difference to literals? - // TODO rename to Constructor call?? - inferenceRuleForLiteral?: InferClassLiteral, // InferClassLiteral | Array>, does not work: https://stackoverflow.com/questions/65129070/defining-an-array-of-differing-generic-types-in-typescript + inferenceRuleForDeclaration?: (domainElement: unknown) => boolean, + inferenceRuleForConstructor?: InferClassLiteral, // InferClassLiteral | Array>, does not work: https://stackoverflow.com/questions/65129070/defining-an-array-of-differing-generic-types-in-typescript inferenceRuleForReference?: InferClassLiteral, inferenceRuleForFieldAccess?: (domainElement: unknown) => string | unknown | InferenceRuleNotApplicable, // name of the field | element to infer the type of the field (e.g. the type) | rule not applicable // inference rules for Method calls are part of "methods: CreateFunctionTypeDetails[]" above! } -// TODO nominal vs structural typing ?? +/** + * Depending on whether the class is structurally or nominally typed, + * different values might be specified, e.g. 'inputValuesForFields' could be empty for nominal classes. + */ export type InferClassLiteral = { filter: (domainElement: unknown) => domainElement is T; matching: (domainElement: T) => boolean; - inputValuesForFields: (domainElement: T) => Map; // simple field name (including inherited fields) => value for this field! TODO implement that, [] for nominal typing + inputValuesForFields: (domainElement: T) => Map; // simple field name (including inherited fields) => value for this field! }; @@ -78,8 +80,13 @@ export class ClassKind implements Kind, ClassFactoryService { constructor(services: TypirServices, options?: Partial) { this.$name = ClassKindName; this.services = services; - this.services.kinds.register(this); - this.options = { // TODO in eigene Methode auslagern! + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + assertTrue(this.options.maximumNumberOfSuperClasses >= 0); // no negative values + } + + protected collectOptions(options?: Partial): ClassKindOptions { + return { // the default values: typing: 'Nominal', maximumNumberOfSuperClasses: 1, @@ -88,7 +95,6 @@ export class ClassKind implements Kind, ClassFactoryService { // the actually overriden values: ...options }; - assertTrue(this.options.maximumNumberOfSuperClasses >= 0); // no negative values } /** @@ -139,7 +145,7 @@ export class ClassKind implements Kind, ClassFactoryService { if (this.options.typing === 'Structural') { // fields const fields: string = typeDetails.fields - .map(f => `${f.name}:${resolveTypeSelector(this.services, f.type)}`) // the names and the types of the fields are relevant, since different field types lead to different class types! + .map(f => `${f.name}:${this.services.infrastructure.TypeResolver.resolve(f.type)}`) // the names and the types of the fields are relevant, since different field types lead to different class types! .sort() // the order of fields does not matter, therefore we need a stable order to make the identifiers comparable .join(','); // methods @@ -153,7 +159,7 @@ export class ClassKind implements Kind, ClassFactoryService { // super classes (TODO oder strukturell per getAllSuperClassX lösen?!) const superClasses: string = toArray(typeDetails.superClasses) .map(selector => { - const type = resolveTypeSelector(this.services, selector); + const type = this.services.infrastructure.TypeResolver.resolve(selector); assertType(type, isClassType); return type.getIdentifier(); }) @@ -181,16 +187,12 @@ export class ClassKind implements Kind, ClassFactoryService { } getMethodFactory(): FunctionFactoryService { - return this.services.factory.functions; - } - - getOrCreateTopClassType(typeDetails: TopClassTypeDetails): TopClassType { - return this.getTopClassKind().getOrCreateTopClassType(typeDetails); + return this.services.factory.Functions; } getTopClassKind(): TopClassKind { // ensure, that Typir uses the predefined 'TopClass' kind - const kind = this.services.kinds.get(TopClassKindName); + const kind = this.services.infrastructure.Kinds.get(TopClassKindName); return isTopClassKind(kind) ? kind : new TopClassKind(this.services); } diff --git a/packages/typir/src/kinds/class/class-type.ts b/packages/typir/src/kinds/class/class-type.ts index cc98223..2144b7b 100644 --- a/packages/typir/src/kinds/class/class-type.ts +++ b/packages/typir/src/kinds/class/class-type.ts @@ -42,7 +42,8 @@ export class ClassType extends Type { constructor(kind: ClassKind, typeDetails: ClassTypeDetails) { super(kind.options.typing === 'Nominal' ? kind.calculateIdentifierWithClassNameOnly(typeDetails) // use the name of the class as identifier already now - : undefined); // the identifier for structurally typed classes will be set later after resolving all fields and methods + : undefined, // the identifier for structurally typed classes will be set later after resolving all fields and methods + typeDetails); this.kind = kind; this.className = typeDetails.className; @@ -161,7 +162,7 @@ export class ClassType extends Type { if (this.kind.options.typing === 'Structural') { // for structural typing: return checkNameTypesMap(this.getFields(true), otherType.getFields(true), // including fields of super-classes - (t1, t2) => this.kind.services.equality.getTypeEqualityProblem(t1, t2)); + (t1, t2) => this.kind.services.Equality.getTypeEqualityProblem(t1, t2)); } else if (this.kind.options.typing === 'Nominal') { // for nominal typing: return checkValueForConflict(this.getIdentifier(), otherType.getIdentifier(), 'name'); @@ -244,7 +245,7 @@ export class ClassType extends Type { const allSub = subType.getAllSuperClasses(true); const globalResult: TypirProblem[] = []; for (const oneSub of allSub) { - const localResult = this.kind.services.equality.getTypeEqualityProblem(superType, oneSub); + const localResult = this.kind.services.Equality.getTypeEqualityProblem(superType, oneSub); if (localResult === undefined) { return []; // class is found in the class hierarchy } diff --git a/packages/typir/src/kinds/class/class-validation.ts b/packages/typir/src/kinds/class/class-validation.ts index a5b3ca7..829fd96 100644 --- a/packages/typir/src/kinds/class/class-validation.ts +++ b/packages/typir/src/kinds/class/class-validation.ts @@ -31,7 +31,7 @@ export class UniqueClassValidation implements ValidationRuleWithBeforeAfter { validation(domainElement: unknown, _typir: TypirServices): ValidationProblem[] { if (this.isRelevant(domainElement)) { // improves performance, since type inference need to be done only for relevant elements - const type = this.services.inference.inferType(domainElement); + const type = this.services.Inference.inferType(domainElement); if (isClassType(type)) { // register domain elements which have ClassTypes with a key for their uniques const key = this.calculateClassKey(type); @@ -119,10 +119,10 @@ export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter validation(domainElement: unknown, _typir: TypirServices): ValidationProblem[] { if (this.isMethodDeclaration(domainElement)) { // improves performance, since type inference need to be done only for relevant elements - const methodType = this.services.inference.inferType(domainElement); + const methodType = this.services.Inference.inferType(domainElement); if (isFunctionType(methodType)) { const classDeclaration = this.getClassOfMethod(domainElement, methodType); - const classType = this.services.inference.inferType(classDeclaration); + const classType = this.services.Inference.inferType(classDeclaration); if (isClassType(classType)) { const key = this.calculateMethodKey(classType, methodType); let entries = this.foundDeclarations.get(key); @@ -188,7 +188,7 @@ export function createNoSuperClassCyclesValidation(isRelevant: (domainElement: u return (domainElement: unknown, typir: TypirServices) => { const result: ValidationProblem[] = []; if (isRelevant(domainElement)) { // improves performance, since type inference need to be done only for relevant elements - const classType = typir.inference.inferType(domainElement); + const classType = typir.Inference.inferType(domainElement); if (isClassType(classType) && classType.isInStateOrLater('Completed')) { // check for cycles in sub-type-relationships if (classType.hasSubSuperClassCycles()) { diff --git a/packages/typir/src/kinds/class/top-class-kind.ts b/packages/typir/src/kinds/class/top-class-kind.ts index 992bd47..0114a19 100644 --- a/packages/typir/src/kinds/class/top-class-kind.ts +++ b/packages/typir/src/kinds/class/top-class-kind.ts @@ -4,13 +4,14 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { TypeDetails } from '../../graph/type-node.js'; import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; import { assertTrue, toArray } from '../../utils/utils.js'; import { isKind, Kind } from '../kind.js'; import { TopClassType } from './top-class-type.js'; -export interface TopClassTypeDetails { +export interface TopClassTypeDetails extends TypeDetails { inferenceRules?: InferTopClassType | InferTopClassType[] } @@ -31,8 +32,12 @@ export class TopClassKind implements Kind { constructor(services: TypirServices, options?: Partial) { this.$name = TopClassKindName; this.services = services; - this.services.kinds.register(this); - this.options = { + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + } + + protected collectOptions(options?: Partial): TopClassKindOptions { + return { // the default values: name: 'TopClass', // the actually overriden values: @@ -42,16 +47,7 @@ export class TopClassKind implements Kind { getTopClassType(typeDetails: TopClassTypeDetails): TopClassType | undefined { const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as TopClassType; - } - - getOrCreateTopClassType(typeDetails: TopClassTypeDetails): TopClassType { - const topType = this.getTopClassType(typeDetails); - if (topType) { - this.registerInferenceRules(typeDetails, topType); - return topType; - } - return this.createTopClassType(typeDetails); + return this.services.infrastructure.Graph.getType(key) as TopClassType; } createTopClassType(typeDetails: TopClassTypeDetails): TopClassType { @@ -62,9 +58,9 @@ export class TopClassKind implements Kind { // note, that the given inference rules are ignored in this case! return this.instance; } - const topType = new TopClassType(this, this.calculateIdentifier(typeDetails)); + const topType = new TopClassType(this, this.calculateIdentifier(typeDetails), typeDetails); this.instance = topType; - this.services.graph.addNode(topType); + this.services.infrastructure.Graph.addNode(topType); this.registerInferenceRules(typeDetails, topType); @@ -75,7 +71,7 @@ export class TopClassKind implements Kind { protected registerInferenceRules(typeDetails: TopClassTypeDetails, topType: TopClassType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { - this.services.inference.addInferenceRule((domainElement, _typir) => { + this.services.Inference.addInferenceRule((domainElement, _typir) => { for (const inferenceRule of rules) { if (inferenceRule(domainElement)) { return topType; diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 9ecee60..88be3b3 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -9,15 +9,16 @@ import { SubTypeProblem } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; -import { TopClassKind, isTopClassKind } from './top-class-kind.js'; +import { TopClassKind, TopClassTypeDetails, isTopClassKind } from './top-class-kind.js'; import { isClassType } from './class-type.js'; export class TopClassType extends Type { override readonly kind: TopClassKind; - constructor(kind: TopClassKind, identifier: string) { - super(identifier); + constructor(kind: TopClassKind, identifier: string, typeDetails: TopClassTypeDetails) { + super(identifier, typeDetails); this.kind = kind; + this.defineTheInitializationProcessOfThisType({}); // no preconditions } override getName(): string { diff --git a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts index 7a4b0bc..64118e9 100644 --- a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts +++ b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-kind.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { Type } from '../../graph/type-node.js'; +import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, toArray } from '../../utils/utils.js'; @@ -21,7 +21,7 @@ export class Parameter { } } -export interface FixedParameterTypeDetails { +export interface FixedParameterTypeDetails extends TypeDetails { parameterTypes: Type | Type[] } @@ -44,32 +44,27 @@ export class FixedParameterKind implements Kind { constructor(typir: TypirServices, baseName: string, options?: Partial, ...parameterNames: string[]) { this.$name = `${FixedParameterKindName}-${baseName}`; this.services = typir; - this.services.kinds.register(this); + this.services.infrastructure.Kinds.register(this); this.baseName = baseName; - this.options = { + this.options = this.collectOptions(options); + this.parameters = parameterNames.map((name, index) => { name, index }); + + // check input + assertTrue(this.parameters.length >= 1); + } + + protected collectOptions(options?: Partial): FixedParameterKindOptions { + return { // the default values: parameterSubtypeCheckingStrategy: 'EQUAL_TYPE', // the actually overriden values: ...options }; - this.parameters = parameterNames.map((name, index) => { name, index }); - - // check input - assertTrue(this.parameters.length >= 1); } getFixedParameterType(typeDetails: FixedParameterTypeDetails): FixedParameterType | undefined { const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as FixedParameterType; - } - - getOrCreateFixedParameterType(typeDetails: FixedParameterTypeDetails): FixedParameterType { - const typeWithParameters = this.getFixedParameterType(typeDetails); - if (typeWithParameters) { - this.registerInferenceRules(typeDetails, typeWithParameters); - return typeWithParameters; - } - return this.createFixedParameterType(typeDetails); + return this.services.infrastructure.Graph.getType(key) as FixedParameterType; } // the order of parameters matters! @@ -77,8 +72,8 @@ export class FixedParameterKind implements Kind { assertTrue(this.getFixedParameterType(typeDetails) === undefined); // create the class type - const typeWithParameters = new FixedParameterType(this, this.calculateIdentifier(typeDetails), ...toArray(typeDetails.parameterTypes)); - this.services.graph.addNode(typeWithParameters); + const typeWithParameters = new FixedParameterType(this, this.calculateIdentifier(typeDetails), typeDetails); + this.services.infrastructure.Graph.addNode(typeWithParameters); this.registerInferenceRules(typeDetails, typeWithParameters); diff --git a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts index f090c54..2f48e38 100644 --- a/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts +++ b/packages/typir/src/kinds/fixed-parameters/fixed-parameters-type.ts @@ -9,8 +9,8 @@ import { SubTypeProblem } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, checkTypeArrays, createKindConflict, createTypeCheckStrategy } from '../../utils/utils-type-comparison.js'; -import { assertTrue } from '../../utils/utils.js'; -import { Parameter, FixedParameterKind, isFixedParametersKind } from './fixed-parameters-kind.js'; +import { assertTrue, toArray } from '../../utils/utils.js'; +import { Parameter, FixedParameterKind, isFixedParametersKind, FixedParameterTypeDetails } from './fixed-parameters-kind.js'; export class ParameterValue { readonly parameter: Parameter; @@ -26,11 +26,12 @@ export class FixedParameterType extends Type { override readonly kind: FixedParameterKind; readonly parameterValues: ParameterValue[] = []; - constructor(kind: FixedParameterKind, identifier: string, ...typeValues: Type[]) { - super(identifier); + constructor(kind: FixedParameterKind, identifier: string, typeDetails: FixedParameterTypeDetails) { + super(identifier, typeDetails); this.kind = kind; // set the parameter values + const typeValues = toArray(typeDetails.parameterTypes); assertTrue(kind.parameters.length === typeValues.length); for (let i = 0; i < typeValues.length; i++) { this.parameterValues.push({ @@ -63,7 +64,7 @@ export class FixedParameterType extends Type { } else { // all parameter types must match, e.g. Set !== Set const conflicts: TypirProblem[] = []; - conflicts.push(...checkTypeArrays(this.getParameterTypes(), otherType.getParameterTypes(), (t1, t2) => this.kind.services.equality.getTypeEqualityProblem(t1, t2), false)); + conflicts.push(...checkTypeArrays(this.getParameterTypes(), otherType.getParameterTypes(), (t1, t2) => this.kind.services.Equality.getTypeEqualityProblem(t1, t2), false)); return conflicts; } } else { diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index eac719e..f758065 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -4,10 +4,10 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; -import { Type, TypeStateListener } from '../../graph/type-node.js'; +import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; -import { resolveTypeSelector } from '../../initialization/type-reference.js'; +import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; +import { ValidationRule } from '../../services/validation.js'; import { TypirServices } from '../../typir.js'; import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; import { assertType } from '../../utils/utils.js'; @@ -33,6 +33,9 @@ export class FunctionTypeInitializer extends TypeInitializer im throw new Error(`A function '${functionName}' without output parameter cannot have an inferred type, when this function is called!`); } kind.enforceFunctionName(functionName, kind.options.enforceFunctionName); + if (typeDetails.validationForCall && typeDetails.inferenceRuleForCalls === undefined) { + throw new Error(`A function '${functionName}' with validation of its calls need an inference rule which defines these inference calls!`); + } // prepare the overloads let overloaded = this.kind.mapNameTypes.get(functionName); @@ -45,14 +48,14 @@ export class FunctionTypeInitializer extends TypeInitializer im sameOutputType: undefined, }; this.kind.mapNameTypes.set(functionName, overloaded); - this.services.inference.addInferenceRule(overloaded.inference); + this.services.Inference.addInferenceRule(overloaded.inference); } // create the new Function type this.initialFunctionType = new FunctionType(kind, typeDetails); - this.inferenceRules = createInferenceRules(typeDetails, kind, this.initialFunctionType); - registerInferenceRules(this.inferenceRules, kind, functionName, undefined); + this.inferenceRules = this.createInferenceRules(typeDetails, this.initialFunctionType); + this.registerInferenceRules(functionName, undefined); this.initialFunctionType.addListener(this, true); } @@ -67,12 +70,12 @@ export class FunctionTypeInitializer extends TypeInitializer im const readyFunctionType = this.producedType(functionType); if (readyFunctionType !== functionType) { functionType.removeListener(this); - deregisterInferenceRules(this.inferenceRules, this.kind, functionName, undefined); - this.inferenceRules = createInferenceRules(this.typeDetails, this.kind, readyFunctionType); - registerInferenceRules(this.inferenceRules, this.kind, functionName, readyFunctionType); + this.deregisterInferenceRules(functionName, undefined); + this.inferenceRules = this.createInferenceRules(this.typeDetails, readyFunctionType); + this.registerInferenceRules(functionName, readyFunctionType); } else { - deregisterInferenceRules(this.inferenceRules, this.kind, functionName, undefined); - registerInferenceRules(this.inferenceRules, this.kind, functionName, readyFunctionType); + this.deregisterInferenceRules(functionName, undefined); + this.registerInferenceRules(functionName, readyFunctionType); } // remember the new function for later in order to enable overloaded functions! @@ -83,7 +86,7 @@ export class FunctionTypeInitializer extends TypeInitializer im // remember the output type of the first function overloaded.sameOutputType = outputTypeForFunctionCalls; } else { - if (overloaded.sameOutputType && outputTypeForFunctionCalls && this.services.equality.areTypesEqual(overloaded.sameOutputType, outputTypeForFunctionCalls) === true) { + if (overloaded.sameOutputType && outputTypeForFunctionCalls && this.services.Equality.areTypesEqual(overloaded.sameOutputType, outputTypeForFunctionCalls) === true) { // the output types of all overloaded functions are the same for now } else { // there is a difference @@ -103,127 +106,172 @@ export class FunctionTypeInitializer extends TypeInitializer im switchedToInvalid(_functionType: Type): void { // nothing specific needs to be done for Functions here, since the base implementation takes already care about all relevant stuff } -} - -interface FunctionInferenceRules { - forCall?: TypeInferenceRule; - forDeclaration?: TypeInferenceRule; -} - -function registerInferenceRules(rules: FunctionInferenceRules, functionKind: FunctionKind, functionName: string, functionType: FunctionType | undefined): void { - if (rules.forCall) { - const overloaded = functionKind.mapNameTypes.get(functionName)!; - overloaded.inference.addInferenceRule(rules.forCall, functionType); - } - - if (rules.forDeclaration) { - functionKind.services.inference.addInferenceRule(rules.forDeclaration, functionType); - } -} -function deregisterInferenceRules(rules: FunctionInferenceRules, functionKind: FunctionKind, functionName: string, functionType: FunctionType | undefined): void { - if (rules.forCall) { - const overloaded = functionKind.mapNameTypes.get(functionName); - overloaded?.inference.removeInferenceRule(rules.forCall, functionType); + protected registerInferenceRules(functionName: string, functionType: FunctionType | undefined): void { + if (this.inferenceRules.inferenceForCall) { + const overloaded = this.kind.mapNameTypes.get(functionName)!; + overloaded.inference.addInferenceRule(this.inferenceRules.inferenceForCall, functionType); + } + if (this.inferenceRules.validationForCall) { + this.kind.services.validation.Collector.addValidationRule(this.inferenceRules.validationForCall); + } + if (this.inferenceRules.inferenceForDeclaration) { + this.kind.services.Inference.addInferenceRule(this.inferenceRules.inferenceForDeclaration, functionType); + } } - if (rules.forDeclaration) { - functionKind.services.inference.removeInferenceRule(rules.forDeclaration, functionType); + protected deregisterInferenceRules(functionName: string, functionType: FunctionType | undefined): void { + if (this.inferenceRules.inferenceForCall) { + const overloaded = this.kind.mapNameTypes.get(functionName); + overloaded?.inference.removeInferenceRule(this.inferenceRules.inferenceForCall, functionType); + } + if (this.inferenceRules.validationForCall) { + this.kind.services.validation.Collector.removeValidationRule(this.inferenceRules.validationForCall); + } + if (this.inferenceRules.inferenceForDeclaration) { + this.kind.services.Inference.removeInferenceRule(this.inferenceRules.inferenceForDeclaration, functionType); + } } -} -function createInferenceRules(typeDetails: CreateFunctionTypeDetails, functionKind: FunctionKind, functionType: FunctionType): FunctionInferenceRules { - const result: FunctionInferenceRules = {}; - const functionName = typeDetails.functionName; - const mapNameTypes = functionKind.mapNameTypes; - const outputTypeForFunctionCalls = functionKind.getOutputTypeForFunctionCalls(functionType); - if (typeDetails.inferenceRuleForCalls) { // TODO warum wird hier nicht einfach "outputTypeForFunctionCalls !== undefined" überprüft?? - /** Preconditions: - * - there is a rule which specifies how to infer the current function type - * - the current function has an output type/parameter, otherwise, this function could not provide any type (and throws an error), when it is called! - * (exception: the options contain a type to return in this special case) - */ - function check(returnType: Type | undefined): Type { - if (returnType) { - return returnType; - } else { - throw new Error(`The function ${functionName} is called, but has no output type to infer.`); + protected createInferenceRules(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType): FunctionInferenceRules { + const result: FunctionInferenceRules = {}; + const functionName = typeDetails.functionName; + const mapNameTypes = this.kind.mapNameTypes; + const outputTypeForFunctionCalls = this.kind.getOutputTypeForFunctionCalls(functionType); + if (typeDetails.inferenceRuleForCalls) { + /** Preconditions: + * - there is a rule which specifies how to infer the current function type + * - the current function has an output type/parameter, otherwise, this function could not provide any type (and throws an error), when it is called! + * (exception: the options contain a type to return in this special case) + */ + function check(returnType: Type | undefined): Type { + if (returnType) { // this condition is checked here, since 'undefined' is OK, as long as it is not used; extracting this function is difficult due to TypeScripts strict rules for using 'this' + return returnType; + } else { + throw new Error(`The function ${functionName} is called, but has no output type to infer.`); + } } - } - // register inference rule for calls of the new function - // TODO what about the case, that multiple variants match?? after implicit conversion for example?! => overload with the lowest number of conversions wins! - result.forCall = { - inferTypeWithoutChildren(domainElement, _typir) { - const result = typeDetails.inferenceRuleForCalls!.filter(domainElement); - if (result) { - const matching = typeDetails.inferenceRuleForCalls!.matching(domainElement); - if (matching) { - const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(domainElement); - if (inputArguments && inputArguments.length >= 1) { - // this function type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 - const overloadInfos = mapNameTypes.get(functionName); - if (overloadInfos && overloadInfos.overloadedFunctions.length >= 2) { - // (only) for overloaded functions: - if (overloadInfos.sameOutputType) { - // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! - return overloadInfos.sameOutputType; + // register inference rule for calls of the new function + // TODO what about the case, that multiple variants match?? after implicit conversion for example?! => overload with the lowest number of conversions wins! + result.inferenceForCall = { + inferTypeWithoutChildren(domainElement, _typir) { + const result = typeDetails.inferenceRuleForCalls!.filter(domainElement); + if (result) { + const matching = typeDetails.inferenceRuleForCalls!.matching(domainElement); + if (matching) { + const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(domainElement); + if (inputArguments && inputArguments.length >= 1) { + // this function type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 + const overloadInfos = mapNameTypes.get(functionName); + if (overloadInfos && overloadInfos.overloadedFunctions.length >= 2) { + // (only) for overloaded functions: + if (overloadInfos.sameOutputType) { + // exception: all(!) overloaded functions have the same(!) output type, save performance and return this type! + return overloadInfos.sameOutputType; + } else { + // otherwise: the types of the parameters need to be inferred in order to determine an exact match + return inputArguments; + } } else { - // otherwise: the types of the parameters need to be inferred in order to determine an exact match - return inputArguments; + // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + return check(outputTypeForFunctionCalls); } } else { - // the current function is not overloaded, therefore, the types of their parameters are not required => save time, ignore inference errors + // there are no operands to check return check(outputTypeForFunctionCalls); } } else { - // there are no operands to check - return check(outputTypeForFunctionCalls); + // the domain element is slightly different } } else { - // the domain element is slightly different + // the domain element has a completely different purpose + } + // does not match at all + return InferenceRuleNotApplicable; + }, + inferTypeWithChildrensTypes(domainElement, actualInputTypes, typir) { + const expectedInputTypes = typeDetails.inputParameters.map(p => typir.infrastructure.TypeResolver.resolve(p.type)); + // all operands need to be assignable(! not equal) to the required types + const comparisonConflicts = checkTypeArrays(actualInputTypes, expectedInputTypes, + (t1, t2) => typir.Assignability.getAssignabilityProblem(t1, t2), true); + if (comparisonConflicts.length >= 1) { + // this function type does not match, due to assignability conflicts => return them as errors + return { + $problem: InferenceProblem, + domainElement, + inferenceCandidate: functionType, + location: 'input parameters', + rule: this, + subProblems: comparisonConflicts, + }; + // We have a dedicated validation for this case (see below), but a resulting error might be ignored by the user => return the problem during type-inference again + } else { + // matching => return the return type of the function for the case of a function call! + return check(outputTypeForFunctionCalls); + } + }, + }; + } + + if (typeDetails.validationForCall) { + result.validationForCall = (domainElement, typir) => { + if (typeDetails.inferenceRuleForCalls!.filter(domainElement) && typeDetails.inferenceRuleForCalls!.matching(domainElement)) { + // check the input arguments, required for overloaded functions + const inputArguments = typeDetails.inferenceRuleForCalls!.inputArguments(domainElement); + if (inputArguments && inputArguments.length >= 1) { + // this function type might match, to be sure, resolve the types of the values for the parameters and continue to step 2 + const overloadInfos = mapNameTypes.get(functionName); + if (overloadInfos && overloadInfos.overloadedFunctions.length >= 2) { + // for overloaded functions: the types of the parameters need to be inferred in order to determine an exact match + // (Note that the short-cut for type inference for function calls, when all overloads return the same output type, does not work here, since the validation here is specific for this single variant!) + // This is also the reason, why the inference rule for call is not reused here.) + const childTypes: Array = inputArguments.map(child => typir.Inference.inferType(child)); + const actualInputTypes = childTypes.filter(t => isType(t)); + if (childTypes.length === actualInputTypes.length) { + const expectedInputTypes = typeDetails.inputParameters.map(p => typir.infrastructure.TypeResolver.resolve(p.type)); + // all operands need to be assignable(! not equal) to the required types + const comparisonConflicts = checkTypeArrays(actualInputTypes, expectedInputTypes, + (t1, t2) => typir.Assignability.getAssignabilityProblem(t1, t2), true); + if (comparisonConflicts.length <= 0) { + // all arguments are assignable to the expected types of the parameters => this function is really called here => validate this call now + return typeDetails.validationForCall!(domainElement, functionType, typir); + } + } else { + // at least one argument could not be inferred + } + } else { + // the current function is not overloaded, therefore, the types of their parameters are not required => save time + return typeDetails.validationForCall!(domainElement, functionType, typir); + } + } else { + // there are no operands to check + return typeDetails.validationForCall!(domainElement, functionType, typir); } - } else { - // the domain element has a completely different purpose } - // does not match at all - return InferenceRuleNotApplicable; - }, - inferTypeWithChildrensTypes(domainElement, childrenTypes, typir) { - const inputTypes = typeDetails.inputParameters.map(p => resolveTypeSelector(typir, p.type)); - // all operands need to be assignable(! not equal) to the required types - const comparisonConflicts = checkTypeArrays(childrenTypes, inputTypes, - (t1, t2) => typir.assignability.getAssignabilityProblem(t1, t2), true); - if (comparisonConflicts.length >= 1) { - // this function type does not match, due to assignability conflicts => return them as errors - return { - $problem: InferenceProblem, - domainElement, - inferenceCandidate: functionType, - location: 'input parameters', - rule: this, - subProblems: comparisonConflicts, - }; - // We have a dedicated validation for this case (see below), but a resulting error might be ignored by the user => return the problem during type-inference again + return []; + }; + } + + // register inference rule for the declaration of the new function + // (regarding overloaded function, for now, it is assumed, that the given inference rule itself is concrete enough to handle overloaded functions itself!) + if (typeDetails.inferenceRuleForDeclaration) { + result.inferenceForDeclaration = (domainElement, _typir) => { + if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { + return functionType; } else { - // matching => return the return type of the function for the case of a function call! - return check(outputTypeForFunctionCalls); + return InferenceRuleNotApplicable; } - }, - }; - } + }; + } - // register inference rule for the declaration of the new function - // (regarding overloaded function, for now, it is assumed, that the given inference rule itself is concrete enough to handle overloaded functions itself!) - if (typeDetails.inferenceRuleForDeclaration) { - result.forDeclaration = (domainElement, _typir) => { - if (typeDetails.inferenceRuleForDeclaration!(domainElement)) { - return functionType; - } else { - return InferenceRuleNotApplicable; - } - }; + return result; } - return result; +} + +interface FunctionInferenceRules { + inferenceForCall?: TypeInferenceRule; + validationForCall?: ValidationRule; + inferenceForDeclaration?: TypeInferenceRule; } diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index 1720dfc..d34dffc 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -6,9 +6,9 @@ import { TypeEdge } from '../../graph/type-edge.js'; import { TypeGraphListener } from '../../graph/type-graph.js'; -import { Type } from '../../graph/type-node.js'; +import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; -import { TypeReference, resolveTypeSelector } from '../../initialization/type-reference.js'; +import { TypeReference } from '../../initialization/type-reference.js'; import { TypeSelector } from '../../initialization/type-selector.js'; import { CompositeTypeInferenceRule } from '../../services/inference.js'; import { ValidationProblem } from '../../services/validation.js'; @@ -35,12 +35,14 @@ export interface FunctionKindOptions { export const FunctionKindName = 'FunctionKind'; +export type FunctionCallValidationRule = (functionCall: T, functionType: FunctionType, typir: TypirServices) => ValidationProblem[]; + export interface CreateParameterDetails { name: string; type: TypeSelector; } -export interface FunctionTypeDetails { +export interface FunctionTypeDetails extends TypeDetails { functionName: string, /** The order of parameters is important! */ outputParameter: CreateParameterDetails | undefined, @@ -52,6 +54,9 @@ export interface CreateFunctionTypeDetails extends FunctionTypeDetails { /** for function calls => returns the return type of the function */ inferenceRuleForCalls?: InferFunctionCall, // TODO for function references (like the declaration, but without any names!) => returns signature (without any names) + + /** This validation will be applied to all domain elements which represent calls of the functions. */ + validationForCall?: FunctionCallValidationRule, } /** Collects all functions with the same name */ @@ -80,14 +85,14 @@ export type InferFunctionCall = { * - overloaded functions are specific for the function kind => solve it inside the FunctionKind! * * How many inference rules? - * - One inference rule for each function type does not work, since TODO ?? + * - The inference rule for calls of each function type with the same name are grouped together in order to provide better error messages, if none of the variants match. * - Checking multiple functions within the same rule (e.g. only one inference rule for the function kind or one inference rule for each function name) does not work, * since multiple different sets of parameters must be returned for overloaded functions! * - multiple IR collectors: how to apply all the other rules?! * * How many validation rules? * - For validation, it is enough that at least one of the function variants match! - * - But checking that is not possible with multiple independent rules. + * - But checking that is not possible with independent rules for each function variant. * - Therefore, it must be a single validation for each function name (with all type variants). * - In order to simplify (de)registering validation rules, only one validation rule for all functions is used here (with an internal loop over all function names). * @@ -131,21 +136,11 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer constructor(services: TypirServices, options?: Partial) { this.$name = FunctionKindName; this.services = services; - this.services.kinds.register(this); - this.options = { - // the default values: - enforceFunctionName: false, - enforceInputParameterNames: false, - enforceOutputParameterName: false, - identifierPrefix: 'function', - typeToInferForCallsOfFunctionsWithoutOutput: 'THROW_ERROR', - subtypeParameterChecking: 'SUB_TYPE', - // the actually overriden values: - ...options - }; + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); // register Validations for input arguments of function calls (must be done here to support overloaded functions) - this.services.validation.collector.addValidationRule( + this.services.validation.Collector.addValidationRule( (domainElement, typir) => { const resultAll: ValidationProblem[] = []; for (const [overloadedName, overloadedFunctions] of this.mapNameTypes.entries()) { @@ -176,7 +171,7 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer }); } else { // there are parameter values to check their types - const inferredParameterTypes = inputArguments.map(p => typir.inference.inferType(p)); + const inferredParameterTypes = inputArguments.map(p => typir.Inference.inferType(p)); for (let i = 0; i < inputArguments.length; i++) { const expectedType = expectedParameterTypes[i]; const inferredType = inferredParameterTypes[i]; @@ -203,7 +198,7 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer $problem: ValidationProblem, domainElement, severity: 'error', - message: `The given operands for the function '${this.services.printer.printTypeName(singleFunction.functionType)}' match the expected types only partially.`, + message: `The given operands for the function '${this.services.Printer.printTypeName(singleFunction.functionType)}' match the expected types only partially.`, subProblems: currentProblems, }); } else { @@ -240,6 +235,20 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer ); } + protected collectOptions(options?: Partial): FunctionKindOptions { + return { + // the default values: + enforceFunctionName: false, + enforceInputParameterNames: false, + enforceOutputParameterName: false, + identifierPrefix: 'function', + typeToInferForCallsOfFunctionsWithoutOutput: 'THROW_ERROR', + subtypeParameterChecking: 'SUB_TYPE', + // the actually overriden values: + ...options + }; + } + get(typeDetails: FunctionTypeDetails): TypeReference { return new TypeReference(() => this.calculateIdentifier(typeDetails), this.services); } @@ -254,7 +263,7 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer // 'THROW_ERROR': an error will be thrown later, when this case actually occurs! (this.options.typeToInferForCallsOfFunctionsWithoutOutput === 'THROW_ERROR' ? undefined - : resolveTypeSelector(this.services, this.options.typeToInferForCallsOfFunctionsWithoutOutput)); + : this.services.infrastructure.TypeResolver.resolve(this.options.typeToInferForCallsOfFunctionsWithoutOutput)); } @@ -289,9 +298,9 @@ export class FunctionKind implements Kind, TypeGraphListener, FunctionFactorySer // function name, if wanted const functionName = this.hasFunctionName(typeDetails.functionName) ? typeDetails.functionName : ''; // inputs: type identifiers in defined order - const inputsString = typeDetails.inputParameters.map(input => resolveTypeSelector(this.services, input.type).getIdentifier()).join(','); + const inputsString = typeDetails.inputParameters.map(input => this.services.infrastructure.TypeResolver.resolve(input.type).getIdentifier()).join(','); // output: type identifier - const outputString = typeDetails.outputParameter ? resolveTypeSelector(this.services, typeDetails.outputParameter.type).getIdentifier() : ''; + const outputString = typeDetails.outputParameter ? this.services.infrastructure.TypeResolver.resolve(typeDetails.outputParameter.type).getIdentifier() : ''; // complete signature return `${prefix}${functionName}(${inputsString}):${outputString}`; } diff --git a/packages/typir/src/kinds/function/function-type.ts b/packages/typir/src/kinds/function/function-type.ts index 94747f5..d9c3ad3 100644 --- a/packages/typir/src/kinds/function/function-type.ts +++ b/packages/typir/src/kinds/function/function-type.ts @@ -26,7 +26,7 @@ export class FunctionType extends Type { readonly inputParameters: ParameterDetails[]; constructor(kind: FunctionKind, typeDetails: FunctionTypeDetails) { - super(undefined); + super(undefined, typeDetails); this.kind = kind; this.functionName = typeDetails.functionName; @@ -110,10 +110,10 @@ export class FunctionType extends Type { } // same output? conflicts.push(...checkTypes(this.getOutput(), otherType.getOutput(), - (s, t) => this.kind.services.equality.getTypeEqualityProblem(s, t), this.kind.options.enforceOutputParameterName)); + (s, t) => this.kind.services.Equality.getTypeEqualityProblem(s, t), this.kind.options.enforceOutputParameterName)); // same input? conflicts.push(...checkTypeArrays(this.getInputs(), otherType.getInputs(), - (s, t) => this.kind.services.equality.getTypeEqualityProblem(s, t), this.kind.options.enforceInputParameterNames)); + (s, t) => this.kind.services.Equality.getTypeEqualityProblem(s, t), this.kind.options.enforceInputParameterNames)); return conflicts; } else { return [{ diff --git a/packages/typir/src/kinds/function/function-validation.ts b/packages/typir/src/kinds/function/function-validation.ts index 260ca6f..00ff9f5 100644 --- a/packages/typir/src/kinds/function/function-validation.ts +++ b/packages/typir/src/kinds/function/function-validation.ts @@ -28,7 +28,7 @@ export class UniqueFunctionValidation implements ValidationRuleWithBeforeAfter { validation(domainElement: unknown, _typir: TypirServices): ValidationProblem[] { if (this.isRelevant(domainElement)) { // improves performance, since type inference need to be done only for relevant elements - const type = this.services.inference.inferType(domainElement); + const type = this.services.Inference.inferType(domainElement); if (isFunctionType(type)) { // register domain elements which have FunctionTypes with a key for their uniques const key = this.calculateFunctionKey(type); diff --git a/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts b/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts index 5271b6c..7f52a66 100644 --- a/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts +++ b/packages/typir/src/kinds/multiplicity/multiplicity-kind.ts @@ -4,13 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { Type } from '../../graph/type-node.js'; +import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypirServices } from '../../typir.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, isKind } from '../kind.js'; import { MultiplicityType } from './multiplicity-type.js'; -export interface MultiplicityTypeDetails { +export interface MultiplicityTypeDetails extends TypeDetails { constrainedType: Type, lowerBound: number, upperBound: number @@ -35,8 +35,12 @@ export class MultiplicityKind implements Kind { constructor(services: TypirServices, options?: Partial) { this.$name = MultiplicityKindName; this.services = services; - this.services.kinds.register(this); - this.options = { + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + } + + protected collectOptions(options?: Partial): MultiplicityKindOptions { + return { // the default values: symbolForUnlimited: '*', // the actually overriden values: @@ -46,16 +50,7 @@ export class MultiplicityKind implements Kind { getMultiplicityType(typeDetails: MultiplicityTypeDetails): MultiplicityType | undefined { const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as MultiplicityType; - } - - getOrCreateMultiplicityType(typeDetails: MultiplicityTypeDetails): MultiplicityType { - const typeWithMultiplicity = this.getMultiplicityType(typeDetails); - if (typeWithMultiplicity) { - this.registerInferenceRules(typeDetails, typeWithMultiplicity); - return typeWithMultiplicity; - } - return this.createMultiplicityType(typeDetails); + return this.services.infrastructure.Graph.getType(key) as MultiplicityType; } createMultiplicityType(typeDetails: MultiplicityTypeDetails): MultiplicityType { @@ -66,8 +61,8 @@ export class MultiplicityKind implements Kind { } // create the type with multiplicities - const typeWithMultiplicity = new MultiplicityType(this, this.calculateIdentifier(typeDetails), typeDetails.constrainedType, typeDetails.lowerBound, typeDetails.upperBound); - this.services.graph.addNode(typeWithMultiplicity); + const typeWithMultiplicity = new MultiplicityType(this, this.calculateIdentifier(typeDetails), typeDetails); + this.services.infrastructure.Graph.addNode(typeWithMultiplicity); this.registerInferenceRules(typeDetails, typeWithMultiplicity); diff --git a/packages/typir/src/kinds/multiplicity/multiplicity-type.ts b/packages/typir/src/kinds/multiplicity/multiplicity-type.ts index b09718b..8bcf574 100644 --- a/packages/typir/src/kinds/multiplicity/multiplicity-type.ts +++ b/packages/typir/src/kinds/multiplicity/multiplicity-type.ts @@ -9,7 +9,7 @@ import { SubTypeProblem } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, createKindConflict } from '../../utils/utils-type-comparison.js'; -import { MultiplicityKind, isMultiplicityKind } from './multiplicity-kind.js'; +import { MultiplicityKind, MultiplicityTypeDetails, isMultiplicityKind } from './multiplicity-kind.js'; export class MultiplicityType extends Type { override readonly kind: MultiplicityKind; @@ -17,13 +17,12 @@ export class MultiplicityType extends Type { readonly lowerBound: number; readonly upperBound: number; - constructor(kind: MultiplicityKind, identifier: string, - constrainedType: Type, lowerBound: number, upperBound: number) { - super(identifier); + constructor(kind: MultiplicityKind, identifier: string, typeDetails: MultiplicityTypeDetails) { + super(identifier, typeDetails); this.kind = kind; - this.constrainedType = constrainedType; - this.lowerBound = lowerBound; - this.upperBound = upperBound; + this.constrainedType = typeDetails.constrainedType; + this.lowerBound = typeDetails.lowerBound; + this.upperBound = typeDetails.upperBound; this.defineTheInitializationProcessOfThisType({}); // TODO preconditions } @@ -42,7 +41,7 @@ export class MultiplicityType extends Type { conflicts.push(...checkValueForConflict(this.getLowerBound(), this.getLowerBound(), 'lower bound')); conflicts.push(...checkValueForConflict(this.getUpperBound(), this.getUpperBound(), 'upper bound')); // check the constrained type - const constrainedTypeConflict = this.kind.services.equality.getTypeEqualityProblem(this.getConstrainedType(), this.getConstrainedType()); + const constrainedTypeConflict = this.kind.services.Equality.getTypeEqualityProblem(this.getConstrainedType(), this.getConstrainedType()); if (constrainedTypeConflict !== undefined) { conflicts.push(constrainedTypeConflict); } @@ -89,7 +88,7 @@ export class MultiplicityType extends Type { conflicts.push(...checkValueForConflict(subType.getLowerBound(), superType.getLowerBound(), 'lower bound', this.kind.isBoundGreaterEquals)); conflicts.push(...checkValueForConflict(subType.getUpperBound(), superType.getUpperBound(), 'upper bound', this.kind.isBoundGreaterEquals)); // check the constrained type - const constrainedTypeConflict = this.kind.services.subtype.getSubTypeProblem(subType.getConstrainedType(), superType.getConstrainedType()); + const constrainedTypeConflict = this.kind.services.Subtype.getSubTypeProblem(subType.getConstrainedType(), superType.getConstrainedType()); if (constrainedTypeConflict !== undefined) { conflicts.push(constrainedTypeConflict); } diff --git a/packages/typir/src/kinds/primitive/primitive-kind.ts b/packages/typir/src/kinds/primitive/primitive-kind.ts index 74f73f1..c821aef 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -4,13 +4,18 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { TypeDetails } from '../../graph/type-node.js'; import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; import { assertTrue, toArray } from '../../utils/utils.js'; import { isKind, Kind } from '../kind.js'; import { PrimitiveType } from './primitive-type.js'; -export interface PrimitiveTypeDetails { +export interface PrimitiveKindOptions { + // empty for now +} + +export interface PrimitiveTypeDetails extends TypeDetails { primitiveName: string; /** In case of multiple inference rules, later rules are not evaluated anymore, if an earler rule already matched. */ inferenceRules?: InferPrimitiveType | InferPrimitiveType[]; @@ -28,24 +33,32 @@ export interface PrimitiveFactoryService { export class PrimitiveKind implements Kind, PrimitiveFactoryService { readonly $name: 'PrimitiveKind'; readonly services: TypirServices; + readonly options: PrimitiveKindOptions; - constructor(services: TypirServices) { + constructor(services: TypirServices, options?: Partial) { this.$name = PrimitiveKindName; this.services = services; - this.services.kinds.register(this); + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + } + + protected collectOptions(options?: Partial): PrimitiveKindOptions { + return { + ...options, + }; } get(typeDetails: PrimitiveTypeDetails): PrimitiveType | undefined { const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as PrimitiveType; + return this.services.infrastructure.Graph.getType(key) as PrimitiveType; } create(typeDetails: PrimitiveTypeDetails): PrimitiveType { assertTrue(this.get(typeDetails) === undefined); // create the primitive type - const primitiveType = new PrimitiveType(this, this.calculateIdentifier(typeDetails)); - this.services.graph.addNode(primitiveType); + const primitiveType = new PrimitiveType(this, this.calculateIdentifier(typeDetails), typeDetails); + this.services.infrastructure.Graph.addNode(primitiveType); this.registerInferenceRules(typeDetails, primitiveType); @@ -56,7 +69,7 @@ export class PrimitiveKind implements Kind, PrimitiveFactoryService { protected registerInferenceRules(typeDetails: PrimitiveTypeDetails, primitiveType: PrimitiveType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { - this.services.inference.addInferenceRule((domainElement, _typir) => { + this.services.Inference.addInferenceRule((domainElement, _typir) => { for (const inferenceRule of rules) { if (inferenceRule(domainElement)) { return primitiveType; diff --git a/packages/typir/src/kinds/primitive/primitive-type.ts b/packages/typir/src/kinds/primitive/primitive-type.ts index 2818d5f..840963d 100644 --- a/packages/typir/src/kinds/primitive/primitive-type.ts +++ b/packages/typir/src/kinds/primitive/primitive-type.ts @@ -9,13 +9,13 @@ import { SubTypeProblem } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { checkValueForConflict, createKindConflict } from '../../utils/utils-type-comparison.js'; -import { PrimitiveKind, isPrimitiveKind } from './primitive-kind.js'; +import { PrimitiveKind, PrimitiveTypeDetails, isPrimitiveKind } from './primitive-kind.js'; export class PrimitiveType extends Type { override readonly kind: PrimitiveKind; - constructor(kind: PrimitiveKind, identifier: string) { - super(identifier); + constructor(kind: PrimitiveKind, identifier: string, typeDetails: PrimitiveTypeDetails) { + super(identifier, typeDetails); this.kind = kind; this.defineTheInitializationProcessOfThisType({}); // no preconditions } diff --git a/packages/typir/src/kinds/top/top-kind.ts b/packages/typir/src/kinds/top/top-kind.ts index f9a387b..dee439b 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -4,13 +4,14 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +import { TypeDetails } from '../../graph/type-node.js'; import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { TypirServices } from '../../typir.js'; import { assertTrue, toArray } from '../../utils/utils.js'; import { isKind, Kind } from '../kind.js'; import { TopType } from './top-type.js'; -export interface TopTypeDetails { +export interface TopTypeDetails extends TypeDetails { /** In case of multiple inference rules, later rules are not evaluated anymore, if an earler rule already matched. */ inferenceRules?: InferTopType | InferTopType[] } @@ -37,8 +38,12 @@ export class TopKind implements Kind, TopFactoryService { constructor(services: TypirServices, options?: Partial) { this.$name = TopKindName; this.services = services; - this.services.kinds.register(this); - this.options = { + this.services.infrastructure.Kinds.register(this); + this.options = this.collectOptions(options); + } + + protected collectOptions(options?: Partial): TopKindOptions { + return { // the default values: name: 'any', // the actually overriden values: @@ -48,7 +53,7 @@ export class TopKind implements Kind, TopFactoryService { get(typeDetails: TopTypeDetails): TopType | undefined { const key = this.calculateIdentifier(typeDetails); - return this.services.graph.getType(key) as TopType; + return this.services.infrastructure.Graph.getType(key) as TopType; } create(typeDetails: TopTypeDetails): TopType { @@ -59,9 +64,9 @@ export class TopKind implements Kind, TopFactoryService { // note, that the given inference rules are ignored in this case! return this.instance; } - const topType = new TopType(this, this.calculateIdentifier(typeDetails)); + const topType = new TopType(this, this.calculateIdentifier(typeDetails), typeDetails); this.instance = topType; - this.services.graph.addNode(topType); + this.services.infrastructure.Graph.addNode(topType); this.registerInferenceRules(typeDetails, topType); @@ -72,7 +77,7 @@ export class TopKind implements Kind, TopFactoryService { protected registerInferenceRules(typeDetails: TopTypeDetails, topType: TopType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { - this.services.inference.addInferenceRule((domainElement, _typir) => { + this.services.Inference.addInferenceRule((domainElement, _typir) => { for (const inferenceRule of rules) { if (inferenceRule(domainElement)) { return topType; diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index 358921b..13d1942 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -9,13 +9,13 @@ import { SubTypeProblem } from '../../services/subtype.js'; import { isType, Type } from '../../graph/type-node.js'; import { TypirProblem } from '../../utils/utils-definitions.js'; import { createKindConflict } from '../../utils/utils-type-comparison.js'; -import { TopKind, isTopKind } from './top-kind.js'; +import { TopKind, TopTypeDetails, isTopKind } from './top-kind.js'; export class TopType extends Type { override readonly kind: TopKind; - constructor(kind: TopKind, identifier: string) { - super(identifier); + constructor(kind: TopKind, identifier: string, typeDetails: TopTypeDetails) { + super(identifier, typeDetails); this.kind = kind; this.defineTheInitializationProcessOfThisType({}); // no preconditions } diff --git a/packages/typir/src/services/assignability.ts b/packages/typir/src/services/assignability.ts index ed5d603..9a228d4 100644 --- a/packages/typir/src/services/assignability.ts +++ b/packages/typir/src/services/assignability.ts @@ -34,9 +34,9 @@ export class DefaultTypeAssignability implements TypeAssignability { protected readonly equality: TypeEquality; constructor(services: TypirServices) { - this.conversion = services.conversion; - this.subtype = services.subtype; - this.equality = services.equality; + this.conversion = services.Conversion; + this.subtype = services.Subtype; + this.equality = services.Equality; } isAssignable(source: Type, target: Type): boolean { diff --git a/packages/typir/src/services/caching.ts b/packages/typir/src/services/caching.ts index 03a84ab..63d1c25 100644 --- a/packages/typir/src/services/caching.ts +++ b/packages/typir/src/services/caching.ts @@ -35,7 +35,7 @@ export class DefaultTypeRelationshipCaching implements TypeRelationshipCaching { protected readonly graph: TypeGraph; constructor(services: TypirServices) { - this.graph = services.graph; + this.graph = services.infrastructure.Graph; } getRelationshipUnidirectional(from: Type, to: Type, $relation: T['$relation']): T | undefined { diff --git a/packages/typir/src/services/conversion.ts b/packages/typir/src/services/conversion.ts index f336807..d0226d6 100644 --- a/packages/typir/src/services/conversion.ts +++ b/packages/typir/src/services/conversion.ts @@ -105,8 +105,8 @@ export class DefaultTypeConversion implements TypeConversion { protected readonly graph: TypeGraph; constructor(services: TypirServices) { - this.equality = services.equality; - this.graph = services.graph; + this.equality = services.Equality; + this.graph = services.infrastructure.Graph; } markAsConvertible(from: Type | Type[], to: Type | Type[], mode: ConversionModeForSpecification): void { diff --git a/packages/typir/src/services/equality.ts b/packages/typir/src/services/equality.ts index 2dc1c65..04462fa 100644 --- a/packages/typir/src/services/equality.ts +++ b/packages/typir/src/services/equality.ts @@ -22,7 +22,12 @@ export function isTypeEqualityProblem(problem: unknown): problem is TypeEquality return isSpecificTypirProblem(problem, TypeEqualityProblem); } -// TODO comments +/** + * Analyzes, whether there is an equality-relationship between two types. + * + * In contrast to type comparisons with type1 === type2 or type1.identifier === type2.identifier, + * equality will take alias types and so on into account as well. + */ export interface TypeEquality { areTypesEqual(type1: Type, type2: Type): boolean; getTypeEqualityProblem(type1: Type, type2: Type): TypeEqualityProblem | undefined; @@ -32,7 +37,7 @@ export class DefaultTypeEquality implements TypeEquality { protected readonly typeRelationships: TypeRelationshipCaching; constructor(services: TypirServices) { - this.typeRelationships = services.caching.typeRelationships; + this.typeRelationships = services.caching.TypeRelationships; } areTypesEqual(type1: Type, type2: Type): boolean { diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 5afcb9c..fac8e9a 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -103,6 +103,7 @@ export interface TypeInferenceCollector { * @returns the found Type or some inference problems (might be empty), when none of the inference rules were able to infer a type */ inferType(domainElement: unknown): Type | InferenceProblem[] + /** * Registers an inference rule. * When inferring the type for an element, all registered inference rules are checked until the first match. @@ -126,8 +127,8 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty constructor(services: TypirServices) { this.services = services; - this.domainElementInference = services.caching.domainElementInference; - this.services.graph.addListener(this); + this.domainElementInference = services.caching.DomainElementInference; + this.services.infrastructure.Graph.addListener(this); } addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void { @@ -190,73 +191,16 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty protected inferTypeLogic(domainElement: unknown): Type | InferenceProblem[] { this.checkForError(domainElement); - // otherwise, check all rules + + // check all rules const collectedInferenceProblems: InferenceProblem[] = []; for (const rules of this.inferenceRules.values()) { for (const rule of rules) { - if (typeof rule === 'function') { - // simple case without type inference for children - const ruleResult: TypeInferenceResultWithoutInferringChildren = rule(domainElement, this.services); - this.checkForError(ruleResult); - const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); - if (checkResult) { - // this inference rule was applicable and produced a final result - return checkResult; - } else { - // no result for this inference rule => check the next inference rules - } - } else if (typeof rule === 'object') { - // more complex case with inferring the type for children - const ruleResult: TypeInferenceResultWithInferringChildren = rule.inferTypeWithoutChildren(domainElement, this.services); - if (Array.isArray(ruleResult)) { - // this rule might match => continue applying this rule - // resolve the requested child types - const childElements = ruleResult; - const childTypes: Array = childElements.map(child => this.services.inference.inferType(child)); - // check, whether inferring the children resulted in some other inference problems - const childTypeProblems: InferenceProblem[] = []; - for (let i = 0; i < childTypes.length; i++) { - const child = childTypes[i]; - if (Array.isArray(child)) { - childTypeProblems.push({ - $problem: InferenceProblem, - domainElement: childElements[i], - location: `child element ${i}`, - rule, - subProblems: child, - }); - } - } - if (childTypeProblems.length >= 1) { - collectedInferenceProblems.push({ - $problem: InferenceProblem, - domainElement, - location: 'inferring depending children', - rule, - subProblems: childTypeProblems, - }); - } else { - // the types of all children are successfully inferred - const finalInferenceResult = rule.inferTypeWithChildrensTypes(domainElement, childTypes as Type[], this.services); - if (isType(finalInferenceResult)) { - // type is inferred! - return finalInferenceResult; - } else { - // inference is not applicable (probably due to a mismatch of the children's types) => check the next rule - collectedInferenceProblems.push(finalInferenceResult); - } - } - } else { - const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); - if (checkResult) { - // this inference rule was applicable and produced a final result - return checkResult; - } else { - // no result for this inference rule => check the next inference rules - } - } + const result = this.executeSingleInferenceRuleLogic(rule, domainElement, collectedInferenceProblems); + if (result) { + return result; // return the first inferred type } else { - assertUnreachable(rule); + // no result for this inference rule => check the next inference rules } } } @@ -274,20 +218,84 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty return collectedInferenceProblems; } - protected inferTypeLogicWithoutChildren(result: TypeInferenceResultWithoutInferringChildren, collectedInferenceProblems: InferenceProblem[]): Type | InferenceProblem[] | undefined { + protected executeSingleInferenceRuleLogic(rule: TypeInferenceRule, domainElement: unknown, collectedInferenceProblems: InferenceProblem[]): Type | undefined { + if (typeof rule === 'function') { + // simple case without type inference for children + const ruleResult: TypeInferenceResultWithoutInferringChildren = rule(domainElement, this.services); + this.checkForError(ruleResult); + return this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); + } else if (typeof rule === 'object') { + // more complex case with inferring the type for children + const ruleResult: TypeInferenceResultWithInferringChildren = rule.inferTypeWithoutChildren(domainElement, this.services); + if (Array.isArray(ruleResult)) { + // this rule might match => continue applying this rule + // resolve the requested child types + const childElements = ruleResult; + const childTypes: Array = childElements.map(child => this.services.Inference.inferType(child)); + // check, whether inferring the children resulted in some other inference problems + const childTypeProblems: InferenceProblem[] = []; + for (let i = 0; i < childTypes.length; i++) { + const child = childTypes[i]; + if (Array.isArray(child)) { + childTypeProblems.push({ + $problem: InferenceProblem, + domainElement: childElements[i], + location: `child element ${i}`, + rule, + subProblems: child, + }); + } + } + if (childTypeProblems.length >= 1) { + collectedInferenceProblems.push({ + $problem: InferenceProblem, + domainElement, + location: 'inferring depending children', + rule, + subProblems: childTypeProblems, + }); + return undefined; + } else { + // the types of all children are successfully inferred + const finalInferenceResult = rule.inferTypeWithChildrensTypes(domainElement, childTypes as Type[], this.services); + if (isType(finalInferenceResult)) { + // type is inferred! + return finalInferenceResult; + } else { + // inference is not applicable (probably due to a mismatch of the children's types) => check the next rule + collectedInferenceProblems.push(finalInferenceResult); + return undefined; + } + } + } else { + return this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); + } + } else { + assertUnreachable(rule); + } + } + + protected inferTypeLogicWithoutChildren(result: TypeInferenceResultWithoutInferringChildren, collectedInferenceProblems: InferenceProblem[]): Type | undefined { if (result === InferenceRuleNotApplicable) { // this rule is not applicable at all => ignore this rule + return undefined; } else if (isType(result)) { // the result type is already found! return result; } else if (isInferenceProblem(result)) { // found some inference problems collectedInferenceProblems.push(result); + return undefined; } else { // this 'result' domain element is used instead to infer its type, which is the type for the current domain element as well - return this.inferType(result); + const recursiveResult = this.inferType(result); + if (Array.isArray(recursiveResult)) { + collectedInferenceProblems.push(...recursiveResult); + return undefined; + } else { + return recursiveResult; + } } - return undefined; } diff --git a/packages/typir/src/services/kind-registry.ts b/packages/typir/src/services/kind-registry.ts index dbe5ad1..79b4574 100644 --- a/packages/typir/src/services/kind-registry.ts +++ b/packages/typir/src/services/kind-registry.ts @@ -5,15 +5,21 @@ ******************************************************************************/ import { Kind } from '../kinds/kind.js'; +import { TypirServices } from '../typir.js'; export interface KindRegistry { register(kind: Kind): void; - get(type: string): Kind | undefined; + get(type: T['$name']): T | undefined; + getOrCreateKind(type: T['$name'], factory: (services: TypirServices) => T): T; } export class DefaultKindRegistry implements KindRegistry { - // name of kind => kind (for an easier look-up) - protected readonly kinds: Map = new Map(); + protected readonly services: TypirServices; + protected readonly kinds: Map = new Map(); // name of kind => kind (for an easier look-up) + + constructor(services: TypirServices) { + this.services = services; + } register(kind: Kind): void { const key = kind.$name; @@ -28,7 +34,15 @@ export class DefaultKindRegistry implements KindRegistry { } } - get(type: string): Kind | undefined { - return this.kinds.get(type)!; + get(type: T['$name']): T | undefined { + return this.kinds.get(type) as (T | undefined); + } + + getOrCreateKind(type: T['$name'], factory: (services: TypirServices) => T): T { + const existing = this.get(type); + if (existing) { + return existing; + } + return factory(this.services); } } diff --git a/packages/typir/src/services/operator.ts b/packages/typir/src/services/operator.ts index 83fc502..043ed02 100644 --- a/packages/typir/src/services/operator.ts +++ b/packages/typir/src/services/operator.ts @@ -7,9 +7,11 @@ import { Type } from '../graph/type-node.js'; import { TypeInitializer } from '../initialization/type-initializer.js'; import { FunctionFactoryService, NO_PARAMETER_NAME } from '../kinds/function/function-kind.js'; +import { FunctionType } from '../kinds/function/function-type.js'; import { TypirServices } from '../typir.js'; import { NameTypePair, TypeInitializers } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; +import { ValidationProblem } from './validation.js'; // export type InferOperatorWithSingleOperand = (domainElement: unknown, operatorName: string) => boolean | unknown; export type InferOperatorWithSingleOperand = { @@ -24,9 +26,17 @@ export type InferOperatorWithMultipleOperands = { operands: (domainElement: T, operatorName: string) => unknown[]; }; -export interface UnaryOperatorDetails { +export type OperatorValidationRule = (operatorCall: T, operatorName: string, operatorType: Type, typir: TypirServices) => ValidationProblem[]; + +export interface AnyOperatorDetails { name: string; - signature: UnaryOperatorSignature | UnaryOperatorSignature[]; + // TODO Review: should OperatorValidationRule and InferOperatorWithSingleOperand/InferOperatorWithMultipleOperands be merged/combined, since they shared the same type parameter T ? + validationRule?: OperatorValidationRule; +} + +export interface UnaryOperatorDetails extends AnyOperatorDetails { + signature?: UnaryOperatorSignature; + signatures?: UnaryOperatorSignature[]; inferenceRule?: InferOperatorWithSingleOperand; } export interface UnaryOperatorSignature { @@ -34,9 +44,9 @@ export interface UnaryOperatorSignature { return: Type; } -export interface BinaryOperatorDetails { - name: string; - signature: BinaryOperatorSignature | BinaryOperatorSignature[]; +export interface BinaryOperatorDetails extends AnyOperatorDetails { + signature?: BinaryOperatorSignature; + signatures?: BinaryOperatorSignature[]; inferenceRule?: InferOperatorWithMultipleOperands; } export interface BinaryOperatorSignature { @@ -45,9 +55,9 @@ export interface BinaryOperatorSignature { return: Type; } -export interface TernaryOperatorDetails { - name: string; - signature: TernaryOperatorSignature | TernaryOperatorSignature[]; +export interface TernaryOperatorDetails extends AnyOperatorDetails { + signature?: TernaryOperatorSignature; + signatures?: TernaryOperatorSignature[]; inferenceRule?: InferOperatorWithMultipleOperands; } export interface TernaryOperatorSignature { @@ -57,15 +67,13 @@ export interface TernaryOperatorSignature { return: Type; } -export interface GenericOperatorDetails { - name: string; +export interface GenericOperatorDetails extends AnyOperatorDetails { outputType: Type; inputParameter: NameTypePair[]; inferenceRule?: InferOperatorWithSingleOperand | InferOperatorWithMultipleOperands; } -// TODO rename it to "OperatorFactory", when there are no more responsibilities! -export interface OperatorManager { +export interface OperatorFactoryService { createUnary(typeDetails: UnaryOperatorDetails): TypeInitializers createBinary(typeDetails: BinaryOperatorDetails): TypeInitializers createTernary(typeDetails: TernaryOperatorDetails): TypeInitializers @@ -92,7 +100,7 @@ export interface OperatorManager { * * All operands are mandatory. */ -export class DefaultOperatorManager implements OperatorManager { +export class DefaultOperatorFactory implements OperatorFactoryService { protected readonly services: TypirServices; constructor(services: TypirServices) { @@ -100,7 +108,7 @@ export class DefaultOperatorManager implements OperatorManager { } createUnary(typeDetails: UnaryOperatorDetails): TypeInitializers { - const signatures = toArray(typeDetails.signature); + const signatures = toSignatureArray(typeDetails); const result: Array> = []; for (const signature of signatures) { result.push(this.createGeneric({ @@ -109,14 +117,15 @@ export class DefaultOperatorManager implements OperatorManager { inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! inputParameter: [ { name: 'operand', type: signature.operand }, - ] + ], + validationRule: typeDetails.validationRule, })); } return result.length === 1 ? result[0] : result; } createBinary(typeDetails: BinaryOperatorDetails): TypeInitializers { - const signatures = toArray(typeDetails.signature); + const signatures = toSignatureArray(typeDetails); const result: Array> = []; for (const signature of signatures) { result.push(this.createGeneric({ @@ -126,14 +135,15 @@ export class DefaultOperatorManager implements OperatorManager { inputParameter: [ { name: 'left', type: signature.left}, { name: 'right', type: signature.right} - ] + ], + validationRule: typeDetails.validationRule, })); } return result.length === 1 ? result[0] : result; } createTernary(typeDetails: TernaryOperatorDetails): TypeInitializers { - const signatures = toArray(typeDetails.signature); + const signatures = toSignatureArray(typeDetails); const result: Array> = []; for (const signature of signatures) { result.push(this.createGeneric({ @@ -144,7 +154,8 @@ export class DefaultOperatorManager implements OperatorManager { { name: 'first', type: signature.first }, { name: 'second', type: signature.second }, { name: 'third', type: signature.third }, - ] + ], + validationRule: typeDetails.validationRule, })); } return result.length === 1 ? result[0] : result; @@ -153,10 +164,11 @@ export class DefaultOperatorManager implements OperatorManager { createGeneric(typeDetails: GenericOperatorDetails): TypeInitializer { // define/register the wanted operator as "special" function const functionFactory = this.getFunctionFactory(); + const operatorName = typeDetails.name; // create the operator as type of kind 'function' const newOperatorType = functionFactory.create({ - functionName: typeDetails.name, + functionName: operatorName, outputParameter: { name: NO_PARAMETER_NAME, type: typeDetails.outputType }, inputParameters: typeDetails.inputParameter, inferenceRuleForDeclaration: undefined, // operators have no declaration in the code => no inference rule for the operator declaration! @@ -164,17 +176,38 @@ export class DefaultOperatorManager implements OperatorManager { ? { filter: (domainElement: unknown): domainElement is T => typeDetails.inferenceRule!.filter(domainElement, typeDetails.name), matching: (domainElement: T) => typeDetails.inferenceRule!.matching(domainElement, typeDetails.name), - inputArguments: (domainElement: T) => 'operands' in typeDetails.inferenceRule! - ? (typeDetails.inferenceRule as InferOperatorWithMultipleOperands).operands(domainElement, typeDetails.name) - : [(typeDetails.inferenceRule as InferOperatorWithSingleOperand).operand(domainElement, typeDetails.name)], + inputArguments: (domainElement: T) => this.getInputArguments(typeDetails, domainElement), } - : undefined + : undefined, + validationForCall: typeDetails.validationRule + ? (functionCall: T, functionType: FunctionType, typir: TypirServices) => typeDetails.validationRule!(functionCall, operatorName, functionType, typir) + : undefined, }); return newOperatorType as unknown as TypeInitializer; } + protected getInputArguments(typeDetails: GenericOperatorDetails, domainElement: unknown): unknown[] { + return 'operands' in typeDetails.inferenceRule! + ? (typeDetails.inferenceRule as InferOperatorWithMultipleOperands).operands(domainElement, typeDetails.name) + : [(typeDetails.inferenceRule as InferOperatorWithSingleOperand).operand(domainElement, typeDetails.name)]; + } + protected getFunctionFactory(): FunctionFactoryService { - return this.services.factory.functions; + return this.services.factory.Functions; + } +} + +function toSignatureArray(values: { + signature?: T; + signatures?: T[]; +}): T[] { + const result = toArray(values.signatures); + if (values.signature) { + result.push(values.signature); + } + if (result.length <= 0) { + throw new Error('At least one signature must be given!'); } + return result; } diff --git a/packages/typir/src/services/subtype.ts b/packages/typir/src/services/subtype.ts index 96ad100..aa6a45c 100644 --- a/packages/typir/src/services/subtype.ts +++ b/packages/typir/src/services/subtype.ts @@ -13,7 +13,6 @@ import { TypeEdge, isTypeEdge } from '../graph/type-edge.js'; export interface SubTypeProblem extends TypirProblem { $problem: 'SubTypeProblem'; - // 'undefined' means type or information is missing, 'string' is for data which are no Types superType: Type; subType: Type; subProblems: TypirProblem[]; // might be empty @@ -51,7 +50,7 @@ export class DefaultSubType implements SubType { protected readonly typeRelationships: TypeRelationshipCaching; constructor(services: TypirServices) { - this.typeRelationships = services.caching.typeRelationships; + this.typeRelationships = services.caching.TypeRelationships; } isSubType(subType: Type, superType: Type): boolean { diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 39477ec..e108ab2 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -32,12 +32,12 @@ export function isValidationProblem(problem: unknown): problem is ValidationProb return isSpecificTypirProblem(problem, ValidationProblem); } -export type ValidationRule = (domainElement: unknown, typir: TypirServices) => ValidationProblem[]; +export type ValidationRule = (domainElement: T, typir: TypirServices) => ValidationProblem[]; -export interface ValidationRuleWithBeforeAfter { - beforeValidation(domainRoot: unknown, typir: TypirServices): ValidationProblem[] - validation: ValidationRule - afterValidation(domainRoot: unknown, typir: TypirServices): ValidationProblem[] +export interface ValidationRuleWithBeforeAfter { + beforeValidation(domainRoot: RootType, typir: TypirServices): ValidationProblem[]; + validation: ValidationRule; + afterValidation(domainRoot: RootType, typir: TypirServices): ValidationProblem[]; } /** Annotate types after the validation with additional information in order to ease the creation of usefull messages. */ @@ -67,8 +67,8 @@ export class DefaultValidationConstraints implements ValidationConstraints { constructor(services: TypirServices) { this.services = services; - this.inference = services.inference; - this.printer = services.printer; + this.inference = services.Inference; + this.printer = services.Printer; } ensureNodeIsAssignable(sourceNode: unknown | undefined, expected: Type | undefined | unknown, @@ -142,10 +142,10 @@ export class DefaultValidationConstraints implements ValidationConstraints { } -export interface ValidationCollector { - validateBefore(domainRoot: unknown): ValidationProblem[]; - validate(domainElement: unknown): ValidationProblem[]; - validateAfter(domainRoot: unknown): ValidationProblem[]; +export interface ValidationCollector { + validateBefore(domainRoot: RootType): ValidationProblem[]; + validate(domainElement: ElementType): ValidationProblem[]; + validateAfter(domainRoot: RootType): ValidationProblem[]; /** * Registers a validation rule. @@ -153,27 +153,30 @@ export interface ValidationCollector { * @param boundToType an optional type, if the new validation rule is dedicated for exactly this type. * If the given type is removed from the type system, this rule will be automatically removed as well. */ - addValidationRule(rule: ValidationRule, boundToType?: Type): void; + addValidationRule(rule: ValidationRule, boundToType?: Type): void; + removeValidationRule(rule: ValidationRule, boundToType?: Type): void; + /** * Registers a validation rule which will be called once before and once after the whole validation. * @param rule a new validation rule * @param boundToType an optional type, if the new validation rule is dedicated for exactly this type. * If the given type is removed from the type system, this rule will be automatically removed as well. */ - addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void; + addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void; + removeValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void; } -export class DefaultValidationCollector implements ValidationCollector, TypeGraphListener { +export class DefaultValidationCollector implements ValidationCollector, TypeGraphListener { protected readonly services: TypirServices; - protected readonly validationRules: Map = new Map(); // type identifier (otherwise '') -> validation rules - protected readonly validationRulesBeforeAfter: Map = new Map(); // type identifier (otherwise '') -> validation rules + protected readonly validationRules: Map>> = new Map(); // type identifier (otherwise '') -> validation rules + protected readonly validationRulesBeforeAfter: Map>> = new Map(); // type identifier (otherwise '') -> validation rules constructor(services: TypirServices) { this.services = services; - this.services.graph.addListener(this); + this.services.infrastructure.Graph.addListener(this); } - validateBefore(domainRoot: unknown): ValidationProblem[] { + validateBefore(domainRoot: RootType): ValidationProblem[] { const problems: ValidationProblem[] = []; for (const rules of this.validationRulesBeforeAfter.values()) { for (const rule of rules) { @@ -183,7 +186,7 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap return problems; } - validate(domainElement: unknown): ValidationProblem[] { + validate(domainElement: ElementType): ValidationProblem[] { const problems: ValidationProblem[] = []; for (const rules of this.validationRules.values()) { for (const rule of rules) { @@ -198,7 +201,7 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap return problems; } - validateAfter(domainRoot: unknown): ValidationProblem[] { + validateAfter(domainRoot: RootType): ValidationProblem[] { const problems: ValidationProblem[] = []; for (const rules of this.validationRulesBeforeAfter.values()) { for (const rule of rules) { @@ -208,7 +211,7 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap return problems; } - addValidationRule(rule: ValidationRule, boundToType?: Type): void { + addValidationRule(rule: ValidationRule, boundToType?: Type): void { const key = this.getBoundToTypeKey(boundToType); let rules = this.validationRules.get(key); if (!rules) { @@ -218,7 +221,18 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap rules.push(rule); } - addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void { + removeValidationRule(rule: ValidationRule, boundToType?: Type): void { + const key = this.getBoundToTypeKey(boundToType); + const rules = this.validationRules.get(key); + if (rules) { + const index = rules.indexOf(rule); + if (index >= 0) { + rules.splice(index, 1); + } + } + } + + addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void { const key = this.getBoundToTypeKey(boundToType); let rules = this.validationRulesBeforeAfter.get(key); if (!rules) { @@ -228,6 +242,17 @@ export class DefaultValidationCollector implements ValidationCollector, TypeGrap rules.push(rule); } + removeValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void { + const key = this.getBoundToTypeKey(boundToType); + const rules = this.validationRulesBeforeAfter.get(key); + if (rules) { + const index = rules.indexOf(rule); + if (index >= 0) { + rules.splice(index, 1); + } + } + } + protected getBoundToTypeKey(boundToType?: Type): string { return boundToType?.getIdentifier() ?? ''; } diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index ee10ee4..239c98c 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -5,93 +5,98 @@ ******************************************************************************/ 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 { DefaultTypeResolver, TypeResolvingService } from './initialization/type-selector.js'; +import { BottomFactoryService, BottomKind, BottomKindName } from './kinds/bottom/bottom-kind.js'; +import { ClassFactoryService, ClassKind, ClassKindName } from './kinds/class/class-kind.js'; +import { FunctionFactoryService, FunctionKind, FunctionKindName } from './kinds/function/function-kind.js'; +import { PrimitiveFactoryService, PrimitiveKind, PrimitiveKindName } from './kinds/primitive/primitive-kind.js'; +import { TopFactoryService, TopKind, TopKindName } 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 { DefaultOperatorFactory, OperatorFactoryService } 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 { inject, Module } from './utils/dependency-injection.js'; /** - * Design decisions for Typir - * - no NameProvider for the name of types, since the name depends on the type of the kind => change the implementation of the kind - * - the type 'void' has a primitive kind (no dedicated kind for now) + * Some design decisions for Typir: + * - We don't use a graph library like graphology to realize the type graph in order to be more flexible and to reduce external dependencies. + * - Where should inference rules be stored? Inference rules are stored in the central service, optionally bound to types in order to simplify removal of deleted types. + * Inference rules are not linked to kinds (at least for now), since different types (of the same kind) might have different inference rules. + * - No NameProvider for the name of types, since the name depends on the type of the kind => change the implementation of the kind. + * - The type 'void' has a primitive kind (no dedicated kind for now). * - Once created/initialized, types are constant, e.g. no additional fields can be added to classes (but their types might be resolved a bit later). + * - It is possible to use two different Typir instances side-by-side within the same application in general, + * since the services are not realized by global functions, but by methods of classes which implement service interfaces. */ -/** Open design questions TODO - * - use graphology for the TypeGraph? - * - Where should inference rules be stored? only in the central service? in types? in kinds? - * - Type is generic VS there are specific types like FunctionType (extends Type)?? functionType.kind.getOutput(functionKind) + isFunctionKind() feels bad! vs functionType.getOutput() + isFunctionType() - * - realize "unknown" as a generic "" for whole Typir? for the Langium binding T would be AstNode! - * - Is it easy to use two different Typir instances side-by-side within the same application? +/** Some open design questions for future releases TODO + * - Replace "unknown" as a generic "" for whole Typir? For Typir-Langium T would be AstNode! * - How to bundle Typir configurations for reuse ("presets")? - * - How to handle cycles? - * - Cycles at types: MyClass { myField?: MyClass, myFunction(operand: MyClass) }, MyClass> to return typed sub-class instances - * - Cycles at instances/objects: Parent used as Child?! */ export type TypirServices = { - readonly assignability: TypeAssignability; - readonly equality: TypeEquality; - readonly conversion: TypeConversion; - readonly subtype: SubType; - readonly inference: TypeInferenceCollector; + readonly Assignability: TypeAssignability; + readonly Equality: TypeEquality; + readonly Conversion: TypeConversion; + readonly Subtype: SubType; + readonly Inference: TypeInferenceCollector; readonly caching: { - readonly typeRelationships: TypeRelationshipCaching; - readonly domainElementInference: DomainElementInferenceCaching; + readonly TypeRelationships: TypeRelationshipCaching; + readonly DomainElementInference: DomainElementInferenceCaching; }; - readonly graph: TypeGraph; - readonly kinds: KindRegistry; - readonly printer: ProblemPrinter; + readonly Printer: ProblemPrinter; readonly validation: { - readonly collector: ValidationCollector; - readonly constraints: ValidationConstraints; + 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; + readonly Primitives: PrimitiveFactoryService; + readonly Functions: FunctionFactoryService; + readonly Classes: ClassFactoryService; + readonly Top: TopFactoryService; + readonly Bottom: BottomFactoryService; + readonly Operators: OperatorFactoryService; + }; + readonly infrastructure: { + readonly Graph: TypeGraph; + readonly Kinds: KindRegistry; + readonly TypeResolver: TypeResolvingService; }; }; export const DefaultTypirServiceModule: Module = { - assignability: (services) => new DefaultTypeAssignability(services), - equality: (services) => new DefaultTypeEquality(services), - conversion: (services) => new DefaultTypeConversion(services), - graph: () => new TypeGraph(), - subtype: (services) => new DefaultSubType(services), - inference: (services) => new DefaultTypeInferenceCollector(services), + Assignability: (services) => new DefaultTypeAssignability(services), + Equality: (services) => new DefaultTypeEquality(services), + Conversion: (services) => new DefaultTypeConversion(services), + Subtype: (services) => new DefaultSubType(services), + Inference: (services) => new DefaultTypeInferenceCollector(services), caching: { - typeRelationships: (services) => new DefaultTypeRelationshipCaching(services), - domainElementInference: () => new DefaultDomainElementInferenceCaching() + TypeRelationships: (services) => new DefaultTypeRelationshipCaching(services), + DomainElementInference: () => new DefaultDomainElementInferenceCaching(), }, - kinds: () => new DefaultKindRegistry(), - printer: () => new DefaultTypeConflictPrinter(), + Printer: () => new DefaultTypeConflictPrinter(), validation: { - collector: (services) => new DefaultValidationCollector(services), - constraints: (services) => new DefaultValidationConstraints(services), + 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), + Primitives: (services) => services.infrastructure.Kinds.getOrCreateKind(PrimitiveKindName, services => new PrimitiveKind(services)), + Functions: (services) => services.infrastructure.Kinds.getOrCreateKind(FunctionKindName, services => new FunctionKind(services)), + Classes: (services) => services.infrastructure.Kinds.getOrCreateKind(ClassKindName, services => new ClassKind(services, { typing: 'Nominal' })), + Top: (services) => services.infrastructure.Kinds.getOrCreateKind(TopKindName, services => new TopKind(services)), + Bottom: (services) => services.infrastructure.Kinds.getOrCreateKind(BottomKindName, services => new BottomKind(services)), + Operators: (services) => new DefaultOperatorFactory(services), + }, + infrastructure: { + Graph: () => new TypeGraph(), + Kinds: (services) => new DefaultKindRegistry(services), + TypeResolver: (services) => new DefaultTypeResolver(services), }, }; diff --git a/packages/typir/src/utils/test-utils.ts b/packages/typir/src/utils/test-utils.ts index 698df97..93b1188 100644 --- a/packages/typir/src/utils/test-utils.ts +++ b/packages/typir/src/utils/test-utils.ts @@ -18,7 +18,7 @@ import { TypirServices } from '../typir.js'; * @returns all the found types */ export function expectTypirTypes(services: TypirServices, filterTypes: (type: Type) => boolean, ...namesOfExpectedTypes: string[]): Type[] { - const types = services.graph.getAllRegisteredTypes().filter(filterTypes); + const types = services.infrastructure.Graph.getAllRegisteredTypes().filter(filterTypes); types.forEach(type => expect(type.getInitializationState()).toBe('Completed')); // check that all types are 'Completed' const typeNames = types.map(t => t.getName()); expect(typeNames, typeNames.join(', ')).toHaveLength(namesOfExpectedTypes.length); diff --git a/packages/typir/src/utils/utils-type-comparison.ts b/packages/typir/src/utils/utils-type-comparison.ts index 12301bb..e52f27d 100644 --- a/packages/typir/src/utils/utils-type-comparison.ts +++ b/packages/typir/src/utils/utils-type-comparison.ts @@ -20,14 +20,14 @@ export type TypeCheckStrategy = export function createTypeCheckStrategy(strategy: TypeCheckStrategy, typir: TypirServices): (t1: Type, t2: Type) => TypirProblem | undefined { switch (strategy) { case 'ASSIGNABLE_TYPE': - return typir.assignability.getAssignabilityProblem // t1 === source, t2 === target - .bind(typir.assignability); + return typir.Assignability.getAssignabilityProblem // t1 === source, t2 === target + .bind(typir.Assignability); case 'EQUAL_TYPE': - return typir.equality.getTypeEqualityProblem // (unordered, order does not matter) - .bind(typir.equality); + return typir.Equality.getTypeEqualityProblem // (unordered, order does not matter) + .bind(typir.Equality); case 'SUB_TYPE': - return typir.subtype.getSubTypeProblem // t1 === sub, t2 === super - .bind(typir.subtype); + return typir.Subtype.getSubTypeProblem // t1 === sub, t2 === super + .bind(typir.Subtype); // .bind(...) is required to have the correct value for 'this' inside the referenced function/method! // see https://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback default: diff --git a/packages/typir/test/api-example.test.ts b/packages/typir/test/api-example.test.ts index 9b2548a..b2e8f28 100644 --- a/packages/typir/test/api-example.test.ts +++ b/packages/typir/test/api-example.test.ts @@ -4,6 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +/* eslint-disable @typescript-eslint/parameter-properties */ + import { describe, expect, test } from 'vitest'; import { InferenceRuleNotApplicable, InferOperatorWithMultipleOperands, ValidationMessageDetails } from '../src/index.js'; import { createTypirServices } from '../src/typir.js'; @@ -12,16 +14,15 @@ describe('Tests for the new API', () => { test('Experiments', async () => { const typir = createTypirServices(); - const booleanType = typir.factory.primitives.create({ primitiveName: 'boolean' }); + const booleanType = typir.factory.Primitives.create({ primitiveName: 'boolean' }); expect(booleanType).toBeTruthy(); - const getBool = typir.factory.primitives.get({ primitiveName: 'boolean' }); + const getBool = typir.factory.Primitives.get({ primitiveName: 'boolean' }); expect(getBool).toBe(booleanType); - typir.factory.functions.create({ functionName: 'myFunction', inputParameters: [], outputParameter: undefined }); + 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! + typir.factory.Operators.createBinary({ name: '&&', signature: { left: booleanType, right: booleanType, return: booleanType } }); }); @@ -29,8 +30,8 @@ describe('Tests for the new API', () => { 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 }); + 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 = { @@ -38,17 +39,17 @@ describe('Tests for the new API', () => { matching: (node, operatorName) => node.operator === operatorName, operands: node => [node.left, node.right], }; - typir.factory.operators.createBinary({ name: '+', signature: [ // operator overloading + typir.factory.Operators.createBinary({ name: '+', signatures: [ // 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 + typir.factory.Operators.createBinary({ name: '-', signatures: [{ left: numberType, right: numberType, return: numberType }], inferenceRule }); // 2 - 3 // numbers are implicitly convertable to strings - typir.conversion.markAsConvertible(numberType, stringType, 'IMPLICIT_EXPLICIT'); + typir.Conversion.markAsConvertible(numberType, stringType, 'IMPLICIT_EXPLICIT'); // specify, how Typir can detect the type of a variable - typir.inference.addInferenceRule(node => { + typir.Inference.addInferenceRule(node => { if (node instanceof Variable) { return node.initialValue; // the type of the variable is the type of its initial value } @@ -56,9 +57,9 @@ describe('Tests for the new API', () => { }); // register a type-related validation - typir.validation.collector.addValidationRule(node => { + typir.validation.Collector.addValidationRule(node => { if (node instanceof AssignmentStatement) { - return typir.validation.constraints.ensureNodeIsAssignable(node.right, node.left, (actual, expected) => { message: + 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 []; @@ -66,28 +67,28 @@ describe('Tests for the new API', () => { // 2 + 3 => OK const example1 = new BinaryExpression(new NumberLiteral(2), '+', new NumberLiteral(3)); - expect(typir.validation.collector.validate(example1)).toHaveLength(0); + 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); + 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 + 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); + 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); + const errors2 = typir.validation.Collector.validate(assignStringToNumber); expect(errors2[0].message).toBe("The type 'string' is not assignable to the type 'number'."); }); @@ -98,48 +99,34 @@ abstract class AstElement { } class NumberLiteral extends AstElement { - value: number; - constructor(value: number) { - super(); - this.value = value; - } + constructor( + public value: number, + ) { super(); } } class StringLiteral extends AstElement { - value: string; - constructor(value: string) { - super(); - this.value = value; - } + constructor( + public value: string, + ) { super(); } } 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; - } + constructor( + public left: AstElement, + public operator: string, + public right: AstElement, + ) { super(); } } class Variable extends AstElement { - name: string; - initialValue: AstElement; - constructor(name: string, initialValue: AstElement) { - super(); - this.name = name; - this.initialValue = initialValue; - } + constructor( + public name: string, + public initialValue: AstElement, + ) { super(); } } class AssignmentStatement extends AstElement { - left: Variable; - right: AstElement; - constructor(left: Variable, right: AstElement) { - super(); - this.left = left; - this.right = right; - } + constructor( + public left: Variable, + public right: AstElement, + ) { super(); } } diff --git a/packages/typir/test/type-definitions.test.ts b/packages/typir/test/type-definitions.test.ts index 3d135a4..72b7e36 100644 --- a/packages/typir/test/type-definitions.test.ts +++ b/packages/typir/test/type-definitions.test.ts @@ -19,7 +19,7 @@ describe('Tests for Typir', () => { const typir = createTypirServices({ // customize some default factories for predefined types factory: { - classes: (services) =>new ClassKind(typir, { typing: 'Structural', maximumNumberOfSuperClasses: 1, subtypeFieldChecking: 'SUB_TYPE' }), + Classes: (services) =>new ClassKind(typir, { typing: 'Structural', maximumNumberOfSuperClasses: 1, subtypeFieldChecking: 'SUB_TYPE' }), }, }); @@ -29,14 +29,14 @@ describe('Tests for Typir', () => { const mapKind = new FixedParameterKind(typir, 'Map', { parameterSubtypeCheckingStrategy: 'EQUAL_TYPE' }, 'key', 'value'); // create some primitive types - const typeInt = typir.factory.primitives.create({ primitiveName: 'Integer' }); - const typeString = typir.factory.primitives.create({ 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 = typir.factory.primitives.create({ 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 = typir.factory.classes.create({ + const typePerson = typir.factory.Classes.create({ className: 'Person', fields: [ { name: 'firstName', type: typeString }, @@ -46,7 +46,7 @@ describe('Tests for Typir', () => { methods: [], }); console.log(typePerson.getTypeFinal()!.getUserRepresentation()); - const typeStudent = typir.factory.classes.create({ + const typeStudent = typir.factory.Classes.create({ className: 'Student', superClasses: typePerson, // a Student is a special Person fields: [ @@ -59,50 +59,50 @@ 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 = typir.factory.functions.create({ + 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.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 }, + 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.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 } }); + 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.factory.operators.createUnary({ 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.factory.operators.createTernary({ 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: - typir.conversion.markAsConvertible([typeInt, typeInt], [typeString, typeString, typeString], 'EXPLICIT'); + typir.Conversion.markAsConvertible([typeInt, typeInt], [typeString, typeString, typeString], 'EXPLICIT'); // single relationships are possible as well - typir.conversion.markAsConvertible(typeInt, typeString, 'IMPLICIT_EXPLICIT'); + typir.Conversion.markAsConvertible(typeInt, typeString, 'IMPLICIT_EXPLICIT'); // is assignable? // primitives - expect(typir.assignability.isAssignable(typeInt, typeInt)).toBe(true); - expect(typir.assignability.isAssignable(typeInt, typeString)).toBe(true); - expect(typir.assignability.isAssignable(typeString, typeInt)).not.toBe(true); + expect(typir.Assignability.isAssignable(typeInt, typeInt)).toBe(true); + expect(typir.Assignability.isAssignable(typeInt, typeString)).toBe(true); + expect(typir.Assignability.isAssignable(typeString, typeInt)).not.toBe(true); // List, Map // expect(typir.assignability.isAssignable(typeListInt, typeMapStringPerson)).not.toBe(true); - expect(typir.assignability.isAssignable(typeListInt, typeListString)).not.toBe(true); - expect(typir.assignability.isAssignable(typeListInt, typeListInt)).toBe(true); + expect(typir.Assignability.isAssignable(typeListInt, typeListString)).not.toBe(true); + expect(typir.Assignability.isAssignable(typeListInt, typeListInt)).toBe(true); // classes // expect(typir.assignability.isAssignable(typeStudent, typePerson)).toBe(true); // const assignConflicts = typir.assignability.getAssignabilityProblem(typePerson, typeStudent); diff --git a/resources/talks/2024-10-24-EclipseCon.pdf b/resources/talks/2024-10-24-EclipseCon.pdf new file mode 100644 index 0000000..6a7e298 Binary files /dev/null and b/resources/talks/2024-10-24-EclipseCon.pdf differ