Code Monkey home page Code Monkey logo

pynand's Introduction

From Nand to Tetris in Python

An alternative language and test harness for doing the exercises from the amazing book/course Nand To Tetris. This version, in Python, may provide a better experience as compared to the tools provided by the authors:

  • No need to install Java
  • No clunky UI
  • You only need Python, a text editor, and basic command-line skills
  • Able to simulate any similar CPU design at full speed

Requirements

You need to have Python installed, at least version 3.6.

Pytest is required to run the tests: pip3 install pytest.

For the full computer simulation including display and keyboard, Pygame is required: pip3 install pygame.

And for the best performance, you can install the static compiler Cython: pip3 install cython

Step 1: Do the Exercises

First clone the repo and run pytest. All the tests should pass, because the included solutions are used for every component.

Now open project_01.py in a text editor, find the mkNot function, and the line with solved_01.Not. Replace that with Nand(a=..., b=...) using the inputs so that it computes the expected result.

Run ./test_01.py. If test_not passes, you can move on to the next component.

When you're all done, delete the line from nand.solutions import solved_01 at the top of the file to be sure you didn't miss anything. Actually, if you prefer you can start by deleting that line, then work on getting the tests to pass one at a time.

That's it for the first chapter. Now move on to test_02.py

Quit while you're ahead

Although it's fun to complete the entire course to build everything from the chip to the compiler yourself, you can stop at any time, or indeed implement whichever parts of the stack that you're most interested in.

The amount of time and code required to complete the projects increases pretty substantially after the hardware portion (projects 1–5). In particular, projects 8 and 12 may involve significant code and lots of debugging.

This repo is set up so you only have to work on the projects that interest you; you'll be running programs on your own designs as soon as you complete a single component in any of the project_*.py modules.

Step 2: Enjoy

Run ./computer.py examples/Pong.asm. Bask in the glory of a CPU you built from scratch. Note: the awesomeness starts after about 4 million cycles.

Pong screenshot

Depending on your hardware, you might need to use the simulator's options to get a playable game. Try ./computer.py --help to get started.

You can also run VM/Jack programs with ./computer.py [dir]. The test programs for the compiler are available under examples/project_11 (along with Pong in Jack source form), and the more interesting examples for testing the OS are in examples/project_12.

More interesting/challenging programs can be found on the internet if you're persistent. Beware: many JACK programs you can find are quite large and may or may not fit in ROM when the standard compiler/translator are used. I suspect their authors only ever tested them with the course's included "VM simulator", which doesn't require the program to be translated! You can try one of the alternative implementations (under alt/), several of which are aimed at supporting large programs.

Step 3: Go Further

Your CPU Design

The components here can be used to implement any generally similar CPU design. Ideas:

  • Make a fancier ALU. What features do you think would be useful?
  • Make a better instruction encoding. How can it do more with less?
  • Make an even smaller CPU. What can you take away and still get stuff done?

You can implement alternative designs by providing a new chip, translator, and/or compiler, and see how they measure up to the authors' original design (and your implementation of it.)

Some experiments can be found in this repo under alt/.

Your Language

What language do you like to write? Can you compile that language, or something like it, to the Hack VM? Or directly to assembly?

One interesting (and still experimental) alternative language is included: an interactive interpreter for the Scheme language, ca 1991. See alt/scheme.

License

This code is open source (MIT License), to the extent that I created it. However, if you want to base your own work on it you should be aware that the original work is covered by a different license, and I can provide no advice on the implications.

The entire CPU design, the breakdown of projects, most of the tests, and some example programs are directly taken from the original From Nand to Tetris course materials. Solvers are encouraged to acquire a copy of the text; many questions are answered there.

All the solutions included in this repo are my own work, but I often compared the results against the Nand to Tetris Software Suite. I tried to stay close the spirit of the original, but sometimes strayed when certain choices were especially awkward in a Python context.

The experiments found under alt/ are my own creations.

pynand's People

Contributors

mossprescott avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

pynand's Issues

Memory should model read latency

The current implementation of memory for simulation is unrealistic in that it provides "immediate" access to data; as soon as the address is applied to the input, the content word can be read from the output as if there were no inherent latency.

Part of the rationale for making memory a separate component for simulation is that real chips tend to use a higher-density technology for memory, so simulating a memory built from logic gates is silly. But to keep some level of realism, we shouldn't actually let chip designers treat the memory like a register that you can access within a single cycle. The latency of accessing memory should be a design constraint (i.e. if you want to make your chip go faster by having more accessible data, you have to add gates for registers, you can't cheat by treating the memory like a magic cache that provides sub-cycle access.)

In fact, most of the designs implicitly tolerate one-cycle memory latency; they're designed to do address calculation with one instruction and then consume the value from memory with the next. But the simulator doesn't model that behavior and some of the alternative designs I've added are probably playing fast and loose. In particular, I have a non-pipelined RiSC-16 implementation which uses this cheat (or I think it does; it would be great if the simulation proved it.)

Requirements:

  • RAM updates its output only on the cycle after a (new) address is presented
  • Accept a new address in each cycle, so throughput is still one word per cycle, just with a delay/latch
  • Document/test that behavior so solutions are still consistent with the discrete component

What, if anything, does this mean for writes? If there's write delay, can it be observed? It's OK to set the address, the input word, and the load bit all in the same cycle. If you then unset load and try to read the same word, that would be subject to the normal read delay, right?

Hello - What's the license of PyNand?

Hello Moss Prescott,

A really nice project!

Sometime ago when I was starting to do Nand2Tetris I really like the project but I didn't like the Java tools, they didn't resonate with me.

And at the time I would like to know How I could build all the project in Rust from the simple gates without using there hardware emulator, doing all from start to finish.

But then I come to the conclusion that a logic simulator that can simulate feedback loops isn't as easy at it sounds and the thing stayed that way. Without me doing the simulator in Rust. Only the first steps in Nand2Tetris, but like I said I didn't liked the tools.

Then I tkinkered mentally with the idea of simulating the NAND circuit electrically by simulating the transistors mathematically, even if it was only one logical gate in all the project.

Like allowing the simulation of at least one peace at each level.

And why staying at that level and not learning how to simulate one transistor at a atomic/quantum level? heheheheh It would be really fun to learn all the things about it! And to have at least one component that would work that way.

Then I toyed mentally with the idea that the NAND circuit for a NAND can be made with two transistors one nmos and one pmos and that all the processing of all NAND gates could be made to pass over that two real transistors. With just a control unit made with a Raspberry Pi Pico and the processing of a NAND chopped up into peaces. In with you inject a 3.3V one or a 0V zero and after a delay or instantly you would receive the result of the transistor processing, as a logical value or even as Voltage ADC value.

But currently I'm just interested in knowing what's the license of PyNand to know if it's possible to reimplement it by some kind of translation from Python to Rust.

Thank you for the really good project!

The very best wishes,
João Nuno Carvalho

In codegen, avoid evaluation that's not used

There are three obvious sub-graphs whose results are not used a lot of the time:

  • The entire ALU result is unused on every @ instruction, which is 9.5k out of 27.5k (35%).
  • Only one of the + and & ALU ops is ever used.
  • The JMP condition is unused unless not an @ and one of the three bits is set.

I tried converting each of these into if instr & 0x8000: by hand, and got ~25% speedup overall.

To automate that would involve analyzing the node graph to determine what parts of the graph need to be evaluated when. Something like: "what subgraphs are referenced only on one side of a conditional (i.e. Mux16), and what is the condition?" but it's tricky because the ALU output is referenced many times, with various conditions, all of which include the same bit of the instruction in some way.

Turns out CS is hard.

Add proper source locations to improve parsing error messages

Currently, parse errors come with a source location in the form of a token index:

Expected token ('symbol', ';') at location 4281; next token: ('keyword', 'let')

But there's no practical way to map that index onto a line number and source file, which is what would actually be useful.

What you want is something like:

Parse error at Foo.jack:1110; expected ';':
        let ptr[0] = x
        let ptr[1] = y;
        ^^^

Note: we can't easily enhance the parser to know about raw source locations because the tests for project_10.py require a lexer that produces tuples which are just (type, string).

Implement a simple type checker for Jack

When writing any kind of non-trivial Jack code, the complete lack of any type checking means both wasting time fixing silly errors and adding types that are effectively never used (except as documentation).

A simple type-checker would catch a lot of the simplest problems, although the language and its types are pretty weak.

Motivating example: the Scheme interpreter needs to convert between tagged values (typed as Obj) and pointers (Rib) before using them. These conversion are all over the code, which is fairly complex to begin with, and it's very easy to miss one. On the other hand, the interpreter also indexes directly into ribs to avoid the overhead of method calls, which will produce unhelpful type errors. Arguably, the fix for that is inlining the methods.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.