DEV Community

Cover image for Mais níveis de precedência e mais operadores
Lucas Almeida
Lucas Almeida

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)

This post blew up on DEV in 2020:

js visualized

🚀⚙️ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! 🥳

Happy coding!