迷你编译器:lexing

2020-10-31 19:54:31

我一直想做自己的编译器。编译器是我日常工作中不可或缺的一部分,我经常使用它们的成果。不久前,当我浏览TempleOS源代码时,在::/Demos/Lessons文件夹中发现了MiniCompiler.HC,我有点吃惊。它实现了一个从简单的数学表达式到AMD64字节码的两阶段编译器(完成后将其比特转换为代码稍后跳转到的数组),并且有很多关于编译器如何工作的内容要教授。对于那些手头没有TempleOS虚拟机的人,这里有一段MiniCompiler.HC运行的视频:

您输入一个数学表达式,编译器构建它,然后输出一堆程序集并运行它以返回结果。在本系列中,我们将创建该编译器的一个面向WebAssembly的实现。该编译器将用Rust编写,除了最后的字节码编译和执行阶段外,将只使用标准库。这里发生了很多事情,所以我希望这至少是一个三集的系列。源代码在xe/mini编译器中,如果您想详细了解的话。跟着走,让我们在路上学到一些生锈的东西!

像C这样的语言的编译器是建立在这里的基础之上的,但是它们要复杂得多。

理想情况下,我们应该能够将括号嵌套到我们想要的深度,而不会出现任何问题。

查看这些值,我们可以注意到一些模式,它们将使解析变得容易得多:

这将创建一个名为minicompiler的文件夹和一个名为src/main.rs的文件。在编辑器中打开该文件,并将以下内容复制到其中:

//src/main.rs/编译器可以执行的数学运算。#[Derate(Debug,Eq,PartialEq)]枚举Op{Mul,Div,Add,Sub,}/编译器的所有可能令牌,这将编译器限制为/简单的数学表达式。#[Derate(Debug,Eq,PartialEq)]枚举Token{EOF,Number(I32),Operation(Op),LeftParen,RightParen,}。

在编译器中,标记指的是您正在使用的语言的各个部分。在这种情况下,每个令牌代表程序的每个可能部分。

然后,让我们启动一个函数,该函数可以将程序字符串转换为一串令牌:

等等,你怎么处理不好的输入,比如不是数学表达式的东西?这个函数不应该失败吗?

你说得对!让我们做一个代表错误输入的小错误类型。看在创造力的份上,让我们称它为BadInput:

//src/main.rsuse std::Error::Error;Use std::fmt;/输入错误时返回的错误。这只会告诉用户它是错误的,因为调试信息不在这里的范围内。抱歉。#[Deriate(Debug,Eq,PartialEq)]struct BadInput;//需要显示错误。Iml fmt::Display for BadInput{fn fmt(&;self,f:&;mut fmt::Formatter)->;fmt::result{write!(F,";Something is Bad,Good&34;)}}//默认错误实现将在此执行。BadInput{}。

现在我们有了我们想要的函数类型,让我们通过设置结果并对输入字符串中的字符进行循环来开始实现lex():

//src/main.rsfn lex(input:&;str)->;result<;vec<;Token>;,BadInput>;{let mut result:vec<;Token>;=vec::new();对于input.chars(){TODO!(";Implementate this";);}OK(Result)}。

看一下前面的示例,我们可以开始编写一些样板来将字符转换为标记:

//src/main.rs//...对于input.chars(){匹配字符{//跳过空白';';=&>;继续,//结束字符';;';\n';=>;{result t.ush(Token::EOF);Break;}//数学运算';*';=>;result t.ush(Token::operation(opp::mul)),';/';=>;Result.Push(Token::Operation(Op:div)),';+';=>;Result.Push(Token::Operation(Op::Add)),';-';=>;Result.Push(Token::Operation(Op::Sub)),//括号';(';=>;result t.ush(Token::LeftParen),';)=>;Result t.ush(Token::RightParen),//Numbers';0';|';1';|';2';|';3';|';4';|';5';|';6';|';7';|';8';|';9';=>;{TODO!(";实现数字解析";)}//其他一切都是错误的input_=>;return err(BadInput),}}//...

//src/main.rs//...Use Op::*;Use Token::*;Match Character{//...//数学运算';*';=>;result t.ush(Operation(Mul)),';/';=>;result t.ush(Operation(Div)),';+';=>;result t.ush(Operation(Add)),';-';=>;Result t.ush(Operation(Sub)),//括号';(';=>;result t.ush(LeftParen),';=>;result t.ush(RightParen),//...}//...。

您几乎可以在程序中的任何位置使用USE语句。但是,为了保持更好的流畅性,Use语句就在这些示例中需要它的位置旁边。

现在我们可以开始享受解析数字的乐趣了。在编写MiniCompiler时,Terry Davis使用了类似以下内容的方法(为可读性添加了空格):

案例';0';...';9';:i=0;do{i=i*10+*src-';0';;src++;}While(';0';<;=*src<;=';9';);*num=i;

这会将中间变量i设置为0,然后消耗输入字符串中的字符,只要它们介于';0';和';9';之间。作为以10为基数输入的数字的一个巧妙副作用,您可以将40概念化为(4*10)+2。因此,它将旧数字乘以10,然后将新数字与结果数字相加。我们的设置不会让我们那么容易实现,但是我们可以根据以下规则通过一些堆栈操作来模拟它:

如果最后一个是一个数字,则将该数字乘以10,然后将当前数字加到该数字上。

否则,将节点推回RESULT,并将当前数字也推入RESULT。

//src/main.rs//...//Numbers';0';|';1';|';2';|';3';|';4';|';5';|';6';|';7';|';8';|';9';=>;{let num:I32=(Character as U8-';0';as U8)as I32;if result t.len()==0{result t.ush(number(Num));Continue;}let last=result t.op().unrapp();Match last{number(I)=>;{result t.ush(number((i*10)+num));}_=>;{result t.ush(Last);result t.ush(number(Num));}}//...

这不是世界上最健壮的数字解析代码,但是目前它已经足够了。如果你能辨认出边缘的话就加分!

这应该包括该语言的标记。让我们写一些测试,以确保一切都像我们想象的那样工作!

RUST在标准库中内置了一个健壮的测试框架。我们可以在这里使用它来确保我们生成的令牌是正确的。让我们将以下内容添加到main.rs的底部:

#[cfg(Test)]//告诉编译器仅在运行测试时编译此代码{use Super::{Op::*,Token::*,*};//将以下函数注册为测试函数#[test]FN BASIC_Lexing(){assert!(lex(";420+69";).is_ok());assert!(lex(";Tacos Are Tavy";).is_err();assert_eq!(lex(";420+69";).is_err();assert_eq!(lex(";420+69";).is_err();assert_eq!(lex(";420+69";),确定(vec![number(420),Operation(Add),number(69)]);assert_eq!(lex(";(30+560)/4";),OK(vec![LeftParen,number(30),Operation(Add),number(560),right Paren,Operation(Div),number(4)]);}}。

这个测试可以而且可能应该扩展,但是当我们运行货物测试时:

$Cargo测试编译迷你编译器v0.1.0(/HOME/cadey/code/xe/minicompiler)在0.22s内完成测试[未优化+调试信息]目标运行target/debug/deps/minicompiler-03cad314858b0419running 1测试测试:BASIC_LEXING...。OK测试结果:OK。1通过;0失败;0忽略;0测量;0过滤掉。

嘿,马上!我们验证了所有解析都工作正常。最小的用例应该足以涵盖该语言的所有功能。

第一部分就到这里。我们今天讲了很多。下次我们要在程序上运行avalidation pass时,将中缀表达式转换为反向Polishnotation,然后也开始将其编译为WebAssembly。到目前为止,这一直很有趣,我希望你能从中学到东西。

这篇文章发表在2020年M10 29上。自发表以来,事实和情况可能发生了变化。如果有什么不对劲或不清楚的地方,请在匆忙下结论之前与我联系。