No último post adicionamos os operadores binários + e - em nossa linguagem, nesse post vamos adicionar os operadores binários * e / além dos unários ! e -, repare que o sinal de menos poder tanto um operador binário onde realmente seria uma operação de subtração ou pode ser um operador unário indicando que o número é negativo.
Olhando nas nossas definições de tokens iniciais, podemos já identificar um problema, os token incluem sinalização, teremos que alterar isso para não causar confusão no parser:
Atualmente temos essas duas definições:
//...
float: /[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/,
int: /0|[-+]?[1-9][0-9]*/,
//...
Vamos altera-las para:
//...
float: /(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/,
int: /0|[1-9][0-9]*/,
//...
Dessa forma temos controle total da nossa gramática e o tokenizer não vai nos atrapalhar.
Com isso resolvido vamos continuar com nossa modificação, primeiramente implementando os operadores * e /
Adicionando os operadores * e /
Vamos adicionar os operadores à gramática:
factor_operator
-> %star {% id %}
| %slash {% id %}
E a regra de expressão única do tipo factor:
factor_expression
-> literal __ factor_operator __ literal {% data => ({
type: 'binary_expression',
operator: data[2],
left: data[0],
right: data[4],
}) %}
vamos também adicionar a regra factor_expression como opção para definição de um program válido:
program
-> literal {% id %}
| term_expression {% id %}
| factor_expression {% id %}
Lembrando mais uma vez que por enquanto a linguagem suporta apenas uma expressão por vez. Uma multi-expressão como essa 2 + 3 * 4 ainda não é suportada pela nossa linguagem, trabalharemos nisso mais pra frente.
Para prosseguirmos vamos compilar nossa gramática com o comando pnpm nc.
Também vou alterar nosso programa exemplo dessa forma:
2 * 2
E compilar o programa: node parser ex.ln0
O resultado final é correto e fica dessa forma (omiti algumas informações por efeitos de concisão):
{
"type": "binary_expression",
"operator": {
"type": "star",
"value": "*",
//...
},
"left": {
"type": "int",
"value": "2",
//...
},
"right": {
"type": "int",
"value": "2",
//...
}
}
Como os novos operadores geram nodes do tipo binary_expression não precisamos alterar nosso arquivo typecheck.js. Da mesma forma nossa função gen_binary_expression do nosso arquivo generator.js já funcionará corretamente.
Para verificar vou continuar com o processo rodando o comando node typecheck ast.json, o resultado é true.
E rodando o comando node generator ast.json, o resultado é o arquivo output.js contendo o texto 2 * 2, ou seja, tudo funcionando perfeitamente.
Adicionando operadores unários
Operadores unário são operadores que recebem apenas um operando, os principais são o operador de negação lógica ! e o operador de negação aritmética - repare que o o símbolo - pode ser tanto o operador binário de subtração aritmética quanto a outra versão, o operador unários.
Para evitar confusão vamos definir uma regra geral para a linguagem onde os operadores unário dever estar localizados imediatamente ao lado do operando, por exemplo, esse seria um código inválido - 2, por causa do espaço, o correto seria -2 sem espaço.
Para isso precisamos alterar nossa gramática e nosso tokenizer mais uma vez.
Começando com alterações dos tokens, vamos adicionar o símbolo de negação lógica !:
//...
bang: '!',
//...
Na nossa gramática vamos criar as regras para operadores e expressões unárias:
unary_operator
-> %bang {% id %}
| %dash {% id %}
#...
unary_expression
-> unary_operator literal {% data => ({
type: 'unary_expression',
operator: data[0],
argument: data[1],
}) %}
Perceba que na definição de unary_expression não há nenhuma regra de espaçamento (_ ou __) para indicarmos que espaço entre o operador e o operando é proibido.
Precisamos incluir a nova regra na definição de program:
program
-> literal {% id %}
| term_expression {% id %}
| factor_expression {% id %}
| unary_expression {% id %}
Agora podemos compilar o arquivo de gramática usando pnpm nc
Vamos alterar nosso exemplo agora para fazer o teste de compilação:
-3
rodando o comando node parser ex.ln0 temos como resultado a seguinte AST:
{
"type": "unary_expression",
"operator": {
"type": "dash",
"value": "-",
"text": "-",
"offset": 0,
"lineBreaks": 0,
"line": 1,
"col": 1
},
"argument": {
"type": "int",
"value": "3",
"text": "3",
"offset": 1,
"lineBreaks": 0,
"line": 1,
"col": 2
}
}
O que indica que tudo está funcionando corretamente.
Para finalizar basta adicionar novas funções para expressões unárias nos arquivos typecheck.js e generator.js
Começando com o typecheck.js precisamos criar uma função check_unary_expression e alterar nossa lógica principal.
Primeiro criamos a função:
function check_unary_expression(node) {
const { argument } = node
return check_number(argument)
}
E agora alteramos a lógica principal adicionando a branch de unary_expression:
function check_program(ast) {
const { type } = ast
if (type === 'literal') {
return check_literal(ast)
} else if (type === 'binary_expression') {
return check_binary_expression(ast)
} else if (type === 'unary_expression') {
return check_unary_expression(ast)
} else {
console.log(`Invalid AST has type = ${type}`)
return false
}
}
Rodando o comando node typecheck ast.json o resultado no console é true indicando sucesso.
Por último vamos criar a função no arquivo generator.js:
function gen_unary_expression(node) {
const { operator, argument } = node
return `${operator.value}${argument.value}`
}
E agora basta alterar a lógica principal também:
function gen_program(ast) {
const { type } = ast
if (type === 'literal') {
return gen_literal(ast)
} else if (type === 'binary_expression') {
return gen_binary_expression(ast)
} else if (type === 'unary_expression') {
return gen_unary_expression(ast)
} else {
console.log(`Invalid AST has type = ${type}`)
return ''
}
}
Rodando o comando node generator ast.json o arquivo output.js possuiu o texto -3 indicando que tudo está funcionando corretamente
Próximos passos
Por enquanto nossa expressões são "únicas", ou seja, expressões encadeadas como essa 2 + 3 * 4 simplesmente não são suportadas pela nossa linguagem ainda e é nisso que vamos trabalhar no próximo capítulo.
Top comments (0)