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

Callgraph: Add effects #227

Merged
merged 5 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
176 changes: 157 additions & 19 deletions src/internals/ir/callGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TactASTStore } from "./astStore";
import { IdxGenerator } from "./indices";
import { MistiContext } from "../../";
import { Logger } from "../../internals/logger";
import { findInExpressions, forEachExpression } from "../tact/iterators";
import { forEachExpression } from "../tact/iterators";
import { isSendCall } from "../tact/util";
import {
AstFunctionDef,
Expand All @@ -16,6 +16,11 @@ import {
AstId,
AstContractDeclaration,
AstNode,
AstFieldAccess,
AstStatement,
AstStatementAssign,
AstStatementAugmentedAssign,
AstStatementExpression,
} from "@tact-lang/compiler/dist/grammar/ast";

export type CGNodeId = number & { readonly brand: unique symbol };
Expand All @@ -24,10 +29,13 @@ export type CGEdgeId = number & { readonly brand: unique symbol };
/**
* Flag constants for CGNode.
*
* `FLAG_CALLS_SEND` (0b0001): Indicates that the function represented by this node
* contains a direct or indirect call to a "send" function.
* Each flag represents an effect or property of the function represented by the node.
*/
const FLAG_CALLS_SEND = 0b0001;
const FlagCallsSend = 0b0001;
Esorat marked this conversation as resolved.
Show resolved Hide resolved
const FlagContractStateRead = 0b0010;
const FlagContractStateWrite = 0b0100;
const FlagBlockchainStateRead = 0b1000;
const FlagRandomness = 0b10000;

/**
* Represents an edge in the call graph, indicating a call from one function to another.
Expand Down Expand Up @@ -298,41 +306,78 @@ export class CallGraph {
| AstReceiver;
const funcNodeId = this.astIdToNodeId.get(func.id);
if (funcNodeId !== undefined) {
const funcNode = this.getNode(funcNodeId);
if (!funcNode) continue;

// Process statements
if ("statements" in func && func.statements) {
for (const stmt of func.statements) {
this.processStatement(stmt, funcNodeId, contractName);
}
}

// Process expressions (if any)
forEachExpression(func, (expr) => {
this.processExpression(expr, funcNodeId, contractName);
});
const sendCallFound =
findInExpressions(func, isSendCall) !== null;
if (sendCallFound) {
const funcNode = this.getNode(funcNodeId);
if (funcNode) {
funcNode.setFlag(FLAG_CALLS_SEND);
}
}
}
}
}
} else if (entry.kind === "function_def") {
const func = entry as AstFunctionDef;
const funcNodeId = this.astIdToNodeId.get(func.id);
if (funcNodeId !== undefined) {
const funcNode = this.getNode(funcNodeId);
if (!funcNode) continue;
if (func.statements) {
for (const stmt of func.statements) {
this.processStatement(stmt, funcNodeId);
}
}
forEachExpression(func, (expr) => {
this.processExpression(expr, funcNodeId);
});
const sendCallFound = findInExpressions(func, isSendCall) !== null;
if (sendCallFound) {
const funcNode = this.getNode(funcNodeId);
if (funcNode) {
funcNode.setFlag(FLAG_CALLS_SEND);
}
}
}
}
}
}

/**
* Processes a single statement, identifying assignments and other statements.
* Also detects effects and sets corresponding flags on the function node.
* @param stmt The statement to process.
* @param callerId The node ID of the calling function.
* @param currentContractName The name of the contract, if applicable.
*/
private processStatement(
stmt: AstStatement,
callerId: CGNodeId,
currentContractName?: string,
) {
const funcNode = this.getNode(callerId);
if (!funcNode) {
return;
}
if (
stmt.kind === "statement_assign" ||
stmt.kind === "statement_augmentedassign"
) {
if (isContractStateWrite(stmt)) {
funcNode.setFlag(FlagContractStateWrite);
}
} else if (stmt.kind === "statement_expression") {
const stmtExpr = stmt as AstStatementExpression;
this.processExpression(
stmtExpr.expression,
callerId,
currentContractName,
);
}
}

/**
* Processes a single expression, identifying function or method calls to create edges.
* Also detects effects and sets corresponding flags on the function node.
* @param expr The expression to process.
* @param callerId The node ID of the calling function.
* @param currentContractName The name of the contract, if applicable.
Expand All @@ -356,6 +401,24 @@ export class CallGraph {
);
}
}
// Detect and set effects
const funcNode = this.getNode(callerId);
if (!funcNode) {
return;
}

if (isContractStateRead(expr)) {
funcNode.setFlag(FlagContractStateRead);
}
if (isBlockchainStateRead(expr)) {
funcNode.setFlag(FlagBlockchainStateRead);
}
if (isRandomnessCall(expr)) {
funcNode.setFlag(FlagRandomness);
}
if (isSendCall(expr)) {
funcNode.setFlag(FlagCallsSend);
}
}

/**
Expand Down Expand Up @@ -431,3 +494,78 @@ export class CallGraph {
export function isSelf(expr: AstExpression): boolean {
return expr.kind === "id" && (expr as AstId).text === "self";
}

/**
* Helper function to determine if an expression is a contract state read.
* @param expr The expression to check.
* @returns True if the expression reads from a state variable; otherwise, false.
*/
function isContractStateRead(expr: AstExpression): boolean {
if (expr.kind === "field_access") {
const fieldAccess = expr as AstFieldAccess;
if (fieldAccess.aggregate.kind === "id") {
const idExpr = fieldAccess.aggregate as AstId;
if (idExpr.text === "self") {
return true; // Accessing a state variable via 'self'
}
}
}
return false;
}

/**
* Helper function to determine if a statement is a contract state write.
* @param stmt The statement to check.
* @returns True if the statement writes to a state variable; otherwise, false.
*/
function isContractStateWrite(
Esorat marked this conversation as resolved.
Show resolved Hide resolved
stmt: AstStatementAssign | AstStatementAugmentedAssign,
): boolean {
const pathExpr = stmt.path;
if (pathExpr.kind === "field_access") {
const fieldAccess = pathExpr as AstFieldAccess;
if (fieldAccess.aggregate.kind === "id") {
const idExpr = fieldAccess.aggregate as AstId;
if (idExpr.text === "self") {
return true;
}
}
}
return false;
}

/**
* Helper function to determine if an expression is a blockchain state read.
* @param expr The expression to check.
* @returns True if the expression reads blockchain state; otherwise, false.
*/
function isBlockchainStateRead(expr: AstExpression): boolean {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
if (expr.kind === "static_call") {
const staticCall = expr as AstStaticCall;
const functionName = staticCall.function?.text;
return functionName === "now" || functionName === "timestamp";
} else if (expr.kind === "method_call") {
const methodCall = expr as AstMethodCall;
const methodName = methodCall.method?.text;
return methodName === "now" || methodName === "timestamp";
Esorat marked this conversation as resolved.
Show resolved Hide resolved
}
return false;
}

/**
* Helper function to determine if an expression is a randomness call.
* @param expr The expression to check.
* @returns True if the expression introduces randomness; otherwise, false.
*/
function isRandomnessCall(expr: AstExpression): boolean {
if (expr.kind === "static_call") {
const staticCall = expr as AstStaticCall;
const functionName = staticCall.function?.text;
return functionName === "random";
Esorat marked this conversation as resolved.
Show resolved Hide resolved
} else if (expr.kind === "method_call") {
Esorat marked this conversation as resolved.
Show resolved Hide resolved
const methodCall = expr as AstMethodCall;
const methodName = methodCall.method?.text;
return methodName === "random";
}
return false;
}
64 changes: 40 additions & 24 deletions test/all/sample-jetton.expected.callgraph.dot
Original file line number Diff line number Diff line change
Expand Up @@ -11,59 +11,75 @@ digraph "CallGraph" {
node_9 [label="JettonDefaultWallet::receiver_2832"];
node_10 [label="JettonDefaultWallet::receiver_2876"];
node_11 [label="JettonDefaultWallet::get_wallet_data"];
node_12 [label="context"];
node_13 [label="require"];
node_14 [label="SampleJetton::mint"];
node_15 [label="ctx::readForwardFee"];
node_16 [label="min"];
node_17 [label="ton"];
node_18 [label="contractAddress"];
node_19 [label="send"];
node_12 [label="require"];
node_13 [label="SampleJetton::mint"];
node_14 [label="context"];
node_15 [label="send"];
node_16 [label="ctx::readForwardFee"];
node_17 [label="min"];
node_18 [label="ton"];
node_19 [label="contractAddress"];
node_20 [label="JettonDefaultWallet::toCell"];
node_21 [label="myBalance"];
node_22 [label="msg::loadUint"];
node_23 [label="msg::loadCoins"];
node_2 -> node_12;
node_2 -> node_13;
node_2 -> node_12;
node_2 -> node_13;
node_2 -> node_14;
node_2 -> node_12;
node_2 -> node_12;
node_2 -> node_13;
node_3 -> node_12;
node_3 -> node_13;
node_3 -> node_14;
node_3 -> node_12;
node_3 -> node_13;
node_4 -> node_12;
node_4 -> node_14;
node_4 -> node_12;
node_4 -> node_13;
node_6 -> node_12;
node_6 -> node_13;
node_6 -> node_15;
node_6 -> node_12;
node_6 -> node_12;
node_6 -> node_15;
node_6 -> node_13;
node_6 -> node_14;
node_6 -> node_12;
node_6 -> node_16;
node_6 -> node_16;
node_6 -> node_12;
node_6 -> node_17;
node_6 -> node_13;
node_6 -> node_18;
node_6 -> node_12;
node_6 -> node_19;
node_6 -> node_15;
node_6 -> node_20;
node_7 -> node_12;
node_7 -> node_13;
node_7 -> node_18;
node_7 -> node_13;
node_7 -> node_14;
node_7 -> node_12;
node_7 -> node_19;
node_7 -> node_12;
node_7 -> node_15;
node_7 -> node_20;
node_7 -> node_8;
node_7 -> node_16;
node_7 -> node_15;
node_7 -> node_19;
node_7 -> node_20;
node_8 -> node_21;
node_8 -> node_16;
node_8 -> node_17;
node_9 -> node_12;
node_9 -> node_12;
node_9 -> node_12;
node_9 -> node_15;
node_9 -> node_14;
node_9 -> node_12;
node_9 -> node_12;
node_9 -> node_16;
node_9 -> node_12;
node_9 -> node_13;
node_9 -> node_13;
node_9 -> node_15;
node_9 -> node_13;
node_9 -> node_19;
node_9 -> node_20;
node_10 -> node_12;
node_10 -> node_22;
node_10 -> node_22;
node_10 -> node_23;
node_10 -> node_13;
node_10 -> node_12;
}
Loading