Unit 3 Summary

Unit 3 Summary

by 刘登元

目录
  • Unit 3 Summary
    • JML
      • JML语言理论基础
        • 表达式
        • 方法规格
        • 类型规格
      • 应用工具链
        • openjml
        • SMT Solver
        • JMLUnitNG
    • 部署SMT Solver
    • 部署JMLUnitNG
    • 作业架构梳理
      • 底层实现责任转移
      • 模型建构策略
    • Bug修复与体会
    • 规格撰写和理解

JML

JML语言理论基础

JML是一种用于描述设计规格的程式化语言,能够使得提出功能要求一方和程序开发一方能够无二义性地交流。此外,借助一些工具,还可以验证开发的功能是否符合了JML规格,从出发点来看,JML的用途是十分广泛的。

整体类似于离散数学使用的描述性语言,其语句由基本的表达式加上类似于Java语法的语句构成,而具体的规格则分为方法规格和类型规格。

表达式

  • \result指代函数返回值
  • \forall x; P(x); Q(x);表示对所有满足P(x)条件的x,都有Q(x)成立;
  • \exist x0; P(x0); Q(x0);类似,表示存在一个满足P(x0)条件的x0,有Q(x0)成立;
  • \max x; P(x); Q(x);\min x; P(x); Q(x);表示求所有的最小值;
  • \not_assigned(x0, x1, ...xn)当x0...xn均未被赋值时为真;
  • \nothing\everything表示空集/全集;
  • \sum x; P(x); Q(x);\product x; P(x); Q(x)表示对所有符合P(x)条件的Q(x)求和/乘积;

方法规格

  • requires为前置条件;
  • ensures为后置条件;
  • assignable为副作用(可修改的值);
  • signalssignals_only为需要抛出异常。

类型规格

  • invariant为整个类需要始终满足的约束;
  • constraint为类在一个方法被执行前后状态变化需要满足的约束。

应用工具链

openjml

一个可以对JML进行语法检查,并根据JML对代码进行静态检查或运行时检查的工具。必须在低版本JDK环境下(JDK8)环境下才能运行;并且其运行产生的结果比较艰涩难懂,经常报出莫名其妙的警告和错误;开发者提供的帮助文档也存在很多被提出但尚未解决的问题。应该说,其理想实现的功能是十分强大的,但还只是一个半成品,可用性较差。

SMT Solver

SMT Solver是openjml借助的用来进行逻辑验证的工具,常用的且被openjml集成的是z3检查器。但该检查器提供的API较为底层,一般需要C++/python语言与其配合使用;因此,要对JML进行SMT验证,还是需要借助openjml这样的工具。

JMLUnitNG

这个项目已经被开发者搁置半个多十年了。首先根据其测试原理,只是对边界条件进行验证,并不能验证普遍的情况,可以说能力还是比较有限。此外,其使用逻辑可以说是异常复杂,对一个项目进行测试要在命令行输入两三次不同的编译命令。从其当前的使用情况和开发者更新的频率来看,这并不是一个成功的、适合程序开发者使用的软件。


部署SMT Solver

在z3的项目仓库主页上下载最新的4.8.8-x64-ubuntu-16.04版本,放置在openjml的Solvers-linux目录下。由于Windows上已经安装了高版本JDK,我们在linux虚拟机上安装了JDK8环境,并在此做SMT验证。

又由于本次作业代码过于复杂,使用openjml可能会出现过多莫名其妙的错误,我们就用一些简单的代码来进行SMT验证。

我们书写如下代码:

import java.util.*;

public class Test{
    /*@
      @ requires args.length < 2;
      @ */
    public static void main(String[] args) {
		int a = test();
        System.out.println(args[8]);
    }

	/*@
	  @ ensures \result == 1;
	  @ */
	public static int test() {
		return 2 ;
	}
    
    private static int t = 0;
    /*@
      @ assignable \nothing; 
      @ */
    public static void TTT(int b) { t = b; }
}

在命令行输入命令~/openjml/openjml$ java -jar ./openjml.jar -esc -exec ./Solvers-linux/z3-4.8.8-x64-ubuntu-16.04/bin/z3 src/Test.java进行SMT验证。可见验证结果为:

src/Test.java:9: warning: The prover cannot establish an assertion (PossiblyTooLargeIndex) in method main
        System.out.println(args[8]);
                               ^
src/Test.java:16: warning: The prover cannot establish an assertion (Postcondition: src/Test.java:13: ) in method test
                return 2 ;
                ^
src/Test.java:13: warning: Associated declaration: src/Test.java:16:
          @ ensures \result == 1;
            ^

src/Test.java:23: warning: The prover cannot establish an assertion (Assignable: src/Test.java:21: ) in method TTT:  t
        public static void TTT(int b) { t = b; }
                                          ^
src/Test.java:21: warning: Associated declaration: src/Test.java:23:
          @ assignable \nothing;
            ^
5 warnings


SMT成功查出了代码中的三个错误:下标越界、返回值错误、在不能更改值的函数里修改了值。


部署JMLUnitNG

非常遗憾,将上面的Test.java文件中的逻辑错误全部改对后,即使是如此简单的代码,都能在构建JUnitNG时报出一堆错误,主要是找不到symbol。前文也提到了,JMLUnitNG作为一个半个多十年未更新的软件,和环境、JML等似乎都无法兼容,且检测能力有限,实在不适合我们调试程序使用。我们自行测试代码,还是应该通过自行构造测试用例,用黑盒测试(对拍)或者用JUnit等方式进行测试。JML作为一种辅助开发人员理解需求的工具,just leave it as it should be.


作业架构梳理

作业整体的架构还是根据官方的JML来设计的,但也有些许改动。以下提到的都是有所改动的部分:

Unit 3 Summary_第1张图片

底层实现责任转移

Network类管理的是所有人的整体;对于增删改查的底层操作,应该将其责任交予Person。本次作业中,用static方法在Person类中实现了:

  • bothLink:增加关系时,在两个Person中将互相加入各自的acquaintance,并更新所属组的信息;
  • getFa与setFa:实现并查集的查找代表元素和更改指针操作;
  • findShortPath:寻找最短路;
  • calcBlockSum:用floodFill求连通块数;
  • tarjan:求所有双连通分量。

此外,还额外定义了三个helpClass用于辅助存储:

  • BccComponent:用于存储一个双连通分量中的所有结点,实质是重载了hashCode和equals方法的HashSet;
  • BinIndexTree:存储年龄的树状数组,用于优化queryAgeSum操作的复杂度;
  • EdgeStack:用于tarjan函数存储遍历过程的边。

模型建构策略

整个问题其实就是在图中询问各种信息,并用各种方法实现。

我在研讨课中已经提到了本次作业有难度的三个算法:并查集维护连通性和连通块数、求单源最短路、tarjan求双连通分量,在这里就不再赘述了。研讨课的录播视频在这里。


Bug修复与体会

第一次作业由于对代码能力过于自信,导致bfs逻辑顺序错了两行,强测爆0,非常遗憾;主要是没有做测试。

第二次作业没有对规格中1111的限制进行特殊判断,导致强测爆了4个点;显然评测的答案和给出的JML手册产生了冲突,只能说JML的思想已经领悟了,具体这一次作业的一二十分不足在意。

第三次作业在树状数组中写错了0的边界条件,将大于号写成了大于等于,结果强测爆了5个点。事实上已经用大量随机数据进行了对拍验证,但唯独没有考虑到这里的边界情况。所以吸取教训,一是要对边界条件特别留意,二是慎重优化算法,更加注重架构上的设计。


规格撰写和理解

说白了,规格就是用程式化的语言描述代码需要实现的目的;基础就是离散数学,理应没有什么难度。

但是对于复杂的功能,写出功能所必要的规格是十分容易的,但要写出充要的规格还是应该有相当难度的;从本单元第一次实验课的结果,就能看出这一点要想掌握还是有难度,尤其是要建立在他人已有的思想之上。

阅读规格也很简单,本质就是一些逻辑语句,只要正确断句就能得知其意义。但是要做到理解,还是要拥有概括和转化的思想。就用本次要求实现的函数queryStrongLinkqueryBlockSum来说,规格书写的不是很难,但是要将其转化并产出一个高效的解决方法,而这个方法已经和规格本身相差甚远。所以,规格只是基础,上层的架构设计和算法实现才更为重要。

你可能感兴趣的:(Unit 3 Summary)