MyLang es un lenguaje de programación educativo simple inspirado en Python
, JavaScript
y C
, escrito como un desafío personal, en poco tiempo, principalmente para divertirse escribiendo un analizador de descenso recursivo y explorar el mundo de los intérpretes. No espere un lenguaje de programación completo con bibliotecas y marcos listos para su uso en producción. Sin embargo, MyLang
tiene un conjunto mínimo de funciones integradas y también podría usarse con fines prácticos.
MyLang está escrito en C++ 17 portátil : por el momento, el proyecto no tiene más dependencias que la biblioteca estándar de C++. Para compilarlo, si tienes GNU make
instalado, simplemente ejecuta:
$ make -j
De lo contrario, simplemente pase todos los archivos .cpp a su compilador y agregue el directorio src/
a la ruta de búsqueda de inclusión. Una de las mejores cosas de no tener dependencias es que no hay necesidad de un sistema de compilación para compilaciones únicas.
Simplemente pase la opción BUILD_DIR
para make
:
$ make -j BUILD_DIR=other_build_directory
Si también desea ejecutar las pruebas de MyLang, simplemente debe compilar con TESTS=1 y deshabilitar las optimizaciones con OPT=0, para una mejor experiencia de depuración:
$ make -j TESTS=1 OPT=0
Luego, ejecute todas las pruebas con:
$ ./build/mylang -rt
Vale la pena señalar que, si bien los marcos de prueba como GoogleTest y Boost.Test son infinitamente mucho más potentes y flexibles que el trivial motor de pruebas que tenemos en src/tests.cpp
, son dependencias externas . Cuantas menos dependencias, mejor, ¿verdad? :-)
La forma más corta de describir MyLang
es: un lenguaje dinámico tipo Python con aspecto de C. Probablemente, la forma más rápida de aprender este lenguaje es consultar los scripts en el directorio samples/
mientras lees la breve documentación a continuación.
MyLang
es un lenguaje dinámico de escritura de patos, como Python
. Si conoce Python
y está dispuesto a usar llaves { }
, podrá usarlo automáticamente. Sin sorpresas. Las cadenas son inmutables como en Python
, las matrices se pueden definir usando [ ]
como en Python
, y los diccionarios también se pueden definir usando { }
. El lenguaje también admite cortes de matriz utilizando la misma sintaxis [start:end]
utilizada por Python
.
Dicho esto, MyLang
se diferencia de Python
y otros lenguajes de script en varios aspectos:
Hay soporte para constantes de tiempo de análisis declaradas usando const
.
Todas las variables deben declararse usando var
.
Las variables tienen un alcance como en C
El sombreado se admite cuando una variable se vuelve a declarar explícitamente usando var
, en un bloque anidado.
Todas las declaraciones de expresión deben terminar con ;
como en C
, C++
y Java
.
Las palabras clave true
y false
existen, pero no hay ningún tipo boolean
. Como en C, 0
es falso, todo lo demás es true
. Sin embargo, las cadenas, matrices y diccionarios tienen un valor booleano, exactamente como en Python
(por ejemplo, una matriz vacía se considera false
). La true
función incorporada es solo un alias para el número entero 1
.
El operador de asignación =
se puede usar como en C
, dentro de expresiones, pero no existe el operador de coma debido a la función de expansión de matriz.
MyLang admite tanto el bucle for
clásico como el bucle foreach
explícito.
MyLang no admite tipos personalizados por el momento. Sin embargo, los diccionarios admiten un agradable azúcar sintáctico: además de la sintaxis principal d["key"]
, para las claves de cadena también se admite la sintaxis d.key
.
Las variables siempre se declaran con var
y viven en el ámbito en el que han sido declaradas (aunque son visibles en ámbitos anidados). Por ejemplo:
# Variable declared in the global scope
var a = 42 ;
{
var b = 12 ;
# Here we can see both `a` and `b`
print ( a , b ) ;
}
# But here we cannot see `b` .
Es posible declarar múltiples variables usando la siguiente sintaxis familiar:
var a , b , c ;
Pero hay una advertencia, probablemente la única característica "sorprendente" de MyLang
: la inicialización de variables no funciona como en C. Considere la siguiente afirmación:
var a , b , c = 42 ;
En este caso, en lugar de simplemente declarar a
y b
e inicializar en c
a 42, estamos inicializando las tres variables al valor 42. Para inicializar cada variable a un valor diferente, use la sintaxis de expansión de matriz:
var a , b , c = [ 1 , 2 , 3 ] ;
Las constantes se declaran de manera similar a las variables , pero no se pueden ocultar en ámbitos anidados. Por ejemplo:
const c = 42 ;
{
# That's not allowed
const c = 1 ;
# That's not allowed as well
var c = 99 ;
}
En MyLang
las constantes se evalúan en tiempo de análisis , de manera similar a las declaraciones constexpr de C++
(pero ahí hablamos de tiempo de compilación ). Al inicializar un const
, se puede usar cualquier tipo de literal además del conjunto completo de elementos integrados de const . Por ejemplo:
const val = sum ( [ 1 , 2 , 3 ] ) ;
const x = " hello " + " world " + " " + join ( [ " a " , " b " , " c " ] , " , " ) ;
Para comprender exactamente cómo se evaluó una constante, ejecute el intérprete con la opción -s
para volcar el árbol de sintaxis abstracta antes de ejecutar el script. Para el ejemplo anterior:
$ cat > t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
$ ./build/mylang t
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
)
--------------------------
¿Sorprendido? Bueno, las constantes que no sean matrices y diccionarios ni siquiera se instancian como variables. Simplemente no existen en tiempo de ejecución . Agreguemos una declaración usando x
:
$ cat >> t
print(x);
$ cat t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
print(x);
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
CallExpr(
Id("print")
ExprList(
"hello world a,b,c"
)
)
)
--------------------------
hello world a,b,c
Ahora todo debería tener sentido. Casi lo mismo sucede con las matrices y los diccionarios, con la excepción de que estos últimos también se crean instancias en tiempo de ejecución, para evitar tener literales potencialmente enormes en todas partes. Considere el siguiente ejemplo:
$ ./build/mylang -s -e 'const ar=range(4); const s=ar[2:]; print(ar, s, s[0]);'
Syntax tree
--------------------------
Block(
ConstDecl(
Id("ar")
Op '='
LiteralArray(
Int(0)
Int(1)
Int(2)
Int(3)
)
)
ConstDecl(
Id("s")
Op '='
LiteralArray(
Int(2)
Int(3)
)
)
CallExpr(
Id("print")
ExprList(
Id("ar")
Id("s")
Int(2)
)
)
)
--------------------------
[0, 1, 2, 3] [2, 3] 2
Como puede ver, la operación de corte se evaluó en tiempo de análisis mientras se inicializaba la constante s
, pero ambas matrices también existen en tiempo de ejecución. En cambio, las operaciones de subíndice en expresiones constantes se convierten a literales. Esto parece una buena compensación para el rendimiento: los valores pequeños como números enteros, flotantes y cadenas se convierten en literales durante la evaluación constante , mientras que las matrices y diccionarios (potencialmente grandes) se dejan como símbolos de solo lectura en tiempo de ejecución, pero aún permiten algunas operaciones sobre ellos (como [index]
y len(arr)
) se evaluarán constantemente.
MyLang
soporta, por el momento, sólo los siguientes tipos (integrados):
Ninguno El tipo de none
, el equivalente de None
de Python. Las variables recién declaradas sin tener un valor asignado, no tienen valor none
(por ejemplo, var x;
). Lo mismo se aplica a las funciones que no tienen un valor de retorno. Además, las funciones integradas como find()
lo utilizan como valor especial en caso de falla.
Entero Un entero del tamaño de un puntero con signo (por ejemplo, 3
).
Float Un número de punto flotante (por ejemplo, 1.23
). Internamente es un doble largo.
Cadena Una cadena como "hola". Las cadenas son inmutables y admiten sectores (por ejemplo, s[3:5]
o s[3:]
o s[-2:]
, que tienen el mismo significado que en Python
).
Array Un tipo mutable para matrices y tuplas (por ejemplo, [1,2,3]
). Puede contener elementos de diferentes tipos y admite sectores grabables. Los sectores de matriz se comportan como copias mientras que, en el fondo, utilizan técnicas de copia en escritura.
Diccionario Los diccionarios son mapas hash definidos utilizando la sintaxis de Python
: {"a": 3, "b": 4}
. Se accede a los elementos con la sintaxis familiar d["key-string"]
o d[23]
, se pueden buscar con find()
y eliminar con erase()
. Por el momento, sólo se pueden utilizar cadenas, números enteros y flotantes como claves de un diccionario. Ventajas : también se puede acceder a claves de cadena tipo identificador con la sintaxis "miembro de": d.key
.
Función Tanto las funciones independientes como las lambdas tienen el mismo tipo de objeto y pueden transmitirse como cualquier otro objeto (ver más abajo). Pero solo las lambdas pueden tener una lista de captura. Las funciones regulares no se pueden ejecutar durante la evaluación constante, mientras que las funciones pure
sí. Las funciones puras sólo pueden ver constantes y sus argumentos.
Excepción El único tipo de objetos que se pueden lanzar. Para crearlos, use la exception()
incorporada o su atajo ex()
.
Las declaraciones condicionales funcionan exactamente como en C
La sintaxis es:
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
Y las llaves { }
se pueden omitir como en C
, en el caso de bloques de una sola declaración. conditionExpr
puede ser cualquier expresión, por ejemplo: (a=3)+b >= c && !d
.
Cuando conditionExpr
es una expresión que se puede evaluar de manera constante, toda la declaración if se reemplaza por la rama verdadera, mientras que la rama falsa se descarta. Por ejemplo, considere el siguiente script:
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
No solo siempre imprime "sí", sino que ni siquiera necesita verificar nada antes de hacerlo. Consulte el árbol de sintaxis abstracta:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
Block(
CallExpr(
Id("print")
ExprList(
"yes"
)
)
)
)
--------------------------
yes
MyLang
admite los clásicos bucles while
y for
.
while ( condition ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
for ( var i = 0 ; i < 10 ; i += 1 ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
Aquí, las llaves { }
se pueden omitir como en el caso anterior. Sólo hay algunas diferencias con respecto a C
que vale la pena señalar:
Los operadores ++
y --
no existen en MyLang
por el momento.
Para declarar múltiples variables, use la sintaxis: var a, b = [3,4];
o simplemente var a,b,c,d = 0;
si quieres que todas las variables tengan el mismo valor inicial.
Para aumentar el valor de múltiples variables use la sintaxis: a, b += [1, 2]
. En los casos extremadamente raros y complejos en los que en la instrucción de incremento del bucle for necesitamos asignar a cada variable una nueva variable usando diferentes expresiones, aproveche la sintaxis de expansión en la asignación: i, j = [i+2, my_next(i, j*3)]
.
MyLang
admite bucles foreach
usando una sintaxis bastante familiar:
var arr = [ 1 , 2 , 3 ] ;
foreach ( var e in arr ) {
print ( " elem: " , e ) ;
}
Los bucles Foreach se pueden utilizar para matrices, cadenas y diccionarios. Por ejemplo, iterar a través de cada par <key, value>
en un diccionario es tan fácil como:
var d = { " a " : 3 , " b " : 10 , " c " : 42 } ;
foreach ( var k , v in d ) {
print ( k + " => " + str ( v ) ) ;
}
Para iterar solo a través de cada clave, simplemente use var k in d
en su lugar.
MyLang
también admite la enumeración en bucles foreach. Consulte el siguiente ejemplo:
var arr = [ " a " , " b " , " c " ] ;
foreach ( var i , elem in indexed arr ) {
print ( " elem[ " + str ( i ) + " ] = " + elem ) ;
}
En otras palabras, cuando el nombre del contenedor va precedido de la palabra clave indexed
, a la primera variable se le asigna un número progresivo en cada iteración.
Mientras se itera a través de una matriz de pequeñas matrices de tamaño fijo (piense en tuplas), es posible expandir directamente esas "tuplas" en el bucle foreach:
var arr = [
[ " hello " , 42 ] ,
[ " world " , 11 ]
] ;
foreach ( var name , value in arr ) {
print ( name , value ) ;
}
# This is a shortcut for :
foreach ( var elem in arr ) {
# regular array expansion
var name , value = elem ;
print ( name , value ) ;
}
# Which is a shortcut for :
foreach ( var elem in arr ) {
var name = elem [ 0 ] ;
var value = elem [ 1 ] ;
print ( name , value ) ;
}
Declarar una función es tan simple como:
func add ( x , y ) {
return x + y ;
}
Pero también se admiten varios atajos. Por ejemplo, en el caso de funciones de una sola declaración como la anterior, se puede utilizar la siguiente sintaxis:
func add ( x , y ) => x + y ;
Además, si bien es una buena práctica escribir siempre ()
para funciones sin parámetros, en realidad son opcionales en este lenguaje:
func do_something { print ( " hello " ) ; }
Las funciones se tratan como símbolos regulares en MyLang
y no existen diferencias sustanciales entre las funciones independientes y lambdas en este idioma. Por ejemplo, podemos declarar la función add
(arriba) como lambda de esta manera:
var add = func ( x , y ) => x + y ;
Nota: al crear objetos de función en expresiones, no podemos asignarles un nombre.
Lambdas también admite una lista de captura, pero no se admiten capturas implícitas para reforzar la claridad. Por supuesto, las lambdas se pueden devolver como cualquier otro objeto. Por ejemplo:
func create_adder_func ( val ) =>
func [ val ] ( x ) => x + val ;
var f = create_adder_func ( 5 ) ;
print ( f ( 1 ) ) ; # Will print 6
print ( f ( 10 ) ) ; # Will print 15
Las lambdas con capturas tienen state , como cualquiera esperaría. Considere el siguiente guión:
func gen_counter ( val ) => func [ val ] {
val += 1 ;
return val ;
} ;
var c1 = gen_counter ( 5 ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
# Clone the `c1` lambda object as `c2` : now it will have
# its own state , indipendent from `c1` .
var c2 = clone ( c1 ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c2: " , c2 ( ) ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
Genera la salida:
c1: 6
c1: 7
c1: 8
c2: 9
c2: 10
c2: 11
c1: 9
c1: 10
c1: 11
Los objetos de funciones regulares definidos por el usuario (incluidas las lambdas) no se consideran const
y, por lo tanto, no se pueden ejecutar durante la evaluación constante. Esa es una limitación bastante fuerte. Considere el siguiente ejemplo:
const people = [
[ " jack " , 3 ] ,
[ " alice " , 11 ] ,
[ " mario " , 42 ] ,
[ " bob " , 38 ]
] ;
const sorted_people = sort ( people , func ( a , y ) => a [ 0 ] < b [ 0 ] ) ;
En este caso, el script no puede crear la matriz constante sorted_people
. Debido a que pasamos un objeto de función al incorporado const sort()
, obtendremos un error ExpressionIsNotConstEx
. Claro, si sorted_people
se declarara como var
, el script se ejecutaría, pero la matriz ya no será constante y no podremos beneficiarnos de ninguna optimización del tiempo de análisis . Por lo tanto, si bien se puede llamar a la función integrada sort()
durante la evaluación constante, cuando tiene un parámetro compare func
personalizado, eso ya no es posible.
Para superar la limitación recién descrita, MyLang
tiene una sintaxis especial para funciones puras . Cuando una función se declara con la palabra clave pure
que precede func
, el intérprete la trata de una manera especial: se puede llamar en cualquier momento, tanto durante la evaluación constante como durante el tiempo de ejecución, pero la función no puede ver variables globales ni capturar nada: solo puede usar constantes y el valor de sus parámetros: eso es exactamente lo que necesitamos durante la evaluación constante. Por ejemplo, para generar sorted_people
durante la evaluación constante basta con escribir:
const sorted_people = sort ( people , pure func ( a , b ) => a [ 0 ] < b [ 0 ] ) ;
Las funciones puras se pueden definir como funciones independientes y también se pueden utilizar con parámetros no constantes. Por lo tanto, si una función se puede declarar como pure
, siempre se debe declarar así. Por ejemplo, considere el siguiente script:
pure func add2 ( x ) => x + 2 ;
var non_const = 25 ;
print ( add2 ( non_const ) ) ;
print ( add2 ( 5 ) ) ;
El árbol de sintaxis abstracta que utilizará el motor del lenguaje en tiempo de ejecución será:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
FuncDeclStmt(
Id("add2")
<NoCaptures>
IdList(
Id("x")
)
Expr04(
Id("x")
Op '+'
Int(2)
)
)
VarDecl(
Id("non_const")
Op '='
Int(25)
)
CallExpr(
Id("print")
ExprList(
CallExpr(
Id("add2")
ExprList(
Id("non_const")
)
)
)
)
CallExpr(
Id("print")
ExprList(
Int(7)
)
)
)
--------------------------
27
7
Como puede ver, en el primer caso se produce una llamada de función real porque non_const
no es una constante, mientras que en el segundo caso es COMO SI pasáramos un número entero literal a print()
.
Al igual que otras construcciones, MyLang
tiene un manejo de excepciones similar al de Python
, pero usa una sintaxis similar a C++
. La construcción básica es la declaración try-catch
. Veamos un ejemplo:
try {
var input_str = " blah " ;
var a = int ( input_str ) ;
} catch ( TypeErrorEx ) {
print ( " Cannot convert the string to integer " ) ;
}
Nota: si una excepción se genera mediante expresiones constantes (por ejemplo, int("blah")
), durante la evaluación constante, el error se informará directamente, sin pasar por ninguna lógica de manejo de excepciones. La razón de esto es imponer un fracaso temprano .
También se permiten múltiples declaraciones catch
:
try {
# body
} catch ( TypeErrorEx ) {
# error handling
} catch ( DivisionByZeroEx ) {
# error handling
}
Y, en caso de que se puedan manejar varias excepciones con el mismo código, también se puede utilizar una sintaxis más corta:
try {
# body
} catch ( TypeErrorEx , DivisionByZeroEx as e ) {
# error handling
print ( e ) ;
} catch ( OutOfBoundsEx ) {
# error handling
}
Las excepciones pueden contener datos, pero ninguna de las excepciones integradas actualmente los contiene. La lista de excepciones de tiempo de ejecución integradas que se pueden detectar con bloques try-catch
es:
En cambio, no se pueden detectar otras excepciones como SyntaxErrorEx
. También es posible en MyLang
detectar CUALQUIER excepción utilizando un bloque catch-anything:
try {
# body
} catch {
# Something went wrong .
}
Este idioma no admite tipos personalizados por el momento. Por lo tanto, no es posible lanzar ningún tipo de objeto como en otros lenguajes. Para generar una excepción, es necesario utilizar la función especial incorporada exception()
o su atajo, ex()
. Considere el siguiente ejemplo:
try {
throw ex ( " MyError " , 1234 ) ;
} catch ( MyError as e ) {
print ( " Got MyError, data: " , exdata ( e ) ) ;
}
Como sugiere la intuición, con ex()
creamos y luego lanzamos un objeto de excepción llamado MyError
, que tiene 1234
como datos de carga útil. Más tarde, en el bloque catch
, detectamos la excepción y extrajimos los datos de la carga útil utilizando la función incorporada exdata()
.
En caso de que una excepción dada no necesite tener una carga útil, es posible simplemente guardar el resultado de ex()
en una variable y lanzarlo más tarde usando una sintaxis probablemente más agradable:
var MyError = ex ( " MyError " ) ;
throw MyError ;
MyLang
admite volver a lanzar una excepción en el cuerpo de las declaraciones catch utilizando la palabra clave rethrow
dedicada:
try {
do_something ( ) ;
} catch {
print ( " Something went wrong!! " ) ;
rethrow ;
}
En algunos casos, puede ser necesario realizar alguna limpieza, después de ejecutar un bloque de código que podría generar una excepción. Para estos casos, MyLang
admite la conocida cláusula finally
, que funciona exactamente como en C#
:
try {
step1_might_throw ( ) ;
step2_might_throw ( ) ;
step3_might_throw ( ) ;
step4_might_throw ( ) ;
} catch ( TypeErrorEx ) {
# some error handling
} finally {
# clean - up
}
Vale la pena señalar que también se permiten construcciones try-finally
(sin ninguna cláusula catch
).
Las siguientes funciones integradas se evaluarán durante el tiempo de análisis cuando se les pasen argumentos constantes.
defined(symbol)
Compruebe si symbol
está definido. Devuelve 1 si el símbolo está definido, 0 en caso contrario.
len(container)
Devuelve el número de elementos en el contenedor dado.
str(value, [decimal_digits])
Convierte el valor dado en una cadena. Si value
es un flotante, el segundo parámetro indica el número deseado de dígitos decimales en la cadena de salida.
int(value)
Convierte la cadena dada en un número entero. Si el valor es flotante, se truncará. Si el valor es una cadena, se analizará y convertirá a un número entero, si es posible. Si el valor ya es un número entero, se devolverá tal como está.
float(value)
Convierta el valor dado a flotante. Si el valor es un número entero, se convertirá en un número de punto flotante. Si el valor es una cadena, se analizará y convertirá a flotante, si es posible. Si el valor ya es flotante, se devolverá tal cual.
clone(obj)
Clona el objeto dado. Útil para objetos no triviales como matrices, diccionarios y lambda con capturas.
type(value)
Devuelve el nombre del tipo del valor dado en forma de cadena. Útil para depurar.
hash(value)
Devuelve el valor hash utilizado por los diccionarios internamente cuando value
se utiliza como clave. Por el momento, solo los números enteros, flotantes y cadenas admiten hash()
.
array(N)
Devuelve una matriz de none