diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a2cb93..0352a6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,34 @@ 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 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` (#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 +- + + ## v0.3.3 (2026-02-10) @@ -23,6 +41,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 +49,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 +63,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 +95,6 @@ For each minor and major version, there is a corresponding [milestone on GitHub] LanguageType: unknown; } ``` - - `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 +126,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - 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 +134,13 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - 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 +197,14 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - 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 +212,7 @@ For each minor and major version, there is a corresponding [milestone on GitHub] - 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 80ce2979..f781fd7d 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/bindings/binding-langium.md b/documentation/bindings/binding-langium.md index 56ea45c9..e14743ab 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/customization.md b/documentation/customization.md index 0e97dc94..b960113c 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 cf802bfd..833cf4fa 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,90 @@ 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 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 + +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 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 + +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 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). +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; +} +``` + +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 - services - (default) implementations @@ -41,10 +107,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/getting-started.md b/documentation/getting-started.md index 5ebf36ed..9032b3f2 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/documentation/index.md b/documentation/index.md index 6ebc64e8..cf688b4f 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/inference.md b/documentation/services/inference.md index 1d16d423..63546775 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 some language nodes. +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/documentation/services/language.md b/documentation/services/language.md index af374d4d..5684db2a 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"). diff --git a/documentation/services/validation.md b/documentation/services/validation.md new file mode 100644 index 00000000..fd989e77 --- /dev/null +++ b/documentation/services/validation.md @@ -0,0 +1,101 @@ +# 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. + +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. + +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 ac88f531..06576027 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/expression/src/expression-type-system.ts b/examples/expression/src/expression-type-system.ts index d4294817..79987339 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/examples/lox/src/language/lox-type-checking.ts b/examples/lox/src/language/lox-type-checking.ts index c0bbfce3..d6b6adda 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 { @@ -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, }; @@ -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 @@ -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; @@ -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, @@ -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 { @@ -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(); @@ -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 2b8a5659..b1da27c2 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 { @@ -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!) @@ -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; @@ -116,12 +116,13 @@ 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, (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' })); } } @@ -166,7 +167,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 +175,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/package.json b/package.json index 9106ea05..0885c53f 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", diff --git a/packages/typir-langium/README.md b/packages/typir-langium/README.md index f0be4421..ea6a3213 100644 --- a/packages/typir-langium/README.md +++ b/packages/typir-langium/README.md @@ -72,7 +72,7 @@ export class MyDSLTypeSystem implements LangiumTypeSystemDefinition typir.validation.Constraints.ensureNodeIsAssignable(node.condition, typeBool, accept, () => ({ message: "Conditions need to be evaluated to 'boolean'.", languageProperty: 'condition' })), VariableDeclaration: ... , @@ -101,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) { @@ -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-inference.ts b/packages/typir-langium/src/features/langium-inference.ts index 688f88eb..cf977970 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, TypeInferenceCollector, TypeInferenceRule } from 'typir'; +import { DefaultTypeInferenceCollector, InferenceRulesForLanguageKeys, TypeInferenceCollector, TypeInferenceRule } from 'typir'; import { TypirLangiumSpecifics } from '../typir-langium.js'; -export type LangiumTypeInferenceRules = { - [K in keyof Specifics['AstTypes']]?: Specifics['AstTypes'][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/features/langium-language.ts b/packages/typir-langium/src/features/langium-language.ts index 39088663..6f3ddef8 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']): string { + override getLanguageNodeKey(languageNode: Specifics['LanguageType']): LanguageKey { return languageNode.$type; } - override getAllSubKeys(languageKey: string): string[] { - const result = this.reflection.getAllSubTypes(languageKey); + 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: string): string[] { + 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 8e0cff35..b1e0f380 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, TypirServices, ValidationCollector, ValidationProblem, ValidationRule, ValidationRulesForLanguageKeys } from 'typir'; import { TypirLangiumServices, TypirLangiumSpecifics } from '../typir-langium.js'; export function registerTypirValidationChecks(langiumServices: LangiumDefaultCoreServices, typirServices: TypirLangiumServices) { @@ -86,7 +86,7 @@ export class DefaultLangiumTypirValidator, + property: problem.languageProperty as (Properties | undefined), index: problem.languageIndex, // copy all other DiagnosticInfo properties: ...problem, @@ -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 keyof Specifics['AstTypes']]?: Specifics['AstTypes'][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-langium/src/typir-langium.ts b/packages/typir-langium/src/typir-langium.ts index cbbf6130..7994f901 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'; @@ -18,10 +20,13 @@ 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 + 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", ... } /** @@ -64,7 +69,7 @@ export function createLangiumSpecificTypirServicesModule(langiumServices: LangiumSharedCoreServices): Module, TypirLangiumAddedServices> { return { diff --git a/packages/typir/src/graph/type-graph.ts b/packages/typir/src/graph/type-graph.ts index b223a383..069b771f 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 5c18a900..72fd8e9c 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 @@ -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/bottom/bottom-kind.ts b/packages/typir/src/kinds/bottom/bottom-kind.ts index 7d382a05..590fd260 100644 --- a/packages/typir/src/kinds/bottom/bottom-kind.ts +++ b/packages/typir/src/kinds/bottom/bottom-kind.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeDetails } from '../../graph/type-node.js'; -import { TypirServices, TypirSpecifics } from '../../typir.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'; @@ -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/bottom/bottom-type.ts b/packages/typir/src/kinds/bottom/bottom-type.ts index 0a8c5482..40df4574 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/class-initializer.ts b/packages/typir/src/kinds/class/class-initializer.ts index 8239c771..fbc640d4 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, optionsBoundToType, skipInferenceRuleForExistingType, ValidationRuleWithOptions } 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'; @@ -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; @@ -303,13 +309,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 ede2e321..9e268898 100644 --- a/packages/typir/src/kinds/class/class-kind.ts +++ b/packages/typir/src/kinds/class/class-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 { InferenceRuleNotApplicable } from '../../services/inference.js'; import { ValidationRule } from '../../services/validation.js'; -import { TypirServices, TypirSpecifics } from '../../typir.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'; @@ -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 { @@ -73,20 +81,30 @@ 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) } export interface ClassConfigurationChain { - inferenceRuleForClassDeclaration(rule: InferCurrentTypeRule): ClassConfigurationChain; - inferenceRuleForClassLiterals(rule: InferClassLiteral): ClassConfigurationChain; + inferenceRuleForClassDeclaration< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferCurrentTypeRule): ClassConfigurationChain; + + inferenceRuleForClassLiterals< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassLiteral): ClassConfigurationChain; - inferenceRuleForFieldAccess(rule: InferClassFieldAccess): ClassConfigurationChain; + inferenceRuleForFieldAccess< + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, + >(rule: InferClassFieldAccess): ClassConfigurationChain; finish(): TypeInitializer; } @@ -217,32 +235,32 @@ 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') { + 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 { + 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 { + 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; } @@ -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/class/top-class-type.ts b/packages/typir/src/kinds/class/top-class-type.ts index 264a3fff..81d8cf18 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 c5452b99..8448fb81 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'; @@ -111,7 +111,7 @@ 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/custom/custom-kind.ts b/packages/typir/src/kinds/custom/custom-kind.ts index c3fec2da..3b7e7a65 100644 --- a/packages/typir/src/kinds/custom/custom-kind.ts +++ b/packages/typir/src/kinds/custom/custom-kind.ts @@ -8,7 +8,7 @@ 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 { 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'; @@ -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 449c6695..cc0720bf 100644 --- a/packages/typir/src/kinds/function/function-inference-call.ts +++ b/packages/typir/src/kinds/function/function-inference-call.ts @@ -7,7 +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, 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'; @@ -25,14 +25,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 +48,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 +72,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-initializer.ts b/packages/typir/src/kinds/function/function-initializer.ts index 34e9269c..cb96eb85 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 b13edd19..bea4cbd7 100644 --- a/packages/typir/src/kinds/function/function-kind.ts +++ b/packages/typir/src/kinds/function/function-kind.ts @@ -5,11 +5,11 @@ ******************************************************************************/ 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 { 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'; @@ -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); } /** @@ -116,16 +118,23 @@ 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) } 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) @@ -237,12 +246,12 @@ 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') { + 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; } @@ -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 711a4f27..0510333f 100644 --- a/packages/typir/src/kinds/function/function-overloading.ts +++ b/packages/typir/src/kinds/function/function-overloading.ts @@ -7,7 +7,7 @@ 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 { removeFromArray } from '../../utils/utils.js'; import { OverloadedFunctionsTypeInferenceRule } from './function-inference-overloaded.js'; @@ -30,9 +30,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 +107,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 @@ -122,7 +126,7 @@ export class AvailableFunctionsManager 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/function/function-validation-calls.ts b/packages/typir/src/kinds/function/function-validation-calls.ts index 51c51fa4..c168575e 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'; @@ -18,11 +18,11 @@ 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> { +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 | 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 = []; + const keysToApply: Array | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(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 6f96a315..df86fb2a 100644 --- a/packages/typir/src/kinds/primitive/primitive-kind.ts +++ b/packages/typir/src/kinds/primitive/primitive-kind.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeDetails } from '../../graph/type-node.js'; -import { TypirServices, TypirSpecifics } from '../../typir.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'; @@ -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 4e76edfa..beae6677 100644 --- a/packages/typir/src/kinds/top/top-kind.ts +++ b/packages/typir/src/kinds/top/top-kind.ts @@ -5,7 +5,7 @@ ******************************************************************************/ import { TypeDetails } from '../../graph/type-node.js'; -import { TypirServices, TypirSpecifics } from '../../typir.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'; @@ -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/kinds/top/top-type.ts b/packages/typir/src/kinds/top/top-type.ts index 66a6809a..c37c1fbd 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/services/inference.ts b/packages/typir/src/services/inference.ts index 38f9db72..4c7a9a7e 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, 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'; @@ -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 @@ -87,12 +95,17 @@ export interface TypeInferenceRuleWithInferringChildren = { + [K in LanguageKey]?: TypeInferenceRule> | Array>> +} + + export interface TypeInferenceCollectorListener { - 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 +130,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 +138,16 @@ 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; + + addInferenceRulesForLanguageNodes(rules: InferenceRulesForLanguageKeys): 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,14 +161,28 @@ 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); } + 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); @@ -190,7 +219,7 @@ export class DefaultTypeInferenceCollector imp this.checkForError(languageNode); // determine all keys to check - const keysToApply: Array = []; + const keysToApply: Array | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); @@ -319,11 +348,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 +441,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 +452,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 d0c9dd83..0e3b68d6 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']): string | 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: string): string[]; + 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: string): string[]; + 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']): string | undefined { + getLanguageNodeKey(_languageNode: Specifics['LanguageType']): LanguageKey | undefined { return undefined; } - getAllSubKeys(_languageKey: string): string[] { + getAllSubKeys(_languageKey: LanguageKey): Array> { return []; } - getAllSuperKeys(_languageKey: string): string[] { + getAllSuperKeys(_languageKey: LanguageKey): Array> { return []; } diff --git a/packages/typir/src/services/operator.ts b/packages/typir/src/services/operator.ts index efb2390a..e7a68f60 100644 --- a/packages/typir/src/services/operator.ts +++ b/packages/typir/src/services/operator.ts @@ -8,7 +8,7 @@ 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, TypirServices, TypirSpecifics } from '../typir.js'; import { NameTypePair } from '../utils/utils-definitions.js'; import { toArray } from '../utils/utils.js'; import { ValidationProblemAcceptor } from './validation.js'; @@ -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; @@ -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 546504ee..dbde0e6b 100644 --- a/packages/typir/src/services/validation.ts +++ b/packages/typir/src/services/validation.ts @@ -5,25 +5,27 @@ ******************************************************************************/ import { Type, isType } from '../graph/type-node.js'; -import { TypirSpecifics, TypirServices, MakePropertyOptional } 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'; -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'; 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[]; } 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'>; // TODO If unknown properties are specified, no TypeScript compiler error is shown + 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,28 +96,60 @@ 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'. + */ + + +/** + * 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( + 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 { @@ -119,35 +163,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); @@ -205,11 +254,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 +272,21 @@ 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; + + addValidationRulesForLanguageNodes(rules: ValidationRulesForLanguageKeys): 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> = []; @@ -253,8 +304,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 }); @@ -272,7 +323,7 @@ export class DefaultValidationCollector implem validate(languageNode: Specifics['LanguageType']): Array> { // determine all keys to check - const keysToApply: Array = []; + const keysToApply: Array | undefined> = []; const languageKey = this.services.Language.getLanguageNodeKey(languageNode); if (languageKey === undefined) { keysToApply.push(undefined); @@ -319,7 +370,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 +378,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 { @@ -335,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); } @@ -342,11 +407,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 +439,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 +450,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/test/predefined-language-nodes.ts b/packages/typir/src/test/predefined-language-nodes.ts index 34a7f201..19326cca 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 ac5c819b..ab6d0ca5 100644 --- a/packages/typir/src/typir.ts +++ b/packages/typir/src/typir.ts @@ -24,6 +24,10 @@ 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 */ /** * Some design decisions for Typir: @@ -37,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; @@ -166,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. */ @@ -189,6 +176,66 @@ 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 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) */ 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; } + + +/** 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 LanguageKey ? Specifics['LanguageKeys'][Keys] : + // 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. */ +export type PropertiesOfLanguageType = + T extends Specifics['LanguageType'] + ? 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 +; diff --git a/packages/typir/src/utils/rule-registration.ts b/packages/typir/src/utils/rule-registration.ts index 8db2850b..1d4c1a29 100644 --- a/packages/typir/src/utils/rule-registration.ts +++ b/packages/typir/src/utils/rule-registration.ts @@ -6,17 +6,17 @@ 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 { +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: LanguageKeys; /** * 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|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: LanguageKey | 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: LanguageKey | undefined): boolean { const rules = this.languageTypeToRules.get(languageKey); if (rules) { const result = removeFromArray(rule, rules); @@ -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); @@ -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 b7342574..3283efcc 100644 --- a/packages/typir/src/utils/utils-definitions.ts +++ b/packages/typir/src/utils/utils-definitions.ts @@ -10,7 +10,7 @@ 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'; /** @@ -45,12 +45,17 @@ 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( - rule: InferCurrentTypeRule, type: TypeType -): ValidationRuleWithOptions | undefined { +export function bindValidateCurrentTypeRule< + CurrentType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + rule: InferCurrentTypeRule, type: CurrentType +): 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! @@ -84,12 +89,17 @@ export function bindValidateCurrentTypeRule = (Partial> & { + /** + * 'AUTO' indicates, that the validation rule is automatically registered with the given options now. + */ + registration: 'AUTO'; +}) | { /** - * '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. + * '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: 'MYSELF' | Partial; + registration: 'MANUAL'; } @@ -100,60 +110,55 @@ 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. * This utility type is often used for inference rules which are annotated to the declaration of a type. * At least one of the properties needs to be specified. */ export interface InferCurrentTypeRule< - TypeType extends Type, + CurrentType extends Type, Specifics extends TypirSpecifics, - T extends Specifics['LanguageType'] = Specifics['LanguageType'], + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey, > { - languageKey?: string | string[]; - 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: 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( - inferenceRule: InferCurrentTypeRule, newType: TypeType, existingType: TypeType +export function skipInferenceRuleForExistingType< + CurrentType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + inferenceRule: InferCurrentTypeRule, newType: CurrentType, existingType: CurrentType ): boolean { if (newType !== existingType) { const skipRuleForExisting = inferenceRule.skipThisRuleIfThisTypeAlreadyExists; @@ -163,17 +168,27 @@ export function skipInferenceRuleForExistingType( - rule: InferCurrentTypeRule +function checkRule< + CurrentType 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< + CurrentType extends Type, + Specifics extends TypirSpecifics, + LanguageKey extends LanguageKeys = undefined, + LanguageType extends LanguageTypeOfLanguageKey = LanguageTypeOfLanguageKey +>( + rule: InferCurrentTypeRule, type: CurrentType +): InferenceRuleWithOptions { checkRule(rule); // fail early return { rule: (languageNode, _typir) => { @@ -194,7 +209,7 @@ export function bindInferCurrentTypeRule( - 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 diff --git a/packages/typir/src/utils/utils.ts b/packages/typir/src/utils/utils.ts index ca5131d0..dc6402a5 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>; diff --git a/packages/typir/test/services/inference-registry.test.ts b/packages/typir/test/services/inference-registry.test.ts index 3bc30590..3d7ea96b 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 04fea689..0b210515 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); }