3. A linguagem Jack¶
Este capítulo apresenta a linguagem que vamos compilar ao longo de todo o livro: Jack, desenvolvida no projeto Nand2Tetris com um único objetivo — ser fácil de compilar sem deixar de ser uma linguagem orientada a objetos "de verdade", com classes, objetos, herança de sintaxe do Java e uma biblioteca padrão razoável. Nenhum código de compilador é escrito neste capítulo; ele é puramente sobre a linguagem-alvo que os capítulos 4, 5, 9 e 10 vão aprender a analisar e traduzir.
Por que Jack é fácil de compilar¶
Jack tem uma sintaxe inspirada em Java, mas com uma lista deliberada de simplificações que removem justamente as partes de uma linguagem real que mais complicam um compilador:
| Característica | Jack | Motivo da escolha |
|---|---|---|
| Paradigma | Orientado a objetos, baseado em classes | Suficiente para ensinar tabela de símbolos com escopo e geração de código de objetos |
| Herança / polimorfismo | Não existem | Remove dispatch de métodos e resolução de tipos por hierarquia |
| Tipagem | Estática, mas fracamente verificada | O compilador não precisa (neste curso) implementar checagem de tipos completa |
| Precedência de operadores | Não existe — associatividade sempre à esquerda | Elimina toda a complexidade de tabelas de precedência na gramática de expressões |
| Tipos primitivos | Apenas int, boolean, char (sem float) |
Simplifica geração de código e representação de valores (tudo cabe em uma palavra de 16 bits) |
| Campos de instância | Sempre privados | Não há modificadores de visibilidade para tratar |
| Métodos | Sempre públicos | Idem |
| Sobrecarga | Não existe (um construtor por classe, nomes de subroutine únicos) | Simplifica resolução de chamadas |
Cada uma dessas escolhas de projeto tem uma tradução direta em "uma linha a menos para o compilador se preocupar" — é uma lição de projeto de linguagem tanto quanto de compiladores: a complexidade de compilar uma linguagem é, em grande parte, uma escolha de projeto da própria linguagem.
Um primeiro programa¶
class Main {
function void main() {
var String nome;
let nome = Keyboard.readLine("Qual o seu nome? ");
do Output.printString("Ola ");
do Output.printString(nome);
do Output.println();
return;
}
}
Um programa Jack é uma coleção de classes, cada uma em seu próprio arquivo NomeDaClasse.jack. Para ser executável, uma das classes deve se chamar Main e conter uma função main. A estrutura geral de uma classe é:
class NomeDaClasse {
// variáveis de classe (static) e de instância (field)
// construtor
// funções (métodos de classe)
// métodos (métodos de instância)
}
Tipos e variáveis¶
Jack é estaticamente tipada: toda variável é declarada com um tipo que não muda durante a execução. Existem apenas três tipos primitivos:
| Tipo | Valores |
|---|---|
int |
inteiro de 16 bits, complemento de dois (-32768 a 32767) |
boolean |
true ou false (internamente, true = -1, false = 0) |
char |
um caractere Unicode |
Toda classe declarada pelo programador também é, automaticamente, um novo tipo — variáveis desse tipo armazenam referências (endereços no heap), não cópias. Isso significa que atribuir uma variável de classe a outra faz as duas apontarem para o mesmo objeto:
var Point p1, p2;
let p1 = Point.new(5, 10);
let p2 = p1; // p2 aponta para o MESMO objeto que p1
do p1.moveTo(20, 30);
do Output.printInt(p2.getX()); // imprime o X atualizado, não o original
Dois tipos compostos vêm prontos na biblioteca padrão: String (cadeia de caracteres, entre aspas duplas: "Ola mundo") e Array (estrutura contígua e homogênea, alocada com Array.new(tamanho) e acessada com arr[i]).
Escopo de classe: field e static¶
class Point {
field int x, y; // variável de instância: uma cópia por objeto, sempre privada
static int pointCount; // variável de classe: única, compartilhada por todas as instâncias
}
field (variável de instância) é sempre privado — o acesso de fora da classe exige métodos getter/setter explícitos. static (variável de classe) é acessada via NomeDaClasse.variavel e é compartilhada por todas as instâncias.
Escopo de subroutine: parâmetros e var¶
Parâmetros são declarados como em Java/C: method void setX(int x). Variáveis locais usam a palavra reservada var, com duas restrições importantes que não existem em linguagens mais modernas:
- Toda declaração
vardeve vir no início da subroutine, antes de qualquer comando. - Não é possível atribuir um valor no momento da declaração —
var int sum = 0;é um erro sintático. A inicialização é sempre um comandoletseparado, depois de todas as declaraçõesvar.
function void main() {
var int sum; // ok
var int i; // ok — ainda estamos na zona de declarações
let sum = 0; // primeira atribuição, depois de todas as declarações
// var int j; // ERRO: declaração fora do início da subroutine
}
Toda variável não inicializada explicitamente recebe um valor padrão: 0 para int, false para boolean, null para tipos de classe.
Expressões: sem precedência de operadores¶
Esta é talvez a decisão de projeto mais marcante de Jack: não existe precedência entre operadores. A avaliação é sempre associada da esquerda para a direita, e cabe ao programador usar parênteses para expressar a ordem desejada:
let x = 2 + 3 * 4; // indefinido pela gramática — evite
let x = 2 + (3 * 4); // = 14
let x = (2 + 3) * 4; // = 20
Essa escolha não é apenas didática para quem programa em Jack — ela é o que torna a gramática de expressões do capítulo 5 tão mais simples do que a de uma linguagem com precedência (não precisamos de uma cadeia expr → term → factor → ... codificando níveis de precedência: um único não-terminal expression já resolve tudo, como veremos).
Os operadores disponíveis, todos de um único caractere (mais uma simplificação a favor de um léxico trivial):
- Aritméticos binários:
+-*/ - Aritmético unário:
- - Lógicos binários:
&(e),|(ou) - Lógico unário:
~(não) - Relacionais:
<>=(observe: não existem<=nem>=, justamente porque exigiriam dois caracteres)
Literais válidos em uma expressão: inteiros sem sinal (1256), strings ("Ola"), true, false, e null (referência não definida). Uma expressão também pode conter chamadas de subroutine: 4 + (5 * Math.sqrt(16)).
Comandos¶
Jack tem exatamente cinco tipos de comando — um conjunto propositalmente mínimo:
let— atribuição:let a = 10;ou, para elementos de array,let a[i] = 10;do— chama uma subroutine que não retorna valor útil (umavoid), descartando o resultado:do Output.printString("...");if/else— a única estrutura de seleção. Diferente de C/Java, as chaves são obrigatórias, mesmo para um único comando:
while— a única estrutura de iteração, também com chaves obrigatórias:
function int fatorial(int n) {
var int p, i;
let p = 1;
let i = 1;
let n = n + 1;
while (i < n) {
let p = p * i;
let i = i + 1;
}
return p;
}
return— devolve um valor (ou nada, para subroutinesvoid).
Não existem for, switch, break ou continue — tudo se expressa com while e if. De novo: menos construções sintáticas para o parser tratar.
Construtores, métodos e funções¶
Uma classe pode declarar três tipos de subroutine, cada uma com um papel distinto:
| Palavra-chave | Papel | Recebe this implícito? |
|---|---|---|
constructor |
Cria e inicializa uma nova instância; por convenção, chama-se new e retorna this |
Não (ele o cria) |
method |
Opera sobre uma instância existente | Sim |
function |
Opera apenas sobre a classe (equivalente a um método estático) | Não |
class Point {
field int x, y;
static int pointCount;
constructor Point new(int ax, int ay) {
let x = ax;
let y = ay;
let pointCount = pointCount + 1;
return this;
}
method int getX() {
return x;
}
}
Cada classe tem no máximo um construtor (sem sobrecarga). A chamada usa a convenção NomeDaClasse.new(...):
Métodos são chamados sobre uma instância (objeto.metodo(...)); funções, sobre a classe (Classe.funcao(...)). Essa distinção sintática — que não existe em Java, onde tudo é objeto.metodo ou Classe.metodoEstatico mas o compilador resolve por assinatura — é explícita em Jack através da palavra-chave de declaração, o que facilita bastante a análise semântica no capítulo 6.
Biblioteca padrão (Jack OS)¶
Jack não tem tipos ou operações embutidas além dos três primitivos — tudo o mais (strings, arrays, matemática, entrada/saída, gráficos, gerência de memória) é fornecido por oito classes de uma biblioteca padrão, o "sistema operacional Jack":
| Classe | Papel |
|---|---|
Math |
Operações aritméticas que a linguagem não tem nativamente — multiply, divide, min, max, sqrt. O compilador traduz * e / em chamadas a Math.multiply/Math.divide. |
String |
Construção e manipulação de cadeias de caracteres (length, charAt, appendChar, intValue, ...). |
Array |
Alocação de arrays (Array.new(tamanho)); o acesso arr[i] é sintaxe da linguagem, não um método. |
Output |
Impressão de texto na tela (23 linhas × 64 colunas): printString, printInt, println. |
Screen |
Gráficos em nível de pixel (512×256): drawPixel, drawLine, drawRectangle, drawCircle. |
Keyboard |
Leitura de teclado: readChar, readLine, readInt. |
Memory |
Acesso direto à RAM e gerência de heap: peek, poke, alloc, deAlloc. |
Sys |
Serviços do sistema: halt, error, wait. |
Perceba algo importante para os capítulos de geração de código (9 e 10): Memory.alloc é quem de fato aloca espaço para um novo objeto quando você chama Classe.new(...) — o constructor não faz mágica, ele delega para essa função do sistema operacional Jack.
A gramática de Jack (visão geral)¶
O front-end de Jack (léxico + sintático, capítulos 4 e 5) trabalha sobre uma gramática livre de contexto pequena e sem ambiguidade — uma consequência direta das simplificações de projeto vistas acima (sem precedência de operadores, sem herança, sem sobrecarga). A notação usada segue a convenção do próprio Nand2Tetris: 'x' para terminais literais, x (sem aspas) para não-terminais, () para agrupar, x|y para alternativas, x? para "zero ou um", x* para "zero ou mais".
Elementos léxicos (5 categorias de token — o assunto do capítulo 4):
| Terminal | Regra |
|---|---|
keyword |
class, constructor, function, method, field, static, var, int, char, boolean, void, true, false, null, this, let, do, if, else, while, return |
symbol |
{ } ( ) [ ] . , ; + - * / & | < > = ~ |
integerConstant |
Um número decimal |
stringConstant |
Sequência de caracteres Unicode, sem aspas duplas nem quebra de linha |
identifier |
Sequência de letras, dígitos e _, não iniciando com dígito |
Elementos sintáticos (o assunto do capítulo 5):
| Não-terminal | Regra |
|---|---|
classdef |
'class' className '{' classVarDec subroutineDec '}' |
classVarDec |
('static'|'field') type varName (',' varName)* ';' |
type |
'int' | 'char' | 'boolean' | className |
subroutineDec |
('constructor'|'function'|'method') ('void'|type) subroutineName '(' parameterList ')' subroutineBody |
parameterList |
((type varName) (',' type varName)*)? |
subroutineBody |
'{' varDec* statements '}' |
varDec |
'var' type varName (',' varName)* ';' |
statements |
statement* |
statement |
letStatement | ifStatement | whileStatement | doStatement | returnStatement |
letStatement |
'let' varName ('[' expression ']')? '=' expression ';' |
ifStatement |
'if' '(' expression ')' '{' statements '}' ('else' '{' statements '}')? |
whileStatement |
'while' '(' expression ')' '{' statements '}' |
doStatement |
'do' subroutineCall ';' |
returnStatement |
'return' expression? ';' |
expression |
term (op term)* |
term |
integerConstant | stringConstant | keywordConstant | varName | varName '[' expression ']' | subroutineCall | '(' expression ')' | unaryOp term |
subroutineCall |
subroutineName '(' expressionList ')' | (className|varName) '.' subroutineName '(' expressionList ')' |
expressionList |
(expression (',' expression)*)? |
op |
+ | - | * | / | & | | | < | > | = |
unaryOp |
- | ~ |
keywordConstant |
true | false | null | this |
A gramática completa, com todos os detalhes, está no Apêndice A — vamos voltar a ela linha por linha nos capítulos 4 e 5, à medida que cada regra vira um método do tokenizador ou do parser.
Ambiente de desenvolvimento¶
Para experimentar Jack sem instalar nada, use a IDE online do próprio projeto: nand2tetris.github.io/web-ide/compiler. Para instalação local do conjunto de ferramentas oficial (compilador de referência, VM Emulator, CPU Emulator), veja o Apêndice D.
O que vem a seguir¶
Com a linguagem-alvo em mãos, os próximos dois capítulos constroem o front-end do nosso compilador: primeiro o JackTokenizer (capítulo 4), que transforma o texto-fonte na sequência de tokens da tabela léxica acima; depois o CompilationEngine (capítulo 5), que reconhece a estrutura sintática descrita pela gramática. Ambos, no espírito deste livro, construídos com você, método a método.