Gramática
JavaScript tem uma das gramáticas mais difíceis de analisar; este tutorial registra o trabalho e os tropeços ao aprendê-la.
Gramática LL(1)
Segundo a Wikipedia,
an LL grammar is a context-free grammar that can be parsed by an LL parser, which parses the input from Left to right
O primeiro L significa varrer o código da esquerda para a direita; o segundo L refere-se à derivação pela esquerda.
“Livre de contexto” e o (1) em LL(1) indicam que dá para montar a árvore olhando apenas o próximo token.
LL(1) é popular na academia porque permite gerar parsers automaticamente sem escrever à mão — mas a maior parte das linguagens industriais não tem uma gramática LL(1) feliz, e JavaScript entra nessa estatística.
INFO
Há alguns anos a Mozilla começou jsparagus e escreveu um gerador LALR em Python. Poucas atualizações recentes; ao fim de js-quirks.md a mensagem é clara:
What have we learned today?
- Do not write a JS parser.
- JavaScript has some syntactic horrors in it. But hey, you don't make the world's most widely used programming language by avoiding all mistakes. You do it by shipping a serviceable tool, in the right circumstances, for the right users.
Na prática, escrever descida recursiva manual é quase inevitável por causa da própria gramática. Vamos percorrer os detalhes antes de tomar decisões ruins na implementação.
Lista do simples ao intricado — pegue um café e vá com calma.
Identificadores
Na spec (#sec-identifiers) existem três formas:
IdentifierReference[Yield, Await] :
BindingIdentifier[Yield, Await] :
LabelIdentifier[Yield, Await] :estree (e algumas ASTs) não distingue esses casos, e a especificação textual não explica de forma didática.
BindingIdentifier declara; IdentifierReference referencia vínculos existentes. Em var foo = bar, foo é BindingIdentifier, bar é IdentifierReference:
VariableDeclaration[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt
Initializer[In, Yield, Await] :
= AssignmentExpression[?In, ?Yield, ?Await]Seguindo até PrimaryExpression:
PrimaryExpression[Yield, Await] :
IdentifierReference[?Yield, ?Await]Modelar isso separado no AST facilita muito análise semântica posterior.
pub struct BindingIdentifier {
pub node: Node,
pub name: Atom,
}
pub struct IdentifierReference {
pub node: Node,
pub name: Atom,
}Classe e modo estrito
Classes ECMAScript nasceram depois do modo estrito; todo o corpo de classe é strict. Em #sec-class-definitions aparece a nota explícita (A class definition is always strict mode code).
Associar modo estrito a escopos de função é mecânico; class, porém, não define escopo lexical próprio — o parser ganha estado extra só para esse caso.
// 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");Octal legado e use strict
#sec-string-literals-early-errors proíbe octal escapado em string "\01" em modo estrito:
EscapeSequence ::
LegacyOctalEscapeSequence
NonOctalDecimalEscapeSequence
É um erro de sintaxe se o texto-fonte correspondido por esta produção for código em modo estrito.O léxico costuma detectar junto ao parser perguntando o estado de strict mode.
Mas com diretivas tudo quebra:
https://github.com/tc39/test262/blob/747bed2e8aaafe8fdf2c65e8a10dd7ae64f66c47/test/language/literals/string/legacy-octal-escape-sequence-prologue-strict.js#L16-L19use strict vem depois do octal; ainda assim precisa erro de sintaxe. Felizmente código real não costuma misturar — apenas o caso test262 citado acima.
Parâmetros não simples e modo estrito
Duplicando parâmetros é permitido fora do strict: function foo(a, a) { }, mas "use strict" impede (function foo(a, a) { "use strict" }). ES6 trouxe formulários elaborados tipo function foo({ a }, b = c) {}.
E quando "01" (erro estrito) aparece assim?
function foo(
value = (function() {
return "\01";
}()),
) {
"use strict";
return value;
}Do ponto de vista do parser: o que fazer se o erro estrito aparece dentro da lista de parâmetros?
#sec-function-definitions-static-semantics-early-errors fecha a porta:
FunctionDeclaration :
FunctionExpression :
É um erro de sintaxe se FunctionBodyContainsUseStrict de FunctionBody for verdadeiro e IsSimpleParameterList de FormalParameters for falso.O Chrome diz algo opaco tipo Illegal 'use strict' directive in function with non-simple parameter list. Artigo recomendável: Nicholas C. Zakas.
INFO
Curiosidade: com target: es5 no TypeScript a regra some e vira código transpilado estilo:
function foo(a, b) {
"use strict";
if (b === void 0) b = "\01";
}Expressão entre parênteses
À primeira vista só agrupamento? Na gramática oficial ((x)) pode colapsar em um único IdentifierReference.
Porém há efeitos de runtime conforme #194 do estree:
> fn = function () {};
> fn.name
< "fn"
> (fn) = function () {};
> fn.name
< ''Acorn/Babel ganharam opção preserveParens por compatibilidade.
Declaração de função dentro de if
Em #sec-ecmascript-language-statements-and-declarations, Declaration não entra dentro de Statement “puro”; porém Annex B (#sec-functiondeclarations-in-ifstatement-statement-clauses) permite, fora strict, funções no lugar de comando em cada ramo:
if (x) {
function foo() {}
} else function bar() {}Rótulos são sintaxe válida
Pouca gente usa label, mas é JS moderno e strict não proíbe.
No JSX abaixo o trecho é LabelledStatement, não literal de objeto:
<Foo
bar={() => {
baz: "quaz";
}}
/>
// ^^^^^^^^^^^ `LabelledStatement`let não é palavra-chave
let não é keyword reservada globalmente: pode aparecer na maioria dos lugares salvo onde a gramática veta. O parser precisa olhar o token seguinte:
let a;
let = foo;
let instanceof x;
let + 1;
while (true) let;
a = let[0];for-in / for-of e o parâmetro [In]
#prod-ForInOfStatement assusta de primeira. Dois nós: [lookahead ≠ let] e [+In].
Depois de for (let, o lookahead não pode ser in (evita for (let in)), mas precisa permitir {, [ ou identificador para padrões for (let {} = foo) etc.
Ao chegar em in ou of, o lado direito passa com [+In] correto para não confundir o operador in de RelationalExpression com o in do for:
RelationalExpression[In, Yield, Await] :
[+In] RelationalExpression[+In, ?Yield, ?Await] in ShiftExpression[?Yield, ?Await]
[+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]
Nota 2: O parâmetro gramatical `[In]` evita confundir o operador `in` em uma expressão relacional com o `in` de uma instrução `for`.Esse é praticamente o único uso global de [In]. Ainda há [lookahead ∉ { let, async of }] bloqueando for (async of …) — precisa guard explícito.
Declarações de função em bloco (Annex B)
#sec-block-level-function-declarations-web-legacy-compatibility-semantics explica páginas de comportamento; em termos de implementação lembre do que o Acorn documenta em escopo:
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/scope.js#L30-L35Dentro de função, function aninhada em bloco se comporta como var — daí redeclaração ilegal se misturar var e function no mesmo bloco:
function foo() {
if (true) {
var bar;
function bar() {} // erro de redeclaração
}
}No corpo da função sem bloco intermediário funciona:
function foo() {
var bar;
function bar() {}
}Parâmetros de contexto gramatical
Cinco parâmetros guiam o que é permitido: [In], [Return], [Yield], [Await], [Default]. Exemplo de flags no parser (Biome/Rome legado):
// https://github.com/rome/tools/blob/5a059c0413baf1d54436ac0c149a829f0dfd1f4d/crates/rome_js_parser/src/state.rs#L404-L425
pub(crate) struct ParsingContextFlags: u8 {
/// Whether the parser is in a generator function like `function* a() {}`
/// Matches the `Yield` parameter in the ECMA spec
const IN_GENERATOR = 1 << 0;
/// Whether the parser is inside a function
const IN_FUNCTION = 1 << 1;
/// Whatever the parser is inside a constructor
const IN_CONSTRUCTOR = 1 << 2;
/// Is async allowed in this context. Either because it's an async function or top level await is supported.
/// Equivalent to the `Async` generator in the ECMA spec
const IN_ASYNC = 1 << 3;
/// Whether the parser is parsing a top-level statement (not inside a class, function, parameter) or not
const TOP_LEVEL = 1 << 4;
/// Whether the parser is in an iteration or switch statement and
/// `break` is allowed.
const BREAK_ALLOWED = 1 << 5;
/// Whether the parser is in an iteration statement and `continue` is allowed.
const CONTINUE_ALLOWED = 1 << 6;Alterne os bits conforme a gramática.
AssignmentPattern versus BindingPattern
No estree, o lado esquerdo de AssignmentExpression é Pattern:
extend interface AssignmentExpression {
left: Pattern;
}O mesmo vale para VariableDeclarator:
interface VariableDeclarator <: Node {
type: "VariableDeclarator";
id: Pattern;
init: Expression | null;
}Mas na especificação o mesmo trecho sintático pode ser referência ou binding:
// AssignmentExpression:
{ foo } = bar;
^^^ IdentifierReference
[ foo ] = bar;
^^^ IdentifierReference
// VariableDeclarator
var { foo } = bar;
^^^ BindingIdentifier
var [ foo ] = bar;
^^^ BindingIdentifierSe o enum interno for só Pattern::Identifier, não dá para saber sem inspecionar pais na árvore. A própria spec separa AssignmentPattern e BindingPattern — vale modelar dois enums:
13.15 Assignment Operators
AssignmentExpression[In, Yield, Await] :
LeftHandSideExpression[?Yield, ?Await] = AssignmentExpression[?In, ?Yield, ?Await]
13.15.5 Destructuring Assignment
In certain circumstances when processing an instance of the production
AssignmentExpression : LeftHandSideExpression = AssignmentExpression
the interpretation of LeftHandSideExpression is refined using the following grammar:
AssignmentPattern[Yield, Await] :
ObjectAssignmentPattern[?Yield, ?Await]
ArrayAssignmentPattern[?Yield, ?Await]14.3.2 Variable Statement
VariableDeclaration[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]opt
BindingPattern[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]enum BindingPattern {
BindingIdentifier,
ObjectBindingPattern,
ArrayBindingPattern,
}
enum AssignmentPattern {
IdentifierReference,
ObjectAssignmentPattern,
ArrayAssignmentPattern,
}Eu fiquei uma semana confuso até perceber: precisamos de dois tipos diferentes em vez de um Pattern genérico. A gramática supplemental de AssignmentPattern aparece como subseção 5 de “13.15 Assignment Operators” depois das runtime semantics — fácil de não notar 😉.
TIP
Os trechos a seguir são especialmente densos (“here be dragons”).
Gramática ambígua
Dado um /, divisão ou início de RegExp?
a / b;
a / / regex /;
a /= / regex /;
/ regex / / b;
/=/ / /=/;A gramática sintática orienta o léxico (#sec-ecmascript-language-lexical-grammar):
There are several situations where the identification of lexical input elements is sensitive to the syntactic grammar context that is consuming the input elements.
Ou seja, o parser manda qual “meta léxica” usar. Quando um literal RegExp cabe na posição atual, vale InputElementRegExp; caso contrário InputElementDiv:
InputElementDiv ::
WhiteSpace
LineTerminator
Comment
CommonToken
DivPunctuator <---------- the `/` and `/=` token
RightBracePunctuator
InputElementRegExp ::
WhiteSpace
LineTerminator
Comment
CommonToken
RightBracePunctuator
RegularExpressionLiteral <-------- the `RegExp` tokenPercorrendo a / / regex /:
a / / regex /
^ ------------ PrimaryExpression:: IdentifierReference
^ ---------- MultiplicativeExpression: MultiplicativeExpression MultiplicativeOperator ExponentiationExpression
^^^^^^^^ - PrimaryExpression: RegularExpressionLiteralFluxo vai para ExpressionStatement até PrimaryExpression. Antes da primeira / ainda não estamos esperando regexp → InputElementDiv. Para o lado direito esperamos ExponentiationExpression; segunda / inicia regexp porque a gramática naquele ponto admite RegularExpressionLiteral.
RegularExpressionLiteral ::
/ RegularExpressionBody / RegularExpressionFlagsINFO
Exercício: faça o mesmo para /=/ / /=/.
Cover grammar
Leia antes o post do V8. Há três cover grammars principais na spec:
CoverParenthesizedExpressionAndArrowParameterList
PrimaryExpression[Yield, Await] :
CoverParenthesizedExpressionAndArrowParameterList[?Yield, ?Await]
Ao processar uma instância da produção
PrimaryExpression[Yield, Await] : CoverParenthesizedExpressionAndArrowParameterList[?Yield, ?Await]
a interpretação de CoverParenthesizedExpressionAndArrowParameterList é refinada com a gramática seguinte:
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]let foo = (a, b, c); // SequenceExpression
let bar = (a, b, c) => {}; // ArrowExpression
^^^^^^^^^ CoverParenthesizedExpressionAndArrowParameterListSe os escopos são construídos durante o parse, fica ambíguo — esbuild descreve escopos temporários e ajustes posteriores.
CoverCallExpressionAndAsyncArrowHead
CallExpression :
CoverCallExpressionAndAsyncArrowHead
Ao processar uma instância da produção
CallExpression : CoverCallExpressionAndAsyncArrowHead
a interpretação de CoverCallExpressionAndAsyncArrowHead é refinada com a gramática seguinte:
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]
Ao processar uma instância da produção
AsyncArrowFunction : CoverCallExpressionAndAsyncArrowHead => AsyncConciseBody
a interpretação de CoverCallExpressionAndAsyncArrowHead é refinada com a gramática seguinte:
AsyncArrowHead :
async [no LineTerminator here] ArrowFormalParameters[~Yield, +Await]async (a, b, c); // chamada qualquer primeiro async
async (a, b, c) => {} // função arrow assíncrona
^^^^^^^^^^^^^^^ CoverCallExpressionAndAsyncArrowHeadCoverInitializedName
13.2.5 Object Initializer
ObjectLiteral[Yield, Await] :
...
PropertyDefinition[Yield, Await] :
CoverInitializedName[?Yield, ?Await]
Nota 3: Em determinados contextos, ObjectLiteral é usado como cover grammar para uma gramática secundária mais restrita.
A produção CoverInitializedName é necessária para cobrir plenamente essas gramáticas secundárias. Contudo, o uso dessa produção resulta em erro de sintaxe antecipado em contextos normais onde se espera um ObjectLiteral propriamente dito.
13.2.5.1 Static Semantics: Early Errors
...
PropertyDefinition : CoverInitializedName
É um erro de sintaxe se qualquer texto-fonte for correspondido por esta produção.13.15.1 Static Semantics: Early Errors
AssignmentExpression : LeftHandSideExpression = AssignmentExpression
Se LeftHandSideExpression for um ObjectLiteral ou um ArrayLiteral, aplicam-se as seguintes regras de erro antecipado:
* LeftHandSideExpression deve cobrir (*cover*) um AssignmentPattern.({ prop = value } = {}); // ObjectAssignmentPattern
({ prop: value }); // ObjectLiteral que dispara SyntaxError fora do contexto de patternExercício: qual = abaixo é inválido?
let { x = 1 } = ({ x = 1 } = { x: 1 });