From f5eef85a86014930d42939b78ca60df37fb595ed Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 15 Aug 2025 00:20:35 +0200 Subject: [PATCH 01/17] make LanguageKeys type-safe # Conflicts: # packages/typir/src/utils/rule-registration.ts --- CHANGELOG.md | 2 +- documentation/getting-started.md | 2 +- .../lox/src/language/lox-type-checking.ts | 4 +- examples/ox/src/language/ox-type-checking.ts | 2 +- packages/typir-langium/README.md | 2 +- .../src/features/langium-inference.ts | 2 +- .../src/features/langium-language.ts | 12 +++--- .../src/features/langium-validation.ts | 2 +- packages/typir-langium/src/typir-langium.ts | 4 +- .../src/initialization/type-reference.ts | 4 +- .../src/kinds/class/class-initializer.ts | 10 ++--- packages/typir/src/kinds/class/class-kind.ts | 12 +++--- .../src/kinds/custom/custom-initializer.ts | 10 ++--- .../kinds/function/function-initializer.ts | 10 ++--- .../typir/src/kinds/function/function-kind.ts | 4 +- .../function/function-validation-calls.ts | 10 ++--- packages/typir/src/services/inference.ts | 26 ++++++------ packages/typir/src/services/language.ts | 12 +++--- packages/typir/src/services/validation.ts | 26 ++++++------ packages/typir/src/typir.ts | 7 ++++ packages/typir/src/utils/rule-registration.ts | 42 +++++++++---------- packages/typir/src/utils/utils-definitions.ts | 22 +++------- .../test/services/inference-registry.test.ts | 4 +- .../test/services/validation-registry.test.ts | 4 +- 24 files changed, 116 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a2cb9..c7c75ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,7 +74,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] LanguageType: unknown; } ``` - +TODO - `TypirLangiumSpecifics` extends the Typir specifics for Langium, concretizes the language type and enables to register the available AST types of the current Langium grammar as `AstTypes`: ```typescript diff --git a/documentation/getting-started.md b/documentation/getting-started.md index 5ebf36e..9032b3f 100644 --- a/documentation/getting-started.md +++ b/documentation/getting-started.md @@ -99,7 +99,7 @@ After creating the Langium services (which contain the Typir serivces now) and s ```typescript export interface MyDSLSpecifics extends TypirLangiumSpecifics { - AstTypes: MyDSLAstType; // all AST types from the generated `ast.ts` + LanguageKeys: MyDSLAstType; // all AST types from the generated `ast.ts` // ... more could be customized here ... } ``` diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index c0bbfce..b41eae1 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -12,9 +12,9 @@ import { BinaryExpression, BooleanLiteral, Class, ForStatement, FunctionDeclarat /* eslint-disable @typescript-eslint/no-unused-vars */ export interface LoxSpecifics extends TypirLangiumSpecifics { // concretize some LOX-specifics here - AstTypes: LoxAstType; // all AST types from the generated `ast.ts` + LanguageKeys: LoxAstType; // all AST types from the generated `ast.ts` } -// interface extensions is used to concretize the `AstTypes`, since type intersection would merge `LangiumAstTypes` and `LoxAstType` (https://www.typescriptlang.org/docs/handbook/2/objects.html#interface-extension-vs-intersection) +// interface extensions is used to concretize the `LanguageKeys`, since type intersection would merge `LangiumAstTypes` and `LoxAstType` (https://www.typescriptlang.org/docs/handbook/2/objects.html#interface-extension-vs-intersection) export class LoxTypeSystem implements LangiumTypeSystemDefinition { diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 2b8a565..8585b74 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -10,7 +10,7 @@ import { LangiumTypeSystemDefinition, TypirLangiumServices, TypirLangiumSpecific import { BinaryExpression, ForStatement, FunctionDeclaration, IfStatement, MemberCall, NumberLiteral, OxAstType, TypeReference, UnaryExpression, WhileStatement, isBinaryExpression, isBooleanLiteral, isFunctionDeclaration, isParameter, isTypeReference, isUnaryExpression, isVariableDeclaration } from './generated/ast.js'; export interface OxSpecifics extends TypirLangiumSpecifics { // concretize some OX-specifics here - AstTypes: OxAstType; // all AST types from the generated `ast.ts` + LanguageKeys: OxAstType; // all AST types from the generated `ast.ts` } export class OxTypeSystem implements LangiumTypeSystemDefinition { diff --git a/packages/typir-langium/README.md b/packages/typir-langium/README.md index f0be442..36500ee 100644 --- a/packages/typir-langium/README.md +++ b/packages/typir-langium/README.md @@ -72,7 +72,7 @@ export class MyDSLTypeSystem implements LangiumTypeSystemDefinition = { - [K in keyof Specifics['AstTypes']]?: Specifics['AstTypes'][K] extends Specifics['LanguageType'] ? TypeInferenceRule | Array> : never + [K in keyof Specifics['LanguageKeys']]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? TypeInferenceRule | Array> : never } & { AstNode?: TypeInferenceRule | Array>; } diff --git a/packages/typir-langium/src/features/langium-language.ts b/packages/typir-langium/src/features/langium-language.ts index 3908866..1060756 100644 --- a/packages/typir-langium/src/features/langium-language.ts +++ b/packages/typir-langium/src/features/langium-language.ts @@ -14,27 +14,27 @@ import { TypirLangiumSpecifics } from '../typir-langium.js'; */ export class LangiumLanguageService extends DefaultLanguageService implements LanguageService { protected readonly reflection: AbstractAstReflection; - protected superKeys: Map | undefined = undefined; // key => all its super-keys + protected superKeys: Map> | undefined = undefined; // key => all its super-keys constructor(reflection: AbstractAstReflection) { super(); this.reflection = reflection; } - override getLanguageNodeKey(languageNode: Specifics['LanguageType']): string { + override getLanguageNodeKey(languageNode: Specifics['LanguageType']): keyof Specifics['LanguageKeys'] { return languageNode.$type; } - override getAllSubKeys(languageKey: string): string[] { - const result = this.reflection.getAllSubTypes(languageKey); + override getAllSubKeys(languageKey: keyof Specifics['LanguageKeys']): Array { + const result = this.reflection.getAllSubTypes(languageKey as string); removeFromArray(languageKey, result); // Langium adds the given type in the list of all sub-types, therefore it must be removed here return result; } - override getAllSuperKeys(languageKey: string): string[] { + override getAllSuperKeys(languageKey: keyof Specifics['LanguageKeys']): Array { if (this.superKeys === undefined) { // collect all super types (Sets ensure uniqueness of super-keys) - const map: Map> = new Map(); + const map: Map> = new Map(); for (const superKey of this.reflection.getAllTypes()) { for (const subKey of this.getAllSubKeys(superKey)) { let entries = map.get(subKey); diff --git a/packages/typir-langium/src/features/langium-validation.ts b/packages/typir-langium/src/features/langium-validation.ts index 8e0cff3..91d939d 100644 --- a/packages/typir-langium/src/features/langium-validation.ts +++ b/packages/typir-langium/src/features/langium-validation.ts @@ -110,7 +110,7 @@ export class DefaultLangiumTypirValidator = { - [K in keyof Specifics['AstTypes']]?: Specifics['AstTypes'][K] extends Specifics['LanguageType'] ? ValidationRule | Array> : never + [K in keyof Specifics['LanguageKeys']]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? ValidationRule | Array> : never } & { AstNode?: ValidationRule | Array>; } diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index cbbf613..1df8b7c 100644 --- a/packages/typir-langium/src/typir-langium.ts +++ b/packages/typir-langium/src/typir-langium.ts @@ -18,8 +18,8 @@ import { LangiumAstTypes } from './utils/typir-langium-utils.js'; * This type collects all TypeScript types which might be customized by applications of Typir-Langium. */ export interface TypirLangiumSpecifics extends TypirSpecifics { - LanguageType: AstNode; // concretizes the `LanguageType`, since all language nodes of a Langium AST are AstNode's - AstTypes: LangiumAstTypes; // applications should concretize the `AstTypes` with XXXAstType from the generated `ast.ts` + LanguageType: AstNode; // concretizes the `LanguageType`, since all language nodes of a Langium AST are AstNode's + LanguageKeys: LangiumAstTypes; // applications should concretize the `LanguageKeys` with XXXAstType from the generated `ast.ts` /** Support also the Langium-specific diagnostic properties, e.g. to mark keywords or register code actions */ ValidationMessageProperties: TypirSpecifics['ValidationMessageProperties'] & Omit, 'node'|'property'|'index'>; // 'node', 'property', and 'index' are already coverd by TypirSpecifics['ValidationMessageProperties'] with a different name } diff --git a/packages/typir/src/initialization/type-reference.ts b/packages/typir/src/initialization/type-reference.ts index 5c18a90..43f63d0 100644 --- a/packages/typir/src/initialization/type-reference.ts +++ b/packages/typir/src/initialization/type-reference.ts @@ -148,11 +148,11 @@ export class TypeReference< } } - onAddedInferenceRule(_rule: TypeInferenceRule, _options: TypeInferenceRuleOptions): 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, _options: TypeInferenceRuleOptions): void { + onRemovedInferenceRule(_rule: TypeInferenceRule, _options: TypeInferenceRuleOptions): void { // empty, since removed inference rules don't help to resolve a type } } diff --git a/packages/typir/src/kinds/class/class-initializer.ts b/packages/typir/src/kinds/class/class-initializer.ts index 8239c77..b6c1e21 100644 --- a/packages/typir/src/kinds/class/class-initializer.ts +++ b/packages/typir/src/kinds/class/class-initializer.ts @@ -8,7 +8,7 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; +import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, inferenceOptionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; import { checkNameTypesMap, createTypeCheckStrategy, MapListConverter } from '../../utils/utils-type-comparison.js'; import { assertTypirType, toArray } from '../../utils/utils.js'; import { ClassKind, CreateClassTypeDetails, InferClassLiteral } from './class-kind.js'; @@ -303,13 +303,13 @@ export class ClassTypeInitializer extends Type } protected registerRules(classType: ClassType | undefined): void { - this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, classType))); - this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, classType))); + this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, classType))); + this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, inferenceOptionsBoundToType(rule.options, classType))); } protected deregisterRules(classType: ClassType | undefined): void { - this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, classType))); - this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, optionsBoundToType(rule.options, classType))); + this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, classType))); + this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, inferenceOptionsBoundToType(rule.options, classType))); } } diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index ede2e32..6eeebbc 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -73,11 +73,11 @@ export interface ClassFactoryService { // some predefined valitions: - createUniqueClassValidation(options: RegistrationOptions): UniqueClassValidation; + createUniqueClassValidation(options: RegistrationOptions): UniqueClassValidation; - createUniqueMethodValidation(options: UniqueMethodValidationOptions & RegistrationOptions): ValidationRule; + createUniqueMethodValidation(options: UniqueMethodValidationOptions & RegistrationOptions): ValidationRule; - createNoSuperClassCyclesValidation(options: NoSuperClassCyclesValidationOptions & RegistrationOptions): ValidationRule; + createNoSuperClassCyclesValidation(options: NoSuperClassCyclesValidationOptions & RegistrationOptions): ValidationRule; // benefits of this design decision: the returned rule is easier to exchange, users can use the known factory API with auto-completion (no need to remember the names of the validations) } @@ -217,7 +217,7 @@ export class ClassKind implements Kind, ClassF return this.services.infrastructure.Kinds.getOrCreateKind(TopClassKindName, services => new TopClassKind(services)); } - createUniqueClassValidation(options: RegistrationOptions): UniqueClassValidation { + createUniqueClassValidation(options: RegistrationOptions): UniqueClassValidation { const rule = new UniqueClassValidation(this.services); if (options.registration === 'MYSELF') { // do nothing, the user is responsible to register the rule @@ -227,7 +227,7 @@ export class ClassKind implements Kind, ClassF return rule; } - createUniqueMethodValidation(options: UniqueMethodValidationOptions & RegistrationOptions): ValidationRule { + createUniqueMethodValidation(options: UniqueMethodValidationOptions & RegistrationOptions): ValidationRule { const rule = new UniqueMethodValidation(this.services, options); if (options.registration === 'MYSELF') { // do nothing, the user is responsible to register the rule @@ -237,7 +237,7 @@ export class ClassKind implements Kind, ClassF return rule; } - createNoSuperClassCyclesValidation(options: NoSuperClassCyclesValidationOptions & RegistrationOptions): ValidationRule { + createNoSuperClassCyclesValidation(options: NoSuperClassCyclesValidationOptions & RegistrationOptions): ValidationRule { const rule = new NoSuperClassCyclesValidation(this.services, options); if (options.registration === 'MYSELF') { // do nothing, the user is responsible to register the rule diff --git a/packages/typir/src/kinds/custom/custom-initializer.ts b/packages/typir/src/kinds/custom/custom-initializer.ts index c5452b9..34f5796 100644 --- a/packages/typir/src/kinds/custom/custom-initializer.ts +++ b/packages/typir/src/kinds/custom/custom-initializer.ts @@ -9,7 +9,7 @@ import { Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { MarkSubTypeOptions } from '../../services/subtype.js'; import { TypirSpecifics } from '../../typir.js'; -import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; +import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, inferenceOptionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; import { assertTrue, assertTypirType } from '../../utils/utils.js'; import { CustomTypeProperties } from './custom-definitions.js'; import { CreateCustomTypeDetails, CustomKind } from './custom-kind.js'; @@ -156,13 +156,13 @@ export class CustomTypeInitializer | undefined): void { - this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, customType))); - this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, optionsBoundToType(rule.options, customType))); + this.inferenceRules.forEach(rule => this.services.Inference.addInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, customType))); + this.validationRules.forEach(rule => this.services.validation.Collector.addValidationRule(rule.rule, inferenceOptionsBoundToType(rule.options, customType))); } protected deregisterRules(customType: CustomType | undefined): void { - this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, customType))); - this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, optionsBoundToType(rule.options, customType))); + this.inferenceRules.forEach(rule => this.services.Inference.removeInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, customType))); + this.validationRules.forEach(rule => this.services.validation.Collector.removeValidationRule(rule.rule, inferenceOptionsBoundToType(rule.options, customType))); } } diff --git a/packages/typir/src/kinds/function/function-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 34e9269..cb96eb8 100644 --- a/packages/typir/src/kinds/function/function-initializer.ts +++ b/packages/typir/src/kinds/function/function-initializer.ts @@ -8,7 +8,7 @@ import { Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeInferenceRule } from '../../services/inference.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { bindInferCurrentTypeRule, InferenceRuleWithOptions, optionsBoundToType, skipInferenceRuleForExistingType } from '../../utils/utils-definitions.js'; +import { bindInferCurrentTypeRule, InferenceRuleWithOptions, inferenceOptionsBoundToType, skipInferenceRuleForExistingType } from '../../utils/utils-definitions.js'; import { assertTypirType } from '../../utils/utils.js'; import { FunctionCallInferenceRule } from './function-inference-call.js'; import { CreateFunctionTypeDetails, FunctionKind, FunctionTypeDetails, InferFunctionCall } from './function-kind.js'; @@ -85,20 +85,20 @@ export class FunctionTypeInitializer extends T protected registerRules(functionName: string, functionType: FunctionType | undefined): void { for (const rule of this.inferenceForCall) { const overloaded = this.functions.getOrCreateOverloads(functionName); - overloaded.inferenceRule.addInferenceRule(rule.rule, optionsBoundToType(rule.options, functionType)); + overloaded.inferenceRule.addInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, functionType)); } for (const rule of this.inferenceForDeclaration) { - this.services.Inference.addInferenceRule(rule.rule, optionsBoundToType(rule.options, functionType)); + this.services.Inference.addInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, functionType)); } } protected deregisterRules(functionName: string, functionType: FunctionType | undefined): void { for (const rule of this.inferenceForCall) { const overloaded = this.functions.getOverloads(functionName); - overloaded?.inferenceRule.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, functionType)); + overloaded?.inferenceRule.removeInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, functionType)); } for (const rule of this.inferenceForDeclaration) { - this.services.Inference.removeInferenceRule(rule.rule, optionsBoundToType(rule.options, functionType)); + this.services.Inference.removeInferenceRule(rule.rule, inferenceOptionsBoundToType(rule.options, functionType)); } } diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index b13edd1..aeb1b8a 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -116,7 +116,7 @@ export interface FunctionFactoryService { // some predefined valitions: /** Creates a validation rule which checks, that the function types are unique. */ - createUniqueFunctionValidation(options: RegistrationOptions): ValidationRule; + createUniqueFunctionValidation(options: RegistrationOptions): ValidationRule; // benefits of this design decision: the returned rule is easier to exchange, users can use the known factory API with auto-completion (no need to remember the names of the validations) } @@ -237,7 +237,7 @@ export class FunctionKind implements Kind, Fun return name !== undefined && name !== NO_PARAMETER_NAME; } - createUniqueFunctionValidation(options: RegistrationOptions): ValidationRule { + createUniqueFunctionValidation(options: RegistrationOptions): ValidationRule { const rule = new UniqueFunctionValidation(this.services); if (options.registration === 'MYSELF') { // do nothing, the user is responsible to register the rule diff --git a/packages/typir/src/kinds/function/function-validation-calls.ts b/packages/typir/src/kinds/function/function-validation-calls.ts index 51c51fa..b4c922d 100644 --- a/packages/typir/src/kinds/function/function-validation-calls.ts +++ b/packages/typir/src/kinds/function/function-validation-calls.ts @@ -22,7 +22,7 @@ import { FunctionType } from './function-type.js'; * - and validates this call according to the specific validation rules for this function call. * There is only one instance of this class for each function kind/manager. */ -export class FunctionCallArgumentsValidation implements ValidationRuleLifecycle, RuleCollectorListener> { +export class FunctionCallArgumentsValidation implements ValidationRuleLifecycle, RuleCollectorListener> { protected readonly services: TypirServices; readonly functions: AvailableFunctionsManager; @@ -31,7 +31,7 @@ export class FunctionCallArgumentsValidation i this.functions = functions; } - onAddedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { + onAddedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { // this rule needs to be registered also for all the language keys of the new inner function call rule this.services.validation.Collector.addValidationRule(this, { ...diffOptions, @@ -39,7 +39,7 @@ export class FunctionCallArgumentsValidation i }); } - onRemovedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { + onRemovedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { // remove this "composite" rule for all language keys for which no function call rules are registered anymore if (diffOptions.languageKey === undefined) { if (this.noFunctionCallRulesForThisLanguageKey(undefined)) { @@ -59,7 +59,7 @@ export class FunctionCallArgumentsValidation i } } - protected noFunctionCallRulesForThisLanguageKey(key: undefined | string): boolean { + protected noFunctionCallRulesForThisLanguageKey(key: undefined | (keyof Specifics['LanguageKeys'])): boolean { for (const overloads of this.functions.getAllOverloads()) { if (overloads[1].details.getRulesByLanguageKey(key).length >= 1) { return false; @@ -70,7 +70,7 @@ export class FunctionCallArgumentsValidation i validation(languageNode: Specifics['LanguageType'], accept: ValidationProblemAcceptor, _typir: TypirServices): void { // determine all keys to check - const keysToApply: Array = []; + const keysToApply: Array<(keyof Specifics['LanguageKeys']) | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 38f9db7..3524f80 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -88,11 +88,11 @@ export interface TypeInferenceRuleWithInferringChildren { - onAddedInferenceRule(rule: TypeInferenceRule, options: TypeInferenceRuleOptions): void; - onRemovedInferenceRule(rule: TypeInferenceRule, options: TypeInferenceRuleOptions): void; + onAddedInferenceRule(rule: TypeInferenceRule, options: TypeInferenceRuleOptions): void; + onRemovedInferenceRule(rule: TypeInferenceRule, options: TypeInferenceRuleOptions): void; } -export interface TypeInferenceRuleOptions extends RuleOptions { +export interface TypeInferenceRuleOptions extends RuleOptions { // no additional properties so far } @@ -117,7 +117,7 @@ export interface TypeInferenceCollector { * @param rule a new inference rule * @param options additional options */ - addInferenceRule(rule: TypeInferenceRule, options?: Partial): void; + addInferenceRule(rule: TypeInferenceRule, options?: Partial>): void; /** * Deregisters an inference rule. * @param rule the rule to remove @@ -125,14 +125,14 @@ export interface TypeInferenceCollector { * the inference rule might still be registered for the not-specified options. * Listeners will be informed only about those removed options which were existing before. */ - removeInferenceRule(rule: TypeInferenceRule, options?: Partial): void; + removeInferenceRule(rule: TypeInferenceRule, options?: Partial>): void; addListener(listener: TypeInferenceCollectorListener): void; removeListener(listener: TypeInferenceCollectorListener): void; } -export class DefaultTypeInferenceCollector implements TypeInferenceCollector, RuleCollectorListener> { +export class DefaultTypeInferenceCollector implements TypeInferenceCollector, RuleCollectorListener> { protected readonly ruleRegistry: RuleRegistry, Specifics>; protected readonly languageNodeInference: LanguageNodeInferenceCaching; @@ -146,11 +146,11 @@ export class DefaultTypeInferenceCollector imp this.ruleRegistry.addListener(this); } - addInferenceRule(rule: TypeInferenceRule, givenOptions?: Partial): void { + addInferenceRule(rule: TypeInferenceRule, givenOptions?: Partial>): void { this.ruleRegistry.addRule(rule as unknown as TypeInferenceRule, givenOptions); } - removeInferenceRule(rule: TypeInferenceRule, optionsToRemove?: Partial): void { + removeInferenceRule(rule: TypeInferenceRule, optionsToRemove?: Partial>): void { this.ruleRegistry.removeRule(rule as unknown as TypeInferenceRule, optionsToRemove); } @@ -190,7 +190,7 @@ export class DefaultTypeInferenceCollector imp this.checkForError(languageNode); // determine all keys to check - const keysToApply: Array = []; + const keysToApply: Array<(keyof Specifics['LanguageKeys']) | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); @@ -319,11 +319,11 @@ export class DefaultTypeInferenceCollector imp // This inference collector is notified by the rule registry and forwards these notifications to its own listeners - onAddedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { + onAddedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { // listeners of the composite will be notified about all added inner rules this.listeners.forEach(listener => listener.onAddedInferenceRule(rule, diffOptions)); } - onRemovedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { + onRemovedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { // clear the cache, since its entries might be created using the removed rule // possible performance improvement: remove only entries which depend on the removed rule? this.cacheClear(); @@ -412,7 +412,7 @@ export class CompositeTypeInferenceRule extend throw new Error('This function will not be called.'); } - override onAddedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { + override onAddedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { // an inner rule was added super.onAddedRule(rule, diffOptions); @@ -423,7 +423,7 @@ export class CompositeTypeInferenceRule extend }); } - override onRemovedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { + override onRemovedRule(rule: TypeInferenceRule, diffOptions: RuleOptions): void { // an inner rule was removed super.onRemovedRule(rule, diffOptions); diff --git a/packages/typir/src/services/language.ts b/packages/typir/src/services/language.ts index d0c9dd8..d507cf6 100644 --- a/packages/typir/src/services/language.ts +++ b/packages/typir/src/services/language.ts @@ -30,21 +30,21 @@ export interface LanguageService { * @param languageNode the given language node * @returns the language key or 'undefined', if there is no language key for the given language node */ - getLanguageNodeKey(languageNode: Specifics['LanguageType']): string | undefined; + getLanguageNodeKey(languageNode: Specifics['LanguageType']): keyof Specifics['LanguageKeys'] | undefined; /** * Returns all keys, which are direct or indirect sub-keys of the given language key. * @param languageKey the given language key * @returns the list does not contain the given language key itself */ - getAllSubKeys(languageKey: string): string[]; + getAllSubKeys(languageKey: keyof Specifics['LanguageKeys']): Array; /** * Returns all keys, which are direct or indirect super-keys of the given language key. * @param languageKey the given language key * @returns the list does not contain the given language key itself */ - getAllSuperKeys(languageKey: string): string[]; + getAllSuperKeys(languageKey: keyof Specifics['LanguageKeys']): Array; isLanguageNode(node: unknown): node is Specifics['LanguageType']; } @@ -55,15 +55,15 @@ export interface LanguageService { */ export class DefaultLanguageService implements LanguageService { - getLanguageNodeKey(_languageNode: Specifics['LanguageType']): string | undefined { + getLanguageNodeKey(_languageNode: Specifics['LanguageType']): keyof Specifics['LanguageKeys'] | undefined { return undefined; } - getAllSubKeys(_languageKey: string): string[] { + getAllSubKeys(_languageKey: keyof Specifics['LanguageKeys']): Array { return []; } - getAllSuperKeys(_languageKey: string): string[] { + getAllSuperKeys(_languageKey: keyof Specifics['LanguageKeys']): Array { return []; } diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 546504e..126a798 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -205,11 +205,11 @@ export class DefaultValidationConstraints impl export interface ValidationCollectorListener { - onAddedValidationRule(rule: ValidationRule, options: ValidationRuleOptions): void; - onRemovedValidationRule(rule: ValidationRule, options: ValidationRuleOptions): void; + onAddedValidationRule(rule: ValidationRule, options: ValidationRuleOptions): void; + onRemovedValidationRule(rule: ValidationRule, options: ValidationRuleOptions): void; } -export interface ValidationRuleOptions extends RuleOptions { +export interface ValidationRuleOptions extends RuleOptions { // no additional properties so far } @@ -223,19 +223,19 @@ export interface ValidationCollector { * @param rule a new validation rule * @param options some more options to control the handling of the added validation rule */ - addValidationRule(rule: ValidationRule, options?: Partial): void; + addValidationRule(rule: ValidationRule, options?: Partial>): void; /** * Removes a validation rule. * @param rule the validation rule to remove * @param options the same options as given for the registration of the validation rule must be given for the removal! */ - removeValidationRule(rule: ValidationRule, options?: Partial): void; + removeValidationRule(rule: ValidationRule, options?: Partial>): void; addListener(listener: ValidationCollectorListener): void; removeListener(listener: ValidationCollectorListener): void; } -export class DefaultValidationCollector implements ValidationCollector, RuleCollectorListener> { +export class DefaultValidationCollector implements ValidationCollector, RuleCollectorListener> { protected readonly services: TypirServices; protected readonly listeners: Array> = []; @@ -272,7 +272,7 @@ export class DefaultValidationCollector implem validate(languageNode: Specifics['LanguageType']): Array> { // determine all keys to check - const keysToApply: Array = []; + const keysToApply: Array<(keyof Specifics['LanguageKeys']) | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); @@ -319,7 +319,7 @@ export class DefaultValidationCollector implem return problems; } - addValidationRule(rule: ValidationRule, givenOptions?: Partial): void { + addValidationRule(rule: ValidationRule, givenOptions?: Partial>): void { if (typeof rule === 'function') { this.ruleRegistryFunctional.addRule(rule as ValidationRuleFunctional, givenOptions); } else { @@ -327,7 +327,7 @@ export class DefaultValidationCollector implem } } - removeValidationRule(rule: ValidationRule, givenOptions?: Partial): void { + removeValidationRule(rule: ValidationRule, givenOptions?: Partial>): void { if (typeof rule === 'function') { this.ruleRegistryFunctional.removeRule(rule as ValidationRuleFunctional, givenOptions); } else { @@ -342,11 +342,11 @@ export class DefaultValidationCollector implem removeFromArray(listener, this.listeners); } - onAddedRule(rule: ValidationRule, diffOptions: RuleOptions): void { + onAddedRule(rule: ValidationRule, diffOptions: RuleOptions): void { // listeners of the composite will be notified about all added inner rules this.listeners.forEach(listener => listener.onAddedValidationRule(rule, diffOptions)); } - onRemovedRule(rule: ValidationRule, diffOptions: RuleOptions): void { + onRemovedRule(rule: ValidationRule, diffOptions: RuleOptions): void { // listeners of the composite will be notified about all removed inner rules this.listeners.forEach(listener => listener.onRemovedValidationRule(rule, diffOptions)); } @@ -374,7 +374,7 @@ export class CompositeValidationRule extends D this.validateAfter(languageRoot).forEach(v => accept(v)); } - override onAddedRule(rule: ValidationRule, diffOptions: RuleOptions): void { + override onAddedRule(rule: ValidationRule, diffOptions: RuleOptions): void { // an inner rule was added super.onAddedRule(rule, diffOptions); @@ -385,7 +385,7 @@ export class CompositeValidationRule extends D }); } - override onRemovedRule(rule: ValidationRule, diffOptions: RuleOptions): void { + override onRemovedRule(rule: ValidationRule, diffOptions: RuleOptions): void { // an inner rule was removed super.onRemovedRule(rule, diffOptions); diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index ac5c819..08222cf 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -189,6 +189,13 @@ export type PartialTypirServices = DeepPartial * This type collects all TypeScript types which might be customized by applications or bindings for language workbenches. */ export interface TypirSpecifics { + /** This is the TypeScript super-class of all language nodes in the AST */ LanguageType: unknown; + + /** The set of available language keys: + * Each language key maps to the TypeScript type (which extends 'LanguageType') of corresponding language nodes with this language key. */ + LanguageKeys: Record; + + /** Properties for validation issues (predefined and custom ones) */ ValidationMessageProperties: ValidationMessageProperties; } diff --git a/packages/typir/src/utils/rule-registration.ts b/packages/typir/src/utils/rule-registration.ts index 8db2850..87bb1e5 100644 --- a/packages/typir/src/utils/rule-registration.ts +++ b/packages/typir/src/utils/rule-registration.ts @@ -9,14 +9,14 @@ import { Type } from '../graph/type-node.js'; import { TypirSpecifics, TypirServices } from '../typir.js'; import { removeFromArray, toArray, toArrayWithValue } from './utils.js'; -export interface RuleOptions { +export interface RuleOptions { /** * If a rule is associated with a language key, the rule will be executed only for language nodes, which have this language key, * in order to improve the runtime performance. * In case of multiple language keys, the rule will be applied to all language nodes having ones of these language keys. * Rules without a language key ('undefined') are executed for all language nodes. */ - languageKey: string | string[] | undefined; + languageKey: (keyof Specifics['LanguageKeys']) | Array | undefined; /** * An optional type, if the new rule is dedicated for exactly this type. @@ -28,15 +28,15 @@ export interface RuleOptions { } // corresponding information in a slightly different structure, which is easier to handle internally -export interface InternalRuleOptions { +export interface InternalRuleOptions { languageKeyUndefined: boolean; - languageKeys: string[]; + languageKeys: Array; boundToTypes: Type[]; } -export interface RuleCollectorListener { - onAddedRule(rule: RuleType, diffOptions: RuleOptions): void; - onRemovedRule(rule: RuleType, diffOptions: RuleOptions): void; +export interface RuleCollectorListener { + onAddedRule(rule: RuleType, diffOptions: RuleOptions): void; + onRemovedRule(rule: RuleType, diffOptions: RuleOptions): void; } export class RuleRegistry implements TypeGraphListener { @@ -44,7 +44,7 @@ export class RuleRegistry implements * language node type --> rules * Improves the look-up of related rules, when doing type for a concrete language node. * All rules are registered at least once in this map, since rules without dedicated language key are registered to 'undefined'. */ - protected readonly languageTypeToRules: Map = new Map(); + protected readonly languageTypeToRules: Map<(keyof Specifics['LanguageKeys'])|undefined, RuleType[]> = new Map(); /** * type identifier --> -> rules * Improves the look-up for rules which are bound to types, when these types are removed. @@ -53,19 +53,19 @@ export class RuleRegistry implements /** * rule --> its collected options * Contains the current set of all options for an rule. */ - protected readonly ruleToOptions: Map = new Map(); + protected readonly ruleToOptions: Map> = new Map(); /** Collects all unique rules, lazily managed. */ protected readonly uniqueRules: Set = new Set(); - protected readonly listeners: Array> = []; + protected readonly listeners: Array> = []; constructor(services: TypirServices) { services.infrastructure.Graph.addListener(this); } - getRulesByLanguageKey(languageKey: string | undefined): RuleType[] { + getRulesByLanguageKey(languageKey: (keyof Specifics['LanguageKeys']) | undefined): RuleType[] { const store = this.languageTypeToRules.get(languageKey); if (store === undefined) { return []; @@ -90,7 +90,7 @@ export class RuleRegistry implements return this.getUniqueRules().size; } - protected getRuleOptions(options?: Partial): RuleOptions { + protected getRuleOptions(options?: Partial>): RuleOptions { return { // default values ... languageKey: undefined, @@ -100,13 +100,13 @@ export class RuleRegistry implements }; } - addRule(rule: RuleType, givenOptions?: Partial): void { + addRule(rule: RuleType, givenOptions?: Partial>): void { const newOptions = this.getRuleOptions(givenOptions); const languageKeyUndefined: boolean = newOptions.languageKey === undefined; - const languageKeys: string[] = toArray(newOptions.languageKey, { newArray: true }); + const languageKeys: Array = toArray(newOptions.languageKey, { newArray: true }); const existingOptions = this.ruleToOptions.get(rule); - const diffOptions: RuleOptions = { + const diffOptions: RuleOptions = { ...newOptions, languageKey: [], // empty for now, added keys will be added later boundToType: [], @@ -217,16 +217,16 @@ export class RuleRegistry implements } } - removeRule(rule: RuleType, optionsToRemove?: Partial): void { + removeRule(rule: RuleType, optionsToRemove?: Partial>): void { const existingOptions = this.ruleToOptions.get(rule); if (existingOptions === undefined) { // these options need to be updated (or completely removed at the end) return; // the rule is unknown here => nothing to do } const languageKeyUndefined: boolean = optionsToRemove ? (optionsToRemove.languageKey === undefined) : true; - const languageKeys: string[] = toArray(optionsToRemove?.languageKey, { newArray: true }); + const languageKeys: Array = toArray(optionsToRemove?.languageKey, { newArray: true }); - const diffOptions: RuleOptions = { + const diffOptions: RuleOptions = { // ... maybe more options in the future ... languageKey: [], // empty/nothing boundToType: [], // empty/nothing @@ -298,7 +298,7 @@ export class RuleRegistry implements } } - protected deregisterRuleForLanguageKey(rule: RuleType, languageKey: string | undefined): boolean { + protected deregisterRuleForLanguageKey(rule: RuleType, languageKey: (keyof Specifics['LanguageKeys']) | undefined): boolean { const rules = this.languageTypeToRules.get(languageKey); if (rules) { const result = removeFromArray(rule, rules); @@ -350,11 +350,11 @@ export class RuleRegistry implements } } - addListener(listener: RuleCollectorListener): void { + addListener(listener: RuleCollectorListener): void { this.listeners.push(listener); } - removeListener(listener: RuleCollectorListener): void { + removeListener(listener: RuleCollectorListener): void { removeFromArray(listener, this.listeners); } } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index b734257..b918747 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -45,7 +45,7 @@ export function isNameTypePair(type: unknown): type is NameTypePair { /** A pair of a rule for type inference with its additional options. */ export interface ValidationRuleWithOptions { rule: ValidationRule; - options: Partial; + options: Partial>; } export function bindValidateCurrentTypeRule( @@ -84,12 +84,12 @@ export function bindValidateCurrentTypeRule { /** * 'MYSELF' indicates, that the caller is responsible to register the validation rule, * otherwise the given options are used to register the return validation rule now. */ - registration: 'MYSELF' | Partial; + registration: 'MYSELF' | Partial>; } @@ -100,26 +100,16 @@ export interface RegistrationOptions { /** A pair of a rule for type inference with its additional options. */ export interface InferenceRuleWithOptions { rule: TypeInferenceRule; - options: Partial; + options: Partial>; } -export function optionsBoundToType | Partial>(options: T, type: Type | undefined): T { +export function inferenceOptionsBoundToType> = Partial>>(options: T, type: Type | undefined): T { return { ...options, boundToType: type, }; } -export function ruleWithOptionsBoundToType< - Specifics extends TypirSpecifics, - T extends Specifics['LanguageType'] = Specifics['LanguageType'], ->(rule: InferenceRuleWithOptions, type: Type | undefined): InferenceRuleWithOptions { - return { - rule: rule.rule, - options: optionsBoundToType(rule.options, type), - }; -} - /** * An inference rule which is dedicated for inferrring a certain type. @@ -131,7 +121,7 @@ export interface InferCurrentTypeRule< Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType'], > { - languageKey?: string | string[]; + languageKey?: (keyof Specifics['LanguageKeys']) | Array; filter?: (languageNode: Specifics['LanguageType']) => languageNode is T; matching?: (languageNode: T, typeToInfer: TypeType) => boolean; diff --git a/packages/typir/test/services/inference-registry.test.ts b/packages/typir/test/services/inference-registry.test.ts index 3bc3059..3d7ea96 100644 --- a/packages/typir/test/services/inference-registry.test.ts +++ b/packages/typir/test/services/inference-registry.test.ts @@ -210,10 +210,10 @@ describe('Tests the logic for registering rules (applied to inference rules)', ( function removeType(type: Type): void { typir.infrastructure.Graph.removeNode(type); } - function addInferenceRule(rule: TypeInferenceRuleWithoutInferringChildren, options?: Partial) { + function addInferenceRule(rule: TypeInferenceRuleWithoutInferringChildren, options?: Partial>) { typir.Inference.addInferenceRule(rule, options); } - function removeInferenceRule(rule: TypeInferenceRuleWithoutInferringChildren, options?: Partial) { + function removeInferenceRule(rule: TypeInferenceRuleWithoutInferringChildren, options?: Partial>) { typir.Inference.removeInferenceRule(rule, options); } diff --git a/packages/typir/test/services/validation-registry.test.ts b/packages/typir/test/services/validation-registry.test.ts index 04fea68..0b21051 100644 --- a/packages/typir/test/services/validation-registry.test.ts +++ b/packages/typir/test/services/validation-registry.test.ts @@ -289,10 +289,10 @@ describe('Tests the logic for registering rules (applied to state-less validatio function removeType(type: Type): void { typir.infrastructure.Graph.removeNode(type); } - function addValidationRule(rule: ValidationRule, options?: Partial) { + function addValidationRule(rule: ValidationRule, options?: Partial>) { typir.validation.Collector.addValidationRule(rule, options); } - function removeValidationRule(rule: ValidationRule, options?: Partial) { + function removeValidationRule(rule: ValidationRule, options?: Partial>) { typir.validation.Collector.removeValidationRule(rule, options); } From ea6f4106d65afdeb1e2b1ce97e9b9d735b71c7e7 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 15 Aug 2025 15:58:32 +0200 Subject: [PATCH 02/17] exploit Specifics['LanguageKeys'] to simplify inference rules for functions and operators --- .../lox/src/language/lox-type-checking.ts | 32 ++++----- examples/ox/src/language/ox-type-checking.ts | 8 +-- .../typir/src/kinds/bottom/bottom-kind.ts | 12 +++- .../src/kinds/class/class-initializer.ts | 12 +++- packages/typir/src/kinds/class/class-kind.ts | 49 +++++++++++--- .../typir/src/kinds/custom/custom-kind.ts | 13 +++- .../kinds/function/function-inference-call.ts | 17 +++-- .../typir/src/kinds/function/function-kind.ts | 35 +++++++--- .../kinds/function/function-overloading.ts | 11 ++- .../function/function-validation-calls.ts | 10 +-- .../src/kinds/primitive/primitive-kind.ts | 12 +++- packages/typir/src/kinds/top/top-kind.ts | 12 +++- packages/typir/src/services/inference.ts | 16 +++-- packages/typir/src/services/operator.ts | 8 ++- packages/typir/src/services/validation.ts | 2 +- packages/typir/src/utils/utils-definitions.ts | 67 ++++++++++++++----- 16 files changed, 221 insertions(+), 95 deletions(-) diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index b41eae1..14539df 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -24,22 +24,22 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition const typeBool = typir.factory.Primitives.create({ primitiveName: 'boolean' }) .inferenceRule({ languageKey: BooleanLiteral.$type }) // this is the more performant notation compared to ... // .inferenceRule({ filter: isBooleanLiteral }) // ... this alternative solution, but they provide the same functionality - .inferenceRule({ languageKey: TypeReference.$type, matching: (node: TypeReference) => node.primitive === 'boolean' }) // this is the more performant notation compared to ... + .inferenceRule({ languageKey: TypeReference.$type, matching: node => node.primitive === 'boolean' }) // this is the more performant notation compared to ... // .inferenceRule({ filter: isTypeReference, matching: node => node.primitive === 'boolean' }) // ... this "easier" notation, but they provide the same functionality .finish(); // ... but their primitive kind is provided/preset by Typir const typeNumber = typir.factory.Primitives.create({ primitiveName: 'number' }) .inferenceRule({ languageKey: NumberLiteral.$type }) - .inferenceRule({ languageKey: TypeReference.$type, matching: (node: TypeReference) => node.primitive === 'number' }) + .inferenceRule({ languageKey: TypeReference.$type, matching: node => node.primitive === 'number' }) .finish(); const typeString = typir.factory.Primitives.create({ primitiveName: 'string' }) .inferenceRule({ languageKey: StringLiteral.$type }) - .inferenceRule({ languageKey: TypeReference.$type, matching: (node: TypeReference) => node.primitive === 'string' }) + .inferenceRule({ languageKey: TypeReference.$type, matching: node => node.primitive === 'string' }) .finish(); const typeVoid = typir.factory.Primitives.create({ primitiveName: 'void' }) - .inferenceRule({ languageKey: TypeReference.$type, matching: (node: TypeReference) => node.primitive === 'void' }) + .inferenceRule({ languageKey: TypeReference.$type, matching: node => node.primitive === 'void' }) .inferenceRule({ languageKey: PrintStatement.$type }) - .inferenceRule({ languageKey: ReturnStatement.$type, matching: (node: ReturnStatement) => node.value === undefined }) + .inferenceRule({ languageKey: ReturnStatement.$type, matching: node => node.value === undefined }) .finish(); const typeNil = typir.factory.Primitives.create({ primitiveName: 'nil' }) .inferenceRule({ languageKey: NilLiteral.$type }) @@ -49,14 +49,14 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition // 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 = { languageKey: BinaryExpression.$type, - matching: (node: BinaryExpression, name: string) => node.operator === name, - operands: (node: BinaryExpression, _name: string) => [node.left, node.right], + matching: (node, name) => node.operator === name, + operands: (node, _name) => [node.left, node.right], validateArgumentsOfCalls: true, }; const unaryInferenceRule: InferOperatorWithSingleOperand = { languageKey: UnaryExpression.$type, - matching: (node: UnaryExpression, name: string) => node.operator === name, - operand: (node: UnaryExpression, _name: string) => node.value, + matching: (node, name) => node.operator === name, + operand: (node, _name) => node.value, validateArgumentsOfCalls: true, }; @@ -205,23 +205,23 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition associatedLanguageNode: node, // this is used by the ScopeProvider to get the corresponding class declaration after inferring the (class) type of an expression }) // inference rule for declaration - .inferenceRuleForClassDeclaration({ languageKey: Class.$type, matching: (languageNode: Class) => languageNode === node}) + .inferenceRuleForClassDeclaration({ languageKey: Class.$type, matching: languageNode => languageNode === node}) // inference rule for constructor calls (i.e. class literals) conforming to the current class .inferenceRuleForClassLiterals({ // > languageKey: MemberCall.$type, - matching: (languageNode: MemberCall) => isClass(languageNode.element?.ref) && languageNode.element!.ref.name === className && languageNode.explicitOperationCall, - inputValuesForFields: (_languageNode: MemberCall) => new Map(), // values for fields don't matter for nominal typing + matching: languageNode => isClass(languageNode.element?.ref) && languageNode.element!.ref.name === className && languageNode.explicitOperationCall, + inputValuesForFields: () => new Map(), // values for fields don't matter for nominal typing }) .inferenceRuleForClassLiterals({ // > languageKey: TypeReference.$type, - matching: (languageNode: TypeReference) => isClass(languageNode.reference?.ref) && languageNode.reference!.ref.name === className, - inputValuesForFields: (_languageNode: TypeReference) => new Map(), // values for fields don't matter for nominal typing + matching: languageNode => isClass(languageNode.reference?.ref) && languageNode.reference!.ref.name === className, + inputValuesForFields: () => new Map(), // values for fields don't matter for nominal typing }) // inference rule for accessing fields .inferenceRuleForFieldAccess({ languageKey: MemberCall.$type, - matching: (languageNode: MemberCall) => isFieldMember(languageNode.element?.ref) && languageNode.element!.ref.$container === node && !languageNode.explicitOperationCall, - field: (languageNode: MemberCall) => languageNode.element!.ref!.name, + matching: languageNode => isFieldMember(languageNode.element?.ref) && languageNode.element!.ref.$container === node && !languageNode.explicitOperationCall, + field: languageNode => languageNode.element!.ref!.name, }) .finish(); diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 8585b74..81a3560 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -25,10 +25,10 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { // ... but their primitive kind is provided/preset by Typir const typeNumber = typir.factory.Primitives.create({ primitiveName: 'number' }) .inferenceRule({ languageKey: NumberLiteral.$type }) - .inferenceRule({ languageKey: TypeReference.$type, matching: (node: TypeReference) => node.primitive === 'number' }) + .inferenceRule({ languageKey: TypeReference.$type, matching: node => node.primitive === 'number' }) .finish(); const typeVoid = typir.factory.Primitives.create({ primitiveName: 'void' }) - .inferenceRule({ languageKey: TypeReference.$type, matching: (node: TypeReference) => node.primitive === 'void' }) + .inferenceRule({ languageKey: TypeReference.$type, matching: node => 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!) @@ -166,7 +166,7 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { // inference rule for function declaration: .inferenceRuleForDeclaration({ languageKey: FunctionDeclaration.$type, - matching: (node: FunctionDeclaration) => node === languageNode // only the current function declaration matches! + matching: node => 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! @@ -174,7 +174,7 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { * - additionally, validations for the assigned values to the expected parameter( type)s are derived */ .inferenceRuleForCalls({ languageKey: MemberCall.$type, - matching: (call: MemberCall) => isFunctionDeclaration(call.element.ref) && call.explicitOperationCall && call.element.ref.name === functionName, + matching: call => isFunctionDeclaration(call.element.ref) && call.explicitOperationCall && call.element.ref.name === functionName, inputArguments: (call: MemberCall) => call.arguments, // they are needed to check, 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! validateArgumentsOfFunctionCalls: true, diff --git a/packages/typir/src/kinds/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index 7d382a0..d58b5a1 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -6,7 +6,7 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; +import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, KindOptions } from '../kind.js'; import { BottomType } from './bottom-type.js'; @@ -30,7 +30,10 @@ export interface BottomFactoryService { } export interface BottomConfigurationChain { - inferenceRule(rule: InferCurrentTypeRule): BottomConfigurationChain; + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): BottomConfigurationChain; finish(): BottomType; } @@ -91,7 +94,10 @@ class BottomConfigurationChainImpl implements }; } - inferenceRule(rule: InferCurrentTypeRule): BottomConfigurationChain { + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): BottomConfigurationChain { this.typeDetails.inferenceRules.push(rule as unknown as InferCurrentTypeRule); return this; } diff --git a/packages/typir/src/kinds/class/class-initializer.ts b/packages/typir/src/kinds/class/class-initializer.ts index b6c1e21..c4a7358 100644 --- a/packages/typir/src/kinds/class/class-initializer.ts +++ b/packages/typir/src/kinds/class/class-initializer.ts @@ -8,7 +8,7 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, inferenceOptionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; +import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, inferenceOptionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions, LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; import { checkNameTypesMap, createTypeCheckStrategy, MapListConverter } from '../../utils/utils-type-comparison.js'; import { assertTypirType, toArray } from '../../utils/utils.js'; import { ClassKind, CreateClassTypeDetails, InferClassLiteral } from './class-kind.js'; @@ -197,7 +197,10 @@ export class ClassTypeInitializer extends Type } } - protected createInferenceRuleForLiteral(rule: InferClassLiteral, classType: ClassType): InferenceRuleWithOptions { + protected createInferenceRuleForLiteral< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassLiteral, classType: ClassType): InferenceRuleWithOptions { const mapListConverter = new MapListConverter(); const kind = this.kind; return { @@ -254,7 +257,10 @@ export class ClassTypeInitializer extends Type }; } - protected createValidationRuleForLiteral(rule: InferClassLiteral, classType: ClassType): ValidationRuleWithOptions | undefined { + protected createValidationRuleForLiteral< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassLiteral, classType: ClassType): ValidationRuleWithOptions | undefined { const validationRules = toArray(rule.validation); if (validationRules.length <= 0) { return undefined; diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index 6eeebbc..3c7646b 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -11,7 +11,7 @@ import { TypeDescriptor } from '../../initialization/type-descriptor.js'; import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { ValidationRule } from '../../services/validation.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, RegistrationOptions } from '../../utils/utils-definitions.js'; +import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, RegistrationOptions } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, assertTypirType, assertUnreachable, toArray } from '../../utils/utils.js'; import { FunctionType } from '../function/function-type.js'; @@ -59,12 +59,20 @@ export interface CreateClassTypeDetails extend * Depending on whether the class is structurally or nominally typed, * different values might be specified, e.g. 'inputValuesForFields' could be empty for nominal classes. */ -export interface InferClassLiteral extends InferCurrentTypeRule { - inputValuesForFields: (languageNode: T) => Map; // simple field name (including inherited fields) => value for this field! +export interface InferClassLiteral< + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, +> extends InferCurrentTypeRule { + inputValuesForFields: (languageNode: LanguageType) => Map; // simple field name (including inherited fields) => value for this field! } -export interface InferClassFieldAccess extends InferCurrentTypeRule { - field: (languageNode: T) => string | Specifics | InferenceRuleNotApplicable; // name of the field | language node to infer the type of the field (e.g. the type) | rule not applicable +export interface InferClassFieldAccess< + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, +> extends InferCurrentTypeRule { + field: (languageNode: LanguageType) => string | Specifics | InferenceRuleNotApplicable; // name of the field | language node to infer the type of the field (e.g. the type) | rule not applicable } export interface ClassFactoryService { @@ -83,10 +91,20 @@ export interface ClassFactoryService { } export interface ClassConfigurationChain { - inferenceRuleForClassDeclaration(rule: InferCurrentTypeRule): ClassConfigurationChain; - inferenceRuleForClassLiterals(rule: InferClassLiteral): ClassConfigurationChain; + inferenceRuleForClassDeclaration< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): ClassConfigurationChain; - inferenceRuleForFieldAccess(rule: InferClassFieldAccess): ClassConfigurationChain; + inferenceRuleForClassLiterals< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassLiteral): ClassConfigurationChain; + + inferenceRuleForFieldAccess< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassFieldAccess): ClassConfigurationChain; finish(): TypeInitializer; } @@ -269,17 +287,26 @@ class ClassConfigurationChainImpl implements C }; } - inferenceRuleForClassDeclaration(rule: InferCurrentTypeRule): ClassConfigurationChain { + inferenceRuleForClassDeclaration< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): ClassConfigurationChain { this.typeDetails.inferenceRulesForClassDeclaration.push(rule as unknown as InferCurrentTypeRule); return this; } - inferenceRuleForClassLiterals(rule: InferClassLiteral): ClassConfigurationChain { + inferenceRuleForClassLiterals< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassLiteral): ClassConfigurationChain { this.typeDetails.inferenceRulesForClassLiterals.push(rule as unknown as InferClassLiteral); return this; } - inferenceRuleForFieldAccess(rule: InferClassFieldAccess): ClassConfigurationChain { + inferenceRuleForFieldAccess< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassFieldAccess): ClassConfigurationChain { this.typeDetails.inferenceRulesForFieldAccess.push(rule as unknown as InferClassFieldAccess); return this; } diff --git a/packages/typir/src/kinds/custom/custom-kind.ts b/packages/typir/src/kinds/custom/custom-kind.ts index c3fec2d..6622066 100644 --- a/packages/typir/src/kinds/custom/custom-kind.ts +++ b/packages/typir/src/kinds/custom/custom-kind.ts @@ -9,7 +9,7 @@ import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference } from '../../initialization/type-reference.js'; import { ConversionMode } from '../../services/conversion.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule } from '../../utils/utils-definitions.js'; +import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; import { isMap, isSet } from '../../utils/utils.js'; import { Kind } from '../kind.js'; import { CustomTypeInitialization, CustomTypeProperties, CustomTypePropertyInitialization, CustomTypePropertyTypes, CustomTypeStorage, TypeDescriptorForCustomTypes } from './custom-definitions.js'; @@ -70,7 +70,11 @@ export interface CustomFactoryService { - inferenceRule(rule: InferCurrentTypeRule, Specifics, T>): CustomTypeConfigurationChain; + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule, Specifics, LanguageKey, LanguageType>): CustomTypeConfigurationChain; + finish(): TypeInitializer, Specifics>; } @@ -168,7 +172,10 @@ class CustomConfigurationChainImpl(rule: InferCurrentTypeRule, Specifics, T>): CustomConfigurationChainImpl { + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule, Specifics, LanguageKey, LanguageType>): CustomConfigurationChainImpl { this.typeDetails.inferenceRules.push(rule as unknown as InferCurrentTypeRule, Specifics>); return this; } diff --git a/packages/typir/src/kinds/function/function-inference-call.ts b/packages/typir/src/kinds/function/function-inference-call.ts index 449c669..265e125 100644 --- a/packages/typir/src/kinds/function/function-inference-call.ts +++ b/packages/typir/src/kinds/function/function-inference-call.ts @@ -8,6 +8,7 @@ import { Type } from '../../graph/type-node.js'; import { AssignabilitySuccess, isAssignabilityProblem } from '../../services/assignability.js'; import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceResultWithInferringChildren, TypeInferenceRuleWithInferringChildren } from '../../services/inference.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; import { FunctionTypeDetails, InferFunctionCall } from './function-kind.js'; import { AvailableFunctionsManager } from './function-overloading.js'; @@ -25,14 +26,18 @@ import { FunctionType } from './function-type.js'; * - the current function has an output type/parameter, otherwise, this function could not provide any type (and throws an error), when it is called! * (exception: the options contain a type to return in this special case) */ -export class FunctionCallInferenceRule implements TypeInferenceRuleWithInferringChildren { +export class FunctionCallInferenceRule< + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, +> implements TypeInferenceRuleWithInferringChildren { protected readonly typeDetails: FunctionTypeDetails; - protected readonly inferenceRuleForCalls: InferFunctionCall; + protected readonly inferenceRuleForCalls: InferFunctionCall; protected readonly functionType: FunctionType; protected readonly functions: AvailableFunctionsManager; assignabilitySuccess: Array; // public, since this information is exploited to determine the best overloaded match in case of multiple matches - constructor(typeDetails: FunctionTypeDetails, inferenceRuleForCalls: InferFunctionCall, functionType: FunctionType, functions: AvailableFunctionsManager) { + constructor(typeDetails: FunctionTypeDetails, inferenceRuleForCalls: InferFunctionCall, functionType: FunctionType, functions: AvailableFunctionsManager) { this.typeDetails = typeDetails; this.inferenceRuleForCalls = inferenceRuleForCalls; this.functionType = functionType; @@ -44,13 +49,13 @@ export class FunctionCallInferenceRule); if (!result) { // the language node has a completely different purpose return InferenceRuleNotApplicable; } // 2. Does the inference rule match this language node? - const matching = this.inferenceRuleForCalls.matching === undefined || this.inferenceRuleForCalls.matching(languageNode as T, this.functionType); + const matching = this.inferenceRuleForCalls.matching === undefined || this.inferenceRuleForCalls.matching(languageNode as LanguageType, this.functionType); if (!matching) { // the language node is slightly different return InferenceRuleNotApplicable; @@ -68,7 +73,7 @@ export class FunctionCallInferenceRule infer the types of the parameters now - const inputArguments = this.inferenceRuleForCalls.inputArguments(languageNode as T); + const inputArguments = this.inferenceRuleForCalls.inputArguments(languageNode as LanguageType); return inputArguments; } diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index aeb1b8a..0efc9d7 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -10,7 +10,7 @@ import { TypeReference } from '../../initialization/type-reference.js'; import { TypeDescriptor } from '../../initialization/type-descriptor.js'; import { ValidationRule } from '../../services/validation.js'; import { TypirSpecifics, TypirServices } from '../../typir.js'; -import { InferCurrentTypeRule, NameTypePair, RegistrationOptions } from '../../utils/utils-definitions.js'; +import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, NameTypePair, RegistrationOptions } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { Kind, KindOptions } from '../kind.js'; import { FunctionTypeInitializer } from './function-initializer.js'; @@ -49,17 +49,19 @@ export interface FunctionTypeDetails extends T export interface CreateFunctionTypeDetails extends FunctionTypeDetails { inferenceRulesForDeclaration: Array>, - inferenceRulesForCalls: Array>, + inferenceRulesForCalls: Array>, } export interface InferFunctionCall< - Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType'] -> extends InferCurrentTypeRule { + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, +> extends InferCurrentTypeRule { /** * In case of overloaded functions, these input arguments are used to determine the actual function * by comparing the types of the given arguments with the expected types of the input parameters of the function. */ - inputArguments: (languageNode: T) => Array; + inputArguments: (languageNode: LanguageType) => Array; /** * This property controls the builtin validation which checks, whether the types of the given arguments of the function call @@ -83,7 +85,7 @@ export interface InferFunctionCall< * While different values for this property for different overloads are possible in theory with the defined behaviour, * in practise this seems to be rarely useful. */ - validateArgumentsOfFunctionCalls?: boolean | ((languageNode: T) => boolean); + validateArgumentsOfFunctionCalls?: boolean | ((languageNode: LanguageType) => boolean); } /** @@ -123,9 +125,16 @@ export interface FunctionFactoryService { export interface FunctionConfigurationChain { /** for function declarations => returns the funtion type (the whole signature including all names) */ - inferenceRuleForDeclaration(rule: InferCurrentTypeRule): FunctionConfigurationChain; + inferenceRuleForDeclaration< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): FunctionConfigurationChain; + /** for function calls => returns the return type of the function */ - inferenceRuleForCalls(rule: InferFunctionCall): FunctionConfigurationChain, + inferenceRuleForCalls< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferFunctionCall): FunctionConfigurationChain, // TODO for function references (like the declaration, but without any names!) => returns signature (without any names) @@ -268,12 +277,18 @@ class FunctionConfigurationChainImpl implement }; } - inferenceRuleForDeclaration(rule: InferCurrentTypeRule): FunctionConfigurationChain { + inferenceRuleForDeclaration< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): FunctionConfigurationChain { this.currentFunctionDetails.inferenceRulesForDeclaration.push(rule as unknown as InferCurrentTypeRule); return this; } - inferenceRuleForCalls(rule: InferFunctionCall): FunctionConfigurationChain { + inferenceRuleForCalls< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferFunctionCall): FunctionConfigurationChain { this.currentFunctionDetails.inferenceRulesForCalls.push(rule as unknown as InferFunctionCall); return this; } diff --git a/packages/typir/src/kinds/function/function-overloading.ts b/packages/typir/src/kinds/function/function-overloading.ts index 711a4f2..e1a097f 100644 --- a/packages/typir/src/kinds/function/function-overloading.ts +++ b/packages/typir/src/kinds/function/function-overloading.ts @@ -9,6 +9,7 @@ import { Type } from '../../graph/type-node.js'; import { CompositeTypeInferenceRule } from '../../services/inference.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; import { RuleRegistry } from '../../utils/rule-registration.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; import { removeFromArray } from '../../utils/utils.js'; import { OverloadedFunctionsTypeInferenceRule } from './function-inference-overloaded.js'; import { FunctionKind, InferFunctionCall } from './function-kind.js'; @@ -30,9 +31,13 @@ export interface OverloadedFunctionDetails { sameOutputType: Type | undefined; } -export interface SingleFunctionDetails { +export interface SingleFunctionDetails< + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, +> { functionType: FunctionType; - inferenceRuleForCalls: InferFunctionCall; + inferenceRuleForCalls: InferFunctionCall; } @@ -103,7 +108,7 @@ export class AvailableFunctionsManager impleme return this.mapNameTypes.entries(); } - addFunction(readyFunctionType: FunctionType, inferenceRulesForCalls: Array>): void { + addFunction(readyFunctionType: FunctionType, inferenceRulesForCalls: Array>): void { const overloaded = this.getOrCreateOverloads(readyFunctionType.functionName); // remember the function type itself diff --git a/packages/typir/src/kinds/function/function-validation-calls.ts b/packages/typir/src/kinds/function/function-validation-calls.ts index b4c922d..26878e9 100644 --- a/packages/typir/src/kinds/function/function-validation-calls.ts +++ b/packages/typir/src/kinds/function/function-validation-calls.ts @@ -18,9 +18,9 @@ import { FunctionType } from './function-type.js'; /** * This validation uses the inference rules for all available function calls to check, whether ... - * - the given arguments for a function call fit to one of the defined function signature + * - the given arguments for a function call fit to one of the defined function signatures * - and validates this call according to the specific validation rules for this function call. - * There is only one instance of this class for each function kind/manager. + * There is only one instance of this class for each function kind/factory/manager. */ export class FunctionCallArgumentsValidation implements ValidationRuleLifecycle, RuleCollectorListener> { protected readonly services: TypirServices; @@ -31,7 +31,7 @@ export class FunctionCallArgumentsValidation i this.functions = functions; } - onAddedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { + onAddedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { // this rule needs to be registered also for all the language keys of the new inner function call rule this.services.validation.Collector.addValidationRule(this, { ...diffOptions, @@ -39,7 +39,7 @@ export class FunctionCallArgumentsValidation i }); } - onRemovedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { + onRemovedRule(_rule: SingleFunctionDetails, diffOptions: RuleOptions): void { // remove this "composite" rule for all language keys for which no function call rules are registered anymore if (diffOptions.languageKey === undefined) { if (this.noFunctionCallRulesForThisLanguageKey(undefined)) { @@ -176,7 +176,7 @@ export class FunctionCallArgumentsValidation i } } - protected validateArgumentsOfFunctionCalls(rule: InferFunctionCall, languageNode: Specifics['LanguageType']): boolean { + protected validateArgumentsOfFunctionCalls(rule: InferFunctionCall, languageNode: Specifics['LanguageType']): boolean { if (rule.validateArgumentsOfFunctionCalls === undefined) { return false; // the default value } else if (typeof rule.validateArgumentsOfFunctionCalls === 'boolean') { diff --git a/packages/typir/src/kinds/primitive/primitive-kind.ts b/packages/typir/src/kinds/primitive/primitive-kind.ts index 6f96a31..630e8dd 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -6,7 +6,7 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; +import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, KindOptions } from '../kind.js'; import { PrimitiveType } from './primitive-type.js'; @@ -31,7 +31,10 @@ export interface PrimitiveFactoryService { } export interface PrimitiveConfigurationChain { - inferenceRule(rule: InferCurrentTypeRule): PrimitiveConfigurationChain; + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): PrimitiveConfigurationChain; finish(): PrimitiveType; } @@ -90,7 +93,10 @@ class PrimitiveConfigurationChainImpl implemen }; } - inferenceRule(rule: InferCurrentTypeRule): PrimitiveConfigurationChain { + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): PrimitiveConfigurationChain { this.typeDetails.inferenceRules.push(rule as unknown as InferCurrentTypeRule); return this; } diff --git a/packages/typir/src/kinds/top/top-kind.ts b/packages/typir/src/kinds/top/top-kind.ts index 4e76edf..29c6631 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -6,7 +6,7 @@ import { TypeDetails } from '../../graph/type-node.js'; import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; +import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, KindOptions } from '../kind.js'; import { TopType } from './top-type.js'; @@ -30,7 +30,10 @@ export interface TopFactoryService { } export interface TopConfigurationChain { - inferenceRule(rule: InferCurrentTypeRule): TopConfigurationChain; + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): TopConfigurationChain; finish(): TopType; } @@ -91,7 +94,10 @@ class TopConfigurationChainImpl implements Top }; } - inferenceRule(rule: InferCurrentTypeRule): TopConfigurationChain { + inferenceRule< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): TopConfigurationChain { this.typeDetails.inferenceRules.push(rule as unknown as InferCurrentTypeRule); return this; } diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 3524f80..781b233 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -52,17 +52,25 @@ export type TypeInferenceResultWithInferringChildren = TypeInferenceRuleWithoutInferringChildren | TypeInferenceRuleWithInferringChildren; +export type TypeInferenceRule< + Specifics extends TypirSpecifics, + InputType extends Specifics['LanguageType'] = Specifics['LanguageType'] +> = TypeInferenceRuleWithoutInferringChildren | TypeInferenceRuleWithInferringChildren; /** Usual inference rule which don't depend on children's types. */ -export type TypeInferenceRuleWithoutInferringChildren = - (languageNode: InputType, typir: TypirServices) => TypeInferenceResultWithoutInferringChildren; +export type TypeInferenceRuleWithoutInferringChildren< + Specifics extends TypirSpecifics, + InputType extends Specifics['LanguageType'] = Specifics['LanguageType'] +> = (languageNode: InputType, typir: TypirServices) => TypeInferenceResultWithoutInferringChildren; /** * Inference rule which requires for the type inference of the given parent to take the types of its children into account. * Therefore, the types of the children need to be inferred first. */ -export interface TypeInferenceRuleWithInferringChildren { +export interface TypeInferenceRuleWithInferringChildren< + Specifics extends TypirSpecifics, + InputType extends Specifics['LanguageType'] = Specifics['LanguageType'] +> { /** * 1st step is to check, whether this inference rule is applicable to the given language node. * @param languageNode the language node whose type shall be inferred diff --git a/packages/typir/src/services/operator.ts b/packages/typir/src/services/operator.ts index efb2390..799c0b6 100644 --- a/packages/typir/src/services/operator.ts +++ b/packages/typir/src/services/operator.ts @@ -9,7 +9,7 @@ import { TypeInitializer } from '../initialization/type-initializer.js'; import { FunctionFactoryService, NO_PARAMETER_NAME } from '../kinds/function/function-kind.js'; import { FunctionType } from '../kinds/function/function-type.js'; import { TypirSpecifics, TypirServices } from '../typir.js'; -import { NameTypePair } from '../utils/utils-definitions.js'; +import { LanguageKeys, NameTypePair } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; import { ValidationProblemAcceptor } from './validation.js'; @@ -291,9 +291,11 @@ class OperatorConfigurationGenericChainImpl im }); // infer the operator when the operator is called! for (const inferenceRule of this.typeDetails.inferenceRules) { - newOperatorType.inferenceRuleForCalls({ + newOperatorType.inferenceRuleForCalls, Specifics['LanguageType']>({ languageKey: inferenceRule.languageKey, - filter: inferenceRule.filter ? ((languageNode: Specifics['LanguageType']): languageNode is Specifics['LanguageType'] => inferenceRule.filter!(languageNode, this.typeDetails.name)) : undefined, + filter: inferenceRule.filter + ? ((languageNode: Specifics['LanguageType']): languageNode is Specifics['LanguageType'] => inferenceRule.filter!(languageNode, this.typeDetails.name)) + : undefined, matching: (languageNode: Specifics['LanguageType']) => inferenceRule.matching(languageNode, this.typeDetails.name), inputArguments: (languageNode: Specifics['LanguageType']) => this.getInputArguments(inferenceRule, languageNode), validation: toArray(inferenceRule.validation).map(validationRule => diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 126a798..5b0c53b 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -15,7 +15,7 @@ import { ProblemPrinter } from './printing.js'; export type Severity = 'error' | 'warning' | 'info' | 'hint'; -export interface ValidationMessageProperties { // Using this type only the TypirSpecifics (and not directly in the ValidationProblem below) enables to customize its properties. +export interface ValidationMessageProperties { // Using this type only in the TypirSpecifics (and not directly in the ValidationProblem below) enables to customize its properties. severity: Severity; message: string; subProblems?: TypirProblem[]; diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index b918747..98632f9 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -5,6 +5,8 @@ ******************************************************************************/ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/indent */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { isType, Type } from '../graph/type-node.js'; import { TypeInitializer } from '../initialization/type-initializer.js'; @@ -48,9 +50,14 @@ export interface ValidationRuleWithOptions>; } -export function bindValidateCurrentTypeRule( - rule: InferCurrentTypeRule, type: TypeType -): ValidationRuleWithOptions | undefined { +export function bindValidateCurrentTypeRule< + TypeType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + rule: InferCurrentTypeRule, type: TypeType +): ValidationRuleWithOptions | undefined { // check the given rule checkRule(rule); // fail early if (toArray(rule.validation).length <= 0) { // there are no checks => don't create a validation rule! @@ -110,6 +117,16 @@ export function inferenceOptionsBoundToType = keyof Specifics['LanguageKeys']; +export type LanguageKeys = LanguageKey | Array> | undefined; +export type LanguageTypeOfLanguageKey< + Specifics extends TypirSpecifics, + Keys extends LanguageKeys +> = + Keys extends undefined ? Specifics['LanguageType'] : // no key => use the base language type + Keys extends keyof Specifics['LanguageKeys'] ? Specifics['LanguageKeys'][Keys] : // single key => use the specified language type from the "list type" + Keys extends Array ? Specifics['LanguageType'] : // multiple keys => use the base language type as fall-back; TODO is it possible to improve this with a union of the GivenKeys? + never; /** * An inference rule which is dedicated for inferrring a certain type. @@ -117,19 +134,20 @@ export function inferenceOptionsBoundToType = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, > { - languageKey?: (keyof Specifics['LanguageKeys']) | Array; - filter?: (languageNode: Specifics['LanguageType']) => languageNode is T; - matching?: (languageNode: T, typeToInfer: TypeType) => boolean; + languageKey?: LanguageKey; + filter?: (languageNode: LanguageTypeOfLanguageKey) => languageNode is LanguageType; + matching?: (languageNode: LanguageType, typeToInfer: TypeType) => boolean; /** * This validation will be applied to all language nodes for which the current type is inferred according to this inference rule. * This validation is specific for this inference rule and this inferred type. */ - validation?: InferCurrentTypeValidationRule | Array>; + validation?: InferCurrentTypeValidationRule | Array>; skipThisRuleIfThisTypeAlreadyExists?: boolean | ((existingType: TypeType) => boolean); // default is false } @@ -142,8 +160,13 @@ export type InferCurrentTypeValidationRule< (languageNode: T, inferredType: TypeType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; -export function skipInferenceRuleForExistingType( - inferenceRule: InferCurrentTypeRule, newType: TypeType, existingType: TypeType +export function skipInferenceRuleForExistingType< + TypeType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + inferenceRule: InferCurrentTypeRule, newType: TypeType, existingType: TypeType ): boolean { if (newType !== existingType) { const skipRuleForExisting = inferenceRule.skipThisRuleIfThisTypeAlreadyExists; @@ -153,17 +176,27 @@ export function skipInferenceRuleForExistingType( - rule: InferCurrentTypeRule +function checkRule< + TypeType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + rule: InferCurrentTypeRule ): void { if (rule.languageKey === undefined && rule.filter === undefined && rule.matching === undefined) { throw new Error('This inference rule has none of the properties "languageKey", "filter" and "matching" at all and therefore cannot infer any type!'); } } -export function bindInferCurrentTypeRule( - rule: InferCurrentTypeRule, type: TypeType -): InferenceRuleWithOptions { +export function bindInferCurrentTypeRule< + TypeType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + rule: InferCurrentTypeRule, type: TypeType +): InferenceRuleWithOptions { checkRule(rule); // fail early return { rule: (languageNode, _typir) => { @@ -184,7 +217,7 @@ export function bindInferCurrentTypeRule Date: Mon, 1 Sep 2025 12:42:40 +0200 Subject: [PATCH 03/17] improved the changelog, wrote documentation --- CHANGELOG.md | 21 +++++++- README.md | 1 - documentation/customization.md | 3 +- documentation/design.md | 77 ++++++++++++++++++++++------- documentation/services/inference.md | 2 +- documentation/services/language.md | 2 +- 6 files changed, 83 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7c75ea..2efcf38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,27 @@ Note that the versions "0.x.0" probably will include breaking changes. For each minor and major version, there is a corresponding [milestone on GitHub](https://github.com/TypeFox/typir/milestones). + ## v0.4.0 (2025-??-??) [Linked issues and PRs for v0.4.0](https://github.com/TypeFox/typir/milestone/5) ### New features +- Introduced `TypirSpecifics['LanguageKeys']` (in `typir.ts`) to make the available language nodes with their keys and TypeScript types explicit (#93): + - By default, Typir don't predefine any language nodes in advance (i.e. any `string` values are usable as language key), while Typir-Langium supports exactly the generated types in the `ast.ts`. + - When restricting the possible language keys, now the user gets informed by the TypeScript compiler, if other language keys are used, e.g. inside inference rules or for registering validation rules. + - If language keys are restricted, inside inference rules with value for `languageKey` and without value for `filter`, it is possible now to skip the expected TypeScript type for the input node of the `matching` property, as demonstrated in the updated examples for (L)OX. This improves the usability of the API. + ### Breaking changes +- Renamed `TypirLangiumSpecifics['AstTypes']` to `TypirLangiumSpecifics['LanguageKeys']` to align it with the new `TypirSpecifics['LanguageKeys']`, as described above (#93) + ### Fixed bugs +- + + ## v0.3.3 (2026-02-10) @@ -23,6 +34,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - Updated Typir-Langium to Langium v4.2.0 (#103). + ## v0.3.2 (2026-01-13) ### Fixed bugs @@ -30,6 +42,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - Use browser-safe `isSet` and `isMap` implementation to fix #96 (#98, #97). + ## v0.3.1 (2025-11-27) ### New features @@ -43,6 +56,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - When checking the equality of custom types, the values for the same property might have different TypeScript types, since optional properties might be set to `undefined` (#94). + ## v0.3.0 (2025-08-15) [Linked issues and PRs for v0.3.0](https://github.com/TypeFox/typir/milestone/4) @@ -74,7 +88,6 @@ For each minor and major version, there is a corresponding [milestone on GitHub] LanguageType: unknown; } ``` -TODO - `TypirLangiumSpecifics` extends the Typir specifics for Langium, concretizes the language type and enables to register the available AST types of the current Langium grammar as `AstTypes`: ```typescript @@ -106,6 +119,7 @@ TODO - Fixed the implementation for merging modules for dependency injection (DI), it is exactly the same fix from [Langium](https://github.com/eclipse-langium/langium/pull/1939), since we reused its DI implementation (#79). + ## v0.2.2 (2025-08-01) - Fixed wrong imports of `assertUnreachable` (#86) @@ -113,11 +127,13 @@ TODO - Updated Typir-Langium to Langium v3.5 (#88) + ## v0.2.1 (2025-04-09) - Export `test-utils.ts` which are using `vitest` via the new namespace `'typir/test'` in order to not pollute production code with vitest dependencies (#68) + ## v0.2.0 (2025-03-31) [Linked issues and PRs for v0.2.0](https://github.com/TypeFox/typir/milestone/3) @@ -174,12 +190,14 @@ TODO - The inference logic in case of zero arguments (e.g. for function calls or class literals) was not accurate enough (#64). + ## v0.1.2 (2024-12-20) - Replaced absolute paths in READMEs by relative paths, which is a requirement for correct links on NPM - Edit: Note that the tag for this release was accidentally added on the branch `jm/v0.1.2`, not on the `main` branch. + ## v0.1.1 (2024-12-20) - Improved the READMEs in the packages `typir` and `typir-langium`. @@ -187,6 +205,7 @@ TODO - Improved source code for Tiny Typir in `api-example.test.ts`. + ## v0.1.0 (2024-12-20) This is the first official release of Typir. diff --git a/README.md b/README.md index 80ce297..f781fd7 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,6 @@ The roadmap includes, among other, these features: - More predefined types: structurally typed classes, lambdas, generics, constrained primitive types (e.g. numbers with upper and lower bound), ... - Calculate types, e.g. operators whose return types depend on their current input types -- Simplified API for custom types For the released versions of Typir, see the [CHANGELOG.md](./CHANGELOG.md). diff --git a/documentation/customization.md b/documentation/customization.md index 0e97dc9..b960113 100644 --- a/documentation/customization.md +++ b/documentation/customization.md @@ -9,7 +9,7 @@ As described in the [design section](./design.md), nearly all features of Typir for which Typir provides classes implementing these interfaces as default implementations. These interfaces and implementations are composed in ... - `typir.ts` for Typir (core) -- `typir-langium.ts` for Typir-Langium +- `typir-langium.ts` for Typir-Langium, reusing and adjusting the Typir core services Some examples how to customize existing services and how to add new services are sketched in `customization-example.test.ts`. @@ -28,6 +28,7 @@ const customizedTypir = createTypirServices({ }); ``` + ## Add additional services Additional services need to be explicitly specified. diff --git a/documentation/design.md b/documentation/design.md index cf802bf..4604ea7 100644 --- a/documentation/design.md +++ b/documentation/design.md @@ -1,10 +1,12 @@ # Design -This describes the main design principles of Typir. +This describes the main design principles of and the terminology used by Typir. -## Core principles -### Type +## Type + +Each type exists only once in a Typir instance. Types at runtime are instances of a sub-class of the TypeScript class `Type`. +Two different instances of `Type` represent two different types in Typir. All types need to have *unique identifiers* in order to identifier duplicate types and to access types by their identifier. If Typir reports errors regarding non-unique identifiers, check the following possibles reasons for colliding identifiers: @@ -14,26 +16,70 @@ If Typir reports errors regarding non-unique identifiers, check the following po Types also have a *name*, which is used as a short name for types, e.g. used to be shown in error messages to users. Names don't need to be unique. -TODO: +TODO: states/lifecycle of a type + +Each type has exactly one kind, as explained below. + -- single instances -- kind +## Kind / Factory -### Kind -### Type graph +## Type graph -Each type system, i.e. each instance of the `TypirServices`, has one type graph: +Each type system, i.e. each instance of the `TypirServices`, has one type graph, which stores the available types and their relationships: - nodes are types, e.g. primitive types and function types - edges are relationships between types, e.g. edges representing implicit conversion between two types -### Incrementality (under construction) + +## Incrementality (under construction) - add/remove types - add/remove rules and relationships -### Services and default implementations + +## Language + +Usually, type systems are created to do some type checking on textual languages, including domain-specific languages (DSLs) and general-purpose programming languages. Programs respective text conforming to these languages are parsed and provided as abstract syntax trees (ASTs) in-memory. +ASTs usually consists of a tree of nodes (realized as JavaScript objects at runtime), which represent a small part of the parsed program/text. +Establishing cross-references between the nodes of the tree after parsing results in a graph after linking. +Type checking is done on these ASTs. + +### Language node + +Since Typir has no preconditions regarding the structure of the AST or the technical details of the AST nodes in order to provide type checking for any data structure, +the term *language node* is used to describe a single node in the AST or a single element in a complex data structure. +As an example, in the context of Langium each `AstNode` is a language node in Typir. + +While the definition of types and their relationships is independent from the AST, +type inference and validations are done on language nodes, +e.g. an inference rule gets a language node as input and returns its inferred Typir type. +All information Typir needs to know about language nodes is specified in the APIs, including the APIs for inference rules, validations rules and the [language service](./services/language.md). + +### Language key + +Each language node might have a *language key*. +Language keys are `string` values and are used to increase performance by registering rules for inference and validation not for all language nodes, +but only for language nodes with a particular language node. +Rules associated to no language key are applied to all language nodes. +Rules might be associated to multiple language keys. +Getting the language key of a language node is done by the [language service](./services/language.md). +The available language keys could be restricted by customizing the specifics of your language in this way: + +```typescript +export type MyAstTypes = { + LanguageKey1: LanguageType1; + LanguageKey2: LanguageType2; + // ... +} + +export interface MySpecifics extends TypirSpecifics { + LanguageKeys: MyAstTypes; +} +``` + + +## Services and default implementations - services - (default) implementations @@ -41,10 +87,5 @@ Each type system, i.e. each instance of the `TypirServices`, has one type graph: - It is possible to group services - Names of services start with an uppercase letter, names of groups start with a lowercase letter - Dependency injection (DI) - - -## Terminology / Glossary - -- inference: inference rule, type inference -- language node, language key -- ... + - cyclic dependencies + - compile time vs runtime diff --git a/documentation/services/inference.md b/documentation/services/inference.md index 1d16d42..0d56f91 100644 --- a/documentation/services/inference.md +++ b/documentation/services/inference.md @@ -2,7 +2,7 @@ Type inference infers a Typir type for a given language node, i.e. it answers the question, which Tyir type has a language node. Therefore type inference is the central part which connects the type system and its type graph with an AST consisting of language nodes. -These relationships are defined with *inference rules*, which identify the type for some language nodes. +These relationships are defined with *inference rules*, which identify the type for a given language node. ## API diff --git a/documentation/services/language.md b/documentation/services/language.md index af374d4..5684db2 100644 --- a/documentation/services/language.md +++ b/documentation/services/language.md @@ -11,7 +11,7 @@ these rules are applied only to those language nodes which have this language ke It is possible to associate rules to multiple language keys. Rules which are associated to no language key, are applied to all language nodes. -Language keys are represented by string values and might be depending on the DSL implementation/language workbench, +Language keys are represented by `string` values and might be depending on the DSL implementation/language workbench, class names or `$type`-property-information of the language node implementations. Language keys might have sub/super language keys ("sub-type relationship of language keys"). From 65d6bf85259095a80ebb74e1af28406bc06c7d5f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 1 Sep 2025 13:32:21 +0200 Subject: [PATCH 04/17] refactorings: moved existing definitions into another file, used existing definitions even more --- .../src/features/langium-inference.ts | 4 ++-- .../src/features/langium-language.ts | 12 +++++----- .../src/features/langium-validation.ts | 4 ++-- .../typir/src/kinds/bottom/bottom-kind.ts | 4 ++-- .../src/kinds/class/class-initializer.ts | 4 ++-- packages/typir/src/kinds/class/class-kind.ts | 6 ++--- .../typir/src/kinds/custom/custom-kind.ts | 4 ++-- .../kinds/function/function-inference-call.ts | 3 +-- .../typir/src/kinds/function/function-kind.ts | 6 ++--- .../kinds/function/function-overloading.ts | 3 +-- .../function/function-validation-calls.ts | 6 ++--- .../src/kinds/primitive/primitive-kind.ts | 4 ++-- packages/typir/src/kinds/top/top-kind.ts | 4 ++-- packages/typir/src/services/inference.ts | 4 ++-- packages/typir/src/services/language.ts | 14 +++++------ packages/typir/src/services/operator.ts | 4 ++-- packages/typir/src/services/validation.ts | 4 ++-- packages/typir/src/typir.ts | 23 +++++++++++++++++++ packages/typir/src/utils/rule-registration.ts | 16 ++++++------- packages/typir/src/utils/utils-definitions.ts | 15 +----------- 20 files changed, 76 insertions(+), 68 deletions(-) diff --git a/packages/typir-langium/src/features/langium-inference.ts b/packages/typir-langium/src/features/langium-inference.ts index e7fa68d..b045cad 100644 --- a/packages/typir-langium/src/features/langium-inference.ts +++ b/packages/typir-langium/src/features/langium-inference.ts @@ -4,11 +4,11 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { DefaultTypeInferenceCollector, TypeInferenceCollector, TypeInferenceRule } from 'typir'; +import { DefaultTypeInferenceCollector, LanguageKey, TypeInferenceCollector, TypeInferenceRule } from 'typir'; import { TypirLangiumSpecifics } from '../typir-langium.js'; export type LangiumTypeInferenceRules = { - [K in keyof Specifics['LanguageKeys']]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? TypeInferenceRule | Array> : never + [K in LanguageKey]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? TypeInferenceRule | Array> : never } & { AstNode?: TypeInferenceRule | Array>; } diff --git a/packages/typir-langium/src/features/langium-language.ts b/packages/typir-langium/src/features/langium-language.ts index 1060756..6f3ddef 100644 --- a/packages/typir-langium/src/features/langium-language.ts +++ b/packages/typir-langium/src/features/langium-language.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { AbstractAstReflection, isAstNode } from 'langium'; -import { DefaultLanguageService, LanguageService, removeFromArray } from 'typir'; +import { DefaultLanguageService, LanguageKey, LanguageService, removeFromArray } from 'typir'; import { TypirLangiumSpecifics } from '../typir-langium.js'; /** @@ -14,27 +14,27 @@ import { TypirLangiumSpecifics } from '../typir-langium.js'; */ export class LangiumLanguageService extends DefaultLanguageService implements LanguageService { protected readonly reflection: AbstractAstReflection; - protected superKeys: Map> | undefined = undefined; // key => all its super-keys + protected superKeys: Map, Array>> | undefined = undefined; // key => all its super-keys constructor(reflection: AbstractAstReflection) { super(); this.reflection = reflection; } - override getLanguageNodeKey(languageNode: Specifics['LanguageType']): keyof Specifics['LanguageKeys'] { + override getLanguageNodeKey(languageNode: Specifics['LanguageType']): LanguageKey { return languageNode.$type; } - override getAllSubKeys(languageKey: keyof Specifics['LanguageKeys']): Array { + override getAllSubKeys(languageKey: LanguageKey): Array> { const result = this.reflection.getAllSubTypes(languageKey as string); removeFromArray(languageKey, result); // Langium adds the given type in the list of all sub-types, therefore it must be removed here return result; } - override getAllSuperKeys(languageKey: keyof Specifics['LanguageKeys']): Array { + override getAllSuperKeys(languageKey: LanguageKey): Array> { if (this.superKeys === undefined) { // collect all super types (Sets ensure uniqueness of super-keys) - const map: Map> = new Map(); + const map: Map, Set>> = new Map(); for (const superKey of this.reflection.getAllTypes()) { for (const subKey of this.getAllSubKeys(superKey)) { let entries = map.get(subKey); diff --git a/packages/typir-langium/src/features/langium-validation.ts b/packages/typir-langium/src/features/langium-validation.ts index 91d939d..901130e 100644 --- a/packages/typir-langium/src/features/langium-validation.ts +++ b/packages/typir-langium/src/features/langium-validation.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { LangiumDefaultCoreServices, Properties, ValidationAcceptor, ValidationChecks } from 'langium'; -import { DefaultValidationCollector, TypirServices, ValidationCollector, ValidationProblem, ValidationRule } from 'typir'; +import { DefaultValidationCollector, LanguageKey, TypirServices, ValidationCollector, ValidationProblem, ValidationRule } from 'typir'; import { TypirLangiumServices, TypirLangiumSpecifics } from '../typir-langium.js'; export function registerTypirValidationChecks(langiumServices: LangiumDefaultCoreServices, typirServices: TypirLangiumServices) { @@ -110,7 +110,7 @@ export class DefaultLangiumTypirValidator = { - [K in keyof Specifics['LanguageKeys']]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? ValidationRule | Array> : never + [K in LanguageKey]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? ValidationRule | Array> : never } & { AstNode?: ValidationRule | Array>; } diff --git a/packages/typir/src/kinds/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index d58b5a1..590fd26 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -5,8 +5,8 @@ ******************************************************************************/ import { TypeDetails } from '../../graph/type-node.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, KindOptions } from '../kind.js'; import { BottomType } from './bottom-type.js'; diff --git a/packages/typir/src/kinds/class/class-initializer.ts b/packages/typir/src/kinds/class/class-initializer.ts index c4a7358..fbc640d 100644 --- a/packages/typir/src/kinds/class/class-initializer.ts +++ b/packages/typir/src/kinds/class/class-initializer.ts @@ -7,8 +7,8 @@ import { isType, Type, TypeStateListener } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceRule } from '../../services/inference.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, InferenceRuleWithOptions, inferenceOptionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions, LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { bindInferCurrentTypeRule, bindValidateCurrentTypeRule, inferenceOptionsBoundToType, InferenceRuleWithOptions, skipInferenceRuleForExistingType, ValidationRuleWithOptions } from '../../utils/utils-definitions.js'; import { checkNameTypesMap, createTypeCheckStrategy, MapListConverter } from '../../utils/utils-type-comparison.js'; import { assertTypirType, toArray } from '../../utils/utils.js'; import { ClassKind, CreateClassTypeDetails, InferClassLiteral } from './class-kind.js'; diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index 3c7646b..4853264 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -5,13 +5,13 @@ ******************************************************************************/ import { Type, TypeDetails } from '../../graph/type-node.js'; +import { TypeDescriptor } from '../../initialization/type-descriptor.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference } from '../../initialization/type-reference.js'; -import { TypeDescriptor } from '../../initialization/type-descriptor.js'; import { InferenceRuleNotApplicable } from '../../services/inference.js'; import { ValidationRule } from '../../services/validation.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, RegistrationOptions } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { InferCurrentTypeRule, RegistrationOptions } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { assertTrue, assertTypirType, assertUnreachable, toArray } from '../../utils/utils.js'; import { FunctionType } from '../function/function-type.js'; diff --git a/packages/typir/src/kinds/custom/custom-kind.ts b/packages/typir/src/kinds/custom/custom-kind.ts index 6622066..3b7e7a6 100644 --- a/packages/typir/src/kinds/custom/custom-kind.ts +++ b/packages/typir/src/kinds/custom/custom-kind.ts @@ -8,8 +8,8 @@ import { Type, TypeDetails } from '../../graph/type-node.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference } from '../../initialization/type-reference.js'; import { ConversionMode } from '../../services/conversion.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { InferCurrentTypeRule } from '../../utils/utils-definitions.js'; import { isMap, isSet } from '../../utils/utils.js'; import { Kind } from '../kind.js'; import { CustomTypeInitialization, CustomTypeProperties, CustomTypePropertyInitialization, CustomTypePropertyTypes, CustomTypeStorage, TypeDescriptorForCustomTypes } from './custom-definitions.js'; diff --git a/packages/typir/src/kinds/function/function-inference-call.ts b/packages/typir/src/kinds/function/function-inference-call.ts index 265e125..cc0720b 100644 --- a/packages/typir/src/kinds/function/function-inference-call.ts +++ b/packages/typir/src/kinds/function/function-inference-call.ts @@ -7,8 +7,7 @@ import { Type } from '../../graph/type-node.js'; import { AssignabilitySuccess, isAssignabilityProblem } from '../../services/assignability.js'; import { InferenceProblem, InferenceRuleNotApplicable, TypeInferenceResultWithInferringChildren, TypeInferenceRuleWithInferringChildren } from '../../services/inference.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; import { checkTypeArrays } from '../../utils/utils-type-comparison.js'; import { FunctionTypeDetails, InferFunctionCall } from './function-kind.js'; import { AvailableFunctionsManager } from './function-overloading.js'; diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index 0efc9d7..e5b0731 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -5,12 +5,12 @@ ******************************************************************************/ import { Type, TypeDetails } from '../../graph/type-node.js'; +import { TypeDescriptor } from '../../initialization/type-descriptor.js'; import { TypeInitializer } from '../../initialization/type-initializer.js'; import { TypeReference } from '../../initialization/type-reference.js'; -import { TypeDescriptor } from '../../initialization/type-descriptor.js'; import { ValidationRule } from '../../services/validation.js'; -import { TypirSpecifics, TypirServices } from '../../typir.js'; -import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, NameTypePair, RegistrationOptions } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { InferCurrentTypeRule, NameTypePair, RegistrationOptions } from '../../utils/utils-definitions.js'; import { TypeCheckStrategy } from '../../utils/utils-type-comparison.js'; import { Kind, KindOptions } from '../kind.js'; import { FunctionTypeInitializer } from './function-initializer.js'; diff --git a/packages/typir/src/kinds/function/function-overloading.ts b/packages/typir/src/kinds/function/function-overloading.ts index e1a097f..f37e698 100644 --- a/packages/typir/src/kinds/function/function-overloading.ts +++ b/packages/typir/src/kinds/function/function-overloading.ts @@ -7,9 +7,8 @@ import { TypeGraphListener } from '../../graph/type-graph.js'; import { Type } from '../../graph/type-node.js'; import { CompositeTypeInferenceRule } from '../../services/inference.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; import { RuleRegistry } from '../../utils/rule-registration.js'; -import { LanguageKeys, LanguageTypeOfLanguageKey } from '../../utils/utils-definitions.js'; import { removeFromArray } from '../../utils/utils.js'; import { OverloadedFunctionsTypeInferenceRule } from './function-inference-overloaded.js'; import { FunctionKind, InferFunctionCall } from './function-kind.js'; diff --git a/packages/typir/src/kinds/function/function-validation-calls.ts b/packages/typir/src/kinds/function/function-validation-calls.ts index 26878e9..c168575 100644 --- a/packages/typir/src/kinds/function/function-validation-calls.ts +++ b/packages/typir/src/kinds/function/function-validation-calls.ts @@ -7,7 +7,7 @@ import { Type } from '../../graph/type-node.js'; import { InferenceProblem } from '../../services/inference.js'; import { ValidationProblem, ValidationProblemAcceptor, ValidationRuleLifecycle } from '../../services/validation.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; +import { LanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; import { RuleCollectorListener, RuleOptions } from '../../utils/rule-registration.js'; import { NameTypePair, TypirProblem } from '../../utils/utils-definitions.js'; import { checkTypes, checkValueForConflict, createTypeCheckStrategy, IndexedTypeConflict, ValueConflict } from '../../utils/utils-type-comparison.js'; @@ -59,7 +59,7 @@ export class FunctionCallArgumentsValidation i } } - protected noFunctionCallRulesForThisLanguageKey(key: undefined | (keyof Specifics['LanguageKeys'])): boolean { + protected noFunctionCallRulesForThisLanguageKey(key: undefined | LanguageKey): boolean { for (const overloads of this.functions.getAllOverloads()) { if (overloads[1].details.getRulesByLanguageKey(key).length >= 1) { return false; @@ -70,7 +70,7 @@ export class FunctionCallArgumentsValidation i validation(languageNode: Specifics['LanguageType'], accept: ValidationProblemAcceptor, _typir: TypirServices): void { // determine all keys to check - const keysToApply: Array<(keyof Specifics['LanguageKeys']) | undefined> = []; + const keysToApply: Array | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); diff --git a/packages/typir/src/kinds/primitive/primitive-kind.ts b/packages/typir/src/kinds/primitive/primitive-kind.ts index 630e8dd..df86fb2 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -5,8 +5,8 @@ ******************************************************************************/ import { TypeDetails } from '../../graph/type-node.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, KindOptions } from '../kind.js'; import { PrimitiveType } from './primitive-type.js'; diff --git a/packages/typir/src/kinds/top/top-kind.ts b/packages/typir/src/kinds/top/top-kind.ts index 29c6631..beae667 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -5,8 +5,8 @@ ******************************************************************************/ import { TypeDetails } from '../../graph/type-node.js'; -import { TypirServices, TypirSpecifics } from '../../typir.js'; -import { InferCurrentTypeRule, LanguageKeys, LanguageTypeOfLanguageKey, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../../typir.js'; +import { InferCurrentTypeRule, registerInferCurrentTypeRules } from '../../utils/utils-definitions.js'; import { assertTrue } from '../../utils/utils.js'; import { Kind, KindOptions } from '../kind.js'; import { TopType } from './top-type.js'; diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 781b233..91ca376 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { isType, Type } from '../graph/type-node.js'; -import { TypirSpecifics, TypirServices } from '../typir.js'; +import { LanguageKey, TypirServices, TypirSpecifics } from '../typir.js'; import { RuleCollectorListener, RuleOptions, RuleRegistry } from '../utils/rule-registration.js'; import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; import { assertUnreachable, removeFromArray, toArray } from '../utils/utils.js'; @@ -198,7 +198,7 @@ export class DefaultTypeInferenceCollector imp this.checkForError(languageNode); // determine all keys to check - const keysToApply: Array<(keyof Specifics['LanguageKeys']) | undefined> = []; + const keysToApply: Array | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); diff --git a/packages/typir/src/services/language.ts b/packages/typir/src/services/language.ts index d507cf6..0e3b68d 100644 --- a/packages/typir/src/services/language.ts +++ b/packages/typir/src/services/language.ts @@ -7,7 +7,7 @@ import { Type } from '../graph/type-node.js'; import { TypeInitializer } from '../initialization/type-initializer.js'; import { TypeReference } from '../initialization/type-reference.js'; -import { TypirSpecifics } from '../typir.js'; +import { LanguageKey, TypirSpecifics } from '../typir.js'; /** * This services provides some static information about the language/DSL, for which the type system is created. @@ -30,21 +30,21 @@ export interface LanguageService { * @param languageNode the given language node * @returns the language key or 'undefined', if there is no language key for the given language node */ - getLanguageNodeKey(languageNode: Specifics['LanguageType']): keyof Specifics['LanguageKeys'] | undefined; + getLanguageNodeKey(languageNode: Specifics['LanguageType']): LanguageKey | undefined; /** * Returns all keys, which are direct or indirect sub-keys of the given language key. * @param languageKey the given language key * @returns the list does not contain the given language key itself */ - getAllSubKeys(languageKey: keyof Specifics['LanguageKeys']): Array; + getAllSubKeys(languageKey: LanguageKey): Array>; /** * Returns all keys, which are direct or indirect super-keys of the given language key. * @param languageKey the given language key * @returns the list does not contain the given language key itself */ - getAllSuperKeys(languageKey: keyof Specifics['LanguageKeys']): Array; + getAllSuperKeys(languageKey: LanguageKey): Array>; isLanguageNode(node: unknown): node is Specifics['LanguageType']; } @@ -55,15 +55,15 @@ export interface LanguageService { */ export class DefaultLanguageService implements LanguageService { - getLanguageNodeKey(_languageNode: Specifics['LanguageType']): keyof Specifics['LanguageKeys'] | undefined { + getLanguageNodeKey(_languageNode: Specifics['LanguageType']): LanguageKey | undefined { return undefined; } - getAllSubKeys(_languageKey: keyof Specifics['LanguageKeys']): Array { + getAllSubKeys(_languageKey: LanguageKey): Array> { return []; } - getAllSuperKeys(_languageKey: keyof Specifics['LanguageKeys']): Array { + getAllSuperKeys(_languageKey: LanguageKey): Array> { return []; } diff --git a/packages/typir/src/services/operator.ts b/packages/typir/src/services/operator.ts index 799c0b6..d62582f 100644 --- a/packages/typir/src/services/operator.ts +++ b/packages/typir/src/services/operator.ts @@ -8,8 +8,8 @@ import { Type } from '../graph/type-node.js'; import { TypeInitializer } from '../initialization/type-initializer.js'; import { FunctionFactoryService, NO_PARAMETER_NAME } from '../kinds/function/function-kind.js'; import { FunctionType } from '../kinds/function/function-type.js'; -import { TypirSpecifics, TypirServices } from '../typir.js'; -import { LanguageKeys, NameTypePair } from '../utils/utils-definitions.js'; +import { LanguageKeys, TypirServices, TypirSpecifics } from '../typir.js'; +import { NameTypePair } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; import { ValidationProblemAcceptor } from './validation.js'; diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 5b0c53b..1a7a2b3 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { Type, isType } from '../graph/type-node.js'; -import { TypirSpecifics, TypirServices, MakePropertyOptional } from '../typir.js'; +import { LanguageKey, MakePropertyOptional, TypirServices, TypirSpecifics } from '../typir.js'; import { RuleCollectorListener, RuleOptions, RuleRegistry } from '../utils/rule-registration.js'; import { TypirProblem, isSpecificTypirProblem } from '../utils/utils-definitions.js'; import { TypeCheckStrategy, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; @@ -272,7 +272,7 @@ export class DefaultValidationCollector implem validate(languageNode: Specifics['LanguageType']): Array> { // determine all keys to check - const keysToApply: Array<(keyof Specifics['LanguageKeys']) | undefined> = []; + const keysToApply: Array | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index 08222cf..cff0427 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -25,6 +25,9 @@ import { DefaultSubType, SubType } from './services/subtype.js'; import { DefaultValidationCollector, DefaultValidationConstraints, ValidationCollector, ValidationConstraints, ValidationMessageProperties } from './services/validation.js'; import { inject, Module } from './utils/dependency-injection.js'; +/* eslint-disable @typescript-eslint/indent */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + /** * Some design decisions for Typir: * - We don't use a graph library like graphology to realize the type graph in order to be more flexible and to reduce external dependencies. @@ -199,3 +202,23 @@ export interface TypirSpecifics { /** Properties for validation issues (predefined and custom ones) */ ValidationMessageProperties: ValidationMessageProperties; } + + +/** This type describes a single language key as defined in the given TypirSpecifics, or just `string`, if the keys are not specified. */ +export type LanguageKey = keyof Specifics['LanguageKeys']; + +/** This type allows to specify an arbitrary number of (maybe typed) language keys. */ +export type LanguageKeys = LanguageKey | Array> | undefined; + +/** Given some language keys, this type provides the TypeScript types of the corresponding language nodes. */ +export type LanguageTypeOfLanguageKey< + Specifics extends TypirSpecifics, + Keys extends LanguageKeys +> = + // no key => use the base language type + Keys extends undefined ? Specifics['LanguageType'] : + // single key => use the specified language type from the "list type" + Keys extends keyof Specifics['LanguageKeys'] ? Specifics['LanguageKeys'][Keys] : + // multiple keys => use the base language type (as fall-back for now) + Keys extends Array ? Specifics['LanguageType'] : + never; diff --git a/packages/typir/src/utils/rule-registration.ts b/packages/typir/src/utils/rule-registration.ts index 87bb1e5..03ebce0 100644 --- a/packages/typir/src/utils/rule-registration.ts +++ b/packages/typir/src/utils/rule-registration.ts @@ -6,7 +6,7 @@ import { TypeGraphListener } from '../graph/type-graph.js'; import { Type } from '../graph/type-node.js'; -import { TypirSpecifics, TypirServices } from '../typir.js'; +import { LanguageKey, LanguageKeys, TypirServices, TypirSpecifics } from '../typir.js'; import { removeFromArray, toArray, toArrayWithValue } from './utils.js'; export interface RuleOptions { @@ -16,7 +16,7 @@ export interface RuleOptions { * In case of multiple language keys, the rule will be applied to all language nodes having ones of these language keys. * Rules without a language key ('undefined') are executed for all language nodes. */ - languageKey: (keyof Specifics['LanguageKeys']) | Array | undefined; + languageKey: LanguageKeys; /** * An optional type, if the new rule is dedicated for exactly this type. @@ -30,7 +30,7 @@ export interface RuleOptions { // corresponding information in a slightly different structure, which is easier to handle internally export interface InternalRuleOptions { languageKeyUndefined: boolean; - languageKeys: Array; + languageKeys: Array>; boundToTypes: Type[]; } @@ -44,7 +44,7 @@ export class RuleRegistry implements * language node type --> rules * Improves the look-up of related rules, when doing type for a concrete language node. * All rules are registered at least once in this map, since rules without dedicated language key are registered to 'undefined'. */ - protected readonly languageTypeToRules: Map<(keyof Specifics['LanguageKeys'])|undefined, RuleType[]> = new Map(); + protected readonly languageTypeToRules: Map|undefined, RuleType[]> = new Map(); /** * type identifier --> -> rules * Improves the look-up for rules which are bound to types, when these types are removed. @@ -65,7 +65,7 @@ export class RuleRegistry implements services.infrastructure.Graph.addListener(this); } - getRulesByLanguageKey(languageKey: (keyof Specifics['LanguageKeys']) | undefined): RuleType[] { + getRulesByLanguageKey(languageKey: LanguageKey | undefined): RuleType[] { const store = this.languageTypeToRules.get(languageKey); if (store === undefined) { return []; @@ -103,7 +103,7 @@ export class RuleRegistry implements addRule(rule: RuleType, givenOptions?: Partial>): void { const newOptions = this.getRuleOptions(givenOptions); const languageKeyUndefined: boolean = newOptions.languageKey === undefined; - const languageKeys: Array = toArray(newOptions.languageKey, { newArray: true }); + const languageKeys: Array> = toArray(newOptions.languageKey, { newArray: true }); const existingOptions = this.ruleToOptions.get(rule); const diffOptions: RuleOptions = { @@ -224,7 +224,7 @@ export class RuleRegistry implements } const languageKeyUndefined: boolean = optionsToRemove ? (optionsToRemove.languageKey === undefined) : true; - const languageKeys: Array = toArray(optionsToRemove?.languageKey, { newArray: true }); + const languageKeys: Array> = toArray(optionsToRemove?.languageKey, { newArray: true }); const diffOptions: RuleOptions = { // ... maybe more options in the future ... @@ -298,7 +298,7 @@ export class RuleRegistry implements } } - protected deregisterRuleForLanguageKey(rule: RuleType, languageKey: (keyof Specifics['LanguageKeys']) | undefined): boolean { + protected deregisterRuleForLanguageKey(rule: RuleType, languageKey: LanguageKey | undefined): boolean { const rules = this.languageTypeToRules.get(languageKey); if (rules) { const result = removeFromArray(rule, rules); diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 98632f9..ccbadfc 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -5,14 +5,12 @@ ******************************************************************************/ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/indent */ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { isType, Type } from '../graph/type-node.js'; import { TypeInitializer } from '../initialization/type-initializer.js'; import { InferenceRuleNotApplicable, TypeInferenceRule, TypeInferenceRuleOptions } from '../services/inference.js'; import { ValidationProblemAcceptor, ValidationRule, ValidationRuleOptions } from '../services/validation.js'; -import { TypirSpecifics, TypirServices } from '../typir.js'; +import { LanguageKeys, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../typir.js'; import { toArray } from './utils.js'; /** @@ -117,17 +115,6 @@ export function inferenceOptionsBoundToType = keyof Specifics['LanguageKeys']; -export type LanguageKeys = LanguageKey | Array> | undefined; -export type LanguageTypeOfLanguageKey< - Specifics extends TypirSpecifics, - Keys extends LanguageKeys -> = - Keys extends undefined ? Specifics['LanguageType'] : // no key => use the base language type - Keys extends keyof Specifics['LanguageKeys'] ? Specifics['LanguageKeys'][Keys] : // single key => use the specified language type from the "list type" - Keys extends Array ? Specifics['LanguageType'] : // multiple keys => use the base language type as fall-back; TODO is it possible to improve this with a union of the GivenKeys? - never; - /** * An inference rule which is dedicated for inferrring a certain type. * This utility type is often used for inference rules which are annotated to the declaration of a type. From 7a9db697e54dc2118ac3a3eccda31a03fac7e2c4 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 1 Sep 2025 13:50:30 +0200 Subject: [PATCH 05/17] some renamings to make names more clear --- packages/typir/src/graph/type-graph.ts | 30 +++++++++---------- .../src/initialization/type-reference.ts | 4 +-- .../typir/src/kinds/bottom/bottom-type.ts | 2 +- .../typir/src/kinds/class/top-class-type.ts | 2 +- .../src/kinds/custom/custom-initializer.ts | 2 +- .../kinds/function/function-overloading.ts | 2 +- packages/typir/src/kinds/top/top-type.ts | 2 +- packages/typir/src/utils/rule-registration.ts | 2 +- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index b223a38..069b771 100644 --- a/packages/typir/src/graph/type-graph.ts +++ b/packages/typir/src/graph/type-graph.ts @@ -19,7 +19,7 @@ import { Type } from './type-node.js'; */ export class TypeGraph { - protected readonly nodes: Map = new Map(); // type name => Type + protected readonly nodes: Map = new Map(); // type identifier => Type protected readonly edges: TypeEdge[] = []; protected readonly listeners: TypeGraphListener[] = []; @@ -28,13 +28,13 @@ export class TypeGraph { * Usually this method is called by kinds after creating a corresponding type. * Therefore it is usually not needed to call this method in an other context. * @param type the new type - * @param key an optional key to register the type, since it is allowed to register the same type with different keys in the graph + * @param identifier an optional identifier to register the type, since it is allowed to register the same type with different identifiers in the graph (TODO remove this property when supporting alias/proxy types!) */ - addNode(type: Type, key?: string): void { - if (!key) { + addNode(type: Type, identifier?: string): void { + if (!identifier) { assertTrue(type.isInStateOrLater('Identifiable')); // the key of the type must be available! } - const mapKey = key ?? type.getIdentifier(); + const mapKey = identifier ?? type.getIdentifier(); if (this.nodes.has(mapKey)) { if (this.nodes.get(mapKey) === type) { // this type is already registered => that is OK @@ -53,10 +53,10 @@ export class TypeGraph { * This is the central API call to remove a type from the type system in case that it is no longer valid/existing/needed. * It is not required to directly inform the kind of the removed type yourself, since the kind itself will take care of removed types. * @param typeToRemove the type to remove - * @param key an optional key to register the type, since it is allowed to register the same type with different keys in the graph + * @param identifier an optional identifier to register the type, since it is allowed to register the same type with different identifiers in the graph (TODO remove this property when supporting alias/proxy types!) */ - removeNode(typeToRemove: Type, key?: string): void { - const mapKey = key ?? typeToRemove.getIdentifier(); + removeNode(typeToRemove: Type, identifier?: string): void { + const mapKey = identifier ?? typeToRemove.getIdentifier(); // remove all edges which are connected to the type to remove typeToRemove.getAllIncomingEdges().forEach(e => this.removeEdge(e)); typeToRemove.getAllOutgoingEdges().forEach(e => this.removeEdge(e)); @@ -70,11 +70,11 @@ export class TypeGraph { } } - getNode(key: string): Type | undefined { - return this.nodes.get(key); + getNode(identifier: string): Type | undefined { + return this.nodes.get(identifier); } - getType(key: string): Type | undefined { - return this.getNode(key); + getType(identifier: string): Type | undefined { + return this.getNode(identifier); } getAllRegisteredTypes(): Type[] { @@ -124,7 +124,7 @@ export class TypeGraph { addListener(listener: TypeGraphListener, options?: { callOnAddedForAllExisting: boolean }): void { this.listeners.push(listener); if (options?.callOnAddedForAllExisting && listener.onAddedType) { - this.nodes.forEach((type, key) => listener.onAddedType!.call(listener, type, key)); + this.nodes.forEach((type, identifier) => listener.onAddedType!.call(listener, type, identifier)); } } removeListener(listener: TypeGraphListener): void { @@ -137,8 +137,8 @@ export class TypeGraph { } export type TypeGraphListener = Partial<{ - onAddedType(type: Type, key: string): void; - onRemovedType(type: Type, key: string): void; + onAddedType(type: Type, identifier: string): void; + onRemovedType(type: Type, identifier: string): void; onAddedEdge(edge: TypeEdge): void; onRemovedEdge(edge: TypeEdge): void; }> diff --git a/packages/typir/src/initialization/type-reference.ts b/packages/typir/src/initialization/type-reference.ts index 43f63d0..72fd8e9 100644 --- a/packages/typir/src/initialization/type-reference.ts +++ b/packages/typir/src/initialization/type-reference.ts @@ -133,12 +133,12 @@ export class TypeReference< } - onAddedType(_addedType: Type, _key: string): void { + onAddedType(_addedType: Type, _identifier: string): void { // after adding a new type, try to resolve the type this.resolve(); // possible performance optimization: is it possible to do this more performant by looking at the "addedType"? } - onRemovedType(removedType: Type, _key: string): void { + onRemovedType(removedType: Type, _identifier: string): void { // the resolved type of this TypeReference is removed! if (removedType === this.resolvedType) { // notify observers, that the type reference is broken diff --git a/packages/typir/src/kinds/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 0a8c548..40df457 100644 --- a/packages/typir/src/kinds/bottom/bottom-type.ts +++ b/packages/typir/src/kinds/bottom/bottom-type.ts @@ -29,7 +29,7 @@ export class BottomType extends Type implements TypeGraphListener { this.kind.services.infrastructure.Graph.removeListener(this); } - onAddedType(type: Type, _key: string): void { + onAddedType(type: Type, _identifier: string): void { // this method is called for the already existing types and for all upcomping types if (type !== this) { this.kind.services.Subtype.markAsSubType(this, type, { checkForCycles: false }); diff --git a/packages/typir/src/kinds/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 264a3ff..81d8cf1 100644 --- a/packages/typir/src/kinds/class/top-class-type.ts +++ b/packages/typir/src/kinds/class/top-class-type.ts @@ -30,7 +30,7 @@ export class TopClassType extends Type implements TypeGraphListener { this.kind.services.infrastructure.Graph.removeListener(this); } - onAddedType(type: Type, _key: string): void { + onAddedType(type: Type, _identifier: string): void { if (type !== this && isClassType(type)) { this.kind.services.Subtype.markAsSubType(type, this, { checkForCycles: false }); } diff --git a/packages/typir/src/kinds/custom/custom-initializer.ts b/packages/typir/src/kinds/custom/custom-initializer.ts index 34f5796..8448fb8 100644 --- a/packages/typir/src/kinds/custom/custom-initializer.ts +++ b/packages/typir/src/kinds/custom/custom-initializer.ts @@ -111,7 +111,7 @@ export class CustomTypeInitializer impleme } /* Get informed about deleted types in order to remove inference rules which are bound to them. */ - onRemovedType(type: Type, _key: string): void { + onRemovedType(type: Type, _identifier: string): void { if (isFunctionType(type)) { const overloaded = this.getOverloads(type.functionName); if (overloaded) { diff --git a/packages/typir/src/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index 66a6809..c37c1fb 100644 --- a/packages/typir/src/kinds/top/top-type.ts +++ b/packages/typir/src/kinds/top/top-type.ts @@ -29,7 +29,7 @@ export class TopType extends Type implements TypeGraphListener { this.kind.services.infrastructure.Graph.removeListener(this); } - onAddedType(type: Type, _key: string): void { + onAddedType(type: Type, _identifier: string): void { if (type !== this) { this.kind.services.Subtype.markAsSubType(type, this, { checkForCycles: false }); } diff --git a/packages/typir/src/utils/rule-registration.ts b/packages/typir/src/utils/rule-registration.ts index 03ebce0..1d4c1a2 100644 --- a/packages/typir/src/utils/rule-registration.ts +++ b/packages/typir/src/utils/rule-registration.ts @@ -315,7 +315,7 @@ export class RuleRegistry implements } /* Get informed about deleted types in order to remove rules which are bound to them. */ - onRemovedType(type: Type, _key: string): void { + onRemovedType(type: Type, _identifier: string): void { const typeKey = this.getBoundToTypeKey(type); // TODO only if "typeKey === _key" ?? this needs to be double-checked when making Alias types explicit! const entriesToRemove = this.typirTypeToRules.get(typeKey); From 65877041a80bd616d1a72e6b26abed19b7e383aa Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 1 Sep 2025 15:46:38 +0200 Subject: [PATCH 06/17] refactoring: moved definitions into another file --- packages/typir/src/services/validation.ts | 4 ++-- .../src/test/predefined-language-nodes.ts | 5 +++-- packages/typir/src/typir.ts | 18 +----------------- packages/typir/src/utils/utils.ts | 13 +++++++++++++ 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 1a7a2b3..af06e83 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -5,11 +5,11 @@ ******************************************************************************/ import { Type, isType } from '../graph/type-node.js'; -import { LanguageKey, MakePropertyOptional, TypirServices, TypirSpecifics } from '../typir.js'; +import { LanguageKey, TypirServices, TypirSpecifics } from '../typir.js'; import { RuleCollectorListener, RuleOptions, RuleRegistry } from '../utils/rule-registration.js'; import { TypirProblem, isSpecificTypirProblem } from '../utils/utils-definitions.js'; import { TypeCheckStrategy, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; -import { removeFromArray, toArray } from '../utils/utils.js'; +import { MakePropertyOptional, removeFromArray, toArray } from '../utils/utils.js'; import { TypeInferenceCollector } from './inference.js'; import { ProblemPrinter } from './printing.js'; diff --git a/packages/typir/src/test/predefined-language-nodes.ts b/packages/typir/src/test/predefined-language-nodes.ts index 34a7f20..19326cc 100644 --- a/packages/typir/src/test/predefined-language-nodes.ts +++ b/packages/typir/src/test/predefined-language-nodes.ts @@ -4,11 +4,12 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { inject, Module } from '../utils/dependency-injection.js'; import { DefaultLanguageService } from '../services/language.js'; import { InferOperatorWithMultipleOperands } from '../services/operator.js'; import { DefaultTypeConflictPrinter } from '../services/printing.js'; -import { TypirSpecifics, TypirServices, PartialTypirServices, createTypirServices, DeepPartial, createDefaultTypirServicesModule } from '../typir.js'; +import { createDefaultTypirServicesModule, createTypirServices, PartialTypirServices, TypirServices, TypirSpecifics } from '../typir.js'; +import { inject, Module } from '../utils/dependency-injection.js'; +import { DeepPartial } from '../utils/utils.js'; /** * Base class for all language nodes, diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index cff0427..a03c012 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -24,6 +24,7 @@ import { DefaultTypeConflictPrinter, ProblemPrinter } from './services/printing. import { DefaultSubType, SubType } from './services/subtype.js'; import { DefaultValidationCollector, DefaultValidationConstraints, ValidationCollector, ValidationConstraints, ValidationMessageProperties } from './services/validation.js'; import { inject, Module } from './utils/dependency-injection.js'; +import { DeepPartial } from './utils/utils.js'; /* eslint-disable @typescript-eslint/indent */ /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -40,10 +41,6 @@ import { inject, Module } from './utils/dependency-injection.js'; * since the services are not realized by global functions, but by methods of classes which implement service interfaces. */ -/** Some open design questions for future releases TODO - * - How to bundle Typir configurations for reuse ("presets")? - */ - export type TypirServices = { readonly Assignability: TypeAssignability; readonly Equality: TypeEquality; @@ -169,19 +166,6 @@ export function createTypirServicesWithAdditionalServices = T[keyof T] extends Function ? T : { - [P in keyof T]?: DeepPartial; -} - -/** Makes only the specified properties of the given type optional */ -export type MakePropertyOptional = Omit & Partial>; - /** * Language-specific services to be partially overridden via dependency injection. */ diff --git a/packages/typir/src/utils/utils.ts b/packages/typir/src/utils/utils.ts index ca5131d..dc6402a 100644 --- a/packages/typir/src/utils/utils.ts +++ b/packages/typir/src/utils/utils.ts @@ -85,3 +85,16 @@ export function isSet(value: unknown): value is Set { export function isMap(value: unknown): value is Map { return value instanceof Map || Object.prototype.toString.call(value) === '[object Map]'; } + +/** + * A deep partial type definition for services. We look into T to see whether its type definition contains + * any methods. If it does, it's one of our services and therefore should not be partialized. + * Copied from Langium. + */ +//eslint-disable-next-line @typescript-eslint/ban-types +export type DeepPartial = T[keyof T] extends Function ? T : { + [P in keyof T]?: DeepPartial; +}; + +/** Makes only the specified properties of the given type optional */ +export type MakePropertyOptional = Omit & Partial>; From 7ca40275e0d410b5ab3521c12b996647134556d4 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 1 Sep 2025 17:30:20 +0200 Subject: [PATCH 07/17] renamed internal generics --- packages/typir/src/services/operator.ts | 4 +-- packages/typir/src/utils/utils-definitions.ts | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/typir/src/services/operator.ts b/packages/typir/src/services/operator.ts index d62582f..e7a68f6 100644 --- a/packages/typir/src/services/operator.ts +++ b/packages/typir/src/services/operator.ts @@ -30,8 +30,8 @@ export interface InferOperatorWithMultipleOperands boolean); } -export type OperatorValidationRule = - (operatorCall: T, operatorName: string, operatorType: TypeType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; +export type OperatorValidationRule = + (operatorCall: T, operatorName: string, operatorType: OperatorType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; export interface AnyOperatorDetails { name: string; diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index ccbadfc..9bfc221 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -49,12 +49,12 @@ export interface ValidationRuleWithOptions = undefined, LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey >( - rule: InferCurrentTypeRule, type: TypeType + rule: InferCurrentTypeRule, type: CurrentType ): ValidationRuleWithOptions | undefined { // check the given rule checkRule(rule); // fail early @@ -121,39 +121,39 @@ export function inferenceOptionsBoundToType = undefined, LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, > { languageKey?: LanguageKey; filter?: (languageNode: LanguageTypeOfLanguageKey) => languageNode is LanguageType; - matching?: (languageNode: LanguageType, typeToInfer: TypeType) => boolean; + matching?: (languageNode: LanguageType, typeToInfer: CurrentType) => boolean; /** * This validation will be applied to all language nodes for which the current type is inferred according to this inference rule. * This validation is specific for this inference rule and this inferred type. */ - validation?: InferCurrentTypeValidationRule | Array>; + validation?: InferCurrentTypeValidationRule | Array>; - skipThisRuleIfThisTypeAlreadyExists?: boolean | ((existingType: TypeType) => boolean); // default is false + skipThisRuleIfThisTypeAlreadyExists?: boolean | ((existingType: CurrentType) => boolean); // default is false } export type InferCurrentTypeValidationRule< - TypeType extends Type, + InferredType extends Type, Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType'], > = - (languageNode: T, inferredType: TypeType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; + (languageNode: T, inferredType: InferredType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; export function skipInferenceRuleForExistingType< - TypeType extends Type, + CurrentType extends Type, Specifics extends TypirSpecifics, LanguageKey extends LanguageKeys = undefined, LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey >( - inferenceRule: InferCurrentTypeRule, newType: TypeType, existingType: TypeType + inferenceRule: InferCurrentTypeRule, newType: CurrentType, existingType: CurrentType ): boolean { if (newType !== existingType) { const skipRuleForExisting = inferenceRule.skipThisRuleIfThisTypeAlreadyExists; @@ -164,12 +164,12 @@ export function skipInferenceRuleForExistingType< } function checkRule< - TypeType extends Type, + CurrentType extends Type, Specifics extends TypirSpecifics, LanguageKey extends LanguageKeys = undefined, LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey >( - rule: InferCurrentTypeRule + rule: InferCurrentTypeRule ): void { if (rule.languageKey === undefined && rule.filter === undefined && rule.matching === undefined) { throw new Error('This inference rule has none of the properties "languageKey", "filter" and "matching" at all and therefore cannot infer any type!'); @@ -177,12 +177,12 @@ function checkRule< } export function bindInferCurrentTypeRule< - TypeType extends Type, + CurrentType extends Type, Specifics extends TypirSpecifics, LanguageKey extends LanguageKeys = undefined, LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey >( - rule: InferCurrentTypeRule, type: TypeType + rule: InferCurrentTypeRule, type: CurrentType ): InferenceRuleWithOptions { checkRule(rule); // fail early return { @@ -224,8 +224,8 @@ export function bindInferCurrentTypeRule< }; } -export function registerInferCurrentTypeRules( - rules: InferCurrentTypeRule | Array> | undefined, type: TypeType, services: TypirServices +export function registerInferCurrentTypeRules( + rules: InferCurrentTypeRule | Array> | undefined, type: CurrentType, services: TypirServices ): void { for (const ruleSingle of toArray(rules)) { // inference From 66edf95a8740a91aaa3824d0995926f84603db91 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Mon, 1 Sep 2025 18:36:48 +0200 Subject: [PATCH 08/17] investigated a strange TSC issue --- packages/typir/src/services/validation.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index af06e83..ddda522 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -45,7 +45,7 @@ export type ValidationProblemProperties< /** Make some properties optional for convenience, since there are default values for them. */ export type RelaxedValidationProblem< Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType'] -> = MakePropertyOptional, 'languageNode'|'severity'|'message'>; // TODO If unknown properties are specified, no TypeScript compiler error is shown +> = MakePropertyOptional, 'languageNode'|'severity'|'message'>; export type ValidationProblemAcceptor = (problem: ValidationProblemProperties) => void; @@ -87,6 +87,12 @@ export interface AnnotatedTypeAfterValidation { export type ValidationMessageProvider = // RelaxedValidationProblem enables to specificy only some of the mandatory properties; for the remaining ones, the service implementation provides values (actual: AnnotatedTypeAfterValidation, expected: AnnotatedTypeAfterValidation) => RelaxedValidationProblem; + /* Hint: additional properties in a returned RelaxedValidationProblem object are not marked as errors by the TypeScript compiler, while they are marked, if the same object is used as argument for the ValidationProblemAcceptor. + * Source for this behaviour is, that the TSC checks objects for input parameters differently than objects for return parameters. + * Hint in the specification ("excess property checks"): https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks + * The solution are "exact types", but they are still under discussion: https://github.com/microsoft/TypeScript/issues/12936 + * => Nothing to do/fix at the moment, let's wait until "exact types" are supported in TypeScript. + */ export interface ValidationConstraints { ensureNodeIsAssignable( From 4c10c760dc8dfeb1ad3a423a3844fbea382fda10 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Thu, 4 Sep 2025 11:47:53 +0200 Subject: [PATCH 09/17] `languageProperty` supports only valid property names of the given `languageNode`, wrote documentation about validation --- CHANGELOG.md | 1 + documentation/bindings/binding-langium.md | 6 ++ documentation/design.md | 4 + documentation/index.md | 1 + documentation/services/validation.md | 89 +++++++++++++++++++ documentation/usecases.md | 17 +++- .../lox/src/language/lox-type-checking.ts | 10 +-- examples/ox/src/language/ox-type-checking.ts | 9 +- .../src/features/langium-validation.ts | 2 +- packages/typir/src/services/validation.ts | 88 +++++++++++------- packages/typir/src/typir.ts | 13 ++- 11 files changed, 195 insertions(+), 45 deletions(-) create mode 100644 documentation/services/validation.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2efcf38..6c5abbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - By default, Typir don't predefine any language nodes in advance (i.e. any `string` values are usable as language key), while Typir-Langium supports exactly the generated types in the `ast.ts`. - When restricting the possible language keys, now the user gets informed by the TypeScript compiler, if other language keys are used, e.g. inside inference rules or for registering validation rules. - If language keys are restricted, inside inference rules with value for `languageKey` and without value for `filter`, it is possible now to skip the expected TypeScript type for the input node of the `matching` property, as demonstrated in the updated examples for (L)OX. This improves the usability of the API. +- When reporting validation issues, `languageProperty` accepts only valid property names of the given `languageNode`. ### Breaking changes diff --git a/documentation/bindings/binding-langium.md b/documentation/bindings/binding-langium.md index 56ea45c..e14743a 100644 --- a/documentation/bindings/binding-langium.md +++ b/documentation/bindings/binding-langium.md @@ -4,3 +4,9 @@ Typir-Langium is a dedicated binding of Typir for languages and DSLs which are d the language workbench for developing textual domain-specific languages (DSLs) in the web. TODO + +## Validation + +All properties of usual diagnostics in Langium (as defined in `DiagnosticInfo`) are supported, when creating validation issues in Typir-Langiums. +This enables, among other use cases, to register code actions for type-related validation issues (see `lox-code-actions.ts` for an example). +Note that `node`, `property` and `index` are renamed to `languageNode`, `languageProperty` and `languageIndex` to be in sync with Typir core. diff --git a/documentation/design.md b/documentation/design.md index 4604ea7..f3d050f 100644 --- a/documentation/design.md +++ b/documentation/design.md @@ -56,6 +56,10 @@ type inference and validations are done on language nodes, e.g. an inference rule gets a language node as input and returns its inferred Typir type. All information Typir needs to know about language nodes is specified in the APIs, including the APIs for inference rules, validations rules and the [language service](./services/language.md). +### Language type + +The TypeScript type of a language node is called *language type*. + ### Language key Each language node might have a *language key*. diff --git a/documentation/index.md b/documentation/index.md index 6ebc64e..cf688b4 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -12,6 +12,7 @@ This describes the structure and the main content of the documentation for Typir - [Assignability](./services/assignability.md) - [Language](./services/language.md): Don't interchange "language service" and "language server"! - [Type inference](./services/inference.md) +- [Validation](./services/validation.md) - ... diff --git a/documentation/services/validation.md b/documentation/services/validation.md new file mode 100644 index 0000000..5100d7d --- /dev/null +++ b/documentation/services/validation.md @@ -0,0 +1,89 @@ +# Validation + +Typir provides some services and concepts, to create validation checks, which check some type-related constraints on language nodes. + +## API + +### Validation rules + +Validation rules are single checks, which are executed on a given language node and result in an arbitrary number of validation issues. +Simple validation rules are realized as TypeScript functions: + +```typescript +type ValidationRuleFunctional = (languageNode: LanguageType, accept: ValidationProblemAcceptor, typir: TypirServices) => void; +``` + +The given `languageNode` is the starting point for doing some type-related checks on the AST. +Found validation issues are not returned, but reported to the `ValidationProblemAcceptor` by calling it with `accept({ ... })`. +The properties to specify in the given object are described in the next section. + +To realize more advanced checks in more performant way, there is also `ValidationRuleLifecycle`. + +### Validation collector + +The `ValidationCollector` is the central place for managing the validation. +Validation rules are registered at and collected by the validation collector with `typir.validation.Collector.addValidationRule(rule, { ... })`. +Some options might be given in the options object as second argument: + +- `boundToType`: If the given type is removed from the type system, this rule will be automatically removed as well. +- `languageKey`: By default, all validation rules are performed for all language nodes. + In order to improve performance, validation rules with a given language key are executed only for language nodes with this language key. + +The call `const issues: ValidationProblem = typir.validation.Collector.validate(languageNode)` validates a language node +by executing all validation rules which are applicable to the given language node and returns all found validation issues. +Since Typir doesn't know the structure of the AST, there is *no* automatic traversal of the AST, i.e. *only* the given language node is validated. + +Bindings of Typir for concrete language workbenches might behave differently, +e.g. Typir-Langium hooks into the regular validation mechanisms of Langium. +Therefore neither direct calls of `validate()` nor traversals of the Langium AST are required. + +### Validation issues + +When reporting some issues, different information can be reported. +All values are put into an object representing the validation issue. +This validation issue object is given to the validation problem acceptor, e.g. + +```typescript +accept({ + severity: 'error', + message: 'An error occurred', + languageNode: myCheckedNode, + // ... +}); +``` + +The following properties are supported by default by Typir (core): + +* The `severity` describes, how critical the found issue is, e.g. whether its an error or only a hint. +* The `message` is some text to describe the issue in a human-readable way. +* Optionally, `subProblems` allows to attach some sub-problems, which might give some more details or reasons for the reported validation issue. +* The `languageNode` can be used to specify, where in the validation issue occurred in the validated AST. This "source of the issue" might be different than the language node which was given as input to the validation rule. +* A `languageProperty` can be specified only, if the `languageNode` is specified, and marks a property as more fine-grained source of the issue. +* The `languageIndex` makes only sense, if the `languageProperty` is specified, and gives even more details for the source of the issue. + +The available properties can be customized via `TypirSpecifics['ValidationMessageProperties']`, which is useful for supporting new language workbenches. +Don't forget to store or apply the values for the customized properties, +which requires some more customizations when postprocessing the validtion issues returned by Typir. +As an example, Typir-Langium provides some properties for validation issues, which are specific for Langium. + +### Predefined constraints + +To simplify the checking and creating of validation issues, +the `ValidationConstraints` service available via `typir.validation.Constraints` provides some constraints as short-cuts for recurring validation checks, +which can be used inside validation rules. + +As an example, if you have a `node` which represents a `VariableDeclaration`, you could validate, whether the given initial `value` is assignable to the declared `type` of the variable in this way: + +```typescript +typir.validation.Constraints.ensureNodeIsAssignable( + node.value, // the initial value, its Typir type is inferred internally + node.type, // the declared (language) type, the corresponding Typir type is inferred internally + accept, // the validation acceptor + (actual, expected) => ({ // callback to create a meaningful validation issue, if the value does not fit to the type + message: `The initial value of type '${actual.name}' is not assignable to '${node.name}' of type '${expected.name}'.`, + // more properties might be specified + }) +); +``` + +See (L)OX for some more examples. diff --git a/documentation/usecases.md b/documentation/usecases.md index ac88f53..0657602 100644 --- a/documentation/usecases.md +++ b/documentation/usecases.md @@ -10,10 +10,23 @@ Additionally, inference rules need to be added in order to describe, which langu TODO +- create types +- establish relationships between types, e.g. conversion rules +- add inference rules +- (once vs for each user-defined type) + ## Validation -TODO +The most obvious use case for type systems is to support type-related validations, e.g. to check in programming language-like languages, +that the initial value of a variable fits to its declared type or that only boolean-expressions are used as condition in if-statements. + +Since such constraints usually always hold, corresponding validation checks are added once during the set-up of Typir. +For each validation check, a validation rule is created and registered in the `typir.validation.Collector` service. +After that, language nodes can be validated by the same service. +The result is a list of the found validation issues, which could be presented to the users of the language. + +Read the documentation about [validations](./services/validation.md) to learn the technical details about validations. ## Linking @@ -28,8 +41,10 @@ which get an AST consisting of language nodes as input and produce some output. Often the assumption for language processing is, that the AST is correctly linked and no (critical) validation issues are existing (see the two use cases before). + TODO type inference with the [type inference service](./services/inference.md) + If you transpile or compile programming language-like languages, implicit and explicit conversions of values to variables or parameters often need to be handled. Here the [assignability service](./services/assignability.md) helps you to get information, how types are converted to each other: diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index 14539df..a80e917 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -106,7 +106,7 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition // this validation will be checked for each call of this operator! validation: (node, _opName, _opType, accept, typir) => typir.validation.Constraints.ensureNodeIsAssignable(node.right, node.left, accept, (actual, expected) => ({ message: `The expression '${node.right.$cstNode?.text}' of type '${actual.name}' is not assignable to '${node.left.$cstNode?.text}' with type '${expected.name}'`, - languageProperty: 'value' }))}) + languageNode: node, languageProperty: 'right' }))}) .finish(); // unary operators @@ -282,23 +282,23 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition // the return value must fit to the return type of the function / method typir.validation.Constraints.ensureNodeIsAssignable(node.value, callableDeclaration.returnType, accept, (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}'.`, - languageProperty: 'value' })); + languageNode: node, languageProperty: 'value' })); } } protected validateVariableDeclaration(node: VariableDeclaration, accept: ValidationProblemAcceptor, typir: TypirServices): void { const typeVoid = typir.factory.Primitives.get({ primitiveName: 'void' })!; typir.validation.Constraints.ensureNodeHasNotType(node, typeVoid, accept, - () => ({ message: "Variable can't be declared with a type 'void'.", languageProperty: 'type' })); + () => ({ message: "Variable can't be declared with a type 'void'.", languageNode: node, languageProperty: 'type' })); typir.validation.Constraints.ensureNodeIsAssignable(node.value, node, accept, (actual, expected) => ({ message: `The expression '${node.value?.$cstNode?.text}' of type '${actual.name}' is not assignable to '${node.name}' with type '${expected.name}'`, - languageProperty: 'value' })); + languageNode: node, languageProperty: 'value' })); } protected validateCondition(node: IfStatement | WhileStatement | ForStatement, accept: ValidationProblemAcceptor, typir: TypirServices): void { const typeBool = typir.factory.Primitives.get({ primitiveName: 'boolean' })!; typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool, accept, - () => ({ message: "Conditions need to be evaluated to 'boolean'.", languageProperty: 'condition' })); + () => ({ message: "Conditions need to be evaluated to 'boolean'.", languageNode: node, languageProperty: 'condition' })); } } diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 81a3560..eeb1fc4 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -122,6 +122,7 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { typir.validation.Constraints.ensureNodeIsAssignable(node.value, node.varRef.ref, accept, (actual, expected) => ({ message: `The expression '${node.value.$cstNode?.text}' of type '${actual.name}' is not assignable to the variable '${node.varRef.ref!.name}' with type '${expected.name}'.`, + languageNode: node, languageProperty: 'value', })); } @@ -133,20 +134,20 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { if (functionDeclaration && functionDeclaration.returnType.primitive !== 'void' && node.value) { // the return value must fit to the return type of the function typir.validation.Constraints.ensureNodeIsAssignable(node.value, functionDeclaration.returnType, accept, - () => ({ message: `The expression '${node.value!.$cstNode?.text}' is not usable as return value for the function '${functionDeclaration.name}'.`, languageProperty: 'value' })); + () => ({ message: `The expression '${node.value!.$cstNode?.text}' is not usable as return value for the function '${functionDeclaration.name}'.`, languageNode: node, languageProperty: 'value' })); } }, VariableDeclaration: (node, accept, typir) => { typir.validation.Constraints.ensureNodeHasNotType(node, typeVoid, accept, - () => ({ message: "Variables can't be declared with the type 'void'.", languageProperty: 'type' })); + () => ({ message: "Variables can't be declared with the type 'void'.", languageNode: node, languageProperty: 'type' })); typir.validation.Constraints.ensureNodeIsAssignable(node.value, node, accept, - (actual, expected) => ({ message: `The initialization expression '${node.value?.$cstNode?.text}' of type '${actual.name}' is not assignable to the variable '${node.name}' with type '${expected.name}'.`, languageProperty: 'value' })); + (actual, expected) => ({ message: `The initialization expression '${node.value?.$cstNode?.text}' of type '${actual.name}' is not assignable to the variable '${node.name}' with type '${expected.name}'.`, languageNode: node, languageProperty: 'value' })); }, WhileStatement: validateCondition, }); function validateCondition(node: IfStatement | WhileStatement | ForStatement, accept: ValidationProblemAcceptor, typir: TypirServices): void { typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool, accept, - () => ({ message: "Conditions need to be evaluated to 'boolean'.", languageProperty: 'condition' })); + () => ({ message: "Conditions need to be evaluated to 'boolean'.", languageNode: node, languageProperty: 'condition' })); } } diff --git a/packages/typir-langium/src/features/langium-validation.ts b/packages/typir-langium/src/features/langium-validation.ts index 901130e..8e27db1 100644 --- a/packages/typir-langium/src/features/langium-validation.ts +++ b/packages/typir-langium/src/features/langium-validation.ts @@ -86,7 +86,7 @@ export class DefaultLangiumTypirValidator, + property: problem.languageProperty as (Properties | undefined), index: problem.languageIndex, // copy all other DiagnosticInfo properties: ...problem, diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index ddda522..0266ab5 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { Type, isType } from '../graph/type-node.js'; -import { LanguageKey, TypirServices, TypirSpecifics } from '../typir.js'; +import { LanguageKey, PropertiesOfLanguageType, TypirServices, TypirSpecifics } from '../typir.js'; import { RuleCollectorListener, RuleOptions, RuleRegistry } from '../utils/rule-registration.js'; import { TypirProblem, isSpecificTypirProblem } from '../utils/utils-definitions.js'; import { TypeCheckStrategy, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; @@ -22,8 +22,10 @@ export interface ValidationMessageProperties { // Using this type only in the Ty } export type ValidationProblem< - Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType'] -> = ValidationProblemProperties & TypirProblem & { + Specifics extends TypirSpecifics, + T extends Specifics['LanguageType'] = Specifics['LanguageType'], + P extends PropertiesOfLanguageType | undefined = undefined, +> = ValidationProblemProperties & TypirProblem & { $problem: 'ValidationProblem'; } @@ -34,21 +36,31 @@ export function isValidationProblem | undefined = undefined, // since 'languageProperty' is optional (see the ? below), undefined is the natural choice for the default here > = Specifics['ValidationMessageProperties'] & { // the following properties are provided always and cannot be customized: + /** The validation issue will be associated with / visualized at this language node. */ languageNode: T; - languageProperty?: string; // name of a property of the language node; TODO make this type-safe! - languageIndex?: number; // index, if 'languageProperty' is an Array property + /** Name of a property of the language node to concretize, where to visualize the validation issue. This property requires `languageNode` to be specified. */ + languageProperty?: P; + /** Index of the element to associate the validation issue with, if the specified `languageProperty` is an array property. */ + languageIndex?: number; } /** Make some properties optional for convenience, since there are default values for them. */ export type RelaxedValidationProblem< - Specifics extends TypirSpecifics, T extends Specifics['LanguageType'] = Specifics['LanguageType'] -> = MakePropertyOptional, 'languageNode'|'severity'|'message'>; + Specifics extends TypirSpecifics, + T extends Specifics['LanguageType'] = Specifics['LanguageType'], + P extends PropertiesOfLanguageType | undefined = undefined, +> = MakePropertyOptional, 'languageNode'|'severity'|'message'>; -export type ValidationProblemAcceptor - = (problem: ValidationProblemProperties) => void; +export type ValidationProblemAcceptor // this type describes a function with two generics and one input argument ("accept({ ... })") + = < + T extends Specifics['LanguageType'] = Specifics['LanguageType'], + P extends PropertiesOfLanguageType | undefined = undefined + >(problem: ValidationProblemProperties) => void; export type ValidationRule = | ValidationRuleFunctional @@ -84,34 +96,39 @@ export interface AnnotatedTypeAfterValidation { userRepresentation: string; name: string; } -export type ValidationMessageProvider = +export type ValidationMessageProvider< + Specifics extends TypirSpecifics, + T extends Specifics['LanguageType'] = Specifics['LanguageType'], + P extends PropertiesOfLanguageType | undefined = undefined, +> = // RelaxedValidationProblem enables to specificy only some of the mandatory properties; for the remaining ones, the service implementation provides values - (actual: AnnotatedTypeAfterValidation, expected: AnnotatedTypeAfterValidation) => RelaxedValidationProblem; + (actual: AnnotatedTypeAfterValidation, expected: AnnotatedTypeAfterValidation) => RelaxedValidationProblem; /* Hint: additional properties in a returned RelaxedValidationProblem object are not marked as errors by the TypeScript compiler, while they are marked, if the same object is used as argument for the ValidationProblemAcceptor. * Source for this behaviour is, that the TSC checks objects for input parameters differently than objects for return parameters. * Hint in the specification ("excess property checks"): https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks * The solution are "exact types", but they are still under discussion: https://github.com/microsoft/TypeScript/issues/12936 * => Nothing to do/fix at the moment, let's wait until "exact types" are supported in TypeScript. + * Another observation: It seems, that this problem also decreases the auto-completion proposals, e.g. for 'languageProperty'. */ export interface ValidationConstraints { - ensureNodeIsAssignable( + ensureNodeIsAssignable | undefined = undefined>( sourceNode: S | undefined, expected: Type | undefined | E, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider): void; - ensureNodeIsEquals( + message: ValidationMessageProvider): void; + ensureNodeIsEquals | undefined = undefined>( sourceNode: S | undefined, expected: Type | undefined | E, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider): void; - ensureNodeHasNotType( + message: ValidationMessageProvider): void; + ensureNodeHasNotType | undefined = undefined>( sourceNode: S | undefined, notExpected: Type | undefined | E, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider): void; + message: ValidationMessageProvider): void; - ensureNodeRelatedWithType( + ensureNodeRelatedWithType | undefined = undefined>( languageNode: S | undefined, expected: Type | undefined | E, strategy: TypeCheckStrategy, negated: boolean, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider): void; + message: ValidationMessageProvider): void; } export class DefaultValidationConstraints implements ValidationConstraints { @@ -125,35 +142,40 @@ export class DefaultValidationConstraints impl this.printer = services.Printer; } - ensureNodeIsAssignable( - sourceNode: S | undefined, expected: Type | undefined | E, + ensureNodeIsAssignable | undefined = undefined>( + sourceNode: S | undefined, + expected: Type | undefined | E, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider + message: ValidationMessageProvider ): void { this.ensureNodeRelatedWithType(sourceNode, expected, 'ASSIGNABLE_TYPE', false, accept, message); } - ensureNodeIsEquals( - sourceNode: S | undefined, expected: Type | undefined | E, + ensureNodeIsEquals | undefined = undefined>( + sourceNode: S | undefined, + expected: Type | undefined | E, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider + message: ValidationMessageProvider ): void { this.ensureNodeRelatedWithType(sourceNode, expected, 'EQUAL_TYPE', false, accept, message); } - ensureNodeHasNotType( - sourceNode: S | undefined, notExpected: Type | undefined | E, + ensureNodeHasNotType | undefined = undefined>( + sourceNode: S | undefined, + notExpected: Type | undefined | E, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider + message: ValidationMessageProvider ): void { this.ensureNodeRelatedWithType(sourceNode, notExpected, 'EQUAL_TYPE', true, accept, message); } - ensureNodeRelatedWithType( - languageNode: S | undefined, expected: Type | undefined | E, - strategy: TypeCheckStrategy, negated: boolean, + ensureNodeRelatedWithType | undefined = undefined>( + languageNode: S | undefined, + expected: Type | undefined | E, + strategy: TypeCheckStrategy, + negated: boolean, accept: ValidationProblemAcceptor, - message: ValidationMessageProvider + message: ValidationMessageProvider ): void { if (languageNode !== undefined && expected !== undefined) { const actualType = isType(languageNode) ? languageNode : this.inference.inferType(languageNode); diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index a03c012..c61a953 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -204,5 +204,16 @@ export type LanguageTypeOfLanguageKey< // single key => use the specified language type from the "list type" Keys extends keyof Specifics['LanguageKeys'] ? Specifics['LanguageKeys'][Keys] : // multiple keys => use the base language type (as fall-back for now) - Keys extends Array ? Specifics['LanguageType'] : + Keys extends Array ? Specifics['LanguageType'] : // possible extension: calculate the union of language types never; + +/** Given the type of a language node (i.e. the "language type"), this type provides the relevant properties of the language type. */ +// possible extension: make this type exchangable, if possible +export type PropertiesOfLanguageType = + T extends Specifics['LanguageType'] + ? keyof Omit only the specific properties of the concrete language type remain + | number | symbol + > + : never +; From 82148ace11cc36d1ca907677c645cfc709c1010a Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 11 Feb 2026 09:08:33 +0100 Subject: [PATCH 10/17] fixes according to the review --- CHANGELOG.md | 9 +++++---- documentation/design.md | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5abbc..439ad61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,11 @@ For each minor and major version, there is a corresponding [milestone on GitHub] ### New features -- Introduced `TypirSpecifics['LanguageKeys']` (in `typir.ts`) to make the available language nodes with their keys and TypeScript types explicit (#93): - - By default, Typir don't predefine any language nodes in advance (i.e. any `string` values are usable as language key), while Typir-Langium supports exactly the generated types in the `ast.ts`. - - When restricting the possible language keys, now the user gets informed by the TypeScript compiler, if other language keys are used, e.g. inside inference rules or for registering validation rules. - - If language keys are restricted, inside inference rules with value for `languageKey` and without value for `filter`, it is possible now to skip the expected TypeScript type for the input node of the `matching` property, as demonstrated in the updated examples for (L)OX. This improves the usability of the API. +- Introduced `TypirSpecifics['LanguageKeys']` (in `typir.ts`) to make the available language nodes with their keys and TypeScript types explicit: + - By default, Typir predefines neither language nodes nor language keys in advance, i.e. any `string` values are usable as language key. + - By default, Typir-Langium supports exactly the language nodes and language types, which are derived from the grammar and generated by Langium into the `ast.ts`. Looking into the LOX example, `LoxAstType` inside `examples/lox/src/language/generated/ast.ts` lists all language keys (on the left of colon) and maps them to the (TypeScript interfaces/types of the) language nodes (on the right of the colon). + - When restricting the possible language keys, the TypeScript compiler shows errors, if invalid language keys are used. This happens, among others, inside inference rules or for registering validation rules. This improves type safety on TypeScript level for developers applying Typir. + - If language keys are restricted, inside an inference rule with a value for `languageKey` and without a value for `filter`, it is possible now to skip the expected TypeScript type for the input node of the `matching` property, as demonstrated in the updated examples for (L)OX. This improves the usability and type-safety of the API. - When reporting validation issues, `languageProperty` accepts only valid property names of the given `languageNode`. ### Breaking changes diff --git a/documentation/design.md b/documentation/design.md index f3d050f..456240d 100644 --- a/documentation/design.md +++ b/documentation/design.md @@ -41,8 +41,8 @@ Each type system, i.e. each instance of the `TypirServices`, has one type graph, ## Language Usually, type systems are created to do some type checking on textual languages, including domain-specific languages (DSLs) and general-purpose programming languages. Programs respective text conforming to these languages are parsed and provided as abstract syntax trees (ASTs) in-memory. -ASTs usually consists of a tree of nodes (realized as JavaScript objects at runtime), which represent a small part of the parsed program/text. -Establishing cross-references between the nodes of the tree after parsing results in a graph after linking. +ASTs usually consist of a tree of nodes (realized as JavaScript objects at runtime), which represent a small part of the program/text after parsing. +During linking, cross-references between the nodes of the tree are established, i.e. the tree becomes a graph. Type checking is done on these ASTs. ### Language node @@ -64,7 +64,7 @@ The TypeScript type of a language node is called *language type*. Each language node might have a *language key*. Language keys are `string` values and are used to increase performance by registering rules for inference and validation not for all language nodes, -but only for language nodes with a particular language node. +but only for language nodes with a particular language key. Rules associated to no language key are applied to all language nodes. Rules might be associated to multiple language keys. Getting the language key of a language node is done by the [language service](./services/language.md). From a5e7ea1ab677fb38c63d06544c74812ad3ceaddb Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 11 Feb 2026 09:20:36 +0100 Subject: [PATCH 11/17] calculate union of language types for multiple language keys (as developed together with Markus and Insa) --- packages/typir/src/typir.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index c61a953..4c0cfbd 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -203,9 +203,10 @@ export type LanguageTypeOfLanguageKey< Keys extends undefined ? Specifics['LanguageType'] : // single key => use the specified language type from the "list type" Keys extends keyof Specifics['LanguageKeys'] ? Specifics['LanguageKeys'][Keys] : - // multiple keys => use the base language type (as fall-back for now) - Keys extends Array ? Specifics['LanguageType'] : // possible extension: calculate the union of language types - never; + // multiple keys => calculate the union of language types + Keys extends Array ? (GivenKeys extends LanguageKey ? Specifics['LanguageKeys'][GivenKeys] : never) : + never +; /** Given the type of a language node (i.e. the "language type"), this type provides the relevant properties of the language type. */ // possible extension: make this type exchangable, if possible From 4cf989746e1360f67b7ba8a55c313cdebcdf122f Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Wed, 11 Feb 2026 12:29:44 +0100 Subject: [PATCH 12/17] made registration options more explicit (result of joint discussion with Markus) --- examples/lox/src/language/lox-type-checking.ts | 8 ++++---- packages/typir/src/kinds/class/class-kind.ts | 12 ++++++------ packages/typir/src/kinds/function/function-kind.ts | 4 ++-- packages/typir/src/utils/utils-definitions.ts | 13 +++++++++---- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index a80e917..2e61d5c 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -160,20 +160,20 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition }); // check for unique function declarations - typir.factory.Functions.createUniqueFunctionValidation({ registration: { languageKey: FunctionDeclaration.$type }}); + typir.factory.Functions.createUniqueFunctionValidation({ registration: 'AUTO', languageKey: FunctionDeclaration.$type }); // check for unique class declarations - const uniqueClassValidator = typir.factory.Classes.createUniqueClassValidation({ registration: 'MYSELF' }); + const uniqueClassValidator = typir.factory.Classes.createUniqueClassValidation({ registration: 'MANUAL' }); // check for unique method declarations typir.factory.Classes.createUniqueMethodValidation({ isMethodDeclaration: (node) => isMethodMember(node), // MethodMembers could have other $containers? getClassOfMethod: (method, _type) => method.$container, uniqueClassValidator: uniqueClassValidator, - registration: { languageKey: MethodMember.$type }, + registration: 'AUTO', languageKey: MethodMember.$type, }); typir.validation.Collector.addValidationRule(uniqueClassValidator, { languageKey: Class.$type }); // TODO this order is important, solve it in a different way! // check for cycles in super-sub-type relationships - typir.factory.Classes.createNoSuperClassCyclesValidation({ registration: { languageKey: Class.$type } }); + typir.factory.Classes.createNoSuperClassCyclesValidation({ registration: 'AUTO', languageKey: Class.$type }); } onNewAstNode(node: AstNode, typir: TypirLangiumServices): void { diff --git a/packages/typir/src/kinds/class/class-kind.ts b/packages/typir/src/kinds/class/class-kind.ts index 4853264..9e26889 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-kind.ts @@ -237,30 +237,30 @@ export class ClassKind implements Kind, ClassF createUniqueClassValidation(options: RegistrationOptions): UniqueClassValidation { const rule = new UniqueClassValidation(this.services); - if (options.registration === 'MYSELF') { + if (options.registration === 'MANUAL') { // do nothing, the user is responsible to register the rule } else { - this.services.validation.Collector.addValidationRule(rule, options.registration); + this.services.validation.Collector.addValidationRule(rule, options); } return rule; } createUniqueMethodValidation(options: UniqueMethodValidationOptions & RegistrationOptions): ValidationRule { const rule = new UniqueMethodValidation(this.services, options); - if (options.registration === 'MYSELF') { + if (options.registration === 'MANUAL') { // do nothing, the user is responsible to register the rule } else { - this.services.validation.Collector.addValidationRule(rule, options.registration); + this.services.validation.Collector.addValidationRule(rule, options); } return rule; } createNoSuperClassCyclesValidation(options: NoSuperClassCyclesValidationOptions & RegistrationOptions): ValidationRule { const rule = new NoSuperClassCyclesValidation(this.services, options); - if (options.registration === 'MYSELF') { + if (options.registration === 'MANUAL') { // do nothing, the user is responsible to register the rule } else { - this.services.validation.Collector.addValidationRule(rule, options.registration); + this.services.validation.Collector.addValidationRule(rule, options); } return rule; } diff --git a/packages/typir/src/kinds/function/function-kind.ts b/packages/typir/src/kinds/function/function-kind.ts index e5b0731..bea4cbd 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -248,10 +248,10 @@ export class FunctionKind implements Kind, Fun createUniqueFunctionValidation(options: RegistrationOptions): ValidationRule { const rule = new UniqueFunctionValidation(this.services); - if (options.registration === 'MYSELF') { + if (options.registration === 'MANUAL') { // do nothing, the user is responsible to register the rule } else { - this.services.validation.Collector.addValidationRule(rule, options.registration); + this.services.validation.Collector.addValidationRule(rule, options); } return rule; } diff --git a/packages/typir/src/utils/utils-definitions.ts b/packages/typir/src/utils/utils-definitions.ts index 9bfc221..3283efc 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -89,12 +89,17 @@ export function bindValidateCurrentTypeRule< * These options are used for pre-defined valiations in order to enable the user to decide, * how the created pre-defined valiation should be registered. */ -export interface RegistrationOptions { +export type RegistrationOptions = (Partial> & { /** - * 'MYSELF' indicates, that the caller is responsible to register the validation rule, - * otherwise the given options are used to register the return validation rule now. + * 'AUTO' indicates, that the validation rule is automatically registered with the given options now. */ - registration: 'MYSELF' | Partial>; + registration: 'AUTO'; +}) | { + /** + * 'MANUAL' indicates, that the caller is responsible to register the validation rule. + * In that case, the `ValidationRuleOptions` are specified during the manual registration, i.e. they are not necessary here. + */ + registration: 'MANUAL'; } From f915b8666afaf6839dae968e2b81bc870ff50fef Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 13 Feb 2026 14:26:06 +0100 Subject: [PATCH 13/17] make the properties of language nodes, which shall be hidden, customizable (result of a long coding session with Insa) --- CHANGELOG.md | 5 +++-- packages/typir-langium/src/typir-langium.ts | 7 +++++- packages/typir/src/services/validation.ts | 4 ++-- packages/typir/src/typir.ts | 25 +++++++++++++++++---- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 439ad61..fe78cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,13 @@ For each minor and major version, there is a corresponding [milestone on GitHub] ### New features -- Introduced `TypirSpecifics['LanguageKeys']` (in `typir.ts`) to make the available language nodes with their keys and TypeScript types explicit: +- Introduced `TypirSpecifics['LanguageKeys']` (in `typir.ts`) to make the available language nodes with their keys and TypeScript types explicit (#93): - By default, Typir predefines neither language nodes nor language keys in advance, i.e. any `string` values are usable as language key. - By default, Typir-Langium supports exactly the language nodes and language types, which are derived from the grammar and generated by Langium into the `ast.ts`. Looking into the LOX example, `LoxAstType` inside `examples/lox/src/language/generated/ast.ts` lists all language keys (on the left of colon) and maps them to the (TypeScript interfaces/types of the) language nodes (on the right of the colon). - When restricting the possible language keys, the TypeScript compiler shows errors, if invalid language keys are used. This happens, among others, inside inference rules or for registering validation rules. This improves type safety on TypeScript level for developers applying Typir. - If language keys are restricted, inside an inference rule with a value for `languageKey` and without a value for `filter`, it is possible now to skip the expected TypeScript type for the input node of the `matching` property, as demonstrated in the updated examples for (L)OX. This improves the usability and type-safety of the API. -- When reporting validation issues, `languageProperty` accepts only valid property names of the given `languageNode`. +- When reporting validation issues, `languageProperty` accepts only valid property names of the given `languageNode` (#93). + - Introduced `TypirSpecifics['OmittedLanguageNodeProperties']` to omit some of the existing properties of language nodes. ### Breaking changes diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index 1df8b7c..3d80dcd 100644 --- a/packages/typir-langium/src/typir-langium.ts +++ b/packages/typir-langium/src/typir-langium.ts @@ -4,6 +4,8 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ +/* eslint-disable @typescript-eslint/indent */ + import { AbstractAstReflection, AstNode, DiagnosticInfo, LangiumDefaultCoreServices, LangiumSharedCoreServices } from 'langium'; import { createDefaultTypirServicesModule, DeepPartial, inject, Module, PartialTypirServices, TypirServices, TypirSpecifics } from 'typir'; import { LangiumLanguageNodeInferenceCaching } from './features/langium-caching.js'; @@ -21,7 +23,10 @@ export interface TypirLangiumSpecifics extends TypirSpecifics { LanguageType: AstNode; // concretizes the `LanguageType`, since all language nodes of a Langium AST are AstNode's LanguageKeys: LangiumAstTypes; // applications should concretize the `LanguageKeys` with XXXAstType from the generated `ast.ts` /** Support also the Langium-specific diagnostic properties, e.g. to mark keywords or register code actions */ - ValidationMessageProperties: TypirSpecifics['ValidationMessageProperties'] & Omit, 'node'|'property'|'index'>; // 'node', 'property', and 'index' are already coverd by TypirSpecifics['ValidationMessageProperties'] with a different name + ValidationMessageProperties: TypirSpecifics['ValidationMessageProperties'] // use the default properties and the Langium-specific properties + & Omit, 'node'|'property'|'index'>; // 'node', 'property', and 'index' are already coverd by TypirSpecifics['ValidationMessageProperties'] with a different name + OmittedLanguageNodeProperties: TypirSpecifics['OmittedLanguageNodeProperties'] // enable adopters to ignore even more concrete properties + | keyof AstNode; // omit all meta-data of AstNodes, i.e. omit all "$..."-properties like "$type", "$container", "$cstNode", ... } /** diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 0266ab5..638fdd6 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -281,8 +281,8 @@ export class DefaultValidationCollector implem } protected createAcceptor(problems: Array>): ValidationProblemAcceptor { - return (problem: ValidationProblemProperties) => { - problems.push({ + return | undefined>(problem: ValidationProblemProperties) => { + problems.push(>{ ...problem, $problem: ValidationProblem, // add the missing $property-property }); diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index 4c0cfbd..ba87bac 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -185,6 +185,16 @@ export interface TypirSpecifics { /** Properties for validation issues (predefined and custom ones) */ ValidationMessageProperties: ValidationMessageProperties; + + /** + * Contains properties of language nodes, which shall be omitted for validation issues, + * i.e. these properties are not possible to attach validation markers to. + * + * The types given here are usable as (object) keys in general and therefore enable concrete, inheriting `TypirSpecifics` to specify more concrete keys. + * The types given here don't skip any keys by default, since (for example) the general "string" is not assignable to concrete keys like "property1" or "value2" + * (according to the semantics of the used `Extract<>` below). + */ + OmittedLanguageNodeProperties: string | number | symbol; } @@ -209,12 +219,19 @@ export type LanguageTypeOfLanguageKey< ; /** Given the type of a language node (i.e. the "language type"), this type provides the relevant properties of the language type. */ -// possible extension: make this type exchangable, if possible export type PropertiesOfLanguageType = T extends Specifics['LanguageType'] - ? keyof Omit only the specific properties of the concrete language type remain - | number | symbol + ? keyof Omit< + // support only the properties of the current language node: + T, + // but hide some of these properties: + Extract< + // the properties to hide are defined in the `TypirSpecifics` (and might be customized there): + Specifics['OmittedLanguageNodeProperties'], + // ignore properties, which are not in the current language node + // this enables to have additional/other/non-relevant language keys in the `TypirSpecifics` + keyof T + > > : never ; From 2fc45421bfc401a2a0a02cc4d12ec5bce0306d0d Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 13 Feb 2026 20:15:28 +0100 Subject: [PATCH 14/17] fixed watch script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9106ea0..0885c53 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "postinstall": "npm run langium:generate", "clean": "npm run clean --workspaces && shx rm -rf coverage", "build": "tsc -b tsconfig.build.json && npm run build --workspaces", - "watch": "concurrently -n typir,typir-langium,ox,lox,expression -c blue,blue,green,green \"tsc -b tsconfig.build.json -w\" \"npm run watch --workspace=typir\" \"npm run watch --workspace=typir-langium\" \"npm run watch --workspace=examples/ox\" \"npm run watch --workspace=examples/lox\" \"npm run watch --workspace=examples/expression\"", + "watch": "concurrently -n typir,typir-langium,ox,lox,expression -c blue,blue,green,green,green \"npm run watch --workspace=typir\" \"npm run watch --workspace=typir-langium\" \"npm run watch --workspace=examples/ox\" \"npm run watch --workspace=examples/lox\" \"npm run watch --workspace=examples/expression\"", "lint": "npm run lint --workspaces", "docs": "typedoc", "test": "vitest", From 051bb9208d64df79c67c9c71f2f3ff652b1f44bd Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 13 Feb 2026 20:27:02 +0100 Subject: [PATCH 15/17] documented the case, if there is no list of concrete language keys --- documentation/design.md | 16 ++++++++++++++++ .../expression/src/expression-type-system.ts | 1 + packages/typir/src/typir.ts | 10 +++++++--- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/documentation/design.md b/documentation/design.md index 456240d..833cf4f 100644 --- a/documentation/design.md +++ b/documentation/design.md @@ -59,6 +59,14 @@ All information Typir needs to know about language nodes is specified in the API ### Language type The TypeScript type of a language node is called *language type*. +If the TypeScript types of all possible language nodes have a common super class or interface `CommonSuperType`, +it should be registered in the specifics of your language in this way: + +```typescript +export interface MySpecifics extends TypirSpecifics { + LanguageType: CommonSuperType; +} +``` ### Language key @@ -82,6 +90,14 @@ export interface MySpecifics extends TypirSpecifics { } ``` +Even if there is no list of concrete language keys, adopters should override this property with `Record`, +if the `LanguageType` is set to `CommonSuperType` (see section above): + +```typescript +export interface MySpecifics extends TypirSpecifics { + LanguageKeys: Record; +} +``` ## Services and default implementations diff --git a/examples/expression/src/expression-type-system.ts b/examples/expression/src/expression-type-system.ts index d429481..7998733 100644 --- a/examples/expression/src/expression-type-system.ts +++ b/examples/expression/src/expression-type-system.ts @@ -8,6 +8,7 @@ import { BinaryExpression, isAssignment, isBinaryExpression, isCharString, isNum export interface ExpressionSpecifics extends TypirSpecifics { LanguageType: Node; + LanguageKeys: Record; // there is no list of language keys, but we know, that each element inside the AST extends `Node` } export function initializeTypir() { diff --git a/packages/typir/src/typir.ts b/packages/typir/src/typir.ts index ba87bac..ab6d0ca 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -179,8 +179,12 @@ export interface TypirSpecifics { /** This is the TypeScript super-class of all language nodes in the AST */ LanguageType: unknown; - /** The set of available language keys: - * Each language key maps to the TypeScript type (which extends 'LanguageType') of corresponding language nodes with this language key. */ + /** + * The set of available language keys: + * Each language key maps to the TypeScript type (which has to extend 'LanguageType') of corresponding language nodes with this language key. + * If no list of concrete language keys is provided during adoption, all string values are possible as language keys. + * Even without list of concrete language keys here, adopters should override this property with `Record`, if the `LanguageType` is set to `ABC`. + */ LanguageKeys: Record; /** Properties for validation issues (predefined and custom ones) */ @@ -212,7 +216,7 @@ export type LanguageTypeOfLanguageKey< // no key => use the base language type Keys extends undefined ? Specifics['LanguageType'] : // single key => use the specified language type from the "list type" - Keys extends keyof Specifics['LanguageKeys'] ? Specifics['LanguageKeys'][Keys] : + Keys extends LanguageKey ? Specifics['LanguageKeys'][Keys] : // multiple keys => calculate the union of language types Keys extends Array ? (GivenKeys extends LanguageKey ? Specifics['LanguageKeys'][GivenKeys] : never) : never From bea149bc091dddc149e0e95b84044f7f359aa5e2 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 13 Feb 2026 21:28:44 +0100 Subject: [PATCH 16/17] Provide `addValidationRulesForLanguageNodes` in core Typir --- CHANGELOG.md | 4 +- documentation/services/validation.md | 12 ++++++ .../lox/src/language/lox-type-checking.ts | 2 +- examples/ox/src/language/ox-type-checking.ts | 2 +- packages/typir-langium/README.md | 6 ++- .../src/features/langium-validation.ts | 26 +++++++------ packages/typir/src/services/validation.ts | 39 ++++++++++++++++++- 7 files changed, 73 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe78cad..c4f2186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,12 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - If language keys are restricted, inside an inference rule with a value for `languageKey` and without a value for `filter`, it is possible now to skip the expected TypeScript type for the input node of the `matching` property, as demonstrated in the updated examples for (L)OX. This improves the usability and type-safety of the API. - When reporting validation issues, `languageProperty` accepts only valid property names of the given `languageNode` (#93). - Introduced `TypirSpecifics['OmittedLanguageNodeProperties']` to omit some of the existing properties of language nodes. +- Introduced `typir.validation.Collector.addValidationRulesForLanguageNodes()` to register multiple validation rules for language keys at once with improved TypeScript safety (#93). ### Breaking changes -- Renamed `TypirLangiumSpecifics['AstTypes']` to `TypirLangiumSpecifics['LanguageKeys']` to align it with the new `TypirSpecifics['LanguageKeys']`, as described above (#93) +- Renamed `TypirLangiumSpecifics['AstTypes']` to `TypirLangiumSpecifics['LanguageKeys']` to align it with the new `TypirSpecifics['LanguageKeys']`, as described above (#93). +- Renamed `typir.validation.Collector.addValidationRulesForAstNodes` to `addValidationRulesForLanguageNodes` to align it with the new API in Typir (core), as described above (#93). ### Fixed bugs diff --git a/documentation/services/validation.md b/documentation/services/validation.md index 5100d7d..fd989e7 100644 --- a/documentation/services/validation.md +++ b/documentation/services/validation.md @@ -29,6 +29,18 @@ Some options might be given in the options object as second argument: - `languageKey`: By default, all validation rules are performed for all language nodes. In order to improve performance, validation rules with a given language key are executed only for language nodes with this language key. +To register multiple validation rules for language nodes with language keys at once, use this alternative, +which provides more TypeScript-safety and requires less manual TypeScript-type checking (if `Specifics['LanguageKeys']` is specified): + +```typescript +typir.validation.Collector.addValidationRulesForLanguageNodes({ + 'IfStatement': (node /* is of type IfStatement */, accept) => { /* use `node.condition` without casting */ }, + 'VariableDeclaration': (node /* is of type VariableDeclaration */, accept) => { /* use `node.initialValue` without casting */ }, + 'LanguageKeyWithTwoValidationRules': [(node, accept) => {}, (node, accept) => {}], + // ... +}); +``` + The call `const issues: ValidationProblem = typir.validation.Collector.validate(languageNode)` validates a language node by executing all validation rules which are applicable to the given language node and returns all found validation issues. Since Typir doesn't know the structure of the AST, there is *no* automatic traversal of the AST, i.e. *only* the given language node is validated. diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index 2e61d5c..5c3e251 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -151,7 +151,7 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition }); // some explicit validations for typing issues with Typir (replaces corresponding functions in the LoxValidator!) - typir.validation.Collector.addValidationRulesForAstNodes({ + typir.validation.Collector.addValidationRulesForLanguageNodes({ ForStatement: this.validateCondition, IfStatement: this.validateCondition, ReturnStatement: this.validateReturnStatement, diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index eeb1fc4..97b9657 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -116,7 +116,7 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { }); // explicit validations for typing issues, realized with Typir (which replaced corresponding functions in the OxValidator!) - typir.validation.Collector.addValidationRulesForAstNodes({ + typir.validation.Collector.addValidationRulesForLanguageNodes({ AssignmentStatement: (node, accept, typir) => { if (node.varRef.ref) { typir.validation.Constraints.ensureNodeIsAssignable(node.value, node.varRef.ref, accept, diff --git a/packages/typir-langium/README.md b/packages/typir-langium/README.md index 36500ee..2b68f0a 100644 --- a/packages/typir-langium/README.md +++ b/packages/typir-langium/README.md @@ -82,10 +82,12 @@ export interface MyDSLSpecifics extends TypirLangiumSpecifics { Beyond the APIs inherited from Typir core, Typir-Langium provides some *additional APIs* to ease type checking with Typir in Langium projects. This includes an API to register *validation rules* for `AstNode.$type`s, which is similar to the API of Langium for registering validation checks. +In contrast to the provided similar core API, `AstNode` might be used as key to register validation rules for all AST nodes. +By design, the keys are the `$type` values from the generated types in `ast.ts`. Here is an excerpt from the LOX example: ```typescript -typir.validation.Collector.addValidationRulesForAstNodes({ +typir.validation.Collector.addValidationRulesForLanguageNodes({ IfStatement: (node /* is of type IfStatement */, accept) => typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool, accept, () => ({ message: "Conditions need to be evaluated to 'boolean'.", languageProperty: 'condition' })), VariableDeclaration: ... , @@ -120,7 +122,7 @@ typir.Inference.addInferenceRulesForAstNodes({ ## Examples -Look at the examples in the `examples/` folder of the repo ([here](../../examples)). There we have some demo projects for you to get started. +Look at the examples in the `examples/` folder of the repo ([here](../../examples)). There we have some demo projects for you to get started, including LOX and OX. ## License diff --git a/packages/typir-langium/src/features/langium-validation.ts b/packages/typir-langium/src/features/langium-validation.ts index 8e27db1..b1e0f38 100644 --- a/packages/typir-langium/src/features/langium-validation.ts +++ b/packages/typir-langium/src/features/langium-validation.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { LangiumDefaultCoreServices, Properties, ValidationAcceptor, ValidationChecks } from 'langium'; -import { DefaultValidationCollector, LanguageKey, TypirServices, ValidationCollector, ValidationProblem, ValidationRule } from 'typir'; +import { DefaultValidationCollector, TypirServices, ValidationCollector, ValidationProblem, ValidationRule, ValidationRulesForLanguageKeys } from 'typir'; import { TypirLangiumServices, TypirLangiumSpecifics } from '../typir-langium.js'; export function registerTypirValidationChecks(langiumServices: LangiumDefaultCoreServices, typirServices: TypirLangiumServices) { @@ -101,32 +101,34 @@ export class DefaultLangiumTypirValidator { return [...]; }, + * Another$typeName: (node, typir) => ..., + * // ... + * AstNode: (node, typir) => ..., // executed for all AstNodes * }); * ``` * - * @param T a type definition mapping language specific type names (keys) to the corresponding types (values) + * In contrast to Typir (core), Typir-Langium enables to register validation rules to `AstNode` as well. */ -export type LangiumValidationRules = { - [K in LanguageKey]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? ValidationRule | Array> : never -} & { +export type LangiumValidationRules = ValidationRulesForLanguageKeys & { + // TODO nodes inside ValidationRules are typed by the TypeScript compiler as `any` not as `AstNode` AstNode?: ValidationRule | Array>; } export interface LangiumValidationCollector extends ValidationCollector { - addValidationRulesForAstNodes(rules: LangiumValidationRules): void; + addValidationRulesForLanguageNodes(rules: LangiumValidationRules): void; } export class DefaultLangiumValidationCollector extends DefaultValidationCollector implements LangiumValidationCollector { - addValidationRulesForAstNodes(rules: LangiumValidationRules): void { + override addValidationRulesForLanguageNodes(rules: LangiumValidationRules): void { // map this approach for registering validation rules to the key-value approach from core Typir - for (const [type, ruleCallbacks] of Object.entries(rules)) { - const languageKey = type === 'AstNode' ? undefined : type; // using 'AstNode' as key is equivalent to specifying no key - const callbacks = ruleCallbacks as ValidationRule | Array>; + for (const [$type, validationRules] of Object.entries(rules)) { + const languageKey = $type === 'AstNode' ? undefined : $type; // using 'AstNode' as key is equivalent to specifying no key: the rule is applied to all AstNodes + const callbacks = validationRules as ValidationRule | Array>; if (Array.isArray(callbacks)) { for (const callback of callbacks) { this.addValidationRule(callback, { languageKey }); diff --git a/packages/typir/src/services/validation.ts b/packages/typir/src/services/validation.ts index 638fdd6..dbde0e6 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { Type, isType } from '../graph/type-node.js'; -import { LanguageKey, PropertiesOfLanguageType, TypirServices, TypirSpecifics } from '../typir.js'; +import { LanguageKey, LanguageTypeOfLanguageKey, PropertiesOfLanguageType, TypirServices, TypirSpecifics } from '../typir.js'; import { RuleCollectorListener, RuleOptions, RuleRegistry } from '../utils/rule-registration.js'; import { TypirProblem, isSpecificTypirProblem } from '../utils/utils-definitions.js'; import { TypeCheckStrategy, createTypeCheckStrategy } from '../utils/utils-type-comparison.js'; @@ -111,6 +111,27 @@ export type ValidationMessageProvider< * Another observation: It seems, that this problem also decreases the auto-completion proposals, e.g. for 'languageProperty'. */ + +/** + * Taken and adapted from 'ValidationChecks' from 'langium'. + * + * A utility type for associating language keys to corresponding validation rules. For example: + * + * ```typescript + * addValidationRulesForLanguageNodes({ + * VariableDeclaration: (node, typir) => { return [...]; }, + * AnotherLanguageKey: (node, typir) => ..., + * // ... + * }); + * ``` + * + * If `Specifics['LanguageKeys']` contains no list of concrete language keys, any string values are possible as language keys here. + */ +export type ValidationRulesForLanguageKeys = { + [K in LanguageKey]?: ValidationRule> | Array>> +} + + export interface ValidationConstraints { ensureNodeIsAssignable | undefined = undefined>( sourceNode: S | undefined, expected: Type | undefined | E, @@ -259,6 +280,8 @@ export interface ValidationCollector { */ removeValidationRule(rule: ValidationRule, options?: Partial>): void; + addValidationRulesForLanguageNodes(rules: ValidationRulesForLanguageKeys): void; + addListener(listener: ValidationCollectorListener): void; removeListener(listener: ValidationCollectorListener): void; } @@ -363,6 +386,20 @@ export class DefaultValidationCollector implem } } + addValidationRulesForLanguageNodes(rules: ValidationRulesForLanguageKeys): void { + // map this approach for registering validation rules to the key-value approach above + for (const [languageKey, validationRules] of Object.entries(rules)) { + const callbacks = validationRules as ValidationRule | Array>; + if (Array.isArray(callbacks)) { + for (const callback of callbacks) { + this.addValidationRule(callback, { languageKey }); + } + } else { + this.addValidationRule(callbacks, { languageKey }); + } + } + } + addListener(listener: ValidationCollectorListener): void { this.listeners.push(listener); } From f0a419e2532869a5935ea013511b6275b2513e93 Mon Sep 17 00:00:00 2001 From: Johannes Meier Date: Fri, 13 Feb 2026 21:57:58 +0100 Subject: [PATCH 17/17] Provide `addInferenceRulesForLanguageNodes` in core Typir --- CHANGELOG.md | 2 ++ documentation/services/inference.md | 21 ++++++++++++++--- .../lox/src/language/lox-type-checking.ts | 2 +- examples/ox/src/language/ox-type-checking.ts | 2 +- packages/typir-langium/README.md | 2 +- .../src/features/langium-inference.ts | 17 +++++++------- packages/typir-langium/src/typir-langium.ts | 2 +- packages/typir/src/services/inference.ts | 23 ++++++++++++++++++- 8 files changed, 54 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f2186..0352a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,13 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - When reporting validation issues, `languageProperty` accepts only valid property names of the given `languageNode` (#93). - Introduced `TypirSpecifics['OmittedLanguageNodeProperties']` to omit some of the existing properties of language nodes. - Introduced `typir.validation.Collector.addValidationRulesForLanguageNodes()` to register multiple validation rules for language keys at once with improved TypeScript safety (#93). +- Introduced `typir.Inference.addInferenceRulesForLanguageNodes()` to register multiple inference rules for language keys at once with improved TypeScript safety (#93). ### Breaking changes - Renamed `TypirLangiumSpecifics['AstTypes']` to `TypirLangiumSpecifics['LanguageKeys']` to align it with the new `TypirSpecifics['LanguageKeys']`, as described above (#93). - Renamed `typir.validation.Collector.addValidationRulesForAstNodes` to `addValidationRulesForLanguageNodes` to align it with the new API in Typir (core), as described above (#93). +- Renamed `typir.Inference.addInferenceRulesForAstNodes` to `addInferenceRulesForLanguageNodes` to align it with the new API in Typir (core), as described above (#93). ### Fixed bugs diff --git a/documentation/services/inference.md b/documentation/services/inference.md index 0d56f91..6354677 100644 --- a/documentation/services/inference.md +++ b/documentation/services/inference.md @@ -1,8 +1,8 @@ # Type inference -Type inference infers a Typir type for a given language node, i.e. it answers the question, which Tyir type has a language node. +Type inference infers a Typir type for a given language node, i.e. it answers the question, which Typir type has a language node. Therefore type inference is the central part which connects the type system and its type graph with an AST consisting of language nodes. -These relationships are defined with *inference rules*, which identify the type for a given language node. +These relationships are defined with *inference rules*, which identify the Typir type for a given language node. ## API @@ -14,12 +14,27 @@ and returns either the inferred type or an (maybe empty) array with reasons, why typir.Inference.inferType(languageNode: Specifics['LanguageType']): Type | InferenceProblem[] ``` -Inference rules can be registered with this API call: +Inference rules can be registered with this API call (the `TypeInferenceRuleOptions` are the same as for [validation rules](../services/validation.md)): ```typescript typir.Inference.addInferenceRule(rule: TypeInferenceRule, options?: TypeInferenceRuleOptions): void ``` +It is possible to register multiple inference rules for language keys at once with this API: + +```typescript +typir.Inference.addInferenceRulesForLanguageNodes(rules: InferenceRulesForLanguageKeys): void +``` + +The following example sketches, how to use this API, and shows its benefits regarding TypeScript safety (if `Specifics['LanguageKeys']` is specified): + +```typescript +typir.Inference.addInferenceRulesForLanguageNodes({ + 'VariableDeclaration': (languageNode /* is of type VariableDeclaration */) => languageNode.value, + 'VariableUsage': (languageNode /* is of type VariableUsage */) => languageNode.ref, + // ... +}); +``` ## Default implementation diff --git a/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index 5c3e251..d6b6add 100644 --- a/examples/lox/src/language/lox-type-checking.ts +++ b/examples/lox/src/language/lox-type-checking.ts @@ -114,7 +114,7 @@ export class LoxTypeSystem implements LangiumTypeSystemDefinition typir.factory.Operators.createUnary({ name: '-', signature: { operand: typeNumber, return: typeNumber }}).inferenceRule(unaryInferenceRule).finish(); // additional inference rules for ... - typir.Inference.addInferenceRulesForAstNodes({ + typir.Inference.addInferenceRulesForLanguageNodes({ // ... member calls MemberCall: (languageNode) => { const ref = languageNode.element?.ref; diff --git a/examples/ox/src/language/ox-type-checking.ts b/examples/ox/src/language/ox-type-checking.ts index 97b9657..b1da27c 100644 --- a/examples/ox/src/language/ox-type-checking.ts +++ b/examples/ox/src/language/ox-type-checking.ts @@ -82,7 +82,7 @@ export class OxTypeSystem implements LangiumTypeSystemDefinition { */ // additional inference rules ... - typir.Inference.addInferenceRulesForAstNodes({ + typir.Inference.addInferenceRulesForLanguageNodes({ // ... for member calls (which are used in expressions) MemberCall: (languageNode) => { const ref = languageNode.element.ref; diff --git a/packages/typir-langium/README.md b/packages/typir-langium/README.md index 2b68f0a..ea6a321 100644 --- a/packages/typir-langium/README.md +++ b/packages/typir-langium/README.md @@ -103,7 +103,7 @@ Note that the properties `node`, `property`, and `index` are named `languageNode In similar way, it is possible to register *inference rules* for `AstNode.$type`s, as demonstrated in the LOX example: ```typescript -typir.Inference.addInferenceRulesForAstNodes({ +typir.Inference.addInferenceRulesForLanguageNodes({ // ... VariableDeclaration: (languageNode /* is of type VariableDeclaration */) => { if (languageNode.type) { diff --git a/packages/typir-langium/src/features/langium-inference.ts b/packages/typir-langium/src/features/langium-inference.ts index b045cad..cf97797 100644 --- a/packages/typir-langium/src/features/langium-inference.ts +++ b/packages/typir-langium/src/features/langium-inference.ts @@ -4,26 +4,25 @@ * terms of the MIT License, which is available in the project root. ******************************************************************************/ -import { DefaultTypeInferenceCollector, LanguageKey, TypeInferenceCollector, TypeInferenceRule } from 'typir'; +import { DefaultTypeInferenceCollector, InferenceRulesForLanguageKeys, TypeInferenceCollector, TypeInferenceRule } from 'typir'; import { TypirLangiumSpecifics } from '../typir-langium.js'; -export type LangiumTypeInferenceRules = { - [K in LanguageKey]?: Specifics['LanguageKeys'][K] extends Specifics['LanguageType'] ? TypeInferenceRule | Array> : never -} & { +export type LangiumTypeInferenceRules = InferenceRulesForLanguageKeys & { + // TODO nodes inside ValidationRules are typed by the TypeScript compiler as `any` not as `AstNode` AstNode?: TypeInferenceRule | Array>; } export interface LangiumTypeInferenceCollector extends TypeInferenceCollector { - addInferenceRulesForAstNodes(rules: LangiumTypeInferenceRules): void; + addInferenceRulesForLanguageNodes(rules: LangiumTypeInferenceRules): void; } export class DefaultLangiumTypeInferenceCollector extends DefaultTypeInferenceCollector implements LangiumTypeInferenceCollector { - addInferenceRulesForAstNodes(rules: LangiumTypeInferenceRules): void { + override addInferenceRulesForLanguageNodes(rules: LangiumTypeInferenceRules): void { // map this approach for registering inference rules to the key-value approach from core Typir - for (const [type, ruleCallbacks] of Object.entries(rules)) { - const languageKey = type === 'AstNode' ? undefined : type; // using 'AstNode' as key is equivalent to specifying no key - const callbacks = ruleCallbacks as TypeInferenceRule | Array>; + for (const [$type, inferenceRules] of Object.entries(rules)) { + const languageKey = $type === 'AstNode' ? undefined : $type; // using 'AstNode' as key is equivalent to specifying no key + const callbacks = inferenceRules as TypeInferenceRule | Array>; if (Array.isArray(callbacks)) { for (const callback of callbacks) { this.addInferenceRule(callback, { languageKey }); diff --git a/packages/typir-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index 3d80dcd..7994f90 100644 --- a/packages/typir-langium/src/typir-langium.ts +++ b/packages/typir-langium/src/typir-langium.ts @@ -69,7 +69,7 @@ export function createLangiumSpecificTypirServicesModule(langiumServices: LangiumSharedCoreServices): Module, TypirLangiumAddedServices> { return { diff --git a/packages/typir/src/services/inference.ts b/packages/typir/src/services/inference.ts index 91ca376..4c7a9a7 100644 --- a/packages/typir/src/services/inference.ts +++ b/packages/typir/src/services/inference.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { isType, Type } from '../graph/type-node.js'; -import { LanguageKey, TypirServices, TypirSpecifics } from '../typir.js'; +import { LanguageKey, LanguageTypeOfLanguageKey, TypirServices, TypirSpecifics } from '../typir.js'; import { RuleCollectorListener, RuleOptions, RuleRegistry } from '../utils/rule-registration.js'; import { isSpecificTypirProblem, TypirProblem } from '../utils/utils-definitions.js'; import { assertUnreachable, removeFromArray, toArray } from '../utils/utils.js'; @@ -95,6 +95,11 @@ export interface TypeInferenceRuleWithInferringChildren< } +export type InferenceRulesForLanguageKeys = { + [K in LanguageKey]?: TypeInferenceRule> | Array>> +} + + export interface TypeInferenceCollectorListener { onAddedInferenceRule(rule: TypeInferenceRule, options: TypeInferenceRuleOptions): void; onRemovedInferenceRule(rule: TypeInferenceRule, options: TypeInferenceRuleOptions): void; @@ -135,6 +140,8 @@ export interface TypeInferenceCollector { */ removeInferenceRule(rule: TypeInferenceRule, options?: Partial>): void; + addInferenceRulesForLanguageNodes(rules: InferenceRulesForLanguageKeys): void; + addListener(listener: TypeInferenceCollectorListener): void; removeListener(listener: TypeInferenceCollectorListener): void; } @@ -162,6 +169,20 @@ export class DefaultTypeInferenceCollector imp this.ruleRegistry.removeRule(rule as unknown as TypeInferenceRule, optionsToRemove); } + addInferenceRulesForLanguageNodes(rules: InferenceRulesForLanguageKeys): void { + // map this approach for registering inference rules to the key-value approach above + for (const [languageKey, inferenceRules] of Object.entries(rules)) { + const callbacks = inferenceRules as TypeInferenceRule | Array>; + if (Array.isArray(callbacks)) { + for (const callback of callbacks) { + this.addInferenceRule(callback, { languageKey }); + } + } else { + this.addInferenceRule(callbacks, { languageKey }); + } + } + } + inferType(languageNode: Specifics['LanguageType']): Type | Array> { // is the result already in the cache? const cached = this.cacheGet(languageNode);