Web-Ide mit aufgenommen
@@ -0,0 +1,69 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report
|
||||||
|
type: "Bug"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to fill out a bug report on the NAND2Tetris Web IDE or VSCode Extension
|
||||||
|
- type: dropdown
|
||||||
|
id: program
|
||||||
|
attributes:
|
||||||
|
label: Tool
|
||||||
|
description: Select the tool about which you wish to report the bug / issue
|
||||||
|
options:
|
||||||
|
- Hardware Simulator
|
||||||
|
- CPU Emulator
|
||||||
|
- Assembler
|
||||||
|
- VM Emulator
|
||||||
|
- Jack Compiler
|
||||||
|
- General
|
||||||
|
- type: dropdown
|
||||||
|
id: interface
|
||||||
|
attributes:
|
||||||
|
label: Interface
|
||||||
|
description: Which NAND2Tetris interface were you using?
|
||||||
|
options:
|
||||||
|
- Website (https://nand2tetris.github.io/web-ide)
|
||||||
|
- VSCode Extension (coming soon, or manually installed)
|
||||||
|
- type: input
|
||||||
|
id: contact
|
||||||
|
attributes:
|
||||||
|
label: Contact Details
|
||||||
|
description: How can we get in touch with you if we need more info?
|
||||||
|
placeholder: ex. email@example.com
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Also tell us, what did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "A bug happened!"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional-comments
|
||||||
|
attributes:
|
||||||
|
label: Additional Comments
|
||||||
|
description: What else, if anything, would you like to share with us?
|
||||||
|
placeholder: "Tell us anything!"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: self-fix
|
||||||
|
attributes:
|
||||||
|
label: Do you want to try to fix this bug?
|
||||||
|
description: The IDE is written in TypeScript, and includes React components. Do you want to try to fix the bug yourself (optional question)? If so, you can fork the repo, try to make the fix, and submit a PR.
|
||||||
|
options:
|
||||||
|
- label: I want to try to add this feature!
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/nand2tetris/web-ide/blob/main/CODE_OF_CONDUCT.md)
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Request to add or improve a feature
|
||||||
|
type: "Feature"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to fill out a feature request on the NAND2Tetris Web IDE or VSCode Extension
|
||||||
|
- type: dropdown
|
||||||
|
id: program
|
||||||
|
attributes:
|
||||||
|
label: Tool
|
||||||
|
description: Select the tool for which you wish suggest a feature
|
||||||
|
options:
|
||||||
|
- Hardware Simulator
|
||||||
|
- CPU Emulator
|
||||||
|
- Assembler
|
||||||
|
- VM Emulator
|
||||||
|
- Jack Compiler
|
||||||
|
- General
|
||||||
|
- type: dropdown
|
||||||
|
id: interface
|
||||||
|
attributes:
|
||||||
|
label: Interface
|
||||||
|
description: Which NAND2Tetris interface were you using?
|
||||||
|
options:
|
||||||
|
- Website (https://nand2tetris.github.io/web-ide)
|
||||||
|
- VSCode Extension (coming soon, or manually installed)
|
||||||
|
- type: input
|
||||||
|
id: contact
|
||||||
|
attributes:
|
||||||
|
label: Contact Details
|
||||||
|
description: How can we get in touch with you if we need more info?
|
||||||
|
placeholder: ex. email@example.com
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What feature are you proposing?
|
||||||
|
description: Let us know
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: "Feature description"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional-comments
|
||||||
|
attributes:
|
||||||
|
label: Additional Comments
|
||||||
|
description: What else, if anything, would you like to share with us?
|
||||||
|
placeholder: Tell us anything!
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: self-fix
|
||||||
|
attributes:
|
||||||
|
label: Do you want to try to add this feature?
|
||||||
|
description: The IDE is written in TypeScript, and includes React components. Do you want to try to implement this feature yourself? If so, you can fork the repo, try to make the fix, and submit a PR.
|
||||||
|
options:
|
||||||
|
- label: I want to try to add this feature!
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/nand2tetris/web-ide/blob/main/CODE_OF_CONDUCT.md)
|
||||||
|
options:
|
||||||
|
- label: I agree to follow this project's Code of Conduct
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Deploy to gh-pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- simulator
|
||||||
|
- web
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
id: cache-npm
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: CD
|
||||||
|
run: |
|
||||||
|
git config --global user.name $user_name
|
||||||
|
git config --global user.email $user_email
|
||||||
|
git remote set-url origin https://${github_token}@github.com/${repository}
|
||||||
|
./stamp.sh
|
||||||
|
npm run build
|
||||||
|
npm run -w web deploy
|
||||||
|
VERSION=$(grep version package.json | awk -F\" '{print $4}')
|
||||||
|
git tag "$VERSION" main
|
||||||
|
git push origin "$VERSION"
|
||||||
|
git push origin main
|
||||||
|
env:
|
||||||
|
user_name: "github-actions[bot]"
|
||||||
|
user_email: "github-actions[bot]@users.noreply.github.com"
|
||||||
|
github_token: ${{ secrets.ACTIONS_DEPLOY_ACCESS_TOKEN }}
|
||||||
|
repository: ${{ github.repository }}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Build vscode extension
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- simulator
|
||||||
|
- extension
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
id: cache-npm
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: CD
|
||||||
|
run: |
|
||||||
|
npm run build
|
||||||
|
npm run -w extension package
|
||||||
|
|
||||||
|
- name: Upload extension
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: nand2tetris-vscode
|
||||||
|
path: |
|
||||||
|
extension/nand2tetris-vscode-*.vsix
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Run Workspace Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: ["main", "release/**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
all:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
id: cache-npm
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|
||||||
|
- if: ${{ steps.cache-npm.outputs.cache-hit == 'false' }}
|
||||||
|
name: List the state of node modules
|
||||||
|
continue-on-error: true
|
||||||
|
run: npm list
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: CI
|
||||||
|
run: npm run ci
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# instruct npm to fail if the versions specified in the "engines"
|
||||||
|
# section of package.json are not satisfied
|
||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
arrowParens: "always" # default
|
||||||
|
bracketSameLine: false # default
|
||||||
|
bracketSpacing: true # default
|
||||||
|
embeddedLanguageFormatting: "auto" # default
|
||||||
|
endOfLine: "auto" # default is "lf"
|
||||||
|
filepath: "" # default
|
||||||
|
htmlWhitespaceSensitivity: "css" # default
|
||||||
|
insertPragma: false # default
|
||||||
|
# jsxBracketSameLine: false # deprecated
|
||||||
|
jsxSingleQuote: false # default
|
||||||
|
parser: "" # default
|
||||||
|
printWidth: 80 # default
|
||||||
|
proseWrap: "preserve" # default
|
||||||
|
quoteProps: "as-needed" # default
|
||||||
|
# rangeEnd: Infinity # default; "Infinity" can't be read as int
|
||||||
|
rangeStart: 0 # default
|
||||||
|
requirePragma: false # default
|
||||||
|
semi: true # default
|
||||||
|
singleAttributePerLine: false # default
|
||||||
|
singleQuote: false # default
|
||||||
|
tabWidth: 2 # default
|
||||||
|
trailingComma: "all" # default is "es5"
|
||||||
|
useTabs: false # default
|
||||||
|
vueIndentScriptAndStyle: false # default
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
projects/src/samples/project_06/04_pong_asm.ts
|
||||||
|
web/public/pico.min.css
|
||||||
|
web/src/locales
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Computron Tests",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/react-scripts",
|
||||||
|
"args": ["test", "--runInBand", "--no-cache", "--env=jsdom", "${file}"],
|
||||||
|
"env": { "CI": "true" },
|
||||||
|
"cwd": "${workspaceFolder}/simulator",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run Extension",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"--disable-extensions",
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}/extension"
|
||||||
|
],
|
||||||
|
"outFiles": ["${workspaceFolder}/extension/build/**/*.js"]
|
||||||
|
// "preLaunchTask": "${defaultBuildTask}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Extension Tests",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"--disable-extensions",
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}/extension",
|
||||||
|
"--extensionTestsPath=${workspaceFolder}/extension/build/test/suite/index"
|
||||||
|
],
|
||||||
|
"outFiles": ["${workspaceFolder}/extension/build/test/**/*.js"],
|
||||||
|
"preLaunchTask": "${defaultBuildTask}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to make participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all project spaces, and it also applies when
|
||||||
|
an individual is representing the project or its community in public spaces.
|
||||||
|
Examples of representing a project or community include using an official
|
||||||
|
project e-mail address, posting via an official social media account, or acting
|
||||||
|
as an appointed representative at an online or offline event. Representation of
|
||||||
|
a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at davidsouther@gmail.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Computron 5k Contributions
|
||||||
|
|
||||||
|
- [Deployed Online](https://nand2tetris.github.io/web-ide)
|
||||||
|
- [Source](https://github.com/nand2tetris/web-ide)
|
||||||
|
- [Issues](https://github.com/nand2tetris/web-ide/issues)
|
||||||
|
|
||||||
|
## Dependencies and Environment
|
||||||
|
|
||||||
|
The Nand2Tetris Web IDE is developed in the TypeScript programming language and run on the NodeJS platform. Running the IDE and tests locally requires having `node`, `npm`, and `npx`. We recommend installing all three, as well as keeping them updated, by using the [`nvm` tool](https://github.com/nvm-sh/nvm).
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
The source code is version controlled using `git`, which should be installed using the recommended way depending on your operating system. Issues, pull requests, and other project management is conducted on [GitHub](https://github.com/nand2tetris/web-ide). When developing to contribute a feature, we recommend creating a fork, cloning from your fork, creating a new branch with `git switch -c`, pushing that branch to your fork, and creating a pull request from that branch to main. See documentation on GitHub and the internet for more details.
|
||||||
|
|
||||||
|
After cloning the repository, to install all dependencies, `cd` to the downloaded folder and run `npm install`. To update the compiled TypeScript libraries after making any changes in `simulator`, `projects`, or `components`, run `npm run build`. Alternatively, to only build one of those three, run `npm run build -w <name>`. To run all tests, run `npm test`; for tests in just one part of the project, `npm test -w <name>`. After building the libraries, you can run the web IDE with `npm run web`. This will begin the development server on https://localhost:8080.
|
||||||
|
|
||||||
|
## Issues & Pull Requests
|
||||||
|
|
||||||
|
- Send pull requests to the `main` branch.
|
||||||
|
- Please use the `bug` and `enhancement` tags when creating general issues.
|
||||||
|
- Issues tagged `good first issue` are expected to be small, focused changes to contained parts of the codebase.
|
||||||
|
- Many parts of Hack are not yet implemented. If the feature is part of the core hack book, add the issue to the `Book Parity` milestone and either the `HDL`, `CPU`, or `VM` project as appropriate.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright 2022 David Souther et al
|
||||||
|
|
||||||
|
This software is based on Stefano Volpe's 'Nand2Tetris Tools'. Please check [here](https://github.com/foxyseta/nand-ide/blob/master/LICENSE) for further information.
|
||||||
|
This software is based on Aviv Yaish's 'NAND IDE'. Please check [here](https://github.com/AvivYaish/nand-ide/blob/master/LICENSE) for further information.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Nand2Tetris Software Suite
|
||||||
|
|
||||||
|
A Javascript reimplementation of the software suite described in www.nand2tetris.org and in "The Elements of Computing Systems" by Nisan and Schocken, MIT Press (2nd edition, 2021). The repo also includes the project files described in the website and in the book.
|
||||||
|
|
||||||
|
The goal is to allow students complete the projects using modern, web-based tools, without having to download code to their computers.
|
||||||
|
|
||||||
|
Users can work with the tools via a web IDE, or via a VS Code extension. Both are described below.
|
||||||
|
|
||||||
|
## User Guide
|
||||||
|
|
||||||
|
The user guides for the web IDE are available [here](https://drive.google.com/drive/folders/10hDzWql94MTPIStI3KEx--JYpHTBoeE6) and can also be accessed by clicking "Guide" at the top right of the [published project](https://nand2tetris.github.io/web-ide).
|
||||||
|
|
||||||
|
The user guide for the extension is coming.
|
||||||
|
|
||||||
|
The parts of the user guide that describe the UI may be out of sync with the code since we keep experimenting with differtent UI's.
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
Install the CLI tool:
|
||||||
|
|
||||||
|
npm install
|
||||||
|
npm run build && npm i -g ./cli
|
||||||
|
|
||||||
|
Run the CLI:
|
||||||
|
|
||||||
|
cd nand2tetris/project/01
|
||||||
|
nand2tetris grade
|
||||||
|
nand2tetris run DMux4Way.tst
|
||||||
|
|
||||||
|
Run the CLI with a nand2tetris Java install:
|
||||||
|
|
||||||
|
cd nand2tetris/project/01
|
||||||
|
nand2tetris grade --java_ide=${HOME}/nand2tetris
|
||||||
|
|
||||||
|
### Web IDE
|
||||||
|
|
||||||
|
Build the web IDE:
|
||||||
|
|
||||||
|
npm install
|
||||||
|
npm run build && npm run start
|
||||||
|
|
||||||
|
It will print the URL to the console. Any changes will automatically trigger a rebuild.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
NAND2Tetris kit is a monorepo with several projects.
|
||||||
|
`simulator` is the core NAND2Tetris code.
|
||||||
|
`projects` has copies of project base and test files.
|
||||||
|
`runner` is a utility to execute chips against a Java ide install, looking for nand2tetris.jar in $NAND2TETRIS_PATH.
|
||||||
|
`components` are reusable React UI pieces suitable for both web and extension.
|
||||||
|
`web` is a standalone web IDE.
|
||||||
|
`extension` is a VSCode extension with editor support.
|
||||||
|
`cli` is a command line NodeJS program (runnable with `npx`) to grade one or more project folders.
|
||||||
|
|
||||||
|
### Simulator
|
||||||
|
|
||||||
|
Simulator has code to handle running the various emulators, regardless of interface or runtime.
|
||||||
|
Simulator objects are also independant of language, and serve equally well to running tests as to binding to the DOM or printing to a CLI.
|
||||||
|
`chip`, `cpu`, and `vm` cover the primary hack languages, with `compare`, `output`, and `tst` handling the common project tooling.
|
||||||
|
|
||||||
|
#### Languages
|
||||||
|
|
||||||
|
Languages are parsed using [Ohm](https://ohmjs.org/), a parser combinator library.
|
||||||
|
Ohm works well for simple cases, but does not handle error recovery well.
|
||||||
|
Replacing or augmenting this to handle a number of errors, rather than only the first, is a possible future improvement.
|
||||||
|
|
||||||
|
### Web
|
||||||
|
|
||||||
|
NAND2Tetris Web IDE is a stand-alone single-page app with separate sections for Hack Hardware, CPU, and VM emulators.
|
||||||
|
It has a unified file system using browser local storage to save users' solutions to project work.
|
||||||
|
Emulators share simulator code, especially to handle executing tests as well as converting between Javascript 64-bit floating point numbers and Hack 16-bit integers.
|
||||||
|
|
||||||
|
The interface code is in the `pages` and `components` folders.
|
||||||
|
Generally, a page creates a simulator at the top, some dynamic components in the middle, and a layout of HTML at the bottom.
|
||||||
|
Pages should use semantic blocks as much as possible, with special attention on using `<article>` as a "Card".
|
||||||
|
|
||||||
|
#### React
|
||||||
|
|
||||||
|
The user interface is written in React, using functional components and vanilla hooks as much as possible.
|
||||||
|
Pages are routable things, usually with a store connecting it to the appropriate simulation.
|
||||||
|
Components are reusable pieces of UI, which take props to update their interface.
|
||||||
|
|
||||||
|
#### RXJS
|
||||||
|
|
||||||
|
Asynchronous one-off behavior in the project can be handled with promises & async/await syntax.
|
||||||
|
For evented asynchronous behavior, use RXJS observables and subscriptions.
|
||||||
|
|
||||||
|
#### Pico
|
||||||
|
|
||||||
|
Jiffies extends [`PicoCSS`](https://picocss.com), allowing rapid iteration on custom components.
|
||||||
|
Some ideas have been moved upstream to Pico.
|
||||||
|
Specific components in the forked Pico include [`inline-buttons`](https://github.com/picocss/pico/issues/182) and a [`property sheet`](https://github.com/picocss/pico/issues/195).
|
||||||
|
|
||||||
|
### Extension
|
||||||
|
|
||||||
|
A VSCode extension with language definitions and editor support.
|
||||||
|
|
||||||
|
- `npm run -w extension package` builds the extension & related views into a stand-along `.vsix` file.
|
||||||
|
- `Run Extension` launch configuration starts a new VSCode extension host to debug the extension.
|
||||||
|
|
||||||
|
#### Languages
|
||||||
|
|
||||||
|
Language support for `.hdl` & `.tst` uses the language libraries in `simulator`.
|
||||||
|
Syntax errors are highlighted, with in-editor error diagnostics on the failing token.
|
||||||
|
Syntax highlighting rules activate for `HDL`, `TST`, `CMP`, `OUT`, `ASM`, `VM`, and `Jack` files.
|
||||||
|
Snippets are available for `HDL`, `ASM`, `VM`, `Jack`, and `TST` files.
|
||||||
|
|
||||||
|
#### Views
|
||||||
|
|
||||||
|
The extension adds an activity bar container, `NAND2Tetris`.
|
||||||
|
`NAND2TETRIS: HDL CHIP` opens in the container, and shows a chip panel when the user has opened an HDL file.
|
||||||
|
The panel attempts to update whenever changing HDL files, or when saving the file.
|
||||||
|
It does not update if the new HDL does not parse.
|
||||||
|
|
||||||
|
### Jiffies
|
||||||
|
|
||||||
|
Jiffies contains a few utility functions & types.
|
||||||
|
|
||||||
|
- `Result` and `Option` encapsulate "Ok/Err" and "Some/None" variant types.
|
||||||
|
- `assert`, `assertExists`, and `checkExhaustive` provide strongly-typed, portable assertions.
|
||||||
|
- `fs`, a thin wrapper around LocalStorage and similar `Record<string, string>` objects allowing Filesystem like access.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
This project is governed by its [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
|
||||||
|
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
||||||
|
"files": { "ignoreUnknown": false, "includes": ["**/src/**/*.ts"] },
|
||||||
|
"formatter": { "enabled": true, "indentStyle": "space" },
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": false,
|
||||||
|
"complexity": {
|
||||||
|
"noAdjacentSpacesInRegex": "error",
|
||||||
|
"noBannedTypes": "error",
|
||||||
|
"noExtraBooleanCast": "error",
|
||||||
|
"noUselessCatch": "error",
|
||||||
|
"noUselessEscapeInRegex": "error",
|
||||||
|
"noUselessTypeConstraint": "error"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"noChildrenProp": "error",
|
||||||
|
"noConstAssign": "error",
|
||||||
|
"noConstantCondition": "error",
|
||||||
|
"noEmptyCharacterClassInRegex": "error",
|
||||||
|
"noEmptyPattern": "error",
|
||||||
|
"noGlobalObjectCalls": "error",
|
||||||
|
"noInnerDeclarations": "error",
|
||||||
|
"noInvalidConstructorSuper": "error",
|
||||||
|
"noNonoctalDecimalEscape": "error",
|
||||||
|
"noPrecisionLoss": "error",
|
||||||
|
"noSelfAssign": "error",
|
||||||
|
"noSetterReturn": "error",
|
||||||
|
"noSwitchDeclarations": "error",
|
||||||
|
"noUndeclaredVariables": "error",
|
||||||
|
"noUnreachable": "error",
|
||||||
|
"noUnreachableSuper": "error",
|
||||||
|
"noUnsafeFinally": "error",
|
||||||
|
"noUnsafeOptionalChaining": "error",
|
||||||
|
"noUnusedLabels": "error",
|
||||||
|
"noUnusedVariables": "error",
|
||||||
|
"useIsNan": "error",
|
||||||
|
"useJsxKeyInIterable": "error",
|
||||||
|
"useValidForDirection": "error",
|
||||||
|
"useValidTypeof": "error",
|
||||||
|
"useYield": "error"
|
||||||
|
},
|
||||||
|
"security": { "noDangerouslySetInnerHtmlWithChildren": "error" },
|
||||||
|
"style": {
|
||||||
|
"noInferrableTypes": "error",
|
||||||
|
"noNamespace": "error",
|
||||||
|
"noNonNullAssertion": "warn",
|
||||||
|
"useArrayLiterals": "error",
|
||||||
|
"useAsConstAssertion": "error"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noAsyncPromiseExecutor": "error",
|
||||||
|
"noCatchAssign": "error",
|
||||||
|
"noClassAssign": "error",
|
||||||
|
"noCommentText": "error",
|
||||||
|
"noCompareNegZero": "error",
|
||||||
|
"noControlCharactersInRegex": "error",
|
||||||
|
"noDebugger": "error",
|
||||||
|
"noDuplicateCase": "error",
|
||||||
|
"noDuplicateClassMembers": "error",
|
||||||
|
"noDuplicateElseIf": "error",
|
||||||
|
"noDuplicateJsxProps": "error",
|
||||||
|
"noDuplicateObjectKeys": "error",
|
||||||
|
"noDuplicateParameters": "error",
|
||||||
|
"noEmptyBlockStatements": "error",
|
||||||
|
"noExplicitAny": "warn",
|
||||||
|
"noExtraNonNullAssertion": "error",
|
||||||
|
"noFallthroughSwitchClause": "error",
|
||||||
|
"noFunctionAssign": "error",
|
||||||
|
"noGlobalAssign": "error",
|
||||||
|
"noImportAssign": "error",
|
||||||
|
"noIrregularWhitespace": "error",
|
||||||
|
"noMisleadingCharacterClass": "error",
|
||||||
|
"noMisleadingInstantiator": "error",
|
||||||
|
"noPrototypeBuiltins": "error",
|
||||||
|
"noRedeclare": "error",
|
||||||
|
"noShadowRestrictedNames": "error",
|
||||||
|
"noSparseArray": "error",
|
||||||
|
"noUnsafeNegation": "error",
|
||||||
|
"noWith": "error",
|
||||||
|
"useAdjacentOverloadSignatures": "error",
|
||||||
|
"useGetterReturn": "error",
|
||||||
|
"useNamespaceKeyword": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": { "quoteStyle": "double" },
|
||||||
|
"globals": [
|
||||||
|
"expect",
|
||||||
|
"it",
|
||||||
|
"describe",
|
||||||
|
"beforeEach",
|
||||||
|
"afterEach",
|
||||||
|
"test",
|
||||||
|
"jest",
|
||||||
|
"acquireVsCodeApi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"includes": ["*.ts", "*.tsx", "*.mts", "*.cts"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"complexity": { "noArguments": "error" },
|
||||||
|
"correctness": {
|
||||||
|
"noConstAssign": "off",
|
||||||
|
"noGlobalObjectCalls": "off",
|
||||||
|
"noInvalidConstructorSuper": "off",
|
||||||
|
"noSetterReturn": "off",
|
||||||
|
"noUndeclaredVariables": "off",
|
||||||
|
"noUnreachable": "off",
|
||||||
|
"noUnreachableSuper": "off",
|
||||||
|
"useValidTypeof": "off"
|
||||||
|
},
|
||||||
|
"style": { "useConst": "error" },
|
||||||
|
"suspicious": {
|
||||||
|
"noDuplicateClassMembers": "off",
|
||||||
|
"noDuplicateObjectKeys": "off",
|
||||||
|
"noDuplicateParameters": "off",
|
||||||
|
"noFunctionAssign": "off",
|
||||||
|
"noImportAssign": "off",
|
||||||
|
"noRedeclare": "off",
|
||||||
|
"noUnsafeNegation": "off",
|
||||||
|
"noVar": "error",
|
||||||
|
"useGetterReturn": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": { "source": { "organizeImports": "on" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@nand2tetris/cli",
|
||||||
|
"description": "NAND2Tetris Command Line tools",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/nand2tetris/web-ide.git"
|
||||||
|
},
|
||||||
|
"author": "David Souther <davidsouther@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/nand2tetris/web-ide/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/nand2tetris/web-ide",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"nand2tetris": "dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@davidsouther/jiffies": "^2.2.5",
|
||||||
|
"@nand2tetris/runner": "file:../runner",
|
||||||
|
"@nand2tetris/simulator": "file:../simulator"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^20.14.2",
|
||||||
|
"@types/yargs": "^17.0.32",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { NodeFileSystemAdapter } from "@davidsouther/jiffies/lib/esm/fs_node.js";
|
||||||
|
import type { Assignment } from "@nand2tetris/projects/base.js";
|
||||||
|
import { Assignments } from "@nand2tetris/projects/full.js";
|
||||||
|
import { JavaRunner } from "@nand2tetris/runner/index.js";
|
||||||
|
import {
|
||||||
|
AssignmentFiles,
|
||||||
|
hasTest,
|
||||||
|
runTests,
|
||||||
|
} from "@nand2tetris/simulator/projects/runner.js";
|
||||||
|
import { join, parse } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a FileSystem wrapper, curry a function that loads the necessary files for running an HDL test.
|
||||||
|
* For grading, tests come from the built-in assignment master test list.
|
||||||
|
*/
|
||||||
|
const loadAssignment = (fs: FileSystem) =>
|
||||||
|
async function (file: Assignment): Promise<AssignmentFiles> {
|
||||||
|
const hdl = await fs.readFile(file.base);
|
||||||
|
const tst = Assignments[
|
||||||
|
`${file.name}.tst` as keyof typeof Assignments
|
||||||
|
] as string;
|
||||||
|
const cmp = Assignments[
|
||||||
|
`${file.name}.cmp` as keyof typeof Assignments
|
||||||
|
] as string;
|
||||||
|
return { ...file, hdl, tst, cmp };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the grader using a NodeJS file system.
|
||||||
|
*
|
||||||
|
* Report results using a simple `{Name} passed/failed`, and if given a java_id, the same for shadow mode results.
|
||||||
|
*
|
||||||
|
* Returns 1 if at least one test was failed or no tests were found to run. Returns 0 otherwise.
|
||||||
|
*/
|
||||||
|
export async function main(folder = process.cwd(), java_ide = "") {
|
||||||
|
const fs = new FileSystem(new NodeFileSystemAdapter());
|
||||||
|
fs.cd(folder);
|
||||||
|
|
||||||
|
const directory = [...(await fs.readdir("."))];
|
||||||
|
const runFiles = directory.filter((file) => file.endsWith(".hdl"));
|
||||||
|
|
||||||
|
const files = runFiles
|
||||||
|
.map((f) => join(folder, f))
|
||||||
|
.map(parse)
|
||||||
|
.filter(hasTest);
|
||||||
|
|
||||||
|
const ideRunner = await JavaRunner.try_init(java_ide);
|
||||||
|
const tests = await runTests(files, loadAssignment(fs), fs, ideRunner);
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
console.log("No tests have run!");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let failsCount = 0;
|
||||||
|
for (const test of tests) {
|
||||||
|
if (!test.pass) {
|
||||||
|
failsCount++;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Test ${test.name}: ${test.pass ? `Passed` : `Failed (${test.out})`}`,
|
||||||
|
);
|
||||||
|
if (test.shadow) {
|
||||||
|
if (test.shadow.code !== 0) {
|
||||||
|
failsCount++;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`\tShadow: ${
|
||||||
|
test.shadow.code === 0
|
||||||
|
? `Passed`
|
||||||
|
: `Errored (${test.shadow.stderr.trim()})`
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log("\tNo shadow");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failsCount > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { NodeFileSystemAdapter } from "@davidsouther/jiffies/lib/esm/fs_node.js";
|
||||||
|
import { compile } from "@nand2tetris/simulator/jack/compiler.js";
|
||||||
|
import * as fsCore from "fs";
|
||||||
|
import path, { dirname, parse, resolve } from "path";
|
||||||
|
import yargs from "yargs";
|
||||||
|
import { hideBin } from "yargs/helpers";
|
||||||
|
import { main } from "./grading.js";
|
||||||
|
import { testRunner } from "./testrunner.js";
|
||||||
|
|
||||||
|
yargs(hideBin(process.argv))
|
||||||
|
.usage("$0 <cmd>")
|
||||||
|
.command(
|
||||||
|
"grade [directory]",
|
||||||
|
"Grade all NAND2Tetris projects in a directory tree.",
|
||||||
|
(yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional("directory", {
|
||||||
|
type: "string",
|
||||||
|
default: process.cwd(),
|
||||||
|
describe: "Path to a folder to grade for nand2tetris projects.",
|
||||||
|
})
|
||||||
|
.option("java_ide", {
|
||||||
|
type: "string",
|
||||||
|
default: process.env.NAND2TETRIS_PATH,
|
||||||
|
describe:
|
||||||
|
"When set, look for the java IDE jars in this path and compare both runs.",
|
||||||
|
}),
|
||||||
|
async (argv) => {
|
||||||
|
console.log("grade", argv.directory, "nand2tetris grader!");
|
||||||
|
const exitCodePromise = main(argv.directory, argv.java_ide);
|
||||||
|
const exitCode = await exitCodePromise;
|
||||||
|
if (exitCode) {
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.command(
|
||||||
|
"run <file>",
|
||||||
|
"Run a NAND2Tetris file. If the file is .tst, executes the test. If the file is .hdl, starts a terminal session for the chip. If the file is .asm, .hack, .vm, or .jack, loads (possibly after compilation) the file into memory and starts the machine execution.",
|
||||||
|
(yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional("file", {
|
||||||
|
type: "string",
|
||||||
|
describe: "Path to nand2tetris tst file to execute.",
|
||||||
|
})
|
||||||
|
.option("debug", {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
describe: "Port for the debugger protocol to listen on.",
|
||||||
|
})
|
||||||
|
.option("debug_port", {
|
||||||
|
type: "number",
|
||||||
|
default: 6163,
|
||||||
|
describe: "Port for the debugger protocol to listen on.",
|
||||||
|
})
|
||||||
|
.option("java_ide", {
|
||||||
|
type: "string",
|
||||||
|
describe:
|
||||||
|
"When set, look for the java IDE jars in this path and compare both runs.",
|
||||||
|
}),
|
||||||
|
(argv) => {
|
||||||
|
console.log("nand2tetris command run", argv);
|
||||||
|
const { name, ext } = parse(argv.file ?? "");
|
||||||
|
switch (ext) {
|
||||||
|
case "":
|
||||||
|
case ".tst":
|
||||||
|
console.log("tst");
|
||||||
|
testRunner(dirname(resolve(argv.file ?? process.cwd())), name);
|
||||||
|
break;
|
||||||
|
case ".hdl":
|
||||||
|
console.log("hdl");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("unknown", ext);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.command(
|
||||||
|
"compile <src> [dst]",
|
||||||
|
"Compile .jack files inside a folder",
|
||||||
|
(yargs) =>
|
||||||
|
yargs
|
||||||
|
.positional("src", {
|
||||||
|
type: "string",
|
||||||
|
describe: "Path to input folder with jack files",
|
||||||
|
})
|
||||||
|
.option("dst", {
|
||||||
|
type: "string",
|
||||||
|
describe: "Path to destination folder",
|
||||||
|
default: "",
|
||||||
|
})
|
||||||
|
.coerce(["src", "dst"], function (arg) {
|
||||||
|
return path.resolve(arg) + "/";
|
||||||
|
})
|
||||||
|
.check((argv, options) => {
|
||||||
|
const src = argv.src;
|
||||||
|
const dst = argv.dst;
|
||||||
|
if (src === undefined) {
|
||||||
|
throw Error("Please provide input folder path");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dst && !fsCore.lstatSync(dst).isDirectory()) {
|
||||||
|
throw Error(src + " is not a folder");
|
||||||
|
}
|
||||||
|
if (!fsCore.lstatSync(src).isDirectory()) {
|
||||||
|
throw Error(src + " is not a folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.showHelpOnFail(false, "Specify --help for available options"),
|
||||||
|
async (argv) => {
|
||||||
|
enum Colors {
|
||||||
|
Red = "\x1b[31m",
|
||||||
|
Green = "\x1b[32m",
|
||||||
|
Reset = "\u001b[0m",
|
||||||
|
}
|
||||||
|
const JACK_EXT = ".jack";
|
||||||
|
const src = argv.src;
|
||||||
|
const dst = argv.dst ?? src;
|
||||||
|
if (src === undefined) {
|
||||||
|
throw Error("Please provde input folder path");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dst === undefined) {
|
||||||
|
throw Error("Please provde input folder path");
|
||||||
|
}
|
||||||
|
const fs = new FileSystem(new NodeFileSystemAdapter());
|
||||||
|
|
||||||
|
const files = await fs.readdir(src);
|
||||||
|
const jackFiles = files.filter((file) => file.endsWith(JACK_EXT));
|
||||||
|
if (jackFiles.length === 0) {
|
||||||
|
throw Error("No jack files inside a folder");
|
||||||
|
}
|
||||||
|
const nameToContent = {} as Record<string, string>;
|
||||||
|
for (const file of jackFiles) {
|
||||||
|
const filepath = path.join(src, file);
|
||||||
|
const content = await fs.readFile(filepath);
|
||||||
|
nameToContent[file.replace(JACK_EXT, "")] = content;
|
||||||
|
}
|
||||||
|
let error = false;
|
||||||
|
for (const [name, compiled] of Object.entries(compile(nameToContent))) {
|
||||||
|
if (typeof compiled === "string") {
|
||||||
|
const outputFilename = name + ".vm";
|
||||||
|
const outpath = path.join(dst, outputFilename);
|
||||||
|
await fs.writeFile(outpath, compiled);
|
||||||
|
} else {
|
||||||
|
if (!error) {
|
||||||
|
console.error("Compilation failed\n");
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
Colors.Red +
|
||||||
|
compiled.message.replace(
|
||||||
|
/Line\s([\d]+):/g,
|
||||||
|
name + ".jack:$1" + Colors.Reset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log(Colors.Green + "Compiled files" + Colors.Reset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.help()
|
||||||
|
.demandCommand(1)
|
||||||
|
.parse();
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { NodeFileSystemAdapter } from "@davidsouther/jiffies/lib/esm/fs_node.js";
|
||||||
|
import type { Assignment } from "@nand2tetris/projects/base.js";
|
||||||
|
import { Assignments } from "@nand2tetris/projects/full.js";
|
||||||
|
import { runner } from "@nand2tetris/simulator/projects/runner.js";
|
||||||
|
import { parse } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load an assignment from the local folder.
|
||||||
|
* Uses built in assignments when the local tests are missing.
|
||||||
|
*/
|
||||||
|
async function loadAssignment(fs: FileSystem, file: Assignment) {
|
||||||
|
const assignment = Assignments[file.name as keyof typeof Assignments];
|
||||||
|
const hdl = await fs.readFile(`${file.name}.hdl`);
|
||||||
|
const tst = await fs
|
||||||
|
.readFile(`${file.name}.tst`)
|
||||||
|
.catch(
|
||||||
|
() => assignment[`${file.name}.tst` as keyof typeof assignment] as string,
|
||||||
|
);
|
||||||
|
const cmp = await fs
|
||||||
|
.readFile(`${file.name}.cmp`)
|
||||||
|
.catch(
|
||||||
|
() => assignment[`${file.name}.cmp` as keyof typeof assignment] as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...file, hdl, tst, cmp };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a nand2tetris.tst file.
|
||||||
|
*/
|
||||||
|
export async function testRunner(dir: string, file: string) {
|
||||||
|
const fs = new FileSystem(new NodeFileSystemAdapter());
|
||||||
|
fs.cd(dir);
|
||||||
|
const assignment = await loadAssignment(fs, parse(file));
|
||||||
|
const tryRun = runner(fs);
|
||||||
|
const run = await tryRun(assignment);
|
||||||
|
console.log(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export async function testDebugger(root: string, name: string, port: number) {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "@nand2tetris/components",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"author": "David Souther <davidsouther@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"homepage": "https://davidsouther.github.io/nand2tetris",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./*": "./build/*"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"build/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vscode/webview-ui-toolkit": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@davidsouther/jiffies": "^2.2.5",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@nand2tetris/projects": "file:../projects",
|
||||||
|
"@nand2tetris/simulator": "file:../simulator",
|
||||||
|
"@testing-library/jest-dom": "^6.4.5",
|
||||||
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@types/error-cause": "^1.0.4",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.14.2",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/vscode": "^1.89.0",
|
||||||
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"make-plural": "^7.4.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.23.1",
|
||||||
|
"react-scripts": "^5.0.1",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sass": "^1.77.4",
|
||||||
|
"source-map-explorer": "^2.5.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"postbuild": "shx rm -rf build/public && shx cp -r src/public/ build/public/",
|
||||||
|
"test": "react-scripts test"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@nand2tetris/([^/]+)/(.*)": "<rootDir>/../node_modules/@nand2tetris/$1/build/$2",
|
||||||
|
"(.*)\\.js$": "$1"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!@davidsouther)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
COMMANDS_ALU,
|
||||||
|
COMMANDS_OP,
|
||||||
|
Flags,
|
||||||
|
} from "@nand2tetris/simulator/cpu/alu.js";
|
||||||
|
|
||||||
|
export const ALUComponent = ({
|
||||||
|
A,
|
||||||
|
op,
|
||||||
|
D,
|
||||||
|
out,
|
||||||
|
flag,
|
||||||
|
}: {
|
||||||
|
A: number;
|
||||||
|
op: COMMANDS_OP;
|
||||||
|
D: number;
|
||||||
|
out: number;
|
||||||
|
flag: keyof typeof Flags;
|
||||||
|
}) => (
|
||||||
|
<div className="alu">
|
||||||
|
<span>ALU</span>
|
||||||
|
<svg width="250" height="250" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<rect
|
||||||
|
x="34.442518"
|
||||||
|
y="54.335354"
|
||||||
|
width="0.91770717"
|
||||||
|
height="20.780869"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="70,10 180,85 180,165 70,240 70,135 90,125 70,115"
|
||||||
|
stroke="#000"
|
||||||
|
fill="#6D97AB"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
xmlSpace="preserve"
|
||||||
|
textAnchor="middle"
|
||||||
|
y="61"
|
||||||
|
x="35"
|
||||||
|
// fill="#000000" // use style from chip.scss
|
||||||
|
>
|
||||||
|
{A}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
xmlSpace="preserve"
|
||||||
|
textAnchor="middle"
|
||||||
|
y="176"
|
||||||
|
x="35"
|
||||||
|
// fill="#000000" // use style from chip.scss
|
||||||
|
>
|
||||||
|
{D}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
xmlSpace="preserve"
|
||||||
|
textAnchor="middle"
|
||||||
|
y="121"
|
||||||
|
x="207"
|
||||||
|
// fill="#000000" // use style from chip.scss
|
||||||
|
>
|
||||||
|
{out}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
xmlSpace="preserve"
|
||||||
|
y="130.50002"
|
||||||
|
x="110.393929"
|
||||||
|
fill="#ffffff"
|
||||||
|
fontSize={24}
|
||||||
|
>
|
||||||
|
{COMMANDS_ALU.op[op] ?? "(??)"}
|
||||||
|
</text>
|
||||||
|
<g>
|
||||||
|
<path /*stroke="black"*/ d="M 6,67.52217 H 68.675994" />
|
||||||
|
<path
|
||||||
|
/*stroke="black"*/ d="M 68.479388,67.746136 60.290279,61.90711"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
/*stroke="black"*/ d="m 68.479388,67.40711 -8.189109,5.839026"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,115.5)">
|
||||||
|
<path /*stroke="black"*/ d="M 6,67.52217 H 68.675994" />
|
||||||
|
<path
|
||||||
|
d="M 68.479388,67.746136 60.290279,61.90711" /*stroke="black"*/
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
/*stroke="black"*/ d="m 68.479388,67.40711 -8.189109,5.839026"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(176,57.5)">
|
||||||
|
<path /*stroke="black"*/ d="M 6,67.52217 H 68.675994" />
|
||||||
|
<path
|
||||||
|
/*stroke="black"*/ d="M 68.479388,67.746136 60.290279,61.90711"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
/*stroke="black"*/ d="m 68.479388,67.40711 -8.189109,5.839026"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { KeyboardAdapter } from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { RegisterComponent } from "./register.js";
|
||||||
|
|
||||||
|
const KeyMap: Record<string, number | undefined> = {
|
||||||
|
// Delete: 127,
|
||||||
|
Enter: 128,
|
||||||
|
Backspace: 129,
|
||||||
|
ArrowLeft: 130,
|
||||||
|
ArrowUp: 131,
|
||||||
|
ArrowRight: 132,
|
||||||
|
ArrowDown: 133,
|
||||||
|
Home: 134,
|
||||||
|
End: 135,
|
||||||
|
PageUp: 136,
|
||||||
|
PageDown: 137,
|
||||||
|
Insert: 138,
|
||||||
|
Delete: 139,
|
||||||
|
Escape: 140,
|
||||||
|
F1: 141,
|
||||||
|
F2: 142,
|
||||||
|
F3: 143,
|
||||||
|
F4: 144,
|
||||||
|
F5: 145,
|
||||||
|
F6: 146,
|
||||||
|
F7: 147,
|
||||||
|
F8: 148,
|
||||||
|
F9: 149,
|
||||||
|
F10: 150,
|
||||||
|
F11: 151,
|
||||||
|
F12: 152,
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyDisplays: Record<string, string> = {
|
||||||
|
ArrowLeft: "L-arrow",
|
||||||
|
ArrowUp: "U-arrow",
|
||||||
|
ArrowRight: "R-arrow",
|
||||||
|
ArrowDown: "D-arrow",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getKeyDisplay(key: string) {
|
||||||
|
return keyDisplays[key] ?? key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyPressToHackCharacter(keypress: KeyboardEvent): number {
|
||||||
|
const mapping = KeyMap[keypress.key];
|
||||||
|
if (mapping !== undefined) {
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
if (keypress.key.length === 1) {
|
||||||
|
const code = keypress.key.charCodeAt(0);
|
||||||
|
if (code >= 32 && code <= 126) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Keyboard = ({
|
||||||
|
keyboard,
|
||||||
|
update,
|
||||||
|
}: {
|
||||||
|
keyboard: KeyboardAdapter;
|
||||||
|
update?: () => void;
|
||||||
|
}) => {
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [character, setCharacter] = useState("");
|
||||||
|
const [bits, setBits] = useState(keyboard.getKey());
|
||||||
|
let currentKey = 0;
|
||||||
|
|
||||||
|
const toggleRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const toggleEnabled = () => {
|
||||||
|
setEnabled(!enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCharacter(getKeyDisplay(event.key));
|
||||||
|
toggleRef.current?.blur();
|
||||||
|
const key = keyPressToHackCharacter(event);
|
||||||
|
if (key) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
if (key === currentKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setKey(key);
|
||||||
|
update?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
toggleRef.current?.blur();
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyboard.getKey()) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentKey = 0;
|
||||||
|
keyboard.clearKey();
|
||||||
|
update?.();
|
||||||
|
setBits(keyboard.getKey());
|
||||||
|
setCharacter("");
|
||||||
|
};
|
||||||
|
|
||||||
|
// note on setCharacter vs setKey:
|
||||||
|
// setCharacter sets the string value that will be displayed in the component,
|
||||||
|
// while setKey actually sets and tracks the value that will be stored in the keyboard memory
|
||||||
|
|
||||||
|
const setKey = (key: number) => {
|
||||||
|
if (key === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keyboard.setKey(key);
|
||||||
|
setBits(keyboard.getKey());
|
||||||
|
currentKey = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
window.addEventListener("keyup", onKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
window.removeEventListener("keyup", onKeyUp);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="panel">
|
||||||
|
<div className="flex row align-baseline">
|
||||||
|
<button
|
||||||
|
onClick={toggleEnabled}
|
||||||
|
ref={toggleRef}
|
||||||
|
className="flex-0"
|
||||||
|
style={{ whiteSpace: "pre" }}
|
||||||
|
>
|
||||||
|
{`${enabled ? "Disable" : "Enable"} Keyboard`}
|
||||||
|
</button>
|
||||||
|
<div className="flex-1"></div> {/* padding */}
|
||||||
|
<div className="flex-4">Key: {character}</div>
|
||||||
|
<div className="flex-4">
|
||||||
|
<RegisterComponent name="Char code" bits={bits} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Memory as MemoryChip } from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { MemoryBlock, MemoryCell } from "./memory.js";
|
||||||
|
|
||||||
|
describe("<Memory />", () => {
|
||||||
|
describe("<MemoryCell />", () => {
|
||||||
|
it("renders a read-only cell", () => {
|
||||||
|
render(<MemoryCell index={16} value={"34"} />);
|
||||||
|
|
||||||
|
const addr = screen.getByText("16");
|
||||||
|
expect(addr).toBeVisible();
|
||||||
|
|
||||||
|
const cell = screen.getByText("34");
|
||||||
|
expect(cell).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("<MemoryBlock />", () => {
|
||||||
|
it.skip("renders a small amount of memory", () => {
|
||||||
|
const memory = new MemoryChip(
|
||||||
|
new Int16Array(
|
||||||
|
range(0, 16).map((i) => (Math.pow(i, 12) ^ 0x9753) & 0xffff)
|
||||||
|
).buffer
|
||||||
|
);
|
||||||
|
render(<MemoryBlock memory={memory} />);
|
||||||
|
|
||||||
|
const zero = screen.getByText("0x0000");
|
||||||
|
expect(zero).toBeVisible();
|
||||||
|
|
||||||
|
// const indexes = document.querySelectorAll("code:nth-of-type(even)");
|
||||||
|
// expect(indexes.length).toBe(16);
|
||||||
|
|
||||||
|
// const cells = document.querySelectorAll("code:nth-of-type(even)");
|
||||||
|
// expect(cells.length).toBe(16);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import { rounded } from "@davidsouther/jiffies/lib/esm/dom/css/border.js";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Format,
|
||||||
|
FORMATS,
|
||||||
|
MemoryAdapter,
|
||||||
|
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import { loadAsm, loadBlob, loadHack } from "@nand2tetris/simulator/loader.js";
|
||||||
|
import { asm } from "@nand2tetris/simulator/util/asm.js";
|
||||||
|
import { bin, dec, hex } from "@nand2tetris/simulator/util/twos.js";
|
||||||
|
|
||||||
|
import { useClockReset } from "../clockface.js";
|
||||||
|
import InlineEdit from "../inline_edit.js";
|
||||||
|
import { LOADING } from "../messages.js";
|
||||||
|
import { useStateInitializer } from "../react.js";
|
||||||
|
import { BaseContext } from "../stores/base.context.js";
|
||||||
|
import VirtualScroll, { VirtualScrollSettings } from "../virtual_scroll.js";
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 34;
|
||||||
|
|
||||||
|
export const MemoryBlock = ({
|
||||||
|
memory,
|
||||||
|
jmp = { value: 0 },
|
||||||
|
highlight = -1,
|
||||||
|
editable = false,
|
||||||
|
justifyLeft = false, // TODO: handle this in css in the future
|
||||||
|
count,
|
||||||
|
maxSize,
|
||||||
|
offset = 0,
|
||||||
|
cellLabels,
|
||||||
|
format = dec,
|
||||||
|
onChange = () => undefined,
|
||||||
|
onFocus = () => undefined,
|
||||||
|
}: {
|
||||||
|
jmp?: { value: number };
|
||||||
|
memory: MemoryAdapter;
|
||||||
|
highlight?: number;
|
||||||
|
editable?: boolean;
|
||||||
|
justifyLeft?: boolean;
|
||||||
|
count?: number;
|
||||||
|
offset?: number;
|
||||||
|
maxSize?: number;
|
||||||
|
cellLabels?: string[];
|
||||||
|
format?: (v: number) => string;
|
||||||
|
onChange?: (i: number, value: string, previous: number) => void;
|
||||||
|
onFocus?: (i: number) => void;
|
||||||
|
}) => {
|
||||||
|
const settings = useMemo<Partial<VirtualScrollSettings>>(
|
||||||
|
() => ({
|
||||||
|
count: Math.min(memory.size, count ?? 25),
|
||||||
|
maxIndex: maxSize ?? memory.size,
|
||||||
|
itemHeight: ITEM_HEIGHT,
|
||||||
|
startIndex: jmp.value,
|
||||||
|
}),
|
||||||
|
[memory.size, jmp],
|
||||||
|
);
|
||||||
|
const get = useCallback(
|
||||||
|
(pos: number, count: number): [number, number][] =>
|
||||||
|
memory
|
||||||
|
.range(pos + offset, pos + offset + count)
|
||||||
|
.map((v, i) => [i + pos + offset, v]),
|
||||||
|
[memory],
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = useCallback(
|
||||||
|
([i, v]: [number, number]) => (
|
||||||
|
<MemoryCell
|
||||||
|
index={i}
|
||||||
|
value={format(v)}
|
||||||
|
label={(cellLabels?.[i] ?? "").padStart(
|
||||||
|
cellLabels ? Math.max(...cellLabels.map((label) => label.length)) : 0,
|
||||||
|
)}
|
||||||
|
showLabel={cellLabels != undefined}
|
||||||
|
size={memory.size}
|
||||||
|
editable={editable}
|
||||||
|
justifyLeft={justifyLeft}
|
||||||
|
highlight={i === highlight}
|
||||||
|
onChange={onChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[format, editable, highlight, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualScroll<[number, number], ReactNode>
|
||||||
|
settings={settings}
|
||||||
|
get={get}
|
||||||
|
row={row}
|
||||||
|
rowKey={([i]) => i}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MemoryCell = ({
|
||||||
|
index,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
showLabel = false,
|
||||||
|
size,
|
||||||
|
highlight = false,
|
||||||
|
editable = false,
|
||||||
|
justifyLeft = false,
|
||||||
|
onChange = () => undefined,
|
||||||
|
onFocus = () => undefined,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
showLabel?: boolean;
|
||||||
|
size?: number;
|
||||||
|
highlight?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
justifyLeft?: boolean;
|
||||||
|
onChange?: (i: number, value: string, previous: number) => void;
|
||||||
|
onFocus?: (i: number) => void;
|
||||||
|
}) => (
|
||||||
|
<div style={{ display: "flex", height: "100%" }}>
|
||||||
|
{showLabel && (
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
...rounded("none"),
|
||||||
|
...(highlight ? { background: "var(--mark-background-color)" } : {}),
|
||||||
|
whiteSpace: "pre",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label ?? ""}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
...rounded("none"),
|
||||||
|
...(highlight ? { background: "var(--mark-background-color)" } : {}),
|
||||||
|
whiteSpace: "pre",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{size
|
||||||
|
? dec(index).padStart(Math.ceil(Math.log10(size)), " ")
|
||||||
|
: dec(index)}
|
||||||
|
</code>
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
flex: "1",
|
||||||
|
textAlign: justifyLeft ? "left" : "right",
|
||||||
|
color: "var(--text-color)",
|
||||||
|
...rounded("none"),
|
||||||
|
...(highlight ? { background: "var(--mark-background-color)" } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{editable ? (
|
||||||
|
<InlineEdit
|
||||||
|
value={value}
|
||||||
|
highlight={highlight}
|
||||||
|
onChange={(newValue: string) =>
|
||||||
|
onChange(index, newValue, Number(value))
|
||||||
|
}
|
||||||
|
onFocus={() => onFocus(index)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: "var(--text-color)" }}>{value}</span>
|
||||||
|
)}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Memory = forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
name = "Memory",
|
||||||
|
className,
|
||||||
|
displayEnabled = true,
|
||||||
|
highlight = -1,
|
||||||
|
editable = true,
|
||||||
|
memory,
|
||||||
|
format = "dec",
|
||||||
|
onSetFormat,
|
||||||
|
excludedFormats = [],
|
||||||
|
count,
|
||||||
|
maxSize,
|
||||||
|
offset,
|
||||||
|
initialAddr,
|
||||||
|
cellLabels,
|
||||||
|
fileSelect,
|
||||||
|
showClear = true,
|
||||||
|
onChange = undefined,
|
||||||
|
onClear = undefined,
|
||||||
|
loadTooltip = undefined,
|
||||||
|
}: {
|
||||||
|
name?: string;
|
||||||
|
className?: string;
|
||||||
|
displayEnabled?: boolean;
|
||||||
|
editable?: boolean;
|
||||||
|
highlight?: number;
|
||||||
|
memory: MemoryAdapter;
|
||||||
|
count?: number;
|
||||||
|
maxSize?: number;
|
||||||
|
offset?: number;
|
||||||
|
initialAddr?: number;
|
||||||
|
format: Format;
|
||||||
|
onSetFormat?: (format: Format) => void;
|
||||||
|
excludedFormats?: Format[];
|
||||||
|
cellLabels?: string[];
|
||||||
|
fileSelect?: () => Promise<{ name: string; content: string }>;
|
||||||
|
showClear?: boolean;
|
||||||
|
onChange?: () => void;
|
||||||
|
onClear?: () => void;
|
||||||
|
loadTooltip?: { value: string; placement: string };
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const [fmt, setFormat] = useStateInitializer(format);
|
||||||
|
const [jmp, setJmp] = useState("");
|
||||||
|
const [goto, setGoto] = useState({ value: initialAddr ?? 0 });
|
||||||
|
const [highlighted, setHighlighted] = useStateInitializer(highlight);
|
||||||
|
const [renderKey, setRenderKey] = useState(0);
|
||||||
|
|
||||||
|
const jumpTo = () => {
|
||||||
|
const value =
|
||||||
|
!isNaN(parseInt(jmp)) && isFinite(parseInt(jmp)) ? Number(jmp) : 0;
|
||||||
|
setHighlighted(value);
|
||||||
|
setGoto({
|
||||||
|
value: value,
|
||||||
|
});
|
||||||
|
rerenderMemoryBlock();
|
||||||
|
};
|
||||||
|
|
||||||
|
const doLoad = async () => {
|
||||||
|
onChange?.();
|
||||||
|
if (fileSelect) {
|
||||||
|
const { name, content } = await fileSelect();
|
||||||
|
setStatus(LOADING);
|
||||||
|
requestAnimationFrame(async () => {
|
||||||
|
const loader = name.endsWith("hack")
|
||||||
|
? loadHack
|
||||||
|
: name.endsWith("asm")
|
||||||
|
? loadAsm
|
||||||
|
: loadBlob;
|
||||||
|
requestAnimationFrame(async () => {
|
||||||
|
try {
|
||||||
|
const bytes = await loader(content);
|
||||||
|
memory.loadBytes(bytes);
|
||||||
|
setStatus("");
|
||||||
|
setFormat(
|
||||||
|
name.endsWith("hack")
|
||||||
|
? "bin"
|
||||||
|
: name.endsWith("asm")
|
||||||
|
? "asm"
|
||||||
|
: fmt,
|
||||||
|
);
|
||||||
|
jumpTo();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: `Error loading memory: ${(e as Error).message}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setStatus } = useContext(BaseContext);
|
||||||
|
|
||||||
|
const rerenderMemoryBlock = () => {
|
||||||
|
setRenderKey(renderKey + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
rerender: rerenderMemoryBlock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
memory.reset();
|
||||||
|
onChange?.();
|
||||||
|
onClear?.();
|
||||||
|
rerenderMemoryBlock();
|
||||||
|
};
|
||||||
|
|
||||||
|
const doUpdate = (i: number, v: string) => {
|
||||||
|
memory.update(i, v, fmt ?? "dec");
|
||||||
|
onChange?.();
|
||||||
|
rerenderMemoryBlock();
|
||||||
|
};
|
||||||
|
|
||||||
|
useClockReset(() => {
|
||||||
|
setJmp("");
|
||||||
|
setGoto({ value: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
const doSetFormat = (format: Format) => {
|
||||||
|
setFormat(format);
|
||||||
|
onSetFormat?.(format);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={`panel memory ${className ?? name}`}>
|
||||||
|
<header>
|
||||||
|
<div style={{ whiteSpace: "nowrap" }}>{name}</div>
|
||||||
|
<fieldset role="group">
|
||||||
|
{fileSelect && (
|
||||||
|
<button
|
||||||
|
onClick={doLoad}
|
||||||
|
className="flex-0"
|
||||||
|
data-tooltip={loadTooltip?.value ?? "Load file"}
|
||||||
|
data-placement={loadTooltip?.placement ?? "bottom"}
|
||||||
|
>
|
||||||
|
{/* <Icon name="upload_file" /> */}
|
||||||
|
📂
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{showClear && (
|
||||||
|
<button
|
||||||
|
onClick={clear}
|
||||||
|
className="flex-0"
|
||||||
|
data-tooltip={"Clear"}
|
||||||
|
data-placement="bottom"
|
||||||
|
>
|
||||||
|
{/* <Icon name="upload_file" /> */}
|
||||||
|
🆑
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
style={{ width: "4em", height: "100%" }}
|
||||||
|
placeholder="Addr"
|
||||||
|
value={jmp}
|
||||||
|
onKeyDown={({ key }) => key === "Enter" && jumpTo()}
|
||||||
|
onChange={({ target: { value } }) => setJmp(value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={jumpTo}
|
||||||
|
className="flex-0"
|
||||||
|
data-tooltip={"Scroll to address"}
|
||||||
|
data-placement="bottom"
|
||||||
|
>
|
||||||
|
{/* <Icon name="move_down" /> */}
|
||||||
|
⤵️
|
||||||
|
</button>
|
||||||
|
<select value={fmt} onChange={(e) => doSetFormat(e.target.value)}>
|
||||||
|
{FORMATS.filter(
|
||||||
|
(option) => !excludedFormats.includes(option),
|
||||||
|
).map((option) => (
|
||||||
|
<option key={option}>{option}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</fieldset>
|
||||||
|
</header>
|
||||||
|
{displayEnabled ? (
|
||||||
|
<MemoryBlock
|
||||||
|
key={renderKey}
|
||||||
|
jmp={goto}
|
||||||
|
memory={memory}
|
||||||
|
highlight={highlighted}
|
||||||
|
editable={editable}
|
||||||
|
justifyLeft={fmt == "asm"}
|
||||||
|
count={count}
|
||||||
|
format={(v: number) => doFormat(fmt, v)}
|
||||||
|
cellLabels={cellLabels}
|
||||||
|
maxSize={maxSize}
|
||||||
|
offset={offset}
|
||||||
|
onChange={doUpdate}
|
||||||
|
onFocus={(i) => setHighlighted(i)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"Memory display is disabled"
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Memory.displayName = "Memory";
|
||||||
|
|
||||||
|
export default Memory;
|
||||||
|
|
||||||
|
function doFormat(format: Format, v: number): string {
|
||||||
|
switch (format) {
|
||||||
|
case "bin":
|
||||||
|
return bin(v);
|
||||||
|
case "hex":
|
||||||
|
return hex(v);
|
||||||
|
case "asm":
|
||||||
|
return asm(v);
|
||||||
|
case "dec":
|
||||||
|
default:
|
||||||
|
return dec(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { dec } from "@nand2tetris/simulator/util/twos.js";
|
||||||
|
|
||||||
|
export const RegisterComponent = ({
|
||||||
|
name,
|
||||||
|
bits,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
bits: number;
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
{name}: {dec(bits)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||||
|
import { Memory } from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { useClockFrame, useClockReset } from "../clockface.js";
|
||||||
|
|
||||||
|
const WHITE = "white";
|
||||||
|
const BLACK = "black";
|
||||||
|
type COLOR = typeof WHITE | typeof BLACK;
|
||||||
|
|
||||||
|
export interface ScreenMemory {
|
||||||
|
get(idx: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reduceScreen(memory: Memory, offset = 0): ScreenMemory {
|
||||||
|
return {
|
||||||
|
get(idx: number): number {
|
||||||
|
return memory.get(offset + idx);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(mem: ScreenMemory, x: number, y: number): COLOR {
|
||||||
|
const byte = mem.get(32 * y + ((x / 16) | 0));
|
||||||
|
const bit = byte & (1 << x % 16);
|
||||||
|
return bit === 0 ? WHITE : BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(data: Uint8ClampedArray, x: number, y: number, value: COLOR) {
|
||||||
|
const pixel = (y * 512 + x) * 4;
|
||||||
|
const color = value === WHITE ? 255 : 0;
|
||||||
|
data[pixel] = color;
|
||||||
|
data[pixel + 1] = color;
|
||||||
|
data[pixel + 2] = color;
|
||||||
|
data[pixel + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawImage(ctx: CanvasRenderingContext2D, memory: ScreenMemory) {
|
||||||
|
const image = assertExists(
|
||||||
|
ctx.getImageData(0, 0, 512, 256),
|
||||||
|
"Failed to create Context2d",
|
||||||
|
);
|
||||||
|
for (let col = 0; col < 512; col++) {
|
||||||
|
for (let row = 0; row < 256; row++) {
|
||||||
|
const color = get(memory, col, row);
|
||||||
|
set(image.data, col, row, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.putImageData(image, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScreenScales = 0 | 1 | 2;
|
||||||
|
|
||||||
|
export const Screen = ({
|
||||||
|
memory,
|
||||||
|
showScaleControls = false,
|
||||||
|
scale = 1,
|
||||||
|
onScale,
|
||||||
|
}: {
|
||||||
|
memory: ScreenMemory;
|
||||||
|
showScaleControls?: boolean;
|
||||||
|
scale?: ScreenScales;
|
||||||
|
onScale?: (scale: ScreenScales) => void;
|
||||||
|
}) => {
|
||||||
|
const canvas = useRef<HTMLCanvasElement>();
|
||||||
|
const [screenScale, setScreenScale] = useState<ScreenScales>(scale);
|
||||||
|
|
||||||
|
const onScaleCB = (scale: ScreenScales) => {
|
||||||
|
onScale?.(scale);
|
||||||
|
setScreenScale(scale);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = useCallback(() => {
|
||||||
|
const ctx =
|
||||||
|
canvas.current?.getContext("2d", { willReadFrequently: true }) ??
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
drawImage(ctx, memory);
|
||||||
|
}
|
||||||
|
}, [memory]);
|
||||||
|
|
||||||
|
const ctxRef = useCallback(
|
||||||
|
(ref: HTMLCanvasElement | null) => {
|
||||||
|
canvas.current = ref ?? undefined;
|
||||||
|
draw();
|
||||||
|
},
|
||||||
|
[canvas, draw],
|
||||||
|
);
|
||||||
|
|
||||||
|
useClockFrame(draw);
|
||||||
|
useClockReset(() => {
|
||||||
|
canvas.current
|
||||||
|
?.getContext("2d")
|
||||||
|
?.clearRect(0, 0, canvas.current.width, canvas.current.height);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="panel">
|
||||||
|
<header>
|
||||||
|
<div>Screen</div>
|
||||||
|
{showScaleControls && (
|
||||||
|
<fieldset role="group">
|
||||||
|
<button
|
||||||
|
aria-current={screenScale === 0}
|
||||||
|
onClick={() => onScaleCB(0)}
|
||||||
|
>
|
||||||
|
x0
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-current={screenScale === 1}
|
||||||
|
onClick={() => onScaleCB(1)}
|
||||||
|
>
|
||||||
|
x1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-current={screenScale === 2}
|
||||||
|
onClick={() => onScaleCB(2)}
|
||||||
|
>
|
||||||
|
x2
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
{screenScale > 0 && (
|
||||||
|
<main style={{ backgroundColor: "var(--code-background-color)" }}>
|
||||||
|
<figure
|
||||||
|
style={{
|
||||||
|
width: `${512 * screenScale}px`,
|
||||||
|
height: `${256 * screenScale}px`,
|
||||||
|
boxSizing: "content-box",
|
||||||
|
marginInline: "auto",
|
||||||
|
margin: "auto",
|
||||||
|
borderTop: "2px solid gray",
|
||||||
|
borderLeft: "2px solid gray",
|
||||||
|
borderBottom: "2px solid lightgray",
|
||||||
|
borderRight: "2px solid lightgray",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={ctxRef}
|
||||||
|
width={512}
|
||||||
|
height={256}
|
||||||
|
style={{
|
||||||
|
transform: `translate(-50%, -50%) scale(${screenScale}) translate(50%, 50%)`,
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
></canvas>
|
||||||
|
</figure>
|
||||||
|
</main>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ALU } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||||
|
import { Chip } from "@nand2tetris/simulator/chip/chip.js";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { makeVisualization, makeVisualizationsWithId } from "./visualizations";
|
||||||
|
|
||||||
|
describe("visualizations", () => {
|
||||||
|
it("returns empty for chips with no parts", () => {
|
||||||
|
const chip = new Chip([], [], "test");
|
||||||
|
|
||||||
|
expect(makeVisualization(chip)).toBeUndefined();
|
||||||
|
expect(makeVisualizationsWithId({ parts: [chip] })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns vis for builtins", async () => {
|
||||||
|
const alu = new ALU();
|
||||||
|
|
||||||
|
const vis = makeVisualizationsWithId({ parts: [alu] });
|
||||||
|
expect(vis.length).toBe(1);
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
{vis.map(([k, v]) => (
|
||||||
|
<div key={k}>{v}</div>
|
||||||
|
))}
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
const rendered = await screen.findAllByText(/ALU/);
|
||||||
|
expect(rendered).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
CPU,
|
||||||
|
Computer,
|
||||||
|
Keyboard,
|
||||||
|
ROM32K,
|
||||||
|
Screen,
|
||||||
|
} from "@nand2tetris/simulator/chip/builtins/computer/computer.js";
|
||||||
|
import { ALU } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||||
|
import {
|
||||||
|
PC,
|
||||||
|
Register,
|
||||||
|
} from "@nand2tetris/simulator/chip/builtins/sequential/bit.js";
|
||||||
|
import {
|
||||||
|
RAM,
|
||||||
|
RAM8,
|
||||||
|
} from "@nand2tetris/simulator/chip/builtins/sequential/ram.js";
|
||||||
|
import { Chip, HIGH } from "@nand2tetris/simulator/chip/chip.js";
|
||||||
|
import { Flags } from "@nand2tetris/simulator/cpu/alu.js";
|
||||||
|
import { decode } from "@nand2tetris/simulator/cpu/cpu.js";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { NO_SCREEN } from "../stores/chip.store.js";
|
||||||
|
import { ALUComponent } from "./alu.js";
|
||||||
|
import { Keyboard as KeyboardComponent } from "./keyboard.js";
|
||||||
|
import { Memory as MemoryComponent } from "./memory.js";
|
||||||
|
import { RegisterComponent } from "./register.js";
|
||||||
|
import { Screen as ScreenComponent } from "./screen.js";
|
||||||
|
|
||||||
|
export function getBuiltinVisualization(part: Chip): ReactElement | undefined {
|
||||||
|
switch (part.name) {
|
||||||
|
case "Register":
|
||||||
|
case "ARegister":
|
||||||
|
case "DRegister":
|
||||||
|
case "PC":
|
||||||
|
case "KEYBOARD":
|
||||||
|
case "RAM8":
|
||||||
|
case "RAM64":
|
||||||
|
case "RAM512":
|
||||||
|
case "RAM4K":
|
||||||
|
case "RAM16K":
|
||||||
|
case "ROM32K":
|
||||||
|
case "Screen":
|
||||||
|
case "Memory":
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMemoryVisualization(chip: RAM) {
|
||||||
|
return (
|
||||||
|
<MemoryComponent
|
||||||
|
name={chip.name}
|
||||||
|
memory={chip.memory}
|
||||||
|
format={chip instanceof ROM32K ? "asm" : "dec"}
|
||||||
|
highlight={chip.address}
|
||||||
|
count={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeVisualization(
|
||||||
|
chip: Chip,
|
||||||
|
updateAction?: () => void,
|
||||||
|
parameters?: Set<string>,
|
||||||
|
): ReactElement | undefined {
|
||||||
|
if (chip instanceof ALU) {
|
||||||
|
return (
|
||||||
|
<ALUComponent
|
||||||
|
A={chip.in("x").busVoltage}
|
||||||
|
op={chip.op()}
|
||||||
|
D={chip.in("y").busVoltage}
|
||||||
|
out={chip.out().busVoltage}
|
||||||
|
flag={
|
||||||
|
(chip.out("zr").voltage() === HIGH
|
||||||
|
? Flags.Zero
|
||||||
|
: chip.out("ng").voltage() === HIGH
|
||||||
|
? Flags.Negative
|
||||||
|
: Flags.Positive) as keyof typeof Flags
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (chip instanceof Register) {
|
||||||
|
return (
|
||||||
|
<RegisterComponent
|
||||||
|
name={chip.name ?? `Chip ${chip.id}`}
|
||||||
|
bits={chip.bits}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (chip instanceof PC) {
|
||||||
|
return <RegisterComponent name="PC" bits={chip.bits} />;
|
||||||
|
}
|
||||||
|
if (chip instanceof Keyboard) {
|
||||||
|
return <KeyboardComponent keyboard={chip} update={updateAction} />;
|
||||||
|
}
|
||||||
|
if (chip instanceof Screen) {
|
||||||
|
return <ScreenComponent memory={chip.memory} />;
|
||||||
|
}
|
||||||
|
if (chip instanceof RAM) {
|
||||||
|
return makeMemoryVisualization(chip);
|
||||||
|
}
|
||||||
|
if (chip instanceof RAM8) {
|
||||||
|
return <span>RAM {chip.width}</span>;
|
||||||
|
}
|
||||||
|
if (chip instanceof CPU) {
|
||||||
|
const bits = decode(chip.in("instruction").busVoltage);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RegisterComponent name={"A"} bits={chip.state.A} />
|
||||||
|
<RegisterComponent name={"D"} bits={chip.state.D} />
|
||||||
|
<RegisterComponent name={"PC"} bits={chip.state.PC} />
|
||||||
|
<ALUComponent
|
||||||
|
A={bits.am ? chip.in("inM").busVoltage : chip.state.A}
|
||||||
|
D={chip.state.D}
|
||||||
|
out={chip.state.ALU}
|
||||||
|
op={bits.op}
|
||||||
|
flag={chip.state.flag as keyof typeof Flags}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (chip instanceof Computer) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RegisterComponent name={"A"} bits={chip.cpu.state.A} />
|
||||||
|
<RegisterComponent name={"D"} bits={chip.cpu.state.D} />
|
||||||
|
<RegisterComponent name={"PC"} bits={chip.cpu.state.PC} />
|
||||||
|
{!parameters?.has(NO_SCREEN) && (
|
||||||
|
<ScreenComponent memory={chip.ram.screen.memory} />
|
||||||
|
)}
|
||||||
|
{makeMemoryVisualization(chip.rom)}
|
||||||
|
{makeMemoryVisualization(chip.ram.ram)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vis = [...chip.parts]
|
||||||
|
.map((chip) => makeVisualization(chip, updateAction))
|
||||||
|
.filter((v) => v !== undefined);
|
||||||
|
return vis.length > 0 ? <>{vis}</> : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeVisualizationsWithId(
|
||||||
|
chip: {
|
||||||
|
parts: Chip[];
|
||||||
|
},
|
||||||
|
updateAction?: () => void,
|
||||||
|
parameters?: Set<string>,
|
||||||
|
): [string, ReactElement][] {
|
||||||
|
return [...chip.parts]
|
||||||
|
.map((part, i): [string, ReactElement | undefined] => [
|
||||||
|
`${part.id}_${i}`,
|
||||||
|
makeVisualization(part, updateAction, parameters),
|
||||||
|
])
|
||||||
|
.filter(([_, v]) => v !== undefined) as [string, ReactElement][];
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||||
|
import { Clock } from "@nand2tetris/simulator/chip/clock.js";
|
||||||
|
|
||||||
|
export function useClock(actions: {
|
||||||
|
tick?: () => void;
|
||||||
|
toggle?: () => void;
|
||||||
|
reset?: () => void;
|
||||||
|
}) {
|
||||||
|
const clock = useMemo(() => Clock.get(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = clock.$.subscribe(() => {
|
||||||
|
actions.tick?.();
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [actions, clock.$]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toggle() {
|
||||||
|
clock.tick();
|
||||||
|
actions.toggle?.();
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
clock.reset();
|
||||||
|
actions.reset?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClockFrame(frameFinished: () => void) {
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = Clock.get().frame$.subscribe(() => {
|
||||||
|
frameFinished();
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [frameFinished]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClockReset(reset: () => void) {
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = Clock.get().reset$.subscribe(() => {
|
||||||
|
reset();
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [reset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function displayClock() {
|
||||||
|
return display(Clock.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClockface() {
|
||||||
|
const [clockface, setClockface] = useState(displayClock());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = Clock.get().$.subscribe(() => {
|
||||||
|
setClockface(displayClock());
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return clockface;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Clockface = () => {
|
||||||
|
const clockface = useClockface();
|
||||||
|
return <span style={{ whiteSpace: "nowrap" }}>{clockface}</span>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { isErr, Ok } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
import { Span } from "@nand2tetris/simulator/languages/base";
|
||||||
|
import { CMP, Cmp } from "@nand2tetris/simulator/languages/cmp.js";
|
||||||
|
|
||||||
|
interface Diff {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
expected: string;
|
||||||
|
given: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiffLineDisplay {
|
||||||
|
expectedLine: string;
|
||||||
|
givenLine: string;
|
||||||
|
correctCellSpans: Span[];
|
||||||
|
incorrectCellSpans: Span[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DecorationType =
|
||||||
|
| "correct-line"
|
||||||
|
| "error-line"
|
||||||
|
| "correct-cell"
|
||||||
|
| "error-cell";
|
||||||
|
|
||||||
|
interface Decoration {
|
||||||
|
span: Span;
|
||||||
|
type: DecorationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiffDisplay {
|
||||||
|
text: string;
|
||||||
|
failureNum: number;
|
||||||
|
decorations: Decoration[];
|
||||||
|
lineNumbers: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiffs(cmpData: Cmp, outData: Cmp): Diff[] {
|
||||||
|
const diffs: Diff[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(cmpData.length, outData.length); i++) {
|
||||||
|
const cmpI = cmpData[i] ?? [];
|
||||||
|
const outI = outData[i] ?? [];
|
||||||
|
|
||||||
|
for (let j = 0; j < Math.max(cmpI.length, outI.length); j++) {
|
||||||
|
const cmpJ = cmpI[j] ?? "";
|
||||||
|
const outJ = outI[j] ?? "";
|
||||||
|
if (!(cmpJ?.trim().match(/^\*+$/) !== null || outJ === cmpJ)) {
|
||||||
|
diffs.push({ row: i, col: j, expected: cmpJ, given: outJ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compare(cmp: string, out: string) {
|
||||||
|
const cmpResult = CMP.parse(cmp);
|
||||||
|
const outResult = CMP.parse(out);
|
||||||
|
|
||||||
|
if (isErr(cmpResult) || isErr(outResult)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmpData = Ok(cmpResult);
|
||||||
|
const outData = Ok(outResult);
|
||||||
|
|
||||||
|
return getDiffs(cmpData, outData).length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDiffs(cmp: string, out: string): DiffDisplay {
|
||||||
|
const cmpResult = CMP.parse(cmp);
|
||||||
|
const outResult = CMP.parse(out);
|
||||||
|
|
||||||
|
if (isErr(cmpResult) || isErr(outResult)) {
|
||||||
|
return {
|
||||||
|
text: "",
|
||||||
|
failureNum: 0,
|
||||||
|
decorations: [],
|
||||||
|
lineNumbers: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmpData = Ok(cmpResult);
|
||||||
|
const outData = Ok(outResult);
|
||||||
|
|
||||||
|
const diffs = getDiffs(cmpData, outData);
|
||||||
|
|
||||||
|
const diffsByLine: Diff[][] = new Array<Diff[]>(cmpData.length);
|
||||||
|
for (const diff of diffs) {
|
||||||
|
const lineDiffs = diffsByLine[diff.row];
|
||||||
|
if (lineDiffs) {
|
||||||
|
lineDiffs.push(diff);
|
||||||
|
} else {
|
||||||
|
diffsByLine[diff.row] = [diff];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = out.split("\n");
|
||||||
|
const diffLines: DiffLineDisplay[] = new Array(cmpData.length);
|
||||||
|
for (let i = 0; i < diffsByLine.length; i++) {
|
||||||
|
if (diffsByLine[i]) {
|
||||||
|
diffLines[i] = generateDiffLine(lines[i], diffsByLine[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalLines: string[] = [];
|
||||||
|
let lineStart = 0;
|
||||||
|
const decorations: Decoration[] = [];
|
||||||
|
const lineNumbers: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const diffLine = diffLines[i];
|
||||||
|
lineNumbers.push((i + 1).toString());
|
||||||
|
if (diffLine) {
|
||||||
|
lineNumbers.push("");
|
||||||
|
finalLines.push(diffLine.givenLine);
|
||||||
|
decorations.push({
|
||||||
|
span: {
|
||||||
|
start: lineStart,
|
||||||
|
end: lineStart + diffLine.givenLine.length,
|
||||||
|
line: finalLines.length,
|
||||||
|
},
|
||||||
|
type: "error-line",
|
||||||
|
});
|
||||||
|
decorations.push(
|
||||||
|
...diffLine.incorrectCellSpans.map((span) => ({
|
||||||
|
span: {
|
||||||
|
start: span.start + lineStart,
|
||||||
|
end: span.end + lineStart,
|
||||||
|
line: span.line,
|
||||||
|
},
|
||||||
|
type: "error-cell" as DecorationType,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
lineStart += diffLine.expectedLine.length + 1; // +1 for the newline character
|
||||||
|
|
||||||
|
finalLines.push(diffLine.expectedLine);
|
||||||
|
decorations.push({
|
||||||
|
span: {
|
||||||
|
start: lineStart,
|
||||||
|
end: lineStart + diffLine.expectedLine.length,
|
||||||
|
line: i,
|
||||||
|
},
|
||||||
|
type: "correct-line",
|
||||||
|
});
|
||||||
|
decorations.push(
|
||||||
|
...diffLine.correctCellSpans.map((span) => ({
|
||||||
|
span: {
|
||||||
|
start: span.start + lineStart,
|
||||||
|
end: span.end + lineStart,
|
||||||
|
line: finalLines.length,
|
||||||
|
},
|
||||||
|
type: "correct-cell" as DecorationType,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
lineStart += diffLine.givenLine.length + 1;
|
||||||
|
} else {
|
||||||
|
finalLines.push(lines[i]);
|
||||||
|
lineStart += lines[i].length + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = finalLines.join("\n");
|
||||||
|
if (text.endsWith("\n")) {
|
||||||
|
text = text.substring(0, text.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: text,
|
||||||
|
failureNum: diffs.length,
|
||||||
|
decorations,
|
||||||
|
lineNumbers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDiffLine(original: string, diffs: Diff[]): DiffLineDisplay {
|
||||||
|
const cells = original.split("|").filter((cell) => cell != "");
|
||||||
|
const newCells = Array.from(cells);
|
||||||
|
|
||||||
|
const cellStarts: number[] = [];
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < cells.length; i++) {
|
||||||
|
cellStarts.push(sum + 1);
|
||||||
|
sum += cells[i].length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const correctCellSpans: Span[] = [];
|
||||||
|
const incorrectCellSpans: Span[] = [];
|
||||||
|
|
||||||
|
for (const diff of diffs) {
|
||||||
|
cells[diff.col] = diff.expected;
|
||||||
|
newCells[diff.col] = diff.given;
|
||||||
|
|
||||||
|
const span = {
|
||||||
|
start: cellStarts[diff.col],
|
||||||
|
end: cellStarts[diff.col] + diff.expected.length,
|
||||||
|
line: 0, // not used
|
||||||
|
};
|
||||||
|
correctCellSpans.push(span);
|
||||||
|
incorrectCellSpans.push(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
expectedLine: `|${cells.join("|")}|`,
|
||||||
|
givenLine: `|${newCells.join("|")}|`,
|
||||||
|
correctCellSpans,
|
||||||
|
incorrectCellSpans,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function useDialog() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return {
|
||||||
|
isOpen: open,
|
||||||
|
open() {
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { CMP } from "@nand2tetris/simulator/languages/cmp.js";
|
||||||
|
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||||
|
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
||||||
|
import { Err, isErr, Ok } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
|
export const DiffTable = ({
|
||||||
|
className = "",
|
||||||
|
out,
|
||||||
|
cmp,
|
||||||
|
zeroState,
|
||||||
|
}: {
|
||||||
|
out: string;
|
||||||
|
cmp: string;
|
||||||
|
className?: string;
|
||||||
|
zeroState?: ReactElement;
|
||||||
|
}) => {
|
||||||
|
const output = CMP.parse(out);
|
||||||
|
const compare = CMP.parse(cmp);
|
||||||
|
|
||||||
|
if (isErr(output)) {
|
||||||
|
return (
|
||||||
|
<details>
|
||||||
|
<summary>Failed to parse output</summary>
|
||||||
|
<pre>{display(Err(output))}</pre>
|
||||||
|
<code>
|
||||||
|
<pre>{out}</pre>
|
||||||
|
</code>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isErr(compare)) {
|
||||||
|
return (
|
||||||
|
<details>
|
||||||
|
<summary>Failed to parse compare</summary>
|
||||||
|
<code>
|
||||||
|
<pre>{display(Err(compare))}</pre>
|
||||||
|
<pre>{cmp}</pre>
|
||||||
|
</code>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmpData = Ok(compare);
|
||||||
|
const outData = Ok(output);
|
||||||
|
let failures = 0;
|
||||||
|
const table = range(0, Math.min(cmpData.length, outData.length)).map((i) => {
|
||||||
|
const cmpI = cmpData[i] ?? [];
|
||||||
|
const outI = outData[i] ?? [];
|
||||||
|
return range(0, Math.max(cmpI.length, outI.length))
|
||||||
|
.map((_, j) => [cmpI[j] ?? "", outI[j] ?? ""])
|
||||||
|
.map(([cmp, out]) => {
|
||||||
|
const cell = {
|
||||||
|
cmp: cmp ?? '"',
|
||||||
|
out: out ?? '"',
|
||||||
|
pass:
|
||||||
|
cmp?.trim().match(/^\*+$/) !== null || out?.trim() === cmp?.trim(),
|
||||||
|
};
|
||||||
|
if (!cell.pass) {
|
||||||
|
failures += 1;
|
||||||
|
}
|
||||||
|
return cell;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"scroll-x " + className}>
|
||||||
|
{failures > 0 && (
|
||||||
|
<p>
|
||||||
|
{failures} failure{failures === 1 ? "" : "s"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{table.length > 0 ? (
|
||||||
|
<table
|
||||||
|
style={{
|
||||||
|
fontFamily: "var(--font-family-monospace)",
|
||||||
|
marginBottom: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
{table.map((row, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
{row.map(({ cmp, out, pass }, i) => (
|
||||||
|
<DiffCell cmp={cmp} out={out} pass={pass} key={i} />
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
(zeroState ?? <p>Execute test script to compare output.</p>)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DiffCell = ({
|
||||||
|
cmp,
|
||||||
|
out,
|
||||||
|
pass,
|
||||||
|
}: {
|
||||||
|
cmp: string;
|
||||||
|
out: string;
|
||||||
|
pass: boolean;
|
||||||
|
}) => {
|
||||||
|
return pass ? (
|
||||||
|
<>
|
||||||
|
<td>{cmp}</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td>
|
||||||
|
<ins>{cmp}</ins>
|
||||||
|
<br />
|
||||||
|
<del>{out}</del>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { FileSystem, Stats } from "@davidsouther/jiffies/lib/esm/fs";
|
||||||
|
import { Err, Ok, Result } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
|
||||||
|
interface TestFiles {
|
||||||
|
tst: string;
|
||||||
|
cmp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTestFiles(
|
||||||
|
fs: FileSystem,
|
||||||
|
tstPath: string,
|
||||||
|
): Promise<Result<TestFiles>> {
|
||||||
|
try {
|
||||||
|
const tst = await fs.readFile(tstPath);
|
||||||
|
let cmp: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
const cpmPath = tstPath
|
||||||
|
.replace("VME.tst", ".tst")
|
||||||
|
.replace(".tst", ".cmp");
|
||||||
|
cmp = await fs.readFile(cpmPath);
|
||||||
|
} catch (_e) {
|
||||||
|
// There doesn't have to be a compare file
|
||||||
|
}
|
||||||
|
return Ok({ tst: tst, cmp: cmp });
|
||||||
|
} catch (e) {
|
||||||
|
return Err(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortFiles(files: Stats[]) {
|
||||||
|
return files.sort((a, b) => {
|
||||||
|
const aIsNum = /^\d+/.test(a.name);
|
||||||
|
const bIsNum = /^\d+/.test(b.name);
|
||||||
|
if (aIsNum && !bIsNum) {
|
||||||
|
return -1;
|
||||||
|
} else if (!aIsNum && bIsNum) {
|
||||||
|
return 1;
|
||||||
|
} else if (aIsNum && bIsNum) {
|
||||||
|
return parseInt(a.name, 10) - parseInt(b.name, 10);
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cloneTree(
|
||||||
|
sourceFs: FileSystem,
|
||||||
|
targetFs: FileSystem,
|
||||||
|
dir = "/",
|
||||||
|
pathTransform: (path: string) => string,
|
||||||
|
overwrite = false,
|
||||||
|
) {
|
||||||
|
const sourceDir = dir == "/" ? "" : dir;
|
||||||
|
const targetDir = pathTransform(sourceDir);
|
||||||
|
|
||||||
|
const sourceItems = await sourceFs.scandir(dir);
|
||||||
|
|
||||||
|
targetFs.mkdir(targetDir);
|
||||||
|
const targetItems = new Set(
|
||||||
|
(await targetFs.scandir(targetDir)).map((stat) => stat.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const item of sourceItems) {
|
||||||
|
if (item.isFile()) {
|
||||||
|
if (overwrite || !targetItems.has(item.name)) {
|
||||||
|
await targetFs.writeFile(
|
||||||
|
`${targetDir}/${item.name}`,
|
||||||
|
await sourceFs.readFile(`${sourceDir}/${item.name}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await cloneTree(
|
||||||
|
sourceFs,
|
||||||
|
targetFs,
|
||||||
|
`${sourceDir}/${item.name}`,
|
||||||
|
pathTransform,
|
||||||
|
overwrite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// export { Trans } from "@lingui/macro";
|
||||||
|
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export const Trans = (props: PropsWithChildren) => props.children ?? <></>;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { width } from "@davidsouther/jiffies/lib/esm/dom/css/sizing.js";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useStateInitializer } from "./react.js";
|
||||||
|
import { Action } from "@nand2tetris/simulator/types.js";
|
||||||
|
|
||||||
|
const Mode = { VIEW: 0, EDIT: 1 };
|
||||||
|
|
||||||
|
export const InlineEdit = (props: {
|
||||||
|
mode?: keyof typeof Mode;
|
||||||
|
value: string;
|
||||||
|
highlight: boolean;
|
||||||
|
onChange: Action<string>;
|
||||||
|
onFocus?: () => void;
|
||||||
|
}) => {
|
||||||
|
const [mode, setMode] = useState(props.mode ?? Mode.VIEW);
|
||||||
|
const [value, setValue] = useStateInitializer(props.value);
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
switch (mode) {
|
||||||
|
case Mode.EDIT:
|
||||||
|
return edit();
|
||||||
|
case Mode.VIEW:
|
||||||
|
return view();
|
||||||
|
default:
|
||||||
|
return <span />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const view = () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
cursor: "text",
|
||||||
|
...width("full", "inline"),
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setMode(Mode.EDIT);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const doSelect = useCallback(
|
||||||
|
(ref: HTMLInputElement | null) => ref?.select(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const doChange = useCallback(
|
||||||
|
(target: HTMLInputElement) => {
|
||||||
|
setMode(Mode.VIEW);
|
||||||
|
setValue(target.value ?? "");
|
||||||
|
props.onChange(target.value ?? "");
|
||||||
|
},
|
||||||
|
[props, setMode, setValue],
|
||||||
|
);
|
||||||
|
const edit = () => {
|
||||||
|
const edit = (
|
||||||
|
<span style={{ display: "block", position: "relative" }}>
|
||||||
|
<input
|
||||||
|
ref={doSelect}
|
||||||
|
style={{
|
||||||
|
zIndex: "10",
|
||||||
|
position: "absolute",
|
||||||
|
left: "0",
|
||||||
|
marginTop: "-0.375rem",
|
||||||
|
color: "var(--text-color)",
|
||||||
|
}}
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
onBlur={({ target }) => doChange(target)}
|
||||||
|
onKeyPress={({ key, target }) => {
|
||||||
|
if (key === "Enter") {
|
||||||
|
doChange(target as HTMLInputElement);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
defaultValue={value}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
return edit;
|
||||||
|
};
|
||||||
|
|
||||||
|
return render();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InlineEdit;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const LOADING = "Loading in progress...";
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||||
|
import { REGISTRY as BUILTIN_REGISTRY } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||||
|
|
||||||
|
export class ChipDisplayInfo {
|
||||||
|
signBehaviors: Map<string, boolean> = new Map();
|
||||||
|
|
||||||
|
public constructor(chipName: string, unsigned?: string[]) {
|
||||||
|
if (BUILTIN_REGISTRY.has(chipName)) {
|
||||||
|
const chip = assertExists(BUILTIN_REGISTRY.get(chipName)?.());
|
||||||
|
|
||||||
|
const pins = [...chip.ins.entries(), ...chip.outs.entries()];
|
||||||
|
|
||||||
|
for (const pin of pins) {
|
||||||
|
this.signBehaviors.set(
|
||||||
|
pin.name,
|
||||||
|
!unsigned || !unsigned.includes(pin.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSigned(pin: string) {
|
||||||
|
return this.signBehaviors.get(pin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UNSIGNED_PINS = new Map<string, string[]>([
|
||||||
|
["Mux4Way16", ["sel"]],
|
||||||
|
["Mux8Way16", ["sel"]],
|
||||||
|
["DMux4Way", ["sel"]],
|
||||||
|
["DMux8Way", ["sel"]],
|
||||||
|
["RAM8", ["address"]],
|
||||||
|
["RAM64", ["address"]],
|
||||||
|
["RAM512", ["address"]],
|
||||||
|
["RAM4K", ["address"]],
|
||||||
|
["RAM16K", ["address"]],
|
||||||
|
["Screen", ["address"]],
|
||||||
|
["Memory", ["address"]],
|
||||||
|
["CPU", ["addressM", "pc"]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const getDisplayInfo = (chipName: string) =>
|
||||||
|
new ChipDisplayInfo(chipName, UNSIGNED_PINS.get(chipName));
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Bus, HIGH } from "@nand2tetris/simulator/chip/chip.js";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { act, useState } from "react";
|
||||||
|
import { Pinout, reducePin } from "./pinout.js";
|
||||||
|
|
||||||
|
describe("<Pinout />", () => {
|
||||||
|
it("renders pins", () => {
|
||||||
|
const pin = new Bus("pin");
|
||||||
|
render(<Pinout pins={[reducePin(pin)]} />);
|
||||||
|
|
||||||
|
const pinOut = screen.getByText("0");
|
||||||
|
expect(pinOut).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles bits", () => {
|
||||||
|
const pin = new Bus("pin");
|
||||||
|
const Wrapper = () => {
|
||||||
|
const [pins, setPins] = useState([reducePin(pin)]);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
pin.toggle();
|
||||||
|
setPins([reducePin(pin)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Pinout pins={pins} toggle={toggle} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Wrapper />);
|
||||||
|
|
||||||
|
const pinOut = screen.getByText("0");
|
||||||
|
act(() => {
|
||||||
|
pinOut.click();
|
||||||
|
});
|
||||||
|
expect(pin.busVoltage).toBe(HIGH);
|
||||||
|
expect(screen.getByText("1")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("increments buses", () => {
|
||||||
|
const pin = new Bus("pin", 3);
|
||||||
|
const Wrapper = () => {
|
||||||
|
const [pins, setPins] = useState([reducePin(pin)]);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
pin.busVoltage += 1;
|
||||||
|
setPins([reducePin(pin)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Pinout pins={pins} toggle={toggle} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<Wrapper />);
|
||||||
|
|
||||||
|
const pinOut = screen.getByText("000");
|
||||||
|
act(() => {
|
||||||
|
pinOut.click();
|
||||||
|
});
|
||||||
|
expect(pin.busVoltage).toBe(1);
|
||||||
|
expect(screen.getByText("001")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
import { range } from "@davidsouther/jiffies/lib/esm/range.js";
|
||||||
|
import {
|
||||||
|
Pin as ChipPin,
|
||||||
|
Pins,
|
||||||
|
Voltage,
|
||||||
|
} from "@nand2tetris/simulator/chip/chip.js";
|
||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { ChipDisplayInfo, getDisplayInfo } from "./pin_display.js";
|
||||||
|
import "./public/pin.css";
|
||||||
|
import { ChipSim } from "./stores/chip.store.js";
|
||||||
|
|
||||||
|
export const PinContext = createContext({});
|
||||||
|
|
||||||
|
export interface ImmPin {
|
||||||
|
bits: [number, Voltage][];
|
||||||
|
pin: ChipPin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducePin(pin: ChipPin) {
|
||||||
|
return {
|
||||||
|
pin,
|
||||||
|
bits: range(0, pin.width)
|
||||||
|
.map((i) => [i, pin.voltage(i)] as [number, Voltage])
|
||||||
|
.reverse(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducePins(pins: Pins): ImmPin[] {
|
||||||
|
return [...pins.entries()].map(reducePin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PinoutPins {
|
||||||
|
pins: ImmPin[];
|
||||||
|
toggle?: (pin: ChipPin, bit?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FullPinout = (props: {
|
||||||
|
sim: ChipSim;
|
||||||
|
toggle: (pin: ChipPin, i: number | undefined) => void;
|
||||||
|
setInputValid: (pending: boolean) => void;
|
||||||
|
hideInternal?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { inPins, outPins, internalPins } = props.sim;
|
||||||
|
const displayInfo = getDisplayInfo(props.sim.chip[0].name ?? "");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
table.pinout th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.pinout tbody td:first-child {
|
||||||
|
text-align: right;
|
||||||
|
--font-size: 1rem;
|
||||||
|
width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-right: var(--border-width) solid var(--table-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.pinout tbody button {
|
||||||
|
--font-size: 0.875em;
|
||||||
|
font-family: var(--font-family-monospace);
|
||||||
|
max-width: 2em;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<table className="pinout">
|
||||||
|
<tbody>
|
||||||
|
<PinoutBlock
|
||||||
|
pins={inPins}
|
||||||
|
header="Input pins"
|
||||||
|
toggle={props.toggle}
|
||||||
|
setInputValid={props.setInputValid}
|
||||||
|
displayInfo={displayInfo}
|
||||||
|
/>
|
||||||
|
<PinoutBlock
|
||||||
|
pins={outPins}
|
||||||
|
header="Output pins"
|
||||||
|
disabled={props.sim.pending}
|
||||||
|
enableEdit={false}
|
||||||
|
displayInfo={displayInfo}
|
||||||
|
/>
|
||||||
|
{!props.hideInternal && (
|
||||||
|
<PinoutBlock
|
||||||
|
pins={internalPins}
|
||||||
|
header="Internal pins"
|
||||||
|
disabled={props.sim.pending}
|
||||||
|
enableEdit={false}
|
||||||
|
displayInfo={displayInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PinoutBlock = (
|
||||||
|
props: PinoutPins & {
|
||||||
|
header: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
enableEdit?: boolean;
|
||||||
|
setInputValid?: (valid: boolean) => void;
|
||||||
|
displayInfo: ChipDisplayInfo;
|
||||||
|
},
|
||||||
|
) => (
|
||||||
|
<>
|
||||||
|
{props.pins.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<th colSpan={2}>{props.header}</th>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{[...props.pins].map((immPin) => (
|
||||||
|
<tr key={immPin.pin.name}>
|
||||||
|
<td>{immPin.pin.name}</td>
|
||||||
|
<td>
|
||||||
|
<Pin
|
||||||
|
pin={immPin}
|
||||||
|
toggle={props.toggle}
|
||||||
|
disabled={props.disabled}
|
||||||
|
enableEdit={props.enableEdit}
|
||||||
|
signed={props.displayInfo.isSigned(immPin.pin.name)}
|
||||||
|
setInputValid={props.setInputValid}
|
||||||
|
internal={props.header === "Internal pins" ? true : false}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Pinout = ({
|
||||||
|
pins,
|
||||||
|
toggle,
|
||||||
|
}: {
|
||||||
|
pins: ImmPin[];
|
||||||
|
toggle?: (pin: ChipPin, bit?: number) => void;
|
||||||
|
}) => {
|
||||||
|
if (pins.length === 0) {
|
||||||
|
return <>None</>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<table className="pinout">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[...pins].map((immPin) => (
|
||||||
|
<tr key={immPin.pin.name}>
|
||||||
|
<td>{immPin.pin.name}</td>
|
||||||
|
<td>
|
||||||
|
<Pin pin={immPin} toggle={toggle} internal />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pin = ({
|
||||||
|
pin,
|
||||||
|
toggle,
|
||||||
|
disabled = false,
|
||||||
|
enableEdit = true,
|
||||||
|
signed = true,
|
||||||
|
setInputValid,
|
||||||
|
internal = false,
|
||||||
|
}: {
|
||||||
|
pin: ImmPin;
|
||||||
|
toggle: ((pin: ChipPin, bit?: number) => void) | undefined;
|
||||||
|
disabled?: boolean;
|
||||||
|
enableEdit?: boolean;
|
||||||
|
signed?: boolean;
|
||||||
|
setInputValid?: (valid: boolean) => void;
|
||||||
|
internal: boolean;
|
||||||
|
}) => {
|
||||||
|
const [isBin, setIsBin] = useState(true);
|
||||||
|
let inputValid = true;
|
||||||
|
const [decimal, setDecimal] = useState("");
|
||||||
|
|
||||||
|
const toggleBin = () => {
|
||||||
|
setIsBin(!isBin);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetDispatcher = useContext(PinContext);
|
||||||
|
if (resetDispatcher instanceof PinResetDispatcher) {
|
||||||
|
resetDispatcher.registerCallback(() => {
|
||||||
|
setIsBin(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const setInputValidity = (valid: boolean) => {
|
||||||
|
inputValid = valid;
|
||||||
|
setInputValid?.(valid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecimalChange = (value: string) => {
|
||||||
|
const positive = value.replace(/[^\d]/g, "");
|
||||||
|
const numeric = signed && value[0] === "-" ? `-${positive}` : positive;
|
||||||
|
|
||||||
|
setDecimal(numeric);
|
||||||
|
if (isNaN(parseInt(numeric))) {
|
||||||
|
setInputValidity(false);
|
||||||
|
} else {
|
||||||
|
const newValue = parseInt(numeric);
|
||||||
|
if (
|
||||||
|
(!signed && newValue >= Math.pow(2, pin.bits.length)) ||
|
||||||
|
(signed &&
|
||||||
|
(newValue >= Math.pow(2, pin.bits.length - 1) ||
|
||||||
|
newValue < -Math.pow(2, pin.bits.length - 1)))
|
||||||
|
) {
|
||||||
|
setInputValidity(false);
|
||||||
|
} else {
|
||||||
|
updatePins(newValue);
|
||||||
|
setInputValidity(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePins = (n: number) => {
|
||||||
|
for (let i = 0; i < pin.bits.length; i++) {
|
||||||
|
if (pin.bits[pin.bits.length - i - 1][1] !== ((n >> i) & 1)) {
|
||||||
|
toggle?.(pin.pin, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isBin && inputValid) {
|
||||||
|
let value = 0;
|
||||||
|
if (signed && pin.bits[0][1]) {
|
||||||
|
// negative
|
||||||
|
for (const [i, v] of pin.bits) {
|
||||||
|
if (i < pin.bits.length - 1 && !v) {
|
||||||
|
value += 2 ** i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = -value - 1;
|
||||||
|
} else {
|
||||||
|
// positive
|
||||||
|
const limit = signed ? pin.bits.length - 1 : pin.bits.length;
|
||||||
|
for (const [i, v] of pin.bits) {
|
||||||
|
if (i < limit && v) {
|
||||||
|
value += 2 ** i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDecimal(value.toString());
|
||||||
|
}
|
||||||
|
}, [pin, isBin]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", flexDirection: "row", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<fieldset role="group" style={{ width: `${pin.bits.length}rem` }}>
|
||||||
|
{isBin ? (
|
||||||
|
pin.bits.map(([i, v]) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
disabled={disabled}
|
||||||
|
style={internal ? { backgroundColor: "grey" } : {}}
|
||||||
|
onClick={() => toggle?.(pin.pin, i)}
|
||||||
|
data-testid={`pin-${i}`}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className="colored"
|
||||||
|
value={decimal}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleDecimalChange(e.target.value);
|
||||||
|
}}
|
||||||
|
disabled={!enableEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
{pin.bits.length > 1 && (
|
||||||
|
<>
|
||||||
|
<div style={{ width: "1em" }} />
|
||||||
|
<button className="pin-control" onClick={() => toggleBin()}>
|
||||||
|
{isBin ? "dec" : "bin"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PinResetDispatcher {
|
||||||
|
private callbacks: (() => void)[] = [];
|
||||||
|
|
||||||
|
registerCallback(callback: () => void) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
for (const callback of this.callbacks) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.alu {
|
||||||
|
font-size: 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.pin-control {
|
||||||
|
max-width: 3em !important;
|
||||||
|
background: var(--light-grey);
|
||||||
|
border-color: var(--light-grey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { produce } from "immer";
|
||||||
|
import { Dispatch, useEffect, useReducer, useState } from "react";
|
||||||
|
|
||||||
|
export function useImmerReducer<
|
||||||
|
T,
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: reducer really doesn't care
|
||||||
|
Reducers extends Record<string, (state: T, action?: any) => T | void>,
|
||||||
|
>(reducers: Reducers, initialState: T) {
|
||||||
|
return useReducer(
|
||||||
|
(
|
||||||
|
state: T,
|
||||||
|
command: {
|
||||||
|
action: keyof Reducers;
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: reducer doesn't care and covariants are hard
|
||||||
|
payload?: any;
|
||||||
|
},
|
||||||
|
): T =>
|
||||||
|
produce(state, (draft: T) => {
|
||||||
|
reducers[command.action](draft, command.payload);
|
||||||
|
}),
|
||||||
|
initialState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStateInitializer<T>(init: T): [T, Dispatch<T>] {
|
||||||
|
const [state, setState] = useState<T>(init);
|
||||||
|
useEffect(() => {
|
||||||
|
setState(init);
|
||||||
|
}, [init]);
|
||||||
|
return [state, setState];
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Timer } from "@nand2tetris/simulator/timer.js";
|
||||||
|
import { ChangeEvent, ReactNode, useEffect, useRef } from "react";
|
||||||
|
import { useStateInitializer } from "./react.js";
|
||||||
|
import { useTimer } from "./timer.js";
|
||||||
|
|
||||||
|
interface RunbarTooltipOverrides {
|
||||||
|
step: string;
|
||||||
|
run: string;
|
||||||
|
pause: string;
|
||||||
|
reset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RunSpeed = 0 | 1 | 2 | 3 | 4;
|
||||||
|
|
||||||
|
export const Runbar = (props: {
|
||||||
|
runner: Timer;
|
||||||
|
speed?: RunSpeed;
|
||||||
|
disabled?: boolean;
|
||||||
|
prefix?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
overrideTooltips?: Partial<RunbarTooltipOverrides>;
|
||||||
|
onSpeedChange?: (speed: RunSpeed) => void;
|
||||||
|
}) => {
|
||||||
|
const runner = useTimer(props.runner);
|
||||||
|
const [speedValue, setSpeed] = useStateInitializer(props.speed ?? 2);
|
||||||
|
|
||||||
|
const speedValues: Record<RunSpeed, [number, number]> = {
|
||||||
|
0: [1000, 1],
|
||||||
|
1: [500, 1],
|
||||||
|
2: [16, 1],
|
||||||
|
3: [16, 16666],
|
||||||
|
4: [16, 16666 * 30],
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSpeed();
|
||||||
|
}, [speedValue]);
|
||||||
|
|
||||||
|
const updateSpeed = () => {
|
||||||
|
const [speed, steps] = speedValues[speedValue];
|
||||||
|
runner.dispatch({ action: "setSpeed", payload: speed });
|
||||||
|
runner.dispatch({ action: "setSteps", payload: steps });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const speed = Number(e.target.value) as RunSpeed;
|
||||||
|
setSpeed(speed);
|
||||||
|
props.onSpeedChange?.(speed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const onKeyPress = (event: KeyboardEvent) => {
|
||||||
|
toggleRef.current?.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("keydown", onKeyPress);
|
||||||
|
window.addEventListener("keyup", onKeyPress);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyPress);
|
||||||
|
window.removeEventListener("keyup", onKeyPress);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex row wrap">
|
||||||
|
<fieldset role="group">
|
||||||
|
{props.prefix}
|
||||||
|
<button
|
||||||
|
className="flex-0"
|
||||||
|
disabled={props.disabled}
|
||||||
|
onClick={() => runner.actions.frame()}
|
||||||
|
data-tooltip={props.overrideTooltips?.step ?? `Step`}
|
||||||
|
data-placement="bottom"
|
||||||
|
>
|
||||||
|
{/* <Icon name="play_arrow" /> */}
|
||||||
|
➡️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex-0"
|
||||||
|
ref={toggleRef}
|
||||||
|
disabled={props.disabled}
|
||||||
|
onClick={() =>
|
||||||
|
runner.state.running
|
||||||
|
? runner.actions.stop()
|
||||||
|
: runner.actions.start()
|
||||||
|
}
|
||||||
|
data-tooltip={
|
||||||
|
runner.state.running
|
||||||
|
? (props.overrideTooltips?.pause ?? `Pause`)
|
||||||
|
: (props.overrideTooltips?.run ?? `Run`)
|
||||||
|
}
|
||||||
|
data-placement="bottom"
|
||||||
|
>
|
||||||
|
{/* <Icon name={runner.state.running ? "pause" : "fast_forward"} /> */}
|
||||||
|
{runner.state.running ? "⏸" : "️⏩"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex-0"
|
||||||
|
onClick={() => {
|
||||||
|
if (runner.state.running) {
|
||||||
|
runner.actions.stop();
|
||||||
|
}
|
||||||
|
runner.actions.reset();
|
||||||
|
}}
|
||||||
|
data-tooltip={props.overrideTooltips?.reset ?? `Reset`}
|
||||||
|
data-placement="bottom"
|
||||||
|
>
|
||||||
|
{/* <Icon name="fast_rewind" /> */}⏮
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ padding: "0.2rem" }}>Slow</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={4}
|
||||||
|
step={1}
|
||||||
|
value={speedValue}
|
||||||
|
disabled={runner.state.running}
|
||||||
|
onChange={onChange}
|
||||||
|
style={{ width: "150px", padding: "0.2rem" }}
|
||||||
|
data-tooltip={"Execution speed"}
|
||||||
|
data-placement={"bottom"}
|
||||||
|
/>
|
||||||
|
<span style={{ padding: "0.2rem" }}>Fast</span>
|
||||||
|
</div>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
import "@nand2tetris/simulator/setupTests.js";
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import {
|
||||||
|
Err,
|
||||||
|
isErr,
|
||||||
|
Ok,
|
||||||
|
Result,
|
||||||
|
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
import {
|
||||||
|
CompareResultLengths,
|
||||||
|
CompareResultLine,
|
||||||
|
compareLines,
|
||||||
|
} from "@nand2tetris/simulator/compare.js";
|
||||||
|
import {
|
||||||
|
KEYBOARD_OFFSET,
|
||||||
|
SCREEN_OFFSET,
|
||||||
|
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import {
|
||||||
|
ASM,
|
||||||
|
Asm,
|
||||||
|
fillLabel,
|
||||||
|
isAValueInstruction,
|
||||||
|
translateInstruction,
|
||||||
|
} from "@nand2tetris/simulator/languages/asm.js";
|
||||||
|
import {
|
||||||
|
CompilationError,
|
||||||
|
Span,
|
||||||
|
} from "@nand2tetris/simulator/languages/base.js";
|
||||||
|
import { Action } from "@nand2tetris/simulator/types.js";
|
||||||
|
import { bin } from "@nand2tetris/simulator/util/twos.js";
|
||||||
|
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||||
|
import { RunSpeed } from "src/runbar.js";
|
||||||
|
import { useImmerReducer } from "../react.js";
|
||||||
|
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||||
|
|
||||||
|
export interface TranslatorSymbol {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultSymbols(): TranslatorSymbol[] {
|
||||||
|
return [
|
||||||
|
{ name: "R0", value: "0" },
|
||||||
|
{ name: "R1", value: "1" },
|
||||||
|
{ name: "R2", value: "2" },
|
||||||
|
{ name: "...", value: "" }, // abbreviation of R3 - R14
|
||||||
|
{ name: "R15", value: "15" },
|
||||||
|
{ name: "SCREEN", value: SCREEN_OFFSET.toString() },
|
||||||
|
{ name: "KBD", value: KEYBOARD_OFFSET.toString() },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HighlightInfo {
|
||||||
|
resultHighlight: Span | undefined;
|
||||||
|
sourceHighlight: Span | undefined;
|
||||||
|
highlightMap: Map<Span, Span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsmVariable {
|
||||||
|
name: string;
|
||||||
|
isHidden: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Translator {
|
||||||
|
asm: Asm = { instructions: [] };
|
||||||
|
current = -1;
|
||||||
|
done = false;
|
||||||
|
symbols: TranslatorSymbol[] = [];
|
||||||
|
private variables: Map<number, AsmVariable> = new Map();
|
||||||
|
private lines: string[] = [];
|
||||||
|
lineNumbers: number[] = [];
|
||||||
|
|
||||||
|
getResult() {
|
||||||
|
return this.lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
load(asm: Asm, lineNum: number): Result<void, CompilationError> {
|
||||||
|
this.symbols = defaultSymbols();
|
||||||
|
this.variables.clear();
|
||||||
|
this.asm = asm;
|
||||||
|
|
||||||
|
const result = fillLabel(asm, (name, value, isVar) => {
|
||||||
|
if (isVar) {
|
||||||
|
this.variables.set(value, { name: name, isHidden: true });
|
||||||
|
} else {
|
||||||
|
this.symbols.push({ name: name, value: value.toString() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isErr(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
asm.instructions = asm.instructions.filter(({ type }) => type !== "L");
|
||||||
|
|
||||||
|
this.resolveLineNumbers(lineNum);
|
||||||
|
this.reset();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveLineNumbers(lineNum: number) {
|
||||||
|
this.lineNumbers = Array(lineNum);
|
||||||
|
let currentLine = 0;
|
||||||
|
for (const instruction of this.asm.instructions) {
|
||||||
|
if (
|
||||||
|
(instruction.type === "A" || instruction.type === "C") &&
|
||||||
|
instruction.span != undefined
|
||||||
|
) {
|
||||||
|
this.lineNumbers[instruction.span.line] = currentLine;
|
||||||
|
currentLine += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step(highlightInfo: HighlightInfo) {
|
||||||
|
if (this.current >= this.asm.instructions.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.current += 1;
|
||||||
|
const instruction = this.asm.instructions[this.current];
|
||||||
|
if (instruction.type === "A" || instruction.type === "C") {
|
||||||
|
highlightInfo.sourceHighlight = instruction.span;
|
||||||
|
const result = translateInstruction(this.asm.instructions[this.current]);
|
||||||
|
if (result === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lines.push(`${bin(result)}`);
|
||||||
|
highlightInfo.resultHighlight = {
|
||||||
|
start: this.current * 17,
|
||||||
|
end: (this.current + 1) * 17,
|
||||||
|
line: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (highlightInfo.sourceHighlight) {
|
||||||
|
highlightInfo.highlightMap.set(
|
||||||
|
highlightInfo.sourceHighlight,
|
||||||
|
highlightInfo.resultHighlight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAValueInstruction(instruction)) {
|
||||||
|
const variable = this.variables.get(instruction.value);
|
||||||
|
if (variable != undefined && variable.isHidden) {
|
||||||
|
this.symbols.push({
|
||||||
|
name: variable.name,
|
||||||
|
value: instruction.value.toString(),
|
||||||
|
});
|
||||||
|
variable.isHidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.current === this.asm.instructions.length - 1) {
|
||||||
|
this.done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSymbols() {
|
||||||
|
for (const variable of this.variables.values()) {
|
||||||
|
variable.isHidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableNames = new Set(
|
||||||
|
Array.from(this.variables.values()).map((v) => v.name),
|
||||||
|
);
|
||||||
|
this.symbols = this.symbols.filter(
|
||||||
|
(symbol) => !variableNames.has(symbol.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.current = -1;
|
||||||
|
this.lines = [];
|
||||||
|
this.done = false;
|
||||||
|
this.resetSymbols();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsmPageState {
|
||||||
|
asm: string;
|
||||||
|
path: string | undefined;
|
||||||
|
translating: boolean;
|
||||||
|
current: number;
|
||||||
|
resultHighlight: Span | undefined;
|
||||||
|
sourceHighlight: Span | undefined;
|
||||||
|
symbols: TranslatorSymbol[];
|
||||||
|
result: string;
|
||||||
|
compare: string;
|
||||||
|
compareName: string | undefined;
|
||||||
|
lineNumbers: number[];
|
||||||
|
error?: CompilationError;
|
||||||
|
compareError: boolean;
|
||||||
|
title?: string;
|
||||||
|
config: AsmPageConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsmPageConfig {
|
||||||
|
speed: RunSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AsmStoreDispatch = Dispatch<{
|
||||||
|
action: keyof ReturnType<typeof makeAsmStore>["reducers"];
|
||||||
|
payload?: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function makeAsmStore(
|
||||||
|
fs: FileSystem,
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||||
|
dispatch: MutableRefObject<AsmStoreDispatch>,
|
||||||
|
upgraded: boolean,
|
||||||
|
) {
|
||||||
|
const translator = new Translator();
|
||||||
|
const highlightInfo: HighlightInfo = {
|
||||||
|
resultHighlight: undefined,
|
||||||
|
sourceHighlight: undefined,
|
||||||
|
highlightMap: new Map(),
|
||||||
|
};
|
||||||
|
let path: string | undefined;
|
||||||
|
let animate = true;
|
||||||
|
let compiled = false;
|
||||||
|
let translating = false;
|
||||||
|
let failure = false;
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
setAsm(
|
||||||
|
state: AsmPageState,
|
||||||
|
{ asm, path }: { asm: string; path: string | undefined },
|
||||||
|
) {
|
||||||
|
state.asm = asm;
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
state.path = path;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setCmp(state: AsmPageState, { cmp, name }: { cmp: string; name: string }) {
|
||||||
|
state.compare = cmp;
|
||||||
|
state.compareName = name;
|
||||||
|
setStatus("Loaded compare file");
|
||||||
|
},
|
||||||
|
|
||||||
|
setError(state: AsmPageState, error?: CompilationError) {
|
||||||
|
if (error) {
|
||||||
|
setStatus({
|
||||||
|
message: error.message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.error = error;
|
||||||
|
},
|
||||||
|
|
||||||
|
update(state: AsmPageState) {
|
||||||
|
state.translating = translating;
|
||||||
|
state.current = translator.current;
|
||||||
|
state.result = translator.getResult();
|
||||||
|
state.symbols = Array.from(translator.symbols);
|
||||||
|
state.lineNumbers = Array.from(translator.lineNumbers);
|
||||||
|
state.sourceHighlight = highlightInfo.sourceHighlight;
|
||||||
|
state.resultHighlight = highlightInfo.resultHighlight;
|
||||||
|
state.compareError = failure;
|
||||||
|
},
|
||||||
|
|
||||||
|
compare(state: AsmPageState) {
|
||||||
|
const comparison = compareLines(state.result, state.compare);
|
||||||
|
|
||||||
|
if ((comparison as CompareResultLengths).lenA) {
|
||||||
|
failure = true;
|
||||||
|
setStatus({
|
||||||
|
message: "Comparison failed - different lengths",
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { line } = comparison as CompareResultLine;
|
||||||
|
if (line) {
|
||||||
|
setStatus({
|
||||||
|
message: `Comparison failure: Line ${line}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
failure = true;
|
||||||
|
highlightInfo.resultHighlight = {
|
||||||
|
start: line * 17,
|
||||||
|
end: (line + 1) * 17,
|
||||||
|
line: -1,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus({
|
||||||
|
message: "Comparison successful",
|
||||||
|
severity: "SUCCESS",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setTitle(state: AsmPageState, title: string) {
|
||||||
|
state.title = title;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateConfig(state: AsmPageState, config: Partial<AsmPageConfig>) {
|
||||||
|
state.config = { ...state.config, ...config };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
async loadAsm(_path: string) {
|
||||||
|
path = _path;
|
||||||
|
const source = await fs.readFile(path);
|
||||||
|
actions.setAsm(source, path);
|
||||||
|
},
|
||||||
|
|
||||||
|
setAsm(asm: string, path?: string) {
|
||||||
|
asm = asm.replace(/\r\n/g, "\n");
|
||||||
|
dispatch.current({
|
||||||
|
action: "setAsm",
|
||||||
|
payload: { asm, path },
|
||||||
|
});
|
||||||
|
translating = false;
|
||||||
|
this.saveAsm(asm);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.compileAsm(asm);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
saveAsm(asm: string) {
|
||||||
|
if (path) {
|
||||||
|
fs.writeFile(path, asm);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compileAsm(asm: string) {
|
||||||
|
this.reset();
|
||||||
|
const parseResult = ASM.parse(asm);
|
||||||
|
if (isErr(parseResult)) {
|
||||||
|
dispatch.current({
|
||||||
|
action: "setError",
|
||||||
|
payload: Err(parseResult),
|
||||||
|
});
|
||||||
|
compiled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadResult = translator.load(
|
||||||
|
Ok(parseResult),
|
||||||
|
asm.split("\n").length,
|
||||||
|
);
|
||||||
|
if (isErr(loadResult)) {
|
||||||
|
dispatch.current({
|
||||||
|
action: "setError",
|
||||||
|
payload: Err(loadResult),
|
||||||
|
});
|
||||||
|
compiled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
compiled = translator.asm.instructions.length > 0;
|
||||||
|
setStatus("");
|
||||||
|
dispatch.current({ action: "setError" });
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
setAnimate(value: boolean) {
|
||||||
|
animate = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async step(): Promise<boolean> {
|
||||||
|
if (compiled) {
|
||||||
|
translating = true;
|
||||||
|
}
|
||||||
|
translator.step(highlightInfo);
|
||||||
|
|
||||||
|
if (animate || translator.done) {
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
|
||||||
|
if (path && upgraded) {
|
||||||
|
await fs.writeFile(
|
||||||
|
path.replace(/\.asm$/, ".hack"),
|
||||||
|
translator.getResult(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (translator.done) {
|
||||||
|
setStatus({
|
||||||
|
message: "Translation done.",
|
||||||
|
severity: "SUCCESS",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return translator.done;
|
||||||
|
},
|
||||||
|
|
||||||
|
compare() {
|
||||||
|
dispatch.current({ action: "compare" });
|
||||||
|
this.updateHighlight(highlightInfo.resultHighlight?.start ?? 0, false);
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHighlight(index: number, fromSource: boolean) {
|
||||||
|
if (failure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [sourceSpan, resultSpan] of highlightInfo.highlightMap) {
|
||||||
|
if (
|
||||||
|
(fromSource &&
|
||||||
|
sourceSpan.start <= index &&
|
||||||
|
index <= sourceSpan.end) ||
|
||||||
|
(!fromSource && resultSpan.start <= index && index <= resultSpan.end)
|
||||||
|
) {
|
||||||
|
highlightInfo.sourceHighlight = sourceSpan;
|
||||||
|
highlightInfo.resultHighlight = resultSpan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
resetHighlightInfo() {
|
||||||
|
highlightInfo.sourceHighlight = undefined;
|
||||||
|
highlightInfo.resultHighlight = undefined;
|
||||||
|
highlightInfo.highlightMap.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
failure = false;
|
||||||
|
translating = false;
|
||||||
|
setStatus("Reset");
|
||||||
|
translator.reset();
|
||||||
|
this.resetHighlightInfo();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.setAsm("");
|
||||||
|
dispatch.current({ action: "setTitle", payload: undefined });
|
||||||
|
dispatch.current({ action: "setCmp", payload: "" });
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
overrideState(state: AsmPageState) {
|
||||||
|
this.resetHighlightInfo();
|
||||||
|
this.setAsm(state.asm, state.path);
|
||||||
|
dispatch.current({
|
||||||
|
action: "setCmp",
|
||||||
|
payload: { cmp: state.compare, name: state.compareName },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.translating) {
|
||||||
|
for (let i = 0; i <= state.current; i++) {
|
||||||
|
this.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: AsmPageState = {
|
||||||
|
asm: "",
|
||||||
|
path: undefined,
|
||||||
|
translating: false,
|
||||||
|
current: -1,
|
||||||
|
resultHighlight: undefined,
|
||||||
|
sourceHighlight: undefined,
|
||||||
|
symbols: [],
|
||||||
|
result: "",
|
||||||
|
compare: "",
|
||||||
|
compareName: undefined,
|
||||||
|
lineNumbers: [],
|
||||||
|
compareError: false,
|
||||||
|
config: {
|
||||||
|
speed: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { initialState, reducers, actions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAsmPageStore() {
|
||||||
|
const { setStatus, fs, localFsRoot } = useContext(BaseContext);
|
||||||
|
|
||||||
|
const dispatch = useRef<AsmStoreDispatch>(() => undefined);
|
||||||
|
|
||||||
|
const { initialState, reducers, actions } = useMemo(
|
||||||
|
() => makeAsmStore(fs, setStatus, dispatch, localFsRoot != undefined),
|
||||||
|
[setStatus, dispatch, fs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||||
|
dispatch.current = dispatcher;
|
||||||
|
|
||||||
|
return { state, dispatch, actions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import {
|
||||||
|
FileSystem,
|
||||||
|
LocalStorageFileSystemAdapter,
|
||||||
|
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { Action, AsyncAction } from "@nand2tetris/simulator/types.js";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useDialog } from "../dialog.js";
|
||||||
|
import { cloneTree } from "../file_utils.js";
|
||||||
|
import {
|
||||||
|
FileSystemAccessFileSystemAdapter,
|
||||||
|
openNand2TetrisDirectory,
|
||||||
|
} from "./base/fs.js";
|
||||||
|
import {
|
||||||
|
attemptLoadAdapterFromIndexedDb,
|
||||||
|
createAndStoreLocalAdapterInIndexedDB,
|
||||||
|
removeLocalAdapterFromIndexedDB,
|
||||||
|
} from "./base/indexDb.js";
|
||||||
|
|
||||||
|
export type StatusSeverity = "SUCCESS" | "WARNING" | "ERROR" | "INFO";
|
||||||
|
|
||||||
|
export interface BaseContext {
|
||||||
|
fs: FileSystem;
|
||||||
|
localFsRoot?: string;
|
||||||
|
canUpgradeFs: boolean;
|
||||||
|
upgradeFs: (force?: boolean, createFiles?: boolean) => Promise<void>;
|
||||||
|
closeFs: () => void;
|
||||||
|
status: { message: string; severity: StatusSeverity };
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>;
|
||||||
|
storage: Record<string, string>;
|
||||||
|
permissionPrompt: ReturnType<typeof useDialog>;
|
||||||
|
requestPermission: AsyncAction<void>;
|
||||||
|
loadFs: Action<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBaseContext(): BaseContext {
|
||||||
|
const localAdapter = useMemo(() => new LocalStorageFileSystemAdapter(), []);
|
||||||
|
const [fs, setFs] = useState(new FileSystem(localAdapter));
|
||||||
|
const [root, setRoot] = useState<string>();
|
||||||
|
|
||||||
|
const permissionPrompt = useDialog();
|
||||||
|
|
||||||
|
const setLocalFs = useCallback(
|
||||||
|
async (handle: FileSystemDirectoryHandle, createFiles = false) => {
|
||||||
|
// We will not mirror the changes in localStorage, since they will be saved in the user's file system
|
||||||
|
const newFs = new FileSystem(
|
||||||
|
new FileSystemAccessFileSystemAdapter(handle),
|
||||||
|
);
|
||||||
|
if (createFiles) {
|
||||||
|
if (root) {
|
||||||
|
const loaders = await import("@nand2tetris/projects/loader.js");
|
||||||
|
await loaders.createFiles(newFs);
|
||||||
|
} else {
|
||||||
|
await cloneTree(fs, newFs, "/projects", (path: string) =>
|
||||||
|
path.replace("/projects", "/").replace(/\/0*(\d+)/, "$1"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFs(newFs);
|
||||||
|
setRoot(handle.name);
|
||||||
|
},
|
||||||
|
[setRoot, setFs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestPermission = async () => {
|
||||||
|
attemptLoadAdapterFromIndexedDb().then(async (adapter) => {
|
||||||
|
if (!adapter) return;
|
||||||
|
await adapter.requestPermission({ mode: "readwrite" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFs = () => {
|
||||||
|
attemptLoadAdapterFromIndexedDb().then(async (adapter) => {
|
||||||
|
if (!adapter) return;
|
||||||
|
setLocalFs(adapter);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (root) return;
|
||||||
|
|
||||||
|
if ("showDirectoryPicker" in window) {
|
||||||
|
attemptLoadAdapterFromIndexedDb().then(async (adapter) => {
|
||||||
|
if (!adapter) return;
|
||||||
|
|
||||||
|
const permissions = await adapter.queryPermission({
|
||||||
|
mode: "readwrite",
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (permissions) {
|
||||||
|
case "granted":
|
||||||
|
setLocalFs(adapter);
|
||||||
|
break;
|
||||||
|
case "prompt":
|
||||||
|
permissionPrompt.open();
|
||||||
|
break;
|
||||||
|
case "denied":
|
||||||
|
setStatus({
|
||||||
|
message:
|
||||||
|
"Permission denied. Please allow access to your file system.",
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [root, setLocalFs]);
|
||||||
|
|
||||||
|
const canUpgradeFs = `showDirectoryPicker` in window;
|
||||||
|
|
||||||
|
const upgradeFs = useCallback(
|
||||||
|
async (force = false, createFiles = false) => {
|
||||||
|
if (!canUpgradeFs || (root && !force)) return;
|
||||||
|
const handler = await openNand2TetrisDirectory();
|
||||||
|
if (root) {
|
||||||
|
await removeLocalAdapterFromIndexedDB();
|
||||||
|
}
|
||||||
|
const adapter = await createAndStoreLocalAdapterInIndexedDB(handler);
|
||||||
|
await setLocalFs(adapter, createFiles);
|
||||||
|
},
|
||||||
|
[root, setLocalFs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeFs = useCallback(async () => {
|
||||||
|
if (!root) return;
|
||||||
|
await removeLocalAdapterFromIndexedDB();
|
||||||
|
setRoot(undefined);
|
||||||
|
setFs(new FileSystem(localAdapter));
|
||||||
|
}, [root]);
|
||||||
|
|
||||||
|
const [status, setStatusInternal] = useState<{
|
||||||
|
message: string;
|
||||||
|
severity: StatusSeverity;
|
||||||
|
}>({ message: "", severity: "INFO" });
|
||||||
|
|
||||||
|
const setStatus = useCallback(
|
||||||
|
(input: string | { message: string; severity?: StatusSeverity }) => {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
setStatusInternal({ message: input, severity: "INFO" });
|
||||||
|
} else {
|
||||||
|
setStatusInternal({
|
||||||
|
message: input.message,
|
||||||
|
severity: input.severity || "INFO",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fs,
|
||||||
|
localFsRoot: root,
|
||||||
|
status,
|
||||||
|
setStatus,
|
||||||
|
storage: localStorage,
|
||||||
|
canUpgradeFs,
|
||||||
|
permissionPrompt,
|
||||||
|
upgradeFs,
|
||||||
|
requestPermission,
|
||||||
|
closeFs,
|
||||||
|
loadFs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseContext = createContext<BaseContext>({
|
||||||
|
fs: new FileSystem(new LocalStorageFileSystemAdapter()),
|
||||||
|
canUpgradeFs: false,
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||||
|
async upgradeFs() {},
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||||
|
closeFs() {},
|
||||||
|
status: { message: "", severity: "INFO" },
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||||
|
setStatus() {},
|
||||||
|
storage: {},
|
||||||
|
permissionPrompt: {} as ReturnType<typeof useDialog>,
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||||
|
async requestPermission() {},
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||||
|
loadFs() {},
|
||||||
|
});
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import {
|
||||||
|
basename,
|
||||||
|
FileSystemAdapter,
|
||||||
|
SEP,
|
||||||
|
Stats,
|
||||||
|
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
|
||||||
|
function dirname(path: string): string {
|
||||||
|
return path.split(SEP).slice(0, -1).join(SEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openNand2TetrisDirectory(): Promise<FileSystemDirectoryHandle> {
|
||||||
|
return window.showDirectoryPicker({
|
||||||
|
id: "nand2tetris",
|
||||||
|
mode: "readwrite",
|
||||||
|
startIn: "documents",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileSystemAccessFileSystemAdapter implements FileSystemAdapter {
|
||||||
|
constructor(private baseDir: FileSystemDirectoryHandle) {}
|
||||||
|
|
||||||
|
async getFolder(
|
||||||
|
path: string,
|
||||||
|
create = false,
|
||||||
|
): Promise<FileSystemDirectoryHandle> {
|
||||||
|
let folder = this.baseDir;
|
||||||
|
const parts = path
|
||||||
|
.split(SEP)
|
||||||
|
.slice(1)
|
||||||
|
.filter((part) => part.trim() != "");
|
||||||
|
for (const part of parts) {
|
||||||
|
folder = await folder.getDirectoryHandle(part, { create });
|
||||||
|
}
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyFile(from: string, to: string): Promise<void> {
|
||||||
|
throw new Error(
|
||||||
|
"unimplemented: FileSystemAccessFileSystemAdapter::copyFile",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdir(path: string): Promise<void> {
|
||||||
|
this.getFolder(path, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(path: string): Promise<string> {
|
||||||
|
const folder = await this.getFolder(dirname(path));
|
||||||
|
const file = await (await folder.getFileHandle(basename(path))).getFile();
|
||||||
|
return file.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(path: string, contents: string): Promise<void> {
|
||||||
|
const folder = await this.getFolder(dirname(path), true);
|
||||||
|
const file = await (
|
||||||
|
await folder.getFileHandle(basename(path), { create: true })
|
||||||
|
).createWritable();
|
||||||
|
await file.write(contents);
|
||||||
|
await file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async readdir(path: string): Promise<string[]> {
|
||||||
|
const folder = await this.getFolder(path);
|
||||||
|
const entries: string[] = [];
|
||||||
|
for await (const [entry, _] of folder.entries()) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async scandir(path: string): Promise<Stats[]> {
|
||||||
|
const folder = await this.getFolder(path);
|
||||||
|
const entries: Stats[] = [];
|
||||||
|
for await (const [name, handle] of folder.entries()) {
|
||||||
|
entries.push({
|
||||||
|
name,
|
||||||
|
isDirectory() {
|
||||||
|
return handle.kind == "directory";
|
||||||
|
},
|
||||||
|
isFile() {
|
||||||
|
return handle.kind == "file";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stat(path: string): Promise<Stats> {
|
||||||
|
const folder = await this.getFolder(dirname(path));
|
||||||
|
for await (const [name, handle] of folder.entries()) {
|
||||||
|
if (name == basename(path)) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
isDirectory() {
|
||||||
|
return handle.kind == "directory";
|
||||||
|
},
|
||||||
|
isFile() {
|
||||||
|
return handle.kind == "file";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: basename(path),
|
||||||
|
isDirectory() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isFile() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async rm(path: string): Promise<void> {
|
||||||
|
const folder = await this.getFolder(dirname(path), true);
|
||||||
|
await folder.removeEntry(basename(path), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChainedFileSystemAdapter implements FileSystemAdapter {
|
||||||
|
constructor(
|
||||||
|
protected adapter: FileSystemAdapter,
|
||||||
|
private nextAdapter?: FileSystemAdapter | undefined,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
stat(path: string): Promise<Stats> {
|
||||||
|
return this.adapter.stat(path).catch((e) => {
|
||||||
|
if (this.nextAdapter) {
|
||||||
|
return this.nextAdapter.stat(path);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
readdir(path: string): Promise<string[]> {
|
||||||
|
return this.adapter.readdir(path).catch((e) => {
|
||||||
|
if (this.nextAdapter) {
|
||||||
|
return this.nextAdapter.readdir(path);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
scandir(path: string): Promise<Stats[]> {
|
||||||
|
return this.adapter.scandir(path).catch((e) => {
|
||||||
|
if (this.nextAdapter) {
|
||||||
|
return this.nextAdapter.scandir(path);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdir(path: string): Promise<void> {
|
||||||
|
if (this.nextAdapter) await this.nextAdapter.mkdir(path);
|
||||||
|
return this.adapter.mkdir(path);
|
||||||
|
}
|
||||||
|
async copyFile(from: string, to: string): Promise<void> {
|
||||||
|
if (this.nextAdapter) await this.nextAdapter.copyFile(from, to);
|
||||||
|
return this.adapter.copyFile(from, to);
|
||||||
|
}
|
||||||
|
readFile(path: string): Promise<string> {
|
||||||
|
return this.adapter.readFile(path).catch((e) => {
|
||||||
|
if (this.nextAdapter) {
|
||||||
|
return this.nextAdapter.readFile(path);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async writeFile(path: string, contents: string): Promise<void> {
|
||||||
|
if (this.nextAdapter) await this.nextAdapter?.writeFile(path, contents);
|
||||||
|
return this.adapter.writeFile(path, contents);
|
||||||
|
}
|
||||||
|
async rm(path: string): Promise<void> {
|
||||||
|
if (this.nextAdapter) await this.nextAdapter.rm(path);
|
||||||
|
return this.adapter.rm(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||||
|
|
||||||
|
const IDB_NAME = "NAND2TetrisIndexedDB";
|
||||||
|
const IDB_VERSION = 1;
|
||||||
|
const IDB_FS_ADAPTER_OBJECT_STORE = "FileSystemAccess";
|
||||||
|
const IDB_FS_ADAPTER_KEY = "Handler";
|
||||||
|
function openIndexedDb(): Promise<IDBDatabase> {
|
||||||
|
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||||
|
const request = window.indexedDB.open(IDB_NAME, IDB_VERSION);
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(request.result);
|
||||||
|
};
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(request.error);
|
||||||
|
};
|
||||||
|
request.onupgradeneeded = (e) => {
|
||||||
|
request.result.createObjectStore(IDB_FS_ADAPTER_OBJECT_STORE);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function attemptLoadAdapterFromIndexedDb(): Promise<FileSystemDirectoryHandle | void> {
|
||||||
|
const db = await openIndexedDb();
|
||||||
|
return new Promise<FileSystemDirectoryHandle | void>((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(
|
||||||
|
[IDB_FS_ADAPTER_OBJECT_STORE],
|
||||||
|
"readonly",
|
||||||
|
);
|
||||||
|
const objectStore = transaction.objectStore(IDB_FS_ADAPTER_OBJECT_STORE);
|
||||||
|
const handleRequest = objectStore.get(IDB_FS_ADAPTER_KEY);
|
||||||
|
handleRequest.onsuccess = () => {
|
||||||
|
const handle = handleRequest.result;
|
||||||
|
if (handle === undefined) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
handle instanceof FileSystemDirectoryHandle,
|
||||||
|
`Retrieved ${IDB_FS_ADAPTER_KEY} in ${IDB_FS_ADAPTER_OBJECT_STORE} in ${IDB_NAME} is not a FileSystemDirectoryHandle`,
|
||||||
|
);
|
||||||
|
resolve(handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
transaction.onerror = () => {
|
||||||
|
console.error("Error in loading FileSystemDirectoryHandle transaction", {
|
||||||
|
err: transaction.error,
|
||||||
|
});
|
||||||
|
reject(transaction.error);
|
||||||
|
};
|
||||||
|
handleRequest.onerror = () => {
|
||||||
|
console.error("Error in FileSystemDirectoryHandle handleRequest", {
|
||||||
|
err: handleRequest.error,
|
||||||
|
});
|
||||||
|
reject(handleRequest.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAndStoreLocalAdapterInIndexedDB(
|
||||||
|
handle: FileSystemDirectoryHandle,
|
||||||
|
): Promise<FileSystemDirectoryHandle> {
|
||||||
|
const db = await openIndexedDb();
|
||||||
|
const transaction = db.transaction(
|
||||||
|
[IDB_FS_ADAPTER_OBJECT_STORE],
|
||||||
|
"readwrite",
|
||||||
|
);
|
||||||
|
transaction
|
||||||
|
.objectStore(IDB_FS_ADAPTER_OBJECT_STORE)
|
||||||
|
.add(handle, IDB_FS_ADAPTER_KEY);
|
||||||
|
transaction.commit();
|
||||||
|
return new Promise<FileSystemDirectoryHandle>((resolve, reject) => {
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
resolve(handle);
|
||||||
|
};
|
||||||
|
transaction.onerror = () => {
|
||||||
|
reject(transaction.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeLocalAdapterFromIndexedDB() {
|
||||||
|
const db = await openIndexedDb();
|
||||||
|
const transaction = db.transaction(
|
||||||
|
[IDB_FS_ADAPTER_OBJECT_STORE],
|
||||||
|
"readwrite",
|
||||||
|
);
|
||||||
|
transaction
|
||||||
|
.objectStore(IDB_FS_ADAPTER_OBJECT_STORE)
|
||||||
|
.delete(IDB_FS_ADAPTER_KEY);
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
transaction.onerror = () => {
|
||||||
|
reject(transaction.error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import {
|
||||||
|
FileSystem,
|
||||||
|
ObjectFileSystemAdapter,
|
||||||
|
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { cleanState } from "@davidsouther/jiffies/lib/esm/scope/state.js";
|
||||||
|
import * as not from "@nand2tetris/projects/project_01/01_not.js";
|
||||||
|
import { produce } from "immer";
|
||||||
|
import { MutableRefObject } from "react";
|
||||||
|
import { ImmPin } from "src/pinout.js";
|
||||||
|
import { ChipStoreDispatch, makeChipStore } from "./chip.store.js";
|
||||||
|
|
||||||
|
function testChipStore(
|
||||||
|
fs: Record<string, string> = {
|
||||||
|
"projects/01/Not.hdl": not.hdl,
|
||||||
|
"projects/01/Not.tst": not.tst,
|
||||||
|
"projects/01/Not.cmp": not.cmp,
|
||||||
|
},
|
||||||
|
storage: Record<string, string> = {},
|
||||||
|
) {
|
||||||
|
const dispatch: MutableRefObject<ChipStoreDispatch> = { current: jest.fn() };
|
||||||
|
|
||||||
|
const setStatus = jest.fn();
|
||||||
|
|
||||||
|
const { initialState, actions, reducers } = makeChipStore(
|
||||||
|
new FileSystem(new ObjectFileSystemAdapter(fs)),
|
||||||
|
setStatus,
|
||||||
|
storage,
|
||||||
|
dispatch,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const store = { state: initialState, actions, reducers, dispatch, setStatus };
|
||||||
|
dispatch.current = jest.fn().mockImplementation(
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: covariants are hard
|
||||||
|
(command: { action: keyof typeof reducers; payload: any }) => {
|
||||||
|
store.state = produce(store.state, (draft: typeof initialState) => {
|
||||||
|
reducers[command.action](draft, command.payload);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ChipStore", () => {
|
||||||
|
describe("initialization", () => {
|
||||||
|
it("loads chip", async () => {
|
||||||
|
const store = testChipStore({
|
||||||
|
"projects/01/Not.hdl": not.hdl,
|
||||||
|
"projects/01/Not.tst": not.tst,
|
||||||
|
"projects/01/Not.cmp": not.cmp,
|
||||||
|
});
|
||||||
|
|
||||||
|
await store.actions.initialize();
|
||||||
|
await store.actions.loadChip("projects/01/Not.hdl");
|
||||||
|
|
||||||
|
expect(store.state.controls.project).toBe("01");
|
||||||
|
expect(store.state.controls.chipName).toBe("Not");
|
||||||
|
expect(store.state.files.hdl).toBe(not.hdl);
|
||||||
|
expect(store.state.files.tst).toBe(not.tst);
|
||||||
|
expect(store.state.files.cmp).toBe("");
|
||||||
|
expect(store.state.files.out).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("behavior", () => {
|
||||||
|
const state = cleanState(async () => {
|
||||||
|
const store = testChipStore({
|
||||||
|
"projects/01/Not.hdl": not.hdl,
|
||||||
|
"projects/01/Not.tst": not.tst,
|
||||||
|
"projects/01/Not.cmp": not.cmp,
|
||||||
|
});
|
||||||
|
await store.actions.initialize();
|
||||||
|
await store.actions.loadChip("projects/01/Not.hdl");
|
||||||
|
return { store };
|
||||||
|
}, beforeEach);
|
||||||
|
|
||||||
|
it.todo("loads projects and chips");
|
||||||
|
|
||||||
|
it("toggles bits", async () => {
|
||||||
|
state.store.actions.toggle(state.store.state.sim.chip[0].in(), 0);
|
||||||
|
expect(state.store.state.sim.chip[0].in().busVoltage).toBe(1);
|
||||||
|
expect(state.store.dispatch.current).toHaveBeenCalledWith({
|
||||||
|
action: "updateChip",
|
||||||
|
payload: { pending: true },
|
||||||
|
});
|
||||||
|
expect(state.store.state.sim.pending).toBe(true);
|
||||||
|
|
||||||
|
state.store.actions.eval();
|
||||||
|
expect(state.store.dispatch.current).toHaveBeenCalledWith({
|
||||||
|
action: "updateChip",
|
||||||
|
payload: { pending: false },
|
||||||
|
});
|
||||||
|
expect(state.store.state.sim.pending).toBe(false);
|
||||||
|
expect(state.store.state.sim.chip[0].out().busVoltage).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("execution", () => {
|
||||||
|
const state = cleanState(async () => {
|
||||||
|
const store = testChipStore({
|
||||||
|
"projects/01/Not.hdl": not.hdl,
|
||||||
|
"projects/01/Not.tst": not.tst,
|
||||||
|
"projects/01/Not.cmp": not.cmp,
|
||||||
|
});
|
||||||
|
await store.actions.initialize();
|
||||||
|
await store.actions.loadChip("projects/01/Not.hdl");
|
||||||
|
return { store };
|
||||||
|
}, beforeEach);
|
||||||
|
|
||||||
|
it.todo("compiles chips");
|
||||||
|
|
||||||
|
it("steps tests", async () => {
|
||||||
|
const bits = (pins: ImmPin[]) =>
|
||||||
|
pins.map((pin) => pin.bits.map((bit) => bit[1]));
|
||||||
|
|
||||||
|
expect(bits(state.store.state.sim.inPins)).toEqual([[0]]);
|
||||||
|
expect(bits(state.store.state.sim.outPins)).toEqual([[0]]);
|
||||||
|
|
||||||
|
await state.store.actions.toggleBuiltin();
|
||||||
|
|
||||||
|
expect(bits(state.store.state.sim.inPins)).toEqual([[0]]);
|
||||||
|
expect(bits(state.store.state.sim.outPins)).toEqual([[1]]);
|
||||||
|
|
||||||
|
await state.store.actions.stepTest(); // Load, Compare To and Output List
|
||||||
|
|
||||||
|
await state.store.actions.stepTest(); // Set in 0
|
||||||
|
expect(bits(state.store.state.sim.inPins)).toEqual([[0]]);
|
||||||
|
expect(bits(state.store.state.sim.outPins)).toEqual([[1]]);
|
||||||
|
|
||||||
|
await state.store.actions.stepTest(); // Set in 1
|
||||||
|
expect(bits(state.store.state.sim.inPins)).toEqual([[1]]);
|
||||||
|
expect(bits(state.store.state.sim.outPins)).toEqual([[0]]);
|
||||||
|
|
||||||
|
await state.store.actions.stepTest(); // No change (after end)
|
||||||
|
expect(bits(state.store.state.sim.inPins)).toEqual([[1]]);
|
||||||
|
expect(bits(state.store.state.sim.outPins)).toEqual([[0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts the cursor on the first instruction", () => {
|
||||||
|
expect(state.store.state.files.tst).toBe(not.tst);
|
||||||
|
expect(state.store.state.controls.span).toEqual({
|
||||||
|
start: 167,
|
||||||
|
end: 220,
|
||||||
|
line: 6,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves the cursor on the final character", async () => {
|
||||||
|
// Not.tst has 3 commands
|
||||||
|
await state.store.actions.stepTest();
|
||||||
|
await state.store.actions.stepTest();
|
||||||
|
await state.store.actions.stepTest();
|
||||||
|
|
||||||
|
// Past the end of the test
|
||||||
|
await state.store.actions.stepTest();
|
||||||
|
|
||||||
|
expect(state.store.state.controls.span).toEqual({
|
||||||
|
start: 269,
|
||||||
|
end: 270,
|
||||||
|
line: 16,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||||
|
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { Err, isErr, Ok } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
import {
|
||||||
|
BUILTIN_CHIP_PROJECTS,
|
||||||
|
CHIP_PROJECTS,
|
||||||
|
sortChips,
|
||||||
|
} from "@nand2tetris/projects/base.js";
|
||||||
|
import { parse as parseChip } from "@nand2tetris/simulator/chip/builder.js";
|
||||||
|
import { getBuiltinChip } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||||
|
import {
|
||||||
|
Chip,
|
||||||
|
Low,
|
||||||
|
Pin,
|
||||||
|
Chip as SimChip,
|
||||||
|
} from "@nand2tetris/simulator/chip/chip.js";
|
||||||
|
import { Clock } from "@nand2tetris/simulator/chip/clock.js";
|
||||||
|
import {
|
||||||
|
CompilationError,
|
||||||
|
Span,
|
||||||
|
} from "@nand2tetris/simulator/languages/base.js";
|
||||||
|
import { TST } from "@nand2tetris/simulator/languages/tst.js";
|
||||||
|
import { ChipTest } from "@nand2tetris/simulator/test/chiptst.js";
|
||||||
|
import { Action } from "@nand2tetris/simulator/types.js";
|
||||||
|
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||||
|
import { compare } from "../compare.js";
|
||||||
|
import { sortFiles } from "../file_utils.js";
|
||||||
|
import { ImmPin, reducePins } from "../pinout.js";
|
||||||
|
import { useImmerReducer } from "../react.js";
|
||||||
|
import { RunSpeed } from "../runbar.js";
|
||||||
|
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||||
|
|
||||||
|
export const NO_SCREEN = "noScreen";
|
||||||
|
|
||||||
|
export const PROJECT_NAMES = [
|
||||||
|
["01", `Project 1`],
|
||||||
|
["02", `Project 2`],
|
||||||
|
["03", `Project 3`],
|
||||||
|
["05", `Project 5`],
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEST_NAMES: Record<string, string[]> = {
|
||||||
|
CPU: ["CPU", "CPU-external"],
|
||||||
|
Computer: ["ComputerAdd", "ComputerMax", "ComputerRect"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isBuiltinOnly(
|
||||||
|
project: keyof typeof CHIP_PROJECTS,
|
||||||
|
chipName: string,
|
||||||
|
) {
|
||||||
|
return BUILTIN_CHIP_PROJECTS[project].includes(chipName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToBuiltin(name: string, hdl: string) {
|
||||||
|
return hdl.replace(/PARTS:([\s\S]*?)\}/, `PARTS:\n\tBUILTIN ${name};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChipPageState {
|
||||||
|
title?: string;
|
||||||
|
files: Files;
|
||||||
|
sim: ChipSim;
|
||||||
|
controls: ControlsState;
|
||||||
|
config: ChipPageConfig;
|
||||||
|
dir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChipPageConfig {
|
||||||
|
speed: RunSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChipSim {
|
||||||
|
clocked: boolean;
|
||||||
|
inPins: ImmPin[];
|
||||||
|
outPins: ImmPin[];
|
||||||
|
internalPins: ImmPin[];
|
||||||
|
chip: [Chip];
|
||||||
|
pending: boolean;
|
||||||
|
invalid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Files {
|
||||||
|
hdl: string;
|
||||||
|
cmp: string;
|
||||||
|
tst: string;
|
||||||
|
out: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControlsState {
|
||||||
|
projects: string[];
|
||||||
|
project: string;
|
||||||
|
chips: string[];
|
||||||
|
chipName: string;
|
||||||
|
tests: string[];
|
||||||
|
testName: string;
|
||||||
|
usingBuiltin: boolean;
|
||||||
|
runningTest: boolean;
|
||||||
|
span?: Span;
|
||||||
|
error?: CompilationError;
|
||||||
|
visualizationParameters: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HDLFile {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduceChip(chip: SimChip, pending = false, invalid = false): ChipSim {
|
||||||
|
return {
|
||||||
|
clocked: chip.clocked,
|
||||||
|
inPins: reducePins(chip.ins),
|
||||||
|
outPins: reducePins(chip.outs),
|
||||||
|
internalPins: reducePins(chip.pins),
|
||||||
|
chip: [chip],
|
||||||
|
pending,
|
||||||
|
invalid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const clock = Clock.get();
|
||||||
|
|
||||||
|
export type ChipStoreDispatch = Dispatch<{
|
||||||
|
action: keyof ReturnType<typeof makeChipStore>["reducers"];
|
||||||
|
payload?: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function makeChipStore(
|
||||||
|
fs: FileSystem,
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||||
|
storage: Record<string, string>,
|
||||||
|
dispatch: MutableRefObject<ChipStoreDispatch>,
|
||||||
|
upgraded: boolean,
|
||||||
|
) {
|
||||||
|
let _chipName = "";
|
||||||
|
let _dir = "";
|
||||||
|
let chip = new Low();
|
||||||
|
let backupHdl = "";
|
||||||
|
let tests: string[] = [];
|
||||||
|
let test = new ChipTest();
|
||||||
|
let usingBuiltin = false;
|
||||||
|
let invalid = false;
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
setFiles(
|
||||||
|
state: ChipPageState,
|
||||||
|
{
|
||||||
|
hdl = state.files.hdl,
|
||||||
|
tst = state.files.tst,
|
||||||
|
cmp = state.files.cmp,
|
||||||
|
out = "",
|
||||||
|
}: {
|
||||||
|
hdl?: string;
|
||||||
|
tst?: string;
|
||||||
|
cmp?: string;
|
||||||
|
out?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
state.files.hdl = hdl;
|
||||||
|
state.files.tst = tst;
|
||||||
|
state.files.cmp = cmp;
|
||||||
|
state.files.out = out;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateChip(
|
||||||
|
state: ChipPageState,
|
||||||
|
payload?: {
|
||||||
|
pending?: boolean;
|
||||||
|
invalid?: boolean;
|
||||||
|
chipName?: string;
|
||||||
|
error?: CompilationError | undefined;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
state.sim = reduceChip(
|
||||||
|
chip,
|
||||||
|
payload?.pending ?? state.sim.pending,
|
||||||
|
payload?.invalid ?? state.sim.invalid,
|
||||||
|
);
|
||||||
|
state.controls.error = state.sim.invalid
|
||||||
|
? (payload?.error ?? state.controls.error)
|
||||||
|
: undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
setProjects(state: ChipPageState, projects: string[]) {
|
||||||
|
state.controls.projects = projects;
|
||||||
|
},
|
||||||
|
|
||||||
|
setProject(state: ChipPageState, project: keyof typeof CHIP_PROJECTS) {
|
||||||
|
state.controls.project = project;
|
||||||
|
},
|
||||||
|
|
||||||
|
setChips(state: ChipPageState, chips: string[]) {
|
||||||
|
state.controls.chips = chips;
|
||||||
|
},
|
||||||
|
|
||||||
|
setChip(
|
||||||
|
state: ChipPageState,
|
||||||
|
{ chipName, dir }: { chipName: string; dir: string },
|
||||||
|
) {
|
||||||
|
_dir = dir;
|
||||||
|
_chipName = chipName;
|
||||||
|
state.controls.chipName = chipName;
|
||||||
|
state.title = `${chipName}.hdl`;
|
||||||
|
state.controls.tests = Array.from(tests);
|
||||||
|
state.dir = dir;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearChip(state: ChipPageState) {
|
||||||
|
_chipName = "";
|
||||||
|
state.controls.chipName = "";
|
||||||
|
state.title = undefined;
|
||||||
|
state.controls.tests = [];
|
||||||
|
|
||||||
|
this.setFiles(state, { hdl: "", tst: "", cmp: "", out: "" });
|
||||||
|
},
|
||||||
|
|
||||||
|
setTest(state: ChipPageState, testName: string) {
|
||||||
|
state.controls.testName = testName;
|
||||||
|
},
|
||||||
|
|
||||||
|
setVisualizationParams(state: ChipPageState, params: Set<string>) {
|
||||||
|
state.controls.visualizationParameters = new Set(params);
|
||||||
|
},
|
||||||
|
|
||||||
|
testRunning(state: ChipPageState) {
|
||||||
|
state.controls.runningTest = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
testFinished(state: ChipPageState) {
|
||||||
|
state.controls.runningTest = false;
|
||||||
|
const passed = compare(state.files.cmp.trim(), state.files.out.trim());
|
||||||
|
// For some reason, this is happening during a render but I can't track it down.
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
setStatus(
|
||||||
|
passed
|
||||||
|
? {
|
||||||
|
message: `Simulation successful: The output file is identical to the compare file`,
|
||||||
|
severity: "SUCCESS",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
message: `Simulation error: The output file differs from the compare file`,
|
||||||
|
severity: "ERROR",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTestStep(state: ChipPageState) {
|
||||||
|
state.files.out = test?.log() ?? "";
|
||||||
|
if (test?.currentStep?.span) {
|
||||||
|
state.controls.span = test.currentStep.span;
|
||||||
|
} else {
|
||||||
|
if (test.done) {
|
||||||
|
const end = state.files.tst.length;
|
||||||
|
state.controls.span = {
|
||||||
|
start: end - 1,
|
||||||
|
end,
|
||||||
|
line: state.files.tst.split("\n").length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateChip(state, {
|
||||||
|
pending: state.sim.pending,
|
||||||
|
invalid: state.sim.invalid,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateConfig(state: ChipPageState, config: Partial<ChipPageConfig>) {
|
||||||
|
state.config = { ...state.config, ...config };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUsingBuiltin(state: ChipPageState) {
|
||||||
|
state.controls.usingBuiltin = usingBuiltin;
|
||||||
|
},
|
||||||
|
|
||||||
|
displayBuiltin(state: ChipPageState) {
|
||||||
|
backupHdl = state.files.hdl;
|
||||||
|
this.setFiles(state, {
|
||||||
|
hdl: convertToBuiltin(state.controls.chipName, state.files.hdl),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBuiltin(state: ChipPageState) {
|
||||||
|
state.controls.usingBuiltin = usingBuiltin;
|
||||||
|
if (usingBuiltin) {
|
||||||
|
this.displayBuiltin(state);
|
||||||
|
} else {
|
||||||
|
this.setFiles(state, { hdl: backupHdl });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
async initialize() {
|
||||||
|
const projectsFolder = upgraded ? "/" : "/projects";
|
||||||
|
|
||||||
|
const entries = await fs.scandir(projectsFolder);
|
||||||
|
const hdlProjects = [];
|
||||||
|
|
||||||
|
for (const project of entries.filter((project) =>
|
||||||
|
project.isDirectory(),
|
||||||
|
)) {
|
||||||
|
const items = await fs.scandir(`${projectsFolder}/${project.name}`);
|
||||||
|
if (items.some((item) => item.isFile() && item.name.endsWith(".hdl"))) {
|
||||||
|
hdlProjects.push(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedNames = sortFiles(hdlProjects).map((project) => project.name);
|
||||||
|
|
||||||
|
dispatch.current({
|
||||||
|
action: "setProjects",
|
||||||
|
payload: sortedNames,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hdlProjects.length > 0) {
|
||||||
|
await actions.setProject(sortedNames[0]);
|
||||||
|
} else {
|
||||||
|
dispatch.current({ action: "setChips", payload: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch.current({ action: "clearChip" });
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
Clock.get().reset();
|
||||||
|
chip.reset();
|
||||||
|
test.reset();
|
||||||
|
dispatch.current({ action: "setFiles", payload: {} });
|
||||||
|
dispatch.current({ action: "updateChip" });
|
||||||
|
},
|
||||||
|
|
||||||
|
async setProject(project: string) {
|
||||||
|
storage["/chip/project"] = project;
|
||||||
|
dispatch.current({ action: "setProject", payload: project });
|
||||||
|
|
||||||
|
const prefix = upgraded ? "/" : "/projects";
|
||||||
|
|
||||||
|
const chips = (await fs.scandir(`${prefix}/${project}`))
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".hdl"))
|
||||||
|
.map((file) => file.name.replace(".hdl", ""));
|
||||||
|
|
||||||
|
const payload = sortChips(project, chips);
|
||||||
|
|
||||||
|
dispatch.current({ action: "setChips", payload });
|
||||||
|
|
||||||
|
if (chips.length > 0) {
|
||||||
|
this.loadChip(`${prefix}/${project}/${chips[0]}.hdl`, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadChip(path: string, loadTests = true) {
|
||||||
|
dispatch.current({ action: "updateUsingBuiltin", payload: false });
|
||||||
|
|
||||||
|
const hdl = await fs.readFile(path);
|
||||||
|
|
||||||
|
const parts = path.split("/");
|
||||||
|
const name = assertExists(parts.pop()).replace(".hdl", "");
|
||||||
|
const dir = parts.join("/");
|
||||||
|
|
||||||
|
await this.compileChip(hdl, dir, name);
|
||||||
|
|
||||||
|
if (loadTests) {
|
||||||
|
await this.initializeTests(dir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch.current({
|
||||||
|
action: "setChip",
|
||||||
|
payload: { chipName: name, dir: dir },
|
||||||
|
});
|
||||||
|
dispatch.current({ action: "setFiles", payload: { hdl } });
|
||||||
|
|
||||||
|
if (usingBuiltin) {
|
||||||
|
this.loadBuiltin();
|
||||||
|
dispatch.current({ action: "displayBuiltin" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async compileChip(hdl: string, dir?: string, name?: string) {
|
||||||
|
chip.remove();
|
||||||
|
const maybeChip = await parseChip(hdl, dir, name, fs);
|
||||||
|
if (isErr(maybeChip)) {
|
||||||
|
const error = Err(maybeChip);
|
||||||
|
setStatus({
|
||||||
|
message: Err(maybeChip).message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
invalid = true;
|
||||||
|
dispatch.current({
|
||||||
|
action: "updateChip",
|
||||||
|
payload: { invalid: true, error },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.replaceChip(Ok(maybeChip));
|
||||||
|
},
|
||||||
|
|
||||||
|
replaceChip(nextChip: SimChip) {
|
||||||
|
// Store current inPins
|
||||||
|
const inPins = chip.ins;
|
||||||
|
for (const [pin, { busVoltage }] of inPins) {
|
||||||
|
const nextPin = nextChip.ins.get(pin);
|
||||||
|
if (nextPin) {
|
||||||
|
nextPin.busVoltage = busVoltage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clock.reset();
|
||||||
|
nextChip.eval();
|
||||||
|
chip = nextChip;
|
||||||
|
chip.reset();
|
||||||
|
dispatch.current({ action: "updateChip", payload: { invalid: false } });
|
||||||
|
dispatch.current({ action: "updateTestStep" });
|
||||||
|
},
|
||||||
|
|
||||||
|
async initializeTests(dir: string, chip: string) {
|
||||||
|
tests = TEST_NAMES[chip] ?? [];
|
||||||
|
this.loadTest(tests[0] ?? chip, dir);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTest(name: string, dir?: string) {
|
||||||
|
if (!fs) return;
|
||||||
|
try {
|
||||||
|
dir ??= _dir;
|
||||||
|
|
||||||
|
const tst = await fs.readFile(`${dir}/${name}.tst`);
|
||||||
|
|
||||||
|
dispatch.current({ action: "setFiles", payload: { tst, cmp: "" } });
|
||||||
|
dispatch.current({ action: "setTest", payload: name });
|
||||||
|
this.compileTest(tst, dir);
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: `Could not find ${name}.tst. Please load test file separately.`,
|
||||||
|
severity: "WARNING",
|
||||||
|
});
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compileTest(file: string, path: string) {
|
||||||
|
if (!fs) return;
|
||||||
|
dispatch.current({ action: "setFiles", payload: { tst: file } });
|
||||||
|
const tst = TST.parse(file);
|
||||||
|
if (isErr(tst)) {
|
||||||
|
setStatus({
|
||||||
|
message: `Failed to parse test ${Err(tst).message}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
invalid = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const maybeTest = ChipTest.from(Ok(tst), {
|
||||||
|
dir: path,
|
||||||
|
setStatus: setStatus,
|
||||||
|
loadAction: async (file) => {
|
||||||
|
await this.loadChip(file, false);
|
||||||
|
return chip;
|
||||||
|
},
|
||||||
|
compareTo: async (file) => {
|
||||||
|
const cmp = await fs.readFile(`${_dir}/${file}`);
|
||||||
|
dispatch.current({ action: "setFiles", payload: { cmp } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (isErr(maybeTest)) {
|
||||||
|
invalid = true;
|
||||||
|
setStatus({
|
||||||
|
message: Err(maybeTest).message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
test = Ok(maybeTest).with(chip).reset();
|
||||||
|
test.setFileSystem(fs);
|
||||||
|
dispatch.current({ action: "updateTestStep" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateFiles({
|
||||||
|
hdl,
|
||||||
|
tst,
|
||||||
|
cmp,
|
||||||
|
tstPath,
|
||||||
|
}: {
|
||||||
|
hdl?: string;
|
||||||
|
tst?: string;
|
||||||
|
cmp: string;
|
||||||
|
tstPath?: string;
|
||||||
|
}) {
|
||||||
|
invalid = false;
|
||||||
|
dispatch.current({ action: "setFiles", payload: { hdl, tst, cmp } });
|
||||||
|
try {
|
||||||
|
if (hdl) {
|
||||||
|
await this.compileChip(hdl, _dir, _chipName);
|
||||||
|
}
|
||||||
|
if (tst) {
|
||||||
|
this.compileTest(tst, tstPath ?? _dir);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: display(e),
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "updateChip", payload: { invalid: invalid } });
|
||||||
|
if (!invalid) {
|
||||||
|
setStatus(`HDL code: No syntax errors`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveChip(hdl: string) {
|
||||||
|
dispatch.current({ action: "setFiles", payload: { hdl } });
|
||||||
|
const path = `${_dir}/${_chipName}.hdl`;
|
||||||
|
if (fs && path) {
|
||||||
|
await fs.writeFile(path, hdl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(pin: Pin, i: number | undefined) {
|
||||||
|
if (i !== undefined) {
|
||||||
|
pin.busVoltage = pin.busVoltage ^ (1 << i);
|
||||||
|
} else {
|
||||||
|
if (pin.width === 1) {
|
||||||
|
pin.toggle();
|
||||||
|
} else {
|
||||||
|
pin.busVoltage += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "updateChip", payload: { pending: true } });
|
||||||
|
},
|
||||||
|
|
||||||
|
eval() {
|
||||||
|
chip.eval();
|
||||||
|
dispatch.current({ action: "updateChip", payload: { pending: false } });
|
||||||
|
},
|
||||||
|
|
||||||
|
clock() {
|
||||||
|
clock.toggle();
|
||||||
|
if (clock.isLow) {
|
||||||
|
clock.frame();
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "updateChip" });
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadBuiltin() {
|
||||||
|
const builtinName = _chipName;
|
||||||
|
const nextChip = await getBuiltinChip(builtinName);
|
||||||
|
if (isErr(nextChip)) {
|
||||||
|
setStatus({
|
||||||
|
message: `Failed to load builtin ${builtinName}: ${display(Err(nextChip))}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.replaceChip(Ok(nextChip));
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleBuiltin() {
|
||||||
|
usingBuiltin = !usingBuiltin;
|
||||||
|
dispatch.current({ action: "toggleBuiltin" });
|
||||||
|
if (usingBuiltin) {
|
||||||
|
await this.loadBuiltin();
|
||||||
|
} else {
|
||||||
|
await this.compileChip(backupHdl, _dir, _chipName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
tick(): Promise<boolean> {
|
||||||
|
return this.stepTest();
|
||||||
|
},
|
||||||
|
|
||||||
|
async stepTest(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const done = await test.step();
|
||||||
|
dispatch.current({ action: "updateTestStep" });
|
||||||
|
if (done) {
|
||||||
|
dispatch.current({ action: "testFinished" });
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: (e as Error).message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getProjectFiles() {
|
||||||
|
console.log(_dir);
|
||||||
|
|
||||||
|
return (await fs.scandir(_dir))
|
||||||
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".hdl"))
|
||||||
|
.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
content: fs.readFile(`${_dir}/${entry.name}`),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: ChipPageState = (() => {
|
||||||
|
const controls: ControlsState = {
|
||||||
|
projects: ["1", "2", "3", "5"],
|
||||||
|
project: "1",
|
||||||
|
chips: [],
|
||||||
|
chipName: "",
|
||||||
|
tests,
|
||||||
|
testName: "",
|
||||||
|
usingBuiltin: false,
|
||||||
|
runningTest: false,
|
||||||
|
error: undefined,
|
||||||
|
visualizationParameters: new Set(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sim = reduceChip(new Low());
|
||||||
|
|
||||||
|
return {
|
||||||
|
controls,
|
||||||
|
files: {
|
||||||
|
hdl: "",
|
||||||
|
cmp: "",
|
||||||
|
tst: "",
|
||||||
|
out: "",
|
||||||
|
backupHdl: "",
|
||||||
|
},
|
||||||
|
sim,
|
||||||
|
config: { speed: 2 },
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { initialState, reducers, actions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChipPageStore() {
|
||||||
|
const { fs, setStatus, storage, localFsRoot } = useContext(BaseContext);
|
||||||
|
|
||||||
|
const dispatch = useRef<ChipStoreDispatch>(() => undefined);
|
||||||
|
|
||||||
|
const { initialState, reducers, actions } = useMemo(
|
||||||
|
() =>
|
||||||
|
makeChipStore(fs, setStatus, storage, dispatch, localFsRoot != undefined),
|
||||||
|
[fs, setStatus, storage, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||||
|
dispatch.current = dispatcher;
|
||||||
|
|
||||||
|
return { state, dispatch, actions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { compile } from "@nand2tetris/simulator/jack/compiler.js";
|
||||||
|
import { CompilationError } from "@nand2tetris/simulator/languages/base.js";
|
||||||
|
import { Action } from "@nand2tetris/simulator/types.js";
|
||||||
|
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||||
|
import { useImmerReducer } from "../react.js";
|
||||||
|
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||||
|
|
||||||
|
export interface CompiledFile {
|
||||||
|
vm?: string;
|
||||||
|
valid: boolean;
|
||||||
|
error?: CompilationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompilerPageState {
|
||||||
|
fs?: FileSystem;
|
||||||
|
files: Record<string, string>;
|
||||||
|
compiled: Record<string, CompiledFile>;
|
||||||
|
isValid: boolean;
|
||||||
|
isCompiled: boolean;
|
||||||
|
selected: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CompilerStoreDispatch = Dispatch<{
|
||||||
|
action: keyof ReturnType<typeof makeCompilerStore>["reducers"];
|
||||||
|
payload?: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function classTemplate(name: string) {
|
||||||
|
return `class ${name} {\n\n}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeCompilerStore(
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||||
|
dispatch: MutableRefObject<CompilerStoreDispatch>,
|
||||||
|
) {
|
||||||
|
let fs: FileSystem | undefined;
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
setFs(state: CompilerPageState, fs: FileSystem) {
|
||||||
|
state.fs = fs;
|
||||||
|
},
|
||||||
|
reset(state: CompilerPageState) {
|
||||||
|
state.files = {};
|
||||||
|
state.title = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
setFile(
|
||||||
|
state: CompilerPageState,
|
||||||
|
{ name, content }: { name: string; content: string },
|
||||||
|
) {
|
||||||
|
state.files[name] = content;
|
||||||
|
state.isCompiled = false;
|
||||||
|
this.compile(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
// the keys of 'files' have to be the full file path, not basename
|
||||||
|
setFiles(state: CompilerPageState, files: Record<string, string>) {
|
||||||
|
state.files = files;
|
||||||
|
state.isCompiled = false;
|
||||||
|
this.compile(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
compile(state: CompilerPageState) {
|
||||||
|
const compiledFiles = compile(state.files);
|
||||||
|
state.compiled = {};
|
||||||
|
for (const [name, compiled] of Object.entries(compiledFiles)) {
|
||||||
|
if (typeof compiled === "string") {
|
||||||
|
state.compiled[name] = {
|
||||||
|
valid: true,
|
||||||
|
vm: compiled,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
state.compiled[name] = {
|
||||||
|
valid: false,
|
||||||
|
error: compiled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.isValid = Object.keys(state.files).every(
|
||||||
|
(file) => state.compiled[file].valid,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
writeCompiled(state: CompilerPageState) {
|
||||||
|
if (Object.values(state.compiled).every((compiled) => compiled.valid)) {
|
||||||
|
for (const [name, compiled] of Object.entries(state.compiled)) {
|
||||||
|
if (compiled.vm) {
|
||||||
|
fs?.writeFile(`${name}.vm`, compiled.vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.isCompiled = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelected(state: CompilerPageState, selected: string) {
|
||||||
|
state.selected = selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
setTitle(state: CompilerPageState, title: string) {
|
||||||
|
state.title = title;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
async loadProject(_fs: FileSystem, title: string) {
|
||||||
|
this.reset();
|
||||||
|
fs = _fs;
|
||||||
|
dispatch.current({ action: "setFs", payload: fs });
|
||||||
|
dispatch.current({ action: "setTitle", payload: title });
|
||||||
|
|
||||||
|
const files: Record<string, string> = {};
|
||||||
|
for (const file of (await fs.scandir("/")).filter(
|
||||||
|
(entry) => entry.isFile() && entry.name.endsWith(".jack"),
|
||||||
|
)) {
|
||||||
|
files[file.name.replace(".jack", "")] = await fs.readFile(file.name);
|
||||||
|
}
|
||||||
|
this.loadFiles(files);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadFiles(files: Record<string, string>) {
|
||||||
|
dispatch.current({ action: "setFiles", payload: files });
|
||||||
|
if (Object.entries(files).length > 0) {
|
||||||
|
dispatch.current({
|
||||||
|
action: "setSelected",
|
||||||
|
payload: Object.keys(files)[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async writeFile(name: string, content?: string) {
|
||||||
|
content ??= classTemplate(name);
|
||||||
|
dispatch.current({ action: "setFile", payload: { name, content } });
|
||||||
|
if (fs) {
|
||||||
|
await fs.writeFile(`${name}.jack`, content);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async reset() {
|
||||||
|
fs = undefined;
|
||||||
|
dispatch.current({ action: "reset" });
|
||||||
|
},
|
||||||
|
|
||||||
|
async compile() {
|
||||||
|
dispatch.current({ action: "writeCompiled" });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: CompilerPageState = {
|
||||||
|
files: {},
|
||||||
|
compiled: {},
|
||||||
|
selected: "",
|
||||||
|
isCompiled: false,
|
||||||
|
isValid: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { initialState, reducers, actions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCompilerPageStore() {
|
||||||
|
const { setStatus } = useContext(BaseContext);
|
||||||
|
|
||||||
|
const dispatch = useRef<CompilerStoreDispatch>(() => undefined);
|
||||||
|
|
||||||
|
const { initialState, reducers, actions } = useMemo(
|
||||||
|
() => makeCompilerStore(setStatus, dispatch),
|
||||||
|
[setStatus, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||||
|
dispatch.current = dispatcher;
|
||||||
|
|
||||||
|
return { state, dispatch, actions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs";
|
||||||
|
import {
|
||||||
|
Err,
|
||||||
|
isErr,
|
||||||
|
Ok,
|
||||||
|
unwrap,
|
||||||
|
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
import {
|
||||||
|
Format,
|
||||||
|
KeyboardAdapter,
|
||||||
|
MemoryAdapter,
|
||||||
|
MemoryKeyboard,
|
||||||
|
ROM,
|
||||||
|
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import { Span } from "@nand2tetris/simulator/languages/base.js";
|
||||||
|
import { TST } from "@nand2tetris/simulator/languages/tst.js";
|
||||||
|
import { loadAsm, loadBlob, loadHack } from "@nand2tetris/simulator/loader.js";
|
||||||
|
import { CPUTest } from "@nand2tetris/simulator/test/cputst.js";
|
||||||
|
import { Action } from "@nand2tetris/simulator/types.js";
|
||||||
|
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||||
|
import { ScreenScales } from "src/chips/screen.js";
|
||||||
|
import { RunSpeed } from "src/runbar.js";
|
||||||
|
import { compare } from "../compare.js";
|
||||||
|
import { loadTestFiles } from "../file_utils.js";
|
||||||
|
import { useImmerReducer } from "../react.js";
|
||||||
|
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||||
|
import { ImmMemory } from "./imm_memory.js";
|
||||||
|
|
||||||
|
function makeTst() {
|
||||||
|
return `repeat {
|
||||||
|
ticktock;
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CpuSim {
|
||||||
|
A: number;
|
||||||
|
D: number;
|
||||||
|
PC: number;
|
||||||
|
RAM: MemoryAdapter;
|
||||||
|
ROM: MemoryAdapter;
|
||||||
|
Screen: MemoryAdapter;
|
||||||
|
Keyboard: KeyboardAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CPUTestSim {
|
||||||
|
name: string;
|
||||||
|
tst: string;
|
||||||
|
cmp: string;
|
||||||
|
out: string;
|
||||||
|
highlight: Span | undefined;
|
||||||
|
valid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CPUPageConfig {
|
||||||
|
romFormat: Format;
|
||||||
|
ramFormat: Format;
|
||||||
|
screenScale: ScreenScales;
|
||||||
|
speed: RunSpeed;
|
||||||
|
testSpeed: RunSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CpuPageState {
|
||||||
|
sim: CpuSim;
|
||||||
|
test: CPUTestSim;
|
||||||
|
path: string;
|
||||||
|
tests: string[];
|
||||||
|
title?: string;
|
||||||
|
config: CPUPageConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduceCPUTest(
|
||||||
|
cpuTest: CPUTest,
|
||||||
|
dispatch: MutableRefObject<CpuStoreDispatch>,
|
||||||
|
): CpuSim {
|
||||||
|
const RAM = new ImmMemory(cpuTest.cpu.RAM, dispatch);
|
||||||
|
const ROM = new ImmMemory(cpuTest.cpu.ROM, dispatch);
|
||||||
|
const Screen = new ImmMemory(cpuTest.cpu.Screen, dispatch);
|
||||||
|
const Keyboard = new MemoryKeyboard(new ImmMemory(cpuTest.cpu.RAM, dispatch));
|
||||||
|
|
||||||
|
return {
|
||||||
|
A: cpuTest.cpu.A,
|
||||||
|
D: cpuTest.cpu.D,
|
||||||
|
PC: cpuTest.cpu.PC,
|
||||||
|
RAM,
|
||||||
|
ROM,
|
||||||
|
Screen,
|
||||||
|
Keyboard,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CpuStoreDispatch = Dispatch<{
|
||||||
|
action: keyof ReturnType<typeof makeCpuStore>["reducers"];
|
||||||
|
payload?: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function makeCpuStore(
|
||||||
|
fs: FileSystem,
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||||
|
storage: Record<string, string>,
|
||||||
|
dispatch: MutableRefObject<CpuStoreDispatch>,
|
||||||
|
) {
|
||||||
|
let test = new CPUTest();
|
||||||
|
let animate = true;
|
||||||
|
let valid = true;
|
||||||
|
let path = "";
|
||||||
|
let tests: string[] = [];
|
||||||
|
let tstName = "";
|
||||||
|
let _title: string | undefined;
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
update(state: CpuPageState) {
|
||||||
|
state.sim = reduceCPUTest(test, dispatch);
|
||||||
|
state.test.highlight = test.currentStep?.span;
|
||||||
|
state.test.valid = valid;
|
||||||
|
state.path = path;
|
||||||
|
state.tests = Array.from(tests);
|
||||||
|
state.test.name = tstName;
|
||||||
|
},
|
||||||
|
|
||||||
|
setTest(state: CpuPageState, { tst, cmp }: { tst?: string; cmp?: string }) {
|
||||||
|
state.test.tst = tst ?? state.test.tst;
|
||||||
|
state.test.cmp = cmp ?? state.test.cmp;
|
||||||
|
state.test.out = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
testStep(state: CpuPageState) {
|
||||||
|
state.test.out = test.log();
|
||||||
|
this.update(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
testFinished(state: CpuPageState) {
|
||||||
|
if (state.test.cmp.trim() === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const passed = compare(state.test.cmp.trim(), test.log().trim());
|
||||||
|
setStatus(
|
||||||
|
passed
|
||||||
|
? {
|
||||||
|
message: `Simulation successful: The output file is identical to the compare file`,
|
||||||
|
severity: "SUCCESS",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
message: `Simulation error: The output file differs from the compare file`,
|
||||||
|
severity: "ERROR",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
setTitle(state: CpuPageState, title?: string) {
|
||||||
|
_title = title;
|
||||||
|
state.title = title;
|
||||||
|
if (title) {
|
||||||
|
test.fileLoaded = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateConfig(state: CpuPageState, config: Partial<CPUPageConfig>) {
|
||||||
|
state.config = { ...state.config, ...config };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
tick() {
|
||||||
|
test.cpu.tick();
|
||||||
|
},
|
||||||
|
|
||||||
|
setAnimate(value: boolean) {
|
||||||
|
animate = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setPath(_path: string) {
|
||||||
|
path = _path;
|
||||||
|
|
||||||
|
const dir = path.split("/").slice(0, -1).join("/");
|
||||||
|
const files = await fs.scandir(dir);
|
||||||
|
tests = files
|
||||||
|
.filter((file) => file.name.endsWith(".tst"))
|
||||||
|
.map((file) => file.name);
|
||||||
|
|
||||||
|
if (tests.length > 0) {
|
||||||
|
this.loadTest(tests[0]);
|
||||||
|
} else {
|
||||||
|
tstName = "Default";
|
||||||
|
this.compileTest(makeTst(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
async testStep() {
|
||||||
|
try {
|
||||||
|
const done = await test.step();
|
||||||
|
if (animate || done) {
|
||||||
|
dispatch.current({ action: "testStep" });
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
dispatch.current({ action: "testFinished" });
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: (e as Error).message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetRAM() {
|
||||||
|
test.cpu.RAM.loadBytes([]);
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
setStatus("Reset RAM");
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleUseTest() {
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
test.reset();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.replaceROM(new ROM());
|
||||||
|
this.resetRAM();
|
||||||
|
this.clearTest();
|
||||||
|
this.reset();
|
||||||
|
dispatch.current({ action: "setTitle", payload: undefined });
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTest() {
|
||||||
|
tstName = "";
|
||||||
|
this.compileTest(makeTst(), "");
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
|
||||||
|
replaceROM(rom: ROM) {
|
||||||
|
test = new CPUTest({ dir: path, rom });
|
||||||
|
this.clearTest();
|
||||||
|
},
|
||||||
|
|
||||||
|
compileTest(file: string, cmp?: string, _path?: string) {
|
||||||
|
const tstPath = _path ?? path;
|
||||||
|
dispatch.current({ action: "setTest", payload: { tst: file, cmp } });
|
||||||
|
const tst = TST.parse(file);
|
||||||
|
|
||||||
|
if (isErr(tst)) {
|
||||||
|
setStatus({
|
||||||
|
message: `Failed to parse test - ${Err(tst).message}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
valid = false;
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
valid = true;
|
||||||
|
|
||||||
|
const maybeTest = CPUTest.from(Ok(tst), {
|
||||||
|
dir: tstPath,
|
||||||
|
rom: test.cpu.ROM,
|
||||||
|
doEcho: setStatus,
|
||||||
|
doLoad: async (path) => {
|
||||||
|
let file;
|
||||||
|
try {
|
||||||
|
file = await fs.readFile(path);
|
||||||
|
} catch (_e) {
|
||||||
|
throw new Error(`Cannot find ${path}`);
|
||||||
|
}
|
||||||
|
const loader = path.endsWith("hack")
|
||||||
|
? loadHack
|
||||||
|
: path.endsWith("asm")
|
||||||
|
? loadAsm
|
||||||
|
: loadBlob;
|
||||||
|
const bytes = await loader(file);
|
||||||
|
console.log(bytes);
|
||||||
|
test.cpu.ROM.loadBytes(bytes);
|
||||||
|
},
|
||||||
|
compareTo: async (file) => {
|
||||||
|
const dir = tstPath.split("/").slice(0, -1).join("/");
|
||||||
|
const cmp = await fs.readFile(`${dir}/${file}`);
|
||||||
|
dispatch.current({ action: "setTest", payload: { cmp } });
|
||||||
|
},
|
||||||
|
requireLoad: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isErr(maybeTest)) {
|
||||||
|
setStatus({
|
||||||
|
message: Err(maybeTest).message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
test = Ok(maybeTest);
|
||||||
|
test.fileLoaded = _title != undefined;
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTest(name: string) {
|
||||||
|
const dir = path.split("/").slice(0, -1).join("/");
|
||||||
|
const files = await loadTestFiles(fs, `${dir}/${name}`);
|
||||||
|
if (isErr(files)) {
|
||||||
|
setStatus({
|
||||||
|
message: `Failed to load test`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tstName = name;
|
||||||
|
const { tst } = unwrap(files);
|
||||||
|
this.compileTest(tst, "");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: CpuPageState = {
|
||||||
|
sim: reduceCPUTest(test, dispatch),
|
||||||
|
test: {
|
||||||
|
highlight: test.currentStep?.span,
|
||||||
|
name: "",
|
||||||
|
tst: makeTst(),
|
||||||
|
cmp: "",
|
||||||
|
out: "",
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
path: "",
|
||||||
|
tests: [],
|
||||||
|
config: {
|
||||||
|
romFormat: "asm",
|
||||||
|
ramFormat: "dec",
|
||||||
|
screenScale: 1,
|
||||||
|
speed: 2,
|
||||||
|
testSpeed: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { initialState, reducers, actions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCpuPageStore() {
|
||||||
|
const { fs, setStatus, storage } = useContext(BaseContext);
|
||||||
|
|
||||||
|
const dispatch = useRef<CpuStoreDispatch>(() => undefined);
|
||||||
|
|
||||||
|
const { initialState, reducers, actions } = useMemo(
|
||||||
|
() => makeCpuStore(fs, setStatus, storage, dispatch),
|
||||||
|
[fs, setStatus, storage, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||||
|
dispatch.current = dispatcher;
|
||||||
|
|
||||||
|
return { state, dispatch, actions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import { MemoryAdapter, SubMemory } from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import { MutableRefObject } from "react";
|
||||||
|
|
||||||
|
export class ImmMemory<
|
||||||
|
Action extends { action: "update" },
|
||||||
|
Dispatch extends (a: Action) => void,
|
||||||
|
> extends SubMemory {
|
||||||
|
constructor(
|
||||||
|
parent: MemoryAdapter,
|
||||||
|
private dispatch: MutableRefObject<Dispatch>,
|
||||||
|
) {
|
||||||
|
super(parent, parent.size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async load(fs: FileSystem, path: string): Promise<void> {
|
||||||
|
await super.load(fs, path);
|
||||||
|
this.dispatch.current({ action: "update" } as Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||||
|
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||||
|
import {
|
||||||
|
Err,
|
||||||
|
isErr,
|
||||||
|
Ok,
|
||||||
|
Result,
|
||||||
|
unwrap,
|
||||||
|
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||||
|
import { FIBONACCI } from "@nand2tetris/projects/base.js";
|
||||||
|
import {
|
||||||
|
Format,
|
||||||
|
KeyboardAdapter,
|
||||||
|
MemoryAdapter,
|
||||||
|
MemoryKeyboard,
|
||||||
|
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||||
|
import {
|
||||||
|
CompilationError,
|
||||||
|
Span,
|
||||||
|
} from "@nand2tetris/simulator/languages/base.js";
|
||||||
|
import { TST } from "@nand2tetris/simulator/languages/tst.js";
|
||||||
|
import { VM, VmInstruction } from "@nand2tetris/simulator/languages/vm.js";
|
||||||
|
import { VMTest, VmFile } from "@nand2tetris/simulator/test/vmtst.js";
|
||||||
|
import { Action } from "@nand2tetris/simulator/types.js";
|
||||||
|
import { Vm, VmFrame } from "@nand2tetris/simulator/vm/vm.js";
|
||||||
|
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||||
|
import { ScreenScales } from "../chips/screen.js";
|
||||||
|
import { compare } from "../compare.js";
|
||||||
|
import { useImmerReducer } from "../react.js";
|
||||||
|
import { RunSpeed } from "../runbar.js";
|
||||||
|
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||||
|
import { ImmMemory } from "./imm_memory.js";
|
||||||
|
|
||||||
|
export const DEFAULT_TEST = "repeat {\n\tvmstep;\n}";
|
||||||
|
|
||||||
|
export interface VmSim {
|
||||||
|
RAM: MemoryAdapter;
|
||||||
|
Screen: MemoryAdapter;
|
||||||
|
Keyboard: KeyboardAdapter;
|
||||||
|
Stack: VmFrame[];
|
||||||
|
Prog: VmInstruction[];
|
||||||
|
Statics: number[];
|
||||||
|
Temp: number[];
|
||||||
|
AddedSysInit: boolean;
|
||||||
|
highlight: number;
|
||||||
|
showHighlight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VMTestSim {
|
||||||
|
highlight: Span | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VmPageState {
|
||||||
|
vm: VmSim;
|
||||||
|
controls: ControlsState;
|
||||||
|
test: VMTestSim;
|
||||||
|
files: VMFiles;
|
||||||
|
title?: string;
|
||||||
|
config: VmPageConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VmPageConfig {
|
||||||
|
ram1Format: Format;
|
||||||
|
ram2Format: Format;
|
||||||
|
screenScale: ScreenScales;
|
||||||
|
speed: RunSpeed;
|
||||||
|
testSpeed: RunSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ControlsState {
|
||||||
|
runningTest: boolean;
|
||||||
|
exitCode: number | undefined;
|
||||||
|
animate: boolean;
|
||||||
|
valid: boolean;
|
||||||
|
error?: CompilationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VMFiles {
|
||||||
|
vm: string;
|
||||||
|
tst: string;
|
||||||
|
cmp: string;
|
||||||
|
out: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VmStoreDispatch = Dispatch<{
|
||||||
|
action: keyof ReturnType<typeof makeVmStore>["reducers"];
|
||||||
|
payload?: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function reduceVMTest(
|
||||||
|
vmTest: VMTest,
|
||||||
|
dispatch: MutableRefObject<VmStoreDispatch>,
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||||
|
showHighlight: boolean,
|
||||||
|
): VmSim {
|
||||||
|
const RAM = new ImmMemory(vmTest.vm.RAM, dispatch);
|
||||||
|
const Screen = new ImmMemory(vmTest.vm.Screen, dispatch);
|
||||||
|
const Keyboard = new MemoryKeyboard(new ImmMemory(vmTest.vm.RAM, dispatch));
|
||||||
|
const highlight = vmTest.vm.derivedLine();
|
||||||
|
|
||||||
|
let stack: VmFrame[] = [];
|
||||||
|
try {
|
||||||
|
stack = vmTest.vm.vmStack().reverse();
|
||||||
|
} catch (_e) {
|
||||||
|
dispatch.current({
|
||||||
|
action: "setError",
|
||||||
|
payload: new Error("Runtime error: Invalid stack"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Keyboard,
|
||||||
|
RAM,
|
||||||
|
Screen,
|
||||||
|
Stack: stack,
|
||||||
|
Prog: vmTest.vm.program,
|
||||||
|
Statics: [
|
||||||
|
...vmTest.vm.memory.map((_, v) => v, 16, 16 + vmTest.vm.getStaticCount()),
|
||||||
|
],
|
||||||
|
Temp: [...vmTest.vm.memory.map((_, v) => v, 5, 13)],
|
||||||
|
AddedSysInit: vmTest.vm.addedSysInit,
|
||||||
|
highlight,
|
||||||
|
showHighlight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeVmStore(
|
||||||
|
fs: FileSystem,
|
||||||
|
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||||
|
storage: Record<string, string>,
|
||||||
|
dispatch: MutableRefObject<VmStoreDispatch>,
|
||||||
|
) {
|
||||||
|
const parsed = unwrap(VM.parse(FIBONACCI));
|
||||||
|
let vm = unwrap(Vm.build(parsed.instructions));
|
||||||
|
let test = new VMTest({ doEcho: setStatus }).with(vm);
|
||||||
|
let useTest = false;
|
||||||
|
let animate = true;
|
||||||
|
let vmSource = "";
|
||||||
|
let showHighlight = true;
|
||||||
|
const reducers = {
|
||||||
|
setVm(state: VmPageState, vm: string) {
|
||||||
|
state.files.vm = vm;
|
||||||
|
},
|
||||||
|
setTst(state: VmPageState, { tst }: { tst: string }) {
|
||||||
|
state.files.tst = tst;
|
||||||
|
},
|
||||||
|
setCmp(state: VmPageState, { cmp }: { cmp: string }) {
|
||||||
|
state.files.cmp = cmp;
|
||||||
|
},
|
||||||
|
setExitCode(state: VmPageState, code: number | undefined) {
|
||||||
|
state.controls.exitCode = code;
|
||||||
|
},
|
||||||
|
setValid(state: VmPageState, valid: boolean) {
|
||||||
|
state.controls.valid = valid;
|
||||||
|
},
|
||||||
|
setShowHighlight(state: VmPageState, value: boolean) {
|
||||||
|
state.vm.showHighlight = value;
|
||||||
|
},
|
||||||
|
setError(state: VmPageState, error?: CompilationError) {
|
||||||
|
if (error) {
|
||||||
|
state.controls.valid = false;
|
||||||
|
setStatus({
|
||||||
|
message: error?.message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
state.controls.valid = true;
|
||||||
|
}
|
||||||
|
state.controls.error = error;
|
||||||
|
},
|
||||||
|
update(state: VmPageState) {
|
||||||
|
state.vm = reduceVMTest(test, dispatch, setStatus, showHighlight);
|
||||||
|
state.test.highlight = test.currentStep?.span;
|
||||||
|
},
|
||||||
|
setAnimate(state: VmPageState, value: boolean) {
|
||||||
|
state.controls.animate = value;
|
||||||
|
},
|
||||||
|
testStep(state: VmPageState) {
|
||||||
|
state.files.out = test.log();
|
||||||
|
},
|
||||||
|
testFinished(state: VmPageState) {
|
||||||
|
const passed = compare(state.files.cmp.trim(), state.files.out);
|
||||||
|
setStatus(
|
||||||
|
passed
|
||||||
|
? {
|
||||||
|
message: `Simulation successful: The output file is identical to the compare file`,
|
||||||
|
severity: "SUCCESS",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
message: `Simulation error: The output file differs from the compare file`,
|
||||||
|
severity: "ERROR",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
setTitle(state: VmPageState, title: string) {
|
||||||
|
state.title = title;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateConfig(state: VmPageState, config: Partial<VmPageConfig>) {
|
||||||
|
state.config = { ...state.config, ...config };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const initialState: VmPageState = {
|
||||||
|
vm: reduceVMTest(test, dispatch, setStatus, true),
|
||||||
|
controls: {
|
||||||
|
exitCode: undefined,
|
||||||
|
runningTest: false,
|
||||||
|
animate: true,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
highlight: undefined,
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
vm: "",
|
||||||
|
tst: DEFAULT_TEST,
|
||||||
|
cmp: "",
|
||||||
|
out: "",
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
ram1Format: "dec",
|
||||||
|
ram2Format: "dec",
|
||||||
|
screenScale: 1,
|
||||||
|
speed: 2,
|
||||||
|
testSpeed: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const actions = {
|
||||||
|
async load(path: string) {
|
||||||
|
const files: VmFile[] = [];
|
||||||
|
let title: string;
|
||||||
|
|
||||||
|
if ((await fs.stat(path)).isFile()) {
|
||||||
|
// single file
|
||||||
|
files.push({
|
||||||
|
name: assertExists(path.split("/").pop()).replace(".vm", ""),
|
||||||
|
content: await fs.readFile(path),
|
||||||
|
});
|
||||||
|
title = path.split("/").pop() ?? "";
|
||||||
|
} else {
|
||||||
|
// folder
|
||||||
|
for (const file of (await fs.scandir(path)).filter(
|
||||||
|
(entry) => entry.isFile() && entry.name.endsWith(".vm"),
|
||||||
|
)) {
|
||||||
|
files.push({
|
||||||
|
name: file.name.replace(".vm", ""),
|
||||||
|
content: await fs.readFile(`${path}/${file.name}`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
title = `${path.split("/").pop()} / *.vm`;
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "setTitle", payload: title });
|
||||||
|
this.loadVm(files);
|
||||||
|
this.reset();
|
||||||
|
setStatus("");
|
||||||
|
},
|
||||||
|
setVm(content: string) {
|
||||||
|
showHighlight = false;
|
||||||
|
dispatch.current({
|
||||||
|
action: "setVm",
|
||||||
|
payload: content,
|
||||||
|
});
|
||||||
|
if (vmSource == content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vmSource = content;
|
||||||
|
|
||||||
|
const parseResult = VM.parse(content);
|
||||||
|
|
||||||
|
if (isErr(parseResult)) {
|
||||||
|
dispatch.current({ action: "setError", payload: Err(parseResult) });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const instructions = unwrap(parseResult).instructions;
|
||||||
|
const buildResult = Vm.build(instructions);
|
||||||
|
return this.replaceVm(buildResult);
|
||||||
|
},
|
||||||
|
loadVm(files: VmFile[]) {
|
||||||
|
showHighlight = false;
|
||||||
|
|
||||||
|
const content = files.map((f) => f.content).join("\n");
|
||||||
|
dispatch.current({
|
||||||
|
action: "setVm",
|
||||||
|
payload: content,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vmSource == content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vmSource = content;
|
||||||
|
|
||||||
|
const parsed = [];
|
||||||
|
|
||||||
|
let lineOffset = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
const parseResult = VM.parse(file.content);
|
||||||
|
|
||||||
|
if (isErr(parseResult)) {
|
||||||
|
dispatch.current({ action: "setError", payload: Err(parseResult) });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const instructions = unwrap(parseResult).instructions;
|
||||||
|
|
||||||
|
for (const instruction of instructions) {
|
||||||
|
if (instruction.span?.line != undefined) {
|
||||||
|
instruction.span.line += lineOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineOffset += file.content.split("\n").length;
|
||||||
|
|
||||||
|
parsed.push({
|
||||||
|
name: file.name,
|
||||||
|
instructions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const buildResult = Vm.buildFromFiles(parsed);
|
||||||
|
return this.replaceVm(buildResult);
|
||||||
|
},
|
||||||
|
replaceVm(buildResult: Result<Vm, CompilationError>) {
|
||||||
|
if (isErr(buildResult)) {
|
||||||
|
dispatch.current({ action: "setError", payload: Err(buildResult) });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "setError" });
|
||||||
|
// setStatus("Compiled VM code successfully");
|
||||||
|
|
||||||
|
vm = unwrap(buildResult);
|
||||||
|
test.vm = vm;
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadTest(path: string, source: string) {
|
||||||
|
dispatch.current({ action: "setTst", payload: { tst: source } });
|
||||||
|
const tst = TST.parse(source);
|
||||||
|
|
||||||
|
if (isErr(tst)) {
|
||||||
|
dispatch.current({ action: "setValid", payload: false });
|
||||||
|
setStatus({
|
||||||
|
message: `Failed to parse test`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dispatch.current({ action: "setValid", payload: true });
|
||||||
|
setStatus(`Parsed tst`);
|
||||||
|
|
||||||
|
vm.reset();
|
||||||
|
|
||||||
|
const maybeTest = VMTest.from(unwrap(tst), {
|
||||||
|
dir: path,
|
||||||
|
doLoad: async (path) => {
|
||||||
|
await this.load(path);
|
||||||
|
},
|
||||||
|
doEcho: setStatus,
|
||||||
|
compareTo: async (file) => {
|
||||||
|
const dir = path.split("/").slice(0, -1).join("/");
|
||||||
|
const cmp = await fs.readFile(`${dir}/${file}`);
|
||||||
|
dispatch.current({ action: "setCmp", payload: { cmp } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (isErr(maybeTest)) {
|
||||||
|
setStatus({
|
||||||
|
message: Err(maybeTest).message,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
test = Ok(maybeTest).using(fs);
|
||||||
|
test.vm = vm;
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setAnimate(value: boolean) {
|
||||||
|
animate = value;
|
||||||
|
dispatch.current({ action: "setAnimate", payload: value });
|
||||||
|
},
|
||||||
|
async testStep() {
|
||||||
|
showHighlight = true;
|
||||||
|
let done = false;
|
||||||
|
try {
|
||||||
|
done = await test.step();
|
||||||
|
dispatch.current({ action: "testStep" });
|
||||||
|
if (done) {
|
||||||
|
dispatch.current({ action: "testFinished" });
|
||||||
|
}
|
||||||
|
if (animate) {
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
}
|
||||||
|
return done;
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: `Runtime error: ${(e as Error).message}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
dispatch.current({ action: "setError", payload: e });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPaused(paused = true) {
|
||||||
|
vm.setPaused(paused);
|
||||||
|
},
|
||||||
|
step() {
|
||||||
|
showHighlight = true;
|
||||||
|
try {
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
const exitCode = vm.step();
|
||||||
|
if (exitCode !== undefined) {
|
||||||
|
done = true;
|
||||||
|
dispatch.current({ action: "setExitCode", payload: exitCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animate) {
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return done;
|
||||||
|
} catch (e) {
|
||||||
|
setStatus({
|
||||||
|
message: `Runtime error: ${(e as Error).message}`,
|
||||||
|
severity: "ERROR",
|
||||||
|
});
|
||||||
|
dispatch.current({ action: "setError", payload: e });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
showHighlight = true;
|
||||||
|
test.reset();
|
||||||
|
vm.reset();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
dispatch.current({ action: "setExitCode", payload: undefined });
|
||||||
|
dispatch.current({ action: "setValid", payload: true });
|
||||||
|
},
|
||||||
|
toggleUseTest() {
|
||||||
|
useTest = !useTest;
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
initialize() {
|
||||||
|
dispatch.current({ action: "setTitle", payload: undefined });
|
||||||
|
this.setVm(FIBONACCI);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { initialState, reducers, actions };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVmPageStore() {
|
||||||
|
const { fs, setStatus, storage } = useContext(BaseContext);
|
||||||
|
|
||||||
|
const dispatch = useRef<VmStoreDispatch>(() => undefined);
|
||||||
|
|
||||||
|
const { initialState, reducers, actions } = useMemo(
|
||||||
|
() => makeVmStore(fs, setStatus, storage, dispatch),
|
||||||
|
[fs, setStatus, storage, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||||
|
dispatch.current = dispatcher;
|
||||||
|
|
||||||
|
return { state, dispatch, actions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { rounded } from "@davidsouther/jiffies/lib/esm/dom/css/border.js";
|
||||||
|
import { TranslatorSymbol } from "./stores/asm.store";
|
||||||
|
|
||||||
|
export const Table = ({
|
||||||
|
values = [],
|
||||||
|
highlight = -1,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
values?: TranslatorSymbol[];
|
||||||
|
highlight?: number;
|
||||||
|
onClick?: (id: string, value: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{values.map((entry, i) => (
|
||||||
|
<TableRow
|
||||||
|
key={i}
|
||||||
|
identifier={entry.name}
|
||||||
|
value={entry.value}
|
||||||
|
highlight={i === highlight}
|
||||||
|
onClick={() => onClick?.(entry.name, entry.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableRow = ({
|
||||||
|
identifier,
|
||||||
|
value,
|
||||||
|
highlight = false,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
identifier: string;
|
||||||
|
value: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }} onClick={onClick}>
|
||||||
|
{identifier.length > 0 && (
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
flex: "1",
|
||||||
|
...rounded("none"),
|
||||||
|
...(highlight
|
||||||
|
? { background: "var(--mark-background-color)" }
|
||||||
|
: {}),
|
||||||
|
whiteSpace: "pre",
|
||||||
|
padding: "3px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{identifier}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
{value.length > 0 && (
|
||||||
|
<code
|
||||||
|
style={{
|
||||||
|
flex: "1",
|
||||||
|
textAlign: "right",
|
||||||
|
padding: "3px",
|
||||||
|
...rounded("none"),
|
||||||
|
...(highlight
|
||||||
|
? { background: "var(--mark-background-color)" }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Timer } from "@nand2tetris/simulator/timer.js";
|
||||||
|
import { useImmerReducer } from "./react.js";
|
||||||
|
|
||||||
|
export interface TimerStoreState {
|
||||||
|
steps: number;
|
||||||
|
speed: number;
|
||||||
|
running: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { Dispatch, MutableRefObject, useMemo, useRef } from "react";
|
||||||
|
export type TimerStoreDispatch = Dispatch<{
|
||||||
|
action: keyof ReturnType<typeof makeTimerStore>["reducers"];
|
||||||
|
payload?: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const makeTimerStore = (
|
||||||
|
timer: Timer,
|
||||||
|
dispatch: MutableRefObject<TimerStoreDispatch>,
|
||||||
|
) => {
|
||||||
|
const initialState: TimerStoreState = {
|
||||||
|
running: timer.running,
|
||||||
|
speed: timer.speed,
|
||||||
|
steps: timer.steps,
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishFrame = timer.finishFrame.bind(timer);
|
||||||
|
timer.finishFrame = function () {
|
||||||
|
finishFrame();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducers = {
|
||||||
|
update(state: TimerStoreState) {
|
||||||
|
state.running = timer.running;
|
||||||
|
state.speed = timer.speed;
|
||||||
|
state.steps = timer.steps;
|
||||||
|
},
|
||||||
|
setSteps(state: TimerStoreState, steps: number) {
|
||||||
|
state.steps = steps;
|
||||||
|
timer.steps = steps;
|
||||||
|
},
|
||||||
|
setSpeed(state: TimerStoreState, speed: number) {
|
||||||
|
state.speed = speed;
|
||||||
|
timer.speed = speed;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
frame() {
|
||||||
|
timer.frame();
|
||||||
|
},
|
||||||
|
start() {
|
||||||
|
timer.start();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
stop() {
|
||||||
|
timer.stop();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
timer.reset();
|
||||||
|
dispatch.current({ action: "update" });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { initialState, reducers, actions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTimer(timer: Timer) {
|
||||||
|
const dispatch = useRef<TimerStoreDispatch>(() => undefined);
|
||||||
|
|
||||||
|
const { initialState, reducers, actions } = useMemo(
|
||||||
|
() => makeTimerStore(timer, dispatch),
|
||||||
|
[timer, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||||
|
|
||||||
|
dispatch.current = dispatcher;
|
||||||
|
|
||||||
|
return { state, dispatch: dispatch.current, actions };
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import VirtualScroll, { arrayAdapter } from "./virtual_scroll.js";
|
||||||
|
|
||||||
|
describe("<VirtualScroll />", () => {
|
||||||
|
it("initializes & renders", () => {
|
||||||
|
render(
|
||||||
|
<VirtualScroll<number>
|
||||||
|
settings={{ maxIndex: 3 }}
|
||||||
|
get={arrayAdapter([1, 2, 3])}
|
||||||
|
row={(i) => <div>{i}</div>}
|
||||||
|
rowKey={(i) => i}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const two = screen.getByText("2");
|
||||||
|
expect(two).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
|
import {
|
||||||
|
Key,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export interface VirtualScrollSettings {
|
||||||
|
/**Minimum offset into the adapter. Default is 0. */
|
||||||
|
minIndex: number;
|
||||||
|
/** Maximum offset into the adapter. Default is Number.MAX_SAFE_INTEGER. */
|
||||||
|
maxIndex: number;
|
||||||
|
/** Initial index to start rendering from. Default is minIndex. */
|
||||||
|
startIndex: number;
|
||||||
|
/**
|
||||||
|
* Number of items to render in visible area. Default is entire range from
|
||||||
|
* minIndex to maxIndex.
|
||||||
|
*/
|
||||||
|
count: number;
|
||||||
|
/**
|
||||||
|
* Maximum number of items to render on either side of the visible area.
|
||||||
|
* Default is `count`.
|
||||||
|
*/
|
||||||
|
tolerance: number;
|
||||||
|
/** Height of each item, in pixels. Default is 20px. */
|
||||||
|
itemHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VirtualScrollDataAdapter<T> {
|
||||||
|
(offset: number, limit: number): Iterable<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayAdapter<T>(data: T[]): VirtualScrollDataAdapter<T> {
|
||||||
|
return (offset, limit) => data.slice(offset, offset + limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VirtualScrollProps<T, U extends ReactNode> {
|
||||||
|
settings?: Partial<VirtualScrollSettings>;
|
||||||
|
get: VirtualScrollDataAdapter<T>;
|
||||||
|
row: (t: T) => U;
|
||||||
|
rowKey: (t: T) => Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillVirtualScrollSettings(
|
||||||
|
settings: Partial<VirtualScrollSettings>,
|
||||||
|
): VirtualScrollSettings {
|
||||||
|
const {
|
||||||
|
minIndex = 0,
|
||||||
|
maxIndex = Number.MAX_SAFE_INTEGER,
|
||||||
|
startIndex = 0,
|
||||||
|
itemHeight = 20,
|
||||||
|
count = Math.max(maxIndex - minIndex, 1),
|
||||||
|
tolerance = count,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
return { minIndex, maxIndex, startIndex, itemHeight, count, tolerance };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialState<T>(
|
||||||
|
settings: VirtualScrollSettings,
|
||||||
|
adapter: VirtualScrollDataAdapter<T>,
|
||||||
|
): VirtualScrollState<T> {
|
||||||
|
// From Denis Hilt, https://blog.logrocket.com/virtual-scrolling-core-principles-and-basic-implementation-in-react/
|
||||||
|
const { minIndex, maxIndex, startIndex, itemHeight, count, tolerance } =
|
||||||
|
settings;
|
||||||
|
const bufferedItems = count + 2 * tolerance;
|
||||||
|
const itemsAbove = Math.max(0, startIndex - tolerance - minIndex);
|
||||||
|
|
||||||
|
const viewportHeight = count * itemHeight;
|
||||||
|
const totalHeight = Math.max(maxIndex - minIndex, 1) * itemHeight;
|
||||||
|
const toleranceHeight = tolerance * itemHeight;
|
||||||
|
const bufferHeight = viewportHeight + 2 * toleranceHeight;
|
||||||
|
const topPaddingHeight = itemsAbove * itemHeight;
|
||||||
|
const bottomPaddingHeight = totalHeight - (topPaddingHeight + bufferHeight);
|
||||||
|
|
||||||
|
const state: VirtualScrollState<T> = {
|
||||||
|
scrollTop: 0,
|
||||||
|
settings,
|
||||||
|
viewportHeight,
|
||||||
|
totalHeight,
|
||||||
|
toleranceHeight,
|
||||||
|
bufferedItems,
|
||||||
|
topPaddingHeight,
|
||||||
|
bottomPaddingHeight,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...doScroll(topPaddingHeight + toleranceHeight, state, adapter),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getData<T>(
|
||||||
|
minIndex: number,
|
||||||
|
maxIndex: number,
|
||||||
|
offset: number,
|
||||||
|
limit: number,
|
||||||
|
get: VirtualScrollDataAdapter<T>,
|
||||||
|
): T[] {
|
||||||
|
const start = Math.max(0, minIndex, offset);
|
||||||
|
const end = Math.min(maxIndex, offset + limit - 1);
|
||||||
|
const data = get(start, end - start);
|
||||||
|
return [...data];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScrollUpdate<T> {
|
||||||
|
scrollTop: number;
|
||||||
|
topPaddingHeight: number;
|
||||||
|
bottomPaddingHeight: number;
|
||||||
|
data: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doScroll<T>(
|
||||||
|
scrollTop: number,
|
||||||
|
state: VirtualScrollState<T>,
|
||||||
|
get: VirtualScrollDataAdapter<T>,
|
||||||
|
): ScrollUpdate<T> {
|
||||||
|
const {
|
||||||
|
totalHeight,
|
||||||
|
toleranceHeight,
|
||||||
|
bufferedItems,
|
||||||
|
settings: { itemHeight, minIndex, maxIndex },
|
||||||
|
} = state;
|
||||||
|
const index =
|
||||||
|
minIndex + Math.floor((scrollTop - toleranceHeight) / itemHeight);
|
||||||
|
const data = getData(minIndex, maxIndex, index, bufferedItems, get);
|
||||||
|
const topPaddingHeight = Math.max((index - minIndex) * itemHeight, 0);
|
||||||
|
const bottomPaddingHeight = Math.max(
|
||||||
|
totalHeight - (topPaddingHeight + data.length * itemHeight),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { scrollTop, topPaddingHeight, bottomPaddingHeight, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VirtualScrollState<T> {
|
||||||
|
settings: VirtualScrollSettings;
|
||||||
|
scrollTop: number; // px
|
||||||
|
bufferedItems: number; // count
|
||||||
|
totalHeight: number; // px
|
||||||
|
viewportHeight: number; // px
|
||||||
|
topPaddingHeight: number; // px
|
||||||
|
bottomPaddingHeight: number; // px
|
||||||
|
toleranceHeight: number; // px
|
||||||
|
data: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollReducer =
|
||||||
|
<T extends {}>(get: VirtualScrollDataAdapter<T>) =>
|
||||||
|
(state: VirtualScrollState<T>, scrollTop: number) => ({
|
||||||
|
...state,
|
||||||
|
...doScroll(scrollTop, state, get),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const VirtualScroll = <T extends {}, U extends ReactNode = ReactNode>(
|
||||||
|
props: VirtualScrollProps<T, U> & { className?: string },
|
||||||
|
) => {
|
||||||
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { settings, startState, reducer } = useMemo(() => {
|
||||||
|
const settings = fillVirtualScrollSettings(props.settings ?? {});
|
||||||
|
const startState = initialState<T>(settings, props.get);
|
||||||
|
const reducer = scrollReducer(props.get);
|
||||||
|
return { settings, reducer, startState };
|
||||||
|
}, [props.settings, props.get]);
|
||||||
|
|
||||||
|
const [state, dispatchScroll] = useReducer(reducer, startState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewportRef.current !== null) {
|
||||||
|
dispatchScroll(viewportRef.current.scrollTop);
|
||||||
|
}
|
||||||
|
}, [settings, props.row]);
|
||||||
|
|
||||||
|
const initialScroll = useCallback(
|
||||||
|
(div: HTMLDivElement | null) => {
|
||||||
|
if (div) {
|
||||||
|
div.scrollTop = viewportRef.current
|
||||||
|
? viewportRef.current.scrollTop
|
||||||
|
: settings.startIndex * settings.itemHeight;
|
||||||
|
}
|
||||||
|
viewportRef.current = div;
|
||||||
|
},
|
||||||
|
[viewportRef, settings.startIndex, settings.itemHeight],
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = state.data.map((d) => (
|
||||||
|
<div key={props.rowKey(d)} style={{ height: `${settings.itemHeight}px` }}>
|
||||||
|
{props.row(d)}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={initialScroll}
|
||||||
|
style={{
|
||||||
|
height: `${state.viewportHeight}px`,
|
||||||
|
overflowY: "scroll",
|
||||||
|
overflowAnchor: "none",
|
||||||
|
}}
|
||||||
|
className={props.className ?? ""}
|
||||||
|
onScroll={(e) => dispatchScroll((e.target as HTMLDivElement).scrollTop)}
|
||||||
|
>
|
||||||
|
<div style={{ height: `${state.topPaddingHeight}px` }} />
|
||||||
|
{rows}
|
||||||
|
<div style={{ height: `${state.bottomPaddingHeight}px` }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VirtualScroll;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "build",
|
||||||
|
"rootDir": "src",
|
||||||
|
"tsBuildInfoFile": "build/.tsbuildinfo"
|
||||||
|
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.vsix
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
..
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
src
|
||||||
|
tsconfig*
|
||||||
|
views
|
||||||
|
!out
|
||||||
|
out/**/*.map
|
||||||
|
!fileicons
|
||||||
|
!languages
|
||||||
|
!LICENSE
|
||||||
|
!package.json
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright 2022 David Souther et al
|
||||||
|
|
||||||
|
This software is based on Stefano Volpe's 'Nand2Tetris Tools'. Please check [here](https://github.com/foxyseta/nand-ide/blob/master/LICENSE) for further information.
|
||||||
|
This software is based on Aviv Yaish's 'NAND IDE'. Please check [here](https://github.com/AvivYaish/nand-ide/blob/master/LICENSE) for further information.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# NAND2Tetris VSCode Extension
|
||||||
|
|
||||||
|
This extension adds a NAND2Tetris side panel to VSCode.
|
||||||
|
|
||||||
|
The side panel allows interacting with .HDl files and chips.
|
||||||
|
|
||||||
|
## Developing the extension
|
||||||
|
|
||||||
|
1. Open this project in VSCode.
|
||||||
|
2. Select "Run Extension" from the "Run and Debug" panel
|
||||||
|
3. Click "Run" or press F5.
|
||||||
|
4. In the debug VSCode window, open the [Project Files](https://github.com/nand2tetris/projects).
|
||||||
|
5. Open an HDL file, for instance, demo/Xor.hdl
|
||||||
|
6. The NAND2TETRIS: HDL CHIP view should be open in the side panel.
|
||||||
|
- If it is not, try `NAND2Tetris: Focus on HDL Chip View` in the command pallette.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Your first extension](https://code.visualstudio.com/api/get-started/your-first-extension)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"iconDefinitions": {
|
||||||
|
"_hdl": {
|
||||||
|
"iconPath": "./images/hdl.svg"
|
||||||
|
},
|
||||||
|
"_tst": {
|
||||||
|
"iconPath": "./images/tst.svg"
|
||||||
|
},
|
||||||
|
"_cmp-out": {
|
||||||
|
"iconPath": "./images/cmp-out.svg"
|
||||||
|
},
|
||||||
|
"_hack": {
|
||||||
|
"iconPath": "./images/hack.svg"
|
||||||
|
},
|
||||||
|
"_vm": {
|
||||||
|
"iconPath": "./images/vm.svg"
|
||||||
|
},
|
||||||
|
"_jack": {
|
||||||
|
"iconPath": "./images/jack.svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fileExtensions": {
|
||||||
|
"hdl": "_hdl",
|
||||||
|
"tst": "_tst",
|
||||||
|
"cmp": "_cmp-out",
|
||||||
|
"out": "_cmp-out",
|
||||||
|
"asm": "_asm",
|
||||||
|
"hack": "_hack",
|
||||||
|
"vm": "_vm",
|
||||||
|
"jack": "_jack"
|
||||||
|
},
|
||||||
|
"languageIds": {
|
||||||
|
"hdl": "_hdl",
|
||||||
|
"tst": "_tst",
|
||||||
|
"cmp": "_cmp-out",
|
||||||
|
"out": "_cmp-out",
|
||||||
|
"asm": "_asm",
|
||||||
|
"hack": "_hack",
|
||||||
|
"vm": "_vm",
|
||||||
|
"jack": "_jack"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g style="transform: translate(20%, 10%) scale(80%)">
|
||||||
|
<g style="stroke: #777; fill:none; stroke-width: 20px; stroke-linejoin: round; stroke-linecap: round">
|
||||||
|
<path d="M10,10 H500 V500 H10 z M10,120 H500 M10,240 H500 M10,370 H500 M170,120 V500 M340,120 V500" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g style="transform: translate(20%, 10%) scale(80%)">
|
||||||
|
<g style="stroke: #777; fill:none; stroke-width: 40px; stroke-linejoin: round; stroke-linecap: round">
|
||||||
|
<path d="M106,20 h100 v180 h-100 z" />
|
||||||
|
<path d="M292,292 h100 v180 h-100 z" />
|
||||||
|
<path d="M296,60 l40,-40 v180 m-50,0 h100" />
|
||||||
|
<path d="M116,332 l40,-40 v180 m-50,0 h100" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 533 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g style="transform: translate(20%, 10%) scale(80%)">
|
||||||
|
<g style="stroke: #777; fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 25px">
|
||||||
|
<path d="M75,75 h362 v362 h-362 z" />
|
||||||
|
<path d="M75,75 m72,0 v-60 m72,60 v-60 m72,60 v-60 m72,60 v-60" />
|
||||||
|
<path d="M75,75 m0,72 h-60 m60,72 h-60 m60,72 h-60 m60,72 h-60" />
|
||||||
|
<path d="M440,75 m0,72 h60 m-60,72 h60 m-60,72 h60 m-60,72 h60" />
|
||||||
|
<path d="M75,440 m72,0 v60 m72,-60 v60 m72,-60 v60 m72,-60 v60" />
|
||||||
|
<path d="M145,145 h215 v215 h-215 z m0,80 h70 m-70,55 h40" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 754 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g style="transform: translate(20%, 10%) scale(80%)">
|
||||||
|
<g style="stroke: #777; fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 25px">
|
||||||
|
<path d="M12.5,12.5 h240 v120 h-120 v120 h-120 z m0,120 h20 m40,0 h0 m35,0 h25 v-120 " />
|
||||||
|
<path d="M380,132.5 h120 M255,253 h245 M12.5,376 h486 M12.5,499 h486" />
|
||||||
|
<path d="M12.5,380 v115 M132.5,380 v115 M253,255 v240 M376,132.5 v365 M500,132.5 v365 " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 606 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g style="transform: translate(20%, 10%) scale(80%)">
|
||||||
|
<g style="stroke: #777; fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 25px">
|
||||||
|
<path d="M12.5,12.5 h352 v485 h-352 z" />
|
||||||
|
<path d="M75,90 h75 v75 h-75 z M75,220 h75 v75 h-75 z M75,345 h75 v75 h-75 z" />
|
||||||
|
<path d="M200,90 h100 m-100,66 h100 m-100,66 h100 m-100,66 h100 m-100,66 h100 m-100,66 h100" />
|
||||||
|
<path d="M425,55 a 37.5 37.5 0 1 1 75 0 v366 l-37.5,75 l-37.5,-70 z M425,70 h75 M425,380 h75 " />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 679 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<g style="transform: translate(20%, 10%) scale(80%)">
|
||||||
|
<g style="stroke: #777; fill:none; stroke-width: 40px; stroke-linejoin: round; stroke-linecap: round">
|
||||||
|
<path d="M256,25 l256,124 l-256,124, l-256,-124 z" />
|
||||||
|
<path d="M0,247 l256,124 l256,-124" />
|
||||||
|
<path d="M0,355 l256,124 l256,-124" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,51 @@
|
|||||||
|
<svg version="1.1" width="32" height="32" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--outline: black;
|
||||||
|
--block: red;
|
||||||
|
--gate: cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--outline: white;
|
||||||
|
--block: orange;
|
||||||
|
--gate: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rect, path, circle {
|
||||||
|
stroke: var(--outline);
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke-linecap: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.tetris {
|
||||||
|
--size: 8px;
|
||||||
|
--offset: 1.5px + calc(10px - var(--size));
|
||||||
|
fill: var(--block);
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
x: calc(var(--x) * var(--size) + var(--offset) - 1px);
|
||||||
|
y: calc(var(--y) * var(--size) + var(--offset) + 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate {
|
||||||
|
fill: var(--gate);
|
||||||
|
}
|
||||||
|
|
||||||
|
.letter {
|
||||||
|
fill: var(--outline);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<rect class="tetris" style="--x: 0; --y: 2"></rect>
|
||||||
|
<rect class="tetris" style="--x: 1; --y: 2"></rect>
|
||||||
|
<rect class="tetris" style="--x: 1; --y: 1"></rect>
|
||||||
|
<rect class="tetris" style="--x: 2; --y: 1"></rect>
|
||||||
|
|
||||||
|
<path class="gate" d="M2.5,1.5 h5 a 5 5 0 0 1 0,10 h-5 v-10"></path>
|
||||||
|
<circle class="gate" cx="14.5" cy="6.5" r="2"></circle>
|
||||||
|
|
||||||
|
<path class="letter" d="M26,10 h-6 v-1 l5,-4 v-1 l-1,-1 h-2.5 l-.5,1 h-1 v-1 l1,-1 h4 l1,1 v2 l-5,4 h5 v1"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"If-then Statement": {
|
||||||
|
"prefix": [
|
||||||
|
"if",
|
||||||
|
"then",
|
||||||
|
"condition"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"\t${1:$LINE_COMMENT D = condition}",
|
||||||
|
"\t@${2:IF_END}",
|
||||||
|
"\tD;JEQ",
|
||||||
|
"\t${0:$LINE_COMMENT code}",
|
||||||
|
"(${2:IF_END})"
|
||||||
|
],
|
||||||
|
"description": "An if-then statement."
|
||||||
|
},
|
||||||
|
"If-then-else Statement": {
|
||||||
|
"prefix": [
|
||||||
|
"if",
|
||||||
|
"then",
|
||||||
|
"else",
|
||||||
|
"elif",
|
||||||
|
"condition"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"\t${1:$LINE_COMMENT D = condition}",
|
||||||
|
"\t@${2:IF_ELSE}",
|
||||||
|
"\tD;JEQ",
|
||||||
|
"\t${4:$LINE_COMMENT code}",
|
||||||
|
"\t@${3:IF_END}",
|
||||||
|
"\t0;JMP",
|
||||||
|
"(${2:IF_ELSE})",
|
||||||
|
"\t${0:$LINE_COMMENT code}",
|
||||||
|
"(${3:IF_END})"
|
||||||
|
],
|
||||||
|
"description": "An if-then-else statement."
|
||||||
|
},
|
||||||
|
"While Loop": {
|
||||||
|
"prefix": [
|
||||||
|
"repeat",
|
||||||
|
"loop",
|
||||||
|
"for",
|
||||||
|
"while"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"(${1:LOOP})",
|
||||||
|
"\t${2:$LINE_COMMENT D = condition}",
|
||||||
|
"\t@${3:LOOP_END}",
|
||||||
|
"\tD;JEQ",
|
||||||
|
"\t${0:$LINE_COMMENT code}",
|
||||||
|
"\t@${1:LOOP}",
|
||||||
|
"\t0;JMP",
|
||||||
|
"(${3:LOOP_END})"
|
||||||
|
],
|
||||||
|
"description": "A while loop."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": [
|
||||||
|
"asm"
|
||||||
|
],
|
||||||
|
"name": "Hack Assembly Language",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#declaration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#A-instruction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#destination"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#jump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#comment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"declaration": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Label declaration",
|
||||||
|
"match": "\\(([^ \\/]*)\\)",
|
||||||
|
"name": "storage.type",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Label",
|
||||||
|
"match": "[a-zA-Z\\_\\.\\$\\:]+[a-zA-Z\\_\\.\\$\\:\\d]*",
|
||||||
|
"name": "variable.other"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"A-instruction": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "@XXX instruction",
|
||||||
|
"match": "\\@([^ \\/]*)",
|
||||||
|
"name": "keyword.operator",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "R10|R11|R12|R13|R14|R15|R0|R1|R2|R3|R4|R5|R6|R7|R8|R9|SP|LCL|ARG|THIS|THAT|SCREEN|KBD",
|
||||||
|
"name": "variable.language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "A-instruction string argument",
|
||||||
|
"match": "[a-zA-Z\\_\\.\\$\\:]+[a-zA-Z\\_\\.\\$\\:\\d]*",
|
||||||
|
"name": "variable.other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "\\d+",
|
||||||
|
"name": "constant.numeric"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Left-hand side",
|
||||||
|
"match": "null|M|D|A|MD|AM|AD|AMD",
|
||||||
|
"name": "variable.language"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jump": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Jump type",
|
||||||
|
"match": "JGT|JLE|JEQ|JLT|JNE|JMP|JGE",
|
||||||
|
"name": "keyword.control"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Inline comment",
|
||||||
|
"begin": "\\/\\/",
|
||||||
|
"end": "\\n",
|
||||||
|
"name": "comment.line.double-slash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Multiline comment",
|
||||||
|
"begin": "\\/\\*",
|
||||||
|
"end": "\\*\\/",
|
||||||
|
"name": "comment.block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scopeName": "source.asm"
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": [
|
||||||
|
"cmp",
|
||||||
|
"out"
|
||||||
|
],
|
||||||
|
"name": "CMP/OUT",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#border"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#variable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#placeholder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"border": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Vertical table border",
|
||||||
|
"name": "keyword.operator",
|
||||||
|
"match": "\\|"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "The variable identifier (table header)",
|
||||||
|
"name": "support.variable",
|
||||||
|
"match": "[a-zA-Z]+"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "The value of a certain variable at a given time",
|
||||||
|
"name": "constant.numeric",
|
||||||
|
"match": "-*\\d+\\+*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "List of wildcard characters",
|
||||||
|
"name": "constant.language",
|
||||||
|
"match": "\\*+"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scopeName": "source.cmp"
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": ["hack"],
|
||||||
|
"name": "Hack Machine Language",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"a": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "0-value",
|
||||||
|
"begin": "\\b0",
|
||||||
|
"end": "\\b",
|
||||||
|
"name": "comment",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "value",
|
||||||
|
"match": "[01]{15}\\b",
|
||||||
|
"name": "constant.numeric"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "1-??-a-comp-dest-jump",
|
||||||
|
"match": "\\b1[01]{2}([01]{1})([01]{6})([01]{3})([01]{3})\\b",
|
||||||
|
"name": "comment",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"comment": "control",
|
||||||
|
"name": "keyword.control"
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"comment": "op",
|
||||||
|
"name": "keyword.operarator"
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"comment": "dest",
|
||||||
|
"name": "variable.language"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"comment": "jump",
|
||||||
|
"name": "keyword.control"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scopeName": "source.hack"
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"CHIP Class": {
|
||||||
|
"body": [
|
||||||
|
"CHIP $1 {",
|
||||||
|
" IN $2;",
|
||||||
|
" OUT $3;",
|
||||||
|
"",
|
||||||
|
" PARTS:",
|
||||||
|
" $4",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Create CHIP class.\n",
|
||||||
|
"prefix": "CHIP"
|
||||||
|
},
|
||||||
|
"Chip Add16": {
|
||||||
|
"body": [
|
||||||
|
"Add16(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": [
|
||||||
|
"* Adds two 16-bit values.\n* The most significant carry bit is ignored.\n"
|
||||||
|
],
|
||||||
|
"prefix": "Add16"
|
||||||
|
},
|
||||||
|
"Chip ALU": {
|
||||||
|
"body": [
|
||||||
|
"ALU(x=$1, y=$2, zx=$3, nx=$4, zy=$5, ny=$6, f=$7, no=$8, out=$9, zr=$10, ng=$11);"
|
||||||
|
],
|
||||||
|
"description": [
|
||||||
|
"* the ALU manipulates the x and y.\n"
|
||||||
|
],
|
||||||
|
"prefix": "ALU"
|
||||||
|
},
|
||||||
|
"Chip And": {
|
||||||
|
"body": [
|
||||||
|
"And(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* And gate:\n* out = 1 if (a == 1 and b == 1)\n* 0 otherwise\n",
|
||||||
|
"prefix": "And"
|
||||||
|
},
|
||||||
|
"Chip And16": {
|
||||||
|
"body": [
|
||||||
|
"And16(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* 16-bit bitwise And:\n* for i = 0..15: out[i] = (a[i] and b[i])\n",
|
||||||
|
"prefix": "And16"
|
||||||
|
},
|
||||||
|
"Chip ARegister": {
|
||||||
|
"body": [
|
||||||
|
"ARegister(in=$1, load=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* A 16-Bit register called \"A Register\"\n",
|
||||||
|
"prefix": "ARegister"
|
||||||
|
},
|
||||||
|
"Chip Bit": {
|
||||||
|
"body": [
|
||||||
|
"Bit(in=$1, load=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* 1-bit register:\n* If load[t] == 1 then out[t+1] = in[t]\n* else out does not change (out[t+1] = out[t])\n",
|
||||||
|
"prefix": "Bit"
|
||||||
|
},
|
||||||
|
"Chip DFF": {
|
||||||
|
"body": [
|
||||||
|
"DFF(in=$1, out=$2);"
|
||||||
|
],
|
||||||
|
"description": "* Data Flip-flop:\n* out(t) = in(t-1)\n* where t is the current time unit, or clock cycle.\n",
|
||||||
|
"prefix": "DFF"
|
||||||
|
},
|
||||||
|
"Chip DMux": {
|
||||||
|
"body": [
|
||||||
|
"DMux(in=$1, sel=$2, a=$3, b=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Demultiplexor:\n* {a, b} = {in, 0} if sel == 0\n* {0, in} if sel == 1\n",
|
||||||
|
"prefix": "DMux"
|
||||||
|
},
|
||||||
|
"Chip DMux4Way": {
|
||||||
|
"body": [
|
||||||
|
"DMux4Way(in=$1, sel=$2, a=$3, b=$4, c=$5, d=$6);"
|
||||||
|
],
|
||||||
|
"description": "* 4-way demultiplexor:\n* {a, b, c, d} = {in, 0, 0, 0} if sel == 00\n* {0, in, 0, 0} if sel == 01\n* {0, 0, in, 0} if sel == 10\n* {0, 0, 0, in} if sel == 11\n",
|
||||||
|
"prefix": "DMux4Way"
|
||||||
|
},
|
||||||
|
"Chip DMux8Way": {
|
||||||
|
"body": [
|
||||||
|
"DMux8Way(in=$1, sel=$2, a=$3, b=$4, c=$5, d=$6, e=$7, f=$8, g=$9, h=$10);"
|
||||||
|
],
|
||||||
|
"description": "* 8-way demultiplexor:\n* {a, b, c, d, e, f, g, h} = {in, 0, 0, 0, 0, 0, 0, 0} if sel == 000\n* {0, in, 0, 0, 0, 0, 0, 0} if sel == 001\n* etc.\n* {0, 0, 0, 0, 0, 0, 0, in} if sel == 111\n",
|
||||||
|
"prefix": "DMux8Way"
|
||||||
|
},
|
||||||
|
"Chip DRegister": {
|
||||||
|
"body": [
|
||||||
|
"DRegister(in=$1, load=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* A 16-Bit register called \"D Register\"\n",
|
||||||
|
"prefix": "DRegister"
|
||||||
|
},
|
||||||
|
"Chip FullAdder": {
|
||||||
|
"body": [
|
||||||
|
"FullAdder(a=$1, b=$2, c=$3, sum=$4, carry=$5);"
|
||||||
|
],
|
||||||
|
"description": "* Full adder:\n* Computes sum, the least significant bit of a + b + c, and carry, the most significant bit of a + b + c.\n",
|
||||||
|
"prefix": "FullAdder"
|
||||||
|
},
|
||||||
|
"Chip HalfAdder": {
|
||||||
|
"body": [
|
||||||
|
"HalfAdder(a=$1, b=$2, sum=$3, carry=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Half adder:\n* Computes sum, the least significnat bit of a + b, and carry, the most significnat bit of a + b.\n",
|
||||||
|
"prefix": "HalfAdder"
|
||||||
|
},
|
||||||
|
"Chip Inc16": {
|
||||||
|
"body": [
|
||||||
|
"Inc16(in=$1, out=$2);"
|
||||||
|
],
|
||||||
|
"description": "* 16-bit incrementer:\n* out = in + 1 (arithmetic addition)\n",
|
||||||
|
"prefix": "Inc16"
|
||||||
|
},
|
||||||
|
"Chip Keyboard": {
|
||||||
|
"body": [
|
||||||
|
"Keyboard(out=$1);"
|
||||||
|
],
|
||||||
|
"description": "* The keyboard (memory map).\n* Outputs the code of the currently pressed key\n",
|
||||||
|
"prefix": "Keyboard"
|
||||||
|
},
|
||||||
|
"Chip Mux": {
|
||||||
|
"body": [
|
||||||
|
"Mux(a=$1, b=$2, sel=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Multiplexor:\n* If sel == 1 then out = b else out = a.\n",
|
||||||
|
"prefix": "Mux"
|
||||||
|
},
|
||||||
|
"Chip Mux16": {
|
||||||
|
"body": [
|
||||||
|
"Mux16(a=$1, b=$2, sel=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* 16-bit multiplexor:\n* for i = 0..15 out[i] = a[i] if sel == 0\n* b[i] if sel == 1\n",
|
||||||
|
"prefix": "Mux16"
|
||||||
|
},
|
||||||
|
"Chip Mux4Way16": {
|
||||||
|
"body": [
|
||||||
|
"Mux4Way16(a=$1, b=$2, c=$3, d=$4, sel=$5, out=$6);"
|
||||||
|
],
|
||||||
|
"description": "* 4-way 16-bit multiplexor:\n* out = a if sel == 00\n* b if sel == 01\n* c if sel == 10\n* d if sel == 11\n",
|
||||||
|
"prefix": "Mux4Way16"
|
||||||
|
},
|
||||||
|
"Chip Mux8Way16": {
|
||||||
|
"body": [
|
||||||
|
"Mux8Way16(a=$1, b=$2, c=$3, d=$4, e=$5, f=$6, g=$7, h=$8, sel=$9, out=$10);"
|
||||||
|
],
|
||||||
|
"description": "* 8-way 16-bit multiplexor:\n* out = a if sel == 000\n* b if sel == 001\n* etc.\n* h if sel == 111\n",
|
||||||
|
"prefix": "Mux8Way16"
|
||||||
|
},
|
||||||
|
"Chip Nand": {
|
||||||
|
"body": [
|
||||||
|
"Nand(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* Nand gate:\n* out = a Nand b.\n",
|
||||||
|
"prefix": "Nand"
|
||||||
|
},
|
||||||
|
"Chip Not": {
|
||||||
|
"body": [
|
||||||
|
"Not(in=$1, out=$2);"
|
||||||
|
],
|
||||||
|
"description": "* Not gate:\n* out = not in\n",
|
||||||
|
"prefix": "Not"
|
||||||
|
},
|
||||||
|
"Chip Not16": {
|
||||||
|
"body": [
|
||||||
|
"Not16(in=$1, out=$2);"
|
||||||
|
],
|
||||||
|
"description": "* 16-bit Not:\n* for i=0..15: out[i] = not in[i]\n",
|
||||||
|
"prefix": "Not16"
|
||||||
|
},
|
||||||
|
"Chip Or": {
|
||||||
|
"body": [
|
||||||
|
"Or(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* Or gate:\n* out = 1 if (a == 1 or b == 1)\n* 0 otherwise\n",
|
||||||
|
"prefix": "Or"
|
||||||
|
},
|
||||||
|
"Chip Or16": {
|
||||||
|
"body": [
|
||||||
|
"Or16(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* 16-bit bitwise Or gate:\n* for i = 0..15 out[i] = a[i] or b[i].\n",
|
||||||
|
"prefix": "Or16"
|
||||||
|
},
|
||||||
|
"Chip Or8Way": {
|
||||||
|
"body": [
|
||||||
|
"Or8Way(in=$1, out=$2);"
|
||||||
|
],
|
||||||
|
"description": "* 8-way Or\n* out = (in[0] or in[1] or ... or in[7])\n",
|
||||||
|
"prefix": "Or8Way"
|
||||||
|
},
|
||||||
|
"Chip PC": {
|
||||||
|
"body": [
|
||||||
|
"PC(in=$1, load=$2, inc=$3, reset=$4, out=$5);"
|
||||||
|
],
|
||||||
|
"description": "* A 16-bit counter with load and reset control bits.\n* if (reset[t] == 1) out[t+1] = 0\n* else if (load[t] == 1) out[t+1] = in[t]\n* else if (inc[t] == 1) out[t+1] = out[t] + 1 (integer addition)\n* else out[t+1] = out[t]\n",
|
||||||
|
"prefix": "PC"
|
||||||
|
},
|
||||||
|
"Chip RAM16K": {
|
||||||
|
"body": [
|
||||||
|
"RAM16K(in=$1, load=$2, address=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Memory of 16K registers, each 16 bit-wide. Out holds the value stored at the memory location specified by address.\n* If load==1, then the in value is loaded into the memory location specified by address (the loaded value will be emitted to out from the next time step onward).\n",
|
||||||
|
"prefix": "RAM16K"
|
||||||
|
},
|
||||||
|
"Chip RAM4K": {
|
||||||
|
"body": [
|
||||||
|
"RAM4K(in=$1, load=$2, address=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Memory of 4K registers, each 16 bit-wide. Out holds the value stored at the memory location specified by address.\n* If load==1, then the in value is loaded into the memory location specified by address (the loaded value will be emitted to out from the next time step onward).\n",
|
||||||
|
"prefix": "RAM4K"
|
||||||
|
},
|
||||||
|
"Chip RAM512": {
|
||||||
|
"body": [
|
||||||
|
"RAM512(in=$1, load=$2, address=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Memory of 512 registers, each 16 bit-wide. Out holds the value stored at the memory location specified by address.\n* If load==1, then the in value is loaded into the memory location specified by address (the loaded value will be emitted to out from the next time step onward).\n",
|
||||||
|
"prefix": "RAM512"
|
||||||
|
},
|
||||||
|
"Chip RAM64": {
|
||||||
|
"body": [
|
||||||
|
"RAM64(in=$1, load=$2, address=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Memory of 64 registers, each 16 bit-wide. Out holds the value stored at the memory location specified by address.\n* If load==1, then the in value is loaded into the memory location specified by address (the loaded value will be emitted to out from the next time step onward).\n",
|
||||||
|
"prefix": "RAM64"
|
||||||
|
},
|
||||||
|
"Chip RAM8": {
|
||||||
|
"body": [
|
||||||
|
"RAM8(in=$1, load=$2, address=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* Memory of 8 registers, each 16 bit-wide. Out holds the value stored at the memory location specified by address.\n* If load==1, then the in value is loaded into the memory location specified by address (the loaded value will be emitted to out from the next time step onward).\n",
|
||||||
|
"prefix": "RAM8"
|
||||||
|
},
|
||||||
|
"Chip Register": {
|
||||||
|
"body": [
|
||||||
|
"Register(in=$1, load=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* 16-bit register:\n* If load[t] == 1 then out[t+1] = in[t]\n* else out does not change\n",
|
||||||
|
"prefix": "Register"
|
||||||
|
},
|
||||||
|
"Chip ROM32K": {
|
||||||
|
"body": [
|
||||||
|
"ROM32K(address=$1, out=$2);"
|
||||||
|
],
|
||||||
|
"description": "* Read-Only memory (ROM) of 16K registers, each 16-bit wide.\n* The chip is designed to facilitate data read, as follows:\n* out(t) = ROM32K[address(t)](t)\n",
|
||||||
|
"prefix": "ROM32K"
|
||||||
|
},
|
||||||
|
"Chip Screen": {
|
||||||
|
"body": [
|
||||||
|
"Screen(in=$1, load=$2, address=$3, out=$4);"
|
||||||
|
],
|
||||||
|
"description": "* The Screen (memory map).\n* Functions exactly like a 16-bit 8K RAM:\n* 1. out(t)=Screen[address(t)](t)\n* 2. If load(t-1) then Screen[address(t-1)](t)=in(t-1)\n",
|
||||||
|
"prefix": "Screen"
|
||||||
|
},
|
||||||
|
"Chip Xor": {
|
||||||
|
"body": [
|
||||||
|
"Xor(a=$1, b=$2, out=$3);"
|
||||||
|
],
|
||||||
|
"description": "* Exclusive-or gate:\n* out = !(a == b).\n",
|
||||||
|
"prefix": "Xor"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": [
|
||||||
|
"hdl"
|
||||||
|
],
|
||||||
|
"name": "HDL",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Block comment",
|
||||||
|
"begin": "\\/\\*",
|
||||||
|
"end": "\\*\\/",
|
||||||
|
"name": "comment.block.hdl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Line comment",
|
||||||
|
"begin": "\\/\\/",
|
||||||
|
"end": "\\n",
|
||||||
|
"name": "comment.line.hdl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "keywords",
|
||||||
|
"name": "keyword.hdl",
|
||||||
|
"match": "IN|OUT|PARTS|BUILTIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "true, false",
|
||||||
|
"name": "constant.language.hdl",
|
||||||
|
"match": "true|false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "bus index",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"name": "constant.numeric.hdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match": "\\[([\\d]+)\\]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": ".. in bus slicing",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"name": "constant.numeric.hdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match": "\\[(\\d+\\.{2}\\d+)\\]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "chip name",
|
||||||
|
"name": "storage.type.hdl",
|
||||||
|
"match": "CHIP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "inner chip",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"name": "entity.name.function.hdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match": "([\\w\\d\\_]+)\\s*\\("
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "inner chip inputs and outputs",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"name": "variable.parameter.hdl"
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"name": "constant.numeric.hdl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match": "([\\w\\d\\_]+)\\s*(\\[.+\\])*="
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scopeName": "source.hdl"
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"Class": {
|
||||||
|
"prefix": [
|
||||||
|
"class",
|
||||||
|
"struct"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"class ${1:Name} {",
|
||||||
|
"\t${0:$LINE_COMMENT attributes and methods}",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "A class interface."
|
||||||
|
},
|
||||||
|
"Function/method": {
|
||||||
|
"prefix": [
|
||||||
|
"function",
|
||||||
|
"method",
|
||||||
|
"procedure",
|
||||||
|
"void",
|
||||||
|
"int",
|
||||||
|
"String",
|
||||||
|
"Array",
|
||||||
|
"Char"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"${1|function,method|} ${2:void} ${3:name} (${4:$BLOCK_COMMENT_START arguments $BLOCK_COMMENT_END}) {",
|
||||||
|
"\t${0:$LINE_COMMENT code}",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "A function/method definition."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": [
|
||||||
|
"jack"
|
||||||
|
],
|
||||||
|
"name": "Jack Language",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#keyword"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#constant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#declaration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#comment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"keyword": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Keyword related to flow control",
|
||||||
|
"match": "\\b(if|else|while|do|return)\\b",
|
||||||
|
"name": "keyword.control"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Miscellaneous keyword",
|
||||||
|
"match": "\\blet\\b",
|
||||||
|
"name": "keyword.other"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"constant": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Numeric constant",
|
||||||
|
"name": "constant.numeric",
|
||||||
|
"match": "\\b[0-9]+\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "String literal",
|
||||||
|
"begin": "\\\"",
|
||||||
|
"end": "\\\"",
|
||||||
|
"name": "string.quoted.double",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Escape characters",
|
||||||
|
"match": "\\\\.",
|
||||||
|
"name": "constant.character.escape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Language constant",
|
||||||
|
"name": "constant.language",
|
||||||
|
"match": "\\b(true|false|null|this)\\b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Class declaration",
|
||||||
|
"match": "\\bclass\\b",
|
||||||
|
"name": "storage.type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Variable declaration",
|
||||||
|
"name": "storage.type",
|
||||||
|
"match": "\\b(field|static|var)\\s+(\\w+)\\b",
|
||||||
|
"captures": {
|
||||||
|
"1": {
|
||||||
|
"name": "storage.modifier"
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"name": "storage.type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Function declaration",
|
||||||
|
"begin": "\\b(constructor|function|method)\\s+([A-Za-z_][A-Za-z_0-9]*)\\s+([A-Za-z_][A-Za-z_0-9]*)\\s*\\(",
|
||||||
|
"end": "\\)",
|
||||||
|
"match": "meta.function",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": {
|
||||||
|
"name": "storage.modifier"
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"name": "storage.type"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "\\s*([A-Za-z_][A-Za-z_0-9]*)\\s+(?=[A-Za-z_][A-Za-z_0-9]*)\\b",
|
||||||
|
"name": "storage.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Inline comment",
|
||||||
|
"begin": "\\/\\/",
|
||||||
|
"end": "\\n",
|
||||||
|
"name": "comment.line.double-slash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Multiline comment",
|
||||||
|
"begin": "\\/\\*",
|
||||||
|
"end": "\\*\\/",
|
||||||
|
"name": "comment.block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scopeName": "source.jack"
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"comments": {
|
||||||
|
"lineComment": "//",
|
||||||
|
"blockComment": ["/*", "*/"]
|
||||||
|
},
|
||||||
|
// symbols used as brackets
|
||||||
|
"brackets": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"]
|
||||||
|
],
|
||||||
|
// symbols that are auto closed when typing
|
||||||
|
"autoClosingPairs": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"],
|
||||||
|
["\"", "\""],
|
||||||
|
["'", "'"]
|
||||||
|
],
|
||||||
|
// symbols that that can be used to surround a selection
|
||||||
|
"surroundingPairs": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"],
|
||||||
|
["\"", "\""],
|
||||||
|
["'", "'"]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"Repeat Loop": {
|
||||||
|
"prefix": [
|
||||||
|
"repeat",
|
||||||
|
"loop",
|
||||||
|
"while",
|
||||||
|
"for"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"repeat ${1:iterations} {",
|
||||||
|
"\t${0:$LINE_COMMENT code",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "A repeat loop."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": [
|
||||||
|
"tst"
|
||||||
|
],
|
||||||
|
"name": "TST",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#instruction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#comment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"instruction": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Everything which is not a comment is a function",
|
||||||
|
"begin": "\\b",
|
||||||
|
"end": "(,|;)",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Every actual instruction starts with a function",
|
||||||
|
"name": "support.function",
|
||||||
|
"match": "(output-file|compare-to|output-list|set|eval|output|load)\\b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "A single bit",
|
||||||
|
"name": "constant.numeric",
|
||||||
|
"match": "\\s(1|0)|%B(\\d|\\.)+"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Filename with a known extension",
|
||||||
|
"name": "string.interpolated",
|
||||||
|
"match": "\\w+\\.(hdl|out|cmp)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Inline comment",
|
||||||
|
"begin": "\\/\\/",
|
||||||
|
"end": "\\n",
|
||||||
|
"name": "comment.line.double-slash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Multiline comment",
|
||||||
|
"begin": "\\/\\*",
|
||||||
|
"end": "\\*\\/",
|
||||||
|
"name": "comment.block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scopeName": "source.tst"
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"If-then Statement": {
|
||||||
|
"prefix": [
|
||||||
|
"if",
|
||||||
|
"then",
|
||||||
|
"condition"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"\t${1:$LINE_COMMENT not-condition}",
|
||||||
|
"\tif-goto\t${2:if_end}",
|
||||||
|
"\t${0:$LINE_COMMENT code}",
|
||||||
|
"label\t\t${2:IF_END}"
|
||||||
|
],
|
||||||
|
"description": "An if-then statement."
|
||||||
|
},
|
||||||
|
"If-then-else Statement": {
|
||||||
|
"prefix": [
|
||||||
|
"if",
|
||||||
|
"then",
|
||||||
|
"else",
|
||||||
|
"elif",
|
||||||
|
"condition"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"\t${1:$LINE_COMMENT not-condition}",
|
||||||
|
"\tif-goto\t${2:if_else}",
|
||||||
|
"\t${3:$LINE_COMMENT code}",
|
||||||
|
"\tgoto\t${4:if_end}",
|
||||||
|
"label\t\t${2:if_else}",
|
||||||
|
"\t${0:$LINE_COMMENT code}",
|
||||||
|
"label\t\t${4:if_end}"
|
||||||
|
],
|
||||||
|
"description": "An if-then-else statement."
|
||||||
|
},
|
||||||
|
"While Loop": {
|
||||||
|
"prefix": [
|
||||||
|
"repeat",
|
||||||
|
"loop",
|
||||||
|
"for",
|
||||||
|
"while"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"label\t\t${1:loop}",
|
||||||
|
"\t${2:$LINE_COMMENT not condition}",
|
||||||
|
"\tif-goto\t${3:loop_end}",
|
||||||
|
"\t${4:$LINE_COMMENT code}",
|
||||||
|
"\tgoto\t${1:loop}",
|
||||||
|
"label\t\t${3:loop_end}"
|
||||||
|
],
|
||||||
|
"description": "A while loop."
|
||||||
|
},
|
||||||
|
"Function": {
|
||||||
|
"prefix": [
|
||||||
|
"function",
|
||||||
|
"procedure"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"function ${TM_FILENAME/(.*)\\..+$/$1/}.${1:name} ${2:0}",
|
||||||
|
"\t${0:$LINE_COMMENT code}"
|
||||||
|
],
|
||||||
|
"description": "A function definition."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"fileTypes": [
|
||||||
|
"vm"
|
||||||
|
],
|
||||||
|
"name": "Virtual Machine Language",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#arithmetic-command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#memory-access-command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#program-flow-command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#function-calling-command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "#comment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"arithmetic-command": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Performs arithmetic and logical operations on the stack.",
|
||||||
|
"match": "\\b(add|sub|neg|eq|gt|lt|and|or|not|shiftleft|shiftright)\\b",
|
||||||
|
"name": "keyword.operator"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"memory-access-command": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Transfers data between the stack and virtual memory segments.",
|
||||||
|
"match": "\\b(push|pop)\\s+([^ \\/]*)\\s+(\\d+)\\b",
|
||||||
|
"name": "keyword.operator",
|
||||||
|
"captures": {
|
||||||
|
"2": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Segment",
|
||||||
|
"match": "[a-zA-Z\\_\\.\\$\\:]+[a-zA-Z\\_\\.\\$\\:\\d]*",
|
||||||
|
"name": "storage.type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Location",
|
||||||
|
"match": "\\d+",
|
||||||
|
"name": "constant.numeric"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"program-flow-command": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Facilitates conditional and unconditional branching operations.",
|
||||||
|
"match": "\\b(label|goto|if-goto)\\s+([^ \\/]*)\\b",
|
||||||
|
"name": "keyword.control",
|
||||||
|
"captures": {
|
||||||
|
"2": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Label",
|
||||||
|
"match": "[a-zA-Z\\_\\.\\$\\:]+[a-zA-Z\\_\\.\\$\\:\\d]*",
|
||||||
|
"name": "entity.name.section"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"function-calling-command": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Defines/calls functions.",
|
||||||
|
"match": "\\b(function|call)\\s+([^ \\/]*)\\s+(\\w+)\\b",
|
||||||
|
"name": "keyword.control",
|
||||||
|
"captures": {
|
||||||
|
"2": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Function name",
|
||||||
|
"match": "[a-zA-Z\\_\\.\\$\\:]+[a-zA-Z\\_\\.\\$\\:\\d]*",
|
||||||
|
"name": "entity.name.function"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Number of local variables/arguments passed",
|
||||||
|
"match": "\\d+",
|
||||||
|
"name": "constant.numeric"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Returns from functions.",
|
||||||
|
"match": "return",
|
||||||
|
"name": "keyword.control"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"comment": "Inline comment",
|
||||||
|
"begin": "\\/\\/",
|
||||||
|
"end": "\\n",
|
||||||
|
"name": "comment.line.double-slash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "Multiline comment",
|
||||||
|
"begin": "\\/\\*",
|
||||||
|
"end": "\\*\\/",
|
||||||
|
"name": "comment.block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scopeName": "source.vm"
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"name": "@nand2tetris/vscode",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "NAND2Tetris IDE features for VSCode",
|
||||||
|
"author": "David Souther <davidsouther@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/davidsouther/nand2tetris.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://davidsouther.github.io/nand2tetris",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "shx mkdir -p out/views/hdl && cd .. && npm run build -w extension/views/hdl",
|
||||||
|
"build": "npx esbuild ./src/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node --sourcemap",
|
||||||
|
"watch": "npm run build -- --watch",
|
||||||
|
"vscode:prepublish": "npm run build",
|
||||||
|
"package": "vsce package"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.61.0"
|
||||||
|
},
|
||||||
|
"main": "./out/main.js",
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "nand2tetris.run",
|
||||||
|
"title": "Nand2Tetris: run code",
|
||||||
|
"icon": "./images/button.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "nand2tetris.stop",
|
||||||
|
"title": "Nand2Tetris: stop running"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "nand2tetris.hardware",
|
||||||
|
"title": "Nand2Tetris: open hardware Simulator"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "Nand2Tetris IDE",
|
||||||
|
"properties": {
|
||||||
|
"nand2tetris.showRunIconInEditorTitleMenu": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Whether to show 'Run Code' icon in editor title menu.",
|
||||||
|
"scope": "resource"
|
||||||
|
},
|
||||||
|
"nand2tetris.showTranslateIconInEditorTitleMenu": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Whether to show 'Translate Code' icon in editor title menu.",
|
||||||
|
"scope": "resource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"id": "hdl",
|
||||||
|
"aliases": [
|
||||||
|
"HDL"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".hdl"
|
||||||
|
],
|
||||||
|
"configuration": "./languages/language-configuration.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tst",
|
||||||
|
"aliases": [
|
||||||
|
"TST",
|
||||||
|
"TEST",
|
||||||
|
"Test",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".tst"
|
||||||
|
],
|
||||||
|
"configuration": "./languages/language-configuration.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cmp",
|
||||||
|
"aliases": [
|
||||||
|
"CMP",
|
||||||
|
"cmp"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".cmp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "out",
|
||||||
|
"aliases": [
|
||||||
|
"OUT",
|
||||||
|
"out"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".out"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asm",
|
||||||
|
"aliases": [
|
||||||
|
"ASM",
|
||||||
|
"asm"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".asm"
|
||||||
|
],
|
||||||
|
"configuration": "./languages/language-configuration.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hack",
|
||||||
|
"aliases": [
|
||||||
|
"HACK",
|
||||||
|
"hack"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".hack"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vm",
|
||||||
|
"aliases": [
|
||||||
|
"VM",
|
||||||
|
"vm"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".vm"
|
||||||
|
],
|
||||||
|
"configuration": "./languages/language-configuration.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "jack",
|
||||||
|
"aliases": [
|
||||||
|
"JACK",
|
||||||
|
"jack"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".jack"
|
||||||
|
],
|
||||||
|
"configuration": "./languages/language-configuration.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grammars": [
|
||||||
|
{
|
||||||
|
"language": "hdl",
|
||||||
|
"scopeName": "source.hdl",
|
||||||
|
"path": "./languages/hdl.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "tst",
|
||||||
|
"scopeName": "source.tst",
|
||||||
|
"path": "./languages/tst.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "cmp",
|
||||||
|
"scopeName": "source.cmp",
|
||||||
|
"path": "./languages/cmp-out.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "out",
|
||||||
|
"scopeName": "source.out",
|
||||||
|
"path": "./languages/cmp-out.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "asm",
|
||||||
|
"scopeName": "source.asm",
|
||||||
|
"path": "./languages/asm.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "hack",
|
||||||
|
"scopeName": "source.hack",
|
||||||
|
"path": "./languages/hack.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "vm",
|
||||||
|
"scopeName": "source.vm",
|
||||||
|
"path": "./languages/vm.tmLanguage.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "jack",
|
||||||
|
"scopeName": "source.jack",
|
||||||
|
"path": "./languages/jack.tmLanguage.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"snippets": [
|
||||||
|
{
|
||||||
|
"language": "hdl",
|
||||||
|
"path": "./languages/hdl.snippets.json.code-snippets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "tst",
|
||||||
|
"path": "./languages/tst.snippets.json.code-snippets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "asm",
|
||||||
|
"path": "./languages/asm.snippets.json.code-snippets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "vm",
|
||||||
|
"path": "./languages/vm.snippets.json.code-snippets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"language": "jack",
|
||||||
|
"path": "./languages/jack.snippets.json.code-snippets"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"iconThemes": [
|
||||||
|
{
|
||||||
|
"id": "nand-ide",
|
||||||
|
"label": "Nand2Tetris IDE Icon Theme",
|
||||||
|
"path": "./fileicons/icon-theme.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"viewsContainers": {
|
||||||
|
"activitybar": [
|
||||||
|
{
|
||||||
|
"id": "nand2tetris",
|
||||||
|
"icon": "./fileicons/logo.svg",
|
||||||
|
"title": "NAND2Tetris"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"nand2tetris": [
|
||||||
|
{
|
||||||
|
"type": "webview",
|
||||||
|
"id": "nand2tetris.hdlView",
|
||||||
|
"name": "HDL Chip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"activationEvents": [
|
||||||
|
"onCommand:nand2tetris.hardware",
|
||||||
|
"onLanguage:cmp",
|
||||||
|
"onLanguage:hdl",
|
||||||
|
"onLanguage:out",
|
||||||
|
"onLanguage:tst"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@davidsouther/jiffies": "^2.0.6",
|
||||||
|
"@nand2tetris/simulator": "file:../simulator",
|
||||||
|
"@types/error-cause": "^1.0.1",
|
||||||
|
"@types/node": "^16.11.41",
|
||||||
|
"@types/vscode": "^1.74.0",
|
||||||
|
"@vscode/vsce": "^2.27.0",
|
||||||
|
"@vscode/webview-ui-toolkit": "^1.2.1",
|
||||||
|
"esbuild": "^0.15.18",
|
||||||
|
"ohm-js": "^17.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"gh-pages": "6.1.1",
|
||||||
|
"react-scripts": "5.0.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"esbuild-windows-64": "^0.15.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
type Callback = Parameters<typeof vscode.commands.registerCommand>[1];
|
||||||
|
|
||||||
|
export function makeCommands(): [string, Callback][] {
|
||||||
|
const hardwareCommand: [string, Callback] = [
|
||||||
|
"nand2tetris.hardware",
|
||||||
|
async (fileUri: string) => {
|
||||||
|
console.log("Hardware Command");
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return [hardwareCommand];
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export async function hardware(fileUri: string) {
|
||||||
|
// The await eval() hack is for https://github.com/microsoft/TypeScript/issues/43329
|
||||||
|
const tst = await import("@nand2tetris/simulator/test/chiptst.js");
|
||||||
|
console.log(`Hardware for ${fileUri}`);
|
||||||
|
console.log(new tst.ChipTest());
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
import * as lang from "./languages/index.js";
|
||||||
|
|
||||||
|
async function getDiagnostics(document: vscode.TextDocument) {
|
||||||
|
switch (document.languageId) {
|
||||||
|
case "cmp":
|
||||||
|
case "out":
|
||||||
|
return lang.cmp.getDiagnostics(document);
|
||||||
|
case "hdl":
|
||||||
|
return lang.hdl.getDiagnostics(document);
|
||||||
|
case "tst":
|
||||||
|
return lang.tst.getDiagnostics(document);
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let diagnosticCollection: vscode.DiagnosticCollection;
|
||||||
|
function getDiagnosticCollection() {
|
||||||
|
if (diagnosticCollection === undefined) {
|
||||||
|
diagnosticCollection = vscode.languages.createDiagnosticCollection();
|
||||||
|
}
|
||||||
|
return diagnosticCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDiagnostics(document: vscode.TextDocument) {
|
||||||
|
getDiagnosticCollection().delete(document.uri);
|
||||||
|
const allDiagnostics = await getDiagnostics(document);
|
||||||
|
for (const [file, diagnostics] of allDiagnostics) {
|
||||||
|
getDiagnosticCollection().set(file, diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeDiagnostics() {
|
||||||
|
vscode.workspace.textDocuments.forEach(runDiagnostics);
|
||||||
|
vscode.workspace.onDidOpenTextDocument(runDiagnostics);
|
||||||
|
vscode.workspace.onDidChangeTextDocument(async (event) => {
|
||||||
|
runDiagnostics(event.document);
|
||||||
|
});
|
||||||
|
return getDiagnosticCollection();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
import { makeCommands } from "./commands.js";
|
||||||
|
import { makeDiagnostics } from "./diagnostics.js";
|
||||||
|
import { activateHdlView } from "./views/hdl.js";
|
||||||
|
|
||||||
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
makeCommands().forEach(([name, callback]) =>
|
||||||
|
context.subscriptions.push(vscode.commands.registerCommand(name, callback)),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.subscriptions.push(makeDiagnostics());
|
||||||
|
|
||||||
|
activateHdlView(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivate() {
|
||||||
|
console.log("Deactivating extension");
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Grammar } from "ohm-js";
|
||||||
|
import {
|
||||||
|
Diagnostic,
|
||||||
|
DiagnosticSeverity,
|
||||||
|
Range,
|
||||||
|
TextDocument,
|
||||||
|
Uri,
|
||||||
|
} from "vscode";
|
||||||
|
|
||||||
|
export async function getDiagnostics(
|
||||||
|
document: TextDocument,
|
||||||
|
parser: Grammar,
|
||||||
|
): Promise<[Uri, Diagnostic[]][]> {
|
||||||
|
const parsed = parser.match(document.getText());
|
||||||
|
if (!parsed.failed()) return [];
|
||||||
|
|
||||||
|
const { line, column, message } =
|
||||||
|
/Line (?<line>\d+), col (?<column>\d+): (?<message>.*)/.exec(
|
||||||
|
parsed.shortMessage ?? "",
|
||||||
|
)?.groups ?? { line: 1, column: 1, message: "could not parse error" };
|
||||||
|
|
||||||
|
const startLineNumber = Number(line);
|
||||||
|
const endLineNumber = startLineNumber;
|
||||||
|
const startColumn = Number(column);
|
||||||
|
const restOfLine = document
|
||||||
|
.lineAt(startLineNumber)
|
||||||
|
.text.substring(startColumn - 1);
|
||||||
|
let endColumn = startColumn + (restOfLine.match(/([^\s]+)/)?.[0].length ?? 1);
|
||||||
|
if (endColumn <= startColumn) {
|
||||||
|
endColumn = startColumn + 1;
|
||||||
|
}
|
||||||
|
const range = new Range(
|
||||||
|
startLineNumber - 1,
|
||||||
|
startColumn - 1,
|
||||||
|
endLineNumber - 1,
|
||||||
|
endColumn - 1,
|
||||||
|
);
|
||||||
|
const diagnostic = new Diagnostic(range, message, DiagnosticSeverity.Error);
|
||||||
|
return [[document.uri, [diagnostic]]];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { CMP } from "@nand2tetris/simulator/languages/cmp";
|
||||||
|
import { Diagnostic, TextDocument, Uri } from "vscode";
|
||||||
|
import * as base from "./base.js";
|
||||||
|
|
||||||
|
// import { load } from "../loader.js";
|
||||||
|
|
||||||
|
let cmp: typeof CMP | undefined = undefined;
|
||||||
|
async function getCmp(): Promise<typeof CMP> {
|
||||||
|
if (cmp) return Promise.resolve(cmp);
|
||||||
|
cmp = (await import("@nand2tetris/simulator/languages/cmp.js"))
|
||||||
|
.CMP as typeof CMP;
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDiagnostics(
|
||||||
|
document: TextDocument,
|
||||||
|
): Promise<[Uri, Diagnostic[]][]> {
|
||||||
|
try {
|
||||||
|
const { parser } = await getCmp();
|
||||||
|
return base.getDiagnostics(document, parser);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load tst parser", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { HDL } from "@nand2tetris/simulator/languages/hdl";
|
||||||
|
import { Diagnostic, TextDocument, Uri } from "vscode";
|
||||||
|
import * as base from "./base.js";
|
||||||
|
|
||||||
|
let hdl: typeof HDL | undefined = undefined;
|
||||||
|
async function getHdl(): Promise<typeof HDL> {
|
||||||
|
if (hdl) return Promise.resolve(hdl);
|
||||||
|
hdl = (await import("@nand2tetris/simulator/languages/hdl.js"))
|
||||||
|
.HDL as typeof HDL;
|
||||||
|
return hdl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDiagnostics(
|
||||||
|
document: TextDocument,
|
||||||
|
): Promise<[Uri, Diagnostic[]][]> {
|
||||||
|
const { parser } = await getHdl();
|
||||||
|
|
||||||
|
return base.getDiagnostics(document, parser);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const LANGUAGE_IDS = ["cmp", "hdl", "out", "tst"];
|
||||||
|
export * as base from "./base.js";
|
||||||
|
export * as cmp from "./cmp.js";
|
||||||
|
export * as hdl from "./hdl.js";
|
||||||
|
export * as tst from "./tst.js";
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { TST } from "@nand2tetris/simulator/languages/tst";
|
||||||
|
import { Diagnostic, TextDocument, Uri } from "vscode";
|
||||||
|
import * as base from "./base.js";
|
||||||
|
|
||||||
|
let tst: typeof TST | undefined = undefined;
|
||||||
|
async function getTst(): Promise<typeof TST> {
|
||||||
|
if (tst) return Promise.resolve(tst);
|
||||||
|
tst = (await import("@nand2tetris/simulator/languages/tst.js"))
|
||||||
|
.TST as typeof TST;
|
||||||
|
return tst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDiagnostics(
|
||||||
|
document: TextDocument,
|
||||||
|
): Promise<[Uri, Diagnostic[]][]> {
|
||||||
|
try {
|
||||||
|
const { parser } = await getTst();
|
||||||
|
return base.getDiagnostics(document, parser);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load tst parser", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { parse } from "path";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { hdl as HDL } from "../languages/index.js";
|
||||||
|
|
||||||
|
export function activateHdlView(context: vscode.ExtensionContext) {
|
||||||
|
const provider = new HdlViewProvider(context.extensionUri);
|
||||||
|
vscode.window.registerWebviewViewProvider(HdlViewProvider.viewType, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
class HdlViewProvider implements vscode.WebviewViewProvider {
|
||||||
|
public static readonly viewType = "nand2tetris.hdlView";
|
||||||
|
|
||||||
|
private _hdl = "";
|
||||||
|
private _view?: vscode.WebviewView;
|
||||||
|
|
||||||
|
constructor(private readonly extensionUri: vscode.Uri) {}
|
||||||
|
|
||||||
|
public resolveWebviewView(
|
||||||
|
webviewView: vscode.WebviewView,
|
||||||
|
context: vscode.WebviewViewResolveContext,
|
||||||
|
_token: vscode.CancellationToken,
|
||||||
|
) {
|
||||||
|
this._view = webviewView;
|
||||||
|
|
||||||
|
webviewView.webview.options = {
|
||||||
|
// Allow scripts in the webview
|
||||||
|
enableScripts: true,
|
||||||
|
localResourceRoots: [this.extensionUri],
|
||||||
|
};
|
||||||
|
|
||||||
|
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
|
||||||
|
|
||||||
|
webviewView.webview.onDidReceiveMessage(
|
||||||
|
(message: { nand2tetris: boolean; ready: boolean }) => {
|
||||||
|
if (message.nand2tetris && message.ready) {
|
||||||
|
this.updateHdl(vscode.window.activeTextEditor?.document);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
webviewView.onDidChangeVisibility(() => {
|
||||||
|
if (this._view?.visible !== true) {
|
||||||
|
this.clearHdl();
|
||||||
|
} else {
|
||||||
|
this.updateHdl(vscode.window.activeTextEditor?.document);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
vscode.window.onDidChangeActiveTextEditor((e) => {
|
||||||
|
this.updateHdl(e?.document);
|
||||||
|
});
|
||||||
|
|
||||||
|
vscode.workspace.onDidSaveTextDocument(async (document) => {
|
||||||
|
this.updateHdl(document);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHdl() {
|
||||||
|
this._hdl = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateHdl(document?: vscode.TextDocument) {
|
||||||
|
if (document?.languageId !== "hdl") return;
|
||||||
|
const hdl = document.getText();
|
||||||
|
if (this._hdl === hdl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const diagnostics = await HDL.getDiagnostics(document);
|
||||||
|
if ((diagnostics[0] ?? ["", []])[1].length === 0) {
|
||||||
|
const chipName = parse(document.fileName).name;
|
||||||
|
this._view?.webview.postMessage({ nand2tetris: true, hdl, chipName });
|
||||||
|
this._hdl = hdl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getHtmlForWebview(webview: vscode.Webview) {
|
||||||
|
const stylesUri = this.getUri(webview, ["hdl", "styles.css"]);
|
||||||
|
const scriptUri = this.getUri(webview, ["hdl", "main.js"]);
|
||||||
|
|
||||||
|
return /*html*/ `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||||
|
<meta name="theme-color" content="#000000">
|
||||||
|
<link rel="stylesheet" type="text/css" href="${stylesUri}">
|
||||||
|
<title>HDL - NAND2Tetris</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="${scriptUri}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUri(webview: vscode.Webview, pathList: string[]) {
|
||||||
|
return webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(this.extensionUri, "out", "views", ...pathList),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "build",
|
||||||
|
"rootDir": "src",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"tsBuildInfoFile": "build/.tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
[role="group"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="group"] > label:has(input[type="radio"]) {
|
||||||
|
color: var(--vscode-button-foreground);
|
||||||
|
background-color: var(--vscode-button-background);
|
||||||
|
align-self: stretch;
|
||||||
|
padding: 2px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="group"] > label:has(input[type="radio"]):first-of-type {
|
||||||
|
padding-left: 2px;
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="group"] > label:has(input[type="radio"]):larst-of-type {
|
||||||
|
padding-right: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="group"] > label[aria-current="true"]:has(input[type="radio"]) {
|
||||||
|
background-color: var(--vscode-button-hoverBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="group"] > label > input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||