Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Designed new API and simple example #43

Merged
merged 7 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 18 additions & 31 deletions examples/lox/src/language/type-system/lox-type-checking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,47 @@

import { AstNode, AstUtils, Module, assertUnreachable } from 'langium';
import { LangiumSharedServices } from 'langium/lsp';
import { ClassKind, CreateFieldDetails, CreateFunctionTypeDetails, CreateParameterDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, PrimitiveKind, TopKind, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, createNoSuperClassCyclesValidation, ValidationMessageDetails } from 'typir';
import { CreateFieldDetails, CreateFunctionTypeDetails, CreateParameterDetails, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, TypirServices, UniqueClassValidation, UniqueFunctionValidation, UniqueMethodValidation, ValidationMessageDetails, createNoSuperClassCyclesValidation } from 'typir';
import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium';
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;
protected readonly functionKind: FunctionKind;
protected readonly classKind: ClassKind;
protected readonly anyKind: TopKind;
protected readonly operators: OperatorManager;

constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) {
super(typirServices, langiumServices);
this.typir = typirServices;

this.primitiveKind = new PrimitiveKind(this.typir);
this.functionKind = new FunctionKind(this.typir);
this.classKind = new ClassKind(this.typir, {
typing: 'Nominal',
});
this.anyKind = new TopKind(this.typir);
this.operators = this.typir.operators;
}

onInitialize(): void {
// primitive types
// typeBool, typeNumber and typeVoid are specific types for OX, ...
const typeBool = this.primitiveKind.createPrimitiveType({ primitiveName: 'boolean',
const typeBool = this.typir.factory.primitives.create({ primitiveName: 'boolean',
inferenceRules: [
isBooleanLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'boolean'
]});
// ... but their primitive kind is provided/preset by Typir
const typeNumber = this.primitiveKind.createPrimitiveType({ primitiveName: 'number',
const typeNumber = this.typir.factory.primitives.create({ primitiveName: 'number',
inferenceRules: [
isNumberLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'number'
]});
const typeString = this.primitiveKind.createPrimitiveType({ primitiveName: 'string',
const typeString = this.typir.factory.primitives.create({ primitiveName: 'string',
inferenceRules: [
isStringLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'string'
]});
const typeVoid = this.primitiveKind.createPrimitiveType({ primitiveName: 'void',
const typeVoid = this.typir.factory.primitives.create({ primitiveName: 'void',
inferenceRules: [
(node: unknown) => isTypeReference(node) && node.primitive === 'void',
isPrintStatement,
(node: unknown) => isReturnStatement(node) && node.value === undefined
] });
const typeNil = this.primitiveKind.createPrimitiveType({ primitiveName: 'nil',
const typeNil = this.typir.factory.primitives.create({ primitiveName: 'nil',
inferenceRules: isNilLiteral }); // From "Crafting Interpreters" no value, like null in other languages. Uninitialised variables default to nil. When the execution reaches the end of the block of a function body without hitting a return, nil is implicitly returned.
const typeAny = this.anyKind.createTopType({});
const typeAny = this.typir.factory.top.create({});

// extract inference rules, which is possible here thanks to the unified structure of the Langium grammar (but this is not possible in general!)
const binaryInferenceRule: InferOperatorWithMultipleOperands<BinaryExpression> = {
Expand All @@ -75,9 +62,9 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator {

// binary operators: numbers => number
for (const operator of ['-', '*', '/']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule });
}
this.operators.createBinaryOperator({ name: '+', signature: [
this.typir.factory.operators.createBinary({ name: '+', signature: [
{ left: typeNumber, right: typeNumber, return: typeNumber },
{ left: typeString, right: typeString, return: typeString },
{ left: typeNumber, right: typeString, return: typeString },
Expand All @@ -86,24 +73,24 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator {

// binary operators: numbers => boolean
for (const operator of ['<', '<=', '>', '>=']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule });
}

// binary operators: booleans => boolean
for (const operator of ['and', 'or']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule });
}

// ==, != for all data types (the warning for different types is realized below)
for (const operator of ['==', '!=']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeAny, right: typeAny, return: typeBool }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeAny, right: typeAny, return: typeBool }, inferenceRule: binaryInferenceRule });
}
// = for SuperType = SubType (TODO integrate the validation here? should be replaced!)
this.operators.createBinaryOperator({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: '=', signature: { left: typeAny, right: typeAny, return: typeAny }, inferenceRule: binaryInferenceRule });

// unary operators
this.operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule });
this.operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule });
this.typir.factory.operators.createUnary({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule });
this.typir.factory.operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule });

// additional inference rules for ...
this.typir.inference.addInferenceRule((domainElement: unknown) => {
Expand Down Expand Up @@ -208,15 +195,15 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator {

// function types: they have to be updated after each change of the Langium document, since they are derived from FunctionDeclarations!
if (isFunctionDeclaration(node)) {
this.functionKind.createFunctionType(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar
this.typir.factory.functions.create(createFunctionDetails(node)); // this logic is reused for methods of classes, since the LOX grammar defines them very similar
}

// TODO support lambda (type references)!

// class types (nominal typing):
if (isClass(node)) {
const className = node.name;
const classType = this.classKind.createClassType({
const classType = this.typir.factory.classes.create({
className,
superClasses: node.superClass?.ref, // note that type inference is used here
fields: node.members
Expand Down Expand Up @@ -250,7 +237,7 @@ export class LoxTypeCreator extends AbstractLangiumTypeCreator {
// 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');
classType.addListener(type => {
this.typir.conversion.markAsConvertible(this.primitiveKind.getPrimitiveType({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT');
this.typir.conversion.markAsConvertible(this.typir.factory.primitives.get({ primitiveName: 'nil' })!, type, 'IMPLICIT_EXPLICIT');
});
}
}
Expand Down
29 changes: 11 additions & 18 deletions examples/ox/src/language/ox-type-checking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,32 @@

import { AstNode, AstUtils, Module, assertUnreachable } from 'langium';
import { LangiumSharedServices } from 'langium/lsp';
import { CreateParameterDetails, FunctionKind, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, OperatorManager, PrimitiveKind, TypirServices, UniqueFunctionValidation } from 'typir';
import { CreateParameterDetails, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, TypirServices, UniqueFunctionValidation } from 'typir';
import { AbstractLangiumTypeCreator, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium';
import { ValidationMessageDetails } from '../../../../packages/typir/lib/services/validation.js';
import { BinaryExpression, MemberCall, UnaryExpression, isAssignmentStatement, isBinaryExpression, isBooleanLiteral, isForStatement, isFunctionDeclaration, isIfStatement, isMemberCall, isNumberLiteral, isParameter, isReturnStatement, isTypeReference, isUnaryExpression, isVariableDeclaration, isWhileStatement } from './generated/ast.js';

export class OxTypeCreator extends AbstractLangiumTypeCreator {
protected readonly typir: TypirServices;
protected readonly primitiveKind: PrimitiveKind;
protected readonly functionKind: FunctionKind;
protected readonly operators: OperatorManager;

constructor(typirServices: TypirServices, langiumServices: LangiumSharedServices) {
super(typirServices, langiumServices);
this.typir = typirServices;

this.primitiveKind = new PrimitiveKind(this.typir);
this.functionKind = new FunctionKind(this.typir);
this.operators = this.typir.operators;
}

onInitialize(): void {
// define primitive types
// typeBool, typeNumber and typeVoid are specific types for OX, ...
const typeBool = this.primitiveKind.createPrimitiveType({ primitiveName: 'boolean', inferenceRules: [
const typeBool = this.typir.factory.primitives.create({ primitiveName: 'boolean', inferenceRules: [
isBooleanLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'boolean',
]});
// ... but their primitive kind is provided/preset by Typir
const typeNumber = this.primitiveKind.createPrimitiveType({ primitiveName: 'number', inferenceRules: [
const typeNumber = this.typir.factory.primitives.create({ primitiveName: 'number', inferenceRules: [
isNumberLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'number',
]});
const typeVoid = this.primitiveKind.createPrimitiveType({ primitiveName: 'void', inferenceRules:
const typeVoid = this.typir.factory.primitives.create({ primitiveName: 'void', inferenceRules:
(node: unknown) => isTypeReference(node) && node.primitive === 'void'
});

Expand All @@ -57,29 +50,29 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator {
// define operators
// binary operators: numbers => number
for (const operator of ['+', '-', '*', '/']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeNumber }, inferenceRule: binaryInferenceRule });
}
// TODO better name: overloads, overloadRules, selectors, signatures
// TODO better name for "inferenceRule": astSelectors
// binary operators: numbers => boolean
for (const operator of ['<', '<=', '>', '>=']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeNumber, right: typeNumber, return: typeBool }, inferenceRule: binaryInferenceRule });
}
// binary operators: booleans => boolean
for (const operator of ['and', 'or']) {
this.operators.createBinaryOperator({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule });
this.typir.factory.operators.createBinary({ name: operator, signature: { left: typeBool, right: typeBool, return: typeBool }, inferenceRule: binaryInferenceRule });
}
// ==, != for booleans and numbers
for (const operator of ['==', '!=']) {
this.operators.createBinaryOperator({ name: operator, signature: [
this.typir.factory.operators.createBinary({ name: operator, signature: [
{ left: typeNumber, right: typeNumber, return: typeBool },
{ left: typeBool, right: typeBool, return: typeBool },
], inferenceRule: binaryInferenceRule });
}

// unary operators
this.operators.createUnaryOperator({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule });
this.operators.createUnaryOperator({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule });
this.typir.factory.operators.createUnary({ name: '!', signature: { operand: typeBool, return: typeBool }, inferenceRule: unaryInferenceRule });
this.typir.factory.operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }, inferenceRule: unaryInferenceRule });

/** Hints regarding the order of Typir configurations for OX:
* - In general, Typir aims to not depend on the order of configurations.
Expand Down Expand Up @@ -171,7 +164,7 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator {
if (isFunctionDeclaration(domainElement)) {
const functionName = domainElement.name;
// define function type
this.functionKind.createFunctionType({
this.typir.factory.functions.create({
functionName,
// note that the following two lines internally use type inference here in order to map language types to Typir types
outputParameter: { name: NO_PARAMETER_NAME, type: domainElement.returnType },
Expand Down
22 changes: 9 additions & 13 deletions packages/typir/src/kinds/bottom/bottom-kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ export type InferBottomType = (domainElement: unknown) => boolean;

export const BottomKindName = 'BottomKind';

export class BottomKind implements Kind {
export interface BottomFactoryService {
create(typeDetails: BottomTypeDetails): BottomType;
get(typeDetails: BottomTypeDetails): BottomType | undefined;
}

export class BottomKind implements Kind, BottomFactoryService {
readonly $name: 'BottomKind';
readonly services: TypirServices;
readonly options: Readonly<BottomKindOptions>;
Expand All @@ -41,22 +46,13 @@ export class BottomKind implements Kind {
};
}

getBottomType(typeDetails: BottomTypeDetails): BottomType | undefined {
get(typeDetails: BottomTypeDetails): BottomType | undefined {
const key = this.calculateIdentifier(typeDetails);
return this.services.graph.getType(key) as BottomType;
}

getOrCreateBottomType(typeDetails: BottomTypeDetails): BottomType {
const bottomType = this.getBottomType(typeDetails);
if (bottomType) {
this.registerInferenceRules(typeDetails, bottomType);
return bottomType;
}
return this.createBottomType(typeDetails);
}

createBottomType(typeDetails: BottomTypeDetails): BottomType {
assertTrue(this.getBottomType(typeDetails) === undefined);
create(typeDetails: BottomTypeDetails): BottomType {
assertTrue(this.get(typeDetails) === undefined);
// create the bottom type (singleton)
if (this.instance) {
// note, that the given inference rules are ignored in this case!
Expand Down
Loading
Loading