编译原理课程设计:简易 rust 编译器
使用 rust 实现的简易 rust 编译器,实现了词法分析、语法分析,支持定义函数、定义变量、运算符、控制流、输出等基本功能。
tiny-rustc <source_file.rs> [-o <output_file>] [-emit=tokens|ast|ir|asm]输出文件默认在同目录下
emit 参数用于输出编译过程的中间产物
tiny-rust 的语法是 rust 的子集,即 tiny-rust 程序都可以由 rustc 编译。
tiny-rust 的程序由若干函数组成,每个函数都以 fn 关键字开头,函数名后跟一对括号,括号内是函数的参数列表,每个参数由变量名、:、类型三部分构成,之后是 -> 和函数的返回类型,函数体由一对大括号包围。
由大括号包围的代码块的结尾如果是表达式,则作为代码块的值。
整个程序以 main 函数作为入口,main 函数的返回类型是 () ,可以省略。
tiny-rust 支持以下 6 种数据类型:
i3232 位有符号整数,如123f6464 位浮点数,如3.14bool布尔类型,值为true或false&str字符串,只支持""形式的字符串字面量,不支持r""形式的原始字符串字面量- 元组,例如
(i32, f64) - 数组,例如
[i32; 10]
由于 tiny-rust 支持的数据类型在 rust 中都实现了 copy trait ,所以赋值时会直接拷贝而不是转移 ownership ,所以 tiny-rustc 并没有实现 rust 中 ownership 的机制。
定义变量的语法为 let [mut] variable_name: variable_type [=value]; ,其中 mut 表示变量可变,变量类型不能省略。
支持的运算符:
+加法-减法,取反*乘法/除法%取模=赋值+=加法赋值-=减法赋值*=乘法赋值/=除法赋值%=取模赋值==等于!=不等于<小于>大于<=小于等于>=大于等于!逻辑非&&逻辑与||逻辑或[]索引 构造数组.访问成员()改变优先级 构造元组
tiny-rust 只支持如下形式的输出:
println!("output: {}", value);
println!("output: {} {}", v1, v2);
...
不支持格式化选项
支持的控制流语句:
if语句,语法为if condition { block }if-else语句,语法为if condition { block } else { block }, 其中block可以是值,但两个分支的类型必须相同while语句,语法为while condition { block }loop语句,语法为loop { block }
支持 break 、 continue 和 return
不支持 for 和 match
只支持单行注释,以 // 开头
fn main() { let mut n: i32 = 1; while n <= 10 { println!("The {}th Fibonacci number is: {}", n, fib(n)); n += 1; } } fn fib(n: i32) -> i32 { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }
以这段代码为例
fn main() { println!("Hello, world!"); }
词法分析结果:
[ Ident( "fn", ), Ident( "main", ), OpenParen, CloseParen, OpenBrace, Ident( "println", ), Bang, OpenParen, Str( "Hello, world!", ), CloseParen, Semi, CloseBrace, ]
语法分析结果:
Program { func_decls: [ FuncDecl { name: "main", params: [], return_type: Tuple( [], ), code_block: CodeBlock { statements: [ Val( Print( [ Lit( Str( "Hello, world!", ), ), ], ), ), ], val: Lit( Tuple( [], ), ), }, }, ], }
.
├── Cargo.toml
├── README.md
├── examples
│ ├── hello.rs
│ └── ...
├── src
│ ├── ast.rs
│ ├── lexer.rs
│ ├── lib.rs
│ ├── llvm.rs
│ └── main.rs
└── tests
└── tests.rs
examples一些示例程序lexer词法分析器ast语法分析器llvm通过 llvm 的接口生成目标代码(目前只实现了一小部分)lib编译器的框架main解析命令行参数tests测试examples中的程序运行结果是否符合预期
实现对标识符、字面量的解析,并去除注释和空白字符。
为了方便对 Option<Box<Self>> 形式的处理,双字符运算符会被解析为两个 token 。
#[derive(Logos, Debug, PartialEq, Clone)] pub enum Token { #[regex(r"\/\/.*\n", skip)] LineComment, #[regex(r"\s+", skip)] Whitespace, #[regex(r"[a-zA-Z_]\w*", |lex| lex.slice().to_string())] Ident(String), #[regex(r"[0-9]+", |lex| lex.slice().parse().ok())] Int(i32), #[regex(r"[0-9]+\.[0-9]+", |lex| lex.slice().parse().ok())] Float(f64), #[regex(r#""([^"\\]|\\.)*""#, |lex| { let s = lex.slice(); unescape(&s[1..s.len()-1]) })] Str(String), #[token(".")] Dot, // ... }
用来表示词法分析器的输出,使用正则表达式来匹配。
主要语法的产生式如下:
Program -> FuncDecl* FuncDecl -> "fn" Ident Params ["->" Type] CodeBlock Params -> "()" | "(" [Param ("," Param)*] [","] ")" Param -> Ident ":" Type Type -> "i32" | "f64" | "bool" | "&str" | ArrayType | TupleType ArrayType -> "[" Type ";" Val "]" TupleType -> "()" | "(" [Type ("," Type)*] [","] ")" CodeBlock -> "{" [Statement*] [Val] "}" Statement -> Val ";" | LetStatement | WhileStatement | LoopStatement | BreakStatement | ContinueStatement LetStatement -> "let" ["mut"] Ident ":" Type ["=" Val] ";" WhileStatement -> "while" Val CodeBlock LoopStatement -> "loop" CodeBlock BreakStatement -> "break" ";" ContinueStatement -> "continue" ";" Val -> OperandD (BiOp OperandD)* OperandD -> UnaryOp* Operand ["." FuncCall | "[" Val "]"]* Operand -> Literal | Ident | Print | CodeBlock | FuncCall | IfExpr Literal -> Int | Float | Bool | Str | Tuple | Array Tuple -> "()" | "(" Val ("," Val)* [","] ")" Array -> "[" [Val ("," Val)* [","]] "]" Print -> "println" Bang Args FuncCall -> Ident Args IfExpr -> "if" Val CodeBlock ["else" CodeBlock] Args -> "()" | "(" [Val ("," Val)* [","]] ")"
其中 Val 表示由二元运算符连接操作数形成的表达式,
OperandD 带有一元运算符、下标、函数链式调用的操作数,
Operand 表示操作数的主题部分。
各种类的组成关系如下
classDiagram
class Program {
Vec~FuncDecl~ func_decls
}
class FuncDecl {
String name
Vec~String, Type~ params
Type return_type
CodeBlock code_block
}
class Type {
<<enumeration>>
I32
F64
Bool
Str
Array(Box~Type~, Value)
Tuple(Vec~Type~)
}
class CodeBlock {
Vec~Statement~ statements
Value val
}
class Statement {
<<enumeration>>
Let(String, Type, bool, Box~Value~)
Val(Value)
While(Value, CodeBlock)
Loop(CodeBlock)
Break
Continue
None
}
class BiOp {
<<enumeration>>
Plus
...
}
class BiOperation {
BiOp op
Box~Value~[2] operands
}
class UnOp {
<<enumeration>>
Neg
Not
}
class UnOperation {
UnOp op
Box~Value~ operand
}
class Literal {
<<enumeration>>
I32(i32)
F64(f64)
Bool(bool)
Str(String)
Array(Vec~Value~)
Tuple(Vec~Value~)
}
class Value {
<<enumeration>>
BiOp(BiOperation)
UnOp(UnOperation)
Lit(Literal)
Ident(String)
FuncCall(String, Vec~Value~)
Print(Vec~Value~)
CodeBlock(Box~CodeBlock~)
IfExpr(Box~Value~, Box~CodeBlock~, Box~CodeBlock~)
}
Program --> FuncDecl
FuncDecl --> Type
FuncDecl --> CodeBlock
CodeBlock --> Statement
CodeBlock --> Value
Statement --> Value
Statement --> CodeBlock
BiOperation --> BiOp
BiOperation --> Value
UnOperation --> UnOp
UnOperation --> Value
Value --> BiOperation
Value --> UnOperation
Value --> Literal
Value --> CodeBlock
Value --> Type
Type --> Value
代码采用递归下降实现,下面是语法分析器的函数头
pub struct Parser<I: Iterator<Item = Token>> { tokens: Peekable<I>, } impl<I: Iterator<Item = Token>> Parser<I> { pub fn parse(iter: I) -> Program; fn expect(&mut self, expected: Token); fn expect_ident(&mut self) -> String; fn parse_program(&mut self) -> Program; fn parse_func_decl(&mut self) -> FuncDecl; fn parse_param(&mut self) -> (String, Type); fn parse_type(&mut self) -> Type; fn parse_code_block(&mut self) -> CodeBlock; fn parse_statement(&mut self) -> (Statement, bool); fn try_parse_biop(&mut self) -> Option<BiOp>; fn try_parse_unop(&mut self) -> Option<UnOp>; fn parse_expr(&mut self) -> Value; fn parse_operand_deco(&mut self) -> Value; fn parse_operand(&mut self) -> Value; fn parse_tuple_array(&mut self) -> Value; fn parse_args(&mut self) -> Vec<Value>; }
I: Iterator<> 是词法分析器产生的 token stream 。
parse_statement 返回的 bool 值表示是否作为代码块的值
在 parse_operand_deco 中会依次记录遇到的一元运算符,然后调用 parse_operand ,之后解析遇到的下标和函数调用。
parse_expr 中维护了两个栈保存操作数和运算符,遇到运算符时会比较优先级,决定是否合并操作数。
编写过程中遇到了一些平时不太会注意的细节:
rust 支持嵌套注释,如
/* /* nested comment */ */嵌套的注释无法直接使用正则表达式进行匹配,需要使用记录嵌套层数的状态机进行解析。
在 rust 中,很多语句都可以作为值,即单元类型(空元组),例如
let u: () = (); let a: () = while false {}; let b: () = loop { break; }; let c: () = if false {};
但值为单元类型的语句和 () 又不完全等价, () 无法被解析为语句
let a; while false {} // ok let b; () // compile error: expected `;` let c;
且返回值不为 () 的 if 表达式无法作为独立语句,不过这应该在语义分析时检查类型,因此不属于语法错误
let a; if true { () } else { () } // ok let b; if true { 1 } else { 2 } // type mismatch: expected `()` but found `i32` let c;
这使得对于流程控制语句的解析有些复杂。
对尾逗号和单元素元组的处理
assert_eq!((1, 2), (1, 2,)); // ok assert_eq!((1), (1,)); // compile error: can't compare integer with (integer,)
一开始对元组的解析和对函数调用参数的解析使用了相同的函数, 导致单参数函数调用无法被正确解析。
运算符 + 只能作为二元运算符使用,和 c++ 不同
项目源码 https://github.com/iuy1/tiny-rustc
- The Rust Programming Language https://doc.rust-lang.org/book/
- Rust source code https://github.com/rust-lang/rust
- Overview of the Rust compiler https://rustc-dev-guide.rust-lang.org/overview.html
- TinyC 编译器 https://pandolia.net/tinyc/index.html
- logos handbook https://logos.maciej.codes/