Saltar al contenido principal

Comprendiendo la especificación ECMAScript, parte 4

· 7 min de lectura
[Marja Hölttä](https://twitter.com/marjakh), espectadora especulativa de especificaciones

Todos los episodios

Mientras tanto, en otras partes de la Web

Jason Orendorff de Mozilla publicó un excelente análisis en profundidad de las peculiaridades sintácticas de JS. Aunque los detalles de implementación difieran, cada motor JS enfrenta los mismos problemas con estas peculiaridades.

Gramáticas de cobertura

En este episodio, profundizamos en las gramáticas de cobertura. Son una forma de especificar la gramática para construcciones sintácticas que inicialmente parecen ambiguas.

Nuevamente, omitiremos los subíndices para [In, Yield, Await] por brevedad, ya que no son importantes para esta publicación del blog. Consulta parte 3 para una explicación de su significado y uso.

Miradas limitadas finitas

Por lo general, los analizadores deciden qué producción usar basándose en un número limitado de miradas hacia adelante (una cantidad fija de tokens siguientes).

En algunos casos, el siguiente token determina de manera inequívoca la producción a usar. Por ejemplo:

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

Si estamos analizando un UpdateExpression y el siguiente token es ++ o --, sabemos la producción a usar de inmediato. Si el siguiente token no es ninguno de los dos, aún no es demasiado difícil: podemos analizar un LeftHandSideExpression comenzando desde la posición en la que estamos y decidir qué hacer después de haberlo analizado.

Si el token que sigue al LeftHandSideExpression es ++, la producción a usar es UpdateExpression : LeftHandSideExpression ++. El caso de -- es similar. Y si el token que sigue al LeftHandSideExpression no es ni ++ ni --, usamos la producción UpdateExpression : LeftHandSideExpression.

¿Lista de parámetros de una función flecha o una expresión entre paréntesis?

Distinguir las listas de parámetros de funciones flecha de las expresiones entre paréntesis es más complicado.

Por ejemplo:

let x = (a,

¿Es este el comienzo de una función flecha, como esta?

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

¿O tal vez es una expresión entre paréntesis, como esta?

let x = (a, 3);

Lo que sea que esté entre paréntesis puede ser arbitrariamente largo - no podemos saber qué es en base a una cantidad finita de tokens.

Imaginemos por un momento que tuviéramos las siguientes producciones simples:

AssignmentExpression :
...
ArrowFunction
ParenthesizedExpression

ArrowFunction :
ArrowParameterList => ConciseBody

Ahora no podemos elegir la producción a usar con una limitada mirada hacia adelante. Si tuviéramos que analizar un AssignmentExpression y el siguiente token fuera (, ¿cómo decidiríamos qué analizar a continuación? Podríamos analizar un ArrowParameterList o un ParenthesizedExpression, pero nuestra decisión podría ser incorrecta.

El nuevo símbolo muy permisivo: CPEAAPL

La especificación resuelve este problema introduciendo el símbolo CoverParenthesizedExpressionAndArrowParameterList (abreviado como CPEAAPL). CPEAAPL es un símbolo que en realidad es un ParenthesizedExpression o un ArrowParameterList detrás de escena, pero aún no sabemos cuál de los dos.

Las producciones para CPEAAPL son muy permisivas, permitiendo todas las construcciones que pueden ocurrir en ParenthesizedExpressions y en ArrowParameterLists:

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

Por ejemplo, las siguientes expresiones son válidas CPEAAPLs:

// Expresión entre paréntesis y lista de parámetros de función flecha válidas:
(a, b)
(a, b = 1)

// Expresión entre paréntesis válida:
(1, 2, 3)
(function foo() { })

// Lista de parámetros de función flecha válidas:
()
(a, b,)
(a, ...b)
(a = 1, ...b)

// No válidas tampoco, pero aún son `CPEAAPL`s:
(1, ...b)
(1, )

La coma final y el ... pueden ocurrir solo en ArrowParameterList. Algunas construcciones, como b = 1, pueden aparecer en ambas, pero tienen significados diferentes: dentro de ParenthesizedExpression es una asignación, dentro de ArrowParameterList es un parámetro con un valor predeterminado. Números y otros PrimaryExpressions, que no son nombres de parámetros válidos (ni patrones de desestructuración de parámetros), solo pueden ocurrir en ParenthesizedExpression. Pero todos pueden ocurrir dentro de un CPEAAPL.

Usando CPEAAPL en producciones

Ahora podemos utilizar el muy permisivo CPEAAPL en las producciones de AssignmentExpression. (Nota: ConditionalExpression lleva a PrimaryExpression a través de una larga cadena de producción que no se muestra aquí.)

AssignmentExpression :
ConditionalExpression
ArrowFunction
...

ArrowFunction :
ArrowParameters => ConciseBody

ArrowParameters :
BindingIdentifier
CPEAAPL

PrimaryExpression :
...
CPEAAPL

Imagina que nuevamente estamos en la situación de necesitar analizar un AssignmentExpression y el siguiente token es (. Ahora podemos analizar un CPEAAPL y determinar posteriormente qué producción usar. No importa si estamos analizando un ArrowFunction o un ConditionalExpression, el próximo símbolo a analizar es CPEAAPL en cualquier caso.

Después de haber analizado el CPEAAPL, podemos decidir qué producción usar para el AssignmentExpression original (el que contiene el CPEAAPL). Esta decisión se toma según el token que sigue al CPEAAPL.

Si el token es =>, usamos la producción:

AssignmentExpression :
ArrowFunction

Si el token es otra cosa, usamos la producción:

AssignmentExpression :
ConditionalExpression

Por ejemplo:

let x = (a, b) => { return a + b; };
// ^^^^^^
// CPEAAPL
// ^^
// El token que sigue al CPEAAPL

let x = (a, 3);
// ^^^^^^
// CPEAAPL
// ^
// El token que sigue al CPEAAPL

En ese punto podemos mantener el CPEAAPL tal como está y continuar analizando el resto del programa. Por ejemplo, si el CPEAAPL está dentro de un ArrowFunction, aún no necesitamos verificar si es una lista de parámetros válida para una función flecha - eso puede hacerse más adelante. (Los analizadores en el mundo real podrían optar por realizar la verificación de validez de inmediato, pero desde el punto de vista de la especificación, no necesitamos hacerlo).

Restringiendo los CPEAAPLs

Como vimos anteriormente, las producciones de gramática para CPEAAPL son muy permisivas y permiten construcciones (como (1, ...a)) que nunca son válidas. Después de haber analizado el programa según la gramática, necesitamos descartar las construcciones ilegales correspondientes.

La especificación hace esto añadiendo las siguientes restricciones:

Semánticas Estáticas: Errores Tempranos

PrimaryExpression : CPEAAPL

Es un Error de Sintaxis si CPEAAPL no está cubriendo un ParenthesizedExpression.

Sintaxis Suplementaria

Al procesar una instancia de la producción

PrimaryExpression : CPEAAPL

la interpretación de CPEAAPL se refina usando la siguiente gramática:

ParenthesizedExpression : ( Expression )

Esto significa que: si un CPEAAPL ocurre en el lugar de PrimaryExpression en el árbol de sintaxis, en realidad es un ParenthesizedExpression y esta es su única producción válida.

Expression nunca puede estar vacío, por lo que ( ) no es un ParenthesizedExpression válido. Las listas separadas por comas como (1, 2, 3) se crean mediante el operador coma:

Expression :
AssignmentExpression
Expression , AssignmentExpression

De manera similar, si un CPEAAPL ocurre en el lugar de ArrowParameters, se aplican las siguientes restricciones:

Semánticas Estáticas: Errores Tempranos

ArrowParameters : CPEAAPL

Es un Error de Sintaxis si CPEAAPL no está cubriendo un ArrowFormalParameters.

Sintaxis Suplementaria

Cuando se reconoce la producción

ArrowParameters : CPEAAPL

se utiliza la siguiente gramática para refinar la interpretación de CPEAAPL:

ArrowFormalParameters : ( UniqueFormalParameters )

Otras gramáticas de cobertura

Además de CPEAAPL, la especificación utiliza gramáticas de cobertura para otros constructos que parecen ambiguos.

ObjectLiteral se utiliza como gramática de cobertura para ObjectAssignmentPattern, que ocurre dentro de las listas de parámetros de funciones flecha. Esto significa que ObjectLiteral permite construcciones que no pueden ocurrir dentro de literales de objeto reales.

ObjectLiteral :
...
{ PropertyDefinitionList }

PropertyDefinition :
...
CoverInitializedName

CoverInitializedName :
IdentifierReference Initializer

Initializer :
= AssignmentExpression

Por ejemplo:

let o = { a = 1 }; // error de sintaxis

// Función flecha con un parámetro de desestructuración con un valor
// predeterminado:
let f = ({ a = 1 }) => { return a; };
f({}); // devuelve 1
f({a : 6}); // devuelve 6

Las funciones flecha asíncronas también parecen ambiguas con una anticipación finita:

let x = async(a,

¿Es esto una llamada a una función llamada async o una función flecha asíncrona?

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

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

Con este fin, la gramática define un símbolo de gramática de cobertura CoverCallExpressionAndAsyncArrowHead que funciona de manera similar a CPEAAPL.

Resumen

En este episodio examinamos cómo la especificación define las gramáticas de cobertura y las usa en casos donde no podemos identificar la construcción sintáctica actual basándonos en una anticipación finita.

En particular, analizamos cómo distinguir las listas de parámetros de funciones flecha de las expresiones entre paréntesis y cómo la especificación utiliza una gramática de cobertura para analizar primero de manera permisiva construcciones aparentemente ambiguas y restringirlas con reglas semánticas estáticas más adelante.