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)