Custom programming language based on the classic english grammar. Created for Insper's 2021.1 Computer Logic class.
A linguagem Thou é uma linguagem de programação baseada na gramática do inglês clássico e inspirada pela linguagem C. A sintática da linguagem tenta ser maix próxima da fala do que apenas de comandos imperativos.
MAIN = FUNCTION ;
FUNCTION = TYPE, IDENTIFIER, "(", [{PARAM}], ")", BLOCK ;
PARAM = TYPE, IDENTIFIER ;
RETURN = "return_to_the_one_whom_bid_thou", (EXPRESSION | COMPARISON), ";" ;
FUNCTION_CALL = IDENTIFIER, "(", (EXPRESSION | COMPARISON), {",", (EXPRESSION | COMPARISON)}, ")", ";" ;
BLOCK = "{", STATEMENT, { STATEMENT }, "}" ;
STATEMENT = ( λ | ASSIGNMENT | PRINT | IF | WHILE | RETURN | FUNCTION_CALL) ;
PRINT = "thou_shalt_utter", "(", (EXPRESSION | COMPARISON), ")", ";" ;
IF = "shouldst_this_be_true", "(", COMPARISSON, ")", BLOCK, { ELSE };
ELSE = "if_naught", BLOCK ;
WHILE = "thou_shall_repeat_if", "(", COMPARISSON, ")", BLOCK ;
TYPE = "maths" | "is*it_true" | "kayne_west_phrase" ;
IDENTIFIER = LETTER, { LETTER | DIGIT | "*" } ;
ASSIGNMENT = TYPE, IDENTIFIER, "=", (COMPARISON | EXPRESSION | NUMBER | STRING | BOOLEAN), ";" ;
ASSIGNMENT = TYPE, IDENTIFIER, ";" ;
COMPARISON = EXPRESSION, (">", "<", "==", ">=", "<=", "!=", "&&", "||"), EXPRESSION ;
EXPRESSION = TERM, { ("+" | "-"), TERM } ;
TERM = FACTOR, { ("\*" | "/"), FACTOR } ;
FACTOR = (("+" | "-"), FACTOR) | NUMBER | "(", EXPRESSION, ")" | IDENTIFIER ;
BOOLEAN = "it_is_sooth" | "it_is_false" ;
NUMBER = DIGIT, { DIGIT } ;
DIGIT = ( 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 ) ;
STRING = '"', LETTER, { LETTER }, '"' ;
LETTER = ( a | ... | z | A | ... | Z ) ;
A linguagem Thou conta apenas com 3 tipos de variáveis. Esses tipos são:
maths
, que é igual ao tipoint
da linguagem C.is_it_true
, que é igual ao tipobool
da linguagem C.kayne_west_phrase
, que é igual ao tipochar[]
da linguagem C.
maths x = 1;
x = 3;
is_it_true y = it_is_sooth;
y = it_is_false;
kanye_west_phrase a = "abc";
a = "abcd";
Toda função na linguagem Thou tem que retornar alguma coisa. O retorno pode ser de qualquer tipo. Além disso, é obrigatório que todas as funções tenham o retorno dentro de seus comandos.
maths main() {
maths x = 0;
return_to_the_one_whom_bid_thou 0;
}
shouldst_this_be_true (x == 1)
{
return_to_the_one_whom_bid_thou 1;
} if_naught
{
return_to_the_one_whom_bid_thou x * factorial(x - 1);
}
maths x = 0;
thou_shall_repeat_if (x < 10)
{
thou_shalt_utter(x);
x = x + 1;
}
maths x = 0;
thou_shalt_utter(x);
maths sum(maths a, maths b)
{
return_to_the_one_whom_bid_thou a + b;
}
maths main()
{
maths x = sum(25, 26);
thou_shalt_utter(x);
}
maths x = 1;
is_it_true y = x == 1;
y = x != 1;
y = x > 1;
y = x >= 1;
y = x < 1;
y = x <= 1;
y = x && 1;
y = x || 1;
maths factorial(maths x)
{
shouldst_this_be_true (x == 1)
{
return_to_the_one_whom_bid_thou 1;
} if_naught
{
return_to_the_one_whom_bid_thou x * factorial(x - 1);
}
}
maths main()
{
thou_shalt_utter(factorial(5));
return_to_the_one_whom_bid_thou 0;
}
maths random(maths x)
{
thou_shall_repeat_if (x < 10)
{
x = x + 1;
thou_shalt_utter(x);
}
return_to_the_one_whom_bid_thou 99;
}
maths main()
{
thou_shalt_utter(random(4));
return_to_the_one_whom_bid_thou 0;
}
A implementação da linguagem foi feita usando o Sly como lexer e parser e o LLVMlite como compilador.
A análise léxica e a tokenização da linguagem são feitas pela classe ThouLexer
, no arquivo tokenizer.py. Todos o marcadores da linaguagem são definidos usando Regex Strings.
O módulo Sly permite que o parsing da linguagem seja feito de maneira muito parecida à EBNF. A partir da definicão de regras de produção (como as da EBNF) podemos fazer o parse rapidamente da linguagem. Apesar da simplicidade do parsing, a criação da AST ainda é feita manualmente. Desse modo, a classe ThouParser
, no arquivo parser.py, fica responsável pelo parsing e a construção da AST.
O módulo LLVMlite traz uma abstração totalmente em Python do LLVM, facilitando a criação de linguagens imensamente. Ele permite que todas as estruturas da linguagem sejam definidas em cada nó e inteiramente em Python.
Sendo assim, a classe CodeGen
, no arquivo codegen.py, é responsável por gerar todo os objetos necessários para a utilização do LLVMlite. É aqui que é definida a função principal main
e seu tipos, tanto de retorno como os argumentos.
Os nós da AST se encontram na pasta ./src/models/nodes. Cada nó é responsável pela criação de suas instruções em relação ao LLVMlite.
Foi utilizado um logger para facilitar o debugging durante a criação da linguagem. Ele funciona printando as mensagens no terminal, caso a flag de debug esteja ativada. A quantidade de mensagens de log pode ser controlada com o nível de verbosity.
Como a linguagem depende dos módulos Sly e LLVMlite, precisamos instalar eles antes de começar a usar o compilador.
Para controlar as dependências do projeto foi usado o Pipenv. Ele permite a criação e compartilhamento de ambientes virtuais de Python facilmente. Existe um arquivo Pipfile que lista todas as dependências do compilador, tanto as de utilização quantos as de desenvolvimento, para usa-lo basta executar o seguinte comando:
pipenv install
Para executar o compilador podemos usar do Makefile, que contém todos os comandos necessário para compilar e gerar um executável. Ele necessita de apenas um argumento IN
, que diz qual o arquivo de entrada do programa.
- Para rodar todas as etapas, basta usar:
make IN="./tests/in00.thou"
- Para apenas gerar o arquivo LLVM resultante basta:
make gen_ll IN="./tests/in00.thou"
- Para apenas gerar o arquivo LLVM resultante juntamente com todos o logs basta:
make gen_ll_debug IN="./tests/in00.thou"
Obs: Todos os arquivos de saída do compilador ficam na pasta out
, incluindo os logs gerados, o arquivo LLVM resultante, e o executável gerado.