UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

186 lines 7.18 kB
"use strict"; /** * Compute the call graph of Quint definitions. Technically, it is a "uses" * graph, as it also captures type aliases. * * @author Igor Konnov, Informal Systems, 2023 * * Copyright 2022-2023 Informal Systems * Licensed under the Apache License, Version 2.0. * See LICENSE in the project root for license information. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CallGraphVisitor = exports.mkCallGraphContext = void 0; const immutable_1 = require("immutable"); const importKeyRecordFactory = (0, immutable_1.Record)({ importingModuleId: -1n, importedModuleName: '', }); function mkImportKey(importingModuleId, importedModuleName) { return importKeyRecordFactory({ importingModuleId, importedModuleName }); } /** * Compute the context for computing the call graph. * * @param modules the modules to compute the context for */ function mkCallGraphContext(modules) { function collectImports(inMap, mod) { // add a single import to the imports map function addImport(map, importId, name) { const key = importKeyRecordFactory({ importingModuleId: mod.id, importedModuleName: name }); const value = map.get(key) ?? (0, immutable_1.Set)(); return map.set(key, value.add(importId)); } // add all imports and instances return mod.declarations.reduce((map, decl) => { if (decl.kind === 'import' || decl.kind === 'instance') { // import A // import B(N = 3) let result = addImport(map, decl.id, decl.protoName); if (decl.qualifiedName) { // import A as A1 // import B(N = 3) as B1 result = addImport(result, decl.id, decl.qualifiedName); } return result; } else { return map; } }, inMap); } function collectDefinedAt(map, mod) { return mod.declarations.reduce((map, decl) => map.set(decl.id, mod), map); } const definedAt = modules.reduce(collectDefinedAt, (0, immutable_1.Map)()); const importsByName = modules.reduce(collectImports, (0, immutable_1.Map)()); const modulesByName = modules.reduce((map, mod) => map.set(mod.name, mod), (0, immutable_1.Map)()); return { modulesByName, importsByName, definedAt }; } exports.mkCallGraphContext = mkCallGraphContext; /** * IR visitor that computes the call graph. This class accumulates the graph in * its state. If you want to compute a new graph, create a new instance. */ class CallGraphVisitor { constructor(lookupTable, context) { this.lookupTable = lookupTable; this.context = context; this._graph = (0, immutable_1.Map)(); this.stack = []; this.currentModuleId = -1n; } get graph() { return this._graph; } /** * Print the graph in the graphviz dot format. Use it for debugging purposes, * e.g., print(console.log) */ print(out) { out(`digraph {`); this._graph.forEach((succ, pred) => { succ.forEach(oneSucc => { out(` n${pred} -> n${oneSucc};`); }); }); out('}'); } enterModule(module) { this.currentModuleId = module.id; } enterDef(def) { this.stack.push(def); const hostModule = this.context.definedAt.get(def.id); if (hostModule && hostModule.id !== this.currentModuleId) { // This definition A is imported from another module B. // Hence, the definition A should appear after the statements // import A... and import A(...)... const key = mkImportKey(this.currentModuleId, hostModule.name); const imports = this.context.importsByName.get(key) ?? (0, immutable_1.Set)(); this.graphAddAll(def.id, imports); } } exitDef(_def) { this.stack.pop(); } enterImport(decl) { const importedModule = this.context.modulesByName.get(decl.protoName); if (importedModule) { this.graphAddAll(decl.id, (0, immutable_1.Set)([importedModule.id])); } } enterInstance(decl) { // Instances are the only non-definition declarations that need to be added to the stack, // because they may contain names in the overrides this.stack.push(decl); const importedModule = this.context.modulesByName.get(decl.protoName); if (importedModule) { this.graphAddAll(decl.id, (0, immutable_1.Set)([importedModule.id])); } } exitInstance(_decl) { this.stack.pop(); } enterExport(decl) { const key = mkImportKey(this.currentModuleId, decl.protoName); const imports = this.context.importsByName.get(key) ?? (0, immutable_1.Set)(); // the imports and instance of the same module must precede the export this.graphAddAll(decl.id, imports); } // e.g., called for plus inside plus(x, y) exitApp(app) { const lookupDef = this.lookupTable.get(app.id); if (lookupDef) { this.graphAddOne(lookupDef.id); this.graphAddImports(lookupDef.id); } } // e.g., called for x and y inside plus(x, y) exitName(name) { const lookupDef = this.lookupTable.get(name.id); if (lookupDef) { this.graphAddOne(lookupDef.id); this.graphAddImports(lookupDef.id); } } // e.g., called for Bar inside type Foo = Set[Bar] exitConstType(tp) { if (tp.id) { const lookupDef = this.lookupTable.get(tp.id); if (lookupDef) { this.graphAddOne(lookupDef.id); this.graphAddImports(lookupDef.id); } } } graphAddOne(usedId) { // Add the reference for every definition on the stack. // Hence, if we have nested definitions, top-level definitions // are also designated as callers of the definition. this.stack.forEach(def => { const callees = this.graph.get(def.id) ?? (0, immutable_1.Set)(); this._graph = this._graph.set(def.id, callees.add(usedId)); }); } // if the referenced operator is defined in another module // via a definition with originId, // add a dependency of occurenceId on the corresponding import graphAddImports(originId) { const hostModule = this.context.definedAt.get(originId); if (hostModule && hostModule.id !== this.currentModuleId) { const key = mkImportKey(this.currentModuleId, hostModule.name); const imports = this.context.importsByName.get(key); if (imports !== undefined) { this.stack.forEach(def => this.graphAddAll(def.id, imports)); } } } graphAddAll(defId, ids) { const callees = this.graph.get(defId) ?? (0, immutable_1.Set)(); this._graph = this._graph.set(defId, callees.union(ids)); } } exports.CallGraphVisitor = CallGraphVisitor; //# sourceMappingURL=callgraph.js.map