Unit 3 Summary
by 刘登元
- Unit 3 Summary
- JML
- JML语言理论基础
- 表达式
- 方法规格
- 类型规格
- 应用工具链
- openjml
- SMT Solver
- JMLUnitNG
- JML语言理论基础
- 部署SMT Solver
- 部署JMLUnitNG
- 作业架构梳理
- 底层实现责任转移
- 模型建构策略
- Bug修复与体会
- 规格撰写和理解
- JML
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
为副作用(可修改的值);signals
、signals_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来设计的,但也有些许改动。以下提到的都是有所改动的部分:
底层实现责任转移
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个点。事实上已经用大量随机数据进行了对拍验证,但唯独没有考虑到这里的边界情况。所以吸取教训,一是要对边界条件特别留意,二是慎重优化算法,更加注重架构上的设计。
规格撰写和理解
说白了,规格就是用程式化的语言描述代码需要实现的目的;基础就是离散数学,理应没有什么难度。
但是对于复杂的功能,写出功能所必要的规格是十分容易的,但要写出充要的规格还是应该有相当难度的;从本单元第一次实验课的结果,就能看出这一点要想掌握还是有难度,尤其是要建立在他人已有的思想之上。
阅读规格也很简单,本质就是一些逻辑语句,只要正确断句就能得知其意义。但是要做到理解,还是要拥有概括和转化的思想。就用本次要求实现的函数queryStrongLink
和queryBlockSum
来说,规格书写的不是很难,但是要将其转化并产出一个高效的解决方法,而这个方法已经和规格本身相差甚远。所以,规格只是基础,上层的架构设计和算法实现才更为重要。