Skip to content

Commit

Permalink
implemented overloaded functions and methods in LOX, more test cases,…
Browse files Browse the repository at this point in the history
… more comments
  • Loading branch information
JohannesMeierSE committed Dec 17, 2024
1 parent e967cbc commit 75537f0
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 19 deletions.
63 changes: 63 additions & 0 deletions examples/lox/src/language/lox-linker.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 3 additions & 1 deletion examples/lox/src/language/lox-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -45,7 +46,8 @@ export function createLoxModule(shared: LangiumSharedCoreServices): Module<LoxSe
createLoxTypirModule(shared), // custom Typir services for LOX
)),
references: {
ScopeProvider: (services) => new LoxScopeProvider(services)
ScopeProvider: (services) => new LoxScopeProvider(services),
Linker: (services) => new LoxLinker(services),
},
};
}
Expand Down
4 changes: 2 additions & 2 deletions examples/lox/src/language/lox-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
10 changes: 7 additions & 3 deletions examples/lox/src/language/lox-type-checking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => <ValidationMessageDetails>{
Expand Down Expand Up @@ -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;
});

Expand Down Expand Up @@ -214,7 +218,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator {
// inference ruleS(?) for objects/class literals conforming to the current class
inferenceRuleForLiteral: { // <InferClassLiteral<MemberCall>>
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: { // <InferClassLiteral<TypeReference>>
Expand All @@ -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,
});
Expand Down
63 changes: 57 additions & 6 deletions examples/lox/test/lox-type-checking-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,94 @@
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import { describe, test } from 'vitest';
import { describe, expect, test } from 'vitest';
import { loxServices, operatorNames, validateLox } from './lox-type-checking-utils.js';
import { expectTypirTypes } from '../../../packages/typir/lib/utils/test-utils.js';
import { isFunctionType } from '../../../packages/typir/lib/kinds/function/function-type.js';
import { isFunctionDeclaration, isMemberCall, LoxProgram } from '../src/language/generated/ast.js';
import { assertTrue, assertType } from '../../../packages/typir/lib/utils/utils.js';
import { isType } from '../../../packages/typir/lib/graph/type-node.js';
import { isPrimitiveType } from '../../../packages/typir/lib/kinds/primitive/primitive-type.js';

describe('Test type checking for user-defined functions', () => {

test('function: return value and return type must match', async () => {
await validateLox('fun myFunction1() : boolean { return true; }', 0);
await validateLox('fun myFunction2() : boolean { return 2; }', 1);
await validateLox('fun myFunction2() : boolean { return 2; }',
"The expression '2' of type 'number' is not usable as return value for the function 'myFunction2' with return type 'boolean'.");
await validateLox('fun myFunction3() : number { return 2; }', 0);
await validateLox('fun myFunction4() : number { return true; }', 1);
await validateLox('fun myFunction4() : number { return true; }',
"The expression 'true' of type 'boolean' is not usable as return value for the function 'myFunction4' with return type 'number'.");
expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction1', 'myFunction2', 'myFunction3', 'myFunction4', ...operatorNames);
});

test('overloaded function: different return types are not enough', async () => {
await validateLox(`
fun myFunction() : boolean { return true; }
fun myFunction() : number { return 2; }
`, 2);
`, [
'Declared functions need to be unique (myFunction()).',
'Declared functions need to be unique (myFunction()).',
]);
expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', 'myFunction', ...operatorNames); // the types are different nevertheless!
});

test('overloaded function: different parameter names are not enough', async () => {
await validateLox(`
fun myFunction(input: boolean) : boolean { return true; }
fun myFunction(other: boolean) : boolean { return true; }
`, 2);
`, [
'Declared functions need to be unique (myFunction(boolean)).',
'Declared functions need to be unique (myFunction(boolean)).',
]);
expectTypirTypes(loxServices.typir, isFunctionType, 'myFunction', ...operatorNames); // but both functions have the same type!
});

test('overloaded function: but different parameter types are fine', async () => {
await validateLox(`
fun myFunction(input: boolean) : boolean { return true; }
fun myFunction(input: number) : boolean { return true; }
`, 0);
`, []);
expectTypirTypes(loxServices.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');
});

});
73 changes: 70 additions & 3 deletions examples/lox/test/lox-type-checking-method.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand Down Expand Up @@ -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.",
]);
});

});
6 changes: 4 additions & 2 deletions examples/lox/test/lox-type-checking-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<LangiumDocument> {
const document = await parseDocument(loxServices, lox.trim());
const diagnostics: Diagnostic[] = await loxServices.validation.DocumentValidator.validateDocument(document);

Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions examples/ox/src/language/ox-type-checking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down

0 comments on commit 75537f0

Please sign in to comment.