Skip to content

Commit

Permalink
register inference rules with a language key, chaining API for an arb…
Browse files Browse the repository at this point in the history
…itrary number of type-safe inference rules (for Functions, operators, classes, primitives), inference made TypeSelector type-safe
  • Loading branch information
JohannesMeierSE committed Feb 14, 2025
1 parent 305a66f commit 30aa25f
Show file tree
Hide file tree
Showing 30 changed files with 1,059 additions and 636 deletions.
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ We roughly follow the ideas of [semantic versioning](https://semver.org/).
Note that the versions "0.x.0" probably will include breaking changes.


## v0.2.0 (upcoming)
## v0.2.0 (??-??-2025)

### New features

Expand All @@ -14,16 +14,21 @@ Note that the versions "0.x.0" probably will include breaking changes.
- Moved the existing graph algorithms into its own dedicated service in order to reuse and to customize them (#58)
- New service `LanguageService` to provide Typir some static information about the currently type-checked language/DSL
- Associate validation rules with language keys for an improved performance
- Typir-Langium: new API to register validations,
e.g. `addValidationsRulesForAstNodes({ ReturnStatement: ..., VariableDeclaration: ... })`, see (L)OX for some examples
- Typir-Langium: new API to register validations to the `$type` of the `AstNode` to validate,
e.g. `addValidationsRulesForAstNodes({ ReturnStatement: <ValidationRule1>, VariableDeclaration: <ValidationRule2>, ... })`, see (L)OX for some examples
- Associate inference rule with language keys for an improved performance
- Thanks to the new chaining API for defining types, they can be annotated in TypeScript-type-safe way with multiple inference rules for the same purpose.

### Breaking changes

- `TypeConversion.markAsConvertible` accepts only one type for source and target now in order to simplify the API (#58)
- `TypeConversion.markAsConvertible` accepts only one type for source and target now in order to simplify the API (#58): Users need to write `for` loops themselves now
- Methods in listeners (`TypeGraphListener`, `TypeStateListener`) are prefixed with `on` (#58)
- Reworked the API to add/remove validation rules in the `ValidationCollector` service:
- Additional arguments need to be specified with an options object now
- Unified validation API by defining `ValidationRule = ValidationRuleStateless | ValidationRuleWithBeforeAfter` and removed dedicated `add/removeValidationRuleWithBeforeAndAfter` methods accordingly
- Reworked the API to add/remove rules for type inference in the `TypeInferenceCollector` service:
- Additional arguments need to be specified with an options object now
- Reworked the APIs to create types by introducing a chaining API to define optional inference rules. This counts for all type factories.


## v0.1.2 (2024-12-20)
Expand Down
213 changes: 118 additions & 95 deletions examples/lox/src/language/lox-type-checking.ts

Large diffs are not rendered by default.

54 changes: 26 additions & 28 deletions examples/ox/src/language/ox-type-checking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
******************************************************************************/

import { AstNode, AstUtils, LangiumSharedCoreServices, Module, assertUnreachable } from 'langium';
import { CreateParameterDetails, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, TypirServices, UniqueFunctionValidation } from 'typir';
import { CreateParameterDetails, InferOperatorWithMultipleOperands, InferOperatorWithSingleOperand, InferenceRuleNotApplicable, NO_PARAMETER_NAME, TypirServices } from 'typir';
import { AbstractLangiumTypeCreator, LangiumLanguageService, LangiumServicesForTypirBinding, PartialTypirLangiumServices } from 'typir-langium';
import { ValidationMessageDetails, ValidationProblem } from '../../../../packages/typir/lib/services/validation.js';
import { BinaryExpression, ForStatement, IfStatement, MemberCall, OxAstType, UnaryExpression, WhileStatement, isBinaryExpression, isBooleanLiteral, isFunctionDeclaration, isMemberCall, isNumberLiteral, isParameter, isTypeReference, isUnaryExpression, isVariableDeclaration, reflection } from './generated/ast.js';
import { BinaryExpression, ForStatement, FunctionDeclaration, IfStatement, MemberCall, NumberLiteral, OxAstType, TypeReference, UnaryExpression, WhileStatement, isBinaryExpression, isBooleanLiteral, isFunctionDeclaration, isMemberCall, isParameter, isTypeReference, isUnaryExpression, isVariableDeclaration, reflection } from './generated/ast.js';

export class OxTypeCreator extends AbstractLangiumTypeCreator {
protected readonly typir: LangiumServicesForTypirBinding;
Expand All @@ -21,18 +21,18 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator {
onInitialize(): void {
// define primitive types
// typeBool, typeNumber and typeVoid are specific types for OX, ...
const typeBool = this.typir.factory.Primitives.create({ primitiveName: 'boolean', inferenceRules: [
isBooleanLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'boolean',
]});
const typeBool = this.typir.factory.Primitives.create({ primitiveName: 'boolean' })
.inferenceRule({ filter: isBooleanLiteral })
.inferenceRule({ filter: isTypeReference, matching: node => node.primitive === 'boolean' })
.finish();
// ... but their primitive kind is provided/preset by Typir
const typeNumber = this.typir.factory.Primitives.create({ primitiveName: 'number', inferenceRules: [
isNumberLiteral,
(node: unknown) => isTypeReference(node) && node.primitive === 'number',
]});
const typeVoid = this.typir.factory.Primitives.create({ primitiveName: 'void', inferenceRules:
(node: unknown) => isTypeReference(node) && node.primitive === 'void'
});
const typeNumber = this.typir.factory.Primitives.create({ primitiveName: 'number' })
.inferenceRule({ languageKey: NumberLiteral })
.inferenceRule({ languageKey: TypeReference, matching: (node: TypeReference) => node.primitive === 'number' })
.finish();
const typeVoid = this.typir.factory.Primitives.create({ primitiveName: 'void' })
.inferenceRule({ languageKey: TypeReference, matching: (node: TypeReference) => node.primitive === 'void' })
.finish();

// 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 @@ -49,28 +49,28 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator {
// define operators
// binary operators: numbers => number
for (const operator of ['+', '-', '*', '/']) {
this.typir.factory.Operators.createBinary({ 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).finish();
}
// TODO better name for "inferenceRule": astSelectors
// binary operators: numbers => boolean
for (const operator of ['<', '<=', '>', '>=']) {
this.typir.factory.Operators.createBinary({ 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).finish();
}
// binary operators: booleans => boolean
for (const operator of ['and', 'or']) {
this.typir.factory.Operators.createBinary({ 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).finish();
}
// ==, != for booleans and numbers
for (const operator of ['==', '!=']) {
this.typir.factory.Operators.createBinary({ name: operator, signatures: [
{ left: typeNumber, right: typeNumber, return: typeBool },
{ left: typeBool, right: typeBool, return: typeBool },
], inferenceRule: binaryInferenceRule });
]}).inferenceRule(binaryInferenceRule).finish();
}

// unary operators
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 });
this.typir.factory.Operators.createUnary({ name: '!', signature: { operand: typeBool, return: typeBool }}).inferenceRule(unaryInferenceRule).finish();
this.typir.factory.Operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }}).inferenceRule(unaryInferenceRule).finish();

/** 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 @@ -155,9 +155,6 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator {
return typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool,
() => <ValidationMessageDetails>{ message: "Conditions need to be evaluated to 'boolean'.", languageProperty: 'condition' });
}

// check for unique function declarations
this.typir.validation.Collector.addValidationRule(new UniqueFunctionValidation(this.typir, isFunctionDeclaration));
}

onNewAstNode(languageNode: AstNode): void {
Expand All @@ -171,20 +168,21 @@ export class OxTypeCreator extends AbstractLangiumTypeCreator {
// 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: languageNode.returnType },
inputParameters: languageNode.parameters.map(p => (<CreateParameterDetails>{ name: p.name, type: p.type })),
associatedLanguageNode: languageNode,
})
// inference rule for function declaration:
inferenceRuleForDeclaration: (node: unknown) => node === languageNode, // only the current function declaration matches!
.inferenceRuleForDeclaration({ languageKey: FunctionDeclaration, matching: (node: FunctionDeclaration) => node === languageNode }) // 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,
.inferenceRuleForCalls({
languageKey: MemberCall,
matching: (call: MemberCall) => isFunctionDeclaration(call.element.ref) && call.explicitOperationCall && call.element.ref.name === functionName,
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!
},
associatedLanguageNode: languageNode,
});
})
.finish();
}
}
}
Expand Down
24 changes: 21 additions & 3 deletions examples/ox/src/language/ox-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* terms of the MIT License, which is available in the project root.
******************************************************************************/

import { AstUtils, type ValidationAcceptor, type ValidationChecks } from 'langium';
import { isFunctionDeclaration, isVariableDeclaration, OxElement, VariableDeclaration, type OxAstType, type ReturnStatement } from './generated/ast.js';
import { AstUtils, MultiMap, type ValidationAcceptor, type ValidationChecks } from 'langium';
import { FunctionDeclaration, isFunctionDeclaration, isVariableDeclaration, OxElement, OxProgram, VariableDeclaration, type OxAstType, type ReturnStatement } from './generated/ast.js';
import type { OxServices } from './ox-module.js';

/**
Expand All @@ -16,7 +16,10 @@ export function registerValidationChecks(services: OxServices) {
const validator = services.validation.OxValidator;
const checks: ValidationChecks<OxAstType> = {
ReturnStatement: validator.checkReturnTypeIsCorrect,
OxProgram: validator.checkUniqueVariableNames,
OxProgram: [
validator.checkUniqueVariableNames,
validator.checkUniqueFunctionNames,
],
Block: validator.checkUniqueVariableNames,
};
registry.register(checks, validator);
Expand Down Expand Up @@ -75,4 +78,19 @@ export class OxValidator {
}
}

checkUniqueFunctionNames(root: OxProgram, accept: ValidationAcceptor): void {
const mappedFunctions: MultiMap<string, FunctionDeclaration> = new MultiMap();
root.elements.filter(isFunctionDeclaration).forEach(decl => mappedFunctions.add(decl.name, decl));
for (const [name, declarations] of mappedFunctions.entriesGroupedByKey()) {
if (declarations.length >= 2) {
for (const f of declarations) {
accept('error', 'Functions need to have unique names: ' + name, {
node: f,
property: 'name'
});
}
}
}
}

}
9 changes: 6 additions & 3 deletions examples/ox/test/ox-type-checking-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ describe('Test type checking for statements and variables in OX', () => {

test('function: the same function name twice (in the same file) is not allowed in Typir', async () => {
await validateOx(`
fun myFunction() : boolean { return true; }
fun myFunction() : boolean { return false; }
`, 2); // both functions should be marked as "duplicate"
fun myFunction() : boolean { return true; }
fun myFunction() : boolean { return false; }
`, [
'Functions need to have unique names',
'Functions need to have unique names',
]);
});

// TODO this test case needs to be investigated in more detail
Expand Down
4 changes: 2 additions & 2 deletions packages/typir/src/initialization/type-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export type TypeInitializerListener<T extends Type = Type> = (type: T) => void;
* it will be tried to create A a second time, again delayed, since B is still not yet available.
* When B is created, A is waiting twice and might be created twice, if no TypeInitializer is used.
*
* Design decision: While this class does not provide some many default implementations,
* a common super class (or interface) of all type initializers is useful,
* Design decision: While this class does not provide so many default implementations,
* a common super class (or interface) of all type initializers is useful nevertheless,
* since they all can be used as TypeSelector in an easy way.
*/
export abstract class TypeInitializer<T extends Type = Type> {
Expand Down
6 changes: 3 additions & 3 deletions packages/typir/src/initialization/type-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { TypeGraphListener } from '../graph/type-graph.js';
import { Type } from '../graph/type-node.js';
import { TypeInferenceCollectorListener, TypeInferenceRule } from '../services/inference.js';
import { TypeInferenceCollectorListener, TypeInferenceRule, TypeInferenceRuleOptions } from '../services/inference.js';
import { TypirServices } from '../typir.js';
import { removeFromArray } from '../utils/utils.js';
import { TypeSelector } from './type-selector.js';
Expand Down Expand Up @@ -144,11 +144,11 @@ export class TypeReference<T extends Type = Type> implements TypeGraphListener,
}
}

onAddedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void {
onAddedInferenceRule(_rule: TypeInferenceRule, _options: TypeInferenceRuleOptions): void {
// after adding a new inference rule, try to resolve the type
this.resolve(); // possible performance optimization: use only the new inference rule to resolve the type
}
onRemovedInferenceRule(_rule: TypeInferenceRule, _boundToType?: Type): void {
onRemovedInferenceRule(_rule: TypeInferenceRule, _options: TypeInferenceRuleOptions): void {
// empty, since removed inference rules don't help to resolve a type
}
}
Loading

0 comments on commit 30aa25f

Please sign in to comment.