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("generator 함수 내에서 `yield`를 식별자로 사용하는 것은 불법입니다")
                .primary(p.cur_range(), ""),
        ),

스코프

선언 오류에 대한 예시:

14.2.1 정적 의미: 조기 오류

블록 : { StatementList }
* StatementList의 LexicallyDeclaredNames에 중복 항목이 포함되어 있으면 구문 오류이다.
* StatementList의 LexicallyDeclaredNames에 포함된 어떤 요소도 그 자체의 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를 호출할 수 있습니다.