@informalsystems/quint
Version:
Core tool for the Quint specification language
186 lines • 7.18 kB
JavaScript
"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