원글 : Marja Hölttä, Understanding the ECMAScript spec, part 4
Mozilla의 Jason Orendorff는 JS 구문상의 단점에 대한 심층 분석을 발표했다. 구현 세부 사항은 다르지만 모든 JavaScript 엔진은 이러한 단점으로 인해 동일한 문제에 직면한다.
이 글에서는 커버 문법에 대해 더 자세히 살펴본다. 먼저 모호해 보이는 구문 구조에 대한 문법을 명시하는 방법에 대해 살펴보자.
다시 말하면, [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
에 대한 프로덕션은 매우 관대하여 ParenthesizedExpressions
및 ArrowParameterLists
에서 발생할 수 있는 모든 구성을 허용한다.
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
내부에서는 기본값이 있는 매개변수이다. 유효한 매개변수 이름(또는 매개변수 구조화 패턴)이 아닌 숫자 및 기타 PrimaryExpressions
는 ParenthesizedExpression
에서만 발생할 수 있다. 그러나 모두 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
을 그대로 유지하고 프로그램의 나머지 부분을 계속 분석할 수 있다. 예를 들어, CPEAAPL
이 ArrowFunction
내부에 있는 경우 나중에 수행할 수 있는 유효한 화살표 함수 매개변수 목록인지 여부를 아직 확인할 필요가 없다. (실제 파서는 유효성 검사를 즉시 수행하도록 선택할 수 있지만 명세 관점에서 볼 때 필요하지 않다.)
CPEAAPL
제한앞에서 보았듯이 CPEAAPL
에 대한 문법적 프로덕션은 매우 관대하며 결코 유효하지 않은 구성(예: (1, ...a))을 허용한다. 문법에 따라 프로그램 구문 분석을 완료한 후 해당하는 불법 구성을 허용하지 않아야 한다.
명세에서는 다음과 같은 제한 사항을 추가하여 이 작업를 수행한다.
Static Semantics: Early Errors
PrimaryExpression : CPEAAPL
CPEAAPL
가ParenthesizedExpression
을 포함하지 않는 경우 구문 오류이다.
프로덕션의 인스턴스를 처리할 때
PrimaryExpression : CPEAAPL
다음 문법을 사용하여
CPEAAPL
의 해석을 구체화 한다.
ParenthesizedExpression : ( Expression )
이 말은 구문 트리에서 PrimaryExpression
대신 CPEAAPL
이 발생하면 실제로는 ParenthesizedExpression
이고 이것이 유일하게 유효한 프로덕션이다.
표현식은 비워둘 수 없으므로 ( )
은 유효한 ParenthesizedExpression
이 아니다. (1, 2, 3)
과 같은 쉼표로 구분된 목록은 쉼표 연산자에 의해 생성된다.
Expression :
AssignmentExpression
Expression , AssignmentExpression
마찬가지로 ArrowParameters
대신 CPEAAPL
이 발생하면 다음과 같은 제한 사항이 적용된다.
Static Semantics: Early Errors
ArrowParameters : CPEAAPL
CPEAAPL
가ArrowFormalParameters
을 포함하지 않는 경우 구문 오류이다.
프로덕션
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
를 정의한다.
이 글에서 명세가 커버 문법을 어떻게 정의하고 한정된 예측을 기반으로 현재 구문 구조를 식별할 수 없는 경우에 어떻게 이를 사용하는지 살펴보았다.
특히, 화살표 함수 매개 변수 목록을 괄호로 묶은 표현식과 구별하고 명세가 커버 문법을 사용하여 모호하게 보이는 구조를 먼저 관대하게 구문 분석하고 나중에 정적 의미 규칙으로 제한하는 방법을 살펴보았다.