O que faremos
Nós já completamos a primeira parte do desafio, criamos uma gramática para uma linguagem que só reconhece números inteiros e usamos o compilador do pacote nearley
para transformar o arquivo em uma AST, depois fizemos uma simples verificação de tipagem e por último criamos um arquivo javascript válido.
Pode não parecer muita coisa porque nossa linguagem só "enxerga" números, mas agora temos uma ideia mais concreta de como continuar com o processo.
O próximo passo é fazer nossa linguagem "enxergar" não só números inteiros, mas também os principais valores primitivos como números float, strings, etc.
Mas antes de continuar com o código queria fazer uma pausa para tomar decisões de design da linguagem. Como já disse antes essa linguagem será tipada, então seria como se fosse Typescript ou Rust e embora no final tudo será compilado para javascript eu acho que será um desafio interessante se fizermos algumas divergências de tipos em relação ao javascript.
No javascript temos alguns tipos literais bem básicos, eles são:
- boolean
- number
- string
- undefined
- null
- symbol
- bigint
Comparado com C por exemplo quem tem 13 variações dos principais tipos de char
até long double
. JavaScript tem uma variação de tipos muito pequena, meu objetivo com essa linguagem é mirar em algo como Rust ou Go, ou seja, será uma linguagem bem simples, porém mais sofisticada do que JavaScript.
Para começar podemos definir os seguintes tipos básicos:
- integer (int)
- float
- char
- string
- boolean (bool)
Não vou incluir nenhum tipo como null
ou undefined
ou algo assim, pois quero usar conceitos mais funcionais. Também não pretendo usar variações de números como octal
ou binary
por enquanto, vamos focar no que é mais importante primeiro.
Ajustando tokenizer e testando
Precisamos criar as RegExps dos novos tokens e alterar nosso objeto do tokenizer
//...
const tokenizer = moo.compile({
WS: /[ \t]+/,
string: /"(?:\\["\\]|[^\n"\\])*"/,
char: /'(?:[^'\\]|\\.)*'/,
float: /[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/,
int: /0|[-+]?[1-9][0-9]*/,
bool: /true|false/,
NL: { match: '\n', lineBreaks: true },
})
//..
Lembrando que a ordem aqui importa, por exemplo, o token string deve ser o primeiro dos valores literais, pois independente do que estiver escrito dentro de uma string (números, keywords, etc) ela sempre vai ser uma string.
Agora vamos alterar nossa gramática, criaremos uma nova regra chamada literal
que representará todos os valores literais possível (os que discutimos anteriormente) e então nosso programa será definido como uma única instância de um de tais valores.
#...
program -> literal {% id %}
literal
-> %int {% id %}
| %float {% id %}
| %string {% id %}
| %char {% id %}
| %bool {% id %}
Com a mudança da gramática temos que compilar a gramática javascript mais uma vez rodando o comando nearleyc lang0.ne -o lang0.js
Com a nova gramática podemos compilar nosso arquivo exemplo: node parser ex.ln0
Esperamos que tudo continue ocorrendo como antes e que o resultado seja esse:
{
"type": "int",
"value": "42",
"text": "42",
"offset": 0,
"lineBreaks": 0,
"line": 1,
"col": 1
}
Porém agora podemos alterar nosso programa para qualquer um dos valores literais que definimos e tudo deve funcionar corretamente, por exemplo, temos aqui um número float:
3.141592
E o resultado depois de rodar o comando node parser ex.ln0
:
{
"type": "float",
"value": "3.141592",
"text": "3.141592",
"offset": 0,
"lineBreaks": 0,
"line": 1,
"col": 1
}
ou a string "3.141592"
:
{
"type": "string",
"value": "\"3.141592\"",
"text": "\"3.141592\"",
"offset": 0,
"lineBreaks": 0,
"line": 1,
"col": 1
}
Perceba que o programa associa corretamente o "type" do objeto.
Ajustando type checker
Agora que nosso partes gera vários tipos de "ASTs" precisamos criar as funções necessárias para verificar os nodes seguindo a mesma lógica de antes.
Já possuíamos a função check_int
e vamos apenas copiar e colar, fazer algumas alterações e criar as outras funções:
//...
function check_float(node) {
const { type } = node
return type === 'float'
}
function check_char(node) {
const { type } = node
return type === 'char'
}
function check_string(node) {
const { type } = node
return type === 'string'
}
function check_bool(node) {
const { type } = node
return type === 'bool'
}
Perceba que notoriamente teria como criar uma abstração ou tornar o código menos repetitivo, vou propositalmente ignorar isso por enquanto e futuramente quando tivermos algo mais estável podemos voltar para refatorar.
Temos agora funções específicas para cada tipo de dado, mas precisamos de uma função que vai checkar se um valor é literal ou não, isso significa que se conseguirmos o valor true de alguma das funções que criamos então valor é literal.
Podemos aproveitar o fato de que todas as funções retornam boolean para tornar esse processo muito fácil, nossa funções pode ser escrita assim:
function check_literal(node) {
return (
check_int(node) ||
check_float(node) ||
check_char(node) ||
check_string(node) ||
check_bool(node)
)
}
E depois disso, basta trocar a função de checkagem para ser a check_literal
ao invés de ser a check_int
que estávamos usando até então:
//...
try {
const content = fs.readFileSync(filename, 'utf8');
const ast = JSON.parse(content);
console.log(check_literal(ast));
} catch(e) {
console.error(e?.message);
process.exit(1);
}
//...
Por enquanto nosso type checker apenas printa na tela true ou false e isso não ajuda muito a gente, mas futuramente podemos usar ele de maneira mais integrada, por enquanto vamos manter dessa forma para avançarmos mais rápido.
Finalizando com generate
O último passo é adaptar nosso generator, tecnicamente nossa função gen_int
já vai funcionar para todos os tipos de node automaticamente, então a solução mais simples é simplesmente renomear nossa função de gen_int
para gen_literal
e pronto:
//...
try {
const contents = fs.readFileSync(filepath, 'utf8');
const ast = JSON.parse(contents);
const output = gen_literal(ast);
fs.writeFileSync('output.js', output);
} catch(e) {
console.error(e?.message);
process.exit(1);
}
function gen_literal(node) {
const { value } = node
return value
}
Finalização
Nossa linguagem acaba de dar mais um passo e agora consegue "enxergar" instância únicas de int
, float
, bool
, string
e char
.
Pensando em próximos passos, já podemos trabalhar com operadores aritméticos como +
, -
, *
e /
.
Te vejo no próximo post :)
Top comments (0)