25-扩展 Palladium:第一部分 - 你想构建一个语言虚拟机吗?
引言
大家好!在本教程中,我们将转换轨道,对 Palladium 进行一些工作。目前,它只能处理简单的算术表达式,比如 2+1
,仅此而已。让我们看看是否能够让它处理更复杂的东西。
目标
-
处理括号表达式(例如,
(4-3)*2
)Handle parenthetical expressions (e.g., (4-3)*2))
-
教会我们的解析器处理浮点数
Teach our parser to handle floating point numbers
解析器
让我们快速回顾一下:
-
一个
Program
由Expressions
组成 -
一个
Expression
返回一个值 -
一个
Operand
是Operator
左右两边的部分,例如在2+1
中,操作数是 2 和 1 -
一个
Operator
是+,-,*,/
-
Nom 将所有这些解析为
Tokens
(在src/tokens.rs
中找到)
我们将稍微改变我们的定义:
-
一个
Expression
由Terms
组成 -
一个
Term
由Factors
组成 -
一个
Term
可以被加或减 -
一个
Factor
可以被乘或除 -
一个
Factor
可以被(
和)
包围
重要 | 是时候进行一些数学讨论了!别担心,这并不复杂。=)除非你使用过 Lisp 风格的语言(例如 Clojure)或者那些老式 HP 计算器,你很可能习惯于看到所有数学表达式都以以下形式书写:1. 3+1 2. 2 * (1+1)看到操作符在中间了吗?这被称为 中缀表示法 (infix notation )。这不是唯一的表达式书写方式。如果我们把 3+1 写成 3 1 + 呢?那被称为 后缀表示法 (postfix notation )。如果我们把它写成 + 3 1 呢?那被称为 前缀表示法 (prefix notation )。前缀和后缀表示法实际上更容易解析,但它提高了你语言的入门门槛,因为人们倾向于不喜欢使用非中缀表示法。 |
新的 Token
前往 src/tokens.rs
。我们需要改变一些事情:
#[derive(Debug, PartialEq, Clone)]
pub enum Token {
AdditionOperator,
SubtractionOperator,
MultiplicationOperator,
DivisionOperator,
Integer{ value: i64 },
Float{ value: f64},
Factor{ value: Box<Token> },
Term{ left: Box<Token>, right: Vec<(Token, Token)> },
Expression{ left: Box<Token>, right: Vec<(Token, Token)> },
Program{ expressions: Vec<Token> }
}
我们为 Float
、Factor
和 Term
添加了 Token。我们还从 Expression
中移除了 op
字段,并替换为元组的 Vec
。我们马上就会看到为什么。
我们还需要在 visitor.rs
中为新 Token 制作一些匹配分支:
&Token::Float{ value } => {},
&Token::Factor{ ref value } => {},
&Token::Term{ ref left, ref right } => {},
&Token::Expression{ ref left, ref right } => {},
&Token::Program{ ref expressions } => {},
因子
现在让我们教解析器什么是因子。创建 src/factor_parsers.rs
,并在其中放置:
use nom::*;
use nom::types::CompleteStr;
use tokens::Token;
use expression_parsers::expression;
named!(float64<CompleteStr, Token>,
ws!(
do_parse!(
sign: opt!(tag!("-")) >>
left_nums: digit >>
tag!(".") >>
right_nums: digit >>
(
{
let mut tmp = String::from("");
if sign.is_some() {
tmp.push_str("-");
}
tmp.push_str(&left_nums.to_string());
tmp.push_str(".");
tmp.push_str(&right_nums.to_string());
let converted = tmp.parse::<f64>().unwrap();
Token::Factor{ value: Box::new(Token::Float{value: converted}) }
}
)
)
)
);
解析器执行以下操作:
- Consume any whitespace
- Check if there is a -. Because we use opt!, it is optional. This is to handle negative floats.
- Consume all digits until it encounters .
- Consume all digits after the .
- Construct the final floating point string representation
- Convert it to a f64
- Return a Factor token that contains a Float token
- 消耗任何空白
- 检查是否有
-
。因为我们使用opt!
,它是可选的。这是为了处理负浮点数。 - 消耗
.
之前的所有数字 - 消耗
.
之后的所有数字 - 构造最终的浮点字符串表示
- 将其转换为
f64
- 返回包含
Float
Token 的Factor
Token
一些这个解析器旨 (meant
) 在检测的浮点数示例:
1.0
-1.0
323.8
1.453
它几乎与我们的整数解析器相同。既然整数也是 Factor
,让我们将整数解析器移过来:
named!(integer<CompleteStr, Token>,
ws!(
do_parse!(
sign: opt!(tag!("-")) >>
reg_num: digit >>
(
{
let mut tmp = String::from("");
if sign.is_some() {
tmp.push_str("-");
}
tmp.push_str(®_num.to_string());
let converted = tmp.parse::<i64>().unwrap();
Token::Integer{ value: converted }
}
)
)
)
);
无需更改,只是更改了位置。
最后,是 Factor
解析器!它看起来像这样:
named!(pub factor<CompleteStr, Token>,
ws!(
do_parse!(
f: alt!(
integer |
float64 |
ws!(delimited!( tag_s!("("), expression, tag_s!(")") ))
) >>
(
{
Token::Factor{value: Box::new(f)}
}
)
)
)
);
这里是它的工作原理:
- Try the integer parser that we just moved into the same file
- Try the float64 parser we just made
- Look for an Expression surrounded by ( and )
-
尝试我们刚刚移到同一个文件中的
integer
解析器 -
尝试我们刚刚制作的
float64
解析器 -
查找被
(
和)
包围的Expression
最后一个非常重要,因为它使我们能够处理带有嵌套括号 (parentheses
) 的表达式。
表达式解析器
打开 src/expression_parsers.rs
。我们需要稍微改变一下这个解析器,以便它能够使用我们的新 Factor
解析器。新的 Expression
解析器看起来像这样:
use nom::types::CompleteStr;
use tokens::Token;
use term_parsers::term;
use operator_parsers::*;
named!(pub expression<CompleteStr,Token>,
do_parse!(
left: term >>
right: many0!(
tuple!(
alt!(
addition_operator |
subtraction_operator
),
term
)
) >>
(
{
Token::Expression{left: Box::new(left), right: right}
}
)
)
);
重要 | 注意 use 更改。我们正在导入 Term 解析器,我们还没有编写。别担心!我们会到那里的! |
这个解析器现在做的是:
- Look for a Term
- Looks for zero or characters that match the following pattern: + or - then a Term
- Because we use the tuple! macro, we will get back a (Token, Token) result, which is why we had to change our Token definitions a bit.
- Construct an Expression and return it
-
查找一个
Term
-
查找零个或多个字符,这些字符匹配以下模式:
+
或-
然后是一个Term
-
因为我们使用了
tuple!
宏,我们将得到一个(Token, Token)
结果,这就是为什么我们不得不稍微改变我们的Token
定义。 -
构造一个
Expression
并返回它
术语解析器
我们的最后一个新解析器是 Term
解析器。创建 src/term_parsers.rs
,并在其中放置:
use nom::types::CompleteStr;
use tokens::Token;
use factor_parsers::factor;
use operator_parsers::{multiplication_operator, division_operator};
named!(pub term<CompleteStr,Token>,
do_parse!(
left: factor >>
right: many0!(
tuple!(
alt!(
multiplication_operator |
division_operator
),
factor
)
) >>
(
{
Token::Term{left: Box::new(left), right: right}
}
)
)
);
它几乎与 Factor
解析器做相同的事情:
- Look for a Factor
- Looks for zero or characters that match the following pattern: *or / then a Factor
- Because we use the tuple! macro, we will get back a (Token, Token) result, which is why we had to change our Token definitions a bit.
- Construct a Term and return it
-
查找一个
Factor
-
查找零个或多个字符,这些字符匹配以下模式:`` *or
/
然后是一个Factor
-
因为我们使用了
tuple!
宏,我们将得到一个(Token, Token)
结果,这就是为什么我们不得不稍微改变我们的Token
定义。 -
构造一个
Term
并返回它
注意 | 注意 Term 查找 * 和 / ,而 Factor 查找 + 和 - 。这就是我们如何建立操作符优先级。 |
示例表达式
让我们看看 Nom 将 3*4
转换为什么:
(
CompleteStr(
""
),
Term {
left: Factor {
value: Integer {
value: 3
}
},
right: [
(
MultiplicationOperator,
Factor {
value: Integer {
value: 4
}
}
)
]
}
)
在左手边,我们有一个 Term
,它有一个 Factor
,它有一个 Integer
。在右边,我们有一个包含 MultiplicationOperator
和包含 Integer
的 Factor
的 Vec
。
更复杂的表达式的 ASTs 可能会变得更大。=)
测试
现在让我们写一些测试!
术语测试
在 term_parsers.rs
中放置:
mod tests {
use super::*;
#[test]
fn test_parse_term() {
let result = term(CompleteStr("3*4"));
assert_eq!(result.is_ok(), true);
}
#[test]
fn test_parse_nested_term() {
let result = term(CompleteStr("(3*4)*2"));
assert_eq!(result.is_ok(), true);
}
#[test]
fn test_parse_really_nested_term() {
let result = term(CompleteStr("((3*4)*2)"));
assert_eq!(result.is_ok(), true);
}
}
因子测试
这些放在 factor_parsers.rs
中:
mod tests {
use super::*;
#[test]
fn test_factor() {
let test_program = CompleteStr("(1+2)");
let result = factor(test_program);
assert_eq!(result.is_ok(), true);
let (_, tree) = result.unwrap();
println!("{:#?}", tree);
}
#[test]
fn test_parse_floats() {
let test_floats = vec!["100.4", "1.02", "-1.02"];
for o in test_floats {
let parsed_o = o.parse::<f64>().unwrap();
let result = float64(CompleteStr(o));
assert_eq!(result.is_ok(), true);
}
}
#[test]
fn test_parse_integer() {
let test_integers = vec!["0", "-1", "1"];
for o in test_integers {
let parsed_o = o.parse::<i64>().unwrap();
let result = integer(CompleteStr(o));
assert_eq!(result.is_ok(), true);
}
}
}
表达式测试
如果我们想解析包含所有操作符的东西,我们需要使用 Expression。让我们在 expression_parsers.rs
中添加一些测试:
#[test]
fn test_parse_expression() {
let result = expression(CompleteStr("3*4"));
assert_eq!(result.is_ok(), true);
}
#[test]
fn test_parse_nested_expression() {
let result = expression(CompleteStr("(3*4)+1"));
assert_eq!(result.is_ok(), true);
}
程序测试
最后,让我们在 program_parsers.rs
中做一个花哨的测试:
#[test]
fn test_parse_program() {
let test_program = CompleteStr("1+2");
let result = program(test_program);
assert_eq!(result.is_ok(), true);
}
如果你运行测试,你可能会遇到类似的失败:
thread 'visitor::tests::test_visit_addition_token' panicked at 'called `Option::unwrap()` on a `None` value', libcore/option.rs:345:21
这意味着在我们的加法函数中:
&Token::AdditionOperator => {
// TODO: Need to clean this up. Remove the unwraps.
let result_register = self.free_registers.pop().unwrap();
let left_register = self.used_registers.pop().unwrap();
let right_register = self.used_registers.pop().unwrap();
let line = format!("ADD ${} ${} ${}", left_register, right_register, result_register);
self.assembly.push(line);
self.free_registers.push(left_register);
self.free_registers.push(right_register);
},
其中一个已使用的寄存器还没有放入使用栈中。记住我们是如何开始返回元组的吗?正在发生的是,在 Expression
匹配分支中:
&Token::Expression{ ref left, ref right } => {
self.visit_token(left);
for expression in right {
self.visit_token(&expression.1);
self.visit_token(&expression.0);
}
},
当它评估 (evaluates
) 我们的树的这一部分时:
right: [
(
AdditionOperator,
Term {
left: Factor {
value: Integer {
value: 2
}
},
right: []
}
)
]
它将在访问 Integer
值 之前 访问加法运算符。这意味着值 2 还没有放入寄存器中。我们希望 Expression
函数以相反的顺序访问它们。所以改变代码:
&Token::Expression{ ref left, ref right } => {
self.visit_token(left);
for expression in right {
self.visit_token(&expression.0);
self.visit_token(&expression.1);
}
},
&Token::Expression{ ref left, ref right } => {
self.visit_token(left);
for expression in right {
self.visit_token(&expression.1);
self.visit_token(&expression.0);
}
},
你还需要对 Term
分支做同样的更改。
如果我们现在运行它..
thread 'visitor::tests::test_nested_operators' panicked at 'called `Option::unwrap()` on a `None` value', libcore/option.rs:345:21
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
GRRRR!
这个问题花了我一些时间调试,我不得不写一些调试函数来打印使用中的寄存器。我还添加了这个测试到 visitor.rs
。
#[test]
fn test_nested_operators() {
let mut compiler = Compiler::new();
let test_program = generate_test_program("(4*3)-1");
compiler.visit_token(&test_program);
let bytecode = compiler.compile();
}
程序解析得很好,直到 -
;乘法 (multiplication
) 部分没问题。通过打印出在它击中减法分支时使用中的寄存器:
--------------------
| Used Registers |
--------------------
0
--------------------
| Free Registers |
--------------------
30
<snip>
1
我们应该有两个使用中的寄存器:一个用于乘法指令的结果,一个用于 1
的加载指令。显然 (Clearly
) 那个寄存器从未被放入使用寄存器列表中。如果我们看一下处理 SUB
操作码的分支:
self.print_used_registers();
self.print_free_registers();
let result_register = self.free_registers.pop().unwrap();
let left_register = self.used_registers.pop().unwrap();
let right_register = self.used_registers.pop().unwrap();
let line = format!("SUB ${} ${} ${}", left_register, right_register, result_register);
self.assembly.push(line);
self.free_registers.push(left_register);
self.free_registers.push(right_register);
看到问题了吗?我们从没把result_register
推送到使用的寄存器栈中。我们可以像这样来修复它:
self.print_used_registers();
self.print_free_registers();
let result_register = self.free_registers.pop().unwrap();
let left_register = self.used_registers.pop().unwrap();
let right_register = self.used_registers.pop().unwrap();
let line = format!("SUB ${} ${} ${}", left_register, right_register, result_register);
self.assembly.push(line);
self.used_registers.push(result_register); // <-- fixed
self.free_registers.push(left_register);
self.free_registers.push(right_register);
这个问题存在于所有的 operator 的分支种,所以我们需要通过同样的方式来修复所有的分支。
现在再让我们来运行他们,他们应该是能够通过的。
running 18 tests
test operator_parsers::tests::test_parse_division_operator ... ok
test operator_parsers::tests::test_parse_addition_operator ... ok
test operator_parsers::tests::test_parse_multiplication_operator ... ok
test factor_parsers::tests::test_parse_integer ... ok
test factor_parsers::tests::test_parse_floats ... ok
test expression_parsers::tests::test_parse_expression ... ok
test expression_parsers::tests::test_parse_nested_expression ... ok
test operator_parsers::tests::test_parse_operator ... ok
test operator_parsers::tests::test_parse_subtraction_operator ... ok
test program_parsers::tests::test_parse_program ... ok
test factor_parsers::tests::test_factor ... ok
test term_parsers::tests::test_parse_nested_term ... ok
test term_parsers::tests::test_parse_term ... ok
test visitor::tests::test_visit_addition_token ... ok
test visitor::tests::test_visit_subtraction_token ... ok
test term_parsers::tests::test_parse_really_nested_term ... ok
test visitor::tests::test_nested_operators ... ok
test visitor::tests::test_visit_multiplication_token ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
好耶!(YAY!)
提示 | 我以及添加了部分测试。你可以通过查看源码来查看更多细节 |
汇编输出 (Assembly Output)
让我们快速地添加一个更改,以便打印出 test_nested_operators
测试的汇编输出。
".data"
".code"
"LOAD $0 #4"
"LOAD $1 #3"
"MUL $1 $0 $2"
"LOAD $0 #1"
"SUB $0 $2 $1"
看起来是对的!如果我们一步步来:
- Load 4 into R0
- Load 3 into R1
- Multiply R0 and R1 and store result in R2 (which has the value of 12)
- Load 1 into R0
- Subtract R0 from R2 and store result in R3 (which should have 11 at the end)
- Crap, that’s -11
- 将 4 载入 R0
- 将 3 载入 R1
- 将 R0 和 R1 相乘并将结果存储在 R2 中(R2 的值为 12)
- 将 1 载入 R0
- 从 R2 中减去 R0 并将结果存储在 R3 中(最终应该是 11)
- 糟糕,结果是 -11
如果我们通过 Iridium REPL 运行生成的代码,我们会看到:
Welcome to Iridium! Let's be productive!
>>> LOAD $0 #4
LOAD $1 #3
>>> MUL $1 $0 $2
>>> LOAD $0 #1
>>> SUB $0 $2 $1
>>> !registers
Listing registers and all contents:
>>> [
1,
-11,
12,
0,
0,
// <snip>
]
该死!在这种情况下,问题是出在 Iridium 上。它处理减法 (subtraction
) 的代码如下:
Opcode::SUB => {
let register1 = self.registers[self.next_8_bits() as usize];
let register2 = self.registers[self.next_8_bits() as usize];
self.registers[self.next_8_bits() as usize] = register1 - register2;
}
以及我们输出代码的方式,我们需要要么改变 Iridium 虚拟机来做:
Opcode::SUB => {
let register1 = self.registers[self.next_8_bits() as usize];
let register2 = self.registers[self.next_8_bits() as usize];
self.registers[self.next_8_bits() as usize] = register2 - register1;
}
或者我们可以改变我们的解析器以相反的顺序输出减法指令。现在,我们将在解析器中这样做,因为这是一个 Palladium 教程。=)
在 visitor.rs
中,我们可以改变:
&Token::SubtractionOperator => {
// TODO: Need to clean this up. Remove the unwraps.
let result_register = self.free_registers.pop().unwrap();
let left_register = self.used_registers.pop().unwrap();
let right_register = self.used_registers.pop().unwrap();
let line = format!("SUB ${} ${} ${}", left_register, right_register, result_register);
self.assembly.push(line);
self.used_registers.push(result_register);
self.free_registers.push(left_register);
self.free_registers.push(right_register);
},
变为:
&Token::SubtractionOperator => {
// TODO: Need to clean this up. Remove the unwraps.
let result_register = self.free_registers.pop().unwrap();
let left_register = self.used_registers.pop().unwrap();
let right_register = self.used_registers.pop().unwrap();
let line = format!("SUB ${} ${} ${}", right_register, left_register, result_register); // reverse the right and left
self.assembly.push(line);
self.used_registers.push(result_register);
self.free_registers.push(left_register);
self.free_registers.push(right_register);
},
现在我们的输出 (Palladium) 是:
".data"
".code"
"LOAD $0 #4"
"LOAD $1 #3"
"MUL $1 $0 $2"
"LOAD $0 #1"
"SUB $2 $0 $1" # 这里是 2 2 0 1, 在上面调换了 left 和 right
通过 Iridium REPL 运行它:
Welcome to Iridium! Let's be productive!
>>> LOAD $0 #4
LOAD $1 #3
>>> MUL $1 $0 $2
>>> LOAD $0 #1
>>> SUB $0 $2 $1 # 这里是 2 0 2 1, 实际上, 如果是通过调换 left, right 的方式来解决 sub 的问题的话, 这里应该输入 2 2 0 1
>>> !registers
Listing registers and all contents:
>>> [
1,
11,
12,
0,
// <snip>
]
好的!这次真的成功了!
结束
这部分到此为止!下次见!
(That’s it for this installment! See everyone next time!
)
原文出处:Extending Palladium: Part 1 - So You Want to Build a Language VM 作者名:Fletcher