diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c6e1b9..f73892a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Add Callgraph: PR [#185](https://github.com/nowarp/misti/pull/185) - `EtaLikeSimplifications` detector: PR [#198](https://github.com/nowarp/misti/pull/198) - `ShortCircuitCondition` detector: PR [#202](https://github.com/nowarp/misti/pull/202) ### Changed diff --git a/cspell.json b/cspell.json index 763e883a..78552d0c 100644 --- a/cspell.json +++ b/cspell.json @@ -101,7 +101,8 @@ "Dont", "consteval", "Georgiy", - "Komarov" + "Komarov", + "callgraph" ], "flagWords": [], "ignorePaths": [ diff --git a/src/internals/ir/builders/ir.ts b/src/internals/ir/builders/ir.ts index ff6ea5c3..c0a338dd 100644 --- a/src/internals/ir/builders/ir.ts +++ b/src/internals/ir/builders/ir.ts @@ -13,12 +13,14 @@ import { FunctionName, ImportGraph, ProjectName, + TactASTStore, } from ".."; -import { TactASTStoreBuilder } from "./astStore"; import { MistiContext } from "../../context"; import { InternalException } from "../../exceptions"; import { formatPosition } from "../../tact"; import { unreachable } from "../../util"; +import { CallGraph } from "../callGraph"; +import { TactASTStoreBuilder } from "./astStore"; import { AstContractDeclaration, AstExpression, @@ -53,6 +55,17 @@ function generateReceiveName(receive: AstReceiver): string { } } +export class TactCallGraphBuilder { + /** + * Builds a CallGraph for the provided AST Store. + * @param astStore The TactASTStore containing AST nodes and function definitions. + * @returns A CallGraph instance built from the AST nodes. + */ + static make(ctx: MistiContext, astStore: TactASTStore): CallGraph { + const callGraph = new CallGraph(ctx); + return callGraph.build(astStore); + } +} /** * Represents a stateful object which is responsible for constructing the IR of a Tact project. * @@ -98,12 +111,15 @@ export class TactIRBuilder { build(): CompilationUnit { const functions = this.createFunctions(); const contracts = this.createContracts(); + const tactASTStore = TactASTStoreBuilder.make(this.ctx, this.ast).build(); + const callGraph = TactCallGraphBuilder.make(this.ctx, tactASTStore); return new CompilationUnit( this.projectName, TactASTStoreBuilder.make(this.ctx, this.ast).build(), this.imports, functions, contracts, + callGraph, ); } diff --git a/src/internals/ir/callGraph.ts b/src/internals/ir/callGraph.ts new file mode 100644 index 00000000..623fe741 --- /dev/null +++ b/src/internals/ir/callGraph.ts @@ -0,0 +1,249 @@ +import { unreachable } from "../util"; +import { TactASTStore } from "./astStore"; +import { IdxGenerator } from "./indices"; +import { MistiContext } from "../../"; +import { Logger } from "../../internals/logger"; +import { forEachExpression } from "../tact/iterators"; +import { + AstFunctionDef, + AstReceiver, + AstContractInit, + AstExpression, + AstMethodCall, + AstStaticCall, +} from "@tact-lang/compiler/dist/grammar/ast"; + +type CGNodeId = number & { readonly brand: unique symbol }; +type CGEdgeId = number & { readonly brand: unique symbol }; + +/** + * Represents an edge in the call graph, indicating a call from one function to another. + */ +class CGEdge { + public idx: CGEdgeId; + + constructor( + public src: CGNodeId, + public dst: CGNodeId, + ) { + this.idx = IdxGenerator.next("cg_edge") as CGEdgeId; + } +} + +/** + * Represents a node in the call graph, corresponding to a function or method. + */ +class CGNode { + public idx: CGNodeId; + public inEdges: Set = new Set(); + public outEdges: Set = new Set(); + + /** + * @param astId AST id of the relevant function definition. It might be `undefined` if this node doesn’t have a corresponding AST entry, + * which indicates an issue in Misti. + */ + constructor( + public astId: number | undefined, + public name: string, + private logger: Logger, + ) { + this.idx = IdxGenerator.next("cg_node") as CGNodeId; + if (astId === undefined) { + this.logger.debug(`CGNode created without AST ID for function "${name}"`); + } + } +} + +/** + * The `CallGraph` class represents a directed graph where nodes correspond to functions + * or methods in a program, and edges indicate calls between them. + * + * Nodes and edges are uniquely identified using indices generated by `IdxGenerator`. + * This class provides methods to construct the graph from AST data, retrieve nodes and edges, + * and check connectivity between nodes. + */ +export class CallGraph { + private nodeMap: Map = new Map(); + private edgesMap: Map = new Map(); + private nameToNodeId: Map = new Map(); + private logger: Logger; + + constructor(private ctx: MistiContext) { + this.logger = ctx.logger; + } + + public getNodes(): Map { + return this.nodeMap; + } + + public getEdges(): Map { + return this.edgesMap; + } + + /** + * Builds the call graph based on functions in the provided AST store. + * @param astStore - The AST store containing functions to be added to the graph. + * @returns The constructed `CallGraph`. + */ + public build(astStore: TactASTStore): CallGraph { + for (const func of astStore.getFunctions()) { + const funcName = this.getFunctionName(func); + if (funcName) { + const node = new CGNode(func.id, funcName, this.logger); + this.nodeMap.set(node.idx, node); + this.nameToNodeId.set(funcName, node.idx); + } else { + this.logger.error( + `Function with id ${func.id} has no name and will be skipped.`, + ); + } + } + this.analyzeFunctionCalls(astStore); + return this; + } + + /** + * Determines if there exists a path in the call graph from the source node to the destination node. + * This method performs a breadth-first search to find if the destination node is reachable from the source node. + * + * @param src The ID of the source node to start the search from + * @param dst The ID of the destination node to search for + * @returns true if there exists a path from src to dst in the call graph, false otherwise + * Returns false if either src or dst node IDs are not found in the graph + */ + public areConnected(src: CGNodeId, dst: CGNodeId): boolean { + const srcNode = this.nodeMap.get(src); + const dstNode = this.nodeMap.get(dst); + if (!srcNode || !dstNode) { + return false; + } + const queue: CGNodeId[] = [src]; + const visited = new Set([src]); + while (queue.length > 0) { + const current = queue.shift()!; + if (current === dst) { + return true; + } + const currentNode = this.nodeMap.get(current); + if (currentNode) { + for (const edgeId of currentNode.outEdges) { + const edge = this.edgesMap.get(edgeId); + if (edge && !visited.has(edge.dst)) { + visited.add(edge.dst); + queue.push(edge.dst); + } + } + } + } + return false; + } + + /** + * Analyzes function calls in the AST store and adds corresponding edges in the call graph. + * @param astStore The AST store to analyze for function calls. + */ + private analyzeFunctionCalls(astStore: TactASTStore) { + for (const func of astStore.getFunctions()) { + const funcName = this.getFunctionName(func); + if (funcName) { + const callerId = this.nameToNodeId.get(funcName); + if (callerId !== undefined) { + forEachExpression(func, (expr) => + this.processExpression(expr, callerId), + ); + } else { + this.logger.warn( + `Caller function ${funcName} not found in node map.`, + ); + } + } + } + } + + /** + * Extracts the function name based on its type. + * @param func The function definition, receiver, or contract initializer. + * @returns The function name if available; otherwise, `undefined`. + */ + private getFunctionName( + func: AstFunctionDef | AstReceiver | AstContractInit, + ): string | undefined { + switch (func.kind) { + case "function_def": + return func.name?.text; + case "contract_init": + return `contract_init_${func.id}`; + case "receiver": + return `receiver_${func.id}`; + default: + unreachable(func); + } + } + + /** + * Processes an expression, identifying static and method calls to add edges. + * @param expr The AST expression to process. + * @param callerId The ID of the calling node. + */ + private processExpression(expr: AstExpression, callerId: CGNodeId) { + if (expr.kind === "static_call") { + const staticCall = expr as AstStaticCall; + const functionName = staticCall.function?.text; + if (functionName) { + const calleeId = this.findOrAddFunction(functionName); + this.addEdge(callerId, calleeId); + } else { + this.logger.warn( + `Static call expression missing function name at caller ${callerId}`, + ); + } + } else if (expr.kind === "method_call") { + const methodCall = expr as AstMethodCall; + const methodName = methodCall.method?.text; + if (methodName) { + const calleeId = this.findOrAddFunction(methodName); + this.addEdge(callerId, calleeId); + } else { + this.logger.warn( + `Method call expression missing method name at caller ${callerId}`, + ); + } + } + } + + /** + * Finds or adds a function to the call graph by name. + * @param name The name of the function to find or add. + * @returns The ID of the found or added function node. + */ + private findOrAddFunction(name: string): CGNodeId { + const nodeId = this.nameToNodeId.get(name); + if (nodeId !== undefined) { + return nodeId; + } + const newNode = new CGNode(undefined, name, this.logger); + this.nodeMap.set(newNode.idx, newNode); + this.nameToNodeId.set(name, newNode.idx); + return newNode.idx; + } + + /** + * Adds an edge between two nodes in the call graph. + * @param src The source node ID. + * @param dst The destination node ID. + */ + private addEdge(src: CGNodeId, dst: CGNodeId) { + const srcNode = this.nodeMap.get(src); + const dstNode = this.nodeMap.get(dst); + if (srcNode && dstNode) { + const edge = new CGEdge(src, dst); + this.edgesMap.set(edge.idx, edge); + srcNode.outEdges.add(edge.idx); + dstNode.inEdges.add(edge.idx); + } else { + this.logger.warn( + `Cannot add edge from ${src} to ${dst}: node(s) not found.`, + ); + } + } +} diff --git a/src/internals/ir/ir.ts b/src/internals/ir/ir.ts index 051ce31a..273a314d 100644 --- a/src/internals/ir/ir.ts +++ b/src/internals/ir/ir.ts @@ -5,6 +5,7 @@ */ import { CFGIdx, ContractName, FunctionName, ProjectName } from "."; import { TactASTStore } from "./astStore"; +import { CallGraph } from "./callGraph"; import { BasicBlock, CFG } from "./cfg"; import { ImportGraph } from "./imports"; import { IdxGenerator } from "./indices"; @@ -31,6 +32,7 @@ export class CompilationUnit { public imports: ImportGraph, public functions: Map, public contracts: Map, + public callGraph: CallGraph, ) {} /** diff --git a/src/tools/dumpCallgraph.ts b/src/tools/dumpCallgraph.ts new file mode 100644 index 00000000..bf3fd98a --- /dev/null +++ b/src/tools/dumpCallgraph.ts @@ -0,0 +1,129 @@ +import { Tool } from "./tool"; +import { ToolOutput } from "../cli/result"; +import { MistiContext } from "../internals/context"; +import { CompilationUnit } from "../internals/ir"; +import { CallGraph } from "../internals/ir/callGraph"; +import { unreachable } from "../internals/util"; +import JSONbig from "json-bigint"; + +interface DumpCallGraphOptions extends Record { + format: "dot" | "json" | "mmd"; +} + +/** + * A tool that dumps the Call Graph (CG) of the given compilation unit in the specified format. + */ +export class DumpCallGraph extends Tool { + constructor(ctx: MistiContext, options: DumpCallGraphOptions) { + super(ctx, options); + } + + public get defaultOptions(): DumpCallGraphOptions { + return { + format: "dot", + }; + } + + /** + * Executes `DumpCallGraph` tool. + * @param cu `CompilationUnit` representing the code to analyze. + * @returns A `ToolOutput` containing the generated call graph data. + */ + public run(cu: CompilationUnit): ToolOutput | never { + const callGraph = cu.callGraph; + const format = this.options.format; + + switch (format) { + case "dot": + return this.makeOutput(cu, GraphvizDumper.dumpCallGraph(callGraph)); + case "mmd": + return this.makeOutput(cu, MermaidDumper.dumpCallGraph(callGraph)); + case "json": + return this.makeOutput(cu, JSONDumper.dumpCallGraph(callGraph)); + default: + throw unreachable(format); + } + } + + public getDescription(): string { + return "Dumps the Call Graph (CG) in the selected format: DOT, Mermaid, or JSON."; + } + + public getOptionDescriptions(): Record { + return { + format: "The output format for the call graph dump: .", + }; + } +} + +/** + * Utility class to dump the call graph in Mermaid format. + */ +class MermaidDumper { + public static dumpCallGraph(callGraph: CallGraph): string { + if (!callGraph || callGraph.getNodes().size === 0) { + return 'graph TD\n empty["Empty Call Graph"]'; + } + let diagram = "graph TD\n"; + callGraph.getNodes().forEach((node) => { + const nodeId = `node_${node.idx}`; + const label = node.name?.replace(/"/g, '\\"') || "Unknown"; + diagram += ` ${nodeId}["${label}"]\n`; + }); + callGraph.getEdges().forEach((edge) => { + const srcId = `node_${edge.src}`; + const dstId = `node_${edge.dst}`; + diagram += ` ${srcId} --> ${dstId}\n`; + }); + return diagram; + } +} + +/** + * Utility class to dump the call graph in DOT (Graphviz) format. + */ +class GraphvizDumper { + public static dumpCallGraph(callGraph: CallGraph): string { + if (!callGraph || callGraph.getNodes().size === 0) { + return 'digraph "CallGraph" {\n node [shape=box];\n empty [label="Empty Call Graph"];\n}\n'; + } + let dot = `digraph "CallGraph" {\n node [shape=box];\n`; + callGraph.getNodes().forEach((node) => { + const nodeId = `node_${node.idx}`; + const label = node.name?.replace(/"/g, '\\"') || "Unknown"; + dot += ` ${nodeId} [label="${label}"];\n`; + }); + callGraph.getEdges().forEach((edge) => { + const srcId = `node_${edge.src}`; + const dstId = `node_${edge.dst}`; + dot += ` ${srcId} -> ${dstId};\n`; + }); + dot += `}\n`; + return dot; + } +} + +/** + * Utility class to dump the call graph in JSON format. + */ +class JSONDumper { + public static dumpCallGraph(callGraph: CallGraph): string { + if (!callGraph) { + return JSONbig.stringify({ nodes: [], edges: [] }, null, 2); + } + const data = { + nodes: Array.from(callGraph.getNodes().values()).map((node) => ({ + idx: node.idx, + name: node.name || "Unknown", + inEdges: Array.from(node.inEdges || []), + outEdges: Array.from(node.outEdges || []), + })), + edges: Array.from(callGraph.getEdges().values()).map((edge) => ({ + idx: edge.idx, + src: edge.src, + dst: edge.dst, + })), + }; + return JSONbig.stringify(data, null, 2); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index ef398ffd..cc04f5fe 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,4 +1,5 @@ export { DumpAst } from "./dumpAst"; export { DumpCfg } from "./dumpCfg"; export { DumpConfig } from "./dumpConfig"; +export { DumpCallGraph } from "./dumpCallgraph"; export * from "./tool"; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 0631f7ac..d255b60e 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -100,6 +100,12 @@ const BuiltInTools: Record> = { (module) => new module.DumpImports(ctx, options), ), }, + DumpCallGraph: { + loader: (ctx: MistiContext, options: any) => + import("./dumpCallgraph").then( + (module) => new module.DumpCallGraph(ctx, options), + ), + }, }; /** diff --git a/test/all/sample-jetton.expected.callgraph.dot b/test/all/sample-jetton.expected.callgraph.dot new file mode 100644 index 00000000..681dce12 --- /dev/null +++ b/test/all/sample-jetton.expected.callgraph.dot @@ -0,0 +1,120 @@ +digraph "CallGraph" { + node [shape=box]; + node_1 [label="reply"]; + node_2 [label="notify"]; + node_3 [label="forward"]; + node_4 [label="requireOwner"]; + node_5 [label="owner"]; + node_6 [label="receiver_1734"]; + node_7 [label="contract_init_1817"]; + node_8 [label="receiver_1857"]; + node_9 [label="receiver_1882"]; + node_10 [label="receiver_1905"]; + node_11 [label="receiver_1939"]; + node_12 [label="receiver_2000"]; + node_13 [label="mint"]; + node_14 [label="requireWallet"]; + node_15 [label="getJettonWalletInit"]; + node_16 [label="get_jetton_data"]; + node_17 [label="get_wallet_address"]; + node_18 [label="contract_init_2359"]; + node_19 [label="receiver_2517"]; + node_20 [label="receiver_2687"]; + node_21 [label="msgValue"]; + node_22 [label="receiver_2832"]; + node_23 [label="receiver_2876"]; + node_24 [label="get_wallet_data"]; + node_25 [label="sender"]; + node_26 [label="context"]; + node_27 [label="myBalance"]; + node_28 [label="nativeReserve"]; + node_29 [label="send"]; + node_30 [label="nativeThrowUnless"]; + node_31 [label="toCell"]; + node_32 [label="require"]; + node_33 [label="contractAddress"]; + node_34 [label="myAddress"]; + node_35 [label="emptySlice"]; + node_36 [label="readForwardFee"]; + node_37 [label="min"]; + node_38 [label="ton"]; + node_39 [label="loadUint"]; + node_40 [label="loadCoins"]; + node_1 -> node_3; + node_1 -> node_25; + node_2 -> node_3; + node_2 -> node_25; + node_3 -> node_26; + node_3 -> node_27; + node_3 -> node_28; + node_3 -> node_29; + node_3 -> node_29; + node_4 -> node_30; + node_4 -> node_25; + node_6 -> node_4; + node_6 -> node_1; + node_6 -> node_31; + node_8 -> node_26; + node_8 -> node_32; + node_8 -> node_32; + node_8 -> node_13; + node_9 -> node_26; + node_9 -> node_32; + node_9 -> node_13; + node_10 -> node_26; + node_10 -> node_32; + node_11 -> node_4; + node_12 -> node_14; + node_12 -> node_29; + node_12 -> node_31; + node_13 -> node_32; + node_13 -> node_15; + node_13 -> node_29; + node_13 -> node_33; + node_13 -> node_31; + node_13 -> node_34; + node_13 -> node_35; + node_14 -> node_26; + node_14 -> node_15; + node_14 -> node_32; + node_14 -> node_33; + node_15 -> node_34; + node_16 -> node_15; + node_16 -> node_34; + node_17 -> node_15; + node_17 -> node_33; + node_19 -> node_26; + node_19 -> node_32; + node_19 -> node_36; + node_19 -> node_36; + node_19 -> node_32; + node_19 -> node_37; + node_19 -> node_38; + node_19 -> node_32; + node_19 -> node_33; + node_19 -> node_29; + node_19 -> node_31; + node_20 -> node_26; + node_20 -> node_32; + node_20 -> node_33; + node_20 -> node_32; + node_20 -> node_29; + node_20 -> node_31; + node_20 -> node_21; + node_20 -> node_36; + node_20 -> node_29; + node_20 -> node_31; + node_21 -> node_27; + node_21 -> node_37; + node_22 -> node_26; + node_22 -> node_32; + node_22 -> node_32; + node_22 -> node_36; + node_22 -> node_32; + node_22 -> node_29; + node_22 -> node_31; + node_23 -> node_39; + node_23 -> node_39; + node_23 -> node_40; + node_23 -> node_32; +} diff --git a/test/all/sample-jetton.expected.callgraph.json b/test/all/sample-jetton.expected.callgraph.json new file mode 100644 index 00000000..9828ca1d --- /dev/null +++ b/test/all/sample-jetton.expected.callgraph.json @@ -0,0 +1,828 @@ +{ + "nodes": [ + { + "idx": 1, + "name": "reply", + "inEdges": [ + 13 + ], + "outEdges": [ + 1, + 2 + ] + }, + { + "idx": 2, + "name": "notify", + "inEdges": [], + "outEdges": [ + 3, + 4 + ] + }, + { + "idx": 3, + "name": "forward", + "inEdges": [ + 1, + 3 + ], + "outEdges": [ + 5, + 6, + 7, + 8, + 9 + ] + }, + { + "idx": 4, + "name": "requireOwner", + "inEdges": [ + 12, + 24 + ], + "outEdges": [ + 10, + 11 + ] + }, + { + "idx": 5, + "name": "owner", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 6, + "name": "receiver_1734", + "inEdges": [], + "outEdges": [ + 12, + 13, + 14 + ] + }, + { + "idx": 7, + "name": "contract_init_1817", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 8, + "name": "receiver_1857", + "inEdges": [], + "outEdges": [ + 15, + 16, + 17, + 18 + ] + }, + { + "idx": 9, + "name": "receiver_1882", + "inEdges": [], + "outEdges": [ + 19, + 20, + 21 + ] + }, + { + "idx": 10, + "name": "receiver_1905", + "inEdges": [], + "outEdges": [ + 22, + 23 + ] + }, + { + "idx": 11, + "name": "receiver_1939", + "inEdges": [], + "outEdges": [ + 24 + ] + }, + { + "idx": 12, + "name": "receiver_2000", + "inEdges": [], + "outEdges": [ + 25, + 26, + 27 + ] + }, + { + "idx": 13, + "name": "mint", + "inEdges": [ + 18, + 21 + ], + "outEdges": [ + 28, + 29, + 30, + 31, + 32, + 33, + 34 + ] + }, + { + "idx": 14, + "name": "requireWallet", + "inEdges": [ + 25 + ], + "outEdges": [ + 35, + 36, + 37, + 38 + ] + }, + { + "idx": 15, + "name": "getJettonWalletInit", + "inEdges": [ + 29, + 36, + 40, + 42 + ], + "outEdges": [ + 39 + ] + }, + { + "idx": 16, + "name": "get_jetton_data", + "inEdges": [], + "outEdges": [ + 40, + 41 + ] + }, + { + "idx": 17, + "name": "get_wallet_address", + "inEdges": [], + "outEdges": [ + 42, + 43 + ] + }, + { + "idx": 18, + "name": "contract_init_2359", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 19, + "name": "receiver_2517", + "inEdges": [], + "outEdges": [ + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54 + ] + }, + { + "idx": 20, + "name": "receiver_2687", + "inEdges": [], + "outEdges": [ + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64 + ] + }, + { + "idx": 21, + "name": "msgValue", + "inEdges": [ + 61 + ], + "outEdges": [ + 65, + 66 + ] + }, + { + "idx": 22, + "name": "receiver_2832", + "inEdges": [], + "outEdges": [ + 67, + 68, + 69, + 70, + 71, + 72, + 73 + ] + }, + { + "idx": 23, + "name": "receiver_2876", + "inEdges": [], + "outEdges": [ + 74, + 75, + 76, + 77 + ] + }, + { + "idx": 24, + "name": "get_wallet_data", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 25, + "name": "sender", + "inEdges": [ + 2, + 4, + 11 + ], + "outEdges": [] + }, + { + "idx": 26, + "name": "context", + "inEdges": [ + 5, + 15, + 19, + 22, + 35, + 44, + 55, + 67 + ], + "outEdges": [] + }, + { + "idx": 27, + "name": "myBalance", + "inEdges": [ + 6, + 65 + ], + "outEdges": [] + }, + { + "idx": 28, + "name": "nativeReserve", + "inEdges": [ + 7 + ], + "outEdges": [] + }, + { + "idx": 29, + "name": "send", + "inEdges": [ + 8, + 9, + 26, + 30, + 53, + 59, + 63, + 72 + ], + "outEdges": [] + }, + { + "idx": 30, + "name": "nativeThrowUnless", + "inEdges": [ + 10 + ], + "outEdges": [] + }, + { + "idx": 31, + "name": "toCell", + "inEdges": [ + 14, + 27, + 32, + 54, + 60, + 64, + 73 + ], + "outEdges": [] + }, + { + "idx": 32, + "name": "require", + "inEdges": [ + 16, + 17, + 20, + 23, + 28, + 37, + 45, + 48, + 51, + 56, + 58, + 68, + 69, + 71, + 77 + ], + "outEdges": [] + }, + { + "idx": 33, + "name": "contractAddress", + "inEdges": [ + 31, + 38, + 43, + 52, + 57 + ], + "outEdges": [] + }, + { + "idx": 34, + "name": "myAddress", + "inEdges": [ + 33, + 39, + 41 + ], + "outEdges": [] + }, + { + "idx": 35, + "name": "emptySlice", + "inEdges": [ + 34 + ], + "outEdges": [] + }, + { + "idx": 36, + "name": "readForwardFee", + "inEdges": [ + 46, + 47, + 62, + 70 + ], + "outEdges": [] + }, + { + "idx": 37, + "name": "min", + "inEdges": [ + 49, + 66 + ], + "outEdges": [] + }, + { + "idx": 38, + "name": "ton", + "inEdges": [ + 50 + ], + "outEdges": [] + }, + { + "idx": 39, + "name": "loadUint", + "inEdges": [ + 74, + 75 + ], + "outEdges": [] + }, + { + "idx": 40, + "name": "loadCoins", + "inEdges": [ + 76 + ], + "outEdges": [] + } + ], + "edges": [ + { + "idx": 1, + "src": 1, + "dst": 3 + }, + { + "idx": 2, + "src": 1, + "dst": 25 + }, + { + "idx": 3, + "src": 2, + "dst": 3 + }, + { + "idx": 4, + "src": 2, + "dst": 25 + }, + { + "idx": 5, + "src": 3, + "dst": 26 + }, + { + "idx": 6, + "src": 3, + "dst": 27 + }, + { + "idx": 7, + "src": 3, + "dst": 28 + }, + { + "idx": 8, + "src": 3, + "dst": 29 + }, + { + "idx": 9, + "src": 3, + "dst": 29 + }, + { + "idx": 10, + "src": 4, + "dst": 30 + }, + { + "idx": 11, + "src": 4, + "dst": 25 + }, + { + "idx": 12, + "src": 6, + "dst": 4 + }, + { + "idx": 13, + "src": 6, + "dst": 1 + }, + { + "idx": 14, + "src": 6, + "dst": 31 + }, + { + "idx": 15, + "src": 8, + "dst": 26 + }, + { + "idx": 16, + "src": 8, + "dst": 32 + }, + { + "idx": 17, + "src": 8, + "dst": 32 + }, + { + "idx": 18, + "src": 8, + "dst": 13 + }, + { + "idx": 19, + "src": 9, + "dst": 26 + }, + { + "idx": 20, + "src": 9, + "dst": 32 + }, + { + "idx": 21, + "src": 9, + "dst": 13 + }, + { + "idx": 22, + "src": 10, + "dst": 26 + }, + { + "idx": 23, + "src": 10, + "dst": 32 + }, + { + "idx": 24, + "src": 11, + "dst": 4 + }, + { + "idx": 25, + "src": 12, + "dst": 14 + }, + { + "idx": 26, + "src": 12, + "dst": 29 + }, + { + "idx": 27, + "src": 12, + "dst": 31 + }, + { + "idx": 28, + "src": 13, + "dst": 32 + }, + { + "idx": 29, + "src": 13, + "dst": 15 + }, + { + "idx": 30, + "src": 13, + "dst": 29 + }, + { + "idx": 31, + "src": 13, + "dst": 33 + }, + { + "idx": 32, + "src": 13, + "dst": 31 + }, + { + "idx": 33, + "src": 13, + "dst": 34 + }, + { + "idx": 34, + "src": 13, + "dst": 35 + }, + { + "idx": 35, + "src": 14, + "dst": 26 + }, + { + "idx": 36, + "src": 14, + "dst": 15 + }, + { + "idx": 37, + "src": 14, + "dst": 32 + }, + { + "idx": 38, + "src": 14, + "dst": 33 + }, + { + "idx": 39, + "src": 15, + "dst": 34 + }, + { + "idx": 40, + "src": 16, + "dst": 15 + }, + { + "idx": 41, + "src": 16, + "dst": 34 + }, + { + "idx": 42, + "src": 17, + "dst": 15 + }, + { + "idx": 43, + "src": 17, + "dst": 33 + }, + { + "idx": 44, + "src": 19, + "dst": 26 + }, + { + "idx": 45, + "src": 19, + "dst": 32 + }, + { + "idx": 46, + "src": 19, + "dst": 36 + }, + { + "idx": 47, + "src": 19, + "dst": 36 + }, + { + "idx": 48, + "src": 19, + "dst": 32 + }, + { + "idx": 49, + "src": 19, + "dst": 37 + }, + { + "idx": 50, + "src": 19, + "dst": 38 + }, + { + "idx": 51, + "src": 19, + "dst": 32 + }, + { + "idx": 52, + "src": 19, + "dst": 33 + }, + { + "idx": 53, + "src": 19, + "dst": 29 + }, + { + "idx": 54, + "src": 19, + "dst": 31 + }, + { + "idx": 55, + "src": 20, + "dst": 26 + }, + { + "idx": 56, + "src": 20, + "dst": 32 + }, + { + "idx": 57, + "src": 20, + "dst": 33 + }, + { + "idx": 58, + "src": 20, + "dst": 32 + }, + { + "idx": 59, + "src": 20, + "dst": 29 + }, + { + "idx": 60, + "src": 20, + "dst": 31 + }, + { + "idx": 61, + "src": 20, + "dst": 21 + }, + { + "idx": 62, + "src": 20, + "dst": 36 + }, + { + "idx": 63, + "src": 20, + "dst": 29 + }, + { + "idx": 64, + "src": 20, + "dst": 31 + }, + { + "idx": 65, + "src": 21, + "dst": 27 + }, + { + "idx": 66, + "src": 21, + "dst": 37 + }, + { + "idx": 67, + "src": 22, + "dst": 26 + }, + { + "idx": 68, + "src": 22, + "dst": 32 + }, + { + "idx": 69, + "src": 22, + "dst": 32 + }, + { + "idx": 70, + "src": 22, + "dst": 36 + }, + { + "idx": 71, + "src": 22, + "dst": 32 + }, + { + "idx": 72, + "src": 22, + "dst": 29 + }, + { + "idx": 73, + "src": 22, + "dst": 31 + }, + { + "idx": 74, + "src": 23, + "dst": 39 + }, + { + "idx": 75, + "src": 23, + "dst": 39 + }, + { + "idx": 76, + "src": 23, + "dst": 40 + }, + { + "idx": 77, + "src": 23, + "dst": 32 + } + ] +} \ No newline at end of file diff --git a/test/all/sample-jetton.expected.callgraph.mmd b/test/all/sample-jetton.expected.callgraph.mmd new file mode 100644 index 00000000..aec3dc9d --- /dev/null +++ b/test/all/sample-jetton.expected.callgraph.mmd @@ -0,0 +1,118 @@ +graph TD + node_1["reply"] + node_2["notify"] + node_3["forward"] + node_4["requireOwner"] + node_5["owner"] + node_6["receiver_1734"] + node_7["contract_init_1817"] + node_8["receiver_1857"] + node_9["receiver_1882"] + node_10["receiver_1905"] + node_11["receiver_1939"] + node_12["receiver_2000"] + node_13["mint"] + node_14["requireWallet"] + node_15["getJettonWalletInit"] + node_16["get_jetton_data"] + node_17["get_wallet_address"] + node_18["contract_init_2359"] + node_19["receiver_2517"] + node_20["receiver_2687"] + node_21["msgValue"] + node_22["receiver_2832"] + node_23["receiver_2876"] + node_24["get_wallet_data"] + node_25["sender"] + node_26["context"] + node_27["myBalance"] + node_28["nativeReserve"] + node_29["send"] + node_30["nativeThrowUnless"] + node_31["toCell"] + node_32["require"] + node_33["contractAddress"] + node_34["myAddress"] + node_35["emptySlice"] + node_36["readForwardFee"] + node_37["min"] + node_38["ton"] + node_39["loadUint"] + node_40["loadCoins"] + node_1 --> node_3 + node_1 --> node_25 + node_2 --> node_3 + node_2 --> node_25 + node_3 --> node_26 + node_3 --> node_27 + node_3 --> node_28 + node_3 --> node_29 + node_3 --> node_29 + node_4 --> node_30 + node_4 --> node_25 + node_6 --> node_4 + node_6 --> node_1 + node_6 --> node_31 + node_8 --> node_26 + node_8 --> node_32 + node_8 --> node_32 + node_8 --> node_13 + node_9 --> node_26 + node_9 --> node_32 + node_9 --> node_13 + node_10 --> node_26 + node_10 --> node_32 + node_11 --> node_4 + node_12 --> node_14 + node_12 --> node_29 + node_12 --> node_31 + node_13 --> node_32 + node_13 --> node_15 + node_13 --> node_29 + node_13 --> node_33 + node_13 --> node_31 + node_13 --> node_34 + node_13 --> node_35 + node_14 --> node_26 + node_14 --> node_15 + node_14 --> node_32 + node_14 --> node_33 + node_15 --> node_34 + node_16 --> node_15 + node_16 --> node_34 + node_17 --> node_15 + node_17 --> node_33 + node_19 --> node_26 + node_19 --> node_32 + node_19 --> node_36 + node_19 --> node_36 + node_19 --> node_32 + node_19 --> node_37 + node_19 --> node_38 + node_19 --> node_32 + node_19 --> node_33 + node_19 --> node_29 + node_19 --> node_31 + node_20 --> node_26 + node_20 --> node_32 + node_20 --> node_33 + node_20 --> node_32 + node_20 --> node_29 + node_20 --> node_31 + node_20 --> node_21 + node_20 --> node_36 + node_20 --> node_29 + node_20 --> node_31 + node_21 --> node_27 + node_21 --> node_37 + node_22 --> node_26 + node_22 --> node_32 + node_22 --> node_32 + node_22 --> node_36 + node_22 --> node_32 + node_22 --> node_29 + node_22 --> node_31 + node_23 --> node_39 + node_23 --> node_39 + node_23 --> node_40 + node_23 --> node_32 diff --git a/test/all/syntax.expected.callgraph.dot b/test/all/syntax.expected.callgraph.dot new file mode 100644 index 00000000..7f7f6bac --- /dev/null +++ b/test/all/syntax.expected.callgraph.dot @@ -0,0 +1,33 @@ +digraph "CallGraph" { + node [shape=box]; + node_1 [label="test_try"]; + node_2 [label="test_loops"]; + node_3 [label="reply"]; + node_4 [label="notify"]; + node_5 [label="forward"]; + node_6 [label="getter"]; + node_7 [label="getter"]; + node_8 [label="test"]; + node_9 [label="getA"]; + node_10 [label="test"]; + node_11 [label="receiver_1722"]; + node_12 [label="dump"]; + node_13 [label="emptyMap"]; + node_14 [label="sender"]; + node_15 [label="context"]; + node_16 [label="myBalance"]; + node_17 [label="nativeReserve"]; + node_18 [label="send"]; + node_1 -> node_12; + node_2 -> node_13; + node_3 -> node_5; + node_3 -> node_14; + node_4 -> node_5; + node_4 -> node_14; + node_5 -> node_15; + node_5 -> node_16; + node_5 -> node_17; + node_5 -> node_18; + node_5 -> node_18; + node_10 -> node_9; +} diff --git a/test/all/syntax.expected.callgraph.json b/test/all/syntax.expected.callgraph.json new file mode 100644 index 00000000..c84e70d4 --- /dev/null +++ b/test/all/syntax.expected.callgraph.json @@ -0,0 +1,213 @@ +{ + "nodes": [ + { + "idx": 1, + "name": "test_try", + "inEdges": [], + "outEdges": [ + 1 + ] + }, + { + "idx": 2, + "name": "test_loops", + "inEdges": [], + "outEdges": [ + 2 + ] + }, + { + "idx": 3, + "name": "reply", + "inEdges": [], + "outEdges": [ + 3, + 4 + ] + }, + { + "idx": 4, + "name": "notify", + "inEdges": [], + "outEdges": [ + 5, + 6 + ] + }, + { + "idx": 5, + "name": "forward", + "inEdges": [ + 3, + 5 + ], + "outEdges": [ + 7, + 8, + 9, + 10, + 11 + ] + }, + { + "idx": 6, + "name": "getter", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 7, + "name": "getter", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 8, + "name": "test", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 9, + "name": "getA", + "inEdges": [ + 12 + ], + "outEdges": [] + }, + { + "idx": 10, + "name": "test", + "inEdges": [], + "outEdges": [ + 12 + ] + }, + { + "idx": 11, + "name": "receiver_1722", + "inEdges": [], + "outEdges": [] + }, + { + "idx": 12, + "name": "dump", + "inEdges": [ + 1 + ], + "outEdges": [] + }, + { + "idx": 13, + "name": "emptyMap", + "inEdges": [ + 2 + ], + "outEdges": [] + }, + { + "idx": 14, + "name": "sender", + "inEdges": [ + 4, + 6 + ], + "outEdges": [] + }, + { + "idx": 15, + "name": "context", + "inEdges": [ + 7 + ], + "outEdges": [] + }, + { + "idx": 16, + "name": "myBalance", + "inEdges": [ + 8 + ], + "outEdges": [] + }, + { + "idx": 17, + "name": "nativeReserve", + "inEdges": [ + 9 + ], + "outEdges": [] + }, + { + "idx": 18, + "name": "send", + "inEdges": [ + 10, + 11 + ], + "outEdges": [] + } + ], + "edges": [ + { + "idx": 1, + "src": 1, + "dst": 12 + }, + { + "idx": 2, + "src": 2, + "dst": 13 + }, + { + "idx": 3, + "src": 3, + "dst": 5 + }, + { + "idx": 4, + "src": 3, + "dst": 14 + }, + { + "idx": 5, + "src": 4, + "dst": 5 + }, + { + "idx": 6, + "src": 4, + "dst": 14 + }, + { + "idx": 7, + "src": 5, + "dst": 15 + }, + { + "idx": 8, + "src": 5, + "dst": 16 + }, + { + "idx": 9, + "src": 5, + "dst": 17 + }, + { + "idx": 10, + "src": 5, + "dst": 18 + }, + { + "idx": 11, + "src": 5, + "dst": 18 + }, + { + "idx": 12, + "src": 10, + "dst": 9 + } + ] +} \ No newline at end of file diff --git a/test/all/syntax.expected.callgraph.mmd b/test/all/syntax.expected.callgraph.mmd new file mode 100644 index 00000000..4393d605 --- /dev/null +++ b/test/all/syntax.expected.callgraph.mmd @@ -0,0 +1,31 @@ +graph TD + node_1["test_try"] + node_2["test_loops"] + node_3["reply"] + node_4["notify"] + node_5["forward"] + node_6["getter"] + node_7["getter"] + node_8["test"] + node_9["getA"] + node_10["test"] + node_11["receiver_1722"] + node_12["dump"] + node_13["emptyMap"] + node_14["sender"] + node_15["context"] + node_16["myBalance"] + node_17["nativeReserve"] + node_18["send"] + node_1 --> node_12 + node_2 --> node_13 + node_3 --> node_5 + node_3 --> node_14 + node_4 --> node_5 + node_4 --> node_14 + node_5 --> node_15 + node_5 --> node_16 + node_5 --> node_17 + node_5 --> node_18 + node_5 --> node_18 + node_10 --> node_9 diff --git a/test/tactIR.spec.ts b/test/tactIR.spec.ts index c5012254..4d42305f 100644 --- a/test/tactIR.spec.ts +++ b/test/tactIR.spec.ts @@ -75,7 +75,7 @@ function processSingleFile(file: string): void { const contractName = file.replace(".tact", ""); const filePath = path.join(ALL_DIR, file); - const tools: string[] = ["DumpCfg", "DumpImports"]; + const tools: string[] = ["DumpCfg", "DumpImports", "DumpCallGraph"]; const formats: string[] = ["json", "dot", "mmd"]; tools.forEach((toolName: string) => {