JML使用基礎——利用openjml和JMLunit聯合操作——SMT solver驗證

目錄

  • 從DBC到JML
  • SMT solver 使用
  • JML toolchain的可視化輸出 和我的測試結果
  • 規格的完善策略
  • 架構設計
  • debug情況
  • 心得體會

一、從DBC到JML

契約式設計(Design by Contract)是一種開發軟件的新思路。不妨通過商業活動的中真實的Contract(契約)來理解這個例子:

  • 供應商必須提供某種產品(這是供應商的義務),並且有權期望客戶付費(這是供應商的權利)。
  • 客戶必須支付費用(這是客戶的義務),並且有權得到產品(這是客戶的權利)。
  • 雙方必須滿足應用於合約的某些義務,如法律和規定。

那我們可以從程序設計的角度看需要哪些契約呢:

  • 可接受和不可接受的輸入的值或類型,以及它們的含義
  • 返回的值或類型,以及它們的含義
  • 錯誤和異常的值或類型,以及它們的含義
  • 副作用
  • 先驗條件
  • 後驗條件
  • 不變性
  • (不太常見)性能保證,例如所需的時間和空間。我在後文中建立了性能相關的規格約定模式

可以說JML是爲DBC而生的(或者在學術上稱之爲DBC語言)。

我們從JML的原生語法來看,就非常符合契約設計的理念:

  1. requires 描述先驗條件
  2. ensures 描述後驗條件
  3. old 描述和消除副作用
  4. exceptional_behavior 描述錯誤和異常

所以說JML是教學和學術研究中對於DBC理論探索的一個利器。

形式化<->可以消除歧義。

形式化<->可以被程序讀取!

形式化<->可以自動分析和推導!

這樣,不僅寫代碼的人可以準確讀懂需求,測試人員可以從測試上透視功能!

二、SMT solver 使用

筆者的java SMT代碼一覽,採用Microsoft Z3 SMT solver

    void prove(Context ctx, BoolExpr f, boolean useMBQI) throws TestFailedException
    {
        BoolExpr[] assumptions = new BoolExpr[0];
        prove(ctx, f, useMBQI, assumptions);
    }

    void prove(Context ctx, BoolExpr f, boolean useMBQI,
            BoolExpr... assumptions) throws TestFailedException
    {
        System.out.println("Proving: " + f);
        Solver s = ctx.mkSolver();
        Params p = ctx.mkParams();
        p.add("mbqi", useMBQI);
        s.setParameters(p);
        for (BoolExpr a : assumptions)
            s.add(a);
        s.add(ctx.mkNot(f));
        Status q = s.check();

        switch (q)
        {
        case UNKNOWN:
            System.out.println("Unknown because: " + s.getReasonUnknown());
            break;
        case SATISFIABLE:
            throw new TestFailedException();
        case UNSATISFIABLE:
            System.out.println("OK, proof: " + s.getProof());
            break;
        }
    }

    void disprove(Context ctx, BoolExpr f, boolean useMBQI) 
        throws TestFailedException
    {
        BoolExpr[] a = {};
        disprove(ctx, f, useMBQI, a);
    }

    void disprove(Context ctx, BoolExpr f, boolean useMBQI,
            BoolExpr... assumptions) throws TestFailedException
    {
        System.out.println("Disproving: " + f);
        Solver s = ctx.mkSolver();
        Params p = ctx.mkParams();
        p.add("mbqi", useMBQI);
        s.setParameters(p);
        for (BoolExpr a : assumptions)
            s.add(a);
        s.add(ctx.mkNot(f));
        Status q = s.check();

        switch (q)
        {
        case UNKNOWN:
            System.out.println("Unknown because: " + s.getReasonUnknown());
            break;
        case SATISFIABLE:
            System.out.println("OK, model: " + s.getModel());
            break;
        case UNSATISFIABLE:
            throw new TestFailedException();
        }
    }

跑一個簡單的數獨驗證先測試一下SMT 在本地有效(代碼來自官方template,有很多改動)

    void sudokuExample(Context ctx) throws TestFailedException
    {
        System.out.println("SudokuExample");
        Log.append("SudokuExample");

        // 9x9 matrix of integer variables
        IntExpr[][] X = new IntExpr[9][];
        for (int i = 0; i < 9; i++)
        {
            X[i] = new IntExpr[9];
            for (int j = 0; j < 9; j++)
                X[i][j] = (IntExpr) ctx.mkConst(
                        ctx.mkSymbol("x_" + (i + 1) + "_" + (j + 1)),
                        ctx.getIntSort());
        }

        // each cell contains a value in {1, ..., 9}
        BoolExpr[][] cells_c = new BoolExpr[9][];
        for (int i = 0; i < 9; i++)
        {
            cells_c[i] = new BoolExpr[9];
            for (int j = 0; j < 9; j++)
                cells_c[i][j] = ctx.mkAnd(ctx.mkLe(ctx.mkInt(1), X[i][j]),
                        ctx.mkLe(X[i][j], ctx.mkInt(9)));
        }

        // each row contains a digit at most once
        BoolExpr[] rows_c = new BoolExpr[9];
        for (int i = 0; i < 9; i++)
            rows_c[i] = ctx.mkDistinct(X[i]);

        // each column contains a digit at most once
        BoolExpr[] cols_c = new BoolExpr[9];
        for (int j = 0; j < 9; j++)
            cols_c[j] = ctx.mkDistinct(X[j]);

        // each 3x3 square contains a digit at most once
        BoolExpr[][] sq_c = new BoolExpr[3][];
        for (int i0 = 0; i0 < 3; i0++)
        {
            sq_c[i0] = new BoolExpr[3];
            for (int j0 = 0; j0 < 3; j0++)
            {
                IntExpr[] square = new IntExpr[9];
                for (int i = 0; i < 3; i++)
                    for (int j = 0; j < 3; j++)
                        square[3 * i + j] = X[3 * i0 + i][3 * j0 + j];
                sq_c[i0][j0] = ctx.mkDistinct(square);
            }
        }

        BoolExpr sudoku_c = ctx.mkTrue();
        for (BoolExpr[] t : cells_c)
            sudoku_c = ctx.mkAnd(ctx.mkAnd(t), sudoku_c);
        sudoku_c = ctx.mkAnd(ctx.mkAnd(rows_c), sudoku_c);
        sudoku_c = ctx.mkAnd(ctx.mkAnd(cols_c), sudoku_c);
        for (BoolExpr[] t : sq_c)
            sudoku_c = ctx.mkAnd(ctx.mkAnd(t), sudoku_c);

        // sudoku instance, we use '0' for empty cells
        int[][] instance = { { 0, 0, 0, 0, 9, 4, 0, 3, 0 },
                { 0, 0, 0, 5, 1, 0, 0, 0, 7 }, { 0, 8, 9, 0, 0, 0, 0, 4, 0 },
                { 0, 0, 0, 0, 0, 0, 2, 0, 8 }, { 0, 6, 0, 2, 0, 1, 0, 5, 0 },
                { 1, 0, 2, 0, 0, 0, 0, 0, 0 }, { 0, 7, 0, 0, 0, 0, 5, 2, 0 },
                { 9, 0, 0, 0, 6, 5, 0, 0, 0 }, { 0, 4, 0, 9, 7, 0, 0, 0, 0 } };

        BoolExpr instance_c = ctx.mkTrue();
        for (int i = 0; i < 9; i++)
            for (int j = 0; j < 9; j++)
                instance_c = ctx.mkAnd(
                        instance_c,
                        (BoolExpr) ctx.mkITE(
                                ctx.mkEq(ctx.mkInt(instance[i][j]),
                                        ctx.mkInt(0)), ctx.mkTrue(),
                                ctx.mkEq(X[i][j], ctx.mkInt(instance[i][j]))));

        Solver s = ctx.mkSolver();
        s.add(sudoku_c);
        s.add(instance_c);

        if (s.check() == Status.SATISFIABLE)
        {
            Model m = s.getModel();
            Expr[][] R = new Expr[9][9];
            for (int i = 0; i < 9; i++)
                for (int j = 0; j < 9; j++)
                    R[i][j] = m.evaluate(X[i][j], false);
            System.out.println("Sudoku solution:");
            for (int i = 0; i < 9; i++)
            {
                for (int j = 0; j < 9; j++)
                    System.out.print(" " + R[i][j]);
                System.out.println();
            }
        } else
        {
            System.out.println("Failed to solve sudoku");
            throw new TestFailedException();
        }
    }

用SMT solver 很難測試像圖論這樣的問題,所以我的策略是先測試一個經典的數獨問題,如上。

此外我做了兩個測試:

  1. 驗證連通塊個數實驗

這種驗證確實非常高級非常有效,但是寫代碼還是相當長的,(包括生成代碼和模板代碼)。

測試用代碼如下


    private BoolExpr generate(BinopExpr expr) {
        BoolExpr ret = null;
        if(expr instanceof ConditionExpr){
            //get two sides and the operator
            ConditionExpr condExpr = (ConditionExpr)expr;
            //lhs can either be constant or a local
            //12-29-14, not right now when we 
            //have more general formula
            Value lhs = condExpr.getOp1();
            IntExpr lhsExpr = evaluateExpr(lhs);

            //rhs can also be an arithmetic expression
            //from converting assignments to equality
            Value rhs = condExpr.getOp2();
            ArithExpr rhsExpr = null;
            //add conditionals here first to check
            if(rhs instanceof BinopExpr){
                BinopExpr rhsBinop = (BinopExpr) rhs;
                IntExpr lhsArith = evaluateExpr(rhsBinop.getOp1());
                IntExpr rhsArith = evaluateExpr(rhsBinop.getOp2());
                //now determine the operator add, sub, mult
                try {
                    if(rhsBinop instanceof AddExpr){
                        ArithExpr[] operands = new ArithExpr[]{lhsArith, rhsArith};
                        rhsExpr = ctx.MkAdd(operands);
                    } else if (rhsBinop instanceof SubExpr){
                        ArithExpr[] operands = new ArithExpr[]{lhsArith, rhsArith};
                        rhsExpr = ctx.MkSub(operands);
                    } else if (rhsBinop instanceof MulExpr){
                        ArithExpr[] operands = new ArithExpr[]{lhsArith, rhsArith};
                        rhsExpr = ctx.MkMul(operands);
                    } else if (rhsBinop instanceof DivExpr){
                        rhsExpr = ctx.MkDiv(lhsArith, rhsArith);
                    } else if(rhsBinop instanceof RemExpr){
                        rhsExpr = ctx.MkMod(lhsArith, rhsArith);
                    } else if (rhsBinop instanceof ShrExpr){
                        //can only handle when rhs,i.e., y is not a variable
                        // x >> y = x / (2^y)
                        if(rhsArith.IsArithmeticNumeral()){
                            IntNum number = (IntNum)rhsArith;
                            rhsArith = ctx.MkInt(1<<number.Int()); // this is 2^y
                            rhsExpr = ctx.MkDiv(lhsArith, rhsArith);
                        } else {
                            System.out.println("Rhs in ShrExpr is not a number " + rhsArith.getClass());
                            System.exit(2);
                        }
                    } else if(rhsBinop instanceof ShlExpr){
                        //can only handle when rhs, i.e., u is not a variable
                        // x << y = x * (2^y)
                        if(rhsArith.IsArithmeticNumeral()){
                            IntNum number = (IntNum)rhsArith;
                            rhsArith = ctx.MkInt(1<<number.Int()); // this is 2^y
                            ArithExpr[] operands = new ArithExpr[]{lhsArith, rhsArith};
                            rhsExpr = ctx.MkMul(operands);
                        } else {
                            System.out.println("Rhs in ShlExpr is not a number " + rhsArith.getClass());
                            System.exit(2);
                        }

                    } else {
                        System.out.println("Cannot process rhsBinop " + rhsBinop.getClass());
                        System.exit(2);
                    }
                } catch (Z3Exception e) {
                    e.printStackTrace();
                }
            } else if (rhs instanceof NegExpr){
                try {
                    ArithExpr[] operands;
                    operands = new ArithExpr[]{ctx.MkInt(0), evaluateExpr(((NegExpr)rhs).getOp())};
                    rhsExpr = ctx.MkSub(operands);
                } catch (Z3Exception e) {
                    e.printStackTrace();
                }
            } else  {
                rhsExpr = evaluateExpr(rhs);
            }

            //now generate the condition
            try {
                if(expr instanceof EqExpr){
                    ret = ctx.MkEq(lhsExpr, rhsExpr);
                } else if (expr instanceof GeExpr){
                    ret = ctx.MkGe(lhsExpr, rhsExpr);
                } else if (expr instanceof GtExpr){
                    ret = ctx.MkGt(lhsExpr, rhsExpr);
                } else if (expr instanceof LeExpr){
                    ret = ctx.MkLe(lhsExpr, rhsExpr);
                } else if (expr instanceof LtExpr){
                    ret = ctx.MkLt(lhsExpr, rhsExpr);
                } else if (expr instanceof NeExpr){
                    ret = ctx.MkNot(ctx.MkEq(lhsExpr, rhsExpr));
                }
            } catch (Z3Exception e) {
                e.printStackTrace();
            }

        } else if (expr instanceof OrExpr){
            BoolExpr lhs = generate((BinopExpr)expr.getOp1());
            BoolExpr rhs = generate((BinopExpr)expr.getOp2());
            try {
                ret = ctx.MkOr(new BoolExpr[]{lhs, rhs});
            } catch (Z3Exception e) {
                e.printStackTrace();
            }
        } else if (expr instanceof AndExpr){
            BoolExpr lhs = generate((BinopExpr)expr.getOp1());
            BoolExpr rhs = generate((BinopExpr)expr.getOp2());
            try {
                ret = ctx.MkAnd(new BoolExpr[]{lhs, rhs});
            } catch (Z3Exception e) {
                e.printStackTrace();
            }
        } else {
            //something else that we don't handle yet :(
            System.out.println("Cannot process " + expr);
            System.exit(2);
        }
        return ret;
    }


public boolean disjoint_solve(BoolExpr z3Formula){
    boolean ret = true;
    try {
        Solver solver = ctx.MkSolver();
        Params p = ctx.MkParams();
        p.Add("soft_timeout", timeout);
        solver.setParameters(p);
        solver.Assert(z3Formula);
        Status result = solver.Check();
        if(result.equals(Status.SATISFIABLE)){
            ret = true;
        } else if (result.equals(Status.UNSATISFIABLE)){
            ret = false;
        } else {
            //unknown
            System.out.println("Warning: " + result + " for " + z3Formula);
        }
    } catch (Z3Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return ret;
}
  1. 驗證多個最短路算法的等價性

此外,部分代碼在python下(驗證)_

三、JML toolchain 用於測試

目前JML的工具有這些:

  • ESC/Java2 1, an extended static checker which uses JML annotations to perform more rigorous static checking than is otherwise possible.
  • OpenJML declares itself the successor of ESC/Java2.
  • Daikon, a dynamic invariant generator.
  • KeY, which provides an open source theorem prover with a JML front-end and an Eclipse plug-in (JML Editing) with support for syntax highlighting of JML.
  • Krakatoa, a static verification tool based on the Why verification platform and using the Coq proof assistant.
  • JMLEclipse, a plugin for the Eclipse integrated development environment with support for JML syntax and interfaces to various tools that make use of JML annotations.
  • Sireum/Kiasan, a symbolic execution based static analyzer which supports JML as a contract language.
  • JMLUnit, a tool to generate files for running JUnit tests on JML annotated Java files.
  • TACO, an open source program analysis tool that statically checks the compliance of a Java program against its Java Modeling Language specification.
  • VerCors verifier

但是這些工具並不能滿足我的開發和證明要求,由於我後續加入,我自行開發了一個命令行工具 Advanced JML check with TesTNG ( AJCT )。(功能很不完善)。

其原理其實是OpenJML + JMLUnitNG + jprofiler + Python + Bash 的一個組合腳本。

根據我自己的類生成的測試代碼截圖如下:

生成結果如下

.
├── AJCT
├── Main.java
├── Mydisgraph.java
├── Mydisgraph_sssp_int_euler_path_gen.class
├── Mydisgraph_sssp_int_euler_path_gen.java
├── Mydisgraph_sssp_int_general_gen.class
├── Mydisgraph_sssp_int_general_gen.java
├── Mydisgraph_sssp_int_rebuild_gen.class
├── Mydisgraph_sssp_int_rebuild_gen.java
├── Mydisgraph_sssp_int_shortest_path_tree_gen.class
├── Mydisgraph_sssp_int_shortest_path_tree_gen.java
├── MyGraph_containsEdge_int_int.class
├── MyGraph_containsEdge_int_int.java
├── MyGraph_containsNode_int.class
├── MyGraph_containsNode_int.java
├── MyGraph_getnodelabel_int.class
├── MyGraph_getnodelabel_int.java
├── MyGraph_getShortestPathLength_int_int.class
├── MyGraph_getShortestPathLength_int_int.java
├── MyGraph_isConnected_int_int.class
├── MyGraph_isConnected_int_int.java
├── MyGraph.java
├── MyNode.java
├── MyPathContainer.java
├── MyPath.java
├── MyRailwaySystem_getFa_int.class
├── MyRailwaySystem_getFa_int.java
├── MyRailwaySystem_getLeastTicketPrice_int_int.class
├── MyRailwaySystem_getLeastTicketPrice_int_int.java
├── MyRailwaySystem_getLeastTransferCount_int_int.class
├── MyRailwaySystem_getLeastTransferCount_int_int.java
├── MyRailwaySystem_getLeastUnpleasantValue_int_int.class
├── MyRailwaySystem_getLeastUnpleasantValue_int_int.java
├── MyRailwaySystem.java
├── MyRailwaySystem_merge_int_int.class
└── MyRailwaySystem_merge_int_int.java

測試結果截圖如下:
性能測試結果如下:

四、規格完善策略

我發現了現有的規格體系的一個缺點:即無法保證描述方法的時空間複雜度,因此,我在此基礎上,加入了關於複雜度的描述規格,用於測試我自己的代碼。

(目前,我的規格只能測試圖的最短路算法和圖論其他基本算法。)

我的複雜度規格描述策略如下:

  1. pre-condition: 表示算法中某一些集合,和他們的大小範圍。
  2. parameter: 表示參數屬於算法中的哪一個集合,和他們的大小範圍。
  3. time complexity: 用時間複雜度的標準形式,要求有 online, offline, worst-case幾個關鍵描述。(省略大O記號)
  4. space complexity: 用空間複雜度的標準形式。

這部分的開發過程有很多困難,由於還沒有徹底完善,不方便開源(還在做進一步測試)。

引入這個規格的考慮有如下考量:

  1. 後續重構要考量該接口是否會喪失原有性能、導致各種問題(進程不同步、需求無法滿足)。

  2. 對調用者友好,能不用透視代碼,而從性能和功能兩個層次考量是否調用該方法、調用的條件是什麼。

  3. 對驗證有效,工程問題的正確性和效率(開發效率、測試效率)都和基本的性能要求分不開,能提前做好性能的規格設計,在沒有遇到性能問題之前大概率無需顧慮。

  4. 性能規格可以做理論推導、可以通過調用方法和後續方法的性能規格推導該規格的bound,做到防禦性設計

關於我的性能測試報告可以看我的優化博客,在此只做簡要的敘述。

在性能測試階段,利用多種輸入情況對程序的運行時間情況做擬合,得到的表達式和規格表達式對比,從而得到驗證複雜度的效果。

我的驗證結果如下

利用
f(n)g(n)f(n) - g(n) 這樣的表達式表達:修改複雜度爲f(n)f(n),詢問複雜度爲g(n)g(n)

algo \ case general gen rebuild gen Euler path gen shortest path tree gen
A-star-algorithm O(n2)O(n+m)O(n^2)-O(n+m) O(n2)O(n+m)O(n^2)-O(n+m) O(n2)O(n+m)O(n^2)-O(n+m) O(n+m)O(n+m)O(n+m)-O(n+m)
O(n2)O(n^2) transfer line algorithm O(n2)O(m)O(n^2)-O(m) O(n2)O(m)O(n^2)-O(m) O(n2)O(m)O(n^2)-O(m) O(n2)O(m)O(n^2)-O(m)
my algorithm $O(n)-O(n + m \log n) $ $O(n)-O(n \log n + m) $ $O(n)-O(n \log n + m) $ $O(n)-O(n + m) $
floyd algorithm $O(n^3)-O(1) $ $O(n^3)-O(1) $ $O(n^3)-O(1) $ $O(n^3)-O(1) $

五、架構設計

這次的架構設計有助教和老師的經驗在裏面,我們自己設計的部分不多!老師和助教的前瞻性充分在這個單元體現出來。

第一次的架構,非常基本

第二次的架構,非常基本

第三次,抽象出了Mydisgraph 封裝出了最短路算法

用於:

  1. 隨時調試性能
  2. 隨時替換其他算法
  3. 封裝共性代碼

三次的架構一脈相成在架構上我改進了以前的開發策略:

  1. 學習相關的模式、接口設計、畫流程圖 (確定如何對象的屬性)
  2. 模塊綁定、避免重構
  3. 做頭腦風暴,考察多種測試數據和思路,並完成代碼
  4. 測試數據構造和覆蓋性測試
  5. 代碼回顧和思考

由此我實現了代碼的複用和解耦。

六、debug情況

我發現了一個核心bug,濫用hashcode

有同學的第一次代碼在equals方法內直接用hashcode判斷,我使用了中間相遇的思路攻擊了他的hash算法:

示例如下:

PATH_ADD 1 1 1
PATH_ADD 1 29792

這兩條路徑具有相同的hashcode。

這裏其實反映了要如何使用hashcode的問題,hashcode的衝突要用equals去避免,值得同學記憶。

七、心得體會

經過這一段時間對JML規格的閱讀以及上次上機時自己真正嘗試寫JML規格,我深深感受到了JML語言的重要性。只有使用JML語言,在進行程序編寫,特別是不同人組隊完成一個大型程序的編寫時,纔可以在最大程度上保證不同人完成在代碼可以融合在一起,而不會產生各種各樣奇妙的bug。

在這一單元的學習後,對前置條件,後置條件,副作用,有了比較好的理解,這是一個方法行爲的核心,有了這樣的規範,程序員間可以在架構層面上進行交流,也就可以在編碼前期,架構設計時期進行交流,減少實現時的錯誤。這是我在這一單元最爲印象深刻的,之後使用JML,我會在代碼的註釋中寫明前置條件,後置條件,以及副作用,保持一個良好的習慣。

感謝OO感謝OO課程教會了我從規格層次審視代碼,從更高的角度去設計去思考,做代碼的主人。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章