Actualités / Jeux

Comment planifier un Luau: augmenter la syntaxe de Lua avec des types

Pendant très longtemps, Lua 5.1 a été la langue de choix pour Roblox. Au fur et à mesure que nous grandissions, la demande d'un meilleur support d'outillage et d'une machine virtuelle plus performante augmentait également. Pour y répondre, nous avons lancé l'initiative de reconstruire notre pile Lua nommée «Luau» (prononcé / lu-wow /), dans le but d'englober les fonctionnalités que les programmeurs attendent d'un langage moderne à offrir – qui inclut un vérificateur de type, un nouveau linter framework, et un interpréteur plus rapide, pour n'en nommer que quelques-uns.

Pour rendre tout cela possible, nous avons dû réécrire la plupart de notre pile à partir de zéro. Le problème est que l’analyseur Lua 5.1 est étroitement associé à la génération de bytecode, ce qui est insuffisant pour nos besoins. Nous voulons pouvoir parcourir l'AST pour une analyse plus approfondie, nous avons donc besoin d'un analyseur pour produire cet arbre de syntaxe. À partir de là, nous sommes libres d'effectuer toutes les opérations que nous souhaitons effectuer sur cet AST.

Par chance, il y avait un analyseur Lua 5.1 existant qui traînait dans Studio uniquement utilisé pour la passe de linting de base. Cela nous a permis d'adopter très facilement cet analyseur et de l'étendre pour reconnaître la syntaxe spécifique à Luau, ce qui minimisait ainsi le risque possible de modifier l'analyse résultante d'une manière subtile. Un détail critique car l'une de nos valeurs sacrées chez Roblox est la rétrocompatibilité. Nous avons déjà écrit des millions de lignes de code Lua et nous nous engageons à ce qu'elles continuent à fonctionner pour toujours.

Donc, avec ces facteurs à l'esprit, les exigences sont claires. Nous devons le faire:

  • éviter les bizarreries de grammaire qui nécessitent un retour en arrière
  • avoir un analyseur efficace
  • maintenir une syntaxe compatible avec les avancées
  • rester rétrocompatible avec Lua 5.1

Cela semble simple, non?

Comment le moteur d'inférence de type a influencé les choix de syntaxe

Pour commencer, nous devons comprendre un certain contexte sur la façon dont nous sommes arrivés dans cette situation. Nous avons choisi ces syntaxes car elles sont déjà immédiatement familières à la majorité des programmeurs, et sont en fait standard de l'industrie. Vous n’avez rien à apprendre de nouveau.

Il y a plusieurs endroits où Luau vous permet d'écrire de telles annotations de type:

  • local foo: chaîne
  • fonction add (x: nombre, y: nombre): nombre… fin
  • type Foo = (nombre, nombre) -> nombre
  • local foo = bar sous forme de chaîne

L'ajout de syntaxe pour annoter vos liaisons est très important pour que le moteur d'inférence de type comprenne mieux les typages prévus. Lua est un langage très puissant qui vous permet de surcharger pratiquement tous les opérateurs du langage. Sans moyen d'annoter ce que sont les choses, nous ne pouvons même pas dire avec certitude que l'expression x + y va produire un nombre!

Expression de cast de type

Quelque chose que nous aimons vraiment de TypeScript, c'est ce qu'ils appellent une assertion de type. C'est essentiellement un moyen d'ajouter des informations de type supplémentaires à un programme pour que le vérificateur puisse les vérifier. Dans TypeScript, la syntaxe est:

barre comme chaîne

Malheureusement, lorsque nous avons essayé cela, nous avons eu une mauvaise surprise: cela casse le code existant! L'un des jeux de nos utilisateurs avait une fonction nommée. Leurs scripts comprenaient donc des extraits comme:

local x = y

as (w, z) – “->” attendu lors de l'analyse du type de fonction, obtenu

Nous aurions probablement pu faire ce travail, sans une complication supplémentaire: nous voulions que notre analyseur fonctionne avec un seul jeton d'anticipation. Les performances sont importantes pour nous, et une partie de l'écriture d'un analyseur très performant consiste à minimiser la quantité de retour en arrière qu'il doit effectuer. Il ne serait pas efficace pour notre parseur de devoir parcourir arbitrairement loin en avant et en arrière pour déterminer ce que signifie réellement une expression.

Il s'avère également que TypeScript peut remercier la règle d'insertion automatique de point-virgule de JavaScript pour avoir rendu ce travail gratuit. Lorsque vous écrivez cet extrait de code dans TypeScript / JavaScript, il insère des points-virgules sur chaque ligne, ce qui entraîne son analyse en tant que deux instructions distinctes. Alors que s'il était sur une seule ligne, il s'agit d'une erreur de syntaxe au niveau du jeton as en JavaScript, mais d'une expression d'assertion de type valide dans TypeScript. Étant donné que Lua ne fait pas cela, ni n'applique les points-virgules, il doit essayer d'analyser chaque instruction la plus longue possible, même si elles s'étendent sur plusieurs lignes.

soit x = y

comme (w, z)

L’expression typographique originale de Luau n’était pas rétrocompatible même si elle avait la performance que nous voulions. Malheureusement, cela a rompu notre promesse de Luau comme un sur-ensemble de Lua 5.1, nous ne pouvons donc pas le faire sans quelques contraintes supplémentaires telles que l’exigence de parenthèses dans certains contextes!

Tapez des arguments dans les appels de fonction

Un autre détail malheureux dans la grammaire de Lua nous empêche d'ajouter des arguments de type aux appels de fonction sans introduire une autre ambiguïté:

retourner une fonction(c)

Cela pourrait signifier deux choses différentes:

  • évaluer une fonction < A and B > c et renvoyer les résultats
  • appeler et retourner une fonction avec deux arguments de type A et B, et un argument de c

Cette ambiguïté ne se produit que dans le contexte d'une liste d'expressions. Ce n’est pas vraiment un gros problème dans TypeScript et C # car ils ont tous deux l’avantage de compiler à l’avance. Par conséquent, ils peuvent tous deux se permettre de passer quelques cycles à essayer de lever l'ambiguïté de cette expression à l'une des deux options.

Bien qu'il semble que nous puissions faire la même chose, comme appliquer des heuristiques lors de l'analyse ou de la vérification de type, nous ne pouvons en fait pas. Lua 5.1 a la capacité d'injecter dynamiquement des globaux dans n'importe quel environnement, et cela peut briser cette heuristique. Nous n'avons pas non plus cet avantage car nous devons être en mesure de générer le bytecode le plus rapidement possible pour que tous les clients puissent commencer à interpréter.

Instruction de type alias

L'analyse de cette instruction d'alias de type n'est pas une modification radicale car sa syntaxe Lua est déjà invalide:

type Foo = nombre

Ce que nous faisons est simple. Nous analysons une expression principale qui ne finit par analyser que dans la mesure où il suffit de taper, puis nous décidons quoi faire en fonction du résultat de l'analyse de cette expression:

  • S'il s'agit d'un appel de fonction, arrêtez d'essayer d'analyser davantage cette expression en tant qu'instruction.
  • Sinon, si le jeton suivant est une virgule ou égal, analysez une instruction d'affectation.

Ce qui manque ci-dessus est très évident. Il n'a pas de branche pour laquelle un identifiant peut être dirigé par un autre. Tout ce que nous avons à faire est alors une correspondance de motif sur l'expression:

  1. Est-ce un identifiant?
  2. Le nom de cet identifiant est-il égal à «type»?
  3. Le jeton suivant est-il un identifiant arbitraire?

Voilà, vous obtenez une syntaxe rétrocompatible avec un mot-clé contextuel.

type Foo = number – type alias

type (x) – appel de fonction

type = {x = 1} – attribution

type.x = 2 – affectation

En tant qu'extrait bonus, cela analyse toujours exactement la même manière que Lua 5.1 car nous n'analysions pas à partir du contexte d'une instruction:

local foo = type

bar = 1

Leçons apprises

Ce qu'il faut retenir ici, semble-t-il, c'est que nous allons devoir concevoir la syntaxe de Luau pour qu'elle soit compatible avec les avancées et avec les chemins d'analyse les moins sensibles au contexte. Cela élimine la nécessité de remettre en question ce qui oblige l'analyseur à revenir en arrière et à essayer autre chose à partir de ce point d'échec. Non seulement cela nous donne l'avantage d'avoir un analyseur rapide pour simplement aller jusqu'à la fin du code source, mais cela peut également rendre l'AST sans avoir besoin d'autres types d'étapes pour lever l'ambiguïté.

Cela signifie également que nous devrons être prudents lors de l'ajout d'une nouvelle syntaxe en général, ce qui n'est pas forcément un mauvais endroit. Un langage bien pensé exige de ses concepteurs une vision à long terme.


Ni Roblox Corporation ni ce blog n'approuvent ou ne soutiennent aucune entreprise ou service. De plus, aucune garantie ou promesse n'est faite concernant l'exactitude, la fiabilité ou l'exhaustivité des informations contenues dans ce blog.

Cet article de blog a été initialement publié sur le blog Roblox Tech.