MyLang é uma linguagem de programação educacional simples inspirada em Python
, JavaScript
e C
, escrita como um desafio pessoal, em pouco tempo, principalmente para se divertir escrevendo um analisador descendente recursivo e explorar o mundo dos intérpretes. Não espere uma linguagem de script completa com bibliotecas e estruturas prontas para uso em produção. No entanto, MyLang
possui um conjunto mínimo de recursos integrados e também pode ser usado para fins práticos.
MyLang é escrito em C++ 17 portátil : no momento, o projeto não possui outras dependências além da biblioteca C++ padrão. Para construí-lo, se você tiver GNU make
instalado, basta executar:
$ make -j
Caso contrário, basta passar todos os arquivos .cpp para o seu compilador e adicionar o diretório src/
ao caminho de pesquisa de inclusão. Uma das coisas mais legais de não ter dependências é que não há necessidade de um sistema de compilação para compilações únicas.
Basta passar a opção BUILD_DIR
para make
:
$ make -j BUILD_DIR=other_build_directory
Se você quiser executar os testes do MyLang também, basta compilar com TESTS=1 e desabilitar as otimizações com OPT=0, para uma melhor experiência de depuração:
$ make -j TESTS=1 OPT=0
Em seguida, execute todos os testes com:
$ ./build/mylang -rt
Vale a pena notar que, embora estruturas de teste como GoogleTest e Boost.Test sejam infinitamente mais poderosas e flexíveis do que o mecanismo de teste trivial que temos em src/tests.cpp
, elas são dependências externas . Quanto menos dependências, melhor, certo? :-)
A maneira mais curta de descrever MyLang
é: uma linguagem python dinâmica com aparência de C. Provavelmente, a maneira mais rápida de aprender esta linguagem é verificar os scripts no diretório samples/
enquanto dá uma olhada na breve documentação abaixo.
MyLang
é uma linguagem dinâmica de digitação de pato, como Python
. Se você conhece Python
e deseja usar colchetes { }
, poderá usá-lo automaticamente. Sem surpresas. Strings são imutáveis como em Python
, arrays podem ser definidos usando [ ]
como em Python
e dicionários podem ser definidos usando { }
, também. A linguagem também suporta array-slices usando a mesma sintaxe [start:end]
usada pelo Python
.
Dito isso, MyLang
difere do Python
e de outras linguagens de script em vários aspectos:
Há suporte para constantes de tempo de análise declaradas usando const
.
Todas as variáveis devem ser declaradas usando var
.
As variáveis têm um escopo como em C
. O sombreamento é suportado quando uma variável é declarada explicitamente novamente usando var
, em um bloco aninhado.
Todas as declarações de expressão devem terminar com ;
como em C
, C++
e Java
.
As palavras-chave true
e false
existem, mas não existe um tipo boolean
. Como em C, 0
é falso, todo o resto é true
. Entretanto, strings, arrays e dicionários possuem um valor booleano, exatamente como em Python
(por exemplo, um array vazio é considerado false
). O true
builtin é apenas um alias para o número inteiro 1
.
O operador de atribuição =
pode ser usado como em C
, dentro de expressões, mas não existe operador vírgula, por causa do recurso de expansão de array.
MyLang suporta tanto o loop for
clássico quanto um loop foreach
explícito.
MyLang não oferece suporte a tipos personalizados no momento. No entanto, os dicionários suportam um bom açúcar sintático: além da sintaxe principal d["key"]
, para chaves de string a sintaxe d.key
também é suportada.
As variáveis são sempre declaradas com var
e vivem no escopo em que foram declaradas (embora sejam visíveis em escopos aninhados). Por exemplo:
# 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` .
É possível declarar múltiplas variáveis usando a seguinte sintaxe familiar:
var a , b , c ;
Mas há uma ressalva, provavelmente o único recurso "surpreendente" do MyLang
: inicializar variáveis não funciona como em C. Considere a seguinte afirmação:
var a , b , c = 42 ;
Nesse caso, em vez de apenas declarar a
e inicializar b
com c
, estamos inicializando todas as três variáveis com o valor 42. Para inicializar cada variável com um valor diferente, use a sintaxe de expansão de array:
var a , b , c = [ 1 , 2 , 3 ] ;
As constantes são declaradas de maneira semelhante às variáveis , mas não podem ser ocultadas em escopos aninhados. Por exemplo:
const c = 42 ;
{
# That's not allowed
const c = 1 ;
# That's not allowed as well
var c = 99 ;
}
No MyLang
as constantes são avaliadas em tempo de análise , de maneira semelhante às declarações constexpr do C++
(mas lá falamos sobre tempo de compilação ). Ao inicializar um const
, qualquer tipo de literal pode ser usado além de todo o conjunto de const internos. Por exemplo:
const val = sum ( [ 1 , 2 , 3 ] ) ;
const x = " hello " + " world " + " " + join ( [ " a " , " b " , " c " ] , " , " ) ;
Para entender exatamente como uma constante foi avaliada, execute o interpretador com a opção -s
, para despejar a árvore de sintaxe abstrata antes de executar o script. Para o exemplo acima:
$ 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(
)
--------------------------
Surpreso? Bem, outras constantes além de arrays e dicionários nem sequer são instanciadas como variáveis. Eles simplesmente não existem em tempo de execução . Vamos adicionar uma instrução 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
Agora, tudo deve fazer sentido. Quase a mesma coisa acontece com arrays e dicionários, com a exceção de que estes últimos também são instanciados em tempo de execução, para evitar literais potencialmente enormes em todos os lugares. Considere o seguinte exemplo:
$ ./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 você pode ver, a operação de fatia foi avaliada em tempo de análise durante a inicialização da constante s
, mas ambas as matrizes também existem em tempo de execução. As operações subscritas em expressões const, em vez disso, são convertidas em literais. Isso parece uma boa compensação para o desempenho: valores pequenos como números inteiros, flutuantes e strings são convertidos em literais durante a avaliação const , enquanto arrays e dicionários (potencialmente grandes) são deixados como símbolos somente leitura em tempo de execução, mas ainda permitindo algumas operações neles (como [index]
e len(arr)
) serão avaliadas const.
MyLang
suporta, no momento, apenas os seguintes tipos (integrados):
None O tipo de none
, o equivalente a None
do Python. Variáveis que acabaram de ser declaradas sem ter um valor atribuído a elas, têm valor none
(por exemplo, var x;
). O mesmo se aplica a funções que não possuem valor de retorno. Além disso, é usado como um valor especial por recursos internos como find()
, em caso de falha.
Inteiro Um inteiro com tamanho de ponteiro assinado (por exemplo, 3
).
Float Um número de ponto flutuante (por exemplo, 1.23
). Internamente, é um duplo longo.
String Uma string como "olá". Strings são imutáveis e suportam fatias (ex. s[3:5]
ou s[3:]
ou s[-2:]
, tendo o mesmo significado que em Python
).
Array Um tipo mutável para arrays e tuplas (por exemplo, [1,2,3]
). Ele pode conter itens de diferentes tipos e suporta fatias graváveis. As fatias da matriz se comportam como cópias enquanto, nos bastidores, usam técnicas de cópia na gravação.
Dicionário Dicionários são mapas hash definidos usando a sintaxe do Python
: {"a": 3, "b": 4}
. Os elementos são acessados com a sintaxe familiar d["key-string"]
ou d[23]
, podem ser consultados com find()
e excluídos com erase()
. No momento, apenas strings, inteiros e flutuantes podem ser usados como chaves de um dicionário. Vantagens : chaves de string semelhantes a identificadores também podem ser acessadas com a sintaxe "membro de": d.key
.
Função Ambas as funções autônomas e lambdas têm o mesmo tipo de objeto e podem ser transmitidas como qualquer outro objeto (veja abaixo). Porém, apenas lambdas podem ter uma lista de captura. Funções regulares não podem ser executadas durante a avaliação const, enquanto funções pure
podem. Funções puras só podem ver consts e seus argumentos.
Exceção O único tipo de objeto que pode ser lançado. Para criá-los, use o exception()
embutido ou seu atalho ex()
.
As instruções condicionais funcionam exatamente como em C
. A sintaxe é:
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
E as chaves { }
podem ser omitidas como em C
, no caso de blocos de instrução única. conditionExpr
pode ser qualquer expressão, por exemplo: (a=3)+b >= c && !d
.
Quando conditionExpr
é uma expressão que pode ser avaliada const, toda a instrução if é substituída pela ramificação verdadeira, enquanto a ramificação falsa é descartada. Por exemplo, considere o seguinte script:
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
Além de sempre imprimir "sim", nem precisa verificar nada antes de fazer isso. Verifique a árvore de sintaxe abstrata:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
Block(
CallExpr(
Id("print")
ExprList(
"yes"
)
)
)
)
--------------------------
yes
MyLang
suporta os clássicos loops while
e 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 ;
}
Aqui, as chaves { }
podem ser omitidas como no caso acima. Existem apenas algumas diferenças em relação C
que vale a pena destacar:
Os operadores ++
e --
não existem no MyLang
no momento.
Para declarar múltiplas variáveis, use a sintaxe: var a, b = [3,4];
ou apenas var a,b,c,d = 0;
se você quiser que todas as variáveis tenham o mesmo valor inicial.
Para aumentar o valor de múltiplas variáveis use a sintaxe: a, b += [1, 2]
. Nos casos extremamente raros e complexos, quando na instrução de incremento do loop for precisamos atribuir a cada variável uma nova variável usando expressões diferentes, aproveite a sintaxe de expansão na atribuição: i, j = [i+2, my_next(i, j*3)]
.
MyLang
suporta loops foreach
usando uma sintaxe bastante familiar:
var arr = [ 1 , 2 , 3 ] ;
foreach ( var e in arr ) {
print ( " elem: " , e ) ;
}
Loops Foreach podem ser usados para arrays, strings e dicionários. Por exemplo, iterar cada par <key, value>
em um dicionário é fácil:
var d = { " a " : 3 , " b " : 10 , " c " : 42 } ;
foreach ( var k , v in d ) {
print ( k + " => " + str ( v ) ) ;
}
Para iterar apenas por cada chave, basta usar var k in d
.
MyLang
também suporta enumeração em loops foreach. Verifique o exemplo a seguir:
var arr = [ " a " , " b " , " c " ] ;
foreach ( var i , elem in indexed arr ) {
print ( " elem[ " + str ( i ) + " ] = " + elem ) ;
}
Em outras palavras, quando o nome do contêiner é precedido pela palavra-chave indexed
, a primeira variável recebe um número progressivo a cada iteração.
Ao iterar por meio de uma matriz de pequenas matrizes de tamanho fixo (pense em tuplas), é possível expandir diretamente essas "tuplas" no loop 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 uma função é simples como:
func add ( x , y ) {
return x + y ;
}
Mas vários atalhos também são suportados. Por exemplo, no caso de funções de instrução única como a acima, a seguinte sintaxe pode ser usada:
func add ( x , y ) => x + y ;
Além disso, embora seja uma boa prática sempre escrever ()
para funções sem parâmetros, eles são opcionais nesta linguagem:
func do_something { print ( " hello " ) ; }
As funções são tratadas como símbolos regulares no MyLang
e não há diferenças substanciais entre funções autônomas e lambdas nesta linguagem. Por exemplo, podemos declarar a função add
(acima) como um lambda desta forma:
var add = func ( x , y ) => x + y ;
Nota: ao criar objetos de função em expressões, não podemos atribuir um nome a eles.
Lambdas também suportam uma lista de capturas, mas capturas implícitas não são suportadas, para garantir clareza. Claro, lambdas podem ser retornados como qualquer outro objeto. Por exemplo:
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
Lambdas com capturas possuem um state , como seria de esperar. Considere o seguinte roteiro:
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 ( ) ) ;
Ele gera a saída:
c1: 6
c1: 7
c1: 8
c2: 9
c2: 10
c2: 11
c1: 9
c1: 10
c1: 11
Objetos de função regulares definidos pelo usuário (incluindo lambdas) não são considerados const
e, portanto, não podem ser executados durante a avaliação const. Essa é uma limitação muito forte. Considere o seguinte exemplo:
const people = [
[ " jack " , 3 ] ,
[ " alice " , 11 ] ,
[ " mario " , 42 ] ,
[ " bob " , 38 ]
] ;
const sorted_people = sort ( people , func ( a , y ) => a [ 0 ] < b [ 0 ] ) ;
Nesse caso, o script não pode criar o array const sorted_people
. Como passamos um objeto de função para o const sort()
integrado, obteremos um erro ExpressionIsNotConstEx
. Claro, se sorted_people
fosse declarado como var
, o script seria executado, mas o array não seria mais const e não poderemos nos beneficiar de nenhuma otimização de tempo de análise . Portanto, embora o sort()
interno possa ser chamado durante a avaliação const, quando ele tem um parâmetro compare func
personalizado, isso não é mais possível.
Para superar a limitação que acabamos de descrever, MyLang
possui uma sintaxe especial para funções puras . Quando uma função é declarada com a palavra-chave pure
precedendo func
, o interpretador a trata de uma maneira especial: ela pode ser chamada a qualquer momento, tanto durante a avaliação const quanto durante o tempo de execução , mas a função não pode ver variáveis globais, nem capturar nada: ela só pode usar constantes e o valor de seus parâmetros: é exatamente disso que precisamos durante a avaliação const. Por exemplo, para gerar sorted_people
durante a avaliação const basta escrever:
const sorted_people = sort ( people , pure func ( a , b ) => a [ 0 ] < b [ 0 ] ) ;
Funções puras podem ser definidas como funções autônomas e também podem ser usadas com parâmetros não const. Portanto, se uma função puder ser declarada como pure
, ela deverá sempre ser declarada dessa forma. Por exemplo, considere o seguinte script:
pure func add2 ( x ) => x + 2 ;
var non_const = 25 ;
print ( add2 ( non_const ) ) ;
print ( add2 ( 5 ) ) ;
A árvore de sintaxe abstrata que o mecanismo da linguagem usará em tempo de execução 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 você pode ver, no primeiro caso, uma chamada de função real acontece porque non_const
não é uma constante, enquanto no segundo caso é COMO SE passássemos um número inteiro literal para print()
.
Como outras construções, MyLang
possui um tratamento de exceções semelhante ao do Python
, mas usando uma sintaxe semelhante ao C++
. A construção básica é a instrução try-catch
. Vejamos um exemplo:
try {
var input_str = " blah " ;
var a = int ( input_str ) ;
} catch ( TypeErrorEx ) {
print ( " Cannot convert the string to integer " ) ;
}
Nota: se uma exceção for gerada por expressões constantes (por exemplo, int("blah")
), durante a avaliação const, o erro será relatado diretamente, ignorando qualquer lógica de tratamento de exceção. A razão para isso é impor falhas precoces .
Várias instruções catch
também são permitidas:
try {
# body
} catch ( TypeErrorEx ) {
# error handling
} catch ( DivisionByZeroEx ) {
# error handling
}
E, caso várias exceções possam ser tratadas com o mesmo código, uma sintaxe mais curta também pode ser usada:
try {
# body
} catch ( TypeErrorEx , DivisionByZeroEx as e ) {
# error handling
print ( e ) ;
} catch ( OutOfBoundsEx ) {
# error handling
}
As exceções podem conter dados, mas nenhuma das exceções integradas atualmente contém. A lista de exceções de tempo de execução integradas que podem ser detectadas com blocos try-catch
é:
Outras exceções como SyntaxErrorEx
não podem ser capturadas. Também é possível no MyLang
capturar QUALQUER exceção usando um bloco catch-anything:
try {
# body
} catch {
# Something went wrong .
}
Esta linguagem não oferece suporte a tipos personalizados no momento. Portanto, não é possível lançar nenhum tipo de objeto como em outras linguagens. Para lançar uma exceção, é necessário usar a função especial integrada exception()
ou seu atalho, ex()
. Considere o seguinte exemplo:
try {
throw ex ( " MyError " , 1234 ) ;
} catch ( MyError as e ) {
print ( " Got MyError, data: " , exdata ( e ) ) ;
}
Como sugere a intuição, com ex()
criamos e posteriormente lançamos um objeto de exceção chamado MyError
, tendo 1234
como dados de carga útil. Posteriormente, no bloco catch
, capturamos a exceção e extraímos os dados da carga útil usando o exdata()
embutido.
Caso uma determinada exceção não precise ter payload, é possível apenas salvar o resultado de ex()
em uma variável e lançá-la posteriormente usando uma sintaxe provavelmente mais agradável:
var MyError = ex ( " MyError " ) ;
throw MyError ;
MyLang
suporta o relançamento de uma exceção no corpo das instruções catch usando a palavra-chave rethrow
dedicada:
try {
do_something ( ) ;
} catch {
print ( " Something went wrong!! " ) ;
rethrow ;
}
Em alguns casos, pode ser necessário fazer alguma limpeza, após executar um bloco de código que possa gerar uma exceção. Para estes casos, MyLang
suporta a conhecida cláusula finally
, que funciona exatamente como em C#
:
try {
step1_might_throw ( ) ;
step2_might_throw ( ) ;
step3_might_throw ( ) ;
step4_might_throw ( ) ;
} catch ( TypeErrorEx ) {
# some error handling
} finally {
# clean - up
}
É importante notar que construções try-finally
(sem qualquer cláusula catch
) também são permitidas.
As seguintes funções integradas serão avaliadas durante o tempo de análise quando argumentos const forem passados para elas.
defined(symbol)
Verifique se symbol
está definido. Retorna 1 se o símbolo estiver definido, 0 caso contrário.
len(container)
Retorne o número de elementos no contêiner fornecido.
str(value, [decimal_digits])
Converta o valor fornecido em uma string. Se value
for flutuante, o segundo parâmetro indica o número desejado de dígitos decimais na string de saída.
int(value)
Converta a string fornecida em um número inteiro. Se o valor for float, ele será trucado. Se o valor for uma string, ele será analisado e convertido em um número inteiro, se possível. Se o valor já for um número inteiro, ele será retornado como está.
float(value)
Converta o valor fornecido em float. Se o valor for um número inteiro, ele será convertido em um número de ponto flutuante. Se o valor for uma string, ele será analisado e convertido em float, se possível. Se o valor já for flutuante, ele será retornado como está.
clone(obj)
Clone o objeto fornecido. Útil para objetos não triviais como arrays, dicionários e lambda com capturas.
type(value)
Retorne o nome do tipo do valor fornecido em formato de string. Útil para depuração.
hash(value)
Retorna o valor hash usado internamente pelos dicionários quando value
é usado como chave. No momento, apenas números inteiros, flutuantes e strings suportam hash()
.
array(N)
Retornar uma matriz de none