diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 845fb1e..d27dea2 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -13,7 +13,7 @@ on: jobs: build: - name: monaco-languageclient + name: typir-build runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -33,10 +33,10 @@ jobs: run: | npm run build - - name: Lint - shell: bash - run: | - npm run lint +# - name: Lint +# shell: bash +# run: | +# npm run lint - name: Test shell: bash diff --git a/examples/lox/package.json b/examples/lox/package.json index 6280c73..e377add 100644 --- a/examples/lox/package.json +++ b/examples/lox/package.json @@ -29,7 +29,7 @@ "dependencies": { "commander": "~12.1.0", "langium": "~3.2.0", - "typir": "~0.0.1", + "typir-langium": "~0.0.1", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" }, diff --git a/examples/lox/src/language/lox-module.ts b/examples/lox/src/language/lox-module.ts index aeed7ed..ac73166 100644 --- a/examples/lox/src/language/lox-module.ts +++ b/examples/lox/src/language/lox-module.ts @@ -8,7 +8,10 @@ import { DefaultSharedCoreModuleContext, LangiumCoreServices, LangiumSharedCoreS import { LoxGeneratedModule, LoxGeneratedSharedModule } from './generated/module.js'; import { LoxScopeProvider } from './lox-scope.js'; import { LoxValidationRegistry, LoxValidator } from './lox-validator.js'; -import { DefaultSharedModuleContext, LangiumSharedServices, createDefaultSharedModule } from 'langium/lsp'; +import { DefaultSharedModuleContext, LangiumServices, LangiumSharedServices, createDefaultSharedModule } from 'langium/lsp'; +import { createLangiumModuleForTypirBinding, initializeLangiumTypirServices, LangiumServicesForTypirBinding } from 'typir-langium'; +import { createLoxTypirModule } from './type-system/lox-type-checking.js'; +import { registerValidationChecks } from 'langium/grammar'; /** * Declaration of custom services - add your own service classes here. @@ -23,7 +26,7 @@ export type LoxAddedServices = { * Union of Langium default services and your custom services - use this as constructor parameter * of custom service classes. */ -export type LoxServices = LangiumCoreServices & LoxAddedServices +export type LoxServices = LangiumServices & LoxAddedServices & LangiumServicesForTypirBinding /** * Dependency injection module that overrides Langium default services and contributes the @@ -66,8 +69,11 @@ export function createLoxServices(context: DefaultSharedModuleContext): { const Lox = inject( createDefaultCoreModule({ shared }), LoxGeneratedModule, - LoxModule + createLangiumModuleForTypirBinding(shared), + LoxModule, + createLoxTypirModule(shared), ); shared.ServiceRegistry.register(Lox); + initializeLangiumTypirServices(Lox); return { shared, Lox }; } diff --git a/examples/lox/src/language/lox-validator.ts b/examples/lox/src/language/lox-validator.ts index 511e0ea..b4677c7 100644 --- a/examples/lox/src/language/lox-validator.ts +++ b/examples/lox/src/language/lox-validator.ts @@ -4,14 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium'; -import { BinaryExpression, Class, ExpressionBlock, FunctionDeclaration, isReturnStatement, LoxAstType, LoxProgram, MethodMember, TypeReference, UnaryExpression, VariableDeclaration } from './generated/ast.js'; +import { AstNode, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium'; +import { BinaryExpression, LoxAstType, VariableDeclaration } from './generated/ast.js'; import type { LoxServices } from './lox-module.js'; -import { isAssignable } from './type-system/assignment.js'; -import { isVoidType, TypeDescription, typeToString } from './type-system/descriptions.js'; +import { TypeDescription } from './type-system/descriptions.js'; import { inferType } from './type-system/infer.js'; import { isLegalOperation } from './type-system/operator.js'; -import { createTypir } from './type-system/lox-type-checking.js'; /** * Registry for validation checks. @@ -23,31 +21,17 @@ export class LoxValidationRegistry extends ValidationRegistry { const checks: ValidationChecks = { BinaryExpression: validator.checkBinaryOperationAllowed, VariableDeclaration: validator.checkVariableDeclaration, - LoxProgram: validator.checkTypingProblemsWithTypir, }; this.register(checks, validator); } } /** - * Implementation of custom validations. + * Implementation of custom validations on the syntactic level (which can be checked without using Typir). + * Validations on type level are done by Typir. */ export class LoxValidator { - checkTypingProblemsWithTypir(node: LoxProgram, accept: ValidationAcceptor) { - // executes all checks, which are directly derived from the current Typir configuration, - // i.e. arguments fit to parameters for function calls (including operands for operators) - const typir = createTypir(node); - AstUtils.streamAllContents(node).forEach(node => { - // print all found problems for each AST node - const typeProblems = typir.validation.collector.validate(node); - for (const problem of typeProblems) { - const message = typir.printer.printValidationProblem(problem); - accept(problem.severity, message, { node, property: problem.domainProperty, index: problem.domainIndex }); - } - }); - } - checkVariableDeclaration(decl: VariableDeclaration, accept: ValidationAcceptor): void { if (!decl.type && !decl.value) { accept('error', 'Variables require a type hint or an assignment at creation', { diff --git a/examples/lox/src/language/type-system/lox-type-checking.ts b/examples/lox/src/language/type-system/lox-type-checking.ts index 3377ed1..be3a808 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -4,108 +4,205 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, Module, assertUnreachable, isAstNode } from 'langium'; -import { ClassKind, CreateFieldDetails, DefaultTypeConflictPrinter, FUNCTION_MISSING_NAME, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, ParameterDetails, PartialTypirServices, PrimitiveKind, TopKind, TypirServices, createTypirServices } from 'typir'; +import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; +import { LangiumSharedServices } from 'langium/lsp'; +import { ClassKind, CreateFieldDetails, FUNCTION_MISSING_NAME, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, OperatorManager, ParameterDetails, PartialTypirServices, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation } from 'typir'; +import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { ValidationMessageDetails } from '../../../../../packages/typir/lib/features/validation.js'; -import { BinaryExpression, FieldMember, MemberCall, TypeReference, UnaryExpression, isBinaryExpression, isBooleanLiteral, isClass, isClassMember, isFieldMember, isForStatement, isFunctionDeclaration, isIfStatement, isLoxProgram, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from '../generated/ast.js'; +import { BinaryExpression, FieldMember, MemberCall, TypeReference, UnaryExpression, isBinaryExpression, isBooleanLiteral, isClass, isClassMember, isFieldMember, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from '../generated/ast.js'; -export function createTypir(domainNodeEntry: AstNode): TypirServices { - // set up Typir and reuse some predefined things - const typir = createTypirServices(LoxTypirModule); - const primitiveKind = new PrimitiveKind(typir); - const functionKind = new FunctionKind(typir); - const classKind = new ClassKind(typir, { - typing: 'Nominal', - }); - const anyKind = new TopKind(typir); - const operators = typir.operators; +export class LoxTypeCreator extends AbstractLangiumTypeCreator { + protected readonly typir: TypirServices; + protected readonly primitiveKind: PrimitiveKind; + protected readonly functionKind: FunctionKind; + protected readonly classKind: ClassKind; + protected readonly anyKind: TopKind; + protected readonly operators: OperatorManager; - // primitive types - // typeBool, typeNumber and typeVoid are specific types for OX, ... - const typeBool = primitiveKind.createPrimitiveType({ primitiveName: 'boolean', - inferenceRules: [ - isBooleanLiteral, - (node: unknown) => isTypeReference(node) && node.primitive === 'boolean' - ]}); - // ... but their primitive kind is provided/preset by Typir - const typeNumber = primitiveKind.createPrimitiveType({ primitiveName: 'number', - inferenceRules: [ - isNumberLiteral, - (node: unknown) => isTypeReference(node) && node.primitive === 'number' - ]}); - const typeString = primitiveKind.createPrimitiveType({ primitiveName: 'string', - inferenceRules: [ - isStringLiteral, - (node: unknown) => isTypeReference(node) && node.primitive === 'string' - ]}); - const typeVoid = primitiveKind.createPrimitiveType({ primitiveName: 'void', - inferenceRules: [ - (node: unknown) => isTypeReference(node) && node.primitive === 'void', - isPrintStatement, - (node: unknown) => isReturnStatement(node) && node.value === undefined - ] }); - const typeNil = primitiveKind.createPrimitiveType({ primitiveName: 'nil', - inferenceRules: isNilLiteral }); // TODO for what is this used? - const typeAny = anyKind.createTopType({}); + constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { + super(typirServices, langiumServices); + this.typir = typirServices; - // 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 = { - filter: isBinaryExpression, - matching: (node: BinaryExpression, name: string) => node.operator === name, - operands: (node: BinaryExpression, _name: string) => [node.left, node.right], - }; - const unaryInferenceRule: InferOperatorWithSingleOperand = { - filter: isUnaryExpression, - matching: (node: UnaryExpression, name: string) => node.operator === name, - operand: (node: UnaryExpression, _name: string) => node.value, - }; - - // binary operators: numbers => number - for (const operator of ['-', '*', '/']) { - operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); + this.primitiveKind = new PrimitiveKind(this.typir); + this.functionKind = new FunctionKind(this.typir); + this.classKind = new ClassKind(this.typir, { + typing: 'Nominal', + }); + this.anyKind = new TopKind(this.typir); + this.operators = this.typir.operators; } - operators.createBinaryOperator({ name: '+', signature: [ - { left: typeNumber, right: typeNumber, return: typeNumber }, - { left: typeString, right: typeString, return: typeString }, - { left: typeNumber, right: typeString, return: typeString }, - { left: typeString, right: typeNumber, return: typeString }, - ], inferenceRule: binaryInferenceRule }); - // TODO design decision: overload with the lowest number of conversions wins! - // TODO remove this later, it is not required for LOX! - // TODO is it possible to skip one of these options?? probably not ... - // TODO docu/guide: this vs operator combinations - // typir.conversion.markAsConvertible(typeNumber, typeString, 'IMPLICIT'); // var my1: string = 42; + onInitialize(): void { + // primitive types + // typeBool, typeNumber and typeVoid are specific types for OX, ... + const typeBool = this.primitiveKind.createPrimitiveType({ primitiveName: 'boolean', + inferenceRules: [ + isBooleanLiteral, + (node: unknown) => isTypeReference(node) && node.primitive === 'boolean' + ]}); + // ... but their primitive kind is provided/preset by Typir + const typeNumber = this.primitiveKind.createPrimitiveType({ primitiveName: 'number', + inferenceRules: [ + isNumberLiteral, + (node: unknown) => isTypeReference(node) && node.primitive === 'number' + ]}); + const typeString = this.primitiveKind.createPrimitiveType({ primitiveName: 'string', + inferenceRules: [ + isStringLiteral, + (node: unknown) => isTypeReference(node) && node.primitive === 'string' + ]}); + const typeVoid = this.primitiveKind.createPrimitiveType({ primitiveName: 'void', + inferenceRules: [ + (node: unknown) => isTypeReference(node) && node.primitive === 'void', + isPrintStatement, + (node: unknown) => isReturnStatement(node) && node.value === undefined + ] }); + const typeNil = this.primitiveKind.createPrimitiveType({ primitiveName: 'nil', + inferenceRules: isNilLiteral }); // From "Crafting Interpreters" no value, like null in other languages. Uninitialised variables default to nil. When the execution reaches the end of the block of a function body without hitting a return, nil is implicitly returned. + const typeAny = this.anyKind.createTopType({}); - // binary operators: numbers => boolean - for (const operator of ['<', '<=', '>', '>=']) { - operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); - } + // 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 = { + filter: isBinaryExpression, + matching: (node: BinaryExpression, name: string) => node.operator === name, + operands: (node: BinaryExpression, _name: string) => [node.left, node.right], + }; + const unaryInferenceRule: InferOperatorWithSingleOperand = { + filter: isUnaryExpression, + matching: (node: UnaryExpression, name: string) => node.operator === name, + operand: (node: UnaryExpression, _name: string) => node.value, + }; - // binary operators: booleans => boolean - for (const operator of ['and', 'or']) { - operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); - } + // binary operators: numbers => number + for (const operator of ['-', '*', '/']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); + } + this.operators.createBinaryOperator({ name: '+', signature: [ + { left: typeNumber, right: typeNumber, return: typeNumber }, + { left: typeString, right: typeString, return: typeString }, + { left: typeNumber, right: typeString, return: typeString }, + { left: typeString, right: typeNumber, return: typeString }, + ], inferenceRule: binaryInferenceRule }); + + // binary operators: numbers => boolean + for (const operator of ['<', '<=', '>', '>=']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); + } + + // binary operators: booleans => boolean + for (const operator of ['and', 'or']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); + } + + // ==, != for all data types (the warning for different types is realized below) + for (const operator of ['==', '!=']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeAny, right: typeAny, return: typeBool }, inferenceRule: binaryInferenceRule }); + } + // = for SuperType = SubType (TODO integrate the validation here? should be replaced!) + this.operators.createBinaryOperator({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule }); + + // unary operators + this.operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); + this.operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + + + // additional inference rules for ... + 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 (isMethodMember(ref)) { + return InferenceRuleNotApplicable!; // TODO + } else if (isVariableDeclaration(ref)) { + // use variables inside expressions! + return ref.type!; + } else if (isParameter(ref)) { + // use parameters inside expressions + return ref.type; + } else if (isFunctionDeclaration(ref)) { + // there is already an inference rule for function calls + return InferenceRuleNotApplicable; + } else if (ref === undefined) { + return InferenceRuleNotApplicable; + } else { + assertUnreachable(ref); + } + } + // ... variable declarations + if (isVariableDeclaration(domainElement)) { + if (domainElement.type) { + // the user declared this variable with a type + return domainElement.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; + } else { + return InferenceRuleNotApplicable; // this case is impossible, there is a validation in the Langium LOX validator for this case + } + } + return InferenceRuleNotApplicable; + }); + + // some explicit validations for typing issues with Typir (replaces corresponding functions in the OxValidator!) + this.typir.validation.collector.addValidationRules( + (node: unknown, typir: TypirServices) => { + if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { + 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, + () => { message: "Variable can't be declared with a type 'void'.", domainProperty: 'type' }), + ...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}' 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 functionDeclaration = AstUtils.getContainerOfType(node, isFunctionDeclaration); + if (functionDeclaration && functionDeclaration.returnType.primitive && 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, (actual, expected) => { + message: `The expression '${node.value!.$cstNode?.text}' of type '${actual.name}' is not usable as return value for the function '${functionDeclaration.name}' with return type '${expected.name}'.`, + domainProperty: 'value' }); + } + } + return []; + } + ); - // ==, != for all data types (the warning for different types is realized below) - for (const operator of ['==', '!=']) { - operators.createBinaryOperator({ name: operator, signature: { left: typeAny, right: typeAny, return: typeBool }, inferenceRule: binaryInferenceRule }); + // validate unique declarations + this.typir.validation.collector.addValidationRulesWithBeforeAndAfter( + new UniqueFunctionValidation(this.typir, isFunctionDeclaration), + new UniqueClassValidation(this.typir, isClass), + ); } - // = for SuperType = SubType (TODO integrate the validation here? should be replaced!) - operators.createBinaryOperator({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule }); - // unary operators - operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); - operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + onNewAstNode(node: AstNode): void { + // define types which are declared by the users of LOX => investigate the current AST - // define types which are declared by the users of LOX => investigate the current AST - const domainNodeRoot = AstUtils.getContainerOfType(domainNodeEntry, isLoxProgram)!; - AstUtils.streamAllContents(domainNodeRoot).forEach((node: AstNode) => { // function types: they have to be updated after each change of the Langium document, since they are derived from FunctionDeclarations! if (isFunctionDeclaration(node)) { const functionName = node.name; // define function type - functionKind.createFunctionType({ + this.functionKind.getOrCreateFunctionType({ functionName, outputParameter: { name: FUNCTION_MISSING_NAME, type: node.returnType }, inputParameters: node.parameters.map(p => ({ name: p.name, type: p.type })), @@ -135,7 +232,7 @@ export function createTypir(domainNodeEntry: AstNode): TypirServices { // class types (nominal typing): if (isClass(node)) { const className = node.name; - classKind.createClassType({ + this.classKind.getOrCreateClassType({ // TODO check for duplicates! className, superClasses: node.superClass?.ref, // note that type inference is used here; TODO delayed fields: node.members @@ -162,104 +259,13 @@ export function createTypir(domainNodeEntry: AstNode): TypirServices { ? domainElement.element!.ref.name : 'N/A', // as an alternative, use 'InferenceRuleNotApplicable' instead, what should we recommend? }); } - }); - - // additional inference rules for ... - 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 undefined!; //TODO - } else if (isMethodMember(ref)) { - return undefined!; //TODO - } else if (isVariableDeclaration(ref)) { - // use variables inside expressions! - return ref.type!; - } else if (isParameter(ref)) { - // use parameters inside expressions - return ref.type; - } else if (isFunctionDeclaration(ref)) { - // there is already an inference rule for function calls (see above for FunctionDeclaration)! - return InferenceRuleNotApplicable; - } else if (ref === undefined) { - return InferenceRuleNotApplicable; - } else { - assertUnreachable(ref); - } - } - // ... variable declarations - if (isVariableDeclaration(domainElement)) { - if (domainElement.type) { - return domainElement.type; - } else if (domainElement.value) { - // the type might be null; no type declared => do type inference of the assigned value instead! - return domainElement.value; - } else { - return InferenceRuleNotApplicable; // this case is impossible, there is a validation in the "usual LOX validator" for this case - } - } - return InferenceRuleNotApplicable; - }); - - // some explicit validations for typing issues with Typir (replaces corresponding functions in the OxValidator!) - typir.validation.collector.addValidationRules( - (node: unknown, typir: TypirServices) => { - if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { - 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, - () => { message: "Variable can't be declared with a type 'void'.", domainProperty: 'type' }), - ...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}' with type '${expected.name}'`, - domainProperty: 'value' }); - } - // TODO Idee: Validierung für Langium-binding an AstTypen hängen wie es standardmäßig in Langium gemacht wird => ist auch performanter => dafür API hier anpassen/umbauen - 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 functionDeclaration = AstUtils.getContainerOfType(node, isFunctionDeclaration); - if (functionDeclaration && functionDeclaration.returnType.primitive && 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, (actual, expected) => { - message: `The expression '${node.value!.$cstNode?.text}' of type '${actual.name}' is not usable as return value for the function '${functionDeclaration.name}' with return type '${expected.name}'.`, - domainProperty: 'value' }); - } - } - return []; - } - ); - - return typir; -} - -// override some default behaviour ... -// ... print the text of the corresponding CstNode -class OxPrinter extends DefaultTypeConflictPrinter { - override printDomainElement(domainElement: unknown, sentenceBegin?: boolean | undefined): string { - if (isAstNode(domainElement)) { - return `${sentenceBegin ? 'T' : 't'}he AstNode '${domainElement.$cstNode?.text}'`; - } - return super.printDomainElement(domainElement, sentenceBegin); } } -export const LoxTypirModule: Module = { - printer: () => new OxPrinter(), -}; + +export function createLoxTypirModule(langiumServices: LangiumSharedServices): Module { + return { + // specific configurations for LOX + TypeCreator: (typirServices) => new LoxTypeCreator(typirServices, langiumServices), + }; +} diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index 602b1ce..f211938 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -6,13 +6,16 @@ import { EmptyFileSystem } from 'langium'; import { parseDocument } from 'langium/test'; -import { describe, expect, test } from 'vitest'; +import { afterEach, describe, expect, test } from 'vitest'; import type { Diagnostic } from 'vscode-languageserver-types'; import { DiagnosticSeverity } from 'vscode-languageserver-types'; import { createLoxServices } from '../src/language/lox-module.js'; +import { deleteAllDocuments } from 'typir-langium'; const loxServices = createLoxServices(EmptyFileSystem).Lox; +afterEach(async () => await deleteAllDocuments(loxServices)); + describe('Explicitly test type checking for LOX', () => { test('multiple nested and', async () => { @@ -41,6 +44,15 @@ describe('Explicitly test type checking for LOX', () => { await validate('var myResult: boolean; myResult = 2 < 3;', 0); }); + test('overloaded operator "+"', async () => { + await validate('var myResult: number = 1 + 2;', 0); + await validate('var myResult: string = "a" + "b";', 0); + await validate('var myResult: string = "a" + 2;', 0); + await validate('var myResult: string = 1 + "b";', 0); + await validate('var myResult: string = true + "b";', 1); + await validate('var myResult: string = "a" + false;', 1); + }); + test('boolean in conditions', async () => { await validate('if ( true ) {}', 0); await validate('if ( 3 ) {}', 1); @@ -52,24 +64,24 @@ describe('Explicitly test type checking for LOX', () => { await validate('var myVar : void;', 1); }); - test('function: return value and return type', async () => { - await validate('fun myFunction() : boolean { return true; }', 0); - await validate('fun myFunction() : boolean { return 2; }', 1); - await validate('fun myFunction() : number { return 2; }', 0); - await validate('fun myFunction() : number { return true; }', 1); + test('function: return value and return type must match', async () => { + await validate('fun myFunction1() : boolean { return true; }', 0); + await validate('fun myFunction2() : boolean { return 2; }', 1); + await validate('fun myFunction3() : number { return 2; }', 0); + await validate('fun myFunction4() : number { return true; }', 1); }); test('overloaded function: different return types are not enough', async () => { await validate(` fun myFunction() : boolean { return true; } fun myFunction() : number { return 2; } - `, 1); + `, 2); }); test('overloaded function: different parameter names are not enough', async () => { await validate(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(other: boolean) : boolean { return true; } - `, 1); + `, 2); }); test('overloaded function: but different parameter types are fine', async () => { await validate(` @@ -96,20 +108,26 @@ describe('Explicitly test type checking for LOX', () => { await validate('var myVar : number = 2 + (2 * false);', 1); }); - test('Class literals', async () => { - await validate(` - class MyClass { name: string age: number } - var v1 = MyClass(); // constructor call - `, 0); - await validate(` - class MyClass { name: string age: number } - var v1: MyClass = MyClass(); // constructor call - `, 0); - await validate(` - class MyClass1 {} - class MyClass2 {} - var v1: boolean = MyClass1() == MyClass2(); // comparing objects with each other - `, 0, 1); + describe('Class literals', () => { + test('Class literals 1', async () => { + await validate(` + class MyClass { name: string age: number } + var v1 = MyClass(); // constructor call + `, 0); + }); + test('Class literals 2', async () => { + await validate(` + class MyClass { name: string age: number } + var v1: MyClass = MyClass(); // constructor call + `, 0); + }); + test('Class literals 3', async () => { + await validate(` + class MyClass1 {} + class MyClass2 {} + var v1: boolean = MyClass1() == MyClass2(); // comparing objects with each other + `, 0, 1); + }); }); test('Class inheritance for assignments', async () => { @@ -125,13 +143,13 @@ describe('Explicitly test type checking for LOX', () => { `, 1); }); - test.fails('Class inheritance and the order of type definitions', async () => { + test('Class inheritance and the order of type definitions', async () => { // the "normal" case: 1st super class, 2nd sub class await validate(` class MyClass1 {} class MyClass2 < MyClass1 {} `, 0); - // switching the order of super and sub class works in Langium, but not in Typir at the moment + // switching the order of super and sub class works in Langium, but not in Typir at the moment, TODO warum nicht mehr?? await validate(` class MyClass2 < MyClass1 {} class MyClass1 {} @@ -153,6 +171,18 @@ describe('Explicitly test type checking for LOX', () => { `, 2); }); + test('Classes must be unique by name', async () => { + await validate(` + class MyClass1 { } + class MyClass1 { } + `, 2); + await validate(` + class MyClass2 { } + class MyClass2 { } + class MyClass2 { } + `, 3); + }); + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { diff --git a/examples/lox/tsconfig.src.json b/examples/lox/tsconfig.src.json index 4027697..b255959 100644 --- a/examples/lox/tsconfig.src.json +++ b/examples/lox/tsconfig.src.json @@ -6,7 +6,7 @@ "lib": ["ESNext", "WebWorker"] }, "references": [{ - "path": "../../packages/typir/tsconfig.src.json" + "path": "../../packages/typir-langium/tsconfig.src.json" }], "include": [ "src/**/*" diff --git a/examples/ox/package.json b/examples/ox/package.json index 1217f50..9470ba6 100644 --- a/examples/ox/package.json +++ b/examples/ox/package.json @@ -29,7 +29,7 @@ "dependencies": { "commander": "~12.1.0", "langium": "~3.2.0", - "typir": "~0.0.1", + "typir-langium": "~0.0.1", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" }, diff --git a/examples/ox/src/language/ox-module.ts b/examples/ox/src/language/ox-module.ts index 09c8fbb..d33dc7c 100644 --- a/examples/ox/src/language/ox-module.ts +++ b/examples/ox/src/language/ox-module.ts @@ -5,8 +5,10 @@ ******************************************************************************/ import { Module, inject } from 'langium'; -import { LangiumServices, PartialLangiumServices, DefaultSharedModuleContext, LangiumSharedServices, createDefaultSharedModule, createDefaultModule } from 'langium/lsp'; +import { DefaultSharedModuleContext, LangiumServices, LangiumSharedServices, PartialLangiumServices, createDefaultModule, createDefaultSharedModule } from 'langium/lsp'; +import { LangiumServicesForTypirBinding, createLangiumModuleForTypirBinding, initializeLangiumTypirServices } from 'typir-langium'; import { OxGeneratedModule, OxGeneratedSharedModule } from './generated/module.js'; +import { createOxTypirModule } from './ox-type-checking.js'; import { OxValidator, registerValidationChecks } from './ox-validator.js'; /** @@ -22,7 +24,7 @@ export type OxAddedServices = { * Union of Langium default services and your custom services - use this as constructor parameter * of custom service classes. */ -export type OxServices = LangiumServices & OxAddedServices +export type OxServices = LangiumServices & OxAddedServices & LangiumServicesForTypirBinding /** * Dependency injection module that overrides Langium default services and contributes the @@ -61,9 +63,12 @@ export function createOxServices(context: DefaultSharedModuleContext): { const Ox = inject( createDefaultModule({ shared }), OxGeneratedModule, - OxModule + createLangiumModuleForTypirBinding(shared), + OxModule, + createOxTypirModule(shared), ); shared.ServiceRegistry.register(Ox); registerValidationChecks(Ox); + initializeLangiumTypirServices(Ox); return { shared, Ox }; } diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 8b39b56..b78f5d8 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -4,195 +4,195 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode, AstUtils, Module, assertUnreachable, isAstNode } from 'langium'; -import { DefaultTypeConflictPrinter, FUNCTION_MISSING_NAME, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, ParameterDetails, PartialTypirServices, PrimitiveKind, TypirServices, createTypirServices } from 'typir'; +import { AstNode, AstUtils, Module, assertUnreachable } from 'langium'; +import { LangiumSharedServices } from 'langium/lsp'; +import { FUNCTION_MISSING_NAME, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, OperatorManager, ParameterDetails, PrimitiveKind, TypirServices, UniqueFunctionValidation } from 'typir'; +import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium'; import { ValidationMessageDetails } from '../../../../packages/typir/lib/features/validation.js'; -import { BinaryExpression, MemberCall, UnaryExpression, isAssignmentStatement, isBinaryExpression, isBooleanLiteral, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isNumberLiteral, isOxProgram, isParameter, isReturnStatement, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from './generated/ast.js'; +import { BinaryExpression, MemberCall, UnaryExpression, isAssignmentStatement, isBinaryExpression, isBooleanLiteral, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isNumberLiteral, isParameter, isReturnStatement, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from './generated/ast.js'; -export function createTypir(domainNodeEntry: AstNode): TypirServices { - // set up Typir and reuse some predefined things - const typir = createTypirServices(OxTypirModule); - const primitiveKind = new PrimitiveKind(typir); - const functionKind = new FunctionKind(typir); - const operators = typir.operators; +export class OxTypeCreator extends AbstractLangiumTypeCreator { + protected readonly typir: TypirServices; + protected readonly primitiveKind: PrimitiveKind; + protected readonly functionKind: FunctionKind; + protected readonly operators: OperatorManager; - // define primitive types - // typeBool, typeNumber and typeVoid are specific types for OX, ... - const typeBool = primitiveKind.createPrimitiveType({ primitiveName: 'boolean', inferenceRules: [ - isBooleanLiteral, - (node: unknown) => isTypeReference(node) && node.primitive === 'boolean', - ]}); - // ... but their primitive kind is provided/preset by Typir - const typeNumber = primitiveKind.createPrimitiveType({ primitiveName: 'number', inferenceRules: [ - isNumberLiteral, - (node: unknown) => isTypeReference(node) && node.primitive === 'number', - ]}); - const typeVoid = primitiveKind.createPrimitiveType({ primitiveName: 'void', inferenceRules: - (node: unknown) => isTypeReference(node) && node.primitive === 'void' - }); + constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { + super(typirServices, langiumServices); + this.typir = typirServices; - // 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 = { - filter: isBinaryExpression, - matching: (node: BinaryExpression, name: string) => node.operator === name, - operands: (node: BinaryExpression, _name: string) => [node.left, node.right], - }; - const unaryInferenceRule: InferOperatorWithSingleOperand = { - filter: isUnaryExpression, - matching: (node: UnaryExpression, name: string) => node.operator === name, - operand: (node: UnaryExpression, _name: string) => node.value, - }; - - // define operators - // binary operators: numbers => number - for (const operator of ['+', '-', '*', '/']) { - operators.createBinaryOperator({ 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 ['<', '<=', '>', '>=']) { - operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); - } - // binary operators: booleans => boolean - for (const operator of ['and', 'or']) { - operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); - } - // ==, != for booleans and numbers - for (const operator of ['==', '!=']) { - operators.createBinaryOperator({ name: operator, signature: [ - { left: typeNumber, right: typeNumber, return: typeBool }, - { left: typeBool, right: typeBool, return: typeBool }, - ], inferenceRule: binaryInferenceRule }); + this.primitiveKind = new PrimitiveKind(this.typir); + this.functionKind = new FunctionKind(this.typir); + this.operators = this.typir.operators; } - // unary operators - operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); - operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule }); + onInitialize(): void { + // define primitive types + // typeBool, typeNumber and typeVoid are specific types for OX, ... + const typeBool = this.primitiveKind.createPrimitiveType({ primitiveName: 'boolean', inferenceRules: [ + isBooleanLiteral, + (node: unknown) => isTypeReference(node) && node.primitive === 'boolean', + ]}); + // ... but their primitive kind is provided/preset by Typir + const typeNumber = this.primitiveKind.createPrimitiveType({ primitiveName: 'number', inferenceRules: [ + isNumberLiteral, + (node: unknown) => isTypeReference(node) && node.primitive === 'number', + ]}); + const typeVoid = this.primitiveKind.createPrimitiveType({ primitiveName: 'void', inferenceRules: + (node: unknown) => isTypeReference(node) && node.primitive === 'void' + }); + + // 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 = { + filter: isBinaryExpression, + matching: (node: BinaryExpression, name: string) => node.operator === name, + operands: (node: BinaryExpression, _name: string) => [node.left, node.right], + }; + const unaryInferenceRule: InferOperatorWithSingleOperand = { + filter: isUnaryExpression, + matching: (node: UnaryExpression, name: string) => node.operator === name, + operand: (node: UnaryExpression, _name: string) => node.value, + }; + + // define operators + // binary operators: numbers => number + for (const operator of ['+', '-', '*', '/']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule }); + } + // TODO better name: overloads, overloadRules, selectors, signatures + // TODO better name for "inferenceRule": astSelectors + // binary operators: numbers => boolean + for (const operator of ['<', '<=', '>', '>=']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule }); + } + // binary operators: booleans => boolean + for (const operator of ['and', 'or']) { + this.operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule }); + } + // ==, != for booleans and numbers + for (const operator of ['==', '!=']) { + this.operators.createBinaryOperator({ name: operator, signature: [ + { left: typeNumber, right: typeNumber, return: typeBool }, + { left: typeBool, right: typeBool, return: typeBool }, + ], inferenceRule: binaryInferenceRule }); + } - // define function types - // they have to be updated after each change of the Langium document, since they are derived from the user-defined FunctionDeclarations! - const domainNodeRoot = AstUtils.getContainerOfType(domainNodeEntry, isOxProgram)!; - AstUtils.streamAllContents(domainNodeRoot).forEach((node: AstNode) => { - if (isFunctionDeclaration(node)) { - const functionName = node.name; + // unary operators + this.operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule }); + this.operators.createUnaryOperator({ 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. + * (Beyond some obvious things, e.g. created Type instances can be used only afterwards and not before their creation.) + * - But at the moment, this objective is not reached in general! + * - As an example, since the function definition above uses type inference for their parameter types, it is necessary, + * that the primitive types and their corresponding inference rules are defined earlier! + * - In the future, the user of Typir will not need to do a topological sorting of type definitions anymore, + * since the type definition process will be split and parts will be delayed. + * - The following inference rules are OK, since they are not relevant for defining function types + */ + + // additional inference rules ... + this.typir.inference.addInferenceRule((domainElement: unknown) => { + // ... for member calls (which are used in expressions) + if (isMemberCall(domainElement)) { + const ref = domainElement.element.ref; + if (isVariableDeclaration(ref)) { + // use variables inside expressions! + return ref.type; + } else if (isParameter(ref)) { + // use parameters inside expressions + return ref.type; + } else if (isFunctionDeclaration(ref)) { + // there is already an inference rule for function calls (see above for FunctionDeclaration)! + return 'N/A'; // as an alternative: use 'InferenceRuleNotApplicable' instead, what should we recommend? + } else if (ref === undefined) { + return InferenceRuleNotApplicable; + } else { + assertUnreachable(ref); + } + } + return InferenceRuleNotApplicable; + }); + // it is up to the user of Typir, how to structure the inference rules! + this.typir.inference.addInferenceRule((domainElement, _typir) => { + // ... and for variable declarations + if (isVariableDeclaration(domainElement)) { + return domainElement.type; + } + return InferenceRuleNotApplicable; + }); + + // explicit validations for typing issues, realized with Typir (which replaced corresponding functions in the OxValidator!) + this.typir.validation.collector.addValidationRules( + (node: unknown, typir: TypirServices) => { + if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { + 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, + () => { message: "Variables can't be declared with the type 'void'.", domainProperty: 'type' }), + ...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, + (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', + }); + } + if (isReturnStatement(node)) { + 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, + () => { message: `The expression '${node.value!.$cstNode?.text}' is not usable as return value for the function '${functionDeclaration.name}'.`, domainProperty: 'value' }); + } + } + return []; + } + ); + + // check for unique function declarations + this.typir.validation.collector.addValidationRulesWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); + } + + onNewAstNode(domainElement: AstNode): void { + // define function types + // they have to be updated after each change of the Langium document, since they are derived from the user-defined FunctionDeclarations! + if (isFunctionDeclaration(domainElement)) { + const functionName = domainElement.name; // define function type - functionKind.createFunctionType({ + this.functionKind.getOrCreateFunctionType({ functionName, // note that the following two lines internally use type inference here in order to map language types to Typir types - outputParameter: { name: FUNCTION_MISSING_NAME, type: node.returnType }, - inputParameters: node.parameters.map(p => ({ name: p.name, type: p.type })), + outputParameter: { name: FUNCTION_MISSING_NAME, type: domainElement.returnType }, + inputParameters: domainElement.parameters.map(p => ({ name: p.name, type: p.type })), // inference rule for function declaration: - inferenceRuleForDeclaration: (domainElement: unknown) => domainElement === node, // only the current function declaration matches! + inferenceRuleForDeclaration: (node: unknown) => node === domainElement, // only the current function declaration matches! /** inference rule for funtion calls: * - inferring of overloaded functions works only, if the actual arguments have the expected types! * - (inferring calls to non-overloaded functions works independently from the types of the given parameters) * - additionally, validations for the assigned values to the expected parameter( type)s are derived */ inferenceRuleForCalls: { filter: isMemberCall, - matching: (domainElement: MemberCall) => isFunctionDeclaration(domainElement.element.ref) && domainElement.element.ref.name === functionName, - inputArguments: (domainElement: MemberCall) => domainElement.arguments + 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 ... } }); } - }); - - /** Hints regarding the order of Typir configurations for OX: - * - In general, Typir aims to not depend on the order of configurations. - * (Beyond some obvious things, e.g. created Type instances can be used only afterwards and not before their creation.) - * - But at the moment, this objective is not reached in general! - * - As an example, since the function definition above uses type inference for their parameter types, it is necessary, - * that the primitive types and their corresponding inference rules are defined earlier! - * - In the future, the user of Typir will not need to do a topological sorting of type definitions anymore, - * since the type definition process will be split and parts will be delayed. - * - The following inference rules are OK, since they are not relevant for defining function types - */ - - // additional inference rules ... - typir.inference.addInferenceRule((domainElement: unknown) => { - // ... for member calls (which are used in expressions) - if (isMemberCall(domainElement)) { - const ref = domainElement.element.ref; - if (isVariableDeclaration(ref)) { - // use variables inside expressions! - return ref.type; - } else if (isParameter(ref)) { - // use parameters inside expressions - return ref.type; - } else if (isFunctionDeclaration(ref)) { - // there is already an inference rule for function calls (see above for FunctionDeclaration)! - return 'N/A'; // as an alternative: use 'InferenceRuleNotApplicable' instead, what should we recommend? - } else if (ref === undefined) { - return InferenceRuleNotApplicable; - } else { - assertUnreachable(ref); - } - } - return InferenceRuleNotApplicable; - }); - // it is up to the user of Typir, how to structure the inference rules! - typir.inference.addInferenceRule((domainElement, _typir) => { - // ... and for variable declarations - if (isVariableDeclaration(domainElement)) { - return domainElement.type; - } - return InferenceRuleNotApplicable; - }); - // TODO: [{ selector: isVariableDeclaration, result: domainElement => domainElement.type }, {}] Array> - // discriminator rule: $type '$VariableDeclaration' + record / "Sprungtabelle" for the Langium-binding (or both in core)? for improved performance (?) - // alternativ discriminator rule: unknown => string; AstNode => node.$type; Vorsicht mit Sub-Typen (Vollständigkeit+Updates, no abstract types)! - // später realisieren - - // explicit validations for typing issues, realized with Typir (which replaced corresponding functions in the OxValidator!) - // TODO selector API + gleiche Diskussion für Inference Rules - typir.validation.collector.addValidationRules( - (node: unknown, typir: TypirServices) => { - if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { - 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, - () => { message: "Variables can't be declared with the type 'void'.", domainProperty: 'type' }), - ...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, - (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', - }); - } - if (isReturnStatement(node)) { - 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, - () => { message: `The expression '${node.value!.$cstNode?.text}' is not usable as return value for the function '${functionDeclaration.name}'.`, domainProperty: 'value' }); - } - } - return []; - } - ); - - return typir; -} - -// override some default behaviour ... -// ... print the text of the corresponding CstNode -class OxPrinter extends DefaultTypeConflictPrinter { - override printDomainElement(domainElement: unknown, sentenceBegin?: boolean | undefined): string { - if (isAstNode(domainElement)) { - return `${sentenceBegin ? 'T' : 't'}he AstNode '${domainElement.$cstNode?.text}'`; - } - return super.printDomainElement(domainElement, sentenceBegin); } } -export const OxTypirModule: Module = { - printer: () => new OxPrinter(), -}; + +export function createOxTypirModule(langiumServices: LangiumSharedServices): Module { + return { + // specific configurations for OX + TypeCreator: (typirServices) => new OxTypeCreator(typirServices, langiumServices), + }; +} diff --git a/examples/ox/src/language/ox-validator.ts b/examples/ox/src/language/ox-validator.ts index 3a0f0ea..6a62c24 100644 --- a/examples/ox/src/language/ox-validator.ts +++ b/examples/ox/src/language/ox-validator.ts @@ -5,9 +5,8 @@ ******************************************************************************/ import { AstUtils, type ValidationAcceptor, type ValidationChecks } from 'langium'; -import { OxProgram, isFunctionDeclaration, type OxAstType, type ReturnStatement } from './generated/ast.js'; +import { isFunctionDeclaration, type OxAstType, type ReturnStatement } from './generated/ast.js'; import type { OxServices } from './ox-module.js'; -import { createTypir } from './ox-type-checking.js'; /** * Register custom validation checks. @@ -17,46 +16,17 @@ export function registerValidationChecks(services: OxServices) { const validator = services.validation.OxValidator; const checks: ValidationChecks = { ReturnStatement: validator.checkReturnTypeIsCorrect, - OxProgram: validator.checkTypingProblemsWithTypir }; registry.register(checks, validator); } /** - * Implementation of custom validations. + * Implementation of custom validations on the syntactic level (which can be checked without using Typir). + * Validations on type level are done by Typir. */ export class OxValidator { - checkTypingProblemsWithTypir(node: OxProgram, accept: ValidationAcceptor) { - // executes all checks, which are directly derived from the current Typir configuration, - // i.e. arguments fit to parameters for function calls (including operands for operators) - const typir = createTypir(node); - AstUtils.streamAllContents(node).forEach(node => { - // print all found problems for each AST node - const typeProblems = typir.validation.collector.validate(node); - for (const problem of typeProblems) { - const message = typir.printer.printValidationProblem(problem); - accept(problem.severity, message, { node, property: problem.domainProperty, index: problem.domainIndex }); - } - }); - } - - /* - * TODO validation with Typir for Langium - * - create additional package "typir-langium" - * - Is it possible to infer a type at all? Type vs undefined - * - Does the inferred type fit to the environment? => "type checking" (expected: unknown|Type, actual: unknown|Type) - * - make it easy to integrate it into the Langium validator - * - provide service to cache Typir in the background; but ensure, that internal caches of Typir need to be cleared, if a document was changed - * - possible Quick-fixes ... - * - for wrong type of variable declaration - * - to add missing explicit type conversion - * - const ref: (kind: unknown) => kind is FunctionKind = isFunctionKind; // use this signature for Langium? - * - no validation of parents, when their children already have some problems/warnings - */ - checkReturnTypeIsCorrect(node: ReturnStatement, accept: ValidationAcceptor) { - // these checks are done here, since these issues already influence the syntactic level (which can be checked without using Typir) const functionDeclaration = AstUtils.getContainerOfType(node, isFunctionDeclaration); if (functionDeclaration) { if (functionDeclaration.returnType.primitive === 'void') { diff --git a/examples/ox/test/ox-type-checking.test.ts b/examples/ox/test/ox-type-checking.test.ts index ca967c6..d22e5c2 100644 --- a/examples/ox/test/ox-type-checking.test.ts +++ b/examples/ox/test/ox-type-checking.test.ts @@ -6,12 +6,15 @@ import { EmptyFileSystem } from 'langium'; import { parseDocument } from 'langium/test'; -import { describe, expect, test } from 'vitest'; +import { afterEach, describe, expect, test } from 'vitest'; import type { Diagnostic } from 'vscode-languageserver-types'; import { createOxServices } from '../src/language/ox-module.js'; +import { deleteAllDocuments } from 'typir-langium'; const oxServices = createOxServices(EmptyFileSystem).Ox; +afterEach(async () => await deleteAllDocuments(oxServices)); + describe('Explicitly test type checking for OX', () => { test('multiple nested and', async () => { @@ -88,10 +91,22 @@ describe('Explicitly test type checking for OX', () => { }); test('function: return value and return type', async () => { + await validate('fun myFunction1() : boolean { return true; }', 0); + await validate('fun myFunction2() : boolean { return 2; }', 1); + await validate('fun myFunction3() : number { return 2; }', 0); + await validate('fun myFunction4() : number { return true; }', 1); + }); + + test('function: the same function name twice (in the same file) is not allowed in Typir', async () => { + await validate(` + fun myFunction() : boolean { return true; } + fun myFunction() : boolean { return false; } + `, 2); // both functions should be marked as "duplicate" + }); + + test.fails('function: the same function name twice (even in different files) is not allowed in Typir', async () => { await validate('fun myFunction() : boolean { return true; }', 0); - await validate('fun myFunction() : boolean { return 2; }', 1); - await validate('fun myFunction() : number { return 2; }', 0); - await validate('fun myFunction() : number { return true; }', 1); + await validate('fun myFunction() : boolean { return false; }', 2); // now, both functions should be marked as "duplicate" }); test('use overloaded operators', async () => { diff --git a/examples/ox/tsconfig.src.json b/examples/ox/tsconfig.src.json index 4027697..b255959 100644 --- a/examples/ox/tsconfig.src.json +++ b/examples/ox/tsconfig.src.json @@ -6,7 +6,7 @@ "lib": ["ESNext", "WebWorker"] }, "references": [{ - "path": "../../packages/typir/tsconfig.src.json" + "path": "../../packages/typir-langium/tsconfig.src.json" }], "include": [ "src/**/*" diff --git a/package-lock.json b/package-lock.json index 644a274..8e66b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "workspaces": [ "packages/typir", + "packages/typir-langium", "examples/ox", "examples/lox" ], @@ -38,7 +39,7 @@ "dependencies": { "commander": "~12.1.0", "langium": "~3.2.0", - "typir": "~0.0.1", + "typir-langium": "~0.0.1", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" }, @@ -67,7 +68,7 @@ "dependencies": { "commander": "~12.1.0", "langium": "~3.2.0", - "typir": "~0.0.1", + "typir-langium": "~0.0.1", "vscode-languageclient": "~9.0.1", "vscode-languageserver": "~9.0.1" }, @@ -3286,6 +3287,10 @@ "resolved": "packages/typir", "link": true }, + "node_modules/typir-langium": { + "resolved": "packages/typir-langium", + "link": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4052,6 +4057,17 @@ "engines": { "node": ">= 18.0.0" } + }, + "packages/typir-langium": { + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "langium": "~3.2.0", + "typir": "~0.0.1" + }, + "engines": { + "node": ">= 18.0.0" + } } } } diff --git a/package.json b/package.json index 60e8a06..62eee7c 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,8 @@ "scripts": { "postinstall": "npm run langium:generate", "clean": "shx rm -rf packages/**/lib packages/**/out packages/**/*.tsbuildinfo examples/**/lib examples/**/out examples/**/*.tsbuildinfo", - "build": "tsc -b tsconfig.build.json && npm run build --workspace=typir --workspace=examples/ox --workspace=examples/lox", - "watch": "concurrently -n tsc,vscode,ox -c blue,yellow,green \"tsc -b tsconfig.build.json -w\" \"npm run watch --workspace=typir\" \"npm run watch --workspace=examples/ox\" \"npm run watch --workspace=examples/lox\"", - "build:clean": "npm run clean && npm run build", + "build": "tsc -b tsconfig.build.json && npm run build --workspaces", + "watch": "concurrently -n typir,typir-langium,ox,lox -c blue,blue,green,green \"tsc -b tsconfig.build.json -w\" \"npm run watch --workspace=typir\" \"npm run watch --workspace=typir-langium\" \"npm run watch --workspace=examples/ox\" \"npm run watch --workspace=examples/lox\"", "lint": "npm run lint --workspaces", "test": "vitest", "test:run": "vitest --run", @@ -45,6 +44,7 @@ }, "workspaces": [ "packages/typir", + "packages/typir-langium", "examples/ox", "examples/lox" ] diff --git a/packages/typir-langium/package-lock.json b/packages/typir-langium/package-lock.json new file mode 100644 index 0000000..d348e89 --- /dev/null +++ b/packages/typir-langium/package-lock.json @@ -0,0 +1,140 @@ +{ + "name": "typir-langium", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typir-langium", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "langium": "^3.2.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==" + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/langium": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.2.0.tgz", + "integrity": "sha512-HxAPgCVC7X+dCN99QKlZMEoaLW4s/mt0IImYrP6ooEBOMh8lJYdFNNSpJ5NIOE+WFwQd3xa2phTJDmJhOWVR7A==", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + } + } +} diff --git a/packages/typir-langium/package.json b/packages/typir-langium/package.json new file mode 100644 index 0000000..0e0c5f8 --- /dev/null +++ b/packages/typir-langium/package.json @@ -0,0 +1,56 @@ +{ + "name": "typir-langium", + "version": "0.0.1", + "description": "Typir binding for Langium", + "homepage": "https://typir.org", + "author": { + "name": "TypeFox", + "url": "https://www.typefox.io" + }, + "license": "MIT", + "exports": { + ".": { + "import": "./lib/index.js", + "types": "./lib/index.d.ts" + } + }, + "type": "module", + "engines": { + "node": ">= 18.0.0" + }, + "volta": { + "node": "18.20.4", + "npm": "10.7.0" + }, + "keywords": [ + "typesystem", + "typescript", + "Langium", + "language", + "dsl" + ], + "files": [ + "lib", + "src", + "node.js", + "node.d.ts", + "test.js", + "test.d.ts" + ], + "scripts": { + "clean": "shx rm -rf lib out coverage", + "build": "tsc", + "watch": "tsc --watch", + "lint": "eslint src test --ext .ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/TypeFox/typir/", + "directory": "packages/typir-langium" + }, + "bugs": "https://github.com/TypeFox/typir/issues", + "dependencies": { + "langium": "~3.2.0", + "typir": "~0.0.1" + } +} diff --git a/packages/typir-langium/src/features/langium-caching.ts b/packages/typir-langium/src/features/langium-caching.ts new file mode 100644 index 0000000..f44215d --- /dev/null +++ b/packages/typir-langium/src/features/langium-caching.ts @@ -0,0 +1,92 @@ +/****************************************************************************** + * 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, ContextCache, DocumentState, LangiumSharedCoreServices, URI } from 'langium'; +import { LangiumSharedServices } from 'langium/lsp'; +import { CachePending, DomainElementInferenceCaching, Type } from 'typir'; +import { getDocumentKey } from '../utils/typir-langium-utils.js'; + +// cache AstNodes +export class LangiumDomainElementInferenceCaching implements DomainElementInferenceCaching { + protected readonly cache: DocumentCache; // removes cached AstNodes, if their underlying LangiumDocuments are invalidated + + constructor(langiumServices: LangiumSharedServices) { + this.cache = new DocumentCache(langiumServices, DocumentState.IndexedReferences); + } + + cacheSet(domainElement: AstNode, type: Type): void { + this.pendingClear(domainElement); + this.cache.set(getDocumentKey(domainElement), domainElement, type); + } + + cacheGet(domainElement: AstNode): Type | undefined { + if (this.pendingGet(domainElement)) { + return undefined; + } else { + return this.cache.get(getDocumentKey(domainElement), domainElement) as (Type | undefined); + } + } + + pendingSet(domainElement: AstNode): void { + this.cache.set(getDocumentKey(domainElement), domainElement, CachePending); + } + + pendingClear(domainElement: AstNode): void { + const key = getDocumentKey(domainElement); + if (this.cache.get(key, domainElement) !== CachePending) { + // do nothing + } else { + this.cache.delete(key, domainElement); + } + } + + pendingGet(domainElement: AstNode): boolean { + const key = getDocumentKey(domainElement); + 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-printing.ts b/packages/typir-langium/src/features/langium-printing.ts new file mode 100644 index 0000000..42153f6 --- /dev/null +++ b/packages/typir-langium/src/features/langium-printing.ts @@ -0,0 +1,20 @@ +/****************************************************************************** + * 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 { isAstNode } from 'langium'; +import { DefaultTypeConflictPrinter } from 'typir'; + +export class LangiumProblemPrinter extends DefaultTypeConflictPrinter { + + /** When printing a domain element, i.e. an AstNode, print the text of the corresponding CstNode. */ + override printDomainElement(domainElement: unknown, sentenceBegin?: boolean | undefined): string { + if (isAstNode(domainElement)) { + return `${sentenceBegin ? 'T' : 't'}he AstNode '${domainElement.$cstNode?.text}'`; + } + return super.printDomainElement(domainElement, sentenceBegin); + } + +} diff --git a/packages/typir-langium/src/features/langium-type-creator.ts b/packages/typir-langium/src/features/langium-type-creator.ts new file mode 100644 index 0000000..63587b5 --- /dev/null +++ b/packages/typir-langium/src/features/langium-type-creator.ts @@ -0,0 +1,132 @@ +/****************************************************************************** + * 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, AstUtils, DocumentState, interruptAndCheck, LangiumDocument } from 'langium'; +import { LangiumSharedServices } from 'langium/lsp'; +import { Type, TypeEdge, TypeGraph, TypeGraphListener, TypirServices } from 'typir'; +import { getDocumentKeyForDocument, getDocumentKeyForURI } from '../utils/typir-langium-utils.js'; + +export interface LangiumTypeCreator { // TODO Registry instead? + 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. + */ + 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; +} + +export abstract class AbstractLangiumTypeCreator implements LangiumTypeCreator, TypeGraphListener { + protected initialized: boolean = false; + protected currentDocumentKey: string = ''; + protected readonly documentTypesMap: Map = new Map(); + protected readonly typeGraph: TypeGraph; + + constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { + this.typeGraph = typirServices.graph; + + // for new and updated documents + langiumServices.workspace.DocumentBuilder.onBuildPhase(DocumentState.IndexedReferences, async (documents, cancelToken) => { + for (const document of documents) { + await interruptAndCheck(cancelToken); + + // notify Typir about each contained node of the processed document + this.handleProcessedDocument(document); + } + }); + // for deleted documents + langiumServices.workspace.DocumentBuilder.onUpdate((_changed, deleted) => { + deleted + .map(del => getDocumentKeyForURI(del)) + .forEach(del => this.invalidateTypesOfDocument(del)); + }); + + // get informed about added/removed types + this.typeGraph.addListener(this); + } + + abstract onInitialize(): void; + + abstract onNewAstNode(domainElement: AstNode): void; + + /** + * Starts the initialization. + * If this method is called multiple times, the initialization is done only once. + */ + triggerInitialization() { + if (!this.initialized) { + this.onInitialize(); + this.initialized = true; + } + } + + protected handleProcessedDocument(document: LangiumDocument): void { + this.triggerInitialization(); + this.currentDocumentKey = getDocumentKeyForDocument(document); // remember the key in order to map newly created types to the current document + + // For a NEW document, this is called, but nothing happens. + // For an UPDATED document, Langium deletes the whole previous AST and creates a complete new AST. + // Therefore all types which were created for such (now invalid) AstNodes and therefore associated with the current document need to be removed. + this.invalidateTypesOfDocument(this.currentDocumentKey); + + // create all types for this document + AstUtils.streamAst(document.parseResult.value) + .forEach((node: AstNode) => this.onNewAstNode(node)); + + this.currentDocumentKey = ''; // reset the key, newly created types will be associated with no document now + } + + protected invalidateTypesOfDocument(documentKey: string): void { + // grab all types which were created for the document + (this.documentTypesMap.get(documentKey) + // there are no types, if the document is new or if no types were created for the previous document version + ?? []) + // this is the central way to remove types from the type systems, there is no need to inform the kinds + .forEach(typeToRemove => this.typeGraph.removeNode(typeToRemove)); + // remove the deleted types from the map + this.documentTypesMap.delete(documentKey); + } + + addedType(newType: Type): void { + // the TypeGraph notifies about newly created Types + if (this.currentDocumentKey) { + // associate the new type with the current Langium document! + let types = this.documentTypesMap.get(this.currentDocumentKey); + if (!types) { + types = []; + this.documentTypesMap.set(this.currentDocumentKey, types); + } + types.push(newType); + } else { + // types which don't belong to a Langium document + } + } + + removedType(_type: Type): void { + // since this type creator actively removes types from the type graph itself, there is no need to react on removed types + } + addedEdge(_edge: TypeEdge): void { + // this type creator does not care about edges => do nothing + } + removedEdge(_edge: TypeEdge): void { + // this type creator does not care about edges => do nothing + } +} + +export class PlaceholderLangiumTypeCreator extends AbstractLangiumTypeCreator { + constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) { + super(typirServices, langiumServices); + } + override onInitialize(): void { + throw new Error('This method needs to be implemented! Extend the AbstractLangiumTypeCreator and register it in the Typir module: TypeCreator: (typirServices) => new MyLangiumTypeCreator(typirServices, langiumServices)'); + } + override onNewAstNode(_domainElement: AstNode): void { + throw new Error('This method needs to be implemented! Extend the AbstractLangiumTypeCreator and register it in the Typir module: TypeCreator: (typirServices) => new MyLangiumTypeCreator(typirServices, langiumServices)'); + } +} diff --git a/packages/typir-langium/src/features/langium-validation.ts b/packages/typir-langium/src/features/langium-validation.ts new file mode 100644 index 0000000..dc58b19 --- /dev/null +++ b/packages/typir-langium/src/features/langium-validation.ts @@ -0,0 +1,74 @@ +/****************************************************************************** + * 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, AstUtils, ValidationAcceptor, ValidationChecks } from 'langium'; +import { LangiumServices } from 'langium/lsp'; +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; + const checks: ValidationChecks = { + AstNode: validator.checkTypingProblemsWithTypir, // checking each node is not performant, improve the API, see below! + }; + registry.register(checks, validator); +} + +/* +* TODO validation with Typir for Langium +* +* What to validate: +* - Is it possible to infer a type at all? Type vs undefined +* - Does the inferred type fit to the environment? => "type checking" (expected: unknown|Type, actual: unknown|Type) +* - possible Quick-fixes ... +* - for wrong type of variable declaration +* - to add missing explicit type conversion +* - no validation of parents, when their children already have some problems/warnings +* +* Improved Validation API for Langium: +* - const ref: (kind: unknown) => kind is FunctionKind = isFunctionKind; // use this signature for Langium? +* - register validations for AST node $types (similar as Langium does it) => this is much more performant +* - [{ selector: isVariableDeclaration, result: domainElement => domainElement.type }, {}] Array> +* - discriminator rule: $type '$VariableDeclaration' + record / "Sprungtabelle" for the Langium-binding (or both in core)? for improved performance (?) +* - alternativ discriminator rule: unknown => string; AstNode => node.$type; Vorsicht mit Sub-Typen (Vollständigkeit+Updates, no abstract types)! +* Apply the same ideas for InferenceRules as well! +*/ + +export class LangiumTypirValidator { + protected readonly services: TypirServices; + + constructor(services: LangiumServicesForTypirBinding) { + this.services = services; + } + + /** + * Executes all checks, which are directly derived from the current Typir configuration, + * i.e. arguments fit to parameters for function calls (including operands for operators). + * @param node the current AST node to check regarding typing issues + * @param accept receives the found validation hints + */ + checkTypingProblemsWithTypir(node: AstNode, accept: ValidationAcceptor) { + // TODO use the new validation registry API in Langium v3.3 instead! + if (node.$container === undefined) { + this.report(this.services.validation.collector.validateBefore(node), node, accept); + + AstUtils.streamAst(node).forEach(child => { + this.report(this.services.validation.collector.validate(child), child, accept); + }); + + this.report(this.services.validation.collector.validateAfter(node), node, accept); + } + } + + protected report(problems: ValidationProblem[], node: AstNode, accept: ValidationAcceptor): void { + // print all found problems for the given AST node + for (const problem of problems) { + const message = this.services.printer.printValidationProblem(problem); + accept(problem.severity, message, { node, property: problem.domainProperty, index: problem.domainIndex }); + } + } +} diff --git a/packages/typir-langium/src/index.ts b/packages/typir-langium/src/index.ts new file mode 100644 index 0000000..bf49da1 --- /dev/null +++ b/packages/typir-langium/src/index.ts @@ -0,0 +1,12 @@ +/****************************************************************************** + * 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. + ******************************************************************************/ + +export * from './typir-langium.js'; +export * from './features/langium-caching.js'; +export * from './features/langium-printing.js'; +export * from './features/langium-type-creator.js'; +export * from './features/langium-validation.js'; +export * from './utils/typir-langium-utils.js'; diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts new file mode 100644 index 0000000..e4abbed --- /dev/null +++ b/packages/typir-langium/src/typir-langium.ts @@ -0,0 +1,59 @@ +/****************************************************************************** + * 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 { LangiumServices, LangiumSharedServices } from 'langium/lsp'; +import { DeepPartial, DefaultTypeRelationshipCaching, DefaultTypirServiceModule, Module, TypirServices } from 'typir'; +import { LangiumDomainElementInferenceCaching } from './features/langium-caching.js'; +import { LangiumProblemPrinter } from './features/langium-printing.js'; +import { PlaceholderLangiumTypeCreator, LangiumTypeCreator } from './features/langium-type-creator.js'; +import { LangiumTypirValidator, registerTypirValidationChecks } from './features/langium-validation.js'; + +/** + * Additional Typir-Langium services to manage the Typir services + * in order to be used e.g. for scoping/linking in Langium. + */ +export type TypirLangiumServices = { + readonly TypeValidation: LangiumTypirValidator, + readonly TypeCreator: LangiumTypeCreator, +} + +export type LangiumServicesForTypirBinding = TypirServices & TypirLangiumServices + +export type PartialTypirLangiumServices = DeepPartial + +/** + * Contains all customizations of Typir to simplify type checking for DSLs developed with Langium, + * the language workbench for textual domain-specific languages (DSLs) in the web (https://langium.org/). + */ +export function createLangiumModuleForTypirBinding(langiumServices: LangiumSharedServices): Module { + return { + // use all core Typir services: + ...DefaultTypirServiceModule, + // replace some of the core Typir default implementations for Langium: + printer: () => new LangiumProblemPrinter(), + caching: { + typeRelationships: (services) => new DefaultTypeRelationshipCaching(services), // this is the same implementation as in core Typir, since all edges of removed types are removed as well + domainElementInference: () => new LangiumDomainElementInferenceCaching(langiumServices), + }, + // provide implementations for the additional services for the Typir-Langium-binding: + TypeValidation: (typirServices) => new LangiumTypirValidator(typirServices), + TypeCreator: (typirServices) => new PlaceholderLangiumTypeCreator(typirServices, langiumServices), + }; +} + +export function initializeLangiumTypirServices(services: LangiumServices & LangiumServicesForTypirBinding): void { + // register the type-related validations of Typir at the Langium validation registry + registerTypirValidationChecks(services); + + // initialize the type creation (this is not done automatically by dependency injection!) + services.TypeCreator.triggerInitialization(); + // TODO This does not work, if there is no Language Server used, e.g. in test cases! + // services.shared.lsp.LanguageServer.onInitialized(_params => { + // services.TypeCreator.triggerInitialization(); + // }); + // maybe using services.shared.workspace.WorkspaceManager.initializeWorkspace/loadAdditionalDocuments + // another idea is to use eagerLoad(inject(...)) when creating the services +} diff --git a/packages/typir-langium/src/utils/typir-langium-utils.ts b/packages/typir-langium/src/utils/typir-langium-utils.ts new file mode 100644 index 0000000..f85442c --- /dev/null +++ b/packages/typir-langium/src/utils/typir-langium-utils.ts @@ -0,0 +1,30 @@ +/****************************************************************************** + * 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, AstUtils, LangiumDocument, URI } from 'langium'; +import { LangiumServices } from 'langium/lsp'; + +export function getDocumentKeyForURI(document: URI): string { + return document.toString(); +} + +export function getDocumentKeyForDocument(document: LangiumDocument): string { + return getDocumentKeyForURI(document.uri); +} + +export function getDocumentKey(node: AstNode): string { + return getDocumentKeyForDocument(AstUtils.getDocument(node)); +} + +export async function deleteAllDocuments(services: LangiumServices) { + const docsToDelete = services.shared.workspace.LangiumDocuments.all + .map((x) => x.uri) + .toArray(); + await services.shared.workspace.DocumentBuilder.update( + [], // update no documents + docsToDelete + ); +} diff --git a/packages/typir-langium/tsconfig.json b/packages/typir-langium/tsconfig.json new file mode 100644 index 0000000..25c9de5 --- /dev/null +++ b/packages/typir-langium/tsconfig.json @@ -0,0 +1,12 @@ +// this file is required for VSCode to work properly +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": [ + "src/**/*", + "test/**/*" + ] +} diff --git a/packages/typir-langium/tsconfig.src.json b/packages/typir-langium/tsconfig.src.json new file mode 100644 index 0000000..f9bbdfe --- /dev/null +++ b/packages/typir-langium/tsconfig.src.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "references": [{ + "path": "../typir/tsconfig.src.json" +}], +"include": [ + "src/**/*.ts" + ] +} diff --git a/packages/typir-langium/tsconfig.test.json b/packages/typir-langium/tsconfig.test.json new file mode 100644 index 0000000..4c31a6d --- /dev/null +++ b/packages/typir-langium/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "test" + }, + "references": [ + { "path": "./tsconfig.src.json" } + ], + "include": [ + "test/**/*", + ] +} diff --git a/packages/typir/package.json b/packages/typir/package.json index ff5ce45..18d10e4 100644 --- a/packages/typir/package.json +++ b/packages/typir/package.json @@ -48,5 +48,5 @@ "url": "https://github.com/TypeFox/typir/", "directory": "packages/typir" }, - "bugs": "https://github.com/TypeFox/typir//issues" + "bugs": "https://github.com/TypeFox/typir/issues" } diff --git a/packages/typir/src/features/caching.ts b/packages/typir/src/features/caching.ts index ceea122..03a84ab 100644 --- a/packages/typir/src/features/caching.ts +++ b/packages/typir/src/features/caching.ts @@ -123,7 +123,6 @@ export class DefaultDomainElementInferenceCaching implements DomainElementInfere } protected initializeCache() { - // TODO reset cache for updated Langium documents! this.cache = new Map(); } diff --git a/packages/typir/src/features/inference.ts b/packages/typir/src/features/inference.ts index 8830cc7..a8b531b 100644 --- a/packages/typir/src/features/inference.ts +++ b/packages/typir/src/features/inference.ts @@ -5,7 +5,9 @@ ******************************************************************************/ import { assertUnreachable } from 'langium'; -import { Type, isType } from '../graph/type-node.js'; +import { TypeEdge } from '../graph/type-edge.js'; +import { TypeGraphListener } from '../graph/type-graph.js'; +import { isType, Type } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; import { DomainElementInferenceCaching } from './caching.js'; @@ -81,50 +83,6 @@ export interface TypeInferenceRuleWithInferringChildren { inferTypeWithChildrensTypes(domainElement: unknown, childrenTypes: Array, typir: TypirServices): Type | InferenceProblem } -/** - * This inference rule uses multiple internal inference rules for doing the type inference. - * If one of the child rules returns a type, this type is the result of the composite rule. - * Otherwise, all problems of all child rules are returned. - */ -// TODO this design looks a bit ugly ... "implements TypeInferenceRuleWithoutInferringChildren" does not work, since it is a function ... -export class CompositeTypeInferenceRule implements TypeInferenceRuleWithInferringChildren { - readonly subRules: TypeInferenceRule[] = []; - - inferTypeWithoutChildren(domainElement: unknown, typir: TypirServices): TypeInferenceResultWithInferringChildren { - class FunctionInference extends DefaultTypeInferenceCollector { - // do not check "pending" (again), since it is already checked by the "parent" DefaultTypeInferenceCollector! - override pendingGet(_domainElement: unknown): boolean { - return false; - } - } - const infer = new FunctionInference(typir); - this.subRules.forEach(r => infer.addInferenceRule(r)); - - // do the type inference - const result = infer.inferType(domainElement); - if (isType(result)) { - return result; - } else { - if (result.length <= 0) { - return InferenceRuleNotApplicable; - } else if (result.length === 1) { - return result[0]; - } else { - return { - $problem: InferenceProblem, - domainElement, - location: 'sub-rules for inference', - rule: this, - subProblems: result, - }; - } - } - } - - inferTypeWithChildrensTypes(_domainElement: unknown, _childrenTypes: Array, _typir: TypirServices): Type | InferenceProblem { - throw new Error('This function will not be called.'); - } -} /** * Collects an arbitrary number of inference rules @@ -137,23 +95,36 @@ export interface TypeInferenceCollector { * @returns the found Type or some inference problems (might be empty), when none of the inference rules were able to infer a type */ inferType(domainElement: unknown): Type | InferenceProblem[] - /** * Registers an inference rule. * When inferring the type for an element, all registered inference rules are checked until the first match. * @param rule a new inference rule + * @param boundToType an optional type, if the new inference rule is dedicated for exactly this type. + * If the given type is removed from the type system, this rule will be automatically removed as well. */ - addInferenceRule(rule: TypeInferenceRule): void; + addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void; } -export class DefaultTypeInferenceCollector implements TypeInferenceCollector { - protected readonly inferenceRules: TypeInferenceRule[] = []; + +export class DefaultTypeInferenceCollector implements TypeInferenceCollector, TypeGraphListener { + protected readonly inferenceRules: Map = new Map(); // type identifier (otherwise '') -> inference rules protected readonly domainElementInference: DomainElementInferenceCaching; protected readonly typir: TypirServices; constructor(services: TypirServices) { this.typir = services; this.domainElementInference = services.caching.domainElementInference; + this.typir.graph.addListener(this); + } + + addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void { + const key = boundToType?.identifier ?? ''; + let rules = this.inferenceRules.get(key); + if (!rules) { + rules = []; + this.inferenceRules.set(key, rules); + } + rules.push(rule); } inferType(domainElement: unknown): Type | InferenceProblem[] { @@ -182,72 +153,82 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector { return result; } + protected checkForError(domainElement: unknown): void { + if (domainElement === undefined || domainElement === null) { + throw new Error('Element must be not undefined/null!'); + } + } + protected inferTypeLogic(domainElement: unknown): Type | InferenceProblem[] { + this.checkForError(domainElement); // otherwise, check all rules const collectedInferenceProblems: InferenceProblem[] = []; - for (const rule of this.inferenceRules) { - if (typeof rule === 'function') { - // simple case without type inference for children - const ruleResult: TypeInferenceResultWithoutInferringChildren = rule(domainElement, this.typir); - const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); - if (checkResult) { - // this inference rule was applicable and produced a final result - return checkResult; - } else { - // no result for this inference rule => check the next inference rules - } - } else if (typeof rule === 'object') { - // more complex case with inferring the type for children - const ruleResult: TypeInferenceResultWithInferringChildren = rule.inferTypeWithoutChildren(domainElement, this.typir); - if (Array.isArray(ruleResult)) { - // this rule might match => continue applying this rule - // resolve the requested child types - const childElements = ruleResult; - const childTypes: Array = childElements.map(child => this.inferType(child)); - // check, whether inferring the children resulted in some other inference problems - const childTypeProblems: InferenceProblem[] = []; - for (let i = 0; i < childTypes.length; i++) { - const child = childTypes[i]; - if (Array.isArray(child)) { - childTypeProblems.push({ + for (const rules of this.inferenceRules.values()) { + for (const rule of rules) { + if (typeof rule === 'function') { + // simple case without type inference for children + const ruleResult: TypeInferenceResultWithoutInferringChildren = rule(domainElement, this.typir); + this.checkForError(ruleResult); + const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); + if (checkResult) { + // this inference rule was applicable and produced a final result + return checkResult; + } else { + // no result for this inference rule => check the next inference rules + } + } else if (typeof rule === 'object') { + // more complex case with inferring the type for children + const ruleResult: TypeInferenceResultWithInferringChildren = rule.inferTypeWithoutChildren(domainElement, this.typir); + if (Array.isArray(ruleResult)) { + // this rule might match => continue applying this rule + // resolve the requested child types + const childElements = ruleResult; + const childTypes: Array = childElements.map(child => this.inferType(child)); + // check, whether inferring the children resulted in some other inference problems + const childTypeProblems: InferenceProblem[] = []; + for (let i = 0; i < childTypes.length; i++) { + const child = childTypes[i]; + if (Array.isArray(child)) { + childTypeProblems.push({ + $problem: InferenceProblem, + domainElement: childElements[i], + location: `child element ${i}`, + rule, + subProblems: child, + }); + } + } + if (childTypeProblems.length >= 1) { + collectedInferenceProblems.push({ $problem: InferenceProblem, - domainElement: childElements[i], - location: `child element ${i}`, + domainElement, + location: 'inferring depending children', rule, - subProblems: child, + subProblems: childTypeProblems, }); + } else { + // the types of all children are successfully inferred + const finalInferenceResult = rule.inferTypeWithChildrensTypes(domainElement, childTypes as Type[], this.typir); + if (isType(finalInferenceResult)) { + // type is inferred! + return finalInferenceResult; + } else { + // inference is not applicable (probably due to a mismatch of the children's types) => check the next rule + collectedInferenceProblems.push(finalInferenceResult); + } } - } - if (childTypeProblems.length >= 1) { - collectedInferenceProblems.push({ - $problem: InferenceProblem, - domainElement, - location: 'inferring depending children', - rule, - subProblems: childTypeProblems, - }); } else { - // the types of all children are successfully inferred - const finalInferenceResult = rule.inferTypeWithChildrensTypes(domainElement, childTypes as Type[], this.typir); - if (isType(finalInferenceResult)) { - // type is inferred! - return finalInferenceResult; + const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); + if (checkResult) { + // this inference rule was applicable and produced a final result + return checkResult; } else { - // inference is not applicable (probably due to a mismatch of the children's types) => check the next rule - collectedInferenceProblems.push(finalInferenceResult); + // no result for this inference rule => check the next inference rules } } } else { - const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); - if (checkResult) { - // this inference rule was applicable and produced a final result - return checkResult; - } else { - // no result for this inference rule => check the next inference rules - } + assertUnreachable(rule); } - } else { - assertUnreachable(rule); } } @@ -280,9 +261,22 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector { return undefined; } - addInferenceRule(rule: TypeInferenceRule): void { - this.inferenceRules.push(rule); + + /* Get informed about deleted types in order to remove inference rules which are bound to them. */ + + addedType(_newType: Type): void { + // do nothing + } + removedType(type: Type): void { + this.inferenceRules.delete(type.identifier); } + addedEdge(_edge: TypeEdge): void { + // do nothing + } + removedEdge(_edge: TypeEdge): void { + // do nothing + } + /* By default, the central cache of Typir is used. */ @@ -304,3 +298,44 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector { return this.domainElementInference.pendingGet(domainElement); } } + + +/** + * This inference rule uses multiple internal inference rules for doing the type inference. + * If one of the child rules returns a type, this type is the result of the composite rule. + * Otherwise, all problems of all child rules are returned. + */ +// This design looks a bit ugly ..., but "implements TypeInferenceRuleWithoutInferringChildren" does not work, since it is a function ... +export class CompositeTypeInferenceRule extends DefaultTypeInferenceCollector implements TypeInferenceRuleWithInferringChildren { + + // do not check "pending" (again), since it is already checked by the "parent" DefaultTypeInferenceCollector! + override pendingGet(_domainElement: unknown): boolean { + return false; + } + + inferTypeWithoutChildren(domainElement: unknown, _typir: TypirServices): TypeInferenceResultWithInferringChildren { + // do the type inference + const result = this.inferType(domainElement); + if (isType(result)) { + return result; + } else { + if (result.length <= 0) { + return InferenceRuleNotApplicable; + } else if (result.length === 1) { + return result[0]; + } else { + return { + $problem: InferenceProblem, + domainElement, + location: 'sub-rules for inference', + rule: this, + subProblems: result, + }; + } + } + } + + inferTypeWithChildrensTypes(_domainElement: unknown, _childrenTypes: Array, _typir: TypirServices): Type | InferenceProblem { + throw new Error('This function will not be called.'); + } +} diff --git a/packages/typir/src/features/operator.ts b/packages/typir/src/features/operator.ts index 6da4435..8bb9f13 100644 --- a/packages/typir/src/features/operator.ts +++ b/packages/typir/src/features/operator.ts @@ -105,7 +105,7 @@ export class DefaultOperatorManager implements OperatorManager { result.push(this.createGenericOperator({ name: typeDetails.name, outputType: signature.return, - inferenceRule: typeDetails.inferenceRule, // TODO zu oft ?? + inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! inputParameter: [ { name: 'operand', type: signature.operand }, ] @@ -121,7 +121,7 @@ export class DefaultOperatorManager implements OperatorManager { result.push(this.createGenericOperator({ name: typeDetails.name, outputType: signature.return, - inferenceRule: typeDetails.inferenceRule, // TODO zu oft ?? + inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! inputParameter: [ { name: 'left', type: signature.left}, { name: 'right', type: signature.right} @@ -138,7 +138,7 @@ export class DefaultOperatorManager implements OperatorManager { result.push(this.createGenericOperator({ name: typeDetails.name, outputType: signature.return, - inferenceRule: typeDetails.inferenceRule, // TODO zu oft ?? + inferenceRule: typeDetails.inferenceRule, // the same inference rule is used (and required) for all overloads, since multiple FunctionTypes are created! inputParameter: [ { name: 'first', type: signature.first }, { name: 'second', type: signature.second }, diff --git a/packages/typir/src/features/validation.ts b/packages/typir/src/features/validation.ts index a49de42..6a7b14b 100644 --- a/packages/typir/src/features/validation.ts +++ b/packages/typir/src/features/validation.ts @@ -6,7 +6,7 @@ import { Type, isType } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; -import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; +import { TypirProblem, isSpecificTypirProblem } from '../utils/utils-definitions.js'; import { TypeCheckStrategy, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; import { TypeInferenceCollector } from './inference.js'; import { ProblemPrinter } from './printing.js'; @@ -32,6 +32,12 @@ export function isValidationProblem(problem: unknown): problem is ValidationProb export type ValidationRule = (domainElement: unknown, typir: TypirServices) => ValidationProblem[]; +export interface ValidationRuleWithBeforeAfter { + beforeValidation(domainRoot: unknown, typir: TypirServices): ValidationProblem[] + validation: ValidationRule + afterValidation(domainRoot: unknown, typir: TypirServices): ValidationProblem[] +} + /** Annotate types after the validation with additional information in order to ease the creation of usefull messages. */ export interface AnnotatedTypeAfterValidation { type: Type; @@ -135,27 +141,55 @@ export class DefaultValidationConstraints implements ValidationConstraints { export interface ValidationCollector { + validateBefore(domainRoot: unknown): ValidationProblem[]; validate(domainElement: unknown): ValidationProblem[]; + validateAfter(domainRoot: unknown): ValidationProblem[]; + addValidationRules(...rules: ValidationRule[]): void; + addValidationRulesWithBeforeAndAfter(...rules: ValidationRuleWithBeforeAfter[]): void; } export class DefaultValidationCollector implements ValidationCollector { protected readonly services: TypirServices; readonly validationRules: ValidationRule[] = []; + readonly validationRulesBeforeAfter: ValidationRuleWithBeforeAfter[] = []; constructor(services: TypirServices) { this.services = services; } + validateBefore(domainRoot: unknown): ValidationProblem[] { + const problems: ValidationProblem[] = []; + for (const rule of this.validationRulesBeforeAfter) { + problems.push(...rule.beforeValidation(domainRoot, this.services)); + } + return problems; + } + validate(domainElement: unknown): ValidationProblem[] { const problems: ValidationProblem[] = []; for (const rule of this.validationRules) { problems.push(...rule(domainElement, this.services)); } + for (const rule of this.validationRulesBeforeAfter) { + problems.push(...rule.validation(domainElement, this.services)); + } + return problems; + } + + validateAfter(domainRoot: unknown): ValidationProblem[] { + const problems: ValidationProblem[] = []; + for (const rule of this.validationRulesBeforeAfter) { + problems.push(...rule.afterValidation(domainRoot, this.services)); + } return problems; } addValidationRules(...rules: ValidationRule[]): void { this.validationRules.push(...rules); } + + addValidationRulesWithBeforeAndAfter(...rules: ValidationRuleWithBeforeAfter[]): void { + this.validationRulesBeforeAfter.push(...rules); + } } diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index 4d13f9c..a0ecc44 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -21,7 +21,13 @@ export class TypeGraph { protected readonly nodes: Map = new Map(); // type name => Type protected readonly edges: TypeEdge[] = []; + protected readonly listeners: TypeGraphListener[] = []; + /** + * Usually this method is called by kinds after creating a a corresponding type. + * Therefore it is usually not needed to call this method in an other context. + * @param type the new type + */ addNode(type: Type): void { const key = type.identifier; if (this.nodes.has(key)) { @@ -32,15 +38,29 @@ export class TypeGraph { } } else { this.nodes.set(key, type); + this.listeners.forEach(listener => listener.addedType(type)); } } + /** + * When removing a type/node, all its edges (incoming and outgoing) are removed as well. + * Design decision: + * This is the central API call to remove a type from the type system in case that it is no longer valid/existing/needed. + * It is not required to directly inform the kind of the removed type yourself, since the kind itself will take care of removed types. + * @param type the type to remove + */ removeNode(type: Type): void { + const key = type.identifier; // remove all edges which are connected to the type to remove type.getAllIncomingEdges().forEach(e => this.removeEdge(e)); type.getAllOutgoingEdges().forEach(e => this.removeEdge(e)); // remove the type itself - this.nodes.delete(type.identifier); + const contained = this.nodes.delete(key); + if (contained) { + this.listeners.forEach(listener => listener.removedType(type)); + } else { + throw new Error(`Type does not exist: ${key}`); + } } getNode(name: string): Type | undefined { @@ -63,17 +83,22 @@ export class TypeGraph { // register this new edge at the connected nodes edge.to.addIncomingEdge(edge); edge.from.addOutgoingEdge(edge); + + this.listeners.forEach(listener => listener.addedEdge(edge)); } removeEdge(edge: TypeEdge): void { + // remove this new edge at the connected nodes + edge.to.removeIncomingEdge(edge); + edge.from.removeOutgoingEdge(edge); + const index = this.edges.indexOf(edge); if (index >= 0) { this.edges.splice(index, 1); + this.listeners.forEach(listener => listener.removedEdge(edge)); + } else { + throw new Error(`Edge does not exist: ${edge.$relation}`); } - - // remove this new edge at the connected nodes - edge.to.removeIncomingEdge(edge); - edge.from.removeOutgoingEdge(edge); } getUnidirectionalEdge(from: Type, to: Type, $relation: T['$relation'], cachingMode: EdgeCachingInformation = 'LINK_EXISTS'): T | undefined { @@ -86,6 +111,26 @@ export class TypeGraph { } + // register listeners for changed types/edges in the type graph + + addListener(listener: TypeGraphListener): void { + this.listeners.push(listener); + } + removeListener(listener: TypeGraphListener): void { + const index = this.listeners.indexOf(listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } + + // add reusable graph algorithms here (or introduce a new service for graph algorithms which might be easier to customize/exchange) } + +export interface TypeGraphListener { + addedType(type: Type): void; + removedType(type: Type): void; + addedEdge(edge: TypeEdge): void; + removedEdge(edge: TypeEdge): void; +} diff --git a/packages/typir/src/index.ts b/packages/typir/src/index.ts index fc34290..b38db96 100644 --- a/packages/typir/src/index.ts +++ b/packages/typir/src/index.ts @@ -13,6 +13,7 @@ export * from './features/inference.js'; export * from './features/operator.js'; export * from './features/printing.js'; export * from './features/subtype.js'; +export * from './features/validation.js'; export * from './graph/type-edge.js'; export * from './graph/type-graph.js'; export * from './graph/type-node.js'; @@ -23,6 +24,7 @@ export * from './kinds/kind.js'; export * from './kinds/multiplicity-kind.js'; export * from './kinds/primitive-kind.js'; export * from './kinds/top-kind.js'; +export * from './utils/dependency-injection.js'; export * from './utils/utils.js'; export * from './utils/utils-definitions.js'; export * from './utils/utils-type-comparison.js'; diff --git a/packages/typir/src/kinds/bottom-kind.ts b/packages/typir/src/kinds/bottom-kind.ts index 8a676f6..53d22cb 100644 --- a/packages/typir/src/kinds/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom-kind.ts @@ -107,9 +107,10 @@ export class BottomKind implements Kind { } getOrCreateBottomType(typeDetails: BottomTypeDetails): BottomType { - const result = this.getBottomType(typeDetails); - if (result) { - return result; + const bottomType = this.getBottomType(typeDetails); + if (bottomType) { + this.registerInferenceRules(typeDetails, bottomType); + return bottomType; } return this.createBottomType(typeDetails); } @@ -126,6 +127,12 @@ export class BottomKind implements Kind { this.services.graph.addNode(bottomType); // register all inference rules for primitives within a single generic inference rule (in order to keep the number of "global" inference rules small) + this.registerInferenceRules(typeDetails, bottomType); + + return bottomType; + } + + protected registerInferenceRules(typeDetails: BottomTypeDetails, bottomType: BottomType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { this.services.inference.addInferenceRule((domainElement, _typir) => { @@ -135,10 +142,8 @@ export class BottomKind implements Kind { } } return InferenceRuleNotApplicable; - }); + }, bottomType); } - - return bottomType; } calculateIdentifier(_typeDetails: BottomTypeDetails): string { diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 279e2fe..187693d 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -8,12 +8,13 @@ import { assertUnreachable } from 'langium'; import { TypeEqualityProblem } from '../features/equality.js'; import { InferenceProblem, InferenceRuleNotApplicable } from '../features/inference.js'; import { SubTypeProblem } from '../features/subtype.js'; +import { ValidationProblem, ValidationRuleWithBeforeAfter } from '../features/validation.js'; import { Type, isType } from '../graph/type-node.js'; +import { TypirServices } from '../typir.js'; import { TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; import { IndexedTypeConflict, MapListConverter, TypeCheckStrategy, checkNameTypesMap, checkValueForConflict, createKindConflict, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; import { assertTrue, assertType, toArray } from '../utils/utils.js'; import { Kind, isKind } from './kind.js'; -import { TypirServices } from '../typir.js'; export class ClassType extends Type { override readonly kind: ClassKind; @@ -163,11 +164,11 @@ export class ClassType extends Type { const allSub = subType.getAllSuperClasses(true); const globalResult: TypirProblem[] = []; for (const oneSub of allSub) { - const localResult = checkValueForConflict(superType.identifier, oneSub.identifier, 'name'); // TODO use equals instead?? - if (localResult.length <= 0) { + const localResult = this.kind.services.equality.getTypeEqualityProblem(superType, oneSub); + if (localResult === undefined) { return []; // class is found in the class hierarchy } - globalResult.push(...localResult); // return all conflicts, TODO: is that too much?? + globalResult.push(localResult); // return all conflicts, is that too much? } return globalResult; } else { @@ -329,21 +330,28 @@ export class ClassKind implements Kind { } getOrCreateClassType(typeDetails: CreateClassTypeDetails): ClassType { - const result = this.getClassType(typeDetails); - if (result) { - return result; + const classType = this.getClassType(typeDetails); + if (classType) { + this.registerInferenceRules(typeDetails, classType); + return classType; } return this.createClassType(typeDetails); } createClassType(typeDetails: CreateClassTypeDetails): ClassType { - assertTrue(this.getClassType(typeDetails) === undefined); + assertTrue(this.getClassType(typeDetails) === undefined, `${typeDetails.className}`); // create the class type const classType = new ClassType(this, this.calculateIdentifier(typeDetails), typeDetails); this.services.graph.addNode(classType); // register inference rules + this.registerInferenceRules(typeDetails, classType); + + return classType; + } + + protected registerInferenceRules(typeDetails: CreateClassTypeDetails, classType: ClassType) { if (typeDetails.inferenceRuleForDeclaration) { this.services.inference.addInferenceRule({ inferTypeWithoutChildren(domainElement, _typir) { @@ -357,13 +365,13 @@ export class ClassKind implements Kind { // TODO check values for fields for nominal typing! return classType; }, - }); + }, classType); } if (typeDetails.inferenceRuleForLiteral) { - this.registerInferenceRule(typeDetails.inferenceRuleForLiteral, this, classType); + this.registerInferenceRuleForLiteral(typeDetails.inferenceRuleForLiteral, this, classType); } if (typeDetails.inferenceRuleForReference) { - this.registerInferenceRule(typeDetails.inferenceRuleForReference, this, classType); + this.registerInferenceRuleForLiteral(typeDetails.inferenceRuleForReference, this, classType); } if (typeDetails.inferenceRuleForFieldAccess) { this.services.inference.addInferenceRule((domainElement, _typir) => { @@ -387,13 +395,11 @@ export class ClassKind implements Kind { } else { return result; // do the type inference for this element instead } - }); + }, classType); } - - return classType; } - protected registerInferenceRule(rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): void { + protected registerInferenceRuleForLiteral(rule: InferClassLiteral, classKind: ClassKind, classType: ClassType): void { const mapListConverter = new MapListConverter(); this.services.inference.addInferenceRule({ inferTypeWithoutChildren(domainElement, _typir) { @@ -440,7 +446,7 @@ export class ClassKind implements Kind { return classType; } }, - }); + }, classType); } calculateIdentifier(typeDetails: ClassTypeDetails): string { @@ -476,3 +482,71 @@ export class ClassKind implements Kind { export function isClassKind(kind: unknown): kind is ClassKind { return isKind(kind) && kind.$name === ClassKindName; } + + +/** + * Predefined validation to produce errors, if the same class is declared more than once. + * This is often relevant for nominally typed classes. + */ +export class UniqueClassValidation implements ValidationRuleWithBeforeAfter { + protected readonly foundDeclarations: Map = new Map(); + protected readonly services: TypirServices; + protected readonly isRelevant: (domainElement: unknown) => boolean; // using this check improves performance a lot + + constructor(services: TypirServices, isRelevant: (domainElement: unknown) => boolean) { + this.services = services; + this.isRelevant = isRelevant; + } + + beforeValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { + this.foundDeclarations.clear(); + return []; + } + + validation(domainElement: unknown, _typir: TypirServices): ValidationProblem[] { + if (this.isRelevant(domainElement)) { // improves performance, since type inference need to be done only for relevant elements + const type = this.services.inference.inferType(domainElement); + if (isClassType(type)) { + // register domain elements which have ClassTypes with a key for their uniques + const key = this.calculateClassKey(type); + let entries = this.foundDeclarations.get(key); + if (!entries) { + entries = []; + this.foundDeclarations.set(key, entries); + } + entries.push(domainElement); + } + } + return []; + } + + /** + * Calculates a key for a class which encodes its unique properties, i.e. duplicate classes have the same key. + * This key is used to identify duplicated classes. + * Override this method to change the properties which make a class unique. + * @param clas the current class type + * @returns a string key + */ + protected calculateClassKey(clas: ClassType): string { + return `${clas.className}`; + } + + afterValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { + const result: ValidationProblem[] = []; + for (const [key, classes] of this.foundDeclarations.entries()) { + if (classes.length >= 2) { + for (const clas of classes) { + result.push({ + $problem: ValidationProblem, + domainElement: clas, + severity: 'error', + message: `Declared classes need to be unique (${key}).`, + }); + } + } + } + + this.foundDeclarations.clear(); + return result; + } +} diff --git a/packages/typir/src/kinds/fixed-parameters-kind.ts b/packages/typir/src/kinds/fixed-parameters-kind.ts index b988466..da655d7 100644 --- a/packages/typir/src/kinds/fixed-parameters-kind.ts +++ b/packages/typir/src/kinds/fixed-parameters-kind.ts @@ -175,9 +175,10 @@ export class FixedParameterKind implements Kind { } getOrCreateFixedParameterType(typeDetails: FixedParameterTypeDetails): FixedParameterType { - const result = this.getFixedParameterType(typeDetails); - if (result) { - return result; + const typeWithParameters = this.getFixedParameterType(typeDetails); + if (typeWithParameters) { + this.registerInferenceRules(typeDetails, typeWithParameters); + return typeWithParameters; } return this.createFixedParameterType(typeDetails); } @@ -190,9 +191,15 @@ export class FixedParameterKind implements Kind { const typeWithParameters = new FixedParameterType(this, this.calculateIdentifier(typeDetails), ...toArray(typeDetails.parameterTypes)); this.services.graph.addNode(typeWithParameters); + this.registerInferenceRules(typeDetails, typeWithParameters); + return typeWithParameters; } + protected registerInferenceRules(_typeDetails: FixedParameterTypeDetails, _typeWithParameters: FixedParameterType): void { + // TODO + } + calculateIdentifier(typeDetails: FixedParameterTypeDetails): string { return this.printSignature(this.baseName, toArray(typeDetails.parameterTypes), ','); // use the signature for a unique name } diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index f722190..f3adc19 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -7,7 +7,9 @@ import { TypeEqualityProblem } from '../features/equality.js'; import { CompositeTypeInferenceRule, InferenceProblem, InferenceRuleNotApplicable } from '../features/inference.js'; import { SubTypeProblem } from '../features/subtype.js'; -import { ValidationProblem } from '../features/validation.js'; +import { ValidationProblem, ValidationRuleWithBeforeAfter } from '../features/validation.js'; +import { TypeEdge } from '../graph/type-edge.js'; +import { TypeGraphListener } from '../graph/type-graph.js'; import { Type, isType } from '../graph/type-node.js'; import { TypirServices } from '../typir.js'; import { NameTypePair, TypeSelector, TypirProblem, resolveTypeSelector } from '../utils/utils-definitions.js'; @@ -246,13 +248,12 @@ export type InferFunctionCall = { * - optional parameters * - parameters which are used for output AND input */ -export class FunctionKind implements Kind { +export class FunctionKind implements Kind, TypeGraphListener { readonly $name: 'FunctionKind'; readonly services: TypirServices; readonly options: FunctionKindOptions; - /** TODO Limitations + /** Limitations * - Works only, if function types are defined using the createFunctionType(...) function below! - * - How to remove function types later? How to observe this case/event? How to remove their inference rules and validations? */ protected readonly mapNameTypes: Map = new Map(); // function name => all overloaded functions with this name/key // TODO try to replace this map with calculating the required identifier for the function @@ -375,9 +376,11 @@ export class FunctionKind implements Kind { } getOrCreateFunctionType(typeDetails: CreateFunctionTypeDetails): FunctionType { - const result = this.getFunctionType(typeDetails); - if (result) { - return result; + const functionType = this.getFunctionType(typeDetails); + if (functionType) { + // register the additional inference rules for the same type! + this.registerInferenceRules(typeDetails, functionType); + return functionType; } return this.createFunctionType(typeDetails); } @@ -386,7 +389,7 @@ export class FunctionKind implements Kind { const functionName = typeDetails.functionName; // check the input - assertTrue(this.getFunctionType(typeDetails) === undefined); // ensures, that no duplicated functions are created! + assertTrue(this.getFunctionType(typeDetails) === undefined, `${functionName}`); // ensures, that no duplicated functions are created! if (!typeDetails) { throw new Error('is undefined'); } @@ -401,23 +404,19 @@ export class FunctionKind implements Kind { this.services.graph.addNode(functionType); // output parameter for function calls - const outputTypeForFunctionCalls = functionType.getOutput()?.type ?? // by default, use the return type of the function ... - // ... if this type is missing, use the specified type for this case in the options: - // 'THROW_ERROR': an error will be thrown later, when this case actually occurs! - (this.options.typeToInferForCallsOfFunctionsWithoutOutput === 'THROW_ERROR' ? undefined : resolveTypeSelector(this.services, this.options.typeToInferForCallsOfFunctionsWithoutOutput)); + const outputTypeForFunctionCalls = this.getOutputTypeForFunctionCalls(functionType); // remember the new function for later in order to enable overloaded functions! - const mapNameTypes = this.mapNameTypes; - let overloaded = mapNameTypes.get(functionName); + let overloaded = this.mapNameTypes.get(functionName); if (overloaded) { // do nothing } else { overloaded = { overloadedFunctions: [], - inference: new CompositeTypeInferenceRule(), + inference: new CompositeTypeInferenceRule(this.services), sameOutputType: undefined, }; - mapNameTypes.set(functionName, overloaded); + this.mapNameTypes.set(functionName, overloaded); this.services.inference.addInferenceRule(overloaded.inference); } if (overloaded.overloadedFunctions.length <= 0) { @@ -436,13 +435,22 @@ export class FunctionKind implements Kind { inferenceRuleForCalls: typeDetails.inferenceRuleForCalls, }); + this.registerInferenceRules(typeDetails, functionType); + + return functionType; + } + + protected registerInferenceRules(typeDetails: CreateFunctionTypeDetails, functionType: FunctionType): void { + const functionName = typeDetails.functionName; + const mapNameTypes = this.mapNameTypes; + const overloaded = mapNameTypes.get(functionName)!; + const outputTypeForFunctionCalls = this.getOutputTypeForFunctionCalls(functionType); if (typeDetails.inferenceRuleForCalls) { /** Preconditions: * - there is a rule which specifies how to infer the current function type * - the current function has an output type/parameter, otherwise, this function could not provide any type (and throws an error), when it is called! * (exception: the options contain a type to return in this special case) */ - // TODO what about the case, that multiple variants match?? after implicit conversion for example?! function check(returnType: Type | undefined): Type { if (returnType) { return returnType; @@ -452,7 +460,8 @@ export class FunctionKind implements Kind { } // register inference rule for calls of the new function - overloaded.inference.subRules.push({ + // TODO what about the case, that multiple variants match?? after implicit conversion for example?! => overload with the lowest number of conversions wins! + overloaded.inference.addInferenceRule({ inferTypeWithoutChildren(domainElement, _typir) { const result = typeDetails.inferenceRuleForCalls!.filter(domainElement); if (result) { @@ -509,7 +518,7 @@ export class FunctionKind implements Kind { return check(outputTypeForFunctionCalls); } }, - }); + }, functionType); } // register inference rule for the declaration of the new function @@ -521,11 +530,45 @@ export class FunctionKind implements Kind { } else { return InferenceRuleNotApplicable; } - }); + }, functionType); } + } - return functionType; + protected getOutputTypeForFunctionCalls(functionType: FunctionType): Type | undefined { + return functionType.getOutput()?.type ?? // by default, use the return type of the function ... + // ... if this type is missing, use the specified type for this case in the options: + // 'THROW_ERROR': an error will be thrown later, when this case actually occurs! + (this.options.typeToInferForCallsOfFunctionsWithoutOutput === 'THROW_ERROR' + ? undefined + : resolveTypeSelector(this.services, this.options.typeToInferForCallsOfFunctionsWithoutOutput)); + } + + + /* Get informed about deleted types in order to remove inference rules which are bound to them. */ + + addedType(_newType: Type): void { + // do nothing + } + removedType(type: Type): void { + if (isFunctionType(type)) { + const overloads = this.mapNameTypes.get(type.functionName); + if (overloads) { + // remove the current function + const index = overloads.overloadedFunctions.findIndex(o => o.functionType === type); + if (index >= 0) { + overloads.overloadedFunctions.splice(index, 1); + } + // its inference rule is removed by the CompositeTypeInferenceRule => nothing to do here + } + } } + addedEdge(_edge: TypeEdge): void { + // do nothing + } + removedEdge(_edge: TypeEdge): void { + // do nothing + } + calculateIdentifier(typeDetails: FunctionTypeDetails): string { // this schema allows to identify duplicated functions! @@ -564,3 +607,70 @@ export const FUNCTION_MISSING_NAME = ''; export function isFunctionKind(kind: unknown): kind is FunctionKind { return isKind(kind) && kind.$name === FunctionKindName; } + + +/** + * Predefined validation to produce errors, if the same function is declared more than once. + */ +export class UniqueFunctionValidation implements ValidationRuleWithBeforeAfter { + protected readonly foundDeclarations: Map = new Map(); + protected readonly services: TypirServices; + protected readonly isRelevant: (domainElement: unknown) => boolean; // using this check improves performance a lot + + constructor(services: TypirServices, isRelevant: (domainElement: unknown) => boolean) { + this.services = services; + this.isRelevant = isRelevant; + } + + beforeValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { + this.foundDeclarations.clear(); + return []; + } + + validation(domainElement: unknown, _typir: TypirServices): ValidationProblem[] { + if (this.isRelevant(domainElement)) { // improves performance, since type inference need to be done only for relevant elements + const type = this.services.inference.inferType(domainElement); + if (isFunctionType(type)) { + // register domain elements which have FunctionTypes with a key for their uniques + const key = this.calculateFunctionKey(type); + let entries = this.foundDeclarations.get(key); + if (!entries) { + entries = []; + this.foundDeclarations.set(key, entries); + } + entries.push(domainElement); + } + } + return []; + } + + /** + * Calculates a key for a function which encodes its unique properties, i.e. duplicate functions have the same key. + * This key is used to identify duplicated functions. + * Override this method to change the properties which make a function unique. + * @param func the current function type + * @returns a string key + */ + protected calculateFunctionKey(func: FunctionType): string { + return `${func.functionName}(${func.getInputs().map(param => param.type.identifier)})`; + } + + afterValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { + const result: ValidationProblem[] = []; + for (const [key, functions] of this.foundDeclarations.entries()) { + if (functions.length >= 2) { + for (const func of functions) { + result.push({ + $problem: ValidationProblem, + domainElement: func, + severity: 'error', + message: `Declared functions need to be unique (${key}).`, + }); + } + } + } + + this.foundDeclarations.clear(); + return result; + } +} diff --git a/packages/typir/src/kinds/multiplicity-kind.ts b/packages/typir/src/kinds/multiplicity-kind.ts index f7c9e3f..e682194 100644 --- a/packages/typir/src/kinds/multiplicity-kind.ts +++ b/packages/typir/src/kinds/multiplicity-kind.ts @@ -156,9 +156,10 @@ export class MultiplicityKind implements Kind { } getOrCreateMultiplicityType(typeDetails: MultiplicityTypeDetails): MultiplicityType { - const result = this.getMultiplicityType(typeDetails); - if (result) { - return result; + const typeWithMultiplicity = this.getMultiplicityType(typeDetails); + if (typeWithMultiplicity) { + this.registerInferenceRules(typeDetails, typeWithMultiplicity); + return typeWithMultiplicity; } return this.createMultiplicityType(typeDetails); } @@ -171,10 +172,16 @@ export class MultiplicityKind implements Kind { } // create the type with multiplicities - const newType = new MultiplicityType(this, this.calculateIdentifier(typeDetails), typeDetails.constrainedType, typeDetails.lowerBound, typeDetails.upperBound); - this.services.graph.addNode(newType); + const typeWithMultiplicity = new MultiplicityType(this, this.calculateIdentifier(typeDetails), typeDetails.constrainedType, typeDetails.lowerBound, typeDetails.upperBound); + this.services.graph.addNode(typeWithMultiplicity); - return newType; + this.registerInferenceRules(typeDetails, typeWithMultiplicity); + + return typeWithMultiplicity; + } + + protected registerInferenceRules(_typeDetails: MultiplicityTypeDetails, _typeWithMultiplicity: MultiplicityType): void { + // TODO } calculateIdentifier(typeDetails: MultiplicityTypeDetails): string { diff --git a/packages/typir/src/kinds/primitive-kind.ts b/packages/typir/src/kinds/primitive-kind.ts index 0825650..b9af4a7 100644 --- a/packages/typir/src/kinds/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive-kind.ts @@ -107,9 +107,10 @@ export class PrimitiveKind implements Kind { } getOrCreatePrimitiveType(typeDetails: PrimitiveTypeDetails): PrimitiveType { - const result = this.getPrimitiveType(typeDetails); - if (result) { - return result; + const primitiveType = this.getPrimitiveType(typeDetails); + if (primitiveType) { + this.registerInferenceRules(typeDetails, primitiveType); + return primitiveType; } return this.createPrimitiveType(typeDetails); } @@ -121,7 +122,13 @@ export class PrimitiveKind implements Kind { const primitiveType = new PrimitiveType(this, this.calculateIdentifier(typeDetails)); this.services.graph.addNode(primitiveType); - // register all inference rules for primitives within a single generic inference rule (in order to keep the number of "global" inference rules small) + this.registerInferenceRules(typeDetails, primitiveType); + + return primitiveType; + } + + /** Register all inference rules for primitives within a single generic inference rule (in order to keep the number of "global" inference rules small). */ + protected registerInferenceRules(typeDetails: PrimitiveTypeDetails, primitiveType: PrimitiveType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { this.services.inference.addInferenceRule((domainElement, _typir) => { @@ -131,10 +138,8 @@ export class PrimitiveKind implements Kind { } } return InferenceRuleNotApplicable; - }); + }, primitiveType); } - - return primitiveType; } calculateIdentifier(typeDetails: PrimitiveTypeDetails): string { diff --git a/packages/typir/src/kinds/top-kind.ts b/packages/typir/src/kinds/top-kind.ts index 676e76c..50e5f25 100644 --- a/packages/typir/src/kinds/top-kind.ts +++ b/packages/typir/src/kinds/top-kind.ts @@ -107,9 +107,10 @@ export class TopKind implements Kind { } getOrCreateTopType(typeDetails: TopTypeDetails): TopType { - const result = this.getTopType(typeDetails); - if (result) { - return result; + const topType = this.getTopType(typeDetails); + if (topType) { + this.registerInferenceRules(typeDetails, topType); + return topType; } return this.createTopType(typeDetails); } @@ -126,7 +127,13 @@ export class TopKind implements Kind { this.instance = topType; this.services.graph.addNode(topType); - // register all inference rules for primitives within a single generic inference rule (in order to keep the number of "global" inference rules small) + this.registerInferenceRules(typeDetails, topType); + + return topType; + } + + /** Register all inference rules for primitives within a single generic inference rule (in order to keep the number of "global" inference rules small). */ + protected registerInferenceRules(typeDetails: TopTypeDetails, topType: TopType) { const rules = toArray(typeDetails.inferenceRules); if (rules.length >= 1) { this.services.inference.addInferenceRule((domainElement, _typir) => { @@ -136,10 +143,8 @@ export class TopKind implements Kind { } } return InferenceRuleNotApplicable; - }); + }, topType); } - - return topType; } calculateIdentifier(_typeDetails: TopTypeDetails): string { diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index c6d54bf..b550d9f 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -4,7 +4,6 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { inject, Module } from './utils/dependency-injection.js'; import { DefaultTypeAssignability, TypeAssignability } from './features/assignability.js'; import { DefaultDomainElementInferenceCaching, DefaultTypeRelationshipCaching, DomainElementInferenceCaching, TypeRelationshipCaching } from './features/caching.js'; import { DefaultTypeConversion, TypeConversion } from './features/conversion.js'; @@ -15,7 +14,8 @@ import { DefaultTypeConflictPrinter, ProblemPrinter } from './features/printing. import { DefaultSubType, SubType } from './features/subtype.js'; import { DefaultValidationCollector, DefaultValidationConstraints, ValidationCollector, ValidationConstraints } from './features/validation.js'; import { TypeGraph } from './graph/type-graph.js'; -import { KindRegistry, DefaultKindRegistry } from './kinds/kind-registry.js'; +import { DefaultKindRegistry, KindRegistry } from './kinds/kind-registry.js'; +import { inject, Module } from './utils/dependency-injection.js'; /** * Design decisions for Typir @@ -73,11 +73,20 @@ export const DefaultTypirServiceModule: Module = { validation: { collector: (services) => new DefaultValidationCollector(services), constraints: (services) => new DefaultValidationConstraints(services), - } + }, }; -export function createTypirServices(customization: Module = {}): TypirServices { - return inject(DefaultTypirServiceModule, customization); +/** + * Creates the TypirServices with the default module containing the default implements for Typir, which might be exchanged by the given optional customized modules. + * @param customization1 optional Typir module with customizations + * @param customization2 optional Typir module with customizations + * @returns a Typir instance, i.e. the TypirServices with implementations + */ +export function createTypirServices( + customization1: Module = {}, + customization2: Module = {} +): TypirServices { + return inject(DefaultTypirServiceModule, customization1, customization2); } /** diff --git a/tsconfig.build.json b/tsconfig.build.json index b9d7fd0..a2cd4bf 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,6 +3,8 @@ "references": [ { "path": "packages/typir/tsconfig.src.json" }, { "path": "packages/typir/tsconfig.test.json" }, + { "path": "packages/typir-langium/tsconfig.src.json" }, + { "path": "packages/typir-langium/tsconfig.test.json" }, { "path": "examples/lox/tsconfig.src.json" }, { "path": "examples/lox/tsconfig.test.json" }, { "path": "examples/ox/tsconfig.src.json" },