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: ')', //...
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] %}
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], }) %}
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], }) %}
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], }) %}
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", //... } } }
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", //... } } }
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] } } %}
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 %}
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: '^', //...
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 %}
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 %}
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
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 %}
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", //... } } }
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)