序言
这是 swc 源代码批判的第二部分,来探究 swc 的词法分析阶段。距离第一部分间隔了相当长的一段时间,期间很大一部分时间投入在 svgr 和 svgo 的 rust 版本开发,它们基于 swc:
开发期间向 swc 提交了 3 个 pr,来解决 swc 存在的一些问题:
- fix(xml/codegen): Escape
<
and>
in child - fix(xml/codegen): Fix wrong minification of spaces in a self-closing tag
- chore: colon colon token is not used in js
通过对 swc 源代码的更深入了解,我将继续撰写这一系列文章。
单词
将字符序列转换为单词(token)序列的过程称为词法分析。进行词法分析的程序或者函数叫作称为 lexer。lexer 可以将单词分为不同的类型,如 INT(整数)、ID(标识符)、FLOAT(浮点数)等。单词至少包含两个信息:单词类型(识别词法结构)和 lexer 为该单词匹配的文本。
在 swc 中,单词被定义为 Token
枚举,其代码位于 crates/swc_ecma_parser/src/token.rs
文件中。
rust
pub enum Token {
/// Identifier, "null", "true", "false".
///
/// Contains `null` and ``
Word(Word),
/// '=>'
Arrow,
/// '#'
Hash,
/// ...
}
为了降低单词文本分配和比较的性能开销,swc 利用 string_cache
crate 对 JavaScript、HTML、CSS 的关键字、标签名和属性名等进行内部化。稍后将详细介绍 string_cache
crate 的使用,现在你需要知道的是,在 swc 中通过 JsWord
结构体来使用这些内部化字符串。
swc 中还定义了另一个内部化字符串结构体Atom
。可以使用 AtomGenerator
、Atom::new
或 .into()
来创建 Atom
。如果你认为同样的值会被多次使用,使用 AtomGenerator
。 否则,使用 .into()
来创建一个 Atom
。
与 JsWord
的区别是,JsWord
是一个有 phf(完美哈希函数 Perfect Hash Function),支持全局内部化字符串,而 Atom
是一个局部内部化字符串。全局内部化可以减少内存使用,但全局化意味着存在互斥锁。因为这个互斥锁,Atom
在多线程环境下表现得更好。但由于缺少 phf 或全局内部化,Atom
的比较和哈希操作会比 JsWord
的慢。
下列情况应使用 Atom
而非 JsWord
:
- 长文本,不太可能被重复。
- 原始值。
string_cache crate
用于内部化 AsRef<str>
类型数据的库。一些字符串可以在编译时使用 string-cache-codegen
crate 进行内部化,或者可以使用没有编译时的内部化字符串 EmptyStaticAtomSet
。Atom
是给定集合(EmptyStaticAtomSet
或生成的 StaticAtomSet
)的内部化字符串。生成的 Atom
将具有关联的宏,以在编译时内部化静态字符串。
在 Cargo.toml
中:
toml
[dependencies]
string_cache = "0.8"
[dev-dependencies]
string_cache_codegen = "0.5"
在 build.rs
中:
rust
extern crate string_cache_codegen;
use std::env;
use std::path::Path;
fn main() {
string_cache_codegen::AtomType::new("foo::FooAtom", "foo_atom!")
.atoms(&["foo", "bar"])
.write_to_file(&Path::new(&env::var("OUT_DIR").unwrap()).join("foo_atom.rs"))
.unwrap()
}
在 lib.rs
中:
rust
extern crate string_cache;
mod foo {
include!(concat!(env!("OUT_DIR"), "/foo_atom.rs"));
}
fn use_the_atom(t: &str) {
match *t {
foo_atom!("foo") => println!("Found foo!"),
foo_atom!("bar") => println!("Found bar!"),
// foo_atom!("baz") => println!("Found baz!"), - 会在编译时报错
_ => {
println!("String not interned");
// We can intern strings at runtime as well
foo::FooAtom::from(t)
}
}
}
使用词法分析器
在 swc 中,词法分析器被定义为 Lexer
结构体,通过它的 new
方法进行构造,new
方法接收4个参数
- syntax - 需要解析的语法,可以是 ecmascript 或 typescript。
- target - 编译目标,默认为 es5。
- input - 需要解析的代码。
- comments - 引用类型,用于记录代码中的注解
rust
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(
FileName::Custom("test.js".into()),
"function foo() {}".into(),
)
let lexer = Lexer::new(
// 我们想解析 ecmascript
Syntax::Es(Default::default()),
// EsVersion 默认为 es5
Default::default(),
StringInput::from(&*fm),
None,
)
let TokenAndSpan = lexer.next(); // 读取当前的单词
Input trait 和 StringInput 结构体
swc 并没有直接使用 String
类型作为输入,而是定义了 Input
trait,包含了一系列方法,用于遍历、查询和修改输入字符串。这可能出于以下原因:
- 抽象 :
Input
trait 为处理输入字符串提供了一个抽象接口。它允许 swc 轻松地切换到不同类型的输入源,而无需更改大量代码。只要新的输入源实现了Input
trait,swc 就可以使用它来执行词法解析和其他文本处理任务。 - 灵活性 :
StringInput
结构体提供了一些特定于 swc 需求的方法,这些方法在标准的String
类型中可能不可用。例如,StringInput
结构体跟踪当前位置,提供了用于访问字符及其偏移量的方法等。而Input
trait 的抽象性,让 swc 可以针对自己的需求对输入源进行优化。 - 性能优化 :
StringInput
结构体可以针对 swc 的特定需求进行性能优化。例如,它使用str::CharIndices
迭代器来遍历字符串中的字符和相应的字节偏移量。这样,在处理 Unicode 字符串时,可以显著提高性能,因为它避免了多次计算字符和字节偏移量之间的关系。
swc 实现了一个名为 StringInput
的结构体,该结构体是 Input
trait 的实现,用于从 SourceFile
结构体中读取字符。
Lexer 结构体
rust
struct Lexer<'a, I: Input> {
comments: Option<&'a dyn Comments>,
/// [Some] if comment comment parsing is enabled. Otherwise [None]
comments_buffer: Option<CommentsBuffer>,
pub(crate) ctx: Context,
input: I,
start_pos: BytePos,
state: State,
pub(crate) syntax: Syntax,
pub(crate) target: EsVersion,
errors: Rc<RefCell<Vec<Error>>>,
module_errors: Rc<RefCell<Vec<Error>>>,
atoms: Rc<RefCell<AtomGenerator>>,
buf: Rc<RefCell<String>>,
}
Lexer
结构体中的关键方法是 read_token
,其功能与 babel 中的 getTokenFromCode
方法相似,即读取源代码文本,解析并返回 Token
结构体。read_token
方法的代码组织方式颇具创意,它参考了 ratel-rust 项目,使用名为 BYTE_HANDLERS
(类型为 [ByteHandler; 256]
)的表来查找处理每个 ASCII 字符的函数。相比于使用 switch case 形式,这种方法可能会具有更好的性能,我不确定。
rust
fn read_token(&mut self) -> LexResult<Option<Token>> {
let byte = match self.input.as_str().as_bytes().first() {
Some(&v) => v,
None => return Ok(None),
};
let handler = unsafe { *(&BYTE_HANDLERS as *const ByteHandler).offset(byte as isize) };
match handler {
Some(handler) => handler(self),
None => {
let start = self.cur_pos();
self.input.bump_bytes(1);
self.error_span(
pos_span(start),
SyntaxError::UnexpectedChar { c: byte as _ },
)
}
}
}
BYTE_HANDLERS
定义在 swc_ecma_parser/src/lexer/table.rs
文件中,代码如下:
rust
/// 查找表,将任何输入字节映射到下面定义的处理函数。
pub(super) static BYTE_HANDLERS: [ByteHandler; 256] = [
// 0 1 2 3 4 5 6 7 8 9 A B C D E F //
EOF, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, // 0
___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, ___, // 1
___, EXL, QOT, HSH, IDT, PRC, AMP, QOT, PNO, PNC, ATR, PLS, COM, MIN, PRD, SLH, // 2
ZER, DIG, DIG, DIG, DIG, DIG, DIG, DIG, DIG, DIG, COL, SEM, LSS, EQL, MOR, QST, // 3
AT_, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, // 4
IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, BTO, IDT, BTC, CRT, IDT, // 5
TPL, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, // 6
IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, IDT, BEO, PIP, BEC, TLD, ERR, // 7
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // 8
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // 9
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // A
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // B
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // C
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // D
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // E
UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, UNI, // F
];
此数组将所有可能的字节值(0-255)映射到各自的处理函数。表中的每一行代表 16 个字节值,每一列代表这些值在当前行之内的位置。下面是一些处理函数的示例:
EOF
:处理空字符(ASCII 值为 0)。___
:处理非法字符。这是一个默认处理函数,用于处理未定义特殊行为的字符。DIG
:处理数字(ASCII 值在 48-57 之间,对应 '0' 到 '9')。
其他处理函数也以类似的方式工作,处理各种字符类型,如字母、标点符号等。
Span 结构体
从语法分析器的角度看,仅提供词法分析器产生的单词 Token
是不够的。它还需要知道每个单词在源代码中的确切位置。这样,当出现错误、警告或其他反馈时,能够准确地指向源代码中的对应位置。Span
结构体的作用就是存储这些单词的位置信息。
rust
pub struct Span {
/// 表示源代码片段的起始字节位置。
pub lo: BytePos,
/// 表示源代码片段的结束字节位置。
pub hi: BytePos,
/// 表示与宏扩展相关的上下文信息。如果这个代码片段是由宏扩展创建的,此字段包含宏来源的信息。
pub ctxt: SyntaxContext,
}
为了进行更详细的处理,Lexer
在实现 Iterator
trait 的 next
方法时,增强了 Token
,通过添加 Span
结构体来包含位置信息,从而将返回结果优化为 TokenAndSpan
。
rust
#[derive(Debug, Clone, PartialEq)]
pub struct TokenAndSpan {
pub token: Token,
/// 在这个单词前有换行符吗?
pub had_line_break: bool,
pub span: Span,
}