DEV Community

Cover image for Mais níveis de precedência e mais operadores
nullen box
nullen box

Posted on • Edited on

Mais níveis de precedência e mais operadores

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]*/, //... 
Enter fullscreen mode Exit fullscreen mode

Vamos altera-las para:

//... float: /(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/, int: /0|[1-9][0-9]*/, //... 
Enter fullscreen mode Exit fullscreen mode

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 %} 
Enter fullscreen mode Exit fullscreen mode

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], }) %} 
Enter fullscreen mode Exit fullscreen mode

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 %} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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", //... } } 
Enter fullscreen mode Exit fullscreen mode

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: '!', //... 
Enter fullscreen mode Exit fullscreen mode

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], }) %} 
Enter fullscreen mode Exit fullscreen mode

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 %} 
Enter fullscreen mode Exit fullscreen mode

Agora podemos compilar o arquivo de gramática usando pnpm nc

Vamos alterar nosso exemplo agora para fazer o teste de compilação:

-3 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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) } 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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}` } 
Enter fullscreen mode Exit fullscreen mode

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 '' } } 
Enter fullscreen mode Exit fullscreen mode

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)