Além dos números: expandindo a linguagem
Tags: javascript, programming, language, series
Published: 2023-12-16 12:11:14

### 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
```tokenizer.js
//...
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 },
})
//..
```
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.
```lang0.ne
#...
program -> literal {% id %}

literal
  -> %int {% id %}
  | %float {% id %}
  | %string {% id %}
  | %char {% id %}
  | %bool {% id %}
```
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:
```ast.json
{
  "type": "int",
  "value": "42",
  "text": "42",
  "offset": 0,
  "lineBreaks": 0,
  "line": 1,
  "col": 1
}
```
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_:
```ex.ln0
3.141592
```
E o resultado depois de rodar o comando `node parser ex.ln0`:
```ast.json
{
  "type": "float",
  "value": "3.141592",
  "text": "3.141592",
  "offset": 0,
  "lineBreaks": 0,
  "line": 1,
  "col": 1
}
```
ou a string `"3.141592"`:
```ast.json
{
  "type": "string",
  "value": "\"3.141592\"",
  "text": "\"3.141592\"",
  "offset": 0,
  "lineBreaks": 0,
  "line": 1,
  "col": 1
}
```
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:
```typecheck.js
//...
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'
}
```
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:

```typecheck.js
function check_literal(node) {
  return (
    check_int(node) ||
    check_float(node) ||
    check_char(node) ||
    check_string(node) ||
    check_bool(node)
  )
}
```
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:
```typecheck.js
//...
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);
}
//...
```
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:
```generator.js
//...
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
}
```

### 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](https://dev.to/lucas_ac_am/operadores-numericos-sao-mais-complexos-do-que-parece-2f16) :)