Skip to content

문법

자바스크립트는 파싱하기 가장 도전적인 문법 중 하나를 가지고 있으며, 이 튜토리얼은 제가 이 문법을 배우면서 겪은 모든 고통과 고민을 자세히 다룹니다.

LL(1) 문법

위키피디아에 따르면,

LL 문법은 복수의 구문 분석기를 사용해 입력을 왼쪽에서 오른쪽으로 파싱할 수 있는 문맥 독립 문법이다.

첫 번째 L은 소스를 왼쪽에서 오른쪽으로 스캔한다는 의미이며, 두 번째 L왼쪽 최초 유도 트리를 구성한다는 의미입니다.

문맥 독립성과 (1)은 단지 다음 토큰을 살펴보기만 하면 트리를 생성할 수 있음을 의미합니다.

학계에서는 특히 중요한 관심 대상이 되는 것이 바로, 인간은 게으르기 때문에 수동적으로 파서를 작성하지 않고 자동으로 파서를 생성하는 프로그램을 작성하고 싶어하기 때문입니다.

불행하게도 대부분의 산업용 프로그래밍 언어는 깔끔한 LL(1) 문법을 가지지 않으며, 자바스크립트 역시 예외가 아닙니다.

INFO

몇 년 전 모질라가 jsparagus 프로젝트를 시작했고, 파이썬으로 LALR 파서 생성기를 작성했습니다. 지난 2년 동안 거의 업데이트되지 않았으며, js-quirks.md의 끝부분에서 강력한 메시지를 전달했습니다.

오늘 우리는 무엇을 배웠나요?

  • 자바스크립트 파서를 작성하지 마십시오.
  • 자바스크립트에는 몇 가지 문법적 공포가 있습니다. 하지만, 모든 실수를 피해서 세계에서 가장 널리 사용되는 프로그래밍 언어를 만들 수는 없습니다. 오히려 올바른 상황에서, 올바른 사용자를 위한 사용 가능한 도구를 제공함으로써 성공합니다.

자바스크립트를 파싱하는 유일한 실용적인 방법은 그 문법의 특성상 수동으로 재귀 내림형 파서를 작성하는 것입니다. 따라서 우리 자신을 발목에 총을 쏘는 일을 피하기 위해 문법의 모든 특이점을 먼저 알아야 합니다.

아래 목록은 간단한 것부터 시작하여 점점 이해하기 어려워질 것입니다. 따라서 커피 한 잔을 들고 천천히 읽어주시기 바랍니다.

식별자

#sec-identifiers에 정의된 세 가지 유형의 식별자가 존재합니다.

IdentifierReference[Yield, Await] :
BindingIdentifier[Yield, Await] :
LabelIdentifier[Yield, Await] :

estree와 일부 추상구문 트리는 위의 식별자를 구분하지 않습니다. 또한 사양 문서는 이를 평문으로 설명하지 않습니다.

BindingIdentifier는 선언이고 IdentifierReference는 바인딩 식별자에 대한 참조입니다. 예를 들어 var foo = bar에서 fooBindingIdentifier, barIdentifierReference입니다:

VariableDeclaration[In, Yield, Await] :
    BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt

Initializer[In, Yield, Await] :
    = AssignmentExpression[?In, ?Yield, ?Await]

AssignmentExpressionPrimaryExpression으로 타고 들어가면 다음과 같습니다.

PrimaryExpression[Yield, Await] :
    IdentifierReference[?Yield, ?Await]

AST에서 이러한 식별자를 다르게 선언하면 후속 도구, 특히 의미 분석 도구를 크게 단순화할 수 있습니다.

rust
pub struct BindingIdentifier {
    pub node: Node,
    pub name: Atom,
}

pub struct IdentifierReference {
    pub node: Node,
    pub name: Atom,
}

클래스와 엄격 모드

에크마스크립트 클래스는 엄격 모드 이후에 탄생했기 때문에, 클래스 내부의 모든 내용은 단순화를 위해 항상 엄격 모드여야 한다고 결정했습니다. #sec-class-definitions에 명시되어 있으며, 단지 "노드: 클래스 정의는 항상 엄격 모드 코드이다."라고만 기술되어 있습니다.

엄격 모드를 함수 범위와 연결함으로써 쉽게 선언할 수 있지만, class 선언에는 범위가 없기 때문에, 클래스 파싱을 위해 추가 상태를 유지해야 합니다.

rust
// https://github.com/swc-project/swc/blob/f9c4eff94a133fa497778328fa0734aa22d5697c/crates/swc_ecma_parser/src/parser/class_and_fn.rs#L85
fn parse_class_inner(
    &mut self,
    _start: BytePos,
    class_start: BytePos,
    decorators: Vec<Decorator>,
    is_ident_required: bool,
) -> PResult<(Option<Ident>, Class)> {
    self.strict_mode().parse_with(|p| {
        expect!(p, "class");

오래된 8진수 및 'use strict'

#sec-string-literals-early-errors는 문자열 내부의 이스케이프된 오래된 8진수("\01")를 금지합니다:

EscapeSequence ::
    LegacyOctalEscapeSequence
    NonOctalDecimalEscapeSequence

이 생산 규칙에 해당하는 소스 텍스트가 엄격 모드 코드인 경우 구문 오류이다.

이러한 오류를 감지하는 가장 좋은 장소는 리커로서, 파서로부터 엄격 모드 상태를 묻고 해당 상태에 따라 오류를 발생시키는 것입니다.

그러나 지시어와 함께 혼합될 경우 이는 불가능해집니다:

javascript
https://github.com/tc39/test262/blob/747bed2e8aaafe8fdf2c65e8a10dd7ae64f66c47/test/language/literals/string/legacy-octal-escape-sequence-prologue-strict.js#L16-L19

use strict는 이스케이프된 오래된 8진수 이후에 선언되었지만, 구문 오류는 여전히 발생해야 합니다. 다행히도 실제 코드에서는 오래된 8진수와 지시어를 함께 사용하는 경우는 거의 없습니다... 다만 위의 테스트262 사례를 통과시키고자 한다면 예외가 될 수 있습니다.


단순하지 않은 매개변수 및 엄격 모드

비엄격 모드에서는 동일한 함수 매개변수가 허용됩니다: function foo(a, a) { }, 그리고 이를 방지하려면 use strict를 추가할 수 있습니다: function foo(a, a) { "use strict" }. 이후 에스6에서는 다른 문법이 함수 매개변수에 추가되었습니다. 예를 들어 function foo({ a }, b = c) {}.

이제 "01"이 엄격 모드 오류인 경우 아래와 같이 작성하면 어떻게 될까요?

javaScript
function foo(
  value = (function() {
    return "\01";
  }()),
) {
  "use strict";
  return value;
}

더 구체적으로 말하자면, 파서 관점에서 보면 매개변수 내부에 엄격 모드 구문 오류가 있을 때 어떻게 해야 할까요? #sec-function-definitions-static-semantics-early-errors에서는 다음과 같이 금지합니다:

FunctionDeclaration :
FunctionExpression :

FunctionBody가 "use strict"를 포함하고 있고, FormalParameters의 IsSimpleParameterList가 거짓인 경우 구문 오류이다.

크롬은 이상한 메시지로 이 오류를 던집니다: "Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list".

더 깊은 설명은 이 블로그 포스트에 나와 있으며, ESLint 개발자의 글입니다.

INFO

흥미로운 사실, 위의 규칙은 타입스크립트에서 es5를 대상으로 하는 경우 적용되지 않습니다. 이것은 다음과 같이 트랜스파일됩니다:

javaScript
function foo(a, b) {
  "use strict";
  if (b === void 0) b = "\01";
}

괄호 표현식

괄호 표현식은 특별한 의미가 없다고 가정되어야 할까요? 예를 들어 ((x))의 추상구문 트리는 단순히 하나의 IdentifierReference여야 하고, ParenthesizedExpression -> ParenthesizedExpression -> IdentifierReference처럼 중첩되어서는 안 됩니다. 이는 자바스크립트 문법에서도 마찬가지입니다.

그러나... 누구도 예상하지 못했지만, 이는 런타임 의미를 가질 수 있다는 사실이 발견되었습니다. 이 estree 이슈에서 보듯이,

javascript
> fn = function () {};
> fn.name
< "fn"

> (fn) = function () {};
> fn.name
< ''

결국 애콘과 벨트는 호환성을 위해 preserveParens 옵션을 추가했습니다.


조건문 내 함수 선언

#sec-ecmascript-language-statements-and-declarations의 문법을 정확히 따르면:

Statement[Yield, Await, Return] :
    ... 많은 문장들

Declaration[Yield, Await] :
    ... 선언들

우리가 정의한 Statement 노드는 분명히 Declaration을 포함하지 않을 것입니다.

그러나 부록 B #sec-functiondeclarations-in-ifstatement-statement-clauses에서는 비엄격 모드에서 조건문의 문장 위치에 선언을 허용합니다:

javascript
if (x) {
  function foo() {}
} else function bar() {}

레이블 문은 유효하다

우리는 아마도 한 줄의 레이블 문을 본 적이 없을 것입니다. 그러나 현대 자바스크립트에서는 유효하며 엄격 모드에서도 금지되지 않습니다.

다음 구문은 올바르며, 객체 리터럴이 아니라 레이블 문을 반환합니다.

javascript
<Foo
  bar={() => {
    baz: "quaz";
  }}
/>
//   ^^^^^^^^^^^ `LabelledStatement`

let은 키워드가 아니다

let은 키워드가 아니므로, 문법에서 명시적으로 허용되지 않는 위치가 아니라면 어디에든 등장할 수 있습니다. 파서는 let 토큰 뒤의 토큰을 미리 보고, 그것이 어떤 것으로 파싱되어야 할지를 결정해야 합니다. 예를 들어:

javascript
let a;
let = foo;
let instanceof x;
let + 1;
while (true) let;
a = let[0];

for-in / for-of 및 [In] 컨텍스트

#prod-ForInOfStatementfor-infor-of 문법을 살펴보면, 어떻게 파싱해야 할지 바로 이해하기 어렵습니다.

이를 이해하는 데 두 가지 주요 장애물이 있습니다: [lookahead ≠ let] 부분과 [+In] 부분.

for (let까지 파싱한 후에는 미리 보는 토큰이 다음 조건을 만족해야 합니다:

  • in이 아니어야 함 (즉, for (let in은 허용되지 않음)
  • {, [ 또는 식별자여야 함 (즉, for (let {} = foo), for (let [] = foo)for (let bar = foo)는 허용됨)

of 또는 in 키워드에 도달한 후에는 우변 표현식을 올바른 [+In] 컨텍스트로 전달하여 #prod-RelationalExpression에서 두 가지 in 표현식을 금지해야 합니다:

RelationalExpression[In, Yield, Await] :
    [+In] RelationalExpression[+In, ?Yield, ?Await] in ShiftExpression[?Yield, ?Await]
    [+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]

참고 2: [In] 문법 매개변수는 관계식 내의 `in` 연산자와 조건문 내의 `in` 연산자를 혼동하는 것을 막기 위해 필요하다.

그리고 이는 사양 전체에서 [In] 컨텍스트가 사용되는 유일한 경우입니다.

또한 주의할 점은, 문법 [lookahead ∉ { let, async of }]for (async of ...)를 금지하며, 이는 명시적으로 보호해야 합니다.


블록 단위 함수 선언

부록 B.3.2 #sec-block-level-function-declarations-web-legacy-compatibility-semantics에서는 FunctionDeclarationBlock 문에서 어떻게 행동해야 하는지 설명하는 전용 페이지가 마련되어 있습니다. 요약하면,

javascript
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/scope.js#L30-L35

함수 선언의 이름은 그 함수 선언 내부에 있다면 var 선언과 동일하게 처리되어야 합니다. 이 코드 조각은 bar가 블록 범위 내에 있기 때문에 재선언 오류로 인해 실패합니다:

javascript
function foo() {
  if (true) {
    var bar;
    function bar() {} // 재선언 오류
  }
}

반면, 다음은 오류가 나지 않습니다. 왜냐하면 함수 범위 내에 있기 때문이며, 함수 barvar 선언처럼 취급되기 때문입니다:

javascript
function foo() {
  var bar;
  function bar() {}
}

문법 컨텍스트

구문 문법은 특정 구문을 허용하거나 금지하기 위한 5개의 컨텍스트 매개변수를 가집니다. 즉 [In], [Return], [Yield], [Await], [Default]입니다.

문법을 파싱하는 동안 컨텍스트를 유지하는 것이 가장 좋습니다. 예를 들어 바이옴(Биоме)에서는 다음과 같습니다:

rust
// https://github.com/rome/tools/blob/5a059c0413baf1d54436ac0c149a829f0dfd1f4d/crates/rome_js_parser/src/state.rs#L404-L425

pub(crate) struct ParsingContextFlags: u8 {
    /// 파서가 `function* a() {}` 같은 제네레이터 함수 내에 있는지 여부
    /// 에크마 스펙의 `Yield` 매개변수와 일치
    const IN_GENERATOR = 1 << 0;
    /// 파서가 함수 내부에 있는지 여부
    const IN_FUNCTION = 1 << 1;
    /// 파서가 생성자 내부에 있는지 여부
    const IN_CONSTRUCTOR = 1 << 2;

    /// 이 컨텍스트에서 `async`가 허용되는지 여부. 또는 비동기 함수이거나 상위 수준의 `await`이 지원되는 경우.
    /// 에크마 스펙의 `Async` 생성자와 동일
    const IN_ASYNC = 1 << 3;

    /// 파서가 상위 수준의 문장(클래스, 함수, 매개변수 내부가 아님)을 파싱하고 있는지 여부
    const TOP_LEVEL = 1 << 4;

    /// 파서가 반복문 또는 스위치 문 내부에 있으며, `break`가 허용되는지 여부
    const BREAK_ALLOWED = 1 << 5;

    /// 파서가 반복문 내부에 있으며, `continue`가 허용되는지 여부
    const CONTINUE_ALLOWED = 1 << 6;

이 플래그들을 문법에 따라 설정하고 확인하세요.

할당 패턴과 바인딩 패턴

estree에서는 AssignmentExpression의 왼쪽 항목은 Pattern입니다:

extend interface AssignmentExpression {
    left: Pattern;
}

그리고 VariableDeclarator의 왼쪽 항목도 Pattern입니다:

interface VariableDeclarator <: Node {
    type: "VariableDeclarator";
    id: Pattern;
    init: Expression | null;
}

PatternIdentifier, ObjectPattern, ArrayPattern일 수 있습니다:

interface Identifier <: Expression, Pattern {
    type: "Identifier";
    name: string;
}

interface ObjectPattern <: Pattern {
    type: "ObjectPattern";
    properties: [ AssignmentProperty ];
}

interface ArrayPattern <: Pattern {
    type: "ArrayPattern";
    elements: [ Pattern | null ];
}

그러나 사양 관점에서는 다음과 같은 자바스크립트가 존재합니다:

javascript
// AssignmentExpression:
{ foo } = bar;
  ^^^ IdentifierReference
[ foo ] = bar;
  ^^^ IdentifierReference

// VariableDeclarator
var { foo } = bar;
      ^^^ BindingIdentifier
var [ foo ] = bar;
      ^^^ BindingIdentifier

이제부터 혼란스럽기 시작합니다. 왜냐하면 Pattern 내부의 IdentifierBindingIdentifier인지 IdentifierReference인지 직접 구분할 수 없기 때문입니다:

rust
enum Pattern {
    Identifier, // 이건 `BindingIdentifier`인지 `IdentifierReference`인가요?
    ArrayPattern,
    ObjectPattern,
}

이는 파서 파이프라인의 이후 단계에서 불필요한 코드로 이어집니다. 예를 들어 의미 분석을 위해 스코프를 설정할 때, 이 Identifier의 부모를 검사하여 스코프에 바인딩해야 할지 여부를 판단해야 합니다.

더 나은 해결책은 사양을 완전히 이해하고, 무엇을 해야 할지를 결정하는 것입니다.

AssignmentExpressionVariableDeclaration의 문법은 다음과 같이 정의됩니다:

13.15 할당 연산자

AssignmentExpression[In, Yield, Await] :
    LeftHandSideExpression[?Yield, ?Await] = AssignmentExpression[?In, ?Yield, ?Await]

13.15.5 구조화 할당

일부 상황에서, `AssignmentExpression : LeftHandSideExpression = AssignmentExpression`의 인스턴스를 처리할 때, `LeftHandSideExpression`의 해석은 다음 문법을 사용하여 개선됩니다:

AssignmentPattern[Yield, Await] :
    ObjectAssignmentPattern[?Yield, ?Await]
    ArrayAssignmentPattern[?Yield, ?Await]
14.3.2 변수 문

VariableDeclaration[In, Yield, Await] :
    BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]opt
    BindingPattern[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]

사양은 이 두 문법을 별도로 정의함으로써 AssignmentPatternBindingPattern을 구분합니다.

따라서 이런 상황에서는 estree에서 벗어나 파서를 위한 추가 AST 노드를 정의하는 것을 두려워하지 마십시오:

rust
enum BindingPattern {
    BindingIdentifier,
    ObjectBindingPattern,
    ArrayBindingPattern,
}

enum AssignmentPattern {
    IdentifierReference,
    ObjectAssignmentPattern,
    ArrayAssignmentPattern,
}

저는 일주일 동안 매우 혼란스러운 상태였다가 마침내 각성이 왔습니다: 단 하나의 Pattern 노드 대신 AssignmentPattern 노드와 BindingPattern 노드를 정의해야 한다는 것을 깨달았습니다.

  • estree는 반드시 옳아야 한다. 사람들이 수년간 사용해왔으니 틀릴 수 없겠지?
  • 두 개의 별개 노드를 정의하지 않고 패턴 내부의 Identifier들을 어떻게 명확히 구분할 수 있을까? 문법이 어디에 있는지 찾을 수가 없었다.
  • 하루 종일 사양을 뒤져보던 끝에, AssignmentPattern의 문법은 "13.15 할당 연산자" 섹션의 다섯 번째 하위 섹션, "보완적 문법(Supplemental Syntax)"에 나와 있었습니다 🤯 - 정말 이상한 위치입니다. 모든 문법은 주 섹션에 정의되는데, 이건 "런타임 의미론(Runtime Semantics)" 섹션 이후에 정의되어 있기 때문입니다.

TIP

다음 사례들은 정말 이해하기 어렵습니다. 여기엔 위험한 일이 숨어 있습니다.

모호한 문법

먼저 파서의 입장에서 생각해봅시다. / 토큰이 나눗셈 연산자인지, 정규표현식의 시작인지 어떻게 알 수 있을까요?

javascript
a / b;
a / / regex /;
a /= / regex /;
/ regex / / b;
/=/ / /=/;

이 문제를 해결하는 것은 거의 불가능하지 않습니까? 하나씩 분해해서 문법을 따라가 봅시다.

우선 우리가 알아야 할 점은, #sec-ecmascript-language-lexical-grammar에 명시된 바와 같이 구문 문법이 리커클 문법을 이끌고 있다는 것입니다.

입력 요소의 식별이 구문 문법 컨텍스트에 민감한 여러 상황이 있습니다.

이는 파서가 리커에게 다음에 어떤 토큰을 반환해야 할지를 결정해야 한다는 의미입니다. 위 예시는 리커가 / 토큰 또는 RegExp 토큰을 반환해야 한다는 것을 시사합니다. 올바른 / 또는 RegExp 토큰을 얻기 위해 사양은 다음과 같이 말합니다:

RegularExpressionLiteral이 허용되는 모든 구문 문법 컨텍스트에서 InputElementRegExp 목표 기호를 사용합니다... 기타 모든 컨텍스트에서는 InputElementDiv가 리커클 목표 기호로 사용됩니다.

그리고 InputElementDivInputElementRegExp의 문법은 다음과 같습니다.

InputElementDiv ::
    WhiteSpace
    LineTerminator
    Comment
    CommonToken
    DivPunctuator <---------- `/` 및 `/=` 토큰
    RightBracePunctuator

InputElementRegExp ::
    WhiteSpace
    LineTerminator
    Comment
    CommonToken
    RightBracePunctuator
    RegularExpressionLiteral <-------- `RegExp` 토큰

이는 문법이 RegularExpressionLiteral에 도달할 때마다 /RegExp 토큰으로 토큰화되어야 하며, 매칭되는 /가 없는 경우 오류를 발생시켜야 함을 의미합니다. 그 외 모든 경우는 /를 슬래시 토큰으로 토큰화합니다.

예를 들어 살펴보겠습니다:

a / / regex /
^ ------------ PrimaryExpression:: IdentifierReference
  ^ ---------- MultiplicativeExpression: MultiplicativeExpression MultiplicativeOperator ExponentiationExpression
    ^^^^^^^^ - PrimaryExpression: RegularExpressionLiteral

이 문장은 다른 Statement의 시작과 일치하지 않기 때문에, ExpressionStatement 경로를 따라갑니다.

ExpressionStatement --> Expression --> AssignmentExpression --> ... --> MultiplicativeExpression --> ... --> MemberExpression --> PrimaryExpression --> IdentifierReference.

우리는 IdentifierReference에서 멈추고 RegularExpressionLiteral에서는 멈추지 않았습니다. 따라서 "기타 모든 컨텍스트에서는 InputElementDiv가 리커클 목표 기호로 사용된다"는 규칙이 적용됩니다. 첫 번째 슬래시는 DivPunctuator 토큰입니다.

DivPunctuator 토큰이 있으므로, 문법 MultiplicativeExpression: MultiplicativeExpression MultiplicativeOperator ExponentiationExpression이 매칭됩니다. 우변은 ExponentiationExpression이 기대됩니다.

이제 a / /의 두 번째 슬래시에 도달했습니다. ExponentiationExpression을 따라가면, RegularExpressionLiteral/와 매칭되는 유일한 문법이므로, PrimaryExpression: RegularExpressionLiteral에 도달합니다:

RegularExpressionLiteral ::
    / RegularExpressionBody / RegularExpressionFlags

두 번째 /는 사양이 "구문 문법 컨텍스트에서 RegularExpressionLiteral이 허용되는 모든 곳에서 InputElementRegExp 목표 기호를 사용한다"고 말했기 때문에 RegExp로 토큰화됩니다.

INFO

연습으로 /=/ / /=/의 문법을 따라가 보세요.


커버 문법

먼저 이 주제에 대한 V8 블로그 포스트를 읽어보세요.

요약하면, 사양은 다음 세 가지 커버 문법을 제시합니다.

CoverParenthesizedExpressionAndArrowParameterList

PrimaryExpression[Yield, Await] :
    CoverParenthesizedExpressionAndArrowParameterList[?Yield, ?Await]

현재 생산 규칙
PrimaryExpression[Yield, Await] : CoverParenthesizedExpressionAndArrowParameterList[?Yield, ?Await]
을 처리할 때, `CoverParenthesizedExpressionAndArrowParameterList`의 해석은 다음 문법을 사용하여 개선됩니다:

ParenthesizedExpression[Yield, Await] :
    ( Expression[+In, ?Yield, ?Await] )
ArrowFunction[In, Yield, Await] :
    ArrowParameters[?Yield, ?Await] [no LineTerminator here] => ConciseBody[?In]

ArrowParameters[Yield, Await] :
    BindingIdentifier[?Yield, ?Await]
    CoverParenthesizedExpressionAndArrowParameterList[?Yield, ?Await]

이 정의는 다음과 같은 것을 정의합니다:

javascript
let foo = (a, b, c); // SequenceExpression
let bar = (a, b, c) => {}; // ArrowExpression
          ^^^^^^^^^ CoverParenthesizedExpressionAndArrowParameterList

이 문제를 해결하는 간단하지만 번거로운 접근 방식은 먼저 Vec<Expression>로 파싱한 후, 이를 ArrowParameters 노드로 변환하는 변환 함수를 작성하는 것입니다. 즉 각각의 ExpressionBindingPattern으로 변환되어야 합니다.

이때 주의할 점은, 만약 파서 내에서 스코프 트리를 구축한다면, 예를 들어 화살표 표현식 동안 스코프를 생성하지만, 시퀀스 표현식은 생성하지 않는다면, 이를 어떻게 수행할지 명확하지 않습니다. esbuild는 이 문제를 임시 스코프를 먼저 생성한 후, 화살표 표현식이 아닌 경우 삭제함으로써 해결했습니다.

이는 아키텍처 문서에 다음과 같이 설명되어 있습니다:

대부분은 아주 간단하지만, 파서가 스코프를 푸시하고 선언을 파싱하는 중간에 실제로 선언이 아니라는 것을 알게 되는 몇 군데가 있습니다. 이는 타입스크립트에서 함수가 본체 없이 선언된 경우, 그리고 자바스크립트에서 괄호 표현식이 화살표 함수인지 아닌지가 => 토큰을 만나기 전까지 모호한 경우에 발생합니다. 이 문제는 두 번이 아니라 세 번의 단계를 거쳐 파싱을 마친 후에 스코프 설정과 심볼 선언을 시작함으로써 해결될 수 있지만, 우리는 단 두 번의 단계로 수행하려고 노력하고 있습니다. 따라서 우리의 가정이 잘못되었다는 것이 밝혀졌을 경우, popScope() 대신 popAndDiscardScope() 또는 popAndFlattenScope()를 호출하여 이후에 스코프 트리를 수정합니다.


CoverCallExpressionAndAsyncArrowHead

CallExpression :
    CoverCallExpressionAndAsyncArrowHead

현재 생산 규칙
CallExpression : CoverCallExpressionAndAsyncArrowHead
을 처리할 때, `CoverCallExpressionAndAsyncArrowHead`의 해석은 다음 문법을 사용하여 개선됩니다:

CallMemberExpression[Yield, Await] :
    MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]
AsyncArrowFunction[In, Yield, Await] :
    CoverCallExpressionAndAsyncArrowHead[?Yield, ?Await] [no LineTerminator here] => AsyncConciseBody[?In]

CoverCallExpressionAndAsyncArrowHead[Yield, Await] :
    MemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await]

현재 생산 규칙
AsyncArrowFunction : CoverCallExpressionAndAsyncArrowHead => AsyncConciseBody
을 처리할 때, `CoverCallExpressionAndAsyncArrowHead`의 해석은 다음 문법을 사용하여 개선됩니다:

AsyncArrowHead :
    async [no LineTerminator here] ArrowFormalParameters[~Yield, +Await]

이 정의는 다음과 같은 것을 정의합니다:

javascript
async (a, b, c); // CallExpression
async (a, b, c) => {} // AsyncArrowFunction
^^^^^^^^^^^^^^^ CoverCallExpressionAndAsyncArrowHead

이것은 이상하게 보입니다. 왜냐하면 async는 키워드가 아니기 때문입니다. 첫 번째 async는 함수 이름이기 때문입니다.


CoverInitializedName

13.2.5 객체 초기화

ObjectLiteral[Yield, Await] :
    ...

PropertyDefinition[Yield, Await] :
    CoverInitializedName[?Yield, ?Await]

참고 3: 일부 컨텍스트에서는, 더 제한적인 보조 문법을 위한 커버 문법으로 `ObjectLiteral`이 사용됩니다.
`CoverInitializedName` 생산 규칙은 이러한 보조 문법을 완전히 커버하기 위해 필요합니다. 그러나 이 생산 규칙의 사용은 일반적인 컨텍스트에서 실제 `ObjectLiteral`이 필요한 곳에서는 조기에 구문 오류를 발생시킵니다.

13.2.5.1 정적 의미론: 조기 오류

실제 객체 초기화를 설명하는 것 외에도, `ObjectLiteral` 생산 규칙은 `ObjectAssignmentPattern`을 위한 커버 문법으로도 사용되며, `CoverParenthesizedExpressionAndArrowParameterList`의 일부로 인식될 수도 있습니다. `ObjectLiteral`이 `ObjectAssignmentPattern`이 요구되는 컨텍스트에 나타날 경우, 다음 조기 오류 규칙은 적용되지 않습니다. 또한 처음에 `CoverParenthesizedExpressionAndArrowParameterList` 또는 `CoverCallExpressionAndAsyncArrowHead`를 파싱할 때도 적용되지 않습니다.

PropertyDefinition : CoverInitializedName
    * 이 생산 규칙에 해당하는 소스 텍스트가 있는 경우 구문 오류이다.
13.15.1 정적 의미론: 조기 오류

AssignmentExpression : LeftHandSideExpression = AssignmentExpression
만약 `LeftHandSideExpression`이 `ObjectLiteral` 또는 `ArrayLiteral`이라면, 다음 조기 오류 규칙이 적용됩니다:
    * `LeftHandSideExpression`은 `AssignmentPattern`을 커버해야 한다.

이 정의는 다음과 같은 것을 정의합니다:

javascript
({ prop = value } = {}); // ObjectAssignmentPattern
({ prop: value }); // SyntaxError를 발생시키는 ObjectLiteral

파서는 CoverInitializedName을 사용해 ObjectLiteral을 파싱하고, ObjectAssignmentPattern에서 =에 도달하지 못하면 구문 오류를 발생시켜야 합니다.

연습으로, 다음 중 어느 =이 구문 오류를 발생시켜야 할까요?

javascript
let { x = 1 } = ({ x = 1 } = { x: 1 });