DEV Community

Cover image for Expressões encadeadas e agrupamento
nullen box
nullen box

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)