【静态分析】软件分析课程实验A2-常量传播和Worklist求解器

Tai-e官网:

概述 | Tai-e

参考:

https://www.cnblogs.com/gonghr/p/17979609


1 作业导览

  • 为 Java 实现常量传播算法。
  • 实现一个通用的 worklist 求解器,并用它来解决一些数据流分析问题,例如本次的常量传播。

在本次实验作业中,你要在 Tai-e 框架下实现常量传播算法和 worklist 求解器的关键部分。

2 实现常量传播

2.1 分析范围

在此次作业中,你需要实现针对 int 类型的常量传播。注意,在 Java 中,booleanbytecharshort 类型在运行时实际上都以 int 值的形式进行表示和计算,因此你的分析算法也应当能处理这些类型。其他基本数据类型(例如 longfloatdouble)以及引用类型(例如 class types、array types)不在此次作业的考虑范围内,所以你可以在分析时忽略它们的值。

至于语句的处理,你只需要关注等号左侧为变量且右侧只能是如下几类表达式的赋值语句:

  • 常量,如 x = 1
  • 变量,如 x = y
  • 二元运算表达式,如 x = a + bx = 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 = yx = m(...) 的语句)。这个类很简单。你可以通过阅读源码来决定如何使用它。

  • pascal.taie.analysis.dataflow.analysis.DataflowAnalysis

    这是具体数据流分析算法需要实现的接口。和作业 1 一样,它会被求解器调用。在本次作业中你只需要关注前 5 个 API。这些 API 会被你在后面完成的 worklist 求解器调用。

  • pascal.taie.analysis.dataflow.analysis.constprop.Value

    这个类表示了常量分析中格上的抽象值。格的定义见【静态分析】静态分析笔记04 - 数据流分析(理论)-CSDN博客

  • 它的代码和注释解释了它的用法。你应该用下列的静态方法获取格上抽象值(即该类的实例):

    • 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 类型的变量)。

此外,你还需要实现下面两个辅助方法:

提示

  1. 作业 1 一样,我们对 meetInto() 的设计比较特殊。如果你不记得了,可以再回顾一下作业 1 文档的相关部分。
  2. 条件表达式(如 a == b)的值由 0(若为 False)和 1(若为 True)来表示。
  3. 对于除以 0 的情况(出现在 /% 中),我们规定结果为 UNDEF。例如,对于 x = a / 0x 的值将会是 UNDEF

算法伪代码描述

  • newBoundaryFact :负责创建和初始化虚拟结点的 Data Flow Fact。但是注意要把方法参数初始化为 NAC 。 根据题目要求,不是所有类型的参数都考虑,只有能转换成 int 类型的参数才考虑,所以别忘了用 canHoldInt 过滤一下。
  • newInitialFact :负责创建和初始化控制流图中除了 EntryExit 之外的结点的 Data Flow Fact。控制流图中一个结点的 INOUT 分别对应一个 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 :负责表达式值的计算。

表达式分三种情况讨论

  1. 常量:直接赋值。
  2. 变量:获取变量的值再赋值。
  3. 二元运算:针对共 12 中运算分别处理。
java 复制代码
/*
 * 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 extends
        AbstractDataflowAnalysis<Stmt, CPFact> {

    public static final String ID = "constprop";

    public ConstantPropagation(AnalysisConfig config) {
        super(config);
    }

    @Override
    public boolean isForward() {
        return true;
    }

    @Override
    public CPFact newBoundaryFact(CFG<Stmt> cfg) {
        // TODO - finish me
        CPFact cpFact = new CPFact();
        for (Var param : cfg.getIR().getParams()) {
            if (canHoldInt(param)) {                   // 只考虑可转换int类型的参数
                cpFact.update(param, Value.getNAC());  // 建立参数到格上值(NAC)的映射
            }
        }
        return cpFact;
    }

    @Override
    public CPFact newInitialFact() {
        // TODO - finish me
        return new CPFact();
    }

    @Override
    public void meetInto(CPFact fact, CPFact target) {
        // TODO - finish me
        for (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 me
        if (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();
        }
    }

    @Override
    public boolean transferNode(Stmt stmt, CPFact in, CPFact out) {
        // TODO - finish me
        CPFact 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 me
        if (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 的值将会是 UNDEF
                    ArithmeticExp.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 中你需要了解的类

与迭代求解器类似,你需要清楚 DataflowResultCFGSolver 的相关用法(我们已经在作业 1 中介绍过了)。除此之外,你还需要知道:

  • pascal.taie.analysis.dataflow.solver.WorkListSolver

    该类继承了 Solver 类,实现了 worklist 算法。它的实现是不完整的,在本此作业中你需要完成它。

3.2 你的任务 [重点!]

你的第二个任务是完成下述两个 API 的实现:

考虑到常量传播是一个前向分析,你只需要关注前向分析相关的方法。initializeForward() 方法的具体实现参考如图前三行。

doSolveForward() 则包含了你要实现的算法的主体部分。

  • Solver.initializeForward(CFG,DataflowResult)
  • WorkListSolver.doSolveForward(CFG,DataflowResult)

提示

  1. 讲义中的 worklist 算法通过比较 old_OUTOUT[B] 来决定后继节点是否应当加入 worklist 中,这个做法比较低效。Tai-e 中 DataflowAnalysis.transferNode() 会返回此次 transfer 是否改变了 OUT fact。利用好这一点可以避免多余的判断;
  2. 作业 1 类似,不要忘了在 Solver.initializeForward() 中初始化每个语句的 INOUT

initializeForward :初始化所有的 Data Flow Fact

doSolveForward :负责实现 Worklist 求解器具体步骤。

java 复制代码
protected void initializeForward(CFG<Node> cfg, DataflowResult<Node, Fact> result) {
        // TODO - finish me
        result.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());
        }
    }
java 复制代码
/*
 * 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);
    }

    @Override
    protected void doSolveForward(CFG<Node> cfg, DataflowResult<Node, Fact> result) {
        // TODO - finish me
        ArrayDeque<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做meet
                analysis.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);
                }
            }
        }
    }

    @Override
    protected 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 框架(教学版)配置指南 中描述的方式来测试你的实现是否正确。

相关推荐
Dream_Snowar1 分钟前
速通Python 第三节
开发语言·python
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
信号处理学渣1 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客1 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
jasmine s1 小时前
Pandas
开发语言·python
biomooc2 小时前
R 语言 | 绘图的文字格式(绘制上标、下标、斜体、文字标注等)
开发语言·r语言
骇客野人2 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
404NooFound2 小时前
Python轻量级NoSQL数据库TinyDB
开发语言·python·nosql
用余生去守护3 小时前
python报错系列(16)--pyinstaller ????????
开发语言·python