基于抽象语法树的rust到fstar编译器

weixin_43382743 2021-12-26 17:24:20

一 项目概述

(一) 需求背景

程序中总是可能存在错误,这些错误可以在运行时得到解决。但若是在一些比较特殊的场合中,无法提供多次动态运行的环境,那么可以在静态检查时尽可能的检查出程序中的错误,就变得至关重要了。本项目尝试从Rust语言出发,生成等价的Fstar程序,利用Fstar编译器来检测程序中可能出现的错误。

 

(二)技术选型

(1)Rust语言

Rust是一门系统编程语言   ,专注于安全   ,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似   ,但是设计者想要在保证性能的同时提供更好的内存安全。创建这个新语言的目的是为了解决一个顽疾:软件的演进速度大大低于硬件的演进,软件在语言级别上无法真正利用多核计算带来的性能提升。Rust是针对多核体系提出的语言,并且吸收一些其他动态语言的重要特性,比如不需要管理内存,比如不会出现Null指针等等。

Rust致力于成为优雅解决高并发和高安全性系统问题的编程语言  ,适用于大型场景,即创造维护能够保持大型系统完整的边界。这就导致了它强调安全,内存布局控制和并发的特点。标准Rust性能与标准C++性能不相上下。

(2)Fstar语言

Fstar是一种通用函数式编程语言,其效果旨在进行程序验证。它将SMT支持的演绎验证工具的自动化与基于依赖类型的证明助手的表达能力结合起来。经过验证后,可以将F*程序提取为高效的OCaml、F#、C、WASM或ASM代码。这可以验证实际应用程序的功能正确性和安全性。F*的主要持续使用案例是在Everest项目中为整个HTTPS堆栈构建一个经过验证的插入式替换。这包括TLS 1.2和1.3以及底层加密原语的验证实现。

F*的类型系统包括依赖类型、一元效应、精化类型和最弱的前提演算。总之,这些特性允许表达程序的精确而紧凑的规范。F*类型检查器旨在结合SMT解决方案和交互式证明来证明程序符合其规范。

 

(三)整体流程

项目整体流程如下图所示:

 由Rust程序到Fstar程序的转换通过遍历Rust程序的抽象语法树来完成,在完成转换过程后,把生成的Fstar程序交给Fstar编译器可以得到程序执行结果是否正确。

 

(四)一些例子

(1)Fib函数

以下是斐波那契数列的一个简单实现,如何验证该函数正确性呢?

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 Pattern)

组合模式使得用户对单个对象和组合对象的使用具有一致性。

有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦

组合模式让你可以优化处理递归或分级数据结构。有许多关于分级数据结构的例子,使得组合模式非常有用武之地。关于分级数据结构的一个普遍性的例子是你每次使用电脑时所遇到的:文件系统。文件系统由目录和文件组成。每个目录都可以装内容。目录的内容可以是文件,也可以是目录。按照这种方式,计算机的文件系统就是以递归结构来组织的。如果你想要描述这样的数据结构,那么你可以使用组合模式Composite。

定义

(GoF《设计模式》):将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

RUST中的宏

在rust中,宏是一种为写其他代码而写代码的方式,即所谓的 元编程metaprogramming。元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。一个函数标签必须声明函数参数个数和类型。相比之下,宏能够接受不同数量的参数:用一个参数调用 println!("hello") 或用两个参数调用 println!("hello {}", name) 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。实现一个宏而不是一个函数的缺点是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 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

...全文
338 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
源码链接: https://pan.quark.cn/s/a4b39357ea24 银行信贷业务作为银行业务的关键构成部分,涵盖了银行向客户提供的各类融资服务,例如贷款、担保以及信用证等。此类业务致力于协助企业与个人解决资金需求问题,进而推动经济活动的开展。银行通过信贷业务获取利息收入,同时需承担相应风险,以保障资金的稳定与流转。 **信贷定义** 信贷意味着银行运用自身资本及信誉为客户提供资金支持,而客户则需以支付利息、费用并偿还本金为前提条件。这种业务模式不仅包含直接贷款,还包括为客户的债务承担提供担保。依照会计准则,信贷业务可分为表内业务与表外业务,前者对银行的资产负债表产生直接影响,后者则不直接关联。 **信贷业务划分标准** 1. **依据会计核算归属**:表内信贷(如贷款、贴现)和表外信贷(如承兑、保证)。 2. **根据期限划分**:短期(不超过1年)、中期(1至5年)与长期(超过5年)。 3. **按照担保方式分类**:信用信贷(无担保)和担保信贷(保证、抵押、质押)。 4. **按照币种区分**:本币信贷与外币信贷。 5. **按照性质和用途区分**:固定资产贷款、流动资金贷款、循环额度贷款、消费贷款等。 6. **按照贷款组织形式区分**:普通贷款、联合贷款和银团贷款。 7. **按照资金来源区分**:信贷资金贷款、委托贷款和境外筹资转贷款。 8. **按照授信对象区分**:公司类信贷与个人类信贷。 **信贷业务产品** 1. **流动资金贷款**:用于企业日常运营周转或临时性资金需求。 2. **固定资产贷款**:用于投资固定资产项目。 3. **房地产开发贷款**:用于土地开发及房屋建设所需资金。 4. **循环额度贷款**:满足企业...

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧