diff --git a/examples/lox/src/language/lox-linker.ts b/examples/lox/src/language/lox-linker.ts new file mode 100644 index 0000000..ff4727e --- /dev/null +++ b/examples/lox/src/language/lox-linker.ts @@ -0,0 +1,63 @@ +/****************************************************************************** + * Copyright 2024 TypeFox GmbH + * This program and the accompanying materials are made available under the + * terms of the MIT License, which is available in the project root. + ******************************************************************************/ + +import { AstNodeDescription, DefaultLinker, LinkingError, ReferenceInfo } from 'langium'; +import { isType } from '../../../../packages/typir/lib/graph/type-node.js'; +import { TypirServices } from '../../../../packages/typir/lib/typir.js'; +import { isClass, isFunctionDeclaration, isMemberCall, isMethodMember } from './generated/ast.js'; +import { LoxServices } from './lox-module.js'; + +export class LoxLinker extends DefaultLinker { + protected readonly typir: TypirServices; + + constructor(services: LoxServices) { + super(services); + this.typir = services.typir; + } + + override getCandidate(refInfo: ReferenceInfo): AstNodeDescription | LinkingError { + const container = refInfo.container; + if (isMemberCall(container) && container.explicitOperationCall) { + // handle overloaded functions/methods + const scope = this.scopeProvider.getScope(refInfo); + const calledDescriptions = scope.getAllElements().filter(d => d.name === refInfo.reference.$refText).toArray(); // same name + if (calledDescriptions.length === 1) { + return calledDescriptions[0]; // no overloaded functions/methods + } if (calledDescriptions.length >= 2) { + // in case of overloaded functions/methods, do type inference for given arguments + const argumentTypes = container.arguments.map(arg => this.typir.Inference.inferType(arg)).filter(isType); + if (argumentTypes.length === container.arguments.length) { // for all given arguments, a type is inferred + for (const calledDescription of calledDescriptions) { + const called = this.loadAstNode(calledDescription); + if (isClass(called)) { + // special case: call of the constructur, without any arguments/parameters + return calledDescription; // there is only one constructor without any parameters + } + if ((isMethodMember(called) || isFunctionDeclaration(called)) && called.parameters.length === container.arguments.length) { // same number of arguments + // infer expected types of parameters + const parameterTypes = called.parameters.map(p => this.typir.Inference.inferType(p)).filter(isType); + if (parameterTypes.length === called.parameters.length) { // for all parameters, a type is inferred + if (argumentTypes.every((arg, index) => this.typir.Assignability.isAssignable(arg, parameterTypes[index]))) { + return calledDescription; + } + } + } + } + } + // no matching method is found, return the first found method => linking works + validation issues regarding the wrong parameter values can be shown! + return calledDescriptions[0]; + + // the following approach does not work, since the container's cross-references are required for type inference, but they are not yet resolved + // const type = this.typir.Inference.inferType(container); + // if (isFunctionType(type)) { + // return type.associatedDomainElement; + // } + } + return this.createLinkingError(refInfo); + } + return super.getCandidate(refInfo); + } +} diff --git a/examples/lox/src/language/lox-module.ts b/examples/lox/src/language/lox-module.ts index 6e20f54..5b9f410 100644 --- a/examples/lox/src/language/lox-module.ts +++ b/examples/lox/src/language/lox-module.ts @@ -11,6 +11,7 @@ import { LoxGeneratedModule, LoxGeneratedSharedModule } from './generated/module import { LoxScopeProvider } from './lox-scope.js'; import { LoxValidationRegistry, LoxValidator } from './lox-validator.js'; import { createLoxTypirModule } from './lox-type-checking.js'; +import { LoxLinker } from './lox-linker.js'; /** * Declaration of custom services - add your own service classes here. @@ -45,7 +46,8 @@ export function createLoxModule(shared: LangiumSharedCoreServices): Module new LoxScopeProvider(services) + ScopeProvider: (services) => new LoxScopeProvider(services), + Linker: (services) => new LoxLinker(services), }, }; } diff --git a/examples/lox/src/language/lox-scope.ts b/examples/lox/src/language/lox-scope.ts index f28bd19..315b479 100644 --- a/examples/lox/src/language/lox-scope.ts +++ b/examples/lox/src/language/lox-scope.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { DefaultScopeProvider, EMPTY_SCOPE, AstUtils, ReferenceInfo, Scope } from 'langium'; -import { Class, isClass, MemberCall } from './generated/ast.js'; +import { Class, isClass, isMemberCall, MemberCall } from './generated/ast.js'; import { getClassChain } from './lox-utils.js'; import { LoxServices } from './lox-module.js'; import { TypirServices } from '../../../../packages/typir/lib/typir.js'; @@ -23,7 +23,7 @@ export class LoxScopeProvider extends DefaultScopeProvider { override getScope(context: ReferenceInfo): Scope { // target element of member calls - if (context.property === 'element') { + if (context.property === 'element' && isMemberCall(context.container)) { // for now, `this` and `super` simply target the container class type if (context.reference.$refText === 'this' || context.reference.$refText === 'super') { const classItem = AstUtils.getContainerOfType(context.container, isClass); diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index 9f795cc..cee81a9 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -91,7 +91,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { severity: 'warning' }), }); } - // = for SuperType = SubType (Note that LOX designed assignments as operators!) + // = for SuperType = SubType (Note that this implementation of LOX realized assignments as operators!) this.typir.factory.Operators.createBinary({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule, // this validation will be checked for each call of this operator! validationRule: (node, _opName, _opType, typir) => typir.validation.Constraints.ensureNodeIsAssignable(node.right, node.left, (actual, expected) => { @@ -136,6 +136,10 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { return InferenceRuleNotApplicable; // this case is impossible, there is a validation in the Langium LOX validator for this case } } + // ... parameters + if (isParameter(domainElement)) { + return domainElement.type; + } return InferenceRuleNotApplicable; }); @@ -214,7 +218,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // inference ruleS(?) for objects/class literals conforming to the current class inferenceRuleForLiteral: { // > filter: isMemberCall, - matching: (domainElement: MemberCall) => isClass(domainElement.element?.ref) && domainElement.element!.ref.name === className, + matching: (domainElement: MemberCall) => isClass(domainElement.element?.ref) && domainElement.element!.ref.name === className && domainElement.explicitOperationCall, inputValuesForFields: (_domainElement: MemberCall) => new Map(), // values for fields don't matter for nominal typing }, inferenceRuleForReference: { // > @@ -223,7 +227,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { inputValuesForFields: (_domainElement: TypeReference) => new Map(), // values for fields don't matter for nominal typing }, // inference rule for accessing fields - inferenceRuleForFieldAccess: (domainElement: unknown) => isMemberCall(domainElement) && isFieldMember(domainElement.element?.ref) && domainElement.element!.ref.$container === node + inferenceRuleForFieldAccess: (domainElement: unknown) => isMemberCall(domainElement) && isFieldMember(domainElement.element?.ref) && domainElement.element!.ref.$container === node && !domainElement.explicitOperationCall ? domainElement.element!.ref.name : InferenceRuleNotApplicable, associatedDomainElement: node, }); diff --git a/examples/lox/test/lox-type-checking-functions.test.ts b/examples/lox/test/lox-type-checking-functions.test.ts index 169b464..c6ac429 100644 --- a/examples/lox/test/lox-type-checking-functions.test.ts +++ b/examples/lox/test/lox-type-checking-functions.test.ts @@ -4,18 +4,24 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { describe, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; import { loxServices, operatorNames, validateLox } from './lox-type-checking-utils.js'; import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; import { isFunctionType } from '../../../packages/typir/lib/kinds/function/function-type.js'; +import { isFunctionDeclaration, isMemberCall, LoxProgram } from '../src/language/generated/ast.js'; +import { assertTrue, assertType } from '../../../packages/typir/lib/utils/utils.js'; +import { isType } from '../../../packages/typir/lib/graph/type-node.js'; +import { isPrimitiveType } from '../../../packages/typir/lib/kinds/primitive/primitive-type.js'; describe('Test type checking for user-defined functions', () => { test('function: return value and return type must match', async () => { await validateLox('fun myFunction1() : boolean { return true; }', 0); - await validateLox('fun myFunction2() : boolean { return 2; }', 1); + await validateLox('fun myFunction2() : boolean { return 2; }', + "The expression '2' of type 'number' is not usable as return value for the function 'myFunction2' with return type 'boolean'."); await validateLox('fun myFunction3() : number { return 2; }', 0); - await validateLox('fun myFunction4() : number { return true; }', 1); + await validateLox('fun myFunction4() : number { return true; }', + "The expression 'true' of type 'boolean' is not usable as return value for the function 'myFunction4' with return type 'number'."); expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction1', 'myFunction2', 'myFunction3', 'myFunction4', ...operatorNames); }); @@ -23,7 +29,10 @@ describe('Test type checking for user-defined functions', () => { await validateLox(` fun myFunction() : boolean { return true; } fun myFunction() : number { return 2; } - `, 2); + `, [ + 'Declared functions need to be unique (myFunction()).', + 'Declared functions need to be unique (myFunction()).', + ]); expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); // the types are different nevertheless! }); @@ -31,7 +40,10 @@ describe('Test type checking for user-defined functions', () => { await validateLox(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(other: boolean) : boolean { return true; } - `, 2); + `, [ + 'Declared functions need to be unique (myFunction(boolean)).', + 'Declared functions need to be unique (myFunction(boolean)).', + ]); expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', ...operatorNames); // but both functions have the same type! }); @@ -39,8 +51,47 @@ describe('Test type checking for user-defined functions', () => { await validateLox(` fun myFunction(input: boolean) : boolean { return true; } fun myFunction(input: number) : boolean { return true; } - `, 0); + `, []); expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); }); + test('overloaded function: check correct type inference and cross-references', async () => { + const rootNode = (await validateLox(` + fun myFunction(input: number) : number { return 987; } + fun myFunction(input: boolean) : boolean { return true; } + myFunction(123); + myFunction(false); + `, [])).parseResult.value as LoxProgram; + expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); + + // check type inference + cross-reference of the two method calls + expect(rootNode.elements).toHaveLength(4); + + // Call 1 should be number + const call1Node = rootNode.elements[2]; + // check cross-reference + assertTrue(isMemberCall(call1Node)); + const method1 = call1Node.element?.ref; + assertTrue(isFunctionDeclaration(method1)); + expect(method1.returnType.primitive).toBe('number'); + // check type inference + const call1Type = loxServices.typir.Inference.inferType(call1Node); + expect(isType(call1Type)).toBeTruthy(); + assertType(call1Type, isPrimitiveType); + expect(call1Type.getName()).toBe('number'); + + // Call 2 should be boolean + const call2Node = rootNode.elements[3]; + // check cross-reference + assertTrue(isMemberCall(call2Node)); + const method2 = call2Node.element?.ref; + assertTrue(isFunctionDeclaration(method2)); + expect(method2.returnType.primitive).toBe('boolean'); + // check type inference + const call2Type = loxServices.typir.Inference.inferType(call2Node); + expect(isType(call2Type)).toBeTruthy(); + assertType(call2Type, isPrimitiveType); + expect(call2Type.getName()).toBe('boolean'); + }); + }); diff --git a/examples/lox/test/lox-type-checking-method.test.ts b/examples/lox/test/lox-type-checking-method.test.ts index 214caf4..6b71468 100644 --- a/examples/lox/test/lox-type-checking-method.test.ts +++ b/examples/lox/test/lox-type-checking-method.test.ts @@ -4,10 +4,15 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { describe, test } from 'vitest'; -import { loxServices, validateLox } from './lox-type-checking-utils.js'; -import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; +import { describe, expect, test } from 'vitest'; +import { isType } from '../../../packages/typir/lib/graph/type-node.js'; import { isClassType } from '../../../packages/typir/lib/kinds/class/class-type.js'; +import { isFunctionType } from '../../../packages/typir/lib/kinds/function/function-type.js'; +import { isPrimitiveType } from '../../../packages/typir/lib/kinds/primitive/primitive-type.js'; +import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js'; +import { assertTrue, assertType } from '../../../packages/typir/lib/utils/utils.js'; +import { isMemberCall, isMethodMember, LoxProgram } from '../src/language/generated/ast.js'; +import { loxServices, operatorNames, validateLox } from './lox-type-checking-utils.js'; describe('Test type checking for methods of classes', () => { @@ -81,3 +86,65 @@ describe('Test type checking for methods of classes', () => { }); }); + +describe('Test overloaded methods', () => { + const methodDeclaration = ` + class MyClass { + method1(input: number): number { + return 987; + } + method1(input: boolean): boolean { + return true; + } + } + `; + + test('Calls with correct arguments', async () => { + const rootNode = (await validateLox(`${methodDeclaration} + var v = MyClass(); + v.method1(123); + v.method1(false); + `, [])).parseResult.value as LoxProgram; + expectTypirTypes(loxServices.typir, isClassType, 'MyClass'); + expectTypirTypes(loxServices.typir, isFunctionType, 'method1', 'method1', ...operatorNames); + + // check type inference + cross-reference of the two method calls + expect(rootNode.elements).toHaveLength(4); + + // Call 1 should be number + const call1Node = rootNode.elements[2]; + // check cross-reference + assertTrue(isMemberCall(call1Node)); + const method1 = call1Node.element?.ref; + assertTrue(isMethodMember(method1)); + expect(method1.returnType.primitive).toBe('number'); + // check type inference + const call1Type = loxServices.typir.Inference.inferType(call1Node); + expect(isType(call1Type)).toBeTruthy(); + assertType(call1Type, isPrimitiveType); + expect(call1Type.getName()).toBe('number'); + + // Call 2 should be boolean + const call2Node = rootNode.elements[3]; + // check cross-reference + assertTrue(isMemberCall(call2Node)); + const method2 = call2Node.element?.ref; + assertTrue(isMethodMember(method2)); + expect(method2.returnType.primitive).toBe('boolean'); + // check type inference + const call2Type = loxServices.typir.Inference.inferType(call2Node); + expect(isType(call2Type)).toBeTruthy(); + assertType(call2Type, isPrimitiveType); + expect(call2Type.getName()).toBe('boolean'); + }); + + test('Call with wrong argument', async () => { + await validateLox(`${methodDeclaration} + var v = MyClass(); + v.method1("wrong"); // the linker provides an Method here, but the arguments don't match + `, [ + "The given operands for the overloaded function 'method1' match the expected types only partially.", + ]); + }); + +}); diff --git a/examples/lox/test/lox-type-checking-utils.ts b/examples/lox/test/lox-type-checking-utils.ts index 94e9ef2..d4a6fbf 100644 --- a/examples/lox/test/lox-type-checking-utils.ts +++ b/examples/lox/test/lox-type-checking-utils.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { EmptyFileSystem } from 'langium'; +import { EmptyFileSystem, LangiumDocument } from 'langium'; import { parseDocument } from 'langium/test'; import { expectTypirTypes, isClassType, isFunctionType } from 'typir'; import { deleteAllDocuments } from 'typir-langium'; @@ -24,7 +24,7 @@ afterEach(async () => { expectTypirTypes(loxServices.typir, isFunctionType, ...operatorNames); }); -export async function validateLox(lox: string, errors: number | string | string[], warnings: number | string | string[] = 0) { +export async function validateLox(lox: string, errors: number | string | string[], warnings: number | string | string[] = 0): Promise { const document = await parseDocument(loxServices, lox.trim()); const diagnostics: Diagnostic[] = await loxServices.validation.DocumentValidator.validateDocument(document); @@ -35,6 +35,8 @@ export async function validateLox(lox: string, errors: number | string | string[ // warnings const diagnosticsWarnings: string[] = diagnostics.filter(d => d.severity === DiagnosticSeverity.Warning).map(d => d.message); checkIssues(diagnosticsWarnings, warnings); + + return document; } function checkIssues(diagnosticsErrors: string[], errors: number | string | string[]): void { diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 55c646e..9fe6427 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -177,8 +177,8 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { inferenceRuleForCalls: { filter: isMemberCall, matching: (call: MemberCall) => isFunctionDeclaration(call.element.ref) && call.explicitOperationCall && call.element.ref.name === functionName, - inputArguments: (call: MemberCall) => call.arguments - // TODO does OX support overloaded function declarations? add a scope provider for that ... + inputArguments: (call: MemberCall) => call.arguments // they are needed to validate, that the given arguments are assignable to the parameters + // Note that OX does not support overloaded function declarations for simplicity: Look into LOX to see how to handle overloaded functions and methods! }, associatedDomainElement: domainElement, });