Show HN: Una computadora programable hecha con compuertas NAND
(github.com/ArhanChaudhary)- NAND es una computadora de 16 bits equivalente a Turing emulada en la web, basada en la premisa educativa de estar compuesta solo por un clock y compuertas NAND, e incluye desde la CPU hasta el IDE y la UI
- Su stack propio se basa en la plataforma Jack-VM-Hack e incluye CPU, lenguaje de máquina, ensamblador, assembler, lenguaje VM, VM translator, lenguaje Jack y compiler
- Los programas de ejemplo incluyen Average, Pong y 2048, además de una demo de VM escape que usa stack overflow y stack smashing, y GeneticAlgorithm, que usa machine learning simple
- Jack es un lenguaje orientado a objetos de tipado débil, similar a la sintaxis de Java, pero internamente depende de un único signed 16-bit integer y tiene muchas restricciones, como ausencia de precedencia de operadores, gestión manual de memoria y comportamiento indefinido
- El proyecto no implica que todos los cálculos se ejecuten realmente en compuertas NAND físicas; es una implementación educativa y teórica que usa un compiler en TypeScript, un VM translator, un simulador de lógica de compuertas NAND basado en Rust y WebAssembly
El stack completo de computadora que ofrece NAND
- NAND se presenta como una computadora web de 16 bits cuyo nombre significa Not A Nand-powered Device
- Emula una computadora equivalente a Turing hecha con clock y compuertas NAND, e incluye por sí misma los siguientes componentes
- CPU
- lenguaje de código máquina
- lenguaje ensamblador
- assembler
- lenguaje de máquina virtual
- traductor de máquina virtual
- lenguaje de programación
- compilador
- IDE
- interfaz de usuario
- Está basada en el curso Nand to Tetris y en la plataforma Jack-VM-Hack del libro relacionado
- Se ofrece un video demo de NAND
Programas de ejemplo y demos
-
Average
- Programa simple que recibe números y calcula el promedio
- Muestra control flow, operaciones aritméticas, I/O y asignación dinámica de memoria
- Es un programa incluido en la Nand to Tetris software suite
-
Pong
- Juego Pong que muestra un modelo orientado a objetos
- El paddle se mueve a la izquierda y derecha con las flechas para rebotar la pelota
- Cada vez que la pelota rebota, el paddle se achica; si la pelota toca la parte inferior de la pantalla, termina el juego
- Es un programa incluido en la Nand to Tetris software suite
-
2048
- Juego 2048 que muestra recursion y application logic compleja
- En una grid 4x4, los números se mueven con las flechas y los números iguales se combinan
- Se gana al llegar al tile 2048, pero se puede seguir jugando hasta perder
-
Overflow
- Provoca intencionalmente un stack overflow mediante recursión infinita para realizar un virtual machine escape
- Aprovecha que en runtime no hay una verificación para prevenir stack overflow
- Cuando el valor del stack pointer supera 2048, el stack sale de su espacio de memoria previsto y se desborda hacia el heap memory space
- Si se ejecuta con la RAM vacía, el programa puede reiniciarse a mitad de camino por una instruction que establece el program counter en 0
- Si se ejecuta justo después de GeneticAlgorithm, puede leer memoria RAM anterior que no fue sobrescrita
-
SecretPassword
- Aprovecha que el runtime no bloquea el stack smashing para llamar a una función que originalmente no era accesible
- El usuario puede sobrescribir una memory address de la RAM con el valor que quiera
- Si se sobrescribe la return address del stack frame con la dirección de otra función, es posible ejecutar código arbitrario dentro del programa
- Los valores de ejemplo son memory location
267y overwrite value1743 - El mismo tipo de vulnerabilidad también existe en el buffer overflow de C
-
GeneticAlgorithm
- Es una simulación de creatures que usa machine learning simple
- Cada dot tiene su propio “brain” compuesto por acceleration vectors y evoluciona mediante selección natural para llegar al goal
- Un dot que muere más cerca del goal tiene mayor probabilidad de ser elegido como parent de la siguiente generación
- Durante el proceso de reproduction, parte del brain muta para simular evolución natural
- Se ofrece un video demo de Genetic Algorithm
Restricciones de hardware que enfrentó GeneticAlgorithm
- GeneticAlgorithm fue el programa individual que más tiempo llevó desarrollar entre los varios componentes de NAND
- Por rendimiento, el único factor que usan los dots para evolucionar es qué tan cerca están del goal al morir, por lo que la entropy del algoritmo de selección natural es baja
- Por uso de memoria, hay límites poco satisfactorios en la cantidad de dots y el tamaño del brain
- Por complejidad técnica, aunque se reubiquen obstacles durante la simulation, no se garantiza que el brain del dot sea lo suficientemente grande como para llegar al goal
- El brain size solo se determina al inicio del programa
- La implementación evita las restricciones de NAND mediante varias optimizaciones
- El ROM instruction memory space es limitado, por lo que si hay demasiado código no compila
- El GeneticAlgorithm final usa el 99.2% del instruction memory space
- El RAM memory space es limitado, por lo que es necesario optimizar el heap memory usage
- La razón por la que la pantalla se llena de static entre generaciones es que el screen memory space se usa como temporary swap memory para la siguiente generación
- NAND no tiene floating point type, y el rango de integer representable va de -32768 a 32767
- La precisión del cálculo de fitness se reduce y también hay que considerar integer overflow
- Las optimizaciones relacionadas y más insight están documentados en el codebase de GeneticAlgorithm
Cómo escribir programas NAND en Jack
- Se destaca que la causa más importante de que un programa no funcione en Jack es la ausencia de precedencia de operadores
4 * 2 + 3debe escribirse como(4 * 2) + 3if (~x & y)debe escribirse comoif ((~x) & y)- El valor de evaluación de una expresión ambigua sin paréntesis es indefinido
- Jack es un lenguaje de programación orientado a objetos de tipado débil de NAND
- Según la descripción, se parece más a “C con sintaxis de Java”
- Como NAND tiene su propio stack tecnológico completo, solo puede programarse en Jack
- El Jack OS básico se empaqueta junto con el programa al compilar
- Proporciona strings, memoria e interfaces con el hardware
- Incluye funciones como
Keyboard.readLine,Keyboard.readInt,Output.printStringyOutput.println
- Jack admite tres tipos primitivos:
int,charyboolean- Se pueden definir tipos de datos abstractos mediante clases
- Las variables
fielddeclaran atributos por instancia - Las variables
fieldtienen alcance privado, y para acceder desde afuera se requiere un método
functionymethodse invocan de maneras distintas- Un método del objeto actual puede llamarse dentro de la misma clase como
do g(); - Una llamada a función debe ir precedida por el nombre de la clase
- Un método de objeto se invoca a través del objeto, como
do b.q();
- Un método del objeto actual puede llamarse dentro de la misma clase como
Tipado débil y gestión de memoria en Jack
- A diferencia de Java, Jack no admite tipado fuerte, down casting, polimorfismo ni herencia
- Internamente, el único tipo real es un entero con signo de 16 bits
- Al compilador no le preocupa que se mezclen tipos en asignaciones y operaciones
- Si se pone
65en unchar, puede tratarse como equivalente a'A' - Si se pone
5000en una variableArrayy se ejecutaa[100] = 77, eso se convierte enRAM[5100] = 77 - Las entradas de un array pueden contener distintos tipos de datos
- Si el layout de memoria coincide, un
Arraypuede usarse como si fuera una instancia de otra clase
- Jack es un lenguaje con gestión manual de memoria
- Si no se libera la memoria que ya no se necesita, se produce una fuga de memoria
- Un heap overflow aparece como
ERR6 - Jack OS almacena arrays y strings en el heap, no en el stack
- Como buena práctica, una clase que representa un objeto debe tener un método
dispose- Primero se llama a
disposeen las variables de campo - Al final, se libera la propia instancia del objeto con
do Memory.deAlloc(this);
- Primero se llama a
- Un loop que crea e imprime repetidamente string literals puede provocar un heap overflow
- Se puede liberar el string en cada iteración, o bien
- Se puede asignar el string una sola vez y reutilizarlo para seguir imprimiendo
Comportamiento indefinido y precauciones
-
Operadores de comparación
a > bya < bno siempre son matemáticamente correctos- La implementación de la VM convierte
a > bena - b > 0 - Como
a - bpuede desbordarse,20000 > -20000resultafalse - Si la distancia absoluta entre
aybes mayor que 32767,>y<pueden equivocarse - Este comportamiento no se corrige por compatibilidad con Nand to Tetris
-
-32768
-32768es el único valor para el que-(-32768) = -32768- Como no tiene contraparte positiva, puede generar falta de solidez y errores lógicos
Output.printIntespera internamente queMath.absdevuelva un número positivo, pero en-32768no ocurre así, lo que hace que Jack OS funcione mal
-
Llamadas a funciones con muy pocos argumentos
- Llamar sin argumentos a una función que tiene parámetros también puede producir comportamiento indefinido
- En cambio, una llamada a función con demasiados argumentos es válida, y los argumentos extra pueden indexarse con la palabra clave
arguments - No hay indicador de cantidad de argumentos
-
Type casting inadecuado
- Con
Arrayse puede castear una variable a otro tipo - Llamar a un método de instancia inexistente en la variable casteada es comportamiento indefinido
- El compilador no es lo suficientemente smart como para detectarlo
- Con
-
Modificación de stack frames y registros internos
- Modificar un stack frame en las direcciones de memoria
256a2047, o un registro interno de1a15, puede provocar comportamiento indefinido - En general, se indica que es difícil hacerlo sin un mal uso de
Memory.pokeo sin indexación negativa de arrays
- Modificar un stack frame en las direcciones de memoria
-
Carga de archivos VM de usuario
- NAND ofrece validación de programas para archivos
.jack, pero no para archivos.vm - En un archivo
.vmse pueden hacer llamadas a funciones inexistentes, referencias a variables no asignadas y operaciones de memoria lógicamente inválidas - En la mayoría de los casos se produce un escape de la máquina virtual y puede que no se muestre nada en pantalla
- NAND ofrece validación de programas para archivos
Especificaciones de hardware y distribución de memoria
- La RAM de NAND está compuesta por 32,768 words, y cada word contiene un número binario de 16 bits
- El hardware reserva 8,192 direcciones de memoria para la pantalla
- Cada bit de cada dirección se mapea linealmente al píxel correspondiente de la pantalla de 512x256
- La numeración de bits usa el esquema LSb 0
- El teclado está mapeado a la dirección de memoria
24576- La tecla presionada actualmente se refleja en esa ubicación
- En lugar de procesar directamente la entrada del usuario desde esa dirección, se recomienda usar la clase
Keyboardde Jack OS
- El teclado reconoce caracteres ASCII y teclas especiales
- nueva línea =
128 - backspace =
129 - flechas izquierda/arriba/derecha/abajo =
130~133 - home/end/page up/page down/insert/delete/ESC =
134~140 - F1~F12 =
141~152
- nueva línea =
- El hardware reserva 240 direcciones de memoria para variables de clase estáticas y 1,792 direcciones de memoria para el stack global
- Se indica que, mientras no se use recursión profunda, esta limitación por lo general no es un problema
Implementar un OS propio más allá de Jack OS
- Básicamente, al compilar, Jack OS se empaqueta junto con el programa y proporciona strings, memoria e interfaces de hardware
- Se puede proporcionar una implementación de OS propia con una interfaz de hardware dedicada
- El IDE trata los archivos de Jack OS igual que los archivos de programa normales
- Los archivos del OS también se pueden eliminar o sobrescribir
- Aunque se use un OS propio, hay funciones core que deben implementarse obligatoriamente para compilar
Sys.init: no esMain.main, sino el verdadero entry point hardcodeado en la implementación de la VMMemory.alloc: es el asignador de memoria heap que se usa internamente cuando un constructor de clase crea un objetoString.newWithStr: constructor interno para string literalsMath.multiply: se invoca internamente en lugar de la expresión Jackx * yMath.divide: se invoca internamente en lugar de la expresión Jackx / y
Sys.initdel Jack OS incluido inicializa la memoria, math, screen y output; luego llama aMain.main()y después aSys.halt()
Funcionamiento interno de NAND
- La computadora NAND sigue una arquitectura Harvard
- La ROM, que es la memoria de instrucciones, está separada de la RAM, que es la memoria de datos
- La CPU hace que ambas funcionen juntas
- La CPU es una accumulator machine
- En el control flow depende mucho de registros integrados
- Aquí el acumulador es el registro
D
- El instruction set de la CPU tiene solo dos opcodes
- El instruction set es relativamente simple, pero ofrece funciones ricas
- La ALU se especifica mediante expresiones que se pueden calcular en una instrucción
- El compilador y la máquina virtual no son conceptos exclusivos de NAND, así que se tratan brevemente
- Algunas características extrañas de la sintaxis son resultado de facilitar la implementación del compilador
- El compilador es un recursive descent parser sobre una gramática LL(1)
- El compilador genera código VM, y la VM se usa como una stack machine simple
- Cada instrucción VM se mapea a assembly y machine code
- El código de implementación se puede consultar en core y en la implementación del compilador
Puntos clave del lenguaje Jack y de la referencia del OS
- Un programa Jack se compone de una colección de clases
- Cada clase se define en un archivo separado
- Se necesita una o más clases, y una de ellas debe ser
Main - Según Jack OS, el entry point es la función
mainde la claseMain
- Una clase puede incluir declaraciones
field,static,constructor,methodyfunction- El orden de las declaraciones
fieldystatices arbitrario - El orden de las declaraciones de subrutinas también es arbitrario
- El tipo es uno de
void,int,boolean,charo el nombre de una clase
- El orden de las declaraciones
- Características de sintaxis
- Los espacios en blanco y comentarios se ignoran
&y|son operadores bitwise y no hacen short-circuittrue,falseynullson-1,0y0, respectivamente- Las constantes string no pueden incluir directamente un newline ni comillas, y tampoco permiten escapes
- Las comillas y el newline son proporcionados por
String.doubleQuote()yString.newLine()del OS - Los identifiers son case-sensitive
- Clases principales de Jack OS
Array: creación y dispose de arraysKeyboard: pulsación de tecla, carácter, línea e input de enterosMath: abs, multiply, divide, sqrt, max, minMemory:peek,poke,alloc,deAllocOutput: salida en pantalla de texto y movimiento del cursorScreen: dibujo de pixel, línea, rectángulo y círculoString: creación de strings, dispose, acceso a caracteres, append y conversión de enterosSys: halt, error, wait
- Un estado inválido muestra un código de error con el formato
"ERR[N]"y finaliza la ejecución del programaERR3: división por ceroERR6: heap overflowERR15,ERR16: índice de string fuera de rangoERR17: el string está llenoERR20: ubicación ilegal del cursor
No es un proyecto hecho solo con compuertas NAND reales
- La FAQ reconoce que la descripción y el título “everything made from NAND gates” son engañosos, aunque de buena fe
- El compilador y el traductor de la máquina virtual están escritos en TypeScript
- El kernel emulado y el hardware emulado no representan exactamente cómo funciona una computadora real
- El simulador real de lógica con compuertas NAND está escrito en Rust y ocupa solo una pequeña parte de todo el codebase
- El código Rust se compila a WebAssembly para ejecutarse en el navegador
- Se indica que, por este motivo, prácticamente se elimina la premisa de que toda la computación se ejecuta en compuertas NAND
- NAND cumple el rol de proyecto educativo y teórico
- En teoría, la misma lógica de CPU podría funcionar también en una manifestación real del hardware emulado
- Se presenta como ejemplo de proyecto de hardware FPGA basado en nand2tetris: https://gitlab.com/x653/nand2tetris-fpga/
Alcance de la implementación y límites del IDE
- NAND sigue la especificación del curso Nand to Tetris y del libro relacionado
- El implementador desarrolló por su cuenta la CPU, el assembler, el traductor de máquina virtual y la especificación del compilador, y al portar la plataforma a la web agregó su propio IDE y UI
- La razón por la que Jack debe especificar tipos es que el compilador necesita determinar a qué clase pertenece un método de instancia
s.appendChar(33)de unasdeclarada con tipoStringse transforma durante la compilación enString.appendChar(s, 33)
- El IDE sacrifica la experiencia de usuario para simplificar la implementación
- Usa
contenteditabley lógica de posicionamiento del cursor para el syntax highlighting - Como resultado, es lento, notoriamente buggy y los keybinds comunes no funcionan
- Usa
- Para compilar y ejecutar el código, basta con presionar “Start”
- Por lo general, el OS tarda un poco menos de 1 segundo en inicializar la memoria y configurar los servicios
1 comentarios
Opiniones en Hacker News
Es un gran proyecto paralelo y el README también está muy bueno. Después de trastear un poco con la 6502 Computer de Ben Eater (https://eater.net/), estaba pensando en seguir Nand to Tetris.
Solo con este material se podrían armar varios cursos universitarios. Es material muy bien hecho.
Está realmente bien hecho. Básicamente recorrió en persona capas de abstracción que la mayoría de los programadores no verá en toda su carrera.
Excelente trabajo. NAND to Tetris me ayudó a conseguir mi primer empleo después de graduarme de la universidad.
A principios de los años 90, en el examen de certificación de hardware de computadoras de UC Berkeley, un diseño parecido era el problema central.
En concreto, se trataba de diseñar desde cero, usando solo compuertas NAND, un procesador RISC con pipeline basado en microcódigo; no hacía falta construirlo físicamente, pero había que entregar un diseño detallado en papel.
Un trabajo realmente impresionante. Cuando hice el curso Nand2Tetris, también quise hacer una implementación virtual parecida.
Me impresiona que realmente lo haya logrado, y ahora seguramente entiende muy bien cómo funciona una computadora.
Pero me sorprende que alguien ya haya hecho un trabajo un orden de magnitud más increíble de lo que yo imaginaba.
Excelente trabajo. Hace poco también empecé Nand2Tetris, y me gustaría terminar en los próximos meses la Parte 1, que es la parte de hardware del curso.
Dejé anotado mi progreso en este blog: https://gurudas.dev/blog/2024/04/13/nand-to-tetris-2024-proj...
Mentira. Usaste compuertas NAND y un reloj.
Excelente trabajo. Lo dejé en marcadores para revisarlo en profundidad después.
Me gusta NAND-to-Tetris, pero nunca lo terminé, así que tengo ganas de explorar este proyecto.
Tengo curiosidad: ¿cuántas compuertas NAND hay en total?