Rust的类型系统是图灵完成(2017年)

2021-03-16 08:23:52

(N.B.这个帖子中的“他妈的”这个词看起来多次。我建议读者暂时不认为“他妈的”作为亵渎,因为它在这里没有使用。)

不久前,某人对铁锈削弱的挑战是挑战,而每个人都喜欢说Rust的类型系统正在完成,而且实际上没有人似乎有一个证据。为了响应该帖子,侵蚀了Smallfuck的实施形式的证据 - 完全在Rust型系统中。这博客文章是一种尝试解释该小职箱的内部工作,如此这是什么意思为锈型系统。

所以什么是图灵完整性?图灵完整性是最大编程语言的属性,这些性质可以模拟普遍的图标记。相关概念是图灵等价。 TINT等效的语言可以通过图灵机模拟和模拟 - 因此,如果您有任何Twotting等效语言,则必须可以将任何程序转换为为另一个程序编写的程序。大多数如果不是所有图灵完成系统也被称为等同的。 (我很遗憾无法找到更好的引用。)

关于图灵完整性有几件重要的事情要记录。我们知道停顿问题。 oneConsequence的情况是,如果您有一个提出完整的语言,它可以屏蔽任何通用图灵机,那么它必须能够有限循环。这就是了解Rust的类型系统是否具有uting-cleant的原因 - 为什么意思是,如果您能够将TINGS-CRESSED语言编码为RUST'STYPE系统,那么检查RUST程序的过程,以确保IT ISWELE类型必须是不可义的。TEACHECKER必须能够有限循环。

我们如何表明Rust型系统正在完成?这样做的最古代的方式(事实上我不知道任何其他方式)是在它中实现已知的完成语言。如果您可以以另一种语言实现actque完整的语言,那么您可以清楚地模拟它 - 通过在嵌入语言中模拟它来模拟它的通用图灵机。

那么,什么是smallfuck? Smallfuck是一aminimalist编程语言,当MemoryRestrics升起时,已知在完成时完成。我选择它在Rust型系统中实现,超越完整的语言,因为其简单性。虽然对实际编写程序相当无用的人来说,但它们非常适合证明完整性。

Smallfuck实际上非常靠近图灵机本身。 SmallFuck的原始方法,它在具有Afinite内存的机器中运行。但是,如果我们举起这一限制并允许它才能将理论上无限的内存阵列进行攻击,那么Smallfuck会花费完成。所以在这里,我们考虑与InfiniteMemory的Smallfuck的变化。 Smallfuck机器由无限的胶带组成,记忆的“细胞”包含位的比特以及指针进入该阵列的单元格。

< |指针递减> |指针增量* |翻转电流位[|如果当前位为0,则跳转到匹配];否则,转到下一个指令] |跳回匹配[指令

这使您能够选择单元格并进行循环。这是一个简单的emplicprogram:

这是一个死亡的简单和完全无用的程序(因为大多数程序都在SmallFuck中,由于它总是缺乏任何类型的I / OWOSHOVER,它只需连续将TheBits设置为1,然后使用循环将它们放入所有回到0,以其启动的相同位置的指针终止。一种可视化:

下一个指令是“翻转电流位”指令,因此我们将指针从0翻转到1。

这发生了三次。让我们跳过循环的开始:

现在我们在循环的开头。 [指令说:“如果阈值为零,则跳转到匹配];否则,转到下一个指令。“指针是1的比特,所以我们转到下一个指令。

这将当前位翻转为零;然后,我们移动内存指针副站位置。

现在我们在关闭]。这是一个无条件的跳回到循环的初始。

V v> *> *> * [*] | ... 0110 ... V v> *> *> * [* *] | ... 0100 ... V v> *> *> * [*<] | ... 0100 ... V v> *> *> * [*<] | ... 0100 ...

这正是我们开始的地方:所有单元格都返回为零,并使用指针引起其起始位置。

因此,这将简单地实现这看起来像生锈?我会首先通过SmallFuck的运行时间实现,这是我的类型级实现,以验证类型级别的运行时间实现是否一致。我们将把Smallfuck程序存储为AST,如下所示:

这与Smallfuck作为一个字符串的表示并不是真的,但它更容易解释。我们还需要一种代表runningsmallfuck程序的状态:

虽然要完成我们在技术上需要无限磁带,但这个运行时期仅用于检查类型级版本的语义。 Afinite磁带是一个完全精细的近似,现在制作。通过使位的小位(STD :: U16 :: Max + 1)/ 8,我们确保我们有一点Forevery U16地址。现在,我们将在实现我们的Internerpreter时使用的一些操作:

ichar州{fn get_bit(&amp; self,a:u16) - &gt; BOOL {SELE .BITS [(AT&gt;&gt; 3)如USIZE]&amp; (0x1&lt;(AT&amp; 0x7))!= 0} fn get_current_bit(&amp; self) - &gt; Bool {self .get_bit(self .ptr)} fn flip_current_bit(&amp; mut self){self .bits [(self .ptr&gt;&gt; 3),如USIZE] ^ = 0x1 <&lt; (自我.ptr&amp; 0x7); }}

这是一些相当标准的位操纵。我们将8位存储到一个单元格中阵列中,以便找出给定位的单元格,我们使用划分8,如果我们尝试非常聪明的结果,那么在三个地方右转到右侧。 。我们基本上使用该寻址素质,其中U16指针的较低三位指示了Byteb中的位,较高的13位指示位点中的字节索引:

索引到“位”的字节 - &gt; 1100011010011 \ 001&lt; - 在字节中的位置

所以很明显,我们可以通过漂亮的小位面具做事。平移右侧的速度追随“哪个位”部分,只需索引。 0x7in十六进制是二进制文件的0b111。这使得其目的是quiteclear:take&amp; 0x7除了最后三个之外,除了最后三个,可以清除我们的指针中的所有位,从而指示字节中的位。将0x1转换为0x1使我们只有一个u8只设置为1,让我们索引我们的字节;最后,!= 0让我们快速检查是否设置了相应的位。 FLIP_CURRENT_BIT中的XOROPETION只是翻转索引位。

现在我们有那些基元,让我们继续实施我们的解释器。我们将通过递归调用函数匹配的函数来实现:

ichsl计划{fn big_step(&amp; self,state:&amp; mut状态){使用self :: program :: *;匹配* self {空=&gt;未实现! (),左(接下来)=&gt;未实现! (),右(Ref Next)=&gt;未实现! (),翻转(Ref Next)=&gt;未实现! (),循环(ref body_and_next)=&gt;未实现! (),}}}

左右只是递增/递减一个指针。由于我们在简单实现中使用包装磁带,我们使用Wrapping_addand Wrapping_sub:

左(接下来)=&gt; {状态.ptr = state .ptr .wrapping_sub(1);下一个.big_step(州); },右(Ref Next)=&gt; {州.ptr = state .ptr .wrapping_add(1);下一个.big_step(州); },

最后但并非最不重要的,循环。我们检查当前位。如果它是1,则WEEXecute循环的主体,然后再次使用更新状态执行循环指令。如果它是0,我们继续前进到下一个指令:

循环(ref body_and_next)=&gt; {让(REF Body,Ref Next)= ** body_and_next;如果状态.get_current_bit(){body .big_step(州);自我.big_step(州); } else {next .big_step(州); }},

我们必须在body_and_next上进行双重DEREF,因为我们盒装元组。通过ref绑定,织布通过绑定出来的盒子。我们完成的.big_step()函数如下所示:

ichsl计划{fn big_step(&amp; self,state:&amp; mut状态){使用self :: program :: *;匹配* self {空=&gt; {},左(Ref Next)=&gt; {状态.ptr = state .ptr .wrapping_sub(1);下一个.big_step(州); },右(Ref Next)=&gt; {州.ptr = state .ptr .wrapping_add(1);下一个.big_step(州); },翻转(Ref Next)=&gt; {状态.flip_current_bit();下一个.big_step(州); },循环(ref body_and_next)=&gt; {让(REF Body,Ref Next)= ** body_and_next;如果状态.get_current_bit(){body .big_step(州);自我.big_step(州); } else {next .big_step(州); }},}}}

由于我们不需要它,我们不会为我们的州实施任何类型的调试印刷。当我们使用我们的运行时解释器来检查我们的Type LeviceInterpreter时,我们将通过迭代Type-LevelInterpreter的输出中的位和检查以确保在运行时输出中设置相同的比特。现在,我们可以自由地从事有趣的东西!

Rust有一个名为特征的功能。特征是编译时间静态分类的方式,也可以用来做运行时调度(尽管在实践中,但在这里非常有用。)我将在这里假设读者知道Rustrand中的特质是什么样的。为了实现SmallFuck,我们将依靠称为相关类型的特征的型特征。

在Rust中,为了调用特征方法或解析相关类型,编译经过一个名为特征分辨率的过程。为了解决特征,编译器必须查找unly unifies涉及的类型。统一是一个用于求解类型之间的方程的呼吸。如果没有解决方案,我们会说outunification失败了。这是一个例子:

特质foo&lt; B&GT; {CODSION CODSION; } icliml&lt; B&GT; foo&lt; B&GT;对于U16 {类型关联= bool; } iclich foo&lt; U64&gt;对于U8 {类型关联=字符串; }

这是一个非常成熟的例子,但它将提供一点。为了解析到相关类型的参考(例如,例如foo&lt; ::关联),RustCompiler必须搜索正确匹配f和t匹配的iclip。让我们扮演我们有f == U16和T == U64。如果编译器尝试Foo&lt; u64&gt;对于U8?然后它将立即发现F不匹配U8 - 所以,它不是我们正在寻找的iclip。如果它试图犯罪者怎么办?然后我们有f == U16,这是真的,所以都很好。现在我们必须匹配T == B.而这是魔法发生的地方。

Rust编译器统一使用B.由于B是变量 - 而不是一个具体类型,如Stringor U64 - 它的值是可以自由分配的。所以现在B被U64替换,并通过B =&GT的替换; U64,我们现在有一个foo&lt; u64&gt;对于有效的U16。统一是一个相当简单的过程:在一个类型(术语)中的ittakes可以具有变量,并且每次运行到尚未分配的尚未分配的次数符合任何其他术语时,都会分配给该术语(即使是另一个术语是一个变量!)然后,随时随地遍布同一术语的时间,它替换为其分配的值,而是统一尝试统一指定的ValueaGainst。

统一的输出是一个“替换” - 变量对术语的映射,使得当您采取术语时,您正在尝试统一和替换替换中提到的所有传播措施,您将您最终与两个相同的术语。这是一个独立的统一示例,试图统一Foo&lt; x,栏&lt; u16,z&gt;与foo&lt; baz,y&gt ;:

首先,我们检查条款的头部 - 我们得到foo与foo,它检查了。所以我们还没有统一。如果我们有,例如,Foo == Bar,那么统一会失败。接下来,我们将其分解为两个子问题。我们知道foo&lt; a,b&gt; == foo&lt; c,d&gt;如果且仅当a == c和b == d:

我们现在解决了一个变量 - X有一个明确的价值,BAZ。所以我们加油加入我们的替换,[x - &gt; baz]。然后,我们将替换替换为第2个术语,栏&lt; u16,z&gt; == Y.由于存在没有发生X,因此没有任何事情发生,我们留下了同样的术语。然后我们有一个解决方案,因此我们的最终替换看起来像[x - &gt; Baz,Y - &gt;棒&lt; u16,z&gt;]。当我们将其应用于我们想要检查的原始方程式时,我们会出现会发生什么:

这显然是真的。这两个术语现在是平等的!那么弥合失败的例子是什么?我们试试吧:

所以我们解决了第一个等式,获得了替代[x - &gt;酒吧]。应用这本来到x == baz产量bar == baz,这显然是假的。所以我们的Unificationshas失败了 - 没有解决方案。

统一是如此有用的过程,实际存在编程规程,prolog,它描述了编程逻辑术语,并且执行是术语的统一。因此,这似乎是可疑的。如果您有一个简化的语言,Prolog,这是基于统一的,那么它似乎很简单地源于rust的特质分辨率 - 这是通过尝试统一特质的作用,直到它发现它的作品 - 可能是图灵的-completeas好。

而现在我们已经让Boilerplate脱开了路,让我们看看我们如何在锈病中真正地锻炼这一点。我们现在将开始我们的Smallfuck实现!我们将使用几个月前使用的宏,称为Type_operators!其中将DSL编译为生锈结构定义,TraitDefinitions和Trait Impls的集合。虽然我将使用type_operators解释我的实现!,我也会显示Type_operators! invocationsexpand到。让我们开始吧!

我们开始简单。我们如何在Rusttypes中代表我们的小福克状态的位?

type_operators! {[ZZ,ZZZ,ZZZZ,ZZZZZ,ZZZZZZ]混凝土位=&GT; bool {f =&gt;假,t =&gt;真的 , } }

这里有几件事可以注意到。首先是奇怪的列表,[zz,zzz,zzzz,zzzzz,zzzzzz]。这一个人很难解释,但它与Rust宏如何无法创建Uniquetype名称。所以你必须通过一个带有自己提供自己的列表的黑客来解决这个问题。它现在与实际实现无关。但是,具体赌注是!

type_operators!采用两种类型级伪数据类型定义 - DataAnd混凝土。差异在于Type_operators!生成特征以确保您不会不匹配这些类型级数据类型。上面的示例编译为文件定义:

酒吧特征位{Fn Reify() - &gt; BOOL; PUB STRUCT T;酒吧结构F;用于T {fn Reify() - &gt; f {fn Reify() - &gt; bool {true}} isc ill。 bool {false}}

希望你现在可以看到发生了什么! t和f成为单位结构模拟位。位给出了它们的Reify()函数,让您将T和F转换为相应的布尔表示。所以你可以赋予&lt; t; :: Reify()将产生真实。只要你有一个类型的变量b:bit,你可以使用b :: Reify()Toturn it toole。我用它来转动我可以检查运行时实现的SmallFuck解释器内容值的输出。

具体列表=&gt; bitvec {nil =&gt; BitVec :: new(),缺点(b:bit,l:list = nil)=&gt; {让mut tail = l;尾巴.push(b);尾巴 }, }

这是一个类型级别的缺点列表。我们摆脱这一件事是一对的一对类型,零和​​缺点:

所以现在我们有位和比特列表。我们可以构造一个列表[t,f,f]作为缺点&lt; xf,cons&lt; f,nil&gt;&gt;&gt。我们还获得一些特征在于:

PUB Trait列表{FN Reify() - &gt; BitVec; } ill icl {fn Reify() - &gt; BitVec {BitVec :: New()}} iscl&lt; B:位,L:list&gt;缺陷列表&lt; B,L&GT; {Fn Reify() - &gt; bitvec {让mut tail =&lt; l作为列表&gt; :: Reify();尾巴.push(&lt; b为bit&gt; :: Reify());尾巴 } }

缺点(b:bit,l:list = nil)=&gt; {让mut tail = l;尾巴.push(b);尾巴 }

导致一种语法糖,其中L和B自动重新加入,然后绑定到这些变量中。这是为了围绕macro卫生的一些限制,并允许用户实际使用这些值。

所以希望现在你已经弄清楚了,让我们检查我们使用的最后一个具体定义:

具体节目=&gt;程序{空=&gt;程序::空,左(p:parowty =空)=&gt;程序:: left(框:: new(p)),右(p:parowty =空)=&gt; Program :: Right(框::新(P)),翻转(P:Programty =空)=&gt;程序::翻转(框::新(p)),循环(p:parowty =空,q:parowty =空)=&gt;程序::循环(框:: new((p,q))),concrete statetette =&gt; Dateettout {St(l:list,c:bit,r:list)=&gt; {让mut bits = l;让loc =位.len();位.push(c);位.extend(r .into_iter().rev()); statetetteout {loc:loc,位:bits,}},}

希望你现在已经弄清楚了所有这些模式。这是一个完整的列表,这些定义如何编译为锈迹,特征和iclips:

方案相当简单,希望您现在可以看到ICHOSE以尽可能地最好地编码Smallfuck程序的运行时编码作为AST - Mirtorthe级编码。 DaterettyTrait下的ST类型是拉链列表,同时是SmallFuckInterpreter的指针位置和存储器。 L和R列表将内存表示为C的任一侧,指针下的当前位。

现在我们可以看看最有趣的部分 - ThesMallfuck解释器的实际实施。它由一个特质和十几个曲目组成。这是type_operators!代码:

(run)运行(程序,DATEQety):SATTETY {forall(p:programty,c:bit,r:list){[(左字p),(st nil c r)] =&gt; (#p(st nil f(con c r)))} forall(p:parowty,l:list,c:bit){[(右图),(st l c nil)] =&gt; (#p(st(cl)f nil))} forall(p:parowty,l:list,c:bit,n:bit,r:list){[(左字p),(st(cons nl)cr )] =&gt; (#p(st l n(cons c r)))[(右图),(st l c(cons n r))] =&gt; (#p(st(cons c l)n r))} forall(p:parowty,l:list,r:list){[(翻转p),(st l f r)] =&gt; (#p(st l t r))[(翻转p),(st l t r)] =&gt; (#p(st lf r))} forall(p:programty,q:parowty,l:list,r:list){[(循环p q),(st l f r)] =&gt; (#q(st l f r))[(循环p q),(st l t r)] =&gt; (#(循环p q)(#p(st l t r)))} forall(s:staretety){[空,s] =&gt; s}}

这对整个乐天的东西编译。 第一件事是特质。 这是奇怪的gensm列表发挥作用。 自从gensyms以来 ......