8. A VM Hack (especificação)¶
Este capítulo é a especificação de referência da VM Hack — a representação intermediária baseada em pilha para a qual o compilador Jack (capítulos 9 e 10) vai gerar código, e que o VM Translator (capítulos 11 e 12) vai traduzir para Assembly Hack. Não há código de compilador aqui; é a "planta" que orienta os próximos quatro capítulos.
Modelo de memória¶
Cada célula de memória da máquina Hack armazena 16 bits, usados para representar de forma uniforme três coisas: um inteiro em complemento de dois (-32768 a 32767), um booleano (0 para falso, -1 — todos os bits em 1 — para verdadeiro), ou um endereço de memória.
A memória é organizada em oito segmentos, cada um endereçado por índice a partir de 0:
| Segmento | Propósito | Mapeado de |
|---|---|---|
local |
Variáveis locais da subroutine em execução | var (tabela de símbolos, capítulo 6) |
argument |
Argumentos recebidos pela subroutine | arg |
static |
Variáveis de classe (compartilhadas por todas as instâncias) | static |
this |
Campos do objeto atual | field (via this) |
that |
Um segundo "objeto atual" — usado para arrays e para o objeto de um método invocado sobre outra instância | (uso interno) |
constant |
Pseudo-segmento somente leitura para constantes literais | literais na expressão |
pointer |
Dois registradores (0 e 1) que apontam para this e that, respectivamente |
uso interno do compilador |
temp |
Oito células de uso livre, compartilhadas por toda a VM | uso interno do compilador |
Além dos segmentos, duas áreas adicionais completam o modelo: a pilha (stack), usada para armazenar valores temporários durante a avaliação de expressões e para implementar o protocolo de chamada de função (capítulo 12); e o heap, a região de RAM dedicada ao armazenamento de objetos e arrays, gerenciada dinamicamente pelas funções Memory.alloc/Memory.deAlloc do sistema operacional Jack.
Comandos de acesso à memória¶
Apenas dois comandos movem dados entre a pilha e os segmentos:
push segmento índice // empilha o valor de segmento[índice]
pop segmento índice // desempilha o topo e armazena em segmento[índice]
segmento é um dos oito acima (exceto que não existe pop constant, já que constantes não são graváveis); índice é um inteiro não negativo.
push local 2 // empilha local[2]
push constant 10 // empilha o literal 10
pop argument 0 // desempilha o topo para argument[0]
Comandos aritméticos e lógicos¶
Nove comandos operam sobre a pilha — sete binários (consomem dois valores do topo, empilham um resultado) e dois unários (consomem um, empilham um):
| Comando | Aridade | Semântica |
|---|---|---|
add |
binário | soma |
sub |
binário | subtração |
neg |
unário | negação aritmética |
eq |
binário | -1 se iguais, 0 caso contrário |
gt |
binário | -1 se o primeiro é maior |
lt |
binário | -1 se o primeiro é menor |
and |
binário | E bit a bit |
or |
binário | OU bit a bit |
not |
unário | complemento bit a bit |
Não existem comandos de multiplicação ou divisão na VM — como veremos no capítulo 9, o compilador Jack traduz os operadores * e / em chamadas às funções Math.multiply/Math.divide da biblioteca padrão, não em comandos VM nativos.
Controle de fluxo¶
Linguagens de alto nível têm if, while, for — a VM reduz tudo isso a três primitivas:
label rótulo // declara uma posição nomeada no código
goto rótulo // salto incondicional
if-goto rótulo // desempilha o topo; se for verdadeiro (≠ 0), salta
Rótulos têm escopo de função — dois label LOOP em funções diferentes não colidem (o capítulo 12 detalha a qualificação funcao$rótulo usada pelo VM Translator para garantir isso na tradução final para Assembly). É com apenas essas três primitivas que o capítulo 9 vai mapear if/else e while de Jack.
Funções¶
Três comandos governam todo o protocolo de chamada:
function nome nLocais // declara uma função, com N variáveis locais (inicializadas em 0)
call nome nArgs // chama uma função, passando os N argumentos já empilhados
return // desempilha um valor e devolve o controle ao chamador
Uma peça importante do design: mesmo subroutines que não retornam valor útil precisam empilhar algo antes do return — por convenção, push constant 0. É simetria pura: todo call espera encontrar um valor no topo da pilha ao retornar, então toda função — void ou não — precisa deixar exatamente um valor lá.
function Main.dobro 0
push constant 2
push argument 0
call Math.multiply 2
return
function Main.main 1
push constant 5
call Main.dobro 1
pop local 0
push constant 0
return
O nome de uma função é sempre Classe.nomeDaSubroutine — é assim que construtores, métodos e funções (na terminologia Jack) se tornam, todos igualmente, "funções" no nível da VM. O mecanismo completo de call/return (salvar e restaurar o estado do chamador, o stack frame) é o assunto do capítulo 12; por ora, basta saber a interface: argumentos entram empilhados, o resultado sai empilhado.
Um programa completo¶
Todo programa VM começa executando Main.main. Um exemplo simples — somar de 1 a n e imprimir o resultado — ilustra push/pop, aritmética, controle de fluxo e chamada de função juntos:
function Soma.soma 2 // variáveis locais: sum (0), i (1)
push constant 0
pop local 0 // sum = 0
push constant 1
pop local 1 // i = 1
label LOOP
push local 1
push argument 0
gt // i > n ?
if-goto FIM
push local 0
push local 1
add
pop local 0 // sum = sum + i
push local 1
push constant 1
add
pop local 1 // i = i + 1
goto LOOP
label FIM
push local 0
return
function Main.main 1
push constant 10
pop local 0 // n = 10
push local 0
call Soma.soma 1
call Output.printInt 1
push constant 0
return
O que vem a seguir¶
Com a especificação da VM Hack em mãos, os próximos dois capítulos (9 e 10) evoluem o CompilationEngine da Unidade 1 para efetivamente gerar este código, expressão por expressão, comando por comando — fechando o compilador Jack → VM (Project 11) por completo. Os capítulos 11 e 12, na Unidade 3, tratam do outro lado: como traduzir esse código VM para Assembly Hack.
Uma versão tabular desta especificação, para consulta rápida durante esses capítulos, está no Apêndice B.