571
社区成员
发帖
与我相关
我的任务
分享程序中总是可能存在错误,这些错误可以在运行时得到解决。但若是在一些比较特殊的场合中,无法提供多次动态运行的环境,那么可以在静态检查时尽可能的检查出程序中的错误,就变得至关重要了。本项目尝试从Rust语言出发,生成等价的Fstar程序,利用Fstar编译器来检测程序中可能出现的错误。
Rust是一门系统编程语言 ,专注于安全 ,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似 ,但是设计者想要在保证性能的同时提供更好的内存安全。创建这个新语言的目的是为了解决一个顽疾:软件的演进速度大大低于硬件的演进,软件在语言级别上无法真正利用多核计算带来的性能提升。Rust是针对多核体系提出的语言,并且吸收一些其他动态语言的重要特性,比如不需要管理内存,比如不会出现Null指针等等。
Rust致力于成为优雅解决高并发和高安全性系统问题的编程语言 ,适用于大型场景,即创造维护能够保持大型系统完整的边界。这就导致了它强调安全,内存布局控制和并发的特点。标准Rust性能与标准C++性能不相上下。
Fstar是一种通用函数式编程语言,其效果旨在进行程序验证。它将SMT支持的演绎验证工具的自动化与基于依赖类型的证明助手的表达能力结合起来。经过验证后,可以将F*程序提取为高效的OCaml、F#、C、WASM或ASM代码。这可以验证实际应用程序的功能正确性和安全性。F*的主要持续使用案例是在Everest项目中为整个HTTPS堆栈构建一个经过验证的插入式替换。这包括TLS 1.2和1.3以及底层加密原语的验证实现。
F*的类型系统包括依赖类型、一元效应、精化类型和最弱的前提演算。总之,这些特性允许表达程序的精确而紧凑的规范。F*类型检查器旨在结合SMT解决方案和交互式证明来证明程序符合其规范。
项目整体流程如下图所示:

由Rust程序到Fstar程序的转换通过遍历Rust程序的抽象语法树来完成,在完成转换过程后,把生成的Fstar程序交给Fstar编译器可以得到程序执行结果是否正确。
以下是斐波那契数列的一个简单实现,如何验证该函数正确性呢?
fn fib(x: i32) -> i32
{
if x == 0 || x == 1
{
1
}
else if x == 2
{
1
}
else
{
fib(x-1) + fib(x-2)
}
}
将源代码作为输入,转换成的Fstar程序如下
module test
open FStar.Mul
let i32 = x:int{ -2147483648 <= x && x <= 2147483647}
let i64 = x:int{-9223372036854775808 <= x && x<= 9223372036854775807}
let rec fib ( x : i32 ) : ret: i32 =
if x = 0 || x = 1 then 1 else if x = 2 then 1 else fib ( x - 1 ) + fib ( x - 2 )
运行结果如下
Fstar编译器无法验证该函数正确性,因为作为32位整型数作为输入无法保证不会溢出。若将rust代码修改为:
///
///
fn fib(x: int) -> int
{
if x == 0 || x == 1
{
1
}
else if x == 2
{
1
}
else
{
fib(x-1) + fib(x-2)
}
}
经转换后fstar源代码如下:
(在Fstar中Int代表无穷的整型,即负无穷到正无穷)
module test
open FStar.Mul
let i32 = x:int{ -2147483648 <= x && x <= 2147483647}
let i64 = x:int{-9223372036854775808 <= x && x<= 9223372036854775807}
let rec fib ( x : int ) : ret: int =
if x = 0 || x = 1 then 1 else if x = 2 then 1 else fib ( x - 1 ) + fib ( x - 2 )
再次运行fstar编译器:
这一次失败的原因是无法验证递归函数是否可以终止,原因是当输入x为小于0的值时该函数永远无法结束。
再次修改rust代码并编译
///{x>=0}
///
fn fib(x: int) -> int
{
if x == 0 || x == 1
{
1
}
else if x == 2
{
1
}
else
{
fib(x-1) + fib(x-2)
}
}
open FStar.Mul
let i32 = x:int{ -2147483648 <= x && x <= 2147483647}
let i64 = x:int{-9223372036854775808 <= x && x<= 9223372036854775807}
let rec fib ( x : int {x>=0} ) : ret: int =
if x = 0 || x = 1 then 1 else if x = 2 then 1 else fib ( x - 1 ) + fib ( x - 2 )
通过将x的类型限制为大于等于0的整型来解决该错误,这次验证成功通过。
(2)验证max(x,y) == x 或者max(x,y) == y
rust代码:
///
///
///
fn max(x:i32, y:i32)->i32
{
if x < y
{
y
}
else
{
x
}
}
///
///
///
fn test(x:i32, y:i32)->unit{
assert!((max(x,y)) == x || (max(x,y)) == y);
}
编译后fstar代码:
open FStar.Mul
let i32 = x:int{ -2147483648 <= x && x <= 2147483647}
let i64 = x:int{-9223372036854775808 <= x && x<= 9223372036854775807}
let rec max ( x : i32 ) ( y : i32 ) : ret: i32 =
if x < y then y else x
let rec test ( x : i32 ) ( y : i32 ) : unit =
assert( (max x y ) = x || (max x y ) = y)
执行结果:

组合模式使得用户对单个对象和组合对象的使用具有一致性。
有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
组合模式让你可以优化处理递归或分级数据结构。有许多关于分级数据结构的例子,使得组合模式非常有用武之地。关于分级数据结构的一个普遍性的例子是你每次使用电脑时所遇到的:文件系统。文件系统由目录和文件组成。每个目录都可以装内容。目录的内容可以是文件,也可以是目录。按照这种方式,计算机的文件系统就是以递归结构来组织的。如果你想要描述这样的数据结构,那么你可以使用组合模式Composite。
定义
(GoF《设计模式》):将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
在rust中,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming。元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。一个函数标签必须声明函数参数个数和类型。相比之下,宏能够接受不同数量的参数:用一个参数调用 println!("hello") 或用两个参数调用 println!("hello {}", name) 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。实现一个宏而不是一个函数的缺点是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
Rust中宏调用语法树节点定义如下:
pub struct MacCall {
pub path: Path,
pub args: P<MacArgs>,
pub prior_type_ascription: Option<(Span, bool)>,
}
其中调用宏名记录在Path变量中,而参数保留在MacArgs中,其中MacArgs定义如下:
pub enum MacArgs {
Empty,
Delimited(DelimSpan, MacDelimiter, TokenStream),
Eq(Span, Token),
}
pub fn trees(&self) -> Cursorⓘ
其中对于TokenStream,可以调用 trees方法来获得一个对于TokenStream中各个TokenTree的迭代器,TokenTree定义如下
pub enum TokenTree {
Token(Token),
Delimited(DelimSpan, DelimToken, TokenStream),
}
TokenTree是一个枚举类型,它要么是一个Token节点,此时相当于一个叶子节点,此时它携带了宏调用信息,要么是一个Delimited节点,其中的TokenStream又可以调用trees方法来获得一个TokenTree的迭代器,进行下一层次的解析。
fn visit_maccall(&mut self, maccall:& MacCall)
{
let mac_name = maccall.path.segments[0].ident.as_str().to_string();
print!("{}", mac_name);
print!("(");
self.visit_MacArgs(&maccall.args);
print!(")");
}
fn visit_MacArgs(&mut self, macarg: &MacArgs)
{
match macarg{
MacArgs::Delimited(_, dlimiter, tokenstream)=>{
for tree in tokenstream.trees()
{
self.visit_TokenTree(&tree);
}
}
_ => {
print!("did not impl in visit_MacArgs");
}
}
}
fn visit_TokenTree(&mut self, tree: &TokenTree)
{
match tree{
TokenTree::Token(t) => {
self.visit_token(&t);
}
TokenTree::Delimited(_, token, tokenstream)=>{
match token{
DelimToken::NoDelim => {
for tree in tokenstream.trees()
{
self.visit_TokenTree(&tree);
}
}
_ =>{
print!("(");
for tree in tokenstream.trees()
{
self.visit_TokenTree(&tree);
}
print!(")");
}
}
}
}
}
编译器目前只能翻译基本的控制流(if或者match)和二元表达式,函数调用,assert宏,对于复杂的数据结构例如vector或者hashmap之类的还暂时不确定如何得到等价的Fstar翻译,Fstar本身是一门纯函数式语言,这代表它的函数调用是没有任何副作用的,而fstar中也不存在堆栈的概念,把实际应用的rust程序翻译到fstar上还有很长一段路要走。而如何证明rust到fstar程序之间的等价性也是一个问题,一种可能的方案是将rust程序转为fstar程序后,再实现一个fstar到rust的编译器,将fstar程序再次编译成rust,然后检查原rust程序和翻译返回后的rust程序之间的等价性。
作者:590