Skip to content

意味解析

意味解析は、ソースコードが正しいかどうかをチェックするプロセスです。 ECMAScript仕様に記載されたすべての「早期エラー」ルールに対して確認する必要があります。

コンテキスト

[Yield][Await] のような文法コンテキストでは、文法がそれらを禁止している場合、エラーを発生させる必要があります。たとえば:

BindingIdentifier[Yield, Await] :
  Identifier
  yield
  await

13.1.1 静的意味論:早期エラー

BindingIdentifier[Yield, Await] : yield
*  この生成規則に [Yield] パラメータがある場合、構文エラーである。

* BindingIdentifier[Yield, Await] : await
  これが [Await] パラメータを持つ場合、構文エラーである。

以下のコードはエラーを発生させる必要があります:

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("ジェネレータ関数内で `yield` が識別子として不正に使用されています")
                .primary(p.cur_range(), ""),
        ),

スコープ

宣言に関するエラーについては:

14.2.1 静的意味論:早期エラー

ブロック : { StatementList }
* StatementList に含まれる「局所的に宣言された名前(LexicallyDeclaredNames)」に重複項目が含まれている場合、構文エラーである。
* StatementList に含まれる「局所的に宣言された名前(LexicallyDeclaredNames)」のいずれかが、同様に「var 宣言された名前(VarDeclaredNames)」にも存在する場合、構文エラーである。

スコープツリーを追加する必要があります。スコープツリーには、その中で宣言されたすべての 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 {
    /// [厳密モードコード](https://tc39.es/ecma262/#sec-strict-mode-code)
    /// [使用厳密指定ディレクティブ・プロローグ](https://tc39.es/ecma262/#sec-directive-prologues-and-the-use-strict-directive)
    pub strict_mode: bool,

    pub flags: ScopeFlags,

    /// [局所的に宣言された名前](https://tc39.es/ecma262/#sec-static-semantics-lexicallydeclarednames)
    pub lexical: IndexMap<Atom, SymbolId, FxBuildHasher>,

    /// [var 宣言された名前](https://tc39.es/ecma262/#sec-static-semantics-vardeclarednames)
    pub var: IndexMap<Atom, SymbolId, FxBuildHasher>,

    /// 関数宣言
    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) {
        // 関数用に厳密モードを継承
        // 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

INFO

このアプローチの欠点の一つは、アロー関数の場合、一時的なスコープを作成して、その後すぐに削除しなければならない可能性があるということです。 これは、アロー関数ではなくシーケンス式だった場合の処理に起因します。 詳細は カバー文法 を参照してください。

ビジター パターン

シンプルさのために、スコープツリーを別のパスで構築することにした場合、AST のすべてのノードを深さ優先前順走査して、スコープツリーを構築する必要があります。

各オブジェクトに対する操作とトラバーサルのプロセスを分離するために、ビジター パターン を利用できます。

訪問時に、スコープツリーを構築するために適切に enter_scopeleave_scope を呼び出すことができます。