Skip to main content

Cómo planear Luau: Aumentar la sintaxis Lua con Tipos

octubre

19, 2020

by Apakovtac & fun_enthusiast


Tecnología

Por mucho tiempo, Lua 5.1 ha sido el lenguaje preferido de Roblox. A medida que crecíamos, también lo hacía la demanda de un mejor soporte de herramientas así como de un VM más eficiente. Para responder a esto, comenzamos la iniciativa de reconstruir nuestra pila de Lua llamada «Luau» (se pronuncia /lu-wow/), con el objetivo de abarcar las características de un lenguaje más moderno —que incluye un corrector de tipos, un nuevo marco de trabajo de linter y un intérprete más rápido—, solo por nombrar algunas funciones.

Para hacer todo eso posible, tuvimos que reescribir la mayor parte de nuestra pila desde cero. El problema es que el analizador Lua 5.1 está estrechamente unido a la generación de códigos de barras, y eso es insuficiente para nuestras necesidades. Queremos ser capaces de atravesar la AST para un análisis más profundo, por lo que necesitamos un analizador para producir ese árbol de sintaxis. A partir de ahí, somos libres de realizar cualquier operación que queramos en esa AST.

Por suerte, había un analizador Lua 5.1 en Studio, que pero se usaba solo para el paso básico de linting. Eso nos facilitó mucho la adopción de ese analizador y su ampliación para reconocer la sintaxis específica de Luau, con lo que se redujo al mínimo el riesgo de modificar de manera sutil el análisis. Este es un detalle crítico porque uno de nuestros valores sagrados en Roblox es la compatibilidad con las tecnologías pasadas. Tenemos millones de líneas de código Lua ya escritas, y nos comprometemos a que sigan funcionando para siempre.

Por lo tanto, teniendo en cuenta estos factores, los requisitos son claros. Tenemos que:

  • evitar las peculiaridades gramaticales que requieren retroceder;
  • tener un analizador eficiente;
  • mantener la sintaxis compatible con las tecnologías futuras;
  • seguir siendo compatibles con el Lua 5.1;

¿Suena sencillo?

Cómo el motor de inferencia de tipo influyó en las elecciones de sintaxis

Para empezar, necesitamos entender el contexto sobre cómo llegamos a esta situación. Elegimos estas sintaxis porque ya son familiares para la mayoría de los programadores y son el estándar de la industria. Lo que significa que no tenemos que aprender nada nuevo.

Hay varios lugares donde Luau nos permite escribir este tipo de anotaciones:

  • local foo: string
  • function add(x: number, y: number): number … end
  • type Foo = (number, number) -> number
  • local foo = bar as string

Es muy importante añadir la sintaxis para anotar sus enlaces con el fin de que el motor de inferencia de tipos entienda mejor los tipos previstos. Lua es un lenguaje muy poderoso que permite sobrecargar virtualmente a todos los operadores del lenguaje. Sin alguna forma de anotar las cosas, no podemos ni siquiera decir con confianza que la expresión x + y va a producir un número.

Expresión de tipo de molde

Algo que nos gusta mucho de TypeScript es lo que llaman una afirmación de tipo. Es básicamente una forma de añadir información adicional de tipo a un programa para que el verificador la verifique. En TypeScript, la sintaxis es:

bar as string

Desafortunadamente, cuando lo probamos, nos llevamos una mala sorpresa: ¡rompe el código existente! Uno de los juegos de nuestros usuarios tenía una función llamada «as». Sus scripts incluían fragmentos como:

local x = y

as(w, z) — Esperábamos ‘->’ al analizar el tipo de función, y recibimos <eof>

Podríamos haberlo hecho funcionar, si no fuera por una complicación adicional: queríamos que nuestro analizador funcionara con un solo token de lectura previa. El rendimiento es importante para nosotros, y parte de escribir un analizador de alto rendimiento es minimizar la cantidad de retrocesos que tiene que hacer. No sería muy eficiente para nuestro analizador tener que escanear hacia adelante y hacia atrás arbitrariamente lejos para determinar lo que realmente significa una expresión.

También resulta que TypeScript puede agradecer a la regla de inserción automática de punto y coma de JavaScript por hacer que esto funcione de forma gratuita. Cuando escribes este fragmento en TypeScript/JavaScript, esto insertará puntos y comas en cada línea para que sea analizado como dos declaraciones separadas. Mientras que si estuviera en una sola línea, «as» es un error de sintaxis en el token de JavaScript, pero una expresión de afirmación de tipo válida en TypeScript. Debido a que Lua no lleva a cabo esta operación, ni aplica punto y coma, tendrá que analizar cada declaración más larga posible, incluso si se extiende a través de varias líneas.

let x = y

as(w, z)

La expresión original de tipo de Luau no era compatible con la tecnología pasada, aunque actuaba como queríamos. Lamentablemente, esto rompió nuestra promesa de que Luau era un superconjunto de Lua 5.1, por lo que no podemos utilizarlo sin algunas restricciones adicionales como la añadir paréntesis en ciertos contextos.

Escribir argumentos de tipo en las llamadas a funciones

Otro detalle desafortunado de la gramática de Lua nos impide añadir argumentos tipográficos a las llamadas a funciones sin introducir otra ambigüedad:

return someFunction<A, B>(c)

Podría significar dos cosas diferentes:

  • evaluar someFunction < A y B > c, y devolver los resultados
  • llamar y devolver someFunction con dos argumentos de tipo A y B, y un argumento de c

Esta ambigüedad solo se produce en el contexto de una lista de expresiones. No es un gran problema en TypeScript y C# porque ambos tienen la ventaja de compilar el código con antelación. Por lo tanto, ambos pueden darse el lujo de pasar algunos ciclos tratando de desambiguar esta expresión en una de las dos opciones.

Aunque parece que podríamos hacer lo mismo, como aplicar la heurística durante el análisis sintáctico o la comprobación de tipos, en realidad no podemos. Lua 5.1 tiene la capacidad de inyectar globales dinámicamente en cualquier ambiente, y eso puede romper esta heurística. Tampoco tenemos ese beneficio porque necesitamos ser capaces de generar código de bytes lo más rápido posible para que todos los clientes empiecen a interpretar.

Declaración tipo alias

Analizar este tipo de declaración de tipo alias no es un cambio radical porque ya es una sintaxis Lua inválida:

type Foo = number

Lo que hacemos es simple. Analizamos una expresión primaria que analiza solo el tipo, y luego decidimos qué hacer basándonos en el resultado del análisis de esa expresión:

  • si es una llamada de función, dejamos de intentar analizar más de esta expresión.
  • de lo contrario, si el siguiente token es una coma o un igual, analizamos una declaración de asignación.

Lo que falta arriba es muy obvio. No tiene ninguna rama en la que un identificador pueda ser dirigido por otro. Todo lo que tenemos que hacer entonces es hacer coincidir el patrón de la expresión:

  1. ¿Es un identificador?
  2. ¿Es el nombre de ese identificador igual a “type”?
  3. ¿Es el siguiente token un identificador arbitrario?

Así es, obtienes una sintaxis compatible con una palabra clave sensible al contexto.

type Foo = number — type alias

type(x) — function call

type = {x = 1} — assignment

type.x = 2 — assignment

Como un fragmento extra, todavía se analiza exactamente de la misma manera que Lua 5.1 porque no estábamos analizando el contexto de una declaración:

local foo = type

bar = 1

Lo que hemos aprendido

Lo que parece indicar que tendremos que diseñar la sintaxis para que Luau sea compatible con el futuro y con las rutas de análisis menos sensibles al contexto. Luau elimina la necesidad de hacer conjeturas que requieren que el analizador retroceda e intente algo más desde ese punto de fallo. No solo nos beneficiamos al tener un analizador rápido que llega hasta el final del código fuente, sino que también este puede devolver la AST sin necesidad de desambiguar otros tipos de etapas.

También significa que tendremos que ser cuidadosos al añadir nueva sintaxis en general. Un lenguaje bien pensado exige que sus diseñadores tengan una visión a largo plazo.


Ni la Corporación Roblox ni este blog respaldan o apoyan a ninguna empresa o servicio. Además, no se ofrecen garantías ni promesas sobre la exactitud, fiabilidad o integridad de la información contenida en este blog.

Este blog se publicó originalmente en Roblox Tech Blog.