DEV Community

Cover image for Além dos números: expandindo a linguagem
Lucas Almeida
Lucas Almeida

Posted on • Edited on

Além dos números: expandindo a linguagem

O que faremos

Nós já completamos a primeira parte do desafio, criamos uma gramática para uma linguagem que só reconhece números inteiros e usamos o compilador do pacote nearley para transformar o arquivo em uma AST, depois fizemos uma simples verificação de tipagem e por último criamos um arquivo javascript válido.
Pode não parecer muita coisa porque nossa linguagem só "enxerga" números, mas agora temos uma ideia mais concreta de como continuar com o processo.

O próximo passo é fazer nossa linguagem "enxergar" não só números inteiros, mas também os principais valores primitivos como números float, strings, etc.

Mas antes de continuar com o código queria fazer uma pausa para tomar decisões de design da linguagem. Como já disse antes essa linguagem será tipada, então seria como se fosse Typescript ou Rust e embora no final tudo será compilado para javascript eu acho que será um desafio interessante se fizermos algumas divergências de tipos em relação ao javascript.

No javascript temos alguns tipos literais bem básicos, eles são:

  • boolean
  • number
  • string
  • undefined
  • null
  • symbol
  • bigint

Comparado com C por exemplo quem tem 13 variações dos principais tipos de char até long double. JavaScript tem uma variação de tipos muito pequena, meu objetivo com essa linguagem é mirar em algo como Rust ou Go, ou seja, será uma linguagem bem simples, porém mais sofisticada do que JavaScript.

Para começar podemos definir os seguintes tipos básicos:

  • integer (int)
  • float
  • char
  • string
  • boolean (bool)

Não vou incluir nenhum tipo como null ou undefined ou algo assim, pois quero usar conceitos mais funcionais. Também não pretendo usar variações de números como octal ou binary por enquanto, vamos focar no que é mais importante primeiro.

Ajustando tokenizer e testando

Precisamos criar as RegExps dos novos tokens e alterar nosso objeto do tokenizer

//...
const tokenizer = moo.compile({
  WS: /[ \t]+/,
  string: /"(?:\\["\\]|[^\n"\\])*"/,
  char: /'(?:[^'\\]|\\.)*'/,
  float: /[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/,
  int: /0|[-+]?[1-9][0-9]*/,
  bool: /true|false/,
  NL: { match: '\n', lineBreaks: true },
})
//..
Enter fullscreen mode Exit fullscreen mode

Lembrando que a ordem aqui importa, por exemplo, o token string deve ser o primeiro dos valores literais, pois independente do que estiver escrito dentro de uma string (números, keywords, etc) ela sempre vai ser uma string.

Agora vamos alterar nossa gramática, criaremos uma nova regra chamada literal que representará todos os valores literais possível (os que discutimos anteriormente) e então nosso programa será definido como uma única instância de um de tais valores.

#...
program -> literal {% id %}

literal
  -> %int {% id %}
  | %float {% id %}
  | %string {% id %}
  | %char {% id %}
  | %bool {% id %}
Enter fullscreen mode Exit fullscreen mode

Com a mudança da gramática temos que compilar a gramática javascript mais uma vez rodando o comando nearleyc lang0.ne -o lang0.js
Com a nova gramática podemos compilar nosso arquivo exemplo: node parser ex.ln0
Esperamos que tudo continue ocorrendo como antes e que o resultado seja esse:

{
  "type": "int",
  "value": "42",
  "text": "42",
  "offset": 0,
  "lineBreaks": 0,
  "line": 1,
  "col": 1
}
Enter fullscreen mode Exit fullscreen mode

Porém agora podemos alterar nosso programa para qualquer um dos valores literais que definimos e tudo deve funcionar corretamente, por exemplo, temos aqui um número float:

3.141592
Enter fullscreen mode Exit fullscreen mode

E o resultado depois de rodar o comando node parser ex.ln0:

{
  "type": "float",
  "value": "3.141592",
  "text": "3.141592",
  "offset": 0,
  "lineBreaks": 0,
  "line": 1,
  "col": 1
}
Enter fullscreen mode Exit fullscreen mode

ou a string "3.141592":

{
  "type": "string",
  "value": "\"3.141592\"",
  "text": "\"3.141592\"",
  "offset": 0,
  "lineBreaks": 0,
  "line": 1,
  "col": 1
}
Enter fullscreen mode Exit fullscreen mode

Perceba que o programa associa corretamente o "type" do objeto.

Ajustando type checker

Agora que nosso partes gera vários tipos de "ASTs" precisamos criar as funções necessárias para verificar os nodes seguindo a mesma lógica de antes.

Já possuíamos a função check_int e vamos apenas copiar e colar, fazer algumas alterações e criar as outras funções:

//...
function check_float(node) {
  const { type } = node
  return type === 'float'
}

function check_char(node) {
  const { type } = node
  return type === 'char'
}

function check_string(node) {
  const { type } = node
  return type === 'string'
}

function check_bool(node) {
  const { type } = node
  return type === 'bool'
}
Enter fullscreen mode Exit fullscreen mode

Perceba que notoriamente teria como criar uma abstração ou tornar o código menos repetitivo, vou propositalmente ignorar isso por enquanto e futuramente quando tivermos algo mais estável podemos voltar para refatorar.

Temos agora funções específicas para cada tipo de dado, mas precisamos de uma função que vai checkar se um valor é literal ou não, isso significa que se conseguirmos o valor true de alguma das funções que criamos então valor é literal.
Podemos aproveitar o fato de que todas as funções retornam boolean para tornar esse processo muito fácil, nossa funções pode ser escrita assim:

function check_literal(node) {
  return (
    check_int(node) ||
    check_float(node) ||
    check_char(node) ||
    check_string(node) ||
    check_bool(node)
  )
}
Enter fullscreen mode Exit fullscreen mode

E depois disso, basta trocar a função de checkagem para ser a check_literal ao invés de ser a check_int que estávamos usando até então:

//...
try {
  const content = fs.readFileSync(filename, 'utf8');
  const ast = JSON.parse(content);
  console.log(check_literal(ast));
} catch(e) {
  console.error(e?.message);
  process.exit(1);
}
//...
Enter fullscreen mode Exit fullscreen mode

Por enquanto nosso type checker apenas printa na tela true ou false e isso não ajuda muito a gente, mas futuramente podemos usar ele de maneira mais integrada, por enquanto vamos manter dessa forma para avançarmos mais rápido.

Finalizando com generate

O último passo é adaptar nosso generator, tecnicamente nossa função gen_int já vai funcionar para todos os tipos de node automaticamente, então a solução mais simples é simplesmente renomear nossa função de gen_int para gen_literal e pronto:

//...
try {
  const contents = fs.readFileSync(filepath, 'utf8');
  const ast = JSON.parse(contents);
  const output = gen_literal(ast);
  fs.writeFileSync('output.js', output);
} catch(e) {
  console.error(e?.message);
  process.exit(1);
}

function gen_literal(node) {
  const { value } = node
  return value
}
Enter fullscreen mode Exit fullscreen mode

Finalização

Nossa linguagem acaba de dar mais um passo e agora consegue "enxergar" instância únicas de int, float, bool, string e char.

Pensando em próximos passos, já podemos trabalhar com operadores aritméticos como +, -, * e /.

Te vejo no próximo post :)

Top comments (0)