Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Reworked the infrastructure for validation rules and inference rules #64

Open
wants to merge 44 commits into
base: main
Choose a base branch
from

Conversation

JohannesMeierSE
Copy link
Collaborator

Main motivation for this PR is to improve the performance of validation and inference rules:

  • Do not apply each validation/inference rule to each language node, since most rules are related only to few language nodes.
  • Therefore register these rules to language keys, calculate the language key of the current language node to validate/infer and apply only those rules, which are registered to this language key (inspired by the ValidationRegistry of Langium!)
  • undefined remains as a fall-back solution
  • Another benefit is, that validation and inference rules can be defined in the same way, as you would register validation checks in Langium! See OX and LOX for examples.

Beyond that, this PR contributes:

  • New concept of "language keys" of language nodes to register inference and validation rules, with the new service LanguageService
  • Nearly all TypeScript types in the source code have <LanguageType = unknown> in order to get rid of unknown in the source code. Benefit: In typir-langium projects, we work everywhere with AstNode (instead of unknown). In our internal test projects, we have now TestLanguageNode instead of unknown. Downside: The additional generics in the code make reading/writing the source code slower.
  • The factory API to create new types (e.g. typir.factory.Primitives.create({ ... })) is extended with a chaining API to register inference rules which are dedicated for the currently created type (e.g. typir.factory.Primitives.create({...}).inferenceRule({...}).finish()): This makes the definition of inference rules more (TypeScript-)type-safe and allows to define an arbitrary number of these rules.
  • Create the predefined validations using the factory API, e.g. typir.factory.Functions.createUniqueFunctionValidation()
  • Reworked the API of validation rules to create validation hints: Instead of returning ValidationProblems, they need to be given to the ValidationProblemAcceptor now, which is provided as additional argument inside validation rules (strongly inspired by Langium!)
  • Lots of fixed bugs, refactorings and new utilities

Suggestions for the review:

  1. Start to read the CHANGELOG.md to get an overview.
  2. Then look at the OX and LOX applications to see, how the API changed.
  3. Look at the new test cases in Typir core to get an idea of the new features/possibilities. Please suggest ideas for additional test cases!
  4. Now it is time to review the changes in more detail, I suggest to look at them commit by commit (note that the commits are less clean/separated from each other compared to my previous PRs). Check whether the CHANGELOG.md is complete.
  5. Finally search for TODO review in the source code to see some more interesting points to discuss during the review.

…itrary number of type-safe inference rules (for Functions, operators, classes, primitives), inference made TypeSelector type-safe
…with `validateArgumentsOfFunctionCalls` inside inference rules, moved validation into its own file, fixed bug, more comments
…ference, prevent duplicates during registration
…tion rules, some test cases, fixed various bugs, improved TypeScript type-safety
…to be given to the new ValidationProblemAcceptor now
…oit language keys, combined validations for arguments matching signatures and for specific calls), renamings
Copy link
Member

@insafuhrmann insafuhrmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much @JohannesMeierSE this is not only a lot of work but also (again) very well described for review. I really appreciate the main contributions of the PR very much, especially the introduction of language keys and the chain notation. This should not only improve performance but also readability. I also like the possibility to create the predefined validations with the factory API.
I have hesitated with a general approval a lot though, as I was not 100% sure about the <LanguageType = unknown> additions. I really appreciate the benefit, but to see the changes overall with the additional generics is quite overwhelming. In the end I think I am OK with it.
I am a bit irritated by the discovery noted in your TODO review in line 19, of the class.test.ts and have no ready answer, I think this should be investigated further.
I added some detailed comments.
Regarding test coverage I had no idea for further tests at the moment but they can be added when something comes to mind.

- Associate inference rules with language keys for an improved performance
- Typir-Langium: new API to register inference rules to the `$type` of the `AstNode` to validate,
e.g. `addInferenceRulesForAstNodes({ MemberCall: <InferenceRule1>, VariableDeclaration: <InferenceRule2>, ...})`, see (L)OX for some examples
- Thanks to the new chaining API for defining types (see corresponding breaking changes below), they can be annotated in TypeScript-type-safe way with multiple inference rules for the same purpose.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unclear to me at this point, what "for the same purpose" refers to (the point before)?

- Typir-Langium: new API to register inference rules to the `$type` of the `AstNode` to validate,
e.g. `addInferenceRulesForAstNodes({ MemberCall: <InferenceRule1>, VariableDeclaration: <InferenceRule2>, ...})`, see (L)OX for some examples
- Thanks to the new chaining API for defining types (see corresponding breaking changes below), they can be annotated in TypeScript-type-safe way with multiple inference rules for the same purpose.
- Provide new `expectValidationHints()` utility for developers to ease the writing of test cases for Typir-based type systems.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering about the naming 'Hints' a bit. Should we maybe stick closer to the langium terminology?


- Clear the cache for inferred types, when an inference rule is removed.
- Remove removed functions from its internal storage in `FunctionKind`.
- Update the returned function type during a performance optimization, when adding or removing some signatures of overloaded functions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear to me while just reading the Changelog.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 42

(node: unknown) => isTypeReference(node) && node.primitive === 'boolean'
]});
const typeBool = this.typir.factory.Primitives.create({ primitiveName: 'boolean' })
.inferenceRule({ languageKey: BooleanLiteral })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this new api in general, it also improves readability imho

// .inferenceRule({ filter: isBooleanLiteral }) // this is the alternative solution
.inferenceRule({ languageKey: TypeReference, matching: (node: TypeReference) => node.primitive === 'boolean' }) // this is the more performant notation
// .inferenceRule({ filter: isTypeReference, matching: node => node.primitive === 'boolean' }) // this is the "easier" notation
.finish();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: always remember to finish. This is a little downside, but fine for me.

* in particular, to support overloaded functions.
* In each type system, exactly one instance of this class is stored by the FunctionKind.
*/
// TODO review: better name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like a name that shows that it handles more that one function and describes better, what it does, like AvailableFunctionsManager. That would also hint to the overload tasks.


beforeValidation(_languageRoot: LanguageType, _accept: ValidationProblemAcceptor<LanguageType>, _typir: TypirServices<LanguageType>): void {
// do nothing
// TODO review: Here ValidationRuleStateless is enough, but since it is a function type (and no interface type), it is not possible to implement it here in this class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a new minimal interface here?

export interface ValidationMessageDetails {
languageNode: unknown;
export interface ValidationMessageDetails<LanguageType = unknown, T extends LanguageType = LanguageType> {
languageNode: T; // TODO review: in OX/LOX, 'unknown' instead of 'AstNode' is inferred by TypeScript, why?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be investigated more deeply independently of the review, I do not know out of the box.

export interface InferCurrentTypeRule<TypeType extends Type = Type, LanguageType = unknown, T extends LanguageType = LanguageType> {
languageKey?: string | string[];
filter?: (languageNode: LanguageType) => languageNode is T;
matching?: (languageNode: T) => boolean; // TODO review: Should we provide "typeToInfer: TypeType" as an additional property here?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have reasons against this? Nothing comes to my mind spontaneously.

.inferenceRulesForFieldAccess({
filter: node => node instanceof ClassFieldAccess,
matching: node => {
const varType = typir.Inference.inferType(node.classVariable); // TODO review: doing type inference on your own here feels a bit strange
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we maybe discuss this in the next meeting? I am not sure I really understand this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants