DEV Community

Cover image for Expressões encadeadas e agrupamento
Lucas Almeida
Lucas Almeida

Posted on

Expressões encadeadas e agrupamento

Já temos um bom arsenal de expressões singulares preparado, nossa linguagem já "entende" expressões aritméticas binárias e unárias e agora precisamos fazer com que ela "entenda" expressões encadeadas.

Pra isso vamos definir todos os tipos de expressões e usaremos a sequência de precedência para fazer as alterações necessárias.

No nível mais baixo temos os valores literais e os agrupamentos, que são expressões entre parênteses, para trabalharmos com agrupamentos primeiro precisamos alterar nosso tokenizer:

//...
  lparen: '(',
  rparen: ')',
//...
Enter fullscreen mode Exit fullscreen mode

Agora podemos criar a regra primary_expression que representa o primeiro nível de expressões:

primary_expression
  -> literal {% id %}
  | %lparen term_expression %rparen {% data => data[1] %}
Enter fullscreen mode Exit fullscreen mode

Essa regra interpreta literais ou expressões entre parênteses.

A próxima na lista é a unary_expression que já temos, porém precisaremos fazer alguma alterações:

unary_expression
  -> primary_expression {% id %}
  | unary_operator primary_expression {% data => ({
    type: 'unary_expression',
    operator: data[0],
    argument: data[1],
  }) %}
Enter fullscreen mode Exit fullscreen mode

Perceba a alteração de literal para primary_expression, e a adição da variante anterior primary_expression como opção de resolução, essas alterações fazem uma regra depender da outra ao mesmo tempo que evita ambiguidade, vamos continuar com essa lógica subindo a hierarquia.

A próxima da lista é a factor_expression:

factor_expression
  -> unary_expression {% id %}
  | factor_expression __ factor_operator __ factor_expression {% data => ({
    type: 'binary_expression',
    operator: data[2],
    left: data[0],
    right: data[4],
  }) %}
Enter fullscreen mode Exit fullscreen mode

E por último a term_expression:

term_expression
  -> factor_expression {% id %}
  | term_expression __ term_operator __ term_expression {% data => ({
    type: 'binary_expression',
    operator: data[2],
    left: data[0],
    right: data[4],
  }) %}
Enter fullscreen mode Exit fullscreen mode

Testando

Basta compilar a gramática com o comando pnpm nc
Vamos também alterar nosso arquivo ex.ln0 escrevendo a expressão 2 + 3 * 4
Ao rodar o comando node parser ex.ln0, temos a seguinte AST:

{
  "type": "binary_expression",
  "operator": {
    "type": "plus",
    "value": "+",
    //...
  },
  "left": {
    "type": "int",
    "value": "2",
    //...
  },
  "right": {
    "type": "binary_expression",
    "operator": {
      "type": "star",
      "value": "*",
      //...
    },
    "left": {
      "type": "int",
      "value": "3",
      //...
    },
    "right": {
      "type": "int",
      "value": "4",
      //...
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

O resultado é o esperado, o primeiro operador colapsado é o +, e a propriedade right é uma expressão binária inteira com o operador * portanto a regra de precedência se faz correta visto que o símbolo de * será avaliado primeiro por estar mais longe da raiz da AST.

Vamos alterar nosso exemplo dessa forma 2 + -(3 * 4 + 1) / 2, assim podemos ver na prática todas as variações de expressões.
Depois de rodar o comando node parser ex.ln0, temos o resultado:

{
  "type": "binary_expression",
  "operator": {
    "type": "plus",
    "value": "+",
    //...
  },
  "left": {
    "type": "int",
    "value": "2",
    //...
  },
  "right": {
    "type": "binary_expression",
    "operator": {
      "type": "slash",
      "value": "/",
      //...
    },
    "left": {
      "type": "unary_expression",
      "operator": {
        "type": "dash",
        "value": "-",
        //...
      },
      "argument": {
        "type": "binary_expression",
        "operator": {
          "type": "plus",
          "value": "+",
          //...
        },
        "left": {
          "type": "binary_expression",
          "operator": {
            "type": "star",
            "value": "*",
            //...
          },
          "left": {
            "type": "int",
            "value": "3",
            //...
          },
          "right": {
            "type": "int",
            "value": "4",
            //...
          }
        },
        "right": {
          "type": "int",
          "value": "1",
          //...
        }
      }
    },
    "right": {
      "type": "int",
      "value": "2",
      //...
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Por enquanto tudo parece estar dentro dos conformes.

Expansão rápida

Já possuímos um "framework" de expressões binárias e unárias agora podemos expandir para não só expressões aritméticas mas também comparativas, lógicas e bitwise. Vamos aproveitar o fato de que é só copiar e colar para melhorar um pouco a nossa organização de código e fazer algumas refatorações.

Primeiramente vamos criar uma função especial para compilar nossos nodes do tipo binary_expression no nosso arquivo de gramática:

@{%
const tokenizer = require('./tokenizer')

function binary_expression(data) {
  return {
    type: 'binary_expression',
    operator: data[2],
    left: data[0],
    right: data[4]
  }
}
%}
Enter fullscreen mode Exit fullscreen mode

Dessa forma podemos para de copiar e colar esse pedaço de código nas expressões binárias, a refatoração fica dessa forma:

factor_expression
  -> unary_expression {% id %}
  | factor_expression __ factor_operator __ factor_expression {% binary_expression %}

term_expression
  -> factor_expression {% id %}
  | term_expression __ term_operator __ term_expression {% binary_expression %}
Enter fullscreen mode Exit fullscreen mode

Antes de continuar precisamos aumentar nosso vocabulário de tokens adicionando os items >=, <=, !=, ==, >, <, =, |, & e ^ (lembrando de definir os tokens duplos antes dos simples para evitar errors):

//...
  gte: '>=',
  lte: '<=',
  neq: '!=',
  eqeq: '==',
  lor: '||',
  land: '&&',
//...
  or: '|',
  gt: '>',
  lt: '<',
  eq: '=',
  and: '&',
  caret: '^',
//...
Enter fullscreen mode Exit fullscreen mode

Vamos agora adicionar nossas regras de operadores comparativos, lógicos e bitwise:

comparison_operator
  -> %gt {% id %}
  | %gte {% id %}
  | %lt {% id %}
  | %lte {% id %}

equality_operator
  -> %eqeq {% id %}
  | %neq {% id %}
Enter fullscreen mode Exit fullscreen mode

Note que não precisamos definir regras para operadores lógicos e bitwise pois cada um deles possuiu um nível de precedência diferente e fazer isso seria redundante.

O próximo passo é definir as regras de expressões de comparação e de igualdade :

comparison_expression
  -> term_expression {% id %}
  | comparison_expression __ comparison_operator __ comparison_expression {% binary_expression %}

equality_expression
  -> comparison_expression {% id %}
  | equality_expression __ equality_operator __ equality_expression {% binary_expression %}
Enter fullscreen mode Exit fullscreen mode

E depois as regras individuais de expressões bitwise e de lógica:

# bitwise start
bitwise_and_expression
  -> equality_expression {% id %}
  | bitwise_and_expression __ %and __ equality_expression {% binary_expression %}

bitwise_xor_expression
  -> bitwise_and_expression {% id %}
  | bitwise_xor_expression __ %caret __ bitwise_and_expression {% binary_expression %}

bitwise_or_expression
  -> bitwise_xor_expression {% id %}
  | bitwise_or_expression __ %or __ bitwise_xor_expression {% binary_expression %}
# bitwise end

# logical start
logical_and_expression
  -> bitwise_or_expression {% id %}
  | logical_and_expression __ %lor __ bitwise_or_expression {% binary_expression %}

logical_or_expression
  -> logical_and_expression {% id %}
  | logical_or_expression __ %land __ logical_and_expression {% binary_expression %}
# logical end
Enter fullscreen mode Exit fullscreen mode

por último basta trocar nossa definição de program para usar a última expressão da lista a logcal_or_expression:

program
  -> logical_or_expression {% id %}
Enter fullscreen mode Exit fullscreen mode

Depois de compilar a gramática com pnpm nc e alterar nosso programa exemplo para 1 + 2 > 3 * 4 == 3 < 1 | 3 - 1
Vamos rodar o comando node parser ex.ln0 e o resultado é:

{
  "type": "binary_expression",
  "operator": {
    "type": "or",
    //...
  },
  "left": {
    "type": "binary_expression",
    "operator": {
      "type": "eqeq",
      //...
    },
    "left": {
      "type": "binary_expression",
      "operator": {
        "type": "gt",
        //...
      },
      "left": {
        "type": "binary_expression",
        "operator": {
          "type": "plus",
          //...
        },
        "left": {
          "type": "int",
          "value": "1",
          //...
        },
        "right": {
          "type": "int",
          "value": "2",
          //...
        }
      },
      "right": {
        "type": "binary_expression",
        "operator": {
          "type": "star",
          //...
        },
        "left": {
          "type": "int",
          "value": "3",
          //...
        },
        "right": {
          "type": "int",
          "value": "4",
          //...
        }
      }
    },
    "right": {
      "type": "binary_expression",
      "operator": {
        "type": "lt",
        //...
      },
      "left": {
        "type": "int",
        "value": "3",
        //...
      },
      "right": {
        "type": "int",
        "value": "1",
        //...
      }
    }
  },
  "right": {
    "type": "binary_expression",
    "operator": {
      "type": "dash",
      //...
    },
    "left": {
      "type": "int",
      "value": "3",
      //...
    },
    "right": {
      "type": "int",
      "value": "1",
      //...
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

O que é bem impressionante!

Próximos Passos

Até agora fizemos a parte da expansão gramatical, porém ainda precisamos adequar nosso type checker e nosso generator para que eles funcionem corretamente. Faremos isso no próximo post, até mais!

Top comments (0)