Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mini Interpreter Pseudo JS

O Problema

Como um computador entende e executa código?

Quando você escreve var x = 1 + 2; em JavaScript, o computador não lê isso como texto. Ele precisa:

  1. Quebrar o texto em pedaços mínimos (tokens) — var, x, =, 1, +, 2, ;
  2. Organizar esses pedaços em uma árvore que mostre a estrutura do programa — "isto é uma declaração de variável, que tem um nome e um valor inicial, e o valor é uma soma de dois números"
  3. Percorrer essa árvore executando cada nó — "criar a variável x na memória com o valor 3"

Este projeto implementa esse pipeline completo em ~700 linhas de Ruby legível, para que você veja exatamente como cada etapa funciona — e possa parar a execução em qualquer ponto para inspecionar o que está acontecendo.


Pré-requisitos

  • Ruby 3.2.4 (ou qualquer Ruby >= 3.0)
  • Nenhuma gem externa — só usamos a biblioteca padrão (minitest para testes, debug para depuração)
ruby --version   # confirme que está instalado

Se não tiver:

# Ubuntu/Debian
sudo apt install ruby

# macOS (com Homebrew)
brew install ruby

O que cada arquivo faz

main.rb — Ponto de entrada / Demo

Executa o pipeline completo com o script nextFrame e exibe cada etapa. Para rodar:

ruby main.rb

lexer.rb — O "separador de palavras"

Transforma texto em tokens. Cada token é um par (tipo, valor):

"var x = 1;"  →  [Token[:VAR, "var"], Token[:ID, "x"], Token[:EQ, "="], Token[:NUM, 1.0], Token[:SEMI, ";"]]

Percorre o texto caractere por caractere, agrupando:

  • Dígitos → números (Token[:NUM, 42.0])
  • Letras → identificadores ou palavras-chave (:ID, :VAR, :FUNCTION, :RETURN)
  • Caracteres especiais → operadores e pontuação (:PLUS, :EQ, :LPAREN)

Saída: Array de Token (Struct com type e value).

ast.rb — Os "blocos de montar" da árvore

Define as classes que representam cada elemento da linguagem:

Classe Significado Exemplo
Program Raiz do programa, contém statements var x = 1; fn();
Block Bloco entre { } { var x = 1; }
VarDecl Declaração de variável var x = 1;
Assign Atribuição x = x + 1;
BinOp Operação binária 1 + 2
Lit Valor literal 42, "hello", [1,2]
Ident Referência a variável rotation
CallExpr Chamada de função foo(1, 2)
Member Acesso por índice colors[0]
FuncDecl Declaração de função function foo() {}
ReturnStmt Comando return return x;
ObjLit Objeto literal { a: 1 }
ArrLit Array literal [1, 2, 3]
ExprStmt Expressão usada como comando foo();

parser.rb — O "montador da árvore"

Pega os tokens e constrói a AST:

tokens = MiniJSLexer.new("var x = 1 + 2;").tokenize
ast    = MiniJSParser.new(tokens).parse
# ast = Program([VarDecl("x", BinOp(:PLUS, Lit(1), Lit(2)))])

Saída: Um nó Program — a raiz da AST.

interpreter.rb — O "executor"

Percorre a AST e executa cada nó:

engine = MiniJSEngine.new(code)
engine.call('nextFrame', [1])   # → { "rotation" => 5.0, "color" => "blue", "frame" => 1 }
engine.variable('rotation')     # → 5.0

Mantém um @global_scope (Hash Ruby) com todas as variáveis e funções. Cada chamada de função cria um escopo local temporário.

Saída: Valores Ruby (Float, String, Array, Hash, nil) que representam os valores que o pseudo-JS produziria.

test_*.rb — Testes unitários

for f in test_*.rb; do ruby "$f"; done

Roteiro de aprendizado: depurando passo a passo

Primeiro passo: depurar var x = 1 + 2;

Comece pelo exemplo mais simples possível. Ele contém apenas 3 conceitos: declaração de variável, operador de soma e literais numéricos. Perfeito para ver o pipeline inteiro sem distrações.

Abra o arquivo que você quer investigar e insira pontos de parada. Vou mostrar como fazer em cada etapa:

No Lexer: ver os tokens nascendo

Abra lexer.rb e adicione no início (após frozen_string_literal):

require 'debug'

Depois, dentro do método read_next_token, insira:

def read_next_token
  binding.break  # <-- a execução pausa aqui a cada token
  current_char = @source_code[@current_position]
  # ...
end

Agora execute no terminal:

ruby -e "
require_relative 'lexer'
tokens = MiniJSLexer.new('var x = 1 + 2;').tokenize
puts tokens.inspect
"

O debug pausa na primeira chamada de read_next_token. Digite s (step) para ver qual caminho é seguido. Digite n (next) para ir para a próxima linha. Digite p @current_position para ver a posição atual no código fonte. Digite c (continue) para ir ao próximo token.

Repita até o token :EOF — você terá testemunhado os 7 tokens sendo criados um a um.

No Parser: ver a árvore sendo montada

Abra parser.rb e insira binding.break no início do parse:

def parse
  binding.break
  program = Program.new
  # ...
end

Execute:

ruby -e "
require_relative 'lexer'
require_relative 'parser'
tokens = MiniJSLexer.new('var x = 1 + 2;').tokenize
ast = MiniJSParser.new(tokens).parse
"

Quando pausar, explore:

  • p @tokens — veja todos os tokens esperando
  • p @current_position — posição atual no array
  • n — avance para ver program.body << parse_statement
  • s — entre em parse_statement para ver como ele decide se é :VAR, :FUNCTION, etc.

No Interpreter: ver o código sendo executado

Abra interpreter.rb e insira binding.break no evaluate_node:

def evaluate_node(node, local_scope)
  binding.break
  dispatch_method = NODE_DISPATCH.fetch(node.class)
  send(dispatch_method, node, local_scope)
end

Execute:

ruby -e "
require_relative 'lexer'
require_relative 'parser'
require_relative 'interpreter'
tokens = MiniJSLexer.new('var x = 1 + 2;').tokenize
ast = MiniJSParser.new(tokens).parse
MiniJSInterpreter.new.run(ast)
puts 'x = ' + MiniJSInterpreter.new.run(ast).lookup_variable('x').to_s
"

A cada c (continue), você verá um nó diferente sendo avaliado:

  1. Program → entra em evaluate_program, itera o body
  2. VarDecl → entra em evaluate_variable_declaration, avalia o initializer
  3. BinOp → entra em evaluate_binary_operation, avalia left e right
  4. Lit(1.0) → retorna o valor 1.0
  5. Lit(2.0) → retorna o valor 2.0
  6. VarDecl de novo → assign_variable("x", 3.0) — a variável nasce!

Segundo passo: evoluir para nextFrame

Quando você já entende como var x = 1 + 2; percorre o pipeline, está pronto para o exemplo completo. Ele adiciona 4 novos conceitos: função, array, objeto, acesso a membro (colors[colorIndex]), atribuição com efeito colateral no escopo global (rotation = rotation + 5), e retorno de objeto.

ruby main.rb

Isso executa o pipeline inteiro e exibe as 4 etapas. Para mergulhar fundo, ative o binding.break no evaluate_node do interpreter.rb de novo e execute:

ruby -e "
require_relative 'lexer'
require_relative 'parser'
require_relative 'interpreter'

SCRIPT = <<~JS
  var rotation = 0;
  var colorIndex = 0;
  var colors = ['red', 'blue', 'green', 'yellow', 'purple'];

  function nextFrame(frame) {
    rotation = rotation + 5;
    colorIndex = (colorIndex + 1) % 5;
    return { rotation: rotation, color: colors[colorIndex], frame: frame };
  }
JS

engine = MiniJSEngine.new(SCRIPT)
puts engine.call('nextFrame', [1]).inspect
"

Siga cada nó da AST sendo avaliado. Quando chegar em CallExpr, veja como o interpretador cria um escopo local com {"frame" => 1} e executa o corpo da função. Repare como Assign modifica o @global_scope (efeito colateral), e como ReturnStmt usa throw :return para desviar o fluxo.

Terceiro passo: este é o embrião de um navegador web

Este mini-interpretador é o núcleo de processamento que um navegador web precisa para entender e executar JavaScript.

Navegador completo:
  HTML  →  DOM (árvore de elementos)
  CSS   →  estilos aplicados aos elementos
  JS    →  ← VOCÊ ESTÁ AQUI (este projeto)
             interpretador que executa o código

O que falta para chegar a um navegador real:

  • HTML parser — transformar <div><p>texto</p></div> em uma árvore DOM
  • CSS parser — entender div { color: red; } e aplicar estilos
  • DOM API — funções como document.getElementById(), element.style
  • Event loop — responder a cliques, teclado, temporizadores

Mas o coração da linguagem — declarar variáveis, chamar funções, criar arrays e objetos, acessar propriedades — é exatamente o que este projeto implementa. Cada peça que você debugou aqui é a mesma lógica que existe dentro do V8 (Chrome), SpiderMonkey (Firefox) e JavaScriptCore (Safari).


Comandos úteis do debug nativo (Ruby 3.1+)

Comando Significado
n next — avança para próxima linha
s step — entra dentro do método
c continue — segue até o breakpoint
p var imprime o valor de var
quit sai do debug

Ordem de leitura dos arquivos

  1. ast.rb — entenda os blocos de construção (5 min)
  2. lexer.rb — veja como texto vira tokens (10 min)
  3. parser.rb — veja como tokens viram árvore (15 min)
  4. interpreter.rb — veja como a árvore executa (15 min)
  5. main.rb — veja o pipeline completo em ação (5 min)
  6. test_*.rb — veja como cada peça é testada (10 min)

O que você vai aprender

Conceitos de Ciência da Computação

  • Pipeline de compilação/interpretação — código fonte processado em estágios
  • Análise léxica — texto separado em tokens
  • Análise sintática — tokens formando uma árvore (AST)
  • Interpretação tree-walk — percorrer a AST para executar o programa
  • Escopo léxico — global vs local (função)
  • Efeito colateral — função que modifica variável global

Conceitos de Ruby

  • Mixinsinclude InterpreterScope para organizar código
  • Dispatch table — Hash mapeando classe → método (NODE_DISPATCH)
  • StructToken = Struct.new(:type, :value)
  • Throw/catch — fluxo não-local para implementar return
  • Minitest — testes sem gems externas
  • Regex — reconhecimento de padrões no lexer

Sobre linguagens de programação

  • Operadores, variáveis, funções e objetos não são mágica — são árvores sendo percorridas
  • Um interpretador não precisa ser complexo: ~700 linhas são suficientes para uma linguagem com escopo, arrays, objetos e funções
  • O núcleo do JavaScript que roda no Chrome é o mesmo conceito — só que com milhões de linhas e décadas de otimização

Licença

Projeto educacional — sinta-se à vontade para usar, modificar e estudar.

About

Creating a little pseudo javascript interpreter by ruby

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages