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.
+
+
+---
+
+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