Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Callgraph #185

Merged
merged 58 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5d8727d
fix: Draft Pr for check CallGraph
Esorat Oct 17, 2024
57bc931
chore: spell for pass ci
Esorat Oct 17, 2024
27ffbba
fix: remove unused export from class
Esorat Oct 17, 2024
b2c2cf0
fix: make properties public
Esorat Oct 18, 2024
26d582f
fix: add callGraph to ir CompilationUnit. When building the IR and c…
Esorat Oct 18, 2024
3c07cec
fix: add switch case, add forEachExpression for processExpressions, u…
Esorat Oct 18, 2024
b5b93e3
fix: add case statement_augmentedassign
Esorat Oct 18, 2024
0b8cb2b
fix: del test/good folder
Esorat Oct 18, 2024
a8f31b1
fix: enhance CallGraph with efficient edge tracking and lookup
Esorat Oct 18, 2024
12a0817
chore: remove extra newlines
Esorat Oct 18, 2024
fd02604
fix: back areConnected func
Esorat Oct 18, 2024
2ce8095
fix: add BFS to areConnected function
Esorat Oct 18, 2024
4695954
fix: add dumpCallgraph
Esorat Oct 19, 2024
8e1b8eb
fix: remake CallGraph add type Aliases.
Esorat Oct 19, 2024
c127dc5
fix: Callgraph export type.
Esorat Oct 20, 2024
5ee3309
chore: enabledByDefault: false
Esorat Oct 20, 2024
608a0d2
chore: remove export { TactASTStore };
Esorat Oct 20, 2024
2ff1b58
Merge remote-tracking branch 'upstream/master' into 91-add-callgraph
Esorat Oct 20, 2024
135e42b
fix: add correct export and import
Esorat Oct 20, 2024
2b47fb0
chore: fix spell
Esorat Oct 20, 2024
879708d
chore: update CHANGELOG
Esorat Oct 20, 2024
7ed87d8
chore: add expected files for test
Esorat Oct 20, 2024
277fc3a
Update CHANGELOG.md
jubnzv Oct 20, 2024
d2a8663
Update src/tools/dumpCallgraph.ts
jubnzv Oct 20, 2024
b7a7c6c
Update src/tools/dumpCallgraph.ts
jubnzv Oct 20, 2024
8c23973
fix: refactor
Esorat Oct 20, 2024
a70024d
Merge branch '91-add-callgraph' of https://github.com/Esorat/misti in…
Esorat Oct 20, 2024
b3411fe
fix: pass ci test
Esorat Oct 20, 2024
7a795db
Merge remote-tracking branch 'upstream' into 91-add-callgraph
Esorat Oct 25, 2024
eea2ec4
fix: remove dublicate and add needed expected output
Esorat Oct 25, 2024
ddba926
Merge remote-tracking branch 'upstream' into 91-add-callgraph
Esorat Oct 27, 2024
997056d
WIP: Refactoring
Esorat Oct 28, 2024
f2db6b9
fix: refactor dumpCallgraph
Esorat Oct 31, 2024
364460e
fix: dumpCallgraph
Esorat Oct 31, 2024
30ff0ec
fix: Update Callgraph and dumpCallgraph
Esorat Nov 2, 2024
7a7c5d3
fix: Refactor logic. Add branded types. Docstrings.
Esorat Nov 3, 2024
e393e1e
Merge remote-tracking branch 'upstream' into 91-add-callgraph
Esorat Nov 4, 2024
e18aa54
Fix: Refactor
Esorat Nov 4, 2024
82eb520
Fix: add 'make' for consistency with other builders.
Esorat Nov 6, 2024
f1e7a23
Chore: Add a proper docstring for the areConnected method
Esorat Nov 6, 2024
e4a9fec
Fix: Remove return undefined because unreachable throws.
Esorat Nov 6, 2024
d3d0397
Fix: Documented undefined value. Add logger.debug
Esorat Nov 6, 2024
ec66e96
Chore: Add needed docstring for parts of the public API
Esorat Nov 6, 2024
dc0a489
Chore: Update CHANGELOG
Esorat Nov 6, 2024
818b826
Update CHANGELOG.md
jubnzv Nov 6, 2024
0f59933
Update src/internals/ir/callGraph.ts
jubnzv Nov 6, 2024
76b8753
Update src/internals/ir/callGraph.ts
jubnzv Nov 6, 2024
5932983
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
53ddeb2
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
89294c1
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
8fe7500
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
33cae65
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
94da560
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
3145037
Update src/internals/ir/callGraph.ts
Esorat Nov 6, 2024
08b21c0
Chore: yarn fix-all
Esorat Nov 6, 2024
d780e4a
Update src/internals/ir/callGraph.ts
jubnzv Nov 6, 2024
e65a098
Update src/internals/ir/callGraph.ts
jubnzv Nov 6, 2024
95359b8
Update src/internals/ir/callGraph.ts
jubnzv Nov 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [#91](https://github.com/nowarp/misti/pull/91)
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
- `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
252 changes: 252 additions & 0 deletions src/internals/ir/callGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
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 {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
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();

/**
* `astId` might be `undefined` if this node doesn’t have a corresponding AST entry,
Esorat marked this conversation as resolved.
Show resolved Hide resolved
* which indicates an issue in Misti.
*/
constructor(
public astId: number | undefined,
Esorat marked this conversation as resolved.
Show resolved Hide resolved
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}". Possible bug in Misti.`,
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
}

/**
* 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 {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
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
Esorat marked this conversation as resolved.
Show resolved Hide resolved
* @param dst - The ID of the destination node to search for
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
* @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 {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
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.
Esorat marked this conversation as resolved.
Show resolved Hide resolved
*/
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.
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
* @returns The function name if available; otherwise, `undefined`.
*/
private getFunctionName(
func: AstFunctionDef | AstReceiver | AstContractInit,
): string | undefined {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
switch (func.kind) {
case "function_def":
return func.name?.text;
case "contract_init":
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
Esorat marked this conversation as resolved.
Show resolved Hide resolved
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.
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
* @param callerId - The ID of the calling node.
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
Esorat marked this conversation as resolved.
Show resolved Hide resolved
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.
Esorat marked this conversation as resolved.
Show resolved Hide resolved
* @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.
Esorat marked this conversation as resolved.
Show resolved Hide resolved
* @param dst - The destination node ID.
Esorat marked this conversation as resolved.
Show resolved Hide resolved
*/
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);
this.logger.debug(`Created edge from ${src} to ${dst}`);
Esorat marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading