UNPKG

typir

Version:

General purpose type checking library

292 lines 13.5 kB
/****************************************************************************** * 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