typir
Version:
General purpose type checking library
292 lines • 13.5 kB
JavaScript
/******************************************************************************
* Copyright 2024 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import { isType } from '../graph/type-node.js';
import { RuleRegistry } from '../utils/rule-registration.js';
import { isSpecificTypirProblem } from '../utils/utils-definitions.js';
import { assertUnreachable, removeFromArray, toArray } from '../utils/utils.js';
export const InferenceProblem = 'InferenceProblem';
export function isInferenceProblem(problem) {
return isSpecificTypirProblem(problem, InferenceProblem);
}
export const InferenceRuleNotApplicable = 'N/A'; // or 'undefined' instead?
export class DefaultTypeInferenceCollector {
constructor(services) {
this.listeners = [];
this.services = services;
this.languageNodeInference = services.caching.LanguageNodeInference;
this.ruleRegistry = new RuleRegistry(services);
this.ruleRegistry.addListener(this);
}
addInferenceRule(rule, givenOptions) {
this.ruleRegistry.addRule(rule, givenOptions);
}
removeInferenceRule(rule, optionsToRemove) {
this.ruleRegistry.removeRule(rule, optionsToRemove);
}
inferType(languageNode) {
// is the result already in the cache?
const cached = this.cacheGet(languageNode);
if (cached) {
return cached;
}
// handle recursion loops
if (this.pendingGet(languageNode)) {
throw new Error(`There is a recursion loop for inferring the type from ${languageNode}! Probably, there are multiple interfering inference rules.`);
}
this.pendingSet(languageNode);
// do the actual type inference
const result = this.inferTypeLogic(languageNode);
// the calculation is done
this.pendingClear(languageNode);
// remember the calculated type in the cache
if (isType(result)) {
this.cacheSet(languageNode, result);
}
return result;
}
checkForError(languageNode) {
if (languageNode === undefined || languageNode === null) {
throw new Error('Language node must be not undefined/null!');
}
}
inferTypeLogic(languageNode) {
this.checkForError(languageNode);
// determine all keys to check
const keysToApply = [];
const languageKey = this.services.Language.getLanguageNodeKey(languageNode);
if (languageKey === undefined) {
keysToApply.push(undefined);
}
else {
keysToApply.push(languageKey); // execute the rules which are associated to the key of the current language node
keysToApply.push(...this.services.Language.getAllSuperKeys(languageKey)); // apply all rules which are associated to super-keys
keysToApply.push(undefined); // rules associated with 'undefined' are applied to all language nodes, apply these rules at the end
}
// execute all rules wich are associated to the relevant language keys
const collectedInferenceProblems = [];
const alreadyExecutedRules = new Set();
for (const key of keysToApply) {
for (const rule of this.ruleRegistry.getRulesByLanguageKey(key)) {
if (alreadyExecutedRules.has(rule)) {
// don't execute rules multiple times, if they are associated with multiple keys (with overlapping sub-keys)
}
else {
const result = this.executeSingleInferenceRuleLogic(rule, languageNode, collectedInferenceProblems);
if (result) {
return result; // return the first inferred type, otherwise, check the next inference rules
}
alreadyExecutedRules.add(rule);
}
}
}
// return all the collected inference problems
if (collectedInferenceProblems.length <= 0) {
// document the reason, why neither a type nor inference problems are found
collectedInferenceProblems.push({
$problem: InferenceProblem,
languageNode: languageNode,
location: 'found no applicable inference rules',
subProblems: [],
});
}
return collectedInferenceProblems;
}
executeSingleInferenceRuleLogic(rule, languageNode, collectedInferenceProblems) {
if (typeof rule === 'function') {
// simple case without type inference for children
const ruleResult = rule(languageNode, this.services);
return this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems);
}
else if (typeof rule === 'object') {
// more complex case with inferring the type for children
const ruleResult = rule.inferTypeWithoutChildren(languageNode, this.services);
if (Array.isArray(ruleResult)) {
// this rule might match => continue applying this rule
// resolve the requested child types
const childLanguageNodes = ruleResult;
const actualChildTypes = childLanguageNodes.map(child => this.services.Inference.inferType(child));
// check, whether inferring the children resulted in some other inference problems
const childTypeProblems = [];
for (let i = 0; i < actualChildTypes.length; i++) {
const child = actualChildTypes[i];
if (Array.isArray(child)) {
childTypeProblems.push({
$problem: InferenceProblem,
languageNode: childLanguageNodes[i],
location: `child language node ${i}`,
rule,
subProblems: child,
});
}
}
if (childTypeProblems.length >= 1) {
collectedInferenceProblems.push({
$problem: InferenceProblem,
languageNode: languageNode,
location: 'inferring depending children',
rule,
subProblems: childTypeProblems,
});
return undefined;
}
else {
// the types of all children are successfully inferred
const finalInferenceResult = rule.inferTypeWithChildrensTypes(languageNode, actualChildTypes, this.services);
if (isType(finalInferenceResult)) {
// type is inferred!
return finalInferenceResult;
}
else {
// inference is not applicable (probably due to a mismatch of the children's types) => check the next rule
collectedInferenceProblems.push(finalInferenceResult);
return undefined;
}
}
}
else {
return this.inferTypeLogicWithoutChildren(ruleResult, collectedInferenceProblems);
}
}
else {
assertUnreachable(rule);
}
}
inferTypeLogicWithoutChildren(result, collectedInferenceProblems) {
if (result === InferenceRuleNotApplicable) {
// this rule is not applicable at all => ignore this rule
return undefined;
}
else if (isType(result)) {
// the result type is found!
return result;
}
else if (isInferenceProblem(result)) {
// found some inference problems
collectedInferenceProblems.push(result);
return undefined;
}
else {
// this 'result' language node is used instead to infer its type, which is the type for the current language node as well
const recursiveResult = this.inferType(result);
if (Array.isArray(recursiveResult)) {
collectedInferenceProblems.push(...recursiveResult);
return undefined;
}
else {
return recursiveResult;
}
}
}
addListener(listener) {
this.listeners.push(listener);
}
removeListener(listener) {
removeFromArray(listener, this.listeners);
}
// This inference collector is notified by the rule registry and forwards these notifications to its own listeners
onAddedRule(rule, diffOptions) {
// listeners of the composite will be notified about all added inner rules
this.listeners.forEach(listener => listener.onAddedInferenceRule(rule, diffOptions));
}
onRemovedRule(rule, diffOptions) {
// 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();
// listeners of the composite will be notified about all removed inner rules
this.listeners.forEach(listener => listener.onRemovedInferenceRule(rule, diffOptions));
}
/* By default, the central cache of Typir is used. */
cacheSet(languageNode, type) {
this.languageNodeInference.cacheSet(languageNode, type);
}
cacheGet(languageNode) {
return this.languageNodeInference.cacheGet(languageNode);
}
cacheClear() {
this.languageNodeInference.cacheClear();
}
pendingSet(languageNode) {
this.languageNodeInference.pendingSet(languageNode);
}
pendingClear(languageNode) {
this.languageNodeInference.pendingClear(languageNode);
}
pendingGet(languageNode) {
return this.languageNodeInference.pendingGet(languageNode);
}
}
/**
* This inference rule uses multiple internal inference rules for doing the type inference.
* If one of the child rules returns a type, this type is the result of the composite rule.
* Otherwise, all problems of all child rules are returned.
*
* This composite rule ensures itself, that it is associated to the set of language keys of the inner rules.
*/
// This design looks a bit ugly ..., but "implements TypeInferenceRuleWithoutInferringChildren" does not work, since it is a function ...
export class CompositeTypeInferenceRule extends DefaultTypeInferenceCollector {
constructor(services, collectorToRegisterThisRule) {
super(services);
this.collectorToRegisterThisRule = collectorToRegisterThisRule;
}
// do not check "pending" (again), since it is already checked by the "parent" DefaultTypeInferenceCollector!
pendingGet(_languageNode) {
return false;
}
pendingSet(_languageNode) {
// nothing to do, since the pending state is not used in this composite rule
}
pendingClear(_languageNode) {
// nothing to do, since the pending state is not used in this composite rule
}
inferTypeWithoutChildren(languageNode, _typir) {
// do the type inference
const result = this.inferType(languageNode);
if (isType(result)) {
return result;
}
else {
if (result.length <= 0) {
return InferenceRuleNotApplicable;
}
else if (result.length === 1) {
return result[0];
}
else {
return {
$problem: InferenceProblem,
languageNode: languageNode,
location: 'sub-rules for inference',
rule: this,
subProblems: result,
};
}
}
}
inferTypeWithChildrensTypes(_languageNode, _childrenTypes, _typir) {
throw new Error('This function will not be called.');
}
onAddedRule(rule, diffOptions) {
// an inner rule was added
super.onAddedRule(rule, diffOptions);
// this composite rule needs to be registered also for all the language keys of the new inner rule
this.collectorToRegisterThisRule.addInferenceRule(this, Object.assign(Object.assign({}, diffOptions), { boundToType: undefined }));
}
onRemovedRule(rule, diffOptions) {
// an inner rule was removed
super.onRemovedRule(rule, diffOptions);
// remove this composite rule for all language keys for which no inner rules are registered anymore
if (diffOptions.languageKey === undefined) {
if (this.ruleRegistry.getRulesByLanguageKey(undefined).length <= 0) {
this.collectorToRegisterThisRule.removeInferenceRule(this, Object.assign(Object.assign({}, diffOptions), { languageKey: undefined, boundToType: undefined }));
}
}
else {
const languageKeysToUnregister = toArray(diffOptions.languageKey).filter(key => this.ruleRegistry.getRulesByLanguageKey(key).length <= 0);
this.collectorToRegisterThisRule.removeInferenceRule(this, Object.assign(Object.assign({}, diffOptions), { languageKey: languageKeysToUnregister, boundToType: undefined }));
}
}
}
//# sourceMappingURL=inference.js.map