ECMAScript 명세 이해, 4부


원글 : Marja Hölttä, Understanding the ECMAScript spec, part 4

한편, 웹의 다른 부분에서는

Mozilla의 Jason OrendorffJS 구문상의 단점에 대한 심층 분석을 발표했다. 구현 세부 사항은 다르지만 모든 JavaScript 엔진은 이러한 단점으로 인해 동일한 문제에 직면한다.

커버 문법(Cover grammars)

이 글에서는 커버 문법에 대해 더 자세히 살펴본다. 먼저 모호해 보이는 구문 구조에 대한 문법을 명시하는 방법에 대해 살펴보자.

다시 말하면, [In, Yield, Await]의 첨자는 이 글에서 중요하지 않으므로 생략한다. 그 의미와 사용법에 대한 설명은 3부를 참조하자.

한정된 예측

일반적으로 파서는 한정된 예측(일정량의 다음 토큰)을 기반으로 사용할 프로덕션을 결정한다.

어떤 경우에는 다음 토큰이 명확하게 사용할 프로덕션을 결정한다.

예를 들면

UpdateExpression :
  LeftHandSideExpression
  LeftHandSideExpression ++
  LeftHandSideExpression --
  ++ UnaryExpression
  -- UnaryExpression

UpdateExpression을 구문 분석하고 다음 토큰이 ++ 또는 --이면 즉시 사용할 프로덕션을 알 수 있다. 다음 토큰이 둘 다 아니어도 현재 위치에서 LeftHandSideExpression을 구문 분석하고 구문 분석 후 무엇을 해야 할지 결정할 수 있다.

LeftHandSideExpression 다음에 오는 토큰이 ++인 경우 사용할 프로덕션은 UpdateExpression : LeftHandSideExpression ++이다. --의 경우도 비슷하다. 그리고 LeftHandSideExpression 다음에 오는 토큰이 ++--도 아닌 경우 프로덕션 UpdateExpression: LeftHandSideExpression을 사용한다.

화살표 함수 매개 변수 목록 또는 괄호로 묶인 표현식?

화살표 함수 매개 변수 목록을 괄호로 묶인 표현식과 구별하는 것은 더 복잡하다.

예를 들면

let x = (a,

위 예제가 화살표 함수의 시작일까?

let x = (a, b) => { return a + b };

아니면 위 예제처럼 괄호로 묶인 표현일까?

let x = (a, 3);

괄호로 묶인 것은 임의로 길 수 있기 때문에 우리는 한정된 양의 토큰을 기반으로 그것이 무엇인지 알 수 없다.

다음과 같은 간단한 프로덕션이 있다고 가정해보자.

AssignmentExpression :
  ...
  ArrowFunction
  ParenthesizedExpression

ArrowFunction :
  ArrowParameterList => ConciseBody

이제 우리는 한정된 예측으로 사용할 프로덕션을 선택할 수 없다. AssignmentExpression을 구문 분석해야 하고 다음 토큰이 (인 경우 다음으로 구문 분석할 것을 어떻게 결정할까? ArrowParameterList 또는 ParenthesizedExpression을 구문 분석할 수 있지만 추측이 잘못될 수 있다.

매우 관대한 새로운 기호 CPEAAPL

명세는 CoverParenthesizedExpressionAndArrowParameterList(줄여서 CPEAAPL) 기호를 도입하여 이 문제를 해결한다. CPEAAPL은 실제로 ParenthesizedExpression 또는 ArrowParameterList 뒤에서 사용되는 기호이지만, 우리는 아직 어느 쪽인지 모른다.

CPEAAPL에 대한 프로덕션은 매우 관대하여 ParenthesizedExpressionsArrowParameterLists에서 발생할 수 있는 모든 구성을 허용한다.

CPEAAPL :
  ( Expression )
  ( Expression , )
  ( )
  ( ... BindingIdentifier )
  ( ... BindingPattern )
  ( Expression , ... BindingIdentifier )
  ( Expression , ... BindingPattern )

예를 들어, 다음 표현식은 유효한 CPEAAPL이다.

// 유효한 ParenthesizedExpression 과 ArrowParameterList
(a, b)
(a, b = 1)

// 유효한 ParenthesizedExpression
(1, 2, 3)
(function foo() { })

// 유효한 ArrowParameterList
()
(a, b,)
(a, ...b)
(a = 1, ...b)

// 둘 다 유효하진 않지만 여전히 CPEAAPL 이다.
(1, ...b)
(1, )

후행 쉼표와 ...ArrowParameterList에서만 발생할 수 있다. b = 1과 같은 일부 구성은 둘 다에서 발생할 수 있지만 의미는 다르다.

ParenthesizedExpression 내부에서는 할당이고 ArrowParameterList 내부에서는 기본값이 있는 매개변수이다. 유효한 매개변수 이름(또는 매개변수 구조화 패턴)이 아닌 숫자 및 기타 PrimaryExpressionsParenthesizedExpression에서만 발생할 수 있다. 그러나 모두 CPEAAPL 내에서 발생할 수 있다.

프로덕션에서 CPEAAPL 사용

이제 AssignmentExpression 프로덕션에서 매우 관대한 CPEAAPL를 사용할 수 있다. (참고: ConditionalExpression은 여기에 표시되지 않은 긴 프로덕션 체인을 통해 PrimaryExpression으로 이어진다.)

AssignmentExpression :
  ConditionalExpression
  ArrowFunction
  ...

ArrowFunction :
  ArrowParameters => ConciseBody

ArrowParameters :
  BindingIdentifier
  CPEAAPL

PrimaryExpression :
  ...
  CPEAAPL

다시 AssignmentExpression을 구문 분석해야 하고 다음 토큰이 (인 상황이라고 가정해보자. 이제 우리는 CPEAAPL을 구문 분석하고 나중에 어떤 프로덕션을 사용할지 알 수 있다. ArrowFunction 또는 ConditionalExpression을 구문 분석하는지 여부는 중요하지 않다. 구문 분석할 다음 기호는 어떤 경우에도 CPEAAPL이다!

CPEAAPL을 구문 분석한 후 원래 AssignmentExpression(CPEAAPL을 포함하는 것)에 사용할 프로덕션을 결정할 수 있다. 이 결정은 CPEAAPL 다음 토큰을 기반으로 한다.

토큰이 =>이면 다음 프로덕션을 사용한다.

AssignmentExpression :
  ArrowFunction

토큰이 다른 것이라면 다음 프로덕션을 사용한다.

AssignmentExpression :
  ConditionalExpression

예를 들어:

let x = (a, b) => { return a + b; };
//      ^^^^^^
//     CPEAAPL
//             ^^
//             CPEAAPL 다음 토큰

let x = (a, 3);
//      ^^^^^^
//     CPEAAPL
//            ^
//            CPEAAPL 다음 토큰

그 시점에서 우리는 CPEAAPL을 그대로 유지하고 프로그램의 나머지 부분을 계속 분석할 수 있다. 예를 들어, CPEAAPLArrowFunction 내부에 있는 경우 나중에 수행할 수 있는 유효한 화살표 함수 매개변수 목록인지 여부를 아직 확인할 필요가 없다. (실제 파서는 유효성 검사를 즉시 수행하도록 선택할 수 있지만 명세 관점에서 볼 때 필요하지 않다.)

CPEAAPL 제한

앞에서 보았듯이 CPEAAPL에 대한 문법적 프로덕션은 매우 관대하며 결코 유효하지 않은 구성(예: (1, ...a))을 허용한다. 문법에 따라 프로그램 구문 분석을 완료한 후 해당하는 불법 구성을 허용하지 않아야 한다.

명세에서는 다음과 같은 제한 사항을 추가하여 이 작업를 수행한다.

Static Semantics: Early Errors

PrimaryExpression : CPEAAPL

CPEAAPLParenthesizedExpression을 포함하지 않는 경우 구문 오류이다.

Supplemental Syntax

프로덕션의 인스턴스를 처리할 때

PrimaryExpression : CPEAAPL

다음 문법을 사용하여 CPEAAPL의 해석을 구체화 한다.

ParenthesizedExpression : ( Expression )

이 말은 구문 트리에서 PrimaryExpression 대신 CPEAAPL이 발생하면 실제로는 ParenthesizedExpression이고 이것이 유일하게 유효한 프로덕션이다.

표현식은 비워둘 수 없으므로 ( )은 유효한 ParenthesizedExpression이 아니다. (1, 2, 3)과 같은 쉼표로 구분된 목록은 쉼표 연산자에 의해 생성된다.

Expression :
  AssignmentExpression
  Expression , AssignmentExpression

마찬가지로 ArrowParameters 대신 CPEAAPL이 발생하면 다음과 같은 제한 사항이 적용된다.

Static Semantics: Early Errors

ArrowParameters : CPEAAPL

CPEAAPLArrowFormalParameters을 포함하지 않는 경우 구문 오류이다.

Supplemental Syntax

프로덕션

ArrowParameters:CPEAAPL

가 인식되면 다음 문법을 사용하여 CPEAAPL의 해석을 구체화 한다.

ArrowFormalParameters :( UniqueFormalParameters )

기타 커버 문법

CPEAAPL 외에도 명세는 모호해 보이는 다른 구성에 대해 커버 문법을 사용한다.

ObjectLiteral은 화살표 함수 매개변수 목록 내에서 발생하는 ObjectAssignmentPattern에 대한 커버 문법으로 사용한다. 즉, ObjectLiteral은 실제 객체 리터럴 내부에서 발생할 수 없는 구성을 허용한다는 것을 의미한다.

ObjectLiteral :
  ...
  { PropertyDefinitionList }

PropertyDefinition :
  ...
  CoverInitializedName

CoverInitializedName :
  IdentifierReference Initializer

Initializer :
  = AssignmentExpression

예를 들어

let o = { a = 1 }; // syntax error

// 기본값이 있는 구조화 매개변수가 있는 화살표 함수:
let f = ({ a = 1 }) => { return a; };
f({}); // returns 1
f({a : 6}); // returns 6

비동기 화살표 함수는 한정된 예측으로 모호해 보인다.

let x = async(a,

위 예제는 async라고 하는 함수에 대한 호출일까 아니면 비동기 화살표 함수에 대한 호출일까?

let x1 = async(a, b);
let x2 = async();
function async() { }

let x3 = async(a, b) => {};
let x4 = async();

이를 위해 문법은 CPEAAPL와 유사하게 작동하는 커버 문법 기호 CoverCallExpressionAndAsyncArrowHead를 정의한다.

요약

이 글에서 명세가 커버 문법을 어떻게 정의하고 한정된 예측을 기반으로 현재 구문 구조를 식별할 수 없는 경우에 어떻게 이를 사용하는지 살펴보았다.

특히, 화살표 함수 매개 변수 목록을 괄호로 묶은 표현식과 구별하고 명세가 커버 문법을 사용하여 모호하게 보이는 구조를 먼저 관대하게 구문 분석하고 나중에 정적 의미 규칙으로 제한하는 방법을 살펴보았다.

김정용2022.11.16
Back to list