Quería tener mi propio lenguaje de programación, que facilitará la creación de juegos de aventuras basados en texto para mi proyecto de código abierto jQuery Terminal . La idea del lenguaje surgió después de que creé un concierto pago para una persona, llamémoslo Ken, que necesitaba este tipo de juego, donde el usuario interactuaba con la terminal y se le hacían un montón de preguntas y era como una aventura. juego, relacionado con Crypo . El código que escribí, que Ken necesitaba, estaba basado en datos mediante un archivo JSON. Estaba funcionando bien, Ken podía cambiar fácilmente el JSON y cambiar el juego como quisiera. Pregunté si podía compartir el código ya que era un proyecto genial y Ken estuvo de acuerdo en que podía hacerlo dos meses después de que publicara el juego. Pero después de un tiempo, me he dado cuenta de que puedo tener algo mucho mejor. Mi propio lenguaje DSL, que simplificará la creación de juegos de aventura basados en texto . Una persona con un poco de conocimiento de programación como Ken podría editar fácilmente el juego, porque el lenguaje será mucho más simple que el complejo código JavaScript que se necesita para algo como esto. E incluso si me pidieran que creara un juego como el de Ken, sería mucho más fácil y rápido para mí. Así ha comenzado el lenguaje de programación Gaiman .
Usé PEG.js antes, por lo que era mi elección obvia para el generador de analizadores . Primero comencé con un ejemplo aritmético , lo modifiqué y luego agregué declaraciones if y expresiones booleanas . Cuando tuve esta primera prueba de concepto que generaba código JavaScript de salida , estaba tan emocionado que tuve que escribir un artículo y compartir lo simple que es crear su propio lenguaje de programación en JavaScript .
Al final, hay un campo de juegos de demostración simple, y si quieres algo más genial, mira el sitio web de Gaiman, enlace en GitHub .
Así que al grano, vamos a sumergirnos.
Un compilador es un programa que traduce código de un lenguaje de programación a otro lenguaje. Por ejemplo, el compilador de C traduce un programa escrito en lenguaje C a código de máquina (binario que puede ser interpretado por la computadora). Pero también hay compiladores que traducen un lenguaje legible por humanos a otro lenguaje legible. Por ejemplo, ClojureScript se compila en JavaScript. Este proceso a menudo se llama transpiling y el programa que hace esto a menudo se llama transpiler.
Un Parser es un programa que puede ser parte del compilador o intérprete . Toma el código de entrada como una secuencia de caracteres y produce AST (árbol de sintaxis abstracta), que puede ser utilizado por el generador de código (parte del compilador) para generar el código de salida o por el evaluador (parte del intérprete) para ejecutarlo.
AST es un acrónimo de Abstract Syntax Tree. Es la forma de representar el código en un formato que las herramientas pueden entender, generalmente en forma de estructura de datos de árbol . Usaremos AST en el formato de Esprima , que es un analizador de JavaScript que genera AST.
El generador de analizadores, como sugiere el nombre, es un programa que genera el código fuente de un analizador basado en la gramática (especificación del lenguaje). Escrito en una sintaxis específica. En este artículo, utilizaremos el generador de analizador PEG.js que genera un código JavaScript que analizará el código para su idioma y generará AST.
Un generador Parser también es un compilador, por lo que puede llamarlo compilador compilador. Un compilador que puede generar un compilador para su idioma.
Lo bueno de la sintaxis de Esprima es que existen herramientas que generan código en función de su AST. Un ejemplo es escodegen que toma Esprima AST como entrada y genera código JavaScript. Puede pensar que puede usar solo cadenas para generar código, pero esta solución no escalará. En este tutorial, solo muestro una sola instrucción if, pero se encontrará con muchos problemas si tiene un código más complejo.
PEG.js es un compilador para analizar gramáticas de expresiones escritas en JavaScript. Se necesita un lenguaje PEG más simple que usa código JavaScript en línea y genera un analizador.
A continuación, le mostraré cómo crear una gramática PEG.js de analizador simple para la declaración if que generará AST, que luego se transformará en código JavaScipt.
La sintaxis de PEG.js no es muy complicada, consiste en el nombre de la regla, luego el bloque coincidente y opcional de JavaScript que se ejecuta y devuelve desde la regla.
Aquí hay un ejemplo aritmético simple proporcionado por la documentación de PEG.js:
{ function makeInteger(o) { return parseInt(o.join(""), 10 ); } } start
= additive additive = left :multiplicative "+" right :additive { return left + right ; } / multiplicative multiplicative = left : primary "*" right :multiplicative { return left * right ; } / primary
primary
= integer
/ "(" additive:additive ")" { return additive; } integer "integer" = digits:[ 0 -9 ] + { return makeInteger(digits); }
El analizador de salida de esta gramática puede analizar y evaluar expresiones aritméticas simples, por ejemplo
10+2*3
que evalúa a 16
. Puede probar este analizador en PEG.js Online Tool . Tenga en cuenta que no maneja espacios entre tokens (para simplificar el código), con un analizador necesita manejar esto explícitamente.Pero lo que necesitamos no es interpretar el código y devolver un solo valor sino devolver Esprima AST. Para ver cómo se ve Esprima AST, puede consultar AST Explorer , seleccionar Esprima como salida y escribir algo de JavaScript.
Aquí hay un ejemplo de código simple como este:
if (foo == "bar" ) { 10 + 10
10 * 20
}
La salida en formato JSON se ve así:
{ "type" : "Program" , "body" : [ { "type" : "IfStatement" , "test" : { "type" : "BinaryExpression" , "operator" : "==" , "left" : { "type" : "Identifier" , "name" : "foo" , "range" : [ 4 , 7
] }, "right" : { "type" : "Literal" , "value" : "bar" , "raw" : "\"bar\"" , "range" : [ 11 , 16
] }, "range" : [ 4 , 16
] }, "consequent" : { "type" : "BlockStatement" , "body" : [ { "type" : "ExpressionStatement" , "expression" : { "type" : "BinaryExpression" , "operator" : "+" , "left" : { "type" : "Literal" , "value" : 10 , "raw" : "10" , "range" : [ 23 , 25
] }, "right" : { "type" : "Literal" , "value" : 10 , "raw" : "10" , "range" : [ 28 , 30
] }, "range" : [ 23 , 30
] }, "range" : [ 23 , 30
] }, { "type" : "ExpressionStatement" , "expression" : { "type" : "BinaryExpression" , "operator" : "*" , "left" : { "type" : "Literal" , "value" : 10 , "raw" : "10" , "range" : [ 34 , 36
] }, "right" : { "type" : "Literal" , "value" : 20 , "raw" : "20" , "range" : [ 39 , 41
] }, "range" : [ 34 , 41
] }, "range" : [ 34 , 41
] } ], "range" : [ 18 , 43
] }, "alternate" : null , "range" : [ 0 , 43
] } ], "sourceType" : "module" , "range" : [ 0 , 43
] }
No necesita preocuparse por el "rango" y "sin procesar". Son parte de la salida del analizador.
Dividamos el JSON en su parte:
La instrucción if debe tener el formato:
{ "type" : "IfStatement" , "test" : { }, "consequent" : { }, "alternate" : null
}
Donde "prueba" y "consecuente" son expresiones:
La condición puede ser cualquier expresión pero aquí tendremos una expresión binaria que compara dos cosas:
{ "type" : "BinaryExpression" , "operator" : "==" , "left" : {}, "right" : {} }
El uso de variables se ve así:
{ "type" : "Identifier" , "name" : "foo"
}
Una cadena literal que se usa en nuestro código se ve así:
{ "type" : "Literal" , "value" : "bar"
}
El bloque dentro de if se crea así:
{ "type" : "BlockStatement" , "body" : [ ] }
Y todo el programa se crea así:
{ "type" : "Program" , "body" : [ ] }
Para nuestro lenguaje de demostración, crearemos un código que se parece a Ruby:
if foo == "bar" then 10 + 10
10 * 20
end
y crearemos AST, que luego creará código JavaScript.
Peg gramática para si se ve así:
if = "if" _ expression:(comparison / expression) _ "then" body:(statements / _) _ "end" { return { "type" : "IfStatement" , "test" : expression, "consequent" : { "type" : "BlockStatement" , "body" : body }, "alternate" : null
}; }
tenemos el token "si", entonces una expresión que es comparación o expresión y el cuerpo son declaraciones o espacios en blanco. _ son espacios en blanco opcionales que se ignoran.
_ = [ \t\n\r]*
La comparación se ve así:
comparison = _ left :expression _ "==" _ right :expression _ { return { "type" : "BinaryExpression" , "operator" : "==" , "left" : left , "right" : right
}; }
La expresión se ve así:
expression = expression :(variable / literal) { return expression ; }
La variable se crea a partir de tres reglas:
variable = !keywords variable:name { return { "type" : "Identifier" , "name" : variable } } keywords = "if" / "then" / "end"
name = [A-Z_$az][A-Z_a-z0 -9 ]* { return text(); }
Ahora veamos las declaraciones:
statements = _ head:( if / expression_statement) _ tail:( ! "end" _ ( if / expression_statement))* { return [head].concat( tail.map ( function ( element ) { return element[ 2 ] ;
})) ;
} expression_statement = expression :expression { return { "type" : "ExpressionStatement" , "expression" : expression } ;
}
Y lo último son los literales:
literal = value:( string / Integer) { return { "type" : "Literal" , "value" : value } ;
} string = "\"" ([^ "] / " \\\\\ "" )* "\"" { return JSON.parse( text ()) ;
} Integer "integer"
= _ [ 0 -9 ]+ { return parseInt( text (), 10 ) ; }
Y ese es todo el analizador, que genera AST. Después de tener Esprima AST, todo lo que tenemos que hacer es generar el código con escodegen.
El código que genera el AST y crea el código JavaScript se ve así:
const ast = parser.parse(code); const js_code = escodegen.generate(ast);
la variable del analizador es el nombre que le das cuando generas el analizador usando PEG.js.
Y aquí hay una demostración simple que estaba usando para escribir el analizador, puedes jugar con la gramática y generar una sintaxis diferente para tu propio lenguaje de programación que se compila en JavaScript.
Demostración del generador de analizador .
Esta sencilla aplicación guarda tu código en LocalStorage, si compila sin errores, en cada cambio. Así que puedes usarlo de forma segura para crear tu propio idioma. Pero no garantizo que no perderá su trabajo, por lo que puede usar algo que sea más robusto.
NOTA: El proyecto PEG.js original ya no se mantiene, pero hay una nueva bifurcación, Peggy , que se mantiene y es compatible con versiones anteriores de PEG.js, por lo que será fácil de cambiar.
En este artículo, usamos un generador de analizador para crear un lenguaje personalizado simple para el compilador de JavaScript . Como puede ver, comenzar un proyecto como este no es tan difícil. Las técnicas explicadas en este artículo deberían permitirle crear cualquier lenguaje de programación que compile en JavaScript por su cuenta. Esta puede ser una forma de crear una PoC de un idioma que desea diseñar. Hasta donde yo sé, esta es la forma más rápida de hacer que algo funcione. Pero puede usar su idioma tal como está y crear su propio DLS (Lenguaje específico del dominio), escribir código en ese idioma y hacer que JavaScript haga el trabajo duro.