Show HN: Computador programável feito com portas NAND
(github.com/ArhanChaudhary)- NAND é um computador de 16 bits Turing-completo emulado na web, incluindo desde a CPU até IDE e UI com a proposta educacional de ser composto apenas por clock e portas NAND
- Sua stack própria é baseada na plataforma Jack-VM-Hack, oferecendo CPU, linguagem de máquina, assembly, assembler, linguagem de VM, tradutor de VM, linguagem Jack e compilador
- Os programas de exemplo incluem Average, Pong e 2048, além de demos de VM escape usando stack overflow e stack smashing, e até o GeneticAlgorithm com machine learning simples
- Jack é uma linguagem orientada a objetos de tipagem fraca, parecida com Java na sintaxe, mas internamente depende de um único signed 16-bit integer e tem grandes limitações, como ausência de precedência de operadores, gerenciamento manual de memória e comportamento indefinido
- O projeto não significa que todos os cálculos sejam realmente executados em portas NAND físicas; trata-se de uma implementação educacional e teórica com compiler em TypeScript, tradutor de VM, simulador de lógica de portas NAND em Rust e WebAssembly
A stack completa de computador oferecida pelo NAND
- NAND é apresentado como sigla de Not A Nand-powered Device, um computador web de 16 bits
- Ele emula um computador Turing-completo feito de clock e portas NAND, incluindo internamente os seguintes componentes
- CPU
- linguagem de máquina
- linguagem assembly
- assembler
- linguagem de máquina virtual
- tradutor de máquina virtual
- linguagem de programação
- compilador
- IDE
- interface de usuário
- A base é a plataforma Jack-VM-Hack do curso Nand to Tetris e de livros relacionados
- Há um vídeo de demonstração do NAND
Programas de exemplo e demos
-
Average
- Um programa simples que recebe números e calcula a média
- Mostra control flow, operações aritméticas, I/O e alocação dinâmica de memória
- É um programa fornecido pela suíte de software do Nand to Tetris
-
Pong
- Um jogo de Pong que demonstra o modelo orientado a objetos
- As setas movem a paddle para a esquerda e para a direita para rebater a bola
- A paddle encolhe a cada rebatida, e o jogo termina quando a bola toca a parte de baixo da tela
- É um programa fornecido pela suíte de software do Nand to Tetris
-
2048
- Um jogo 2048 que demonstra recursion e lógica de aplicação complexa
- Em uma grid 4x4, as setas movem os números, e números iguais se combinam
- Ao alcançar o tile 2048, o jogador vence, mas pode continuar jogando até perder
-
Overflow
- Executa intencionalmente um virtual machine escape ao provocar stack overflow por recursão infinita
- Explora o fato de o runtime não ter verificação de prevenção de stack overflow
- Quando o valor do stack pointer ultrapassa 2048, a stack sai da área de memória pretendida e invade o espaço de memória heap
- Se for executado com a RAM vazia, o programa pode ser resetado no meio por causa de uma instruction que define o program counter como 0
- Se for executado logo após o GeneticAlgorithm, é possível ler dados antigos da RAM memory que não foram sobrescritos
-
SecretPassword
- Explora o fato de o runtime não impedir stack smashing para chamar uma função originalmente inacessível
- O usuário pode sobrescrever uma memory address da RAM com o valor desejado
- Ao sobrescrever o return address de um stack frame com o endereço de outra função, torna-se possível executar código arbitrário dentro do programa
- Os valores de exemplo são memory location
267e overwrite value1743 - O mesmo tipo de vulnerabilidade também existe em buffer overflow em C
-
GeneticAlgorithm
- É uma simulação de criaturas usando machine learning simples
- Cada dot tem seu próprio “brain”, composto por vetores de aceleração, e evolui para alcançar o goal por meio de seleção natural
- Dots que morrem mais perto do goal têm maior chance de ser escolhidos como parents da próxima geração
- Durante a reprodução, partes do brain sofrem mutation para simular evolução natural
- Há um vídeo de demonstração do Genetic Algorithm
Limitações de hardware enfrentadas pelo GeneticAlgorithm
- O GeneticAlgorithm foi o programa individual que mais tempo levou para ser desenvolvido entre os vários componentes do NAND
- Por questões de desempenho, o único fator usado pelos dots na evolução é o quão perto do goal eles morrem, o que reduz a entropy do algoritmo de seleção natural
- Por uso de memória, há limitações insatisfatórias no número de dots e no tamanho do brain
- Por complexidade técnica, mesmo que os obstáculos sejam reposicionados durante a simulação, não há garantia de que o brain dos dots seja grande o suficiente para alcançar o goal
- O brain size é definido apenas no momento em que o programa começa
- A implementação contorna as restrições do NAND com várias otimizações
- O espaço de memória de instruções da ROM é limitado, então se houver código demais ele não compila
- O GeneticAlgorithm final usa 99,2% do espaço de memória de instruções
- O espaço de RAM memory é limitado, então é preciso otimizar o uso de heap memory
- A razão de a tela ficar preenchida de forma static entre gerações é que o espaço de memória da tela é usado como memória temporária de swap para a próxima geração
- O NAND não tem tipo floating point, e o intervalo de integer representável vai de -32768 a 32767
- A precisão do cálculo de fitness diminui, e integer overflow também precisa ser considerado
- Otimizações relacionadas e insights adicionais estão documentados na codebase do GeneticAlgorithm
Escrevendo programas NAND com Jack
- É enfatizado que a principal causa de programas não funcionarem em Jack é a ausência de precedência de operadores
4 * 2 + 3deve ser escrito como(4 * 2) + 3if (~x & y)deve ser escrito comoif ((~x) & y)- O valor de avaliação de uma expression ambígua sem parênteses é undefined
- Jack é a linguagem de programação orientada a objetos fracamente tipada do NAND
- Pela descrição, ela é mais próxima de “C com sintaxe de Java”
- Como o NAND tem sua própria complete tech stack, só é possível programar com Jack
- O Jack OS básico é bundled com o programa no momento da compilação
- Ele fornece strings, memory e interface com o hardware
- Inclui funções como
Keyboard.readLine,Keyboard.readInt,Output.printStringeOutput.println
- Jack oferece suporte a três tipos primitivos:
int,chareboolean- É possível definir abstract data types por meio de classes
- Variáveis
fielddeclaram atributos de cada instância - Variáveis
fieldtêm escopo privado, e o acesso externo exige methods
functionemethodtêm formas de chamada diferentes- O method do objeto atual pode ser chamado dentro da mesma classe como
do g(); - Uma function call precisa ter o nome da classe como prefixo
- O method de um objeto é chamado por meio do objeto, como em
do b.q();
- O method do objeto atual pode ser chamado dentro da mesma classe como
Tipagem fraca e gerenciamento de memória em Jack
- Diferentemente de Java, Jack não oferece suporte a strong typing, down casting, polymorphism nem inheritance
- Internamente, existe de fato apenas um tipo: signed 16-bit integer
- O compiler não se importa se tipos forem misturados em assignments e operations
- Se você colocar
65em umchar, ele pode ser tratado como equivalente a'A' - Se você colocar
5000em uma variávelArraye executara[100] = 77, isso viraRAM[5100] = 77 - Entradas de array podem conter data types diferentes entre si
- Se o memory layout bater,
Arraypode ser usado como se fosse uma instância de outra classe
- Jack é uma linguagem de gerenciamento manual de memória
- Se memory que não é mais necessária não for desalocada, ocorre memory leak
- Heap overflow aparece como
ERR6 - O Jack OS armazena arrays e strings no heap, não na stack
- Como boa prática, classes que representam objetos devem ter um method
dispose- Primeiro, chama-se o
disposedas variáveisfield - No final, a própria instância do objeto é desalocada com
do Memory.deAlloc(this);
- Primeiro, chama-se o
- Um loop que cria repetidamente string literals para exibir pode causar heap overflow
- É possível desalocar a string a cada iteração
- Ou alocar a string apenas uma vez e reutilizá-la para continuar imprimindo
Comportamento indefinido e cuidados
-
Operadores de comparação
a > bea < bnem sempre são matematicamente corretos- A implementação da VM converte
a > bema - b > 0 - Como
a - bpode sofrer overflow,20000 > -20000virafalse - Se a distância absoluta entre
aebfor maior que 32767,>e<podem falhar - Esse comportamento não é corrigido por compatibilidade com Nand to Tetris
-
-32768
-32768é o único valor para o qual-(-32768) = -32768- Como ele não tem equivalente positivo, pode causar unsoundness e logic errors
Output.printIntespera internamente queMath.absretorne um número positivo, mas isso não acontece com-32768, então o Jack OS funciona de forma incorreta
-
Chamada de função com argumentos insuficientes
- Chamar uma function que tem parameters sem argumentos também pode gerar undefined behavior
- Por outro lado, uma function call com argumentos demais é válida, e os argumentos extras podem ser indexados com a keyword
arguments - Não há indicador de quantidade de argumentos
-
Type casting inadequado
- É possível fazer cast de variáveis para outro tipo usando
Array - Chamar um instance method inexistente em uma variável convertida é undefined behavior
- O compiler não é smart o bastante para perceber isso
- É possível fazer cast de variáveis para outro tipo usando
-
Modificação de stack frame e registradores internos
- Modificar o stack frame nos endereços de memória
256~2047ou os internal registers1~15pode causar undefined behavior - Em geral, isso é difícil de fazer sem uso incorreto de
Memory.pokeou negative array indexing
- Modificar o stack frame nos endereços de memória
-
Carregamento de arquivos VM do usuário
- O NAND fornece validação de programa para arquivos
.jack, mas não para arquivos.vm - Em arquivos
.vm, é possível chamar funções inexistentes, referenciar variáveis não atribuídas e realizar operações de memória logicamente inválidas - Na maioria dos casos, ocorre virtual machine escape e nada pode ser exibido na tela
- O NAND fornece validação de programa para arquivos
Especificações de hardware e layout de memória
- A RAM do NAND é composta por 32.768 words, e cada word armazena um número binário de 16 bits
- O hardware reserva 8.192 endereços de memória para a screen
- Cada bit de cada endereço é mapeado linearmente para o pixel correspondente da screen 512x256
- A numeração de bits segue o esquema LSb 0
- O keyboard é mapeado para o endereço de memória
24576- A tecla atualmente pressionada é refletida nessa posição
- Em vez de tratar a entrada do usuário diretamente nesse endereço, recomenda-se usar a classe
Keyboarddo Jack OS
- O keyboard reconhece caracteres ASCII e teclas especiais
- nova linha =
128 - backspace =
129 - seta para esquerda/cima/direita/baixo =
130~133 - home/end/page up/page down/insert/delete/ESC =
134~140 - F1~F12 =
141~152
- nova linha =
- O hardware reserva 240 endereços de memória para static class variables e 1.792 endereços de memória para a global stack
- É dito que esse limite geralmente não causa problemas, a menos que se faça recursão profunda
Indo além do Jack OS e implementando um OS próprio
- Basicamente, o Jack OS é incluído junto com o programa na compilação e fornece string, memória e interface de hardware
- É possível fornecer uma implementação própria de OS com uma interface de hardware dedicada
- A IDE trata o arquivo do Jack OS da mesma forma que um arquivo de programa comum
- O arquivo do OS também pode ser apagado ou sobrescrito
- Mesmo usando um OS próprio, existem funções centrais que obrigatoriamente precisam ser implementadas para a compilação
Sys.init: é o ponto de entrada real codificado diretamente na implementação da VM, e nãoMain.mainMemory.alloc: é o alocador de memória heap usado internamente quando um construtor de classe cria um objetoString.newWithStr: é o construtor interno para literais de stringMath.multiply: é chamado internamente no lugar da expressão Jackx * yMath.divide: é chamado internamente no lugar da expressão Jackx / y
- O
Sys.initdo Jack OS fornecido inicializa memória, matemática, tela e saída, depois chamaMain.main()e entãoSys.halt()
Funcionamento interno do NAND
- O computador NAND segue a arquitetura Harvard
- A ROM, que é a memória de instruções, e a RAM, que é a memória de dados, ficam separadas
- A CPU faz as duas funcionarem em conjunto
- A CPU é uma accumulator machine
- Ela depende fortemente de registradores embutidos no fluxo de controle
- Aqui, o acumulador é o registrador
D
- O conjunto de instruções da CPU tem apenas dois opcodes
- O conjunto de instruções é relativamente simples, mas oferece bastante funcionalidade
- A ALU é definida pelas expressões que podem ser calculadas em uma instrução
- O compilador e a máquina virtual não são conceitos exclusivos do NAND, então são tratados brevemente
- Alguns recursos sintáticos estranhos são resultado de facilitar a implementação do compilador
- O compilador é um parser recursive descent sobre uma gramática LL(1)
- O compilador gera código VM, e a VM é usada como uma stack machine simples
- Cada instrução da VM é mapeada para assembly e código de máquina
- O código de implementação pode ser visto em core e compiler implementation
Pontos centrais da linguagem Jack e da referência do OS
- Um programa Jack é composto por uma coleção de classes
- Cada classe é definida em um arquivo separado
- É necessário haver uma ou mais classes, e uma delas deve ser
Main - No Jack OS, o ponto de entrada é a função
mainda classeMain
- Uma classe pode incluir declarações de
field,static,constructor,methodefunction- A ordem das declarações de
fieldestaticé arbitrária - A ordem das declarações de sub-rotinas também é arbitrária
- O tipo pode ser
void,int,boolean,charou nome de classe
- A ordem das declarações de
- Características da sintaxe
- Espaços em branco e comentários são ignorados
&e|são operadores bit a bit e não fazem short-circuittrue,falseenullsão respectivamente-1,0e0- Constantes de string não podem incluir diretamente quebra de linha nem aspas, e escapes também não são permitidos
- Aspas e quebra de linha são fornecidas pelo OS por meio de
String.doubleQuote()eString.newLine() - Identificadores diferenciam maiúsculas de minúsculas
- Principais classes do Jack OS
Array: criação e descarte de arraysKeyboard: pressionamento de teclas, caractere, linha e entrada de inteiroMath: abs, multiply, divide, sqrt, max, minMemory:peek,poke,alloc,deAllocOutput: saída na tela de texto e movimentação do cursorScreen: desenho de pixel, linha, retângulo e círculoString: criação de string, descarte, acesso a caractere, append e conversão de inteiroSys: halt, error, wait
- Um estado inválido exibe um código de erro no formato
"ERR[N]"e encerra a execução do programaERR3: divisão por zeroERR6: overflow de heapERR15,ERR16: índice de string fora dos limitesERR17: a string está cheiaERR20: posição ilegal do cursor
Não é um projeto feito apenas com portas NAND reais
- O FAQ reconhece que a descrição e o título “everything made from NAND gates” são enganosos, mas feitos de boa-fé
- O compilador e o tradutor da máquina virtual foram escritos em TypeScript
- O kernel emulado e o hardware emulado não representam exatamente a forma como um computador real funciona
- O simulador de lógica de portas NAND real foi escrito em Rust e ocupa apenas uma pequena parte de toda a base de código
- O código em Rust é compilado para WebAssembly para rodar no navegador
- Por isso, afirma-se que a premissa de que toda computação roda em portas NAND é, na prática, removida
- O NAND cumpre o papel de projeto educacional e teórico
- Em teoria, a mesma lógica de CPU poderia funcionar também em uma manifestação real do hardware emulado
- Como exemplo de projeto de hardware FPGA baseado em nand2tetris, é citado https://gitlab.com/x653/nand2tetris-fpga/
Escopo da implementação e limitações da IDE
- O NAND segue a especificação do curso Nand to Tetris e do livro relacionado
- O implementador criou diretamente as especificações da CPU, assembler, tradutor de máquina virtual e compilador, e ao portar a plataforma para a web adicionou sua própria IDE e UI
- O motivo de Jack exigir declaração de tipos é que o compilador precisa determinar a qual classe um método de instância pertence
s.appendChar(33)emsdeclarado como tipoStringé convertido durante a compilação emString.appendChar(s, 33)
- A IDE sacrifica a experiência do usuário para simplificar a implementação
- Para syntax highlighting, ela usa
contenteditablee lógica de posicionamento do cursor - Como resultado, é lenta, visivelmente bugada, e atalhos comuns de teclado não funcionam
- Para syntax highlighting, ela usa
- Para compilar e executar o código, basta clicar em “Start”
- O OS normalmente leva um pouco menos de 1 segundo para inicializar a memória e configurar os serviços
1 comentários
Comentários do Hacker News
Excelente projeto paralelo, e o README também é muito bom. Depois de mexer um pouco no 6502 Computer do Ben Eater (https://eater.net/), eu estava pensando em seguir o Nand to Tetris
Só com esse material daria para montar algumas disciplinas universitárias. É um material bem feito
Muito bem feito. É como se a pessoa tivesse percorrido pessoalmente camadas de abstração que a maioria dos programadores não verá durante toda a carreira
Trabalho incrível. NAND to Tetris me ajudou a conseguir meu primeiro emprego depois da faculdade
No início dos anos 1990, no exame de qualificação em hardware de computadores da UC Berkeley, um projeto parecido era a questão central
Especificamente, era preciso projetar do zero, usando apenas portas NAND, um processador RISC pipeline baseado em microcódigo; não era necessário construí-lo de fato, mas era preciso entregar um projeto detalhado no papel
Trabalho realmente impressionante. Quando fiz o curso Nand2Tetris, eu também quis criar uma implementação virtual parecida
É impressionante que alguém tenha realmente conseguido, e agora essa pessoa deve entender muito bem como um computador funciona
Mas é surpreendente ver que alguém já fez um trabalho uma ordem de grandeza mais impressionante do que eu imaginava
Excelente trabalho. Recentemente também comecei o Nand2Tetris e quero terminar nos próximos meses a parte 1, que é a parte de hardware do curso
Registrei meu progresso neste blog: https://gurudas.dev/blog/2024/04/13/nand-to-tetris-2024-proj...
Mentira. Você usou portas NAND e um clock
Trabalho incrível. Marquei nos favoritos para olhar com mais calma depois
Gosto de NAND-to-Tetris, mas nunca fui até o fim, então estou ansioso para explorar este projeto
Fiquei curioso: qual é o número total de portas NAND?