跳至内容

语义分析

语义分析是检查源代码是否正确或不正确的过程。我们需要根据 ECMAScript 规范检查所有“早期错误”规则。

上下文

对于语法上下文,如 [Yield][Await],在语法禁止它们时需要引发错误,例如

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.

需要引发错误

javascript
async *
  function foo() {
    var yield, await;
  };

因为 AsyncGeneratorDeclarationAsyncGeneratorBody[+Yield][+Await]

AsyncGeneratorBody :
  FunctionBody[+Yield, +Await]

Biome 中檢查 yield 关键字的示例

rust
// 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(), ""),
        ),

范围

对于声明错误

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.

我们需要添加一个作用域树。作用域树包含在其中声明的所有 varlet。它还是一个指向父节点的树,我们可以在其中沿着树向上导航并在父作用域中搜索绑定标识符。我们可以使用的数据结构是 indextree

rust
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 通道中。

通常,需要一个 ScopeBuilder

rust
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_scopeleave_scope,例如在 acorn 中

javascript
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/statement.js#L425-L437

信息

这种方法的一个缺点是,对于箭头函数,我们可能需要创建一个临时作用域,然后在不是箭头函数而是序列表达式时将其删除。这在 覆盖语法 中有详细说明。

访问者模式

如果我们决定为了简单起见在另一重遍历中构建作用域树,那么 AST 中的每个节点都需要以深度优先前序遍历,并构建作用域树。

我们可以使用 访问者模式 将遍历过程与针对每个对象的执行的操作分离出来。

访问时,我们可以相应地调用 enter_scopeleave_scope 来构建作用域树。

在 MIT 许可下发布。