本文将封装一些 Token 与 Node 共用的类型定义, 为后续做准备
源码: src/syntax_kind.rs
分析一下
我们的需求是要实现表达式的计算以及格式化, 这个计算需要支持正负数以及括号, 那么对我们有用的信息只有以下几种
- 数字 1-9
- "+"
- "-"
- "("
- ")"
那么我们其实完全可以直接穷举出所有不同的 Token 类型, 而且实际上确实是应该直接穷举
此外也可以顺便把 AST 上的节点类型顺手写了, 我们需要计算简单的四则运算, 那么就很想当然的需要数字节点和表达式节点, 而表达式节点中通过类型区分不同的计算即可, 总之, 我们需要这些东西
- 数字节点类型
- 加法表达式节点类型
- 减法表达式节点类型
- 乘法表达式节点类型
- 除法表达式节点类型
写一下
SyntaxKind 结构体
这里就非常简单的定义了一个元组结构体, 结构体中只保存一个 16 位无符号整型, 以此来区分不同的类型即可
#[derive(Debug, PartialOrd, PartialEq, Eq, Copy, Clone)]
pub struct SyntaxKind(pub u16);
// node
pub const NUM: SyntaxKind = SyntaxKind(5);
pub const ADD_EXPR: SyntaxKind = SyntaxKind(6);
pub const SUB_EXPR: SyntaxKind = SyntaxKind(7);
pub const MUL_EXPR: SyntaxKind = SyntaxKind(8);
pub const DIV_EXPR: SyntaxKind = SyntaxKind(9);
// token
pub const OPEN_PAREN: SyntaxKind = SyntaxKind(100);
pub const CLOSE_PAREN: SyntaxKind = SyntaxKind(101);
pub const PLUS: SyntaxKind = SyntaxKind(102);
pub const MINUS: SyntaxKind = SyntaxKind(103);
pub const STAR: SyntaxKind = SyntaxKind(104);
pub const SLASH: SyntaxKind = SyntaxKind(105);
// other
pub const UNKNOW: SyntaxKind = SyntaxKind(65534);
如上我们就快速定义出了我们所需的节点类型, 最后的 UNKNOW
类型是本来以为会在某些地方用到的兜底类型, 但是貌似没什么大用
实现一些简单的方法
上面特意将 SyntaxKind
定义为一个结构体就是为了给他添加关联方法, 以下简单写几个会用得到的工具函数
from_operator
用来将字符串类型的操作符转化为 SyntaxKind
类型, 返回值是一个 Option
impl SyntaxKind {
pub fn from_operator(str: &str) -> Option<SyntaxKind> {
let op = match str {
"(" => OPEN_PAREN,
")" => CLOSE_PAREN,
"+" => PLUS,
"-" => MINUS,
"*" => STAR,
"/" => SLASH,
_ => return None,
};
Some(op)
}
}
get_op_priority
获取操作符的优先级, 值越大, 优先级越高
impl SyntaxKind {
pub fn get_op_priority(str: &str) -> usize {
match str {
"*" | "/" => 2,
"+" | "-" => 1,
_ => usize::MAX,
}
}
}
into_str
将一个拥有所有权的 SyntaxKind
类型转化为对应的操作符
impl SyntaxKind {
pub fn into_str<'a>(self) -> &'a str {
match self {
OPEN_PAREN => "(",
CLOSE_PAREN => ")",
PLUS => "+",
MINUS => "-",
STAR => "*",
SLASH => "/",
_ => "unknow",
}
}
}
一个简单的宏
这里是一个简单的宏, 用来直接根据字符串生成对应的操作符 SyntaxKind
类型, 只是偷懒用的, 具体原因见 Q&A
#[macro_export]
macro_rules! token {
["("] => { $crate::syntax_kind::OPEN_PAREN };
[")"] => { $crate::syntax_kind::CLOSE_PAREN };
["+"] => { $crate::syntax_kind::PLUS };
["-"] => { $crate::syntax_kind::MINUS };
["*"] => { $crate::syntax_kind::STAR };
["/"] => { $crate::syntax_kind::SLASH };
}
其使用方式如下
#[test]
fn test_marco() {
assert_eq!(token!["("], OPEN_PAREN);
assert_eq!(token![")"], CLOSE_PAREN);
assert_eq!(token!["+"], PLUS);
assert_eq!(token!["-"], MINUS);
assert_eq!(token!["*"], STAR);
assert_eq!(token!["/"], SLASH);
}
总结一下
这里我们提前定义好了后面需要用到的一些类型, 非常简单, 下一篇文章进入本专栏第一个相对硬核的内容, 尽请期待
Q&A
Q: 为什么专门定义这个宏?
A: 一方面是不需要去记一些符号的命名, 这点在我写另一个项目的时候特别有用, 因为那里面有一大堆一大堆 Token, 另一个方面就是使用宏的话可以在实际编码的时候一定程度提高可读性, 比如 token!["+"]
和 PLUS
两者相比显然是使用宏的可读性更高, 再一个方面就是这个文件其实是我从我写的代码格式化工具里直接 copy 过来的, 改了改就直接用了, 就没留意发现可以去掉这个宏