diff --git a/.gitignore b/.gitignore index 6ebbeaa..788700e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,10 @@ .vscode-test/ coverage/ dist/ +bin/ lib/ out/ node_modules/ *.vsix *.tsbuildinfo -generated/ +/**/generated/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 7e86a2f..2006178 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,10 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/examples/ox", "${workspaceFolder}/examples/ox/examples" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/examples/ox/out/**/*.js" ] }, { @@ -21,6 +25,10 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/examples/lox", "${workspaceFolder}/examples/lox/examples" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/examples/lox/out/**/*.js" ] }, { diff --git a/examples/lox/.eslintrc.json b/examples/lox/.eslintrc.json deleted file mode 100644 index 8252235..0000000 --- a/examples/lox/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - } -} diff --git a/examples/lox/.gitignore b/examples/lox/.gitignore deleted file mode 100644 index b11b606..0000000 --- a/examples/lox/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/node_modules -/out -/**/generated diff --git a/examples/lox/.vscodeignore b/examples/lox/.vscodeignore deleted file mode 100644 index 4f97a26..0000000 --- a/examples/lox/.vscodeignore +++ /dev/null @@ -1,4 +0,0 @@ -.vscode/** -.vscode-test/** -.gitignore -langium-quickstart.md diff --git a/examples/lox/langium-config.json b/examples/lox/langium-config.json index e59c808..7e10b21 100644 --- a/examples/lox/langium-config.json +++ b/examples/lox/langium-config.json @@ -5,7 +5,7 @@ "grammar": "src/language/lox.langium", "fileExtensions": [".lox"], "textMate": { - "out": "syntaxes/lox.tmLanguage.json" + "out": "./syntaxes/lox.tmLanguage.json" } }], "out": "src/language/generated" diff --git a/examples/lox/src/language/lox-module.ts b/examples/lox/src/language/lox-module.ts index ac73166..3254ccf 100644 --- a/examples/lox/src/language/lox-module.ts +++ b/examples/lox/src/language/lox-module.ts @@ -4,14 +4,13 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { DefaultSharedCoreModuleContext, LangiumCoreServices, LangiumSharedCoreServices, Module, PartialLangiumCoreServices, createDefaultCoreModule, createDefaultSharedCoreModule, inject } from 'langium'; +import { Module, PartialLangiumCoreServices, createDefaultCoreModule, inject } from 'langium'; +import { DefaultSharedModuleContext, LangiumServices, LangiumSharedServices, createDefaultSharedModule } from 'langium/lsp'; +import { LangiumServicesForTypirBinding, createLangiumModuleForTypirBinding, initializeLangiumTypirServices } from 'typir-langium'; import { LoxGeneratedModule, LoxGeneratedSharedModule } from './generated/module.js'; import { LoxScopeProvider } from './lox-scope.js'; import { LoxValidationRegistry, LoxValidator } from './lox-validator.js'; -import { 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. @@ -54,6 +53,9 @@ export const LoxModule: Module = { - BinaryExpression: validator.checkBinaryOperationAllowed, VariableDeclaration: validator.checkVariableDeclaration, }; this.register(checks, validator); @@ -41,33 +37,4 @@ export class LoxValidator { } } - checkBinaryOperationAllowed(binary: BinaryExpression, accept: ValidationAcceptor): void { - const map = this.getTypeCache(); - const left = inferType(binary.left, map); - const right = inferType(binary.right, map); - if (!isLegalOperation(binary.operator, left, right)) { - // accept('error', `Cannot perform operation '${binary.operator}' on values of type '${typeToString(left)}' and '${typeToString(right)}'.`, { - // node: binary - // }) - } else if (binary.operator === '=') { - // if (!isAssignable(right, left)) { - // accept('error', `Type '${typeToString(right)}' is not assignable to type '${typeToString(left)}'.`, { - // node: binary, - // property: 'right' - // }) - // } - } else if (['==', '!='].includes(binary.operator)) { - // if (!isAssignable(right, left)) { - // accept('warning', `This comparison will always return '${binary.operator === '==' ? 'false' : 'true'}' as types '${typeToString(left)}' and '${typeToString(right)}' are not compatible.`, { - // node: binary, - // property: 'operator' - // }); - // } - } - } - - private getTypeCache(): Map { - return new Map(); - } - } diff --git a/examples/lox/src/language/type-system/assignment.ts b/examples/lox/src/language/type-system/assignment.ts index 0a2275a..0bad4b8 100644 --- a/examples/lox/src/language/type-system/assignment.ts +++ b/examples/lox/src/language/type-system/assignment.ts @@ -3,8 +3,8 @@ * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { isClassType, isFunctionType, isNilType, TypeDescription } from "./descriptions.js"; -import { getClassChain } from "./infer.js"; +import { isClassType, isFunctionType, isNilType, TypeDescription } from './descriptions.js'; +import { getClassChain } from './infer.js'; export function isAssignable(from: TypeDescription, to: TypeDescription): boolean { if (isClassType(from)) { diff --git a/examples/lox/src/language/type-system/descriptions.ts b/examples/lox/src/language/type-system/descriptions.ts index 1dda49e..93a8720 100644 --- a/examples/lox/src/language/type-system/descriptions.ts +++ b/examples/lox/src/language/type-system/descriptions.ts @@ -4,8 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode } from "langium"; -import { BooleanLiteral, Class, NumberLiteral, StringLiteral } from "../generated/ast.js" +import { AstNode } from 'langium'; +import { BooleanLiteral, Class, NumberLiteral, StringLiteral } from '../generated/ast.js'; export type TypeDescription = | NilTypeDescription @@ -18,83 +18,83 @@ export type TypeDescription = | ErrorType; export interface NilTypeDescription { - readonly $type: "nil" + readonly $type: 'nil' } export function createNilType(): NilTypeDescription { return { - $type: "nil" + $type: 'nil' }; } export function isNilType(item: TypeDescription): item is NilTypeDescription { - return item.$type === "nil"; + return item.$type === 'nil'; } export interface VoidTypeDescription { - readonly $type: "void" + readonly $type: 'void' } export function createVoidType(): VoidTypeDescription { return { - $type: "void" - } + $type: 'void' + }; } export function isVoidType(item: TypeDescription): item is VoidTypeDescription { - return item.$type === "void"; + return item.$type === 'void'; } export interface BooleanTypeDescription { - readonly $type: "boolean" + readonly $type: 'boolean' readonly literal?: BooleanLiteral } export function createBooleanType(literal?: BooleanLiteral): BooleanTypeDescription { return { - $type: "boolean", + $type: 'boolean', literal }; } export function isBooleanType(item: TypeDescription): item is BooleanTypeDescription { - return item.$type === "boolean"; + return item.$type === 'boolean'; } export interface StringTypeDescription { - readonly $type: "string" + readonly $type: 'string' readonly literal?: StringLiteral } export function createStringType(literal?: StringLiteral): StringTypeDescription { return { - $type: "string", + $type: 'string', literal }; } export function isStringType(item: TypeDescription): item is StringTypeDescription { - return item.$type === "string"; + return item.$type === 'string'; } export interface NumberTypeDescription { - readonly $type: "number", + readonly $type: 'number', readonly literal?: NumberLiteral } export function createNumberType(literal?: NumberLiteral): NumberTypeDescription { return { - $type: "number", + $type: 'number', literal }; } export function isNumberType(item: TypeDescription): item is NumberTypeDescription { - return item.$type === "number"; + return item.$type === 'number'; } export interface FunctionTypeDescription { - readonly $type: "function" + readonly $type: 'function' readonly returnType: TypeDescription readonly parameters: FunctionParameter[] } @@ -106,48 +106,48 @@ export interface FunctionParameter { export function createFunctionType(returnType: TypeDescription, parameters: FunctionParameter[]): FunctionTypeDescription { return { - $type: "function", + $type: 'function', parameters, returnType }; } export function isFunctionType(item: TypeDescription): item is FunctionTypeDescription { - return item.$type === "function"; + return item.$type === 'function'; } export interface ClassTypeDescription { - readonly $type: "class" + readonly $type: 'class' readonly literal: Class } export function createClassType(literal: Class): ClassTypeDescription { return { - $type: "class", + $type: 'class', literal }; } export function isClassType(item: TypeDescription): item is ClassTypeDescription { - return item.$type === "class"; + return item.$type === 'class'; } export interface ErrorType { - readonly $type: "error" + readonly $type: 'error' readonly source?: AstNode readonly message: string } export function createErrorType(message: string, source?: AstNode): ErrorType { return { - $type: "error", + $type: 'error', message, source }; } export function isErrorType(item: TypeDescription): item is ErrorType { - return item.$type === "error"; + return item.$type === 'error'; } export function typeToString(item: TypeDescription): string { diff --git a/examples/lox/src/language/type-system/infer.ts b/examples/lox/src/language/type-system/infer.ts index 7c8c3cf..8d80eb4 100644 --- a/examples/lox/src/language/type-system/infer.ts +++ b/examples/lox/src/language/type-system/infer.ts @@ -4,9 +4,9 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { AstNode } from "langium"; -import { BinaryExpression, Class, isBinaryExpression, isBooleanLiteral, isClass, isFieldMember, isFunctionDeclaration, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, MemberCall, TypeReference } from "../generated/ast.js"; -import { createBooleanType, createClassType, createErrorType, createFunctionType, createNilType, createNumberType, createStringType, createVoidType, isFunctionType, isStringType, TypeDescription } from "./descriptions.js"; +import { AstNode } from 'langium'; +import { BinaryExpression, Class, isBinaryExpression, isBooleanLiteral, isClass, isFieldMember, isFunctionDeclaration, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, MemberCall, TypeReference } from '../generated/ast.js'; +import { createBooleanType, createClassType, createErrorType, createFunctionType, createNilType, createNumberType, createStringType, createVoidType, isFunctionType, isStringType, TypeDescription } from './descriptions.js'; export function inferType(node: AstNode | undefined, cache: Map): TypeDescription { let type: TypeDescription | undefined; 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 be3a808..72dc663 100644 --- a/examples/lox/src/language/type-system/lox-type-checking.ts +++ b/examples/lox/src/language/type-system/lox-type-checking.ts @@ -6,11 +6,12 @@ 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 { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, ParameterDetails, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation } 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, isMemberCall, isMethodMember, isNilLiteral, isNumberLiteral, isParameter, isPrintStatement, isReturnStatement, isStringLiteral, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from '../generated/ast.js'; +import { BinaryExpression, FunctionDeclaration, MemberCall, MethodMember, 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'; +/* eslint-disable @typescript-eslint/no-unused-vars */ export class LoxTypeCreator extends AbstractLangiumTypeCreator { protected readonly typir: TypirServices; protected readonly primitiveKind: PrimitiveKind; @@ -29,7 +30,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { typing: 'Nominal', }); this.anyKind = new TopKind(this.typir); - this.operators = this.typir.operators; + this.operators = this.typir.operators; } onInitialize(): void { @@ -105,7 +106,6 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { 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 @@ -114,12 +114,12 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { if (isClass(ref)) { return InferenceRuleNotApplicable; // not required anymore } else if (isClassMember(ref)) { - return InferenceRuleNotApplicable!; // TODO + return InferenceRuleNotApplicable; // TODO } else if (isMethodMember(ref)) { - return InferenceRuleNotApplicable!; // TODO + return InferenceRuleNotApplicable; // TODO } else if (isVariableDeclaration(ref)) { // use variables inside expressions! - return ref.type!; + return ref; // infer the Typir type from the variable, see the case below } else if (isParameter(ref)) { // use parameters inside expressions return ref.type; @@ -148,7 +148,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { }); // some explicit validations for typing issues with Typir (replaces corresponding functions in the OxValidator!) - this.typir.validation.collector.addValidationRules( + this.typir.validation.collector.addValidationRule( (node: unknown, typir: TypirServices) => { if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { return typir.validation.constraints.ensureNodeIsAssignable(node.condition, typeBool, @@ -165,7 +165,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { } 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}'`, + message: `The expression '${node.right.$cstNode?.text}' of type '${actual.name}' is not assignable to '${node.left.$cstNode?.text}' with type '${expected.name}'`, domainProperty: 'value' }); } if (isBinaryExpression(node) && (node.operator === '==' || node.operator === '!=')) { @@ -176,11 +176,11 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { 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}'.`, + const callableDeclaration: FunctionDeclaration | MethodMember | undefined = AstUtils.getContainerOfType(node, node => isFunctionDeclaration(node) || isMethodMember(node)); + if (callableDeclaration && callableDeclaration.returnType.primitive && callableDeclaration.returnType.primitive !== 'void' && node.value) { + // the return value must fit to the return type of the function / method + return typir.validation.constraints.ensureNodeIsAssignable(node.value, callableDeclaration.returnType, (actual, expected) => { + message: `The expression '${node.value!.$cstNode?.text}' of type '${actual.name}' is not usable as return value for the function '${callableDeclaration.name}' with return type '${expected.name}'.`, domainProperty: 'value' }); } } @@ -188,11 +188,14 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { } ); - // validate unique declarations - this.typir.validation.collector.addValidationRulesWithBeforeAndAfter( - new UniqueFunctionValidation(this.typir, isFunctionDeclaration), - new UniqueClassValidation(this.typir, isClass), - ); + // check for unique function declarations + this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); + // check for unique class declarations + this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueClassValidation(this.typir, isClass)); + // check for unique method declarations + this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueMethodValidation(this.typir, + (node) => isMethodMember(node), // MethodMembers could have other $containers? + (method, _type) => method.$container)); } onNewAstNode(node: AstNode): void { @@ -200,24 +203,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // function types: they have to be updated after each change of the Langium document, since they are derived from FunctionDeclarations! if (isFunctionDeclaration(node)) { - const functionName = node.name; - // define function type - this.functionKind.getOrCreateFunctionType({ - functionName, - outputParameter: { name: FUNCTION_MISSING_NAME, type: node.returnType }, - inputParameters: node.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! - /** 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 - }, - }); + this.functionKind.getOrCreateFunctionType(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar } // TODO support lambda (type references)! @@ -232,15 +218,18 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { // class types (nominal typing): if (isClass(node)) { const className = node.name; - this.classKind.getOrCreateClassType({ // TODO check for duplicates! + const classType = this.classKind.getOrCreateClassType({ className, superClasses: node.superClass?.ref, // note that type inference is used here; TODO delayed fields: node.members - .filter(m => isFieldMember(m)).map(f => f as FieldMember) // only Fields, no Methods + .filter(isFieldMember) // only Fields, no Methods .map(f => { name: f.name, type: f.type, // note that type inference is used here; TODO delayed }), + methods: node.members + .filter(isMethodMember) // only Methods, no Fields + .map(member => createFunctionDetails(member)), // same logic as for functions, since the LOX grammar defines them very similar // inference rule for declaration inferenceRuleForDeclaration: (domainElement: unknown) => domainElement === node, // inference ruleS(?) for objects/class literals conforming to the current class @@ -258,10 +247,35 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator { inferenceRuleForFieldAccess: (domainElement: unknown) => isMemberCall(domainElement) && isFieldMember(domainElement.element?.ref) && domainElement.element!.ref.$container === node ? domainElement.element!.ref.name : 'N/A', // as an alternative, use 'InferenceRuleNotApplicable' instead, what should we recommend? }); + + // TODO conversion 'nil' to classes ('TopClass')! + // any class !== all classes; here we want to say, that 'nil' is assignable to each concrete Class type! + // this.typir.conversion.markAsConvertible(typeNil, this.classKind.getOrCreateTopClassType({}), 'IMPLICIT_EXPLICIT'); + this.typir.conversion.markAsConvertible(this.primitiveKind.getPrimitiveType({ primitiveName: 'nil' })!, classType, 'IMPLICIT_EXPLICIT'); } } } +function createFunctionDetails(node: FunctionDeclaration | MethodMember): CreateFunctionTypeDetails { + const callableName = node.name; + return { + functionName: callableName, + outputParameter: { name: NO_PARAMETER_NAME, type: node.returnType }, + inputParameters: node.parameters.map(p => ({ name: p.name, type: p.type })), + // inference rule for function declaration: + inferenceRuleForDeclaration: (domainElement: unknown) => domainElement === node, // only the current function/method declaration matches! + /** inference rule for funtion/method 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) || isMethodMember(domainElement.element?.ref)) + && domainElement.element!.ref.name === callableName, + inputArguments: (domainElement: MemberCall) => domainElement.arguments + }, + }; +} export function createLoxTypirModule(langiumServices: LangiumSharedServices): Module { return { diff --git a/examples/lox/src/language/type-system/operator.ts b/examples/lox/src/language/type-system/operator.ts index 58d1676..01e4a98 100644 --- a/examples/lox/src/language/type-system/operator.ts +++ b/examples/lox/src/language/type-system/operator.ts @@ -4,7 +4,7 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { TypeDescription } from "./descriptions.js"; +import { TypeDescription } from './descriptions.js'; export function isLegalOperation(operator: string, left: TypeDescription, right?: TypeDescription): boolean { if (operator === '+') { @@ -12,7 +12,7 @@ export function isLegalOperation(operator: string, left: TypeDescription, right? return left.$type === 'number'; } return (left.$type === 'number' || left.$type === 'string') - && (right.$type === 'number' || right.$type === 'string') + && (right.$type === 'number' || right.$type === 'string'); } else if (['-', '/', '*', '%', '<', '<=', '>', '>='].includes(operator)) { if (!right) { return left.$type === 'number'; diff --git a/examples/lox/test/lox-type-checking.test.ts b/examples/lox/test/lox-type-checking.test.ts index f211938..e23fd85 100644 --- a/examples/lox/test/lox-type-checking.test.ts +++ b/examples/lox/test/lox-type-checking.test.ts @@ -108,6 +108,62 @@ describe('Explicitly test type checking for LOX', () => { await validate('var myVar : number = 2 + (2 * false);', 1); }); + test('Variables without explicit type: assignment', async () => { + await validate(` + var min = 14; + var max = 22; + max = min; + `, 0); + }); + + test('Variables without explicit type: assign expression to var without type', async () => { + await validate(` + var min = 14; + var max = 22; + var sum = min + max; + `, 0); + }); + + test('Variables without explicit type: assign expression to var with type', async () => { + await validate(` + var min = 14; + var max = 22; + var sum : number = min + max; + `, 0); + }); + + test('Variables without explicit type: assign var again with expression of overloaded operator +', async () => { + await validate(` + var min = 14; + var max = 22; + max = min + max; + `, 0); + }); + + test('Variables without explicit type: assign var again with expression of overloaded operator -', async () => { + await validate(` + var min = 14; + var max = 22; + max = min - max; + `, 0); + }); + + test('Variables without explicit type: assign var again with expression of not overloaded operator *', async () => { + await validate(` + var min = 14; + var max = 22; + max = min * max; + `, 0); + }); + + test('Variables without explicit type: used in function', async () => { + await validate(` + var min = 14; + var max = 22; + var average = (min + max) / 2; + `, 0); + }); + describe('Class literals', () => { test('Class literals 1', async () => { await validate(` @@ -183,6 +239,57 @@ describe('Explicitly test type checking for LOX', () => { `, 3); }); + test('Class methods: OK', async () => await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } + } + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(456); + `, 0)); + + test('Class methods: wrong return value', async () => await validate(` + class MyClass1 { + method1(input: number): number { + return true; + } + } + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(456); + `, 1)); + + test('Class methods: method return type does not fit to variable type', async () => await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } + } + var v1: MyClass1 = MyClass1(); + var v2: boolean = v1.method1(456); + `, 1)); + + test('Class methods: value for input parameter does not fit to the type of the input parameter', async () => await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } + } + var v1: MyClass1 = MyClass1(); + var v2: number = v1.method1(true); + `, 1)); + + test('Class methods: methods are not distinguishable', async () => await validate(` + class MyClass1 { + method1(input: number): number { + return 123; + } + method1(another: number): boolean { + return true; + } + } + `, 2)); // both methods need to be marked + }); describe('Test internal validation of Typir for cycles in the class inheritance hierarchy', () => { @@ -210,6 +317,82 @@ describe('Test internal validation of Typir for cycles in the class inheritance }); }); +describe('LOX', () => { + // this test case will work after having the support for cyclic type definitions, since it will solve also issues with topological order of type definitions + test.todo('complete with difficult order of classes', async () => await validate(` + class SuperClass { + a: number + } + + class SubClass < SuperClass { + // Nested class + nested: NestedClass + } + + class NestedClass { + field: string + method(): string { + return "execute this"; + } + } + + // Constructor call + var x = SubClass(); + // Assigning nil to a class type + var nilTest = SubClass(); + nilTest = nil; + + // Accessing members of a class + var value = x.nested.method() + "wasd"; + print value; + + // Accessing members of a super class + var superValue = x.a; + print superValue; + + // Assigning a subclass to a super class + var superType: SuperClass = x; + print superType.a; + `, 0)); + + test('complete with easy order of classes', async () => await validate(` + class SuperClass { + a: number + } + + class NestedClass { + field: string + method(): string { + return "execute this"; + } + } + + class SubClass < SuperClass { + // Nested class + nested: NestedClass + } + + + // Constructor call + var x = SubClass(); + // Assigning nil to a class type + var nilTest = SubClass(); + nilTest = nil; + + // Accessing members of a class + var value = x.nested.method() + "wasd"; + print value; + + // Accessing members of a super class + var superValue = x.a; + print superValue; + + // Assigning a subclass to a super class + var superType: SuperClass = x; + print superType.a; + `, 0)); +}); + async function validate(lox: string, errors: number, warnings: number = 0) { const document = await parseDocument(loxServices, lox.trim()); const diagnostics: Diagnostic[] = await loxServices.validation.DocumentValidator.validateDocument(document); diff --git a/examples/ox/examples/EclipseCon2024.ox b/examples/ox/examples/EclipseCon2024.ox new file mode 100644 index 0000000..0966cb9 --- /dev/null +++ b/examples/ox/examples/EclipseCon2024.ox @@ -0,0 +1,43 @@ +// variables: numbers, booleans +var varNumber: number = 23; +var varBoolean: boolean = true; + +// Operators +var add: number = 23 + 41; +var multiply: number = 13 * 4; +var negateMe: number = -add; +var less: boolean = add < multiply; +var isTrue: boolean = !false; +var andTrue: boolean = isTrue and !true; + +// nesting expressions +var average: number = (add + multiply) / 2; + +// Variables +add = 5; + +// If branching +if (average < 5) { + print 23; + if (multiply < 30 and add > 3) { + add = add + 15; + } +} + +// Functions +fun inc(a: number): number { + return a + 1; +} +fun inc(b: boolean): number { + return b + 1; +} + +fun printSum(a: number, b: number): void { + print inc(4) + inc(inc(b)); +} + +// print, call functions +// print printSum(3, 2); + +// print true; +print 123; diff --git a/examples/ox/examples/LangDev2024.ox b/examples/ox/examples/LangDev2024.ox new file mode 100644 index 0000000..8baba1a --- /dev/null +++ b/examples/ox/examples/LangDev2024.ox @@ -0,0 +1,77 @@ +// variables: numbers, booleans (void) +var n: number = 12; +var b: boolean = true; + +// Arithmetics +var add: number = 23 + 41; +var subtract: number = 13 - 4; +var multiply: number = 13 * 4; +var divide: number = 62 / 2; +var fractional: number = 61 / 3; + +var negateMe: number = -add; + +// Comparison and equality +var less: boolean = add < subtract; +var more: boolean = multiply > divide; + +var equality: boolean = add == subtract; +var inequality: boolean = multiply != divide; + +// Unary logical operator +var isTrue: boolean = !false; +var isFalse: boolean = !true; + +// Binary logical operator +var andTrue: boolean = isTrue and !isFalse; +var orFalse: boolean = !isTrue or isFalse; + +// Precedence and grouping +var min: number = 14; +var max: number = 22; +var average: number = (min + max) / 2; + +// Variables +// Can reassign an existing variable +min = 5; + +// If branching +var kk: number = average * 5; +if (average > 5) { + print 23; + if (max < 30 and min > 3) { + min = min + 15; + } +} else { + print -12; +} + +// While loops +var a: number = 1; +while (a < 10) { + print a; + a = a + 1; +} + +// Functions +fun inc(a: number): number { + return a + 1; +} +fun inc(a: boolean): boolean { + return !a; +} + +fun printSum(a: number, b: number): void { + print inc(true) + inc(inc(b)); +} + +fun returnSum(a: number, b: number): number { + return a + b; +} + +printSum(3, 2); + +print returnSum(32.3, 123.5); + +print 12; +print true; diff --git a/examples/ox/src/language/ox-module.ts b/examples/ox/src/language/ox-module.ts index d33dc7c..7861bef 100644 --- a/examples/ox/src/language/ox-module.ts +++ b/examples/ox/src/language/ox-module.ts @@ -48,6 +48,9 @@ export const OxModule: Module { - // ... and for variable declarations + // ... variable declarations if (isVariableDeclaration(domainElement)) { - return domainElement.type; + 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; }); // explicit validations for typing issues, realized with Typir (which replaced corresponding functions in the OxValidator!) - this.typir.validation.collector.addValidationRules( + this.typir.validation.collector.addValidationRule( (node: unknown, typir: TypirServices) => { if (isIfStatement(node) || isWhileStatement(node) || isForStatement(node)) { return typir.validation.constraints.ensureNodeIsAssignable(node.condition, typeBool, @@ -158,7 +162,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { ); // check for unique function declarations - this.typir.validation.collector.addValidationRulesWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); + this.typir.validation.collector.addValidationRuleWithBeforeAndAfter(new UniqueFunctionValidation(this.typir, isFunctionDeclaration)); } onNewAstNode(domainElement: AstNode): void { @@ -170,7 +174,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator { 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: domainElement.returnType }, + outputParameter: { name: NO_PARAMETER_NAME, type: domainElement.returnType }, inputParameters: domainElement.parameters.map(p => ({ name: p.name, type: p.type })), // inference rule for function declaration: inferenceRuleForDeclaration: (node: unknown) => node === domainElement, // only the current function declaration matches! diff --git a/examples/ox/src/language/ox-validator.ts b/examples/ox/src/language/ox-validator.ts index 6a62c24..1c9a320 100644 --- a/examples/ox/src/language/ox-validator.ts +++ b/examples/ox/src/language/ox-validator.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { AstUtils, type ValidationAcceptor, type ValidationChecks } from 'langium'; -import { isFunctionDeclaration, type OxAstType, type ReturnStatement } from './generated/ast.js'; +import { isFunctionDeclaration, isVariableDeclaration, OxElement, VariableDeclaration, type OxAstType, type ReturnStatement } from './generated/ast.js'; import type { OxServices } from './ox-module.js'; /** @@ -16,6 +16,8 @@ export function registerValidationChecks(services: OxServices) { const validator = services.validation.OxValidator; const checks: ValidationChecks = { ReturnStatement: validator.checkReturnTypeIsCorrect, + OxProgram: validator.checkUniqueVariableNames, + Block: validator.checkUniqueVariableNames, }; registry.register(checks, validator); } @@ -48,4 +50,29 @@ export class OxValidator { } } + checkUniqueVariableNames(block: { elements: OxElement[]}, accept: ValidationAcceptor): void { + const variables: Map = new Map(); + for (const v of block.elements) { + if (isVariableDeclaration(v)) { + const key = v.name; + let entries = variables.get(key); + if (!entries) { + entries = []; + variables.set(key, entries); + } + entries.push(v); + } + } + for (const [name, vars] of variables.entries()) { + if (vars.length >= 2) { + for (const v of vars) { + accept('error', 'Variables need to have unique names: ' + name, { + node: v, + property: 'name' + }); + } + } + } + } + } diff --git a/examples/ox/src/language/ox.langium b/examples/ox/src/language/ox.langium index 1295500..3dae253 100644 --- a/examples/ox/src/language/ox.langium +++ b/examples/ox/src/language/ox.langium @@ -4,19 +4,19 @@ entry OxProgram: elements+=OxElement*; OxElement: - Block | + Block | IfStatement | WhileStatement | ForStatement | FunctionDeclaration | - VariableDeclaration ';' | + VariableDeclaration ';' | AssignmentStatement ';' | - PrintStatement ';' | - ReturnStatement ';' | + PrintStatement ';' | + ReturnStatement ';' | Expression ';' ; -IfStatement: +IfStatement: 'if' '(' condition=Expression ')' block=Block ('else' elseBlock=Block)? ; diff --git a/examples/ox/test/ox-type-checking.test.ts b/examples/ox/test/ox-type-checking.test.ts index d22e5c2..7080e7a 100644 --- a/examples/ox/test/ox-type-checking.test.ts +++ b/examples/ox/test/ox-type-checking.test.ts @@ -104,7 +104,8 @@ describe('Explicitly test type checking for OX', () => { `, 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 () => { + // TODO this test case needs to be investigated in more detail + test.todo('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 false; }', 2); // now, both functions should be marked as "duplicate" }); diff --git a/packages/typir/src/features/inference.ts b/packages/typir/src/features/inference.ts index a8b531b..94a0de5 100644 --- a/packages/typir/src/features/inference.ts +++ b/packages/typir/src/features/inference.ts @@ -109,12 +109,12 @@ export interface TypeInferenceCollector { 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; + protected readonly services: TypirServices; constructor(services: TypirServices) { - this.typir = services; + this.services = services; this.domainElementInference = services.caching.domainElementInference; - this.typir.graph.addListener(this); + this.services.graph.addListener(this); } addInferenceRule(rule: TypeInferenceRule, boundToType?: Type): void { @@ -167,7 +167,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty for (const rule of rules) { if (typeof rule === 'function') { // simple case without type inference for children - const ruleResult: TypeInferenceResultWithoutInferringChildren = rule(domainElement, this.typir); + const ruleResult: TypeInferenceResultWithoutInferringChildren = rule(domainElement, this.services); this.checkForError(ruleResult); const checkResult = this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems); if (checkResult) { @@ -178,12 +178,12 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty } } else if (typeof rule === 'object') { // more complex case with inferring the type for children - const ruleResult: TypeInferenceResultWithInferringChildren = rule.inferTypeWithoutChildren(domainElement, this.typir); + const ruleResult: TypeInferenceResultWithInferringChildren = rule.inferTypeWithoutChildren(domainElement, this.services); if (Array.isArray(ruleResult)) { // this rule might match => continue applying this rule // resolve the requested child types const childElements = ruleResult; - const childTypes: Array = childElements.map(child => this.inferType(child)); + const childTypes: Array = childElements.map(child => this.services.inference.inferType(child)); // check, whether inferring the children resulted in some other inference problems const childTypeProblems: InferenceProblem[] = []; for (let i = 0; i < childTypes.length; i++) { @@ -208,7 +208,7 @@ export class DefaultTypeInferenceCollector implements TypeInferenceCollector, Ty }); } else { // the types of all children are successfully inferred - const finalInferenceResult = rule.inferTypeWithChildrensTypes(domainElement, childTypes as Type[], this.typir); + const finalInferenceResult = rule.inferTypeWithChildrensTypes(domainElement, childTypes as Type[], this.services); if (isType(finalInferenceResult)) { // type is inferred! return finalInferenceResult; diff --git a/packages/typir/src/features/operator.ts b/packages/typir/src/features/operator.ts index 8bb9f13..a900eb3 100644 --- a/packages/typir/src/features/operator.ts +++ b/packages/typir/src/features/operator.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { Type } from '../graph/type-node.js'; -import { FUNCTION_MISSING_NAME, FunctionKind, FunctionKindName, isFunctionKind } from '../kinds/function-kind.js'; +import { FunctionKind, FunctionKindName, isFunctionKind, NO_PARAMETER_NAME } from '../kinds/function-kind.js'; import { TypirServices } from '../typir.js'; import { NameTypePair, Types } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; @@ -151,15 +151,12 @@ export class DefaultOperatorManager implements OperatorManager { createGenericOperator(typeDetails: GenericOperatorDetails): Type { // define/register the wanted operator as "special" function - - // ensure, that Typir uses the predefined 'function' kind - const kind = this.services.kinds.get(FunctionKindName); - const functionKind = isFunctionKind(kind) ? kind : new FunctionKind(this.services); + const functionKind = this.getFunctionKind(); // create the operator as type of kind 'function' const newOperatorType = functionKind.createFunctionType({ functionName: typeDetails.name, - outputParameter: { name: FUNCTION_MISSING_NAME, type: typeDetails.outputType }, + outputParameter: { name: NO_PARAMETER_NAME, type: typeDetails.outputType }, inputParameters: typeDetails.inputParameter, inferenceRuleForDeclaration: undefined, // operators have no declaration in the code => no inference rule for the operator declaration! inferenceRuleForCalls: typeDetails.inferenceRule // but infer the operator when the operator is called! @@ -175,4 +172,10 @@ export class DefaultOperatorManager implements OperatorManager { return newOperatorType; } + + protected getFunctionKind(): FunctionKind { + // ensure, that Typir uses the predefined 'function' kind + const kind = this.services.kinds.get(FunctionKindName); + return isFunctionKind(kind) ? kind : new FunctionKind(this.services); + } } diff --git a/packages/typir/src/features/validation.ts b/packages/typir/src/features/validation.ts index 6a7b14b..a4a9240 100644 --- a/packages/typir/src/features/validation.ts +++ b/packages/typir/src/features/validation.ts @@ -4,6 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +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 { TypirProblem, isSpecificTypirProblem } from '../utils/utils-definitions.js'; @@ -145,51 +147,101 @@ export interface ValidationCollector { validate(domainElement: unknown): ValidationProblem[]; validateAfter(domainRoot: unknown): ValidationProblem[]; - addValidationRules(...rules: ValidationRule[]): void; - addValidationRulesWithBeforeAndAfter(...rules: ValidationRuleWithBeforeAfter[]): void; + /** + * Registers a validation rule. + * @param rule a new validation rule + * @param boundToType an optional type, if the new validation rule is dedicated for exactly this type. + * If the given type is removed from the type system, this rule will be automatically removed as well. + */ + addValidationRule(rule: ValidationRule, boundToType?: Type): void; + /** + * Registers a validation rule which will be called once before and once after the whole validation. + * @param rule a new validation rule + * @param boundToType an optional type, if the new validation rule is dedicated for exactly this type. + * If the given type is removed from the type system, this rule will be automatically removed as well. + */ + addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void; } -export class DefaultValidationCollector implements ValidationCollector { +export class DefaultValidationCollector implements ValidationCollector, TypeGraphListener { protected readonly services: TypirServices; - readonly validationRules: ValidationRule[] = []; - readonly validationRulesBeforeAfter: ValidationRuleWithBeforeAfter[] = []; + protected readonly validationRules: Map = new Map(); // type identifier (otherwise '') -> validation rules + protected readonly validationRulesBeforeAfter: Map = new Map(); // type identifier (otherwise '') -> validation rules constructor(services: TypirServices) { this.services = services; + this.services.graph.addListener(this); } validateBefore(domainRoot: unknown): ValidationProblem[] { const problems: ValidationProblem[] = []; - for (const rule of this.validationRulesBeforeAfter) { - problems.push(...rule.beforeValidation(domainRoot, this.services)); + for (const rules of this.validationRulesBeforeAfter.values()) { + for (const rule of rules) { + 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 rules of this.validationRules.values()) { + for (const rule of rules) { + problems.push(...rule(domainElement, this.services)); + } } - for (const rule of this.validationRulesBeforeAfter) { - problems.push(...rule.validation(domainElement, this.services)); + for (const rules of this.validationRulesBeforeAfter.values()) { + for (const rule of rules) { + 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)); + for (const rules of this.validationRulesBeforeAfter.values()) { + for (const rule of rules) { + problems.push(...rule.afterValidation(domainRoot, this.services)); + } } return problems; } - addValidationRules(...rules: ValidationRule[]): void { - this.validationRules.push(...rules); + addValidationRule(rule: ValidationRule, boundToType?: Type): void { + const key = boundToType?.identifier ?? ''; + let rules = this.validationRules.get(key); + if (!rules) { + rules = []; + this.validationRules.set(key, rules); + } + rules.push(rule); } - addValidationRulesWithBeforeAndAfter(...rules: ValidationRuleWithBeforeAfter[]): void { - this.validationRulesBeforeAfter.push(...rules); + addValidationRuleWithBeforeAndAfter(rule: ValidationRuleWithBeforeAfter, boundToType?: Type): void { + const key = boundToType?.identifier ?? ''; + let rules = this.validationRulesBeforeAfter.get(key); + if (!rules) { + rules = []; + this.validationRulesBeforeAfter.set(key, rules); + } + rules.push(rule); + } + + + /* Get informed about deleted types in order to remove validation rules which are bound to them. */ + + addedType(_newType: Type): void { + // do nothing + } + removedType(type: Type): void { + this.validationRules.delete(type.identifier); + this.validationRulesBeforeAfter.delete(type.identifier); + } + addedEdge(_edge: TypeEdge): void { + // do nothing + } + removedEdge(_edge: TypeEdge): void { + // do nothing } } diff --git a/packages/typir/src/kinds/class-kind.ts b/packages/typir/src/kinds/class-kind.ts index 187693d..e86c868 100644 --- a/packages/typir/src/kinds/class-kind.ts +++ b/packages/typir/src/kinds/class-kind.ts @@ -14,6 +14,7 @@ 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 { CreateFunctionTypeDetails, FunctionKind, FunctionKindName, FunctionType, isFunctionKind, isFunctionType } from './function-kind.js'; import { Kind, isKind } from './kind.js'; export class ClassType extends Type { @@ -23,6 +24,7 @@ export class ClassType extends Type { protected readonly superClasses: readonly ClassType[]; // if necessary, the array could be replaced by Map: name/form -> ClassType, for faster look-ups protected readonly subClasses: ClassType[] = []; // additional sub classes might be added later on! protected readonly fields: FieldDetails[]; + protected readonly methods: MethodDetails[]; constructor(kind: ClassKind, identifier: string, typeDetails: ClassTypeDetails) { super(identifier); @@ -57,6 +59,15 @@ export class ClassType extends Type { if (this.getFields(false).size !== typeDetails.fields.length) { throw new Error('field names must be unique!'); } + + // methods + this.methods = typeDetails.methods.map(method => { + const methodType = this.kind.getFunctionKind().getOrCreateFunctionType(method); + return { + type: methodType, + }; + }); + // TODO check uniqueness?? } override getName(): string { @@ -247,6 +258,21 @@ export class ClassType extends Type { }); return result; } + + getMethods(withSuperClassMethods: boolean): FunctionType[] { + // own methods + const result: FunctionType[] = this.methods.map(m => m.type); + // methods of super classes + if (withSuperClassMethods) { + for (const superClass of this.getDeclaredSuperClasses()) { + for (const superMethod of superClass.getMethods(true)) { + result.push(superMethod); + } + } + } + return result; + } + } export function isClassType(type: unknown): type is ClassType { @@ -275,18 +301,22 @@ export interface CreateFieldDetails { type: TypeSelector; } -export interface ClassTypeDetails { +export interface MethodDetails { + type: FunctionType; + // methods might have some more properties in the future +} + +export interface ClassTypeDetails { className: string, superClasses?: TypeSelector | TypeSelector[], fields: CreateFieldDetails[], - // TODO methods + methods: Array>, // all details of functions can be configured for methods as well, in particular, inference rules for function/method calls! } -export interface CreateClassTypeDetails extends ClassTypeDetails { // TODO the generics look very bad! +export interface CreateClassTypeDetails extends ClassTypeDetails { // TODO the generics look very bad! inferenceRuleForDeclaration?: (domainElement: unknown) => boolean, // TODO what is the purpose for this? what is the difference to literals? inferenceRuleForLiteral?: InferClassLiteral, // InferClassLiteral | Array>, does not work: https://stackoverflow.com/questions/65129070/defining-an-array-of-differing-generic-types-in-typescript inferenceRuleForReference?: InferClassLiteral, inferenceRuleForFieldAccess?: (domainElement: unknown) => string | unknown | InferenceRuleNotApplicable, // name of the field | element to infer the type of the field (e.g. the type) | rule not applicable - // TODO inference rule for method calls } // TODO nominal vs structural typing ?? @@ -296,6 +326,7 @@ export type InferClassLiteral = { inputValuesForFields: (domainElement: T) => Map; // simple field name (including inherited fields) => value for this field! TODO implement that, [] for nominal typing }; + /** * Classes have a name and have an arbitrary number of fields, consisting of a name and a type, and an arbitrary number of super-classes. * Fields have exactly one type and no multiplicity (which can be realized with a type of kind 'MultiplicityKind'). @@ -324,12 +355,12 @@ export class ClassKind implements Kind { assertTrue(this.options.maximumNumberOfSuperClasses >= 0); // no negative values } - getClassType(typeDetails: ClassTypeDetails | string): ClassType | undefined { // string for nominal typing - const key = this.calculateIdentifier(typeof typeDetails === 'string' ? { className: typeDetails, fields: []} : typeDetails); + getClassType(typeDetails: ClassTypeDetails | string): ClassType | undefined { // string for nominal typing + const key = this.calculateIdentifier(typeof typeDetails === 'string' ? { className: typeDetails, fields: [], methods: [] } : typeDetails); return this.services.graph.getType(key) as ClassType; } - getOrCreateClassType(typeDetails: CreateClassTypeDetails): ClassType { + getOrCreateClassType(typeDetails: CreateClassTypeDetails): ClassType { const classType = this.getClassType(typeDetails); if (classType) { this.registerInferenceRules(typeDetails, classType); @@ -338,20 +369,20 @@ export class ClassKind implements Kind { return this.createClassType(typeDetails); } - createClassType(typeDetails: CreateClassTypeDetails): ClassType { + createClassType(typeDetails: CreateClassTypeDetails): ClassType { assertTrue(this.getClassType(typeDetails) === undefined, `${typeDetails.className}`); // create the class type - const classType = new ClassType(this, this.calculateIdentifier(typeDetails), typeDetails); + const classType = new ClassType(this, this.calculateIdentifier(typeDetails), typeDetails as CreateClassTypeDetails); this.services.graph.addNode(classType); // register inference rules - this.registerInferenceRules(typeDetails, classType); + this.registerInferenceRules(typeDetails, classType); return classType; } - protected registerInferenceRules(typeDetails: CreateClassTypeDetails, classType: ClassType) { + protected registerInferenceRules(typeDetails: CreateClassTypeDetails, classType: ClassType) { if (typeDetails.inferenceRuleForDeclaration) { this.services.inference.addInferenceRule({ inferTypeWithoutChildren(domainElement, _typir) { @@ -449,11 +480,11 @@ export class ClassKind implements Kind { }, classType); } - calculateIdentifier(typeDetails: ClassTypeDetails): string { + calculateIdentifier(typeDetails: ClassTypeDetails): string { return this.printClassType(typeDetails); } - protected printClassType(typeDetails: ClassTypeDetails): string { + protected printClassType(typeDetails: ClassTypeDetails): string { const prefix = this.options.identifierPrefix; if (this.options.typing === 'Structural') { // fields @@ -461,6 +492,12 @@ export class ClassKind implements Kind { for (const [fieldNUmber, fieldDetails] of typeDetails.fields.entries()) { fields.push(`${fieldNUmber}:${fieldDetails.name}`); } + // methods + const methods: string[] = []; + for (const method of typeDetails.methods) { + const methodType = this.getFunctionKind().getOrCreateFunctionType(method); + methods.push(methodType.identifier); // TODO is ".identifier" too strict here? + } // super classes const superClasses = toArray(typeDetails.superClasses).map(selector => { const type = resolveTypeSelector(this.services, selector); @@ -469,7 +506,7 @@ export class ClassKind implements Kind { }); const extendedClasses = superClasses.length <= 0 ? '' : `-extends-${superClasses.map(c => c.identifier).join(',')}`; // whole representation - return `${prefix}-${typeDetails.className}{${fields.join(',')}}${extendedClasses}`; + return `${prefix}-${typeDetails.className}{${fields.join(',')}}{${methods.join(',')}}${extendedClasses}`; } else if (this.options.typing === 'Nominal') { return `${prefix}-${typeDetails.className}`; } else { @@ -477,6 +514,22 @@ export class ClassKind implements Kind { } } + getFunctionKind(): FunctionKind { + // ensure, that Typir uses the predefined 'function' kind + const kind = this.services.kinds.get(FunctionKindName); + return isFunctionKind(kind) ? kind : new FunctionKind(this.services); + } + + getOrCreateTopClassType(typeDetails: TopClassTypeDetails): TopClassType { + return this.getTopClassKind().getOrCreateTopClassType(typeDetails); + } + + getTopClassKind(): TopClassKind { + // ensure, that Typir uses the predefined 'TopClass' kind + const kind = this.services.kinds.get(TopClassKindName); + return isTopClassKind(kind) ? kind : new TopClassKind(this.services); + } + } export function isClassKind(kind: unknown): kind is ClassKind { @@ -528,6 +581,7 @@ export class UniqueClassValidation implements ValidationRuleWithBeforeAfter { * @returns a string key */ protected calculateClassKey(clas: ClassType): string { + // usually duplicated classes are critical only for nominal typing, therefore the classname is used as default implementation here return `${clas.className}`; } @@ -550,3 +604,231 @@ export class UniqueClassValidation implements ValidationRuleWithBeforeAfter { return result; } } + +/** + * Predefined validation to produce errors, if inside a class the same method is declared more than once. + */ +export class UniqueMethodValidation implements ValidationRuleWithBeforeAfter { + protected readonly foundDeclarations: Map = new Map(); + protected readonly services: TypirServices; + /** Determines domain elements which represent declared methods, improves performance a lot. */ + protected readonly isMethodDeclaration: (domainElement: unknown) => domainElement is T; + /** Determines the corresponding domain element of the class declaration, so that Typir can infer its ClassType */ + protected readonly getClassOfMethod: (domainElement: T, methodType: FunctionType) => unknown; + + constructor(services: TypirServices, + isMethodDeclaration: (domainElement: unknown) => domainElement is T, + getClassOfMethod: (domainElement: T, methodType: FunctionType) => unknown) { + this.services = services; + this.isMethodDeclaration = isMethodDeclaration; + this.getClassOfMethod = getClassOfMethod; + } + + beforeValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { + this.foundDeclarations.clear(); + return []; + } + + validation(domainElement: unknown, _typir: TypirServices): ValidationProblem[] { + if (this.isMethodDeclaration(domainElement)) { // improves performance, since type inference need to be done only for relevant elements + const methodType = this.services.inference.inferType(domainElement); + if (isFunctionType(methodType)) { + const classDeclaration = this.getClassOfMethod(domainElement, methodType); + const classType = this.services.inference.inferType(classDeclaration); + if (isClassType(classType)) { + const key = this.calculateMethodKey(classType, methodType); + let entries = this.foundDeclarations.get(key); + if (!entries) { + entries = []; + this.foundDeclarations.set(key, entries); + } + entries.push(domainElement); + } + } + } + return []; + } + + /** + * Calculates a key for a method which encodes its unique properties, i.e. duplicate methods have the same key. + * Additionally, the class of the method needs to be represented in the key as well. + * This key is used to identify duplicated methods. + * Override this method to change the properties which make a method unique. + * @param clas the current class type + * @param func the current function type + * @returns a string key + */ + protected calculateMethodKey(clas: ClassType, func: FunctionType): string { + return `${clas.identifier}.${func.functionName}(${func.getInputs().map(param => param.type.identifier)})`; + } + + afterValidation(_domainRoot: unknown, _typir: TypirServices): ValidationProblem[] { + const result: ValidationProblem[] = []; + for (const [key, methods] of this.foundDeclarations.entries()) { + if (methods.length >= 2) { + for (const method of methods) { + result.push({ + $problem: ValidationProblem, + domainElement: method, + severity: 'error', + message: `Declared methods need to be unique (${key}).`, + }); + } + } + } + + this.foundDeclarations.clear(); + return result; + } +} + + +export class TopClassType extends Type { + override readonly kind: TopClassKind; + + constructor(kind: TopClassKind, identifier: string) { + super(identifier); + this.kind = kind; + } + + override getName(): string { + return this.identifier; + } + + override getUserRepresentation(): string { + return this.identifier; + } + + override analyzeTypeEqualityProblems(otherType: Type): TypirProblem[] { + if (isTopClassType(otherType)) { + return []; + } else { + return [{ + $problem: TypeEqualityProblem, + type1: this, + type2: otherType, + subProblems: [createKindConflict(otherType, this)], + }]; + } + } + + override analyzeIsSubTypeOf(superType: Type): TypirProblem[] { + if (isTopClassType(superType)) { + // special case by definition: TopClassType is sub-type of TopClassType + return []; + } else { + return [{ + $problem: SubTypeProblem, + superType, + subType: this, + subProblems: [createKindConflict(superType, this)], + }]; + } + } + + override analyzeIsSuperTypeOf(subType: Type): TypirProblem[] { + // an TopClassType is the super type of all ClassTypes! + if (isClassType(subType)) { + return []; + } else { + return [{ + $problem: SubTypeProblem, + superType: this, + subType, + subProblems: [createKindConflict(this, subType)], + }]; + } + } + +} + +export function isTopClassType(type: unknown): type is TopClassType { + return isType(type) && isTopClassKind(type.kind); +} + + +export interface TopClassTypeDetails { + inferenceRules?: InferTopClassType | InferTopClassType[] +} + +export type InferTopClassType = (domainElement: unknown) => boolean; + +export interface TopClassKindOptions { + name: string; +} + +export const TopClassKindName = 'TopClassKind'; + +export class TopClassKind implements Kind { + readonly $name: 'TopClassKind'; + readonly services: TypirServices; + readonly options: TopClassKindOptions; + protected instance: TopClassType | undefined; + + constructor(services: TypirServices, options?: Partial) { + this.$name = TopClassKindName; + this.services = services; + this.services.kinds.register(this); + this.options = { + // the default values: + name: 'TopClass', + // the actually overriden values: + ...options + }; + } + + getTopClassType(typeDetails: TopClassTypeDetails): TopClassType | undefined { + const key = this.calculateIdentifier(typeDetails); + return this.services.graph.getType(key) as TopClassType; + } + + getOrCreateTopClassType(typeDetails: TopClassTypeDetails): TopClassType { + const topType = this.getTopClassType(typeDetails); + if (topType) { + this.registerInferenceRules(typeDetails, topType); + return topType; + } + return this.createTopClassType(typeDetails); + } + + createTopClassType(typeDetails: TopClassTypeDetails): TopClassType { + assertTrue(this.getTopClassType(typeDetails) === undefined); + + // create the top type (singleton) + if (this.instance) { + // note, that the given inference rules are ignored in this case! + return this.instance; + } + const topType = new TopClassType(this, this.calculateIdentifier(typeDetails)); + this.instance = topType; + this.services.graph.addNode(topType); + + 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: TopClassTypeDetails, topType: TopClassType) { + const rules = toArray(typeDetails.inferenceRules); + if (rules.length >= 1) { + this.services.inference.addInferenceRule((domainElement, _typir) => { + for (const inferenceRule of rules) { + if (inferenceRule(domainElement)) { + return topType; + } + } + return InferenceRuleNotApplicable; + }, topType); + } + } + + calculateIdentifier(_typeDetails: TopClassTypeDetails): string { + return this.options.name; + } + +} + +export function isTopClassKind(kind: unknown): kind is TopClassKind { + return isKind(kind) && kind.$name === TopClassKindName; +} diff --git a/packages/typir/src/kinds/function-kind.ts b/packages/typir/src/kinds/function-kind.ts index f3adc19..7086018 100644 --- a/packages/typir/src/kinds/function-kind.ts +++ b/packages/typir/src/kinds/function-kind.ts @@ -33,7 +33,7 @@ export class FunctionType extends Type { const outputType = typeDetails.outputParameter ? resolveTypeSelector(this.kind.services, typeDetails.outputParameter.type) : undefined; if (typeDetails.outputParameter) { assertTrue(outputType !== undefined); - this.kind.enforceName(typeDetails.outputParameter.name, this.kind.options.enforceOutputParameterName); + this.kind.enforceParameterName(typeDetails.outputParameter.name, this.kind.options.enforceOutputParameterName); this.outputParameter = { name: typeDetails.outputParameter.name, type: outputType, @@ -45,7 +45,7 @@ export class FunctionType extends Type { // input parameters this.inputParameters = typeDetails.inputParameters.map(input => { - this.kind.enforceName(input.name, this.kind.options.enforceInputParameterNames); + this.kind.enforceParameterName(input.name, this.kind.options.enforceInputParameterNames); return { name: input.name, type: resolveTypeSelector(this.kind.services, input.type), @@ -54,7 +54,7 @@ export class FunctionType extends Type { } override getName(): string { - return `${this.getSimpleFunctionName}`; + return `${this.getSimpleFunctionName()}`; } override getUserRepresentation(): string { @@ -62,14 +62,14 @@ export class FunctionType extends Type { const simpleFunctionName = this.getSimpleFunctionName(); // inputs const inputs = this.getInputs(); - const inputsString = inputs.map(input => this.kind.getNameTypePairRepresentation(input)).join(', '); + const inputsString = inputs.map(input => this.kind.getParameterRepresentation(input)).join(', '); // output const output = this.getOutput(); const outputString = output - ? (this.kind.hasName(output.name) ? `(${this.kind.getNameTypePairRepresentation(output)})` : output.type.getName()) + ? (this.kind.hasParameterName(output.name) ? `(${this.kind.getParameterRepresentation(output)})` : output.type.getName()) : undefined; // complete signature - if (this.kind.hasName(simpleFunctionName)) { + if (this.kind.hasFunctionName(simpleFunctionName)) { const outputValue = outputString ? `: ${outputString}` : ''; return `${simpleFunctionName}(${inputsString})${outputValue}`; } else { @@ -275,7 +275,7 @@ export class FunctionKind implements Kind, TypeGraphListener { }; // register Validations for input arguments of function calls (must be done here to support overloaded functions) - this.services.validation.collector.addValidationRules( + this.services.validation.collector.addValidationRule( (domainElement, typir) => { const resultAll: ValidationProblem[] = []; for (const [overloadedName, overloadedFunctions] of this.mapNameTypes.entries()) { @@ -397,7 +397,7 @@ export class FunctionKind implements Kind, TypeGraphListener { // no output parameter => no inference rule for calling this function throw new Error(`A function '${functionName}' without output parameter cannot have an inferred type, when this function is called!`); } - this.enforceName(functionName, this.options.enforceFunctionName); + this.enforceFunctionName(functionName, this.options.enforceFunctionName); // create the function type const functionType = new FunctionType(this, this.calculateIdentifier(typeDetails), typeDetails); @@ -574,35 +574,45 @@ export class FunctionKind implements Kind, TypeGraphListener { // this schema allows to identify duplicated functions! const prefix = this.options.identifierPrefix; // function name - const functionName = this.hasName(typeDetails.functionName) ? typeDetails.functionName : ''; + const functionName = this.hasFunctionName(typeDetails.functionName) ? typeDetails.functionName : ''; // inputs const inputsString = typeDetails.inputParameters.map(input => resolveTypeSelector(this.services, input.type).getName()).join(','); // complete signature return `${prefix}-${functionName}(${inputsString})`; } - getNameTypePairRepresentation(pair: NameTypePair): string { - const typeName = pair.type.getName(); - if (this.hasName(pair.name)) { - return `${pair.name}: ${typeName}`; + getParameterRepresentation(parameter: NameTypePair): string { + const typeName = parameter.type.getName(); + if (this.hasParameterName(parameter.name)) { + return `${parameter.name}: ${typeName}`; } else { return typeName; } } - enforceName(name: string | undefined, enforce: boolean): void { - if (enforce && this.hasName(name) === false) { - throw new Error('a name is required'); + enforceFunctionName(name: string | undefined, enforce: boolean): void { + if (enforce && this.hasFunctionName(name) === false) { + throw new Error('A name for the function is required.'); } } - hasName(name: string | undefined): name is string { - return name !== undefined && name !== FUNCTION_MISSING_NAME; + hasFunctionName(name: string | undefined): name is string { + return name !== undefined && name !== NO_FUNCTION_NAME; + } + + enforceParameterName(name: string | undefined, enforce: boolean): void { + if (enforce && this.hasParameterName(name) === false) { + throw new Error('A name for the parameter is required.'); + } + } + hasParameterName(name: string | undefined): name is string { + return name !== undefined && name !== NO_PARAMETER_NAME; } } -// when the name is missing (e.g. for functions or their input/output parameters), use this value instead -export const FUNCTION_MISSING_NAME = ''; +// when the name is missing (e.g. for functions or their input/output parameters), use these values instead +export const NO_FUNCTION_NAME = ''; +export const NO_PARAMETER_NAME = ''; export function isFunctionKind(kind: unknown): kind is FunctionKind { return isKind(kind) && kind.$name === FunctionKindName; diff --git a/packages/typir/test/type-definitions.test.ts b/packages/typir/test/type-definitions.test.ts index eb28af1..07986fe 100644 --- a/packages/typir/test/type-definitions.test.ts +++ b/packages/typir/test/type-definitions.test.ts @@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest'; import { AssignabilityProblem } from '../src/features/assignability.js'; import { ClassKind } from '../src/kinds/class-kind.js'; import { FixedParameterKind } from '../src/kinds/fixed-parameters-kind.js'; -import { FUNCTION_MISSING_NAME, FunctionKind } from '../src/kinds/function-kind.js'; +import { FunctionKind, NO_PARAMETER_NAME } from '../src/kinds/function-kind.js'; import { MultiplicityKind } from '../src/kinds/multiplicity-kind.js'; import { PrimitiveKind } from '../src/kinds/primitive-kind.js'; import { createTypirServices } from '../src/typir.js'; @@ -41,14 +41,18 @@ describe('Tests for Typir', () => { { name: 'firstName', type: typeString }, { name: 'lastName', type: typeOneOrTwoStrings }, { name: 'age', type: typeInt } - ]}); + ], + methods: [], + }); console.log(typePerson.getUserRepresentation()); const typeStudent = classKind.createClassType({ className: 'Student', superClasses: typePerson, // a Student is a special Person fields: [ { name: 'studentNumber', type: typeInt } - ]}); + ], + methods: [] + }); // create some more types const typeListInt = listKind.createFixedParameterType({ parameterTypes: typeInt }); @@ -56,7 +60,7 @@ describe('Tests for Typir', () => { const typeMapStringPerson = mapKind.createFixedParameterType({ parameterTypes: [typeString, typePerson] }); const typeFunctionStringLength = functionKind.createFunctionType({ functionName: 'length', - outputParameter: { name: FUNCTION_MISSING_NAME, type: typeInt }, + outputParameter: { name: NO_PARAMETER_NAME, type: typeInt }, inputParameters: [{ name: 'value', type: typeString }] });