Tai-e官网:
概述 | Tai-e
参考:
https://www.cnblogs.com/gonghr/p/17979609
--------------------------------------------------------
1 作业导览
- 为 Java 实现常量传播算法。
- 实现一个通用的 worklist 求解器,并用它来解决一些数据流分析问题,例如本次的常量传播。
在本次实验作业中,你要在 Tai-e 框架下实现常量传播算法和 worklist 求解器的关键部分。
2 实现常量传播
2.1 分析范围
在此次作业中,你需要实现针对 int
类型的常量传播。注意,在 Java 中,boolean
、byte
、char
和 short
类型在运行时实际上都以 int
值的形式进行表示和计算,因此你的分析算法也应当能处理这些类型。其他基本数据类型(例如 long
、float
和 double
)以及引用类型(例如 class types、array types)不在此次作业的考虑范围内,所以你可以在分析时忽略它们的值。
至于语句的处理,你只需要关注等号左侧为变量且右侧只能是如下几类表达式的赋值语句:
- 常量,如
x = 1
- 变量,如
x = y
- 二元运算表达式,如
x = a + b
和x = a >> b
下表列举出了除逻辑运算符以外所有作用在 int
类型上的二元运算符,请确保你的算法能正确处理它们:
运算类型 | 运算符 |
---|---|
Arithmetic | + - * / % |
Condition | == != < > <= >= |
Shift | << >> >>> |
Bitwise | | & ^ |
逻辑运算符怎么办?
在上表中,我们没有列出逻辑运算符。这是因为,在 Java 中,逻辑运算符与(
&&
)和或(||
)没有对应的字节码表示,而是以分支跳转的形式实现的。例如以下语句:a = b || c;
将被转换为如下的语义等价的语句:
if (b) {t = true; // t is temp variable } else if (c) {t = true; } else {t = false; } a = t;
由于作业中的常量传播算法能够处理常量赋值(如
t = true
)、变量赋值(如a = t
)和分支语句(如if (b) {…}
),所以它自然能够处理&&
和||
。
至于等号左侧为变量、等号右侧为其它表达式的赋值语句,例如方法调用(x = m(...)
)和字段 load(x = o.f
),你需要对它们进行保守的近似处理(也许会不够精确),即把它们当作 x = NAC
。后续的作业将逐步解锁方法调用和字段访问的精确分析技巧。
对于上面没有提到的其它语句(例如字段存储 o.f = x
),我们只需要使用恒等函数作为它们的 transfer
函数。在未来我们引入别名分析后,你就可以对字段存储进行精确处理了。
2.2 Tai-e 中你需要了解的类
这一节将会介绍与 IR 相关的类(pascal.taie.ir.*
)以及和本次的分析算法相关的类(pascal.taie.analysis.*
)。细致地了解它们将有助于完成常量分析算法。
-
pascal.taie.ir.IR
这是 Tai-e 的 IR 的核心数据结构。它的每个实例储存了一个 Java 方法的各种信息,例如变量(
variables
)、参数(parameters
)、语句(Stmts
)等等。这里需要注意一点,一个方法体的中间表示由多个中间表示语句(Stmts
)组成。如果想要加深理解,你可以自行阅读 API、源码实现以及注释。 -
pascal.taie.ir.exp.Exp
我们已经在作业 1 中介绍过了这个接口。这是 Tai-e 的 IR 中的一个关键接口,用于表示程序中的所有表达式。它含有很多子类,对应各类具体的表达式。具体细节查看:【静态分析】软件分析课程实验A1-活跃变量分析和迭代求解器-CSDN博客
-
在本次作业中,你需要处理更多
Exp
接口的子类。下图是一个继承关系的简单示意图(已略去与本次作业无关的类)。
-
下面我们将逐一介绍这些子类。
-
pascal.taie.ir.exp.Var
这个类代表 IR 中的变量。
-
pascal.taie.ir.exp.IntLiteral
根据 Java 的语言规范,我们在 Tai-e 中把常量称作字面量(Literals)。每个
IntLiteral
类的实例都表示一个程序中的整数字面量。你可以通过调用getValue()
方法来获取它的值。 -
pascal.taie.ir.exp.BinaryExp
这个类代表程序中的二元表达式。这个类的各个子类对应了不同种类的二元表达式,并且每个子类中都有一个内部枚举类型用于表示该类支持的运算符。例如枚举类型
ArithmeticExp.Op
就代表了ArithmeticExp
(算术表达式类)所支持的运算符,也就是+ - * /
和%
。
需要指出的是,在 Tai-e 中,BinaryExp
的两个操作数都是 Var
类型的。例如下面的语句
x = y + 6;
在 Tai-e 中会被转化成如下的 IR:
%intconst0 = 6; // %intconst* are temp variables introduced
x = y + %intconst0; // by Tai-e to hold constant int values
-
这样的设计简化了分析的实现:在获取
BinaryExp
的操作数时,你只需要考虑它是变量的这一种可能,而不用担心它是常量或其它可能了。 -
pascal.taie.ir.stmt.DefinitionStmt
这是
Stmt
的一个子类。它表示了程序中所有的赋值语句,(即形如x = y
或x = m(…)
的语句)。这个类很简单。你可以通过阅读源码来决定如何使用它。
-
pascal.taie.analysis.dataflow.analysis.DataflowAnalysis
这是具体数据流分析算法需要实现的接口。和作业 1 一样,它会被求解器调用。在本次作业中你只需要关注前 5 个 API。这些 API 会被你在后面完成的 worklist 求解器调用。
-
pascal.taie.analysis.dataflow.analysis.constprop.Value
-
它的代码和注释解释了它的用法。你应该用下列的静态方法获取格上抽象值(即该类的实例):
Value getNAC()
: 返回NAC
Value getUndef()
: 返回UNDEF
Value makeConstant(int)
: 返回给定整数在格上对应的抽象值
-
pascal.taie.analysis.dataflow.analysis.constprop.CPFact
这个类表示常量传播中的 data facts,即一个从变量(
Var
)到格上抽象值(Value
)的映射。该类提供了各种map
相关的操作,例如键值对的查询、更新等等。这些操作大多继承自pascal.taie.analysis.dataflow.fact.MapFact
。这些类的注释都很充分,所以你应该通过阅读源码来决定如何使用其中的 API。 -
pascal.taie.analysis.dataflow.analysis.constprop.ConstantPropagation
这个类实现了
DataflowAnalysis
。你需要在其中编写完整的常量传播算法。具体要求见第 2.3 节。
2.3 你的任务 [重点!]
首先,你需要完成 ConstantPropagation
的下述 API:
-
CPFact newBoundaryFact(CFG)
-
CPFact newInitialFact()
-
void meetInto(CPFact,CPFact)
-
boolean transferNode(Stmt,CPFact,CPFact)
你已经在作业 1 中见到过这几个 API,他们是从 DataflowAnalysis
中继承下来的,需要注意的是:在实现 newBoundaryFact()
的时候,你要小心地处理每个会被分析的方法的参数。具体来说,你要将它们的值初始化为 NAC
(请思考:为什么要这么做?)。
原因是为了 safe-approximation
,我们不知道通过形参传递过来的参数是否是常量,所以为了安全,假设所有参数都是 NAC
,当然这样会导致精度损失问题,后面通过过程间分析可以有效解决这个问题。
提示:
正如第 2.1 节中提到的,本次作业只关注
int
类型的常量传播。为了实现这一点,框架代码在ConstantPropagation
类中提供了canHoldInt(Var)
方法来判断一个变量能否储存int
类型的值。你需要利用这个方法来判断一个变量是否在本次作业的分析范围内,并忽略那些不在范围内的变量(例如float
类型的变量)。
此外,你还需要实现下面两个辅助方法:
-
Value meetValue(Value,Value)
-
你应当在
meetInto()
方法中调用它。 -
Value evaluate(Exp,CPFact)
这个方法会计算表达式(
Exp
)的值(Value
)。当然,此处的值是格上的抽象值。你需要参考【静态分析】静态分析笔记04 - 数据流分析(理论)-CSDN博客 -
来实现它的三种情况。对于其它情况,该方法会像我们在第 2.1 节提到的那样返回
NAC
。你应该在transferNode()
方法中调用它来进行表达式的求值。
提示:
- 和作业 1 一样,我们对
meetInto()
的设计比较特殊。如果你不记得了,可以再回顾一下作业 1 文档的相关部分。- 条件表达式(如
a == b
)的值由0
(若为False
)和1
(若为True
)来表示。- 对于除以
0
的情况(出现在/
和%
中),我们规定结果为UNDEF
。例如,对于x = a / 0
,x
的值将会是UNDEF
。
算法伪代码描述
newBoundaryFact
:负责创建和初始化虚拟结点的Data Flow Fact
。但是注意要把方法参数初始化为NAC
。 根据题目要求,不是所有类型的参数都考虑,只有能转换成int
类型的参数才考虑,所以别忘了用canHoldInt
过滤一下。
newInitialFact
:负责创建和初始化控制流图中除了Entry
和Exit
之外的结点的Data Flow Fact
。控制流图中一个结点的IN
和OUT
分别对应一个Data Flow Fact
,记录当前程序点时变量的状态。直接创建一个空的CPFact
即可,方法体内还没有开始扫描。
meetInto
:负责处理transfer function
之前可能遇到多个OUT
时的合并处理。具体的合并操作通过调用meetValue
来处理。meetValue
:负责对格上的值进行合并。
meet 操作:
- NAC ⊓ v = NAC(非常量)
- UNDEF ⊓ v = v(未初始化的变量不是我们分析的目标)
- c ⊓ v = ?
- c ⊓ c = c
- c1 ⊓ c2 = NAC
transferNode
:负责实现控制流图中结点的transfer function
。如果OUT
改变,返回true
;否则返回false
。
stmt
表示结点中的一条中间表示,一个结点只有一个中间表示。
题目要求只需要对赋值语句处理,所以用 DefinitionStmt
类型过滤。
对于所有赋值语句,只考虑具有左值,并且左值是变量且类型可以转换成 int
的语句。这些语句的右值是一个表达式,可能是常量,也能是变量、二元表达式。这个右值表达式的值将通过 evaluate
函数计算。
对于其他类型的语句,不做处理,out
直接复制 in
即可,相当于经过一个恒等函数。
evaluate
:负责表达式值的计算。
表达式分三种情况讨论
- 常量:直接赋值。
- 变量:获取变量的值再赋值。
- 二元运算:针对共 12 中运算分别处理。
/** Tai-e: A Static Analysis Framework for Java** Copyright (C) 2022 Tian Tan <tiantan@nju.edu.cn>* Copyright (C) 2022 Yue Li <yueli@nju.edu.cn>** This file is part of Tai-e.** Tai-e is free software: you can redistribute it and/or modify* it under the terms of the GNU Lesser General Public License* as published by the Free Software Foundation, either version 3* of the License, or (at your option) any later version.** Tai-e is distributed in the hope that it will be useful,but WITHOUT* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General* Public License for more details.** You should have received a copy of the GNU Lesser General Public* License along with Tai-e. If not, see <https://www.gnu.org/licenses/>.*/package pascal.taie.analysis.dataflow.analysis.constprop;import pascal.taie.analysis.dataflow.analysis.AbstractDataflowAnalysis;
import pascal.taie.analysis.graph.cfg.CFG;
import pascal.taie.config.AnalysisConfig;
import pascal.taie.ir.IR;
import pascal.taie.ir.exp.ArithmeticExp;
import pascal.taie.ir.exp.BinaryExp;
import pascal.taie.ir.exp.BitwiseExp;
import pascal.taie.ir.exp.ConditionExp;
import pascal.taie.ir.exp.Exp;
import pascal.taie.ir.exp.IntLiteral;
import pascal.taie.ir.exp.ShiftExp;
import pascal.taie.ir.exp.Var;
import pascal.taie.ir.stmt.DefinitionStmt;
import pascal.taie.ir.stmt.Stmt;
import pascal.taie.language.type.PrimitiveType;
import pascal.taie.language.type.Type;
import pascal.taie.util.AnalysisException;import pascal.taie.ir.exp.*;public class ConstantPropagation extendsAbstractDataflowAnalysis<Stmt, CPFact> {public static final String ID = "constprop";public ConstantPropagation(AnalysisConfig config) {super(config);}@Overridepublic boolean isForward() {return true;}@Overridepublic CPFact newBoundaryFact(CFG<Stmt> cfg) {// TODO - finish meCPFact cpFact = new CPFact();for (Var param : cfg.getIR().getParams()) {if (canHoldInt(param)) { // 只考虑可转换int类型的参数cpFact.update(param, Value.getNAC()); // 建立参数到格上值(NAC)的映射}}return cpFact;}@Overridepublic CPFact newInitialFact() {// TODO - finish mereturn new CPFact();}@Overridepublic void meetInto(CPFact fact, CPFact target) {// TODO - finish mefor (Var var : fact.keySet()) {Value v1 = fact.get(var);Value v2 = target.get(var);target.update(var, meetValue(v1, v2));}}/*** Meets two Values.*/public Value meetValue(Value v1, Value v2) {// TODO - finish meif (v1.isNAC() || v2.isNAC()) {return Value.getNAC();} else if (v1.isUndef()) {return v2;} else if (v2.isUndef()) {return v1;} else if (v1.isConstant() && v2.isConstant()) {if (v1.getConstant() == v2.getConstant()) {return v1;} else {return Value.getNAC();}} else {return Value.getNAC();}}@Overridepublic boolean transferNode(Stmt stmt, CPFact in, CPFact out) {// TODO - finish meCPFact copy = in.copy(); // 复制in给copy,避免影响in。if (stmt instanceof DefinitionStmt) { // 只处理赋值语句if (stmt.getDef().isPresent()) { // 如果左值存在LValue lValue = stmt.getDef().get(); // 获取左值if ((lValue instanceof Var) && canHoldInt((Var) lValue)) { // 对于符合条件的左值copy.update((Var) lValue, evaluate(((DefinitionStmt<?, ?>) stmt).getRValue(), copy)); // 计算右值表达式的值用来更新左值变量在格上的值}}}return out.copyFrom(copy); // copy复制给out。copy和in相比,有更新,返回true;反之返回false}/*** @return true if the given variable can hold integer value, otherwise false.*/public static boolean canHoldInt(Var var) {Type type = var.getType();if (type instanceof PrimitiveType) {switch ((PrimitiveType) type) {case BYTE:case SHORT:case INT:case CHAR:case BOOLEAN:return true;}}return false;}/*** Evaluates the {@link Value} of given expression.** @param exp the expression to be evaluated* @param in IN fact of the statement* @return the resulting {@link Value}*/public static Value evaluate(Exp exp, CPFact in) {// TODO - finish meif (exp instanceof Var) { // 变量return in.get((Var) exp);} else if (exp instanceof IntLiteral) { // 常量return Value.makeConstant(((IntLiteral) exp).getValue());} else if (exp instanceof BinaryExp) { // 二元运算Value v1 = in.get(((BinaryExp) exp).getOperand1()); // 获取运算分量在格上的值Value v2 = in.get(((BinaryExp) exp).getOperand2());if (v1.isNAC() || v2.isNAC()) {if (v1.isNAC() && v2.isConstant() && exp instanceof ArithmeticExp) { // x = a / 0,x = a % 0,x 的值将会是 UNDEFArithmeticExp.Op operator = ((ArithmeticExp) exp).getOperator();if (operator == ArithmeticExp.Op.DIV || operator == ArithmeticExp.Op.REM) {if (v2.getConstant() == 0) return Value.getUndef();}}return Value.getNAC();}if (v1.isUndef() || v2.isUndef()) {return Value.getUndef();}if (exp instanceof ArithmeticExp) {ArithmeticExp.Op operator = ((ArithmeticExp) exp).getOperator();switch (operator) {case ADD -> {return Value.makeConstant(v1.getConstant() + v2.getConstant());}case DIV -> {if (v2.getConstant() == 0) return Value.getUndef();return Value.makeConstant(v1.getConstant() / v2.getConstant());}case MUL -> {return Value.makeConstant(v1.getConstant() * v2.getConstant());}case SUB -> {return Value.makeConstant(v1.getConstant() - v2.getConstant());}case REM -> {if (v2.getConstant() == 0) return Value.getUndef();return Value.makeConstant(v1.getConstant() % v2.getConstant());}}} else if (exp instanceof ConditionExp) {ConditionExp.Op operator = ((ConditionExp) exp).getOperator();switch (operator) {case EQ -> {if (v1.getConstant() == v2.getConstant()) return Value.makeConstant(1);else return Value.makeConstant(0);}case GE -> {if (v1.getConstant() >= v2.getConstant()) return Value.makeConstant(1);else return Value.makeConstant(0);}case GT -> {if (v1.getConstant() > v2.getConstant()) return Value.makeConstant(1);else return Value.makeConstant(0);}case LE -> {if (v1.getConstant() <= v2.getConstant()) return Value.makeConstant(1);else return Value.makeConstant(0);}case LT -> {if (v1.getConstant() < v2.getConstant()) return Value.makeConstant(1);else return Value.makeConstant(0);}case NE -> {if (v1.getConstant() != v2.getConstant()) return Value.makeConstant(1);else return Value.makeConstant(0);}}} else if (exp instanceof BitwiseExp) {BitwiseExp.Op operator = ((BitwiseExp) exp).getOperator();switch (operator) {case OR -> {return Value.makeConstant(v1.getConstant() | v2.getConstant());}case AND -> {return Value.makeConstant(v1.getConstant() & v2.getConstant());}case XOR -> {return Value.makeConstant(v1.getConstant() ^ v2.getConstant());}}} else if (exp instanceof ShiftExp) {ShiftExp.Op operator = ((ShiftExp) exp).getOperator();switch (operator) {case SHL -> {return Value.makeConstant(v1.getConstant() << v2.getConstant());}case SHR -> {return Value.makeConstant(v1.getConstant() >> v2.getConstant());}case USHR -> {return Value.makeConstant(v1.getConstant() >>> v2.getConstant());}}}else { // 二元表达式中的其他类型表达式return Value.getNAC();}}return Value.getNAC();}
}
3 实现 Worklist 求解器
3.1 Tai-e 中你需要了解的类
与迭代求解器类似,你需要清楚 DataflowResult
,CFG
和 Solver
的相关用法(我们已经在作业 1 中介绍过了)。除此之外,你还需要知道:
-
pascal.taie.analysis.dataflow.solver.WorkListSolver
该类继承了 Solver 类,实现了 worklist 算法。它的实现是不完整的,在本此作业中你需要完成它。
3.2 你的任务 [重点!]
你的第二个任务是完成下述两个 API 的实现:
考虑到常量传播是一个前向分析,你只需要关注前向分析相关的方法。initializeForward()
方法的具体实现参考如图前三行。
doSolveForward()
则包含了你要实现的算法的主体部分。
Solver.initializeForward(CFG,DataflowResult)
WorkListSolver.doSolveForward(CFG,DataflowResult)
提示:
- 讲义中的 worklist 算法通过比较
old_OUT
和OUT[B]
来决定后继节点是否应当加入 worklist 中,这个做法比较低效。Tai-e 中DataflowAnalysis.transferNode()
会返回此次 transfer 是否改变了 OUT fact。利用好这一点可以避免多余的判断;- 与作业 1 类似,不要忘了在
Solver.initializeForward()
中初始化每个语句的IN
和OUT
。
initializeForward
:初始化所有的 Data Flow Fact
。
doSolveForward
:负责实现 Worklist 求解器具体步骤。
protected void initializeForward(CFG<Node> cfg, DataflowResult<Node, Fact> result) {// TODO - finish meresult.setOutFact(cfg.getEntry(), analysis.newBoundaryFact(cfg));for (Node node : cfg) {if (cfg.isEntry(node)) continue;result.setInFact(node, analysis.newInitialFact());result.setOutFact(node, analysis.newInitialFact());}}
/** Tai-e: A Static Analysis Framework for Java** Copyright (C) 2022 Tian Tan <tiantan@nju.edu.cn>* Copyright (C) 2022 Yue Li <yueli@nju.edu.cn>** This file is part of Tai-e.** Tai-e is free software: you can redistribute it and/or modify* it under the terms of the GNU Lesser General Public License* as published by the Free Software Foundation, either version 3* of the License, or (at your option) any later version.** Tai-e is distributed in the hope that it will be useful,but WITHOUT* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General* Public License for more details.** You should have received a copy of the GNU Lesser General Public* License along with Tai-e. If not, see <https://www.gnu.org/licenses/>.*/package pascal.taie.analysis.dataflow.solver;import pascal.taie.analysis.dataflow.analysis.DataflowAnalysis;
import pascal.taie.analysis.dataflow.fact.DataflowResult;
import pascal.taie.analysis.graph.cfg.CFG;import java.util.*;class WorkListSolver<Node, Fact> extends Solver<Node, Fact> {WorkListSolver(DataflowAnalysis<Node, Fact> analysis) {super(analysis);}@Overrideprotected void doSolveForward(CFG<Node> cfg, DataflowResult<Node, Fact> result) {// TODO - finish meArrayDeque<Node> worklist = new ArrayDeque<>(); // 双端堆栈当队列用for (Node node : cfg) { // 添加所有结点到队列中if (cfg.isEntry(node)) {continue;}worklist.addLast(node);}while (!worklist.isEmpty()) {Node node = worklist.pollFirst(); // 弹出队头结点for (Node pred : cfg.getPredsOf(node)) { // 对该结点以及所有前驱结点的OUT做meetanalysis.meetInto(result.getOutFact(pred), result.getInFact(node));}boolean f = analysis.transferNode(node, result.getInFact(node), result.getOutFact(node));if (f) { // 如果该节点OUT发生了变化,将其所有后继节点添加到队列for (Node succ : cfg.getSuccsOf(node)) {worklist.addLast(succ);}}}}@Overrideprotected void doSolveBackward(CFG<Node> cfg, DataflowResult<Node, Fact> result) {throw new UnsupportedOperationException();}
}
4 运行与测试
你可以按我们在 Tai-e 框架(教学版)配置指南 中提到的方式来运行分析。在本作业中,Tai-e 对输入类的每个方法进行常量传播分析,并输出分析的结果,也就是每个语句的 OUT fact 所包含的数据流信息(变量的格上对应值)。
--------------------<Assign: void assign()> (constprop)--------------------
[0@L4] x = 1; null
[1@L5] x = 2; null
[2@L6] x = 3; null
[3@L7] x = 4; null
[4@L8] y = x; null
[5@L8] return; null
当你未完成作业的时候,OUT fact 的结果为 null
,当你完成了所有空缺代码后,分析的输出应当形如:
--------------------<Assign: void assign()> (constprop)--------------------
[0@L4] x = 1; {x=1}
[1@L5] x = 2; {x=2}
[2@L6] x = 3; {x=3}
[3@L7] x = 4; {x=4}
[4@L8] y = x; {x=4, y=4}
[5@L8] return; {x=4, y=4}
此外,Tai-e 将被分析方法的控制流图输出到 output/
文件夹,它们被存储为 .dot
文件,你可以用可视化工具Graphviz来查看这些控制流图。
我们为本次作业提供了测试驱动类 pascal.taie.analysis.dataflow.analysis.constprop.CPTest
,你可以按照 Tai-e 框架(教学版)配置指南 中描述的方式来测试你的实现是否正确。