语义分析
🌐 Semantic Analysis
语义分析是检查我们的源代码是否正确的过程。我们需要根据 ECMAScript 规范中的所有“早期错误”规则进行检查。
🌐 Semantic analysis is the process of checking whether our source code is correct or not. We need to check against all the "Early Error" rules in the ECMAScript specification.
上下文
🌐 Context
对于诸如 [Yield] 或 [Await] 的语法环境,当语法不允许时,需要引发错误,例如:
🌐 For grammar contexts such as [Yield] or [Await], an error need to be raised when the grammar forbids them, for example:
BindingIdentifier[Yield, Await] :
Identifier
yield
await
13.1.1 Static Semantics: Early Errors
BindingIdentifier[Yield, Await] : yield
* It is a Syntax Error if this production has a [Yield] parameter.
* BindingIdentifier[Yield, Await] : await
It is a Syntax Error if this production has an [Await] parameter.需要引发一个错误
🌐 need to raise an error for
async function* foo() {
var yield, await;
}因为 AsyncGeneratorDeclaration 对 AsyncGeneratorBody 有 [+Yield] 和 [+Await]:
🌐 because AsyncGeneratorDeclaration has [+Yield] and [+Await] for AsyncGeneratorBody:
AsyncGeneratorBody :
FunctionBody[+Yield, +Await]在 Biome 中检查 yield 关键字的示例:
🌐 An example in Biome checking for the yield keyword:
// https://github.com/rome/tools/blob/5a059c0413baf1d54436ac0c149a829f0dfd1f4d/crates/rome_js_parser/src/syntax/expr.rs#L1368-L1377
pub(super) fn parse_identifier(p: &mut Parser, kind: JsSyntaxKind) -> ParsedSyntax {
if !is_at_identifier(p) {
return Absent;
}
let error = match p.cur() {
T![yield] if p.state.in_generator() => Some(
p.err_builder("Illegal use of `yield` as an identifier in generator function")
.primary(p.cur_range(), ""),
),范围
🌐 Scope
对于声明错误:
🌐 For declaration errors:
14.2.1 Static Semantics: Early Errors
Block : { StatementList }
* It is a Syntax Error if the LexicallyDeclaredNames of StatementList contains any duplicate entries.
* It is a Syntax Error if any element of the LexicallyDeclaredNames of StatementList also occurs in the VarDeclaredNames of StatementList.我们需要添加一个作用域树。作用域树包含了所有在其中声明的 var 和 let。它也是一个指向父节点的树,在这里我们希望向上遍历树,并在父作用域中搜索绑定标识符。我们可以使用的数据结构是 indextree。
🌐 We need to add a scope tree. A scope tree has all the vars and lets declared inside it. It is also a parent pointing tree where we want to navigate up the tree and search for binding identifiers in parent scopes. The data structure we can use is a indextree.
use indextree::{Arena, Node, NodeId};
use bitflags::bitflags;
pub type Scopes = Arena<Scope>;
pub type ScopeId = NodeId;
bitflags! {
#[derive(Default)]
pub struct ScopeFlags: u8 {
const TOP = 1 << 0;
const FUNCTION = 1 << 1;
const ARROW = 1 << 2;
const CLASS_STATIC_BLOCK = 1 << 4;
const VAR = Self::TOP.bits | Self::FUNCTION.bits | Self::CLASS_STATIC_BLOCK.bits;
}
}
#[derive(Debug, Clone)]
pub struct Scope {
/// [Strict Mode Code](https://tc39.es/ecma262/#sec-strict-mode-code)
/// [Use Strict Directive Prologue](https://tc39.es/ecma262/#sec-directive-prologues-and-the-use-strict-directive)
pub strict_mode: bool,
pub flags: ScopeFlags,
/// [Lexically Declared Names](https://tc39.es/ecma262/#sec-static-semantics-lexicallydeclarednames)
pub lexical: IndexMap<Atom, SymbolId, FxBuildHasher>,
/// [Var Declared Names](https://tc39.es/ecma262/#sec-static-semantics-vardeclarednames)
pub var: IndexMap<Atom, SymbolId, FxBuildHasher>,
/// Function Declarations
pub function: IndexMap<Atom, SymbolId, FxBuildHasher>,
}作用域树可以为了性能原因在解析器内部构建,也可以在单独的 AST 遍历中构建。
🌐 The scope tree can either be built inside the parser for performance reasons, or built-in a separate AST pass.
通常,需要一个 ScopeBuilder:
🌐 Generally, a ScopeBuilder is needed:
pub struct ScopeBuilder {
scopes: Scopes,
root_scope_id: ScopeId,
current_scope_id: ScopeId,
}
impl ScopeBuilder {
pub fn current_scope(&self) -> &Scope {
self.scopes[self.current_scope_id].get()
}
pub fn enter_scope(&mut self, flags: ScopeFlags) {
// Inherit strict mode for functions
// https://tc39.es/ecma262/#sec-strict-mode-code
let mut strict_mode = self.scopes[self.root_scope_id].get().strict_mode;
let parent_scope = self.current_scope();
if !strict_mode
&& parent_scope.flags.intersects(ScopeFlags::FUNCTION)
&& parent_scope.strict_mode
{
strict_mode = true;
}
let scope = Scope::new(flags, strict_mode);
let new_scope_id = self.scopes.new_node(scope);
self.current_scope_id.append(new_scope_id, &mut self.scopes);
self.current_scope_id = new_scope_id;
}
pub fn leave_scope(&mut self) {
self.current_scope_id = self.scopes[self.current_scope_id].parent().unwrap();
}
}然后我们在解析函数中相应地调用 enter_scope 和 leave_scope,例如在 acorn 中:
🌐 We then call enter_scope and leave_scope accordingly inside the parse functions, for example in acorn:
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/statement.js#L425-L437INFO
这种方法的一个缺点是,对于箭头函数,如果它不是箭头函数而是一个序列表达式,我们可能需要创建一个临时作用域,然后在之后删除它。详细内容见 cover grammar。
访问者模式
🌐 The Visitor Pattern
如果我们为了简化而决定在另一次遍历中构建作用域树,那么需要对 AST 中的每个节点进行深度优先前序访问,并构建作用域树。
🌐 If we decide to build the scope tree in another pass for simplicity, then every node in the AST need to be visited in depth-first preorder and build the scope tree.
我们可以使用访问者模式将遍历过程与对每个对象执行的操作分开。
🌐 We can use the Visitor Pattern to separate out the traversal process from the operations performed on each object.
访问时,我们可以相应地调用 enter_scope 和 leave_scope 来构建作用域树。
🌐 Upon visit, we can call enter_scope and leave_scope accordingly to build the scope tree.
