Skip to content

Commit

Permalink
feat(ir): Add Callgraph (#185)
Browse files Browse the repository at this point in the history
Closes #91

---------

Co-authored-by: Georgiy Komarov <[email protected]>
  • Loading branch information
Esorat and jubnzv authored Nov 6, 2024
1 parent e894231 commit a26986c
Show file tree
Hide file tree
Showing 15 changed files with 1,751 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@
"Dont",
"consteval",
"Georgiy",
"Komarov"
"Komarov",
"callgraph"
],
"flagWords": [],
"ignorePaths": [
Expand Down
18 changes: 17 additions & 1 deletion src/internals/ir/builders/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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,
);
}

Expand Down
249 changes: 249 additions & 0 deletions src/internals/ir/callGraph.ts
Original file line number Diff line number Diff line change
@@ -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<CGEdgeId> = new Set();
public outEdges: Set<CGEdgeId> = 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<CGNodeId, CGNode> = new Map();
private edgesMap: Map<CGEdgeId, CGEdge> = new Map();
private nameToNodeId: Map<string, CGNodeId> = new Map();
private logger: Logger;

constructor(private ctx: MistiContext) {
this.logger = ctx.logger;
}

public getNodes(): Map<CGNodeId, CGNode> {
return this.nodeMap;
}

public getEdges(): Map<CGEdgeId, CGEdge> {
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<CGNodeId>([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.`,
);
}
}
}
2 changes: 2 additions & 0 deletions src/internals/ir/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,6 +32,7 @@ export class CompilationUnit {
public imports: ImportGraph,
public functions: Map<CFGIdx, CFG>,
public contracts: Map<ContractIdx, Contract>,
public callGraph: CallGraph,
) {}

/**
Expand Down
Loading

0 comments on commit a26986c

Please sign in to comment.