use grid layout. begin ui cleanup

This commit is contained in:
Alexander Bass 2024-02-21 00:45:14 -05:00
parent e2d3bae806
commit e7cb198123
20 changed files with 1198 additions and 337 deletions

3
TODO
View file

@ -1,6 +1,5 @@
Add screen (VRAM?)
Live memory and register editing (Probably should pause autostep when it reaches the cell you're modifying)
Draw lines between registers and memory when used
Explain mode to explain what instructions are doing
Window showing ISA
Save binary button

View file

@ -4,30 +4,27 @@
<meta charset="UTF-8" />
<script src="./dist/main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
<title>Virtual 8-Bit Computer</title>
</head>
<body>
<div id="main">
<div id="title">VIRTUAL 8-BIT COMPUTER</div>
<div id="container">
<div id="subcontainer">
<div id="registers"></div>
<div id="labelcontainer">
<div id="registers_label">←REGISTERS</div>
<div id="memory_label">MEMORY↯</div>
</div>
</div>
<div id="memory"></div>
<div id="registers"></div>
<div id="labelcontainer">
<div id="registers_label">←REGISTERS</div>
<div id="memory_label">MEMORY↯</div>
</div>
<div id="memory"></div>
<div id="instruction_explainer"></div>
<div id="printout"></div>
</div>
<div id="controls_bar">
<button type="button" id="pause_play_button">Start</button>
<button type="button" id="step_button">Step</button>
<label for="binary_upload" class="button">Load Binary</label>
<input id="binary_upload" name="binary_upload" id="binary_upload" style="visibility: hidden" type="file" />
<div id="controls_bar">
<button type="button" id="pause_play_button">Start</button>
<button type="button" id="step_button">Step</button>
<label for="binary_upload" class="button">Load Binary</label>
<input id="binary_upload" name="binary_upload" id="binary_upload" style="display: none" type="file" />
<input type="range" name="" id="delay_range" min="0" max="1500" />
</div>
<span id="cycles"></span>
</div>
<pre id="ISA"></pre>
</body>

394
package-lock.json generated
View file

@ -7,10 +7,14 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"css-loader": "^6.10.0",
"eslint": "^8.44.0",
"sass": "^1.71.0",
"sass-loader": "^14.1.0",
"style-loader": "^3.3.4",
"ts-loader": "^9.4.4",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack": "^5.90.2",
"webpack-cli": "^5.1.4"
}
},
@ -782,6 +786,19 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -803,6 +820,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -907,6 +933,42 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@ -980,6 +1042,53 @@
"node": ">= 8"
}
},
"node_modules/css-loader": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz",
"integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==",
"dev": true,
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.4",
"postcss-modules-scope": "^3.1.1",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.5.4"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"@rspack/core": "0.x || 1.x",
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -1397,6 +1506,20 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -1534,6 +1657,18 @@
"node": ">= 0.4"
}
},
"node_modules/icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"dev": true,
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@ -1543,6 +1678,12 @@
"node": ">= 4"
}
},
"node_modules/immutable": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
"integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
"dev": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -1612,6 +1753,18 @@
"node": ">=10.13.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
@ -1898,6 +2051,24 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -1916,6 +2087,15 @@
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -2117,6 +2297,112 @@
"node": ">=8"
}
},
"node_modules/postcss": {
"version": "8.4.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-modules-extract-imports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"dev": true,
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-local-by-default": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz",
"integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==",
"dev": true,
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
"postcss-value-parser": "^4.1.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-scope": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz",
"integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==",
"dev": true,
"dependencies": {
"postcss-selector-parser": "^6.0.4"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-modules-values": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
"integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
"dev": true,
"dependencies": {
"icss-utils": "^5.0.0"
},
"engines": {
"node": "^10 || ^12 || >= 14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.15",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
"integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -2164,6 +2450,18 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@ -2291,6 +2589,63 @@
}
]
},
"node_modules/sass": {
"version": "1.71.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz",
"integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/sass-loader": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz",
"integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==",
"dev": true,
"dependencies": {
"neo-async": "^2.6.2"
},
"engines": {
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"@rspack/core": "0.x || 1.x",
"node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"sass": "^1.3.0",
"sass-embedded": "*",
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
"optional": true
},
"node-sass": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
"node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@ -2384,6 +2739,15 @@
"node": ">= 8"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
@ -2427,6 +2791,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-loader": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
"integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==",
"dev": true,
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.0.0"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -2644,6 +3024,12 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
@ -2658,9 +3044,9 @@
}
},
"node_modules/webpack": {
"version": "5.90.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
"integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
"version": "5.90.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz",
"integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",

View file

@ -3,14 +3,18 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"css-loader": "^6.10.0",
"eslint": "^8.44.0",
"sass": "^1.71.0",
"sass-loader": "^14.1.0",
"style-loader": "^3.3.4",
"ts-loader": "^9.4.4",
"typescript": "^5.1.6",
"webpack": "^5.88.2",
"webpack": "^5.90.2",
"webpack-cli": "^5.1.4"
},
"scripts": {
"build": "webpack --mode=production",
"watch": "webpack --mode=development --watch"
}
}
}

View file

@ -1,7 +1,7 @@
import { CpuEvent, CpuEventHandler, MemoryCellType, UiEvent, UiEventHandler } from "./events";
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "./events";
import { byte_array_to_js_source, format_hex, u8 } from "./etc";
import { EventHandler } from "./eventHandler";
import { ConstParam, Instruction, ISA, MemorParam, RegisParam } from "./instructionSet";
import { Instruction, ISA } from "./instructionSet";
export type TempInstrState = {
pos: u8;
@ -9,16 +9,11 @@ export type TempInstrState = {
instr: Instruction;
params: Uint8Array;
};
export type ComputerState = {
memory: Uint8Array;
program_counter: u8;
registers: Uint8Array;
current_instruction: TempInstrState | null;
};
export class Computer {
private memory = new Uint8Array(256);
private registers = new Uint8Array(8);
private call_stack: Array<u8> = [];
private program_counter: u8 = 0;
private current_instr: TempInstrState | null = null;
events: CpuEventHandler = new EventHandler<CpuEvent>() as CpuEventHandler;
@ -36,13 +31,13 @@ export class Computer {
if (this.current_instr === null) {
const parsed_instruction = ISA.getInstruction(current_byte);
if (parsed_instruction === null) {
this.events.dispatch(CpuEvent.MemoryByteParsed, {
type: MemoryCellType.InvalidInstruction,
this.events.dispatch(CpuEvent.InvalidParsed, {
pos: this.program_counter,
code: current_byte,
});
console.log(`Invalid instruction: ${format_hex(current_byte)}`);
this.step_forward();
this.events.dispatch(CpuEvent.ClockCycle, null);
return;
}
this.current_instr = {
@ -51,8 +46,7 @@ export class Computer {
params_found: 0,
params: new Uint8Array(parsed_instruction.params.length),
};
this.events.dispatch(CpuEvent.MemoryByteParsed, {
type: MemoryCellType.Instruction,
this.events.dispatch(CpuEvent.InstructionParsed, {
pos: this.program_counter,
instr: parsed_instruction,
code: current_byte,
@ -61,32 +55,23 @@ export class Computer {
if (this.current_instr.pos === this.program_counter && this.current_instr.params.length > 0) {
this.step_forward();
this.events.dispatch(CpuEvent.ClockCycle, null);
return;
}
if (this.current_instr.params.length !== this.current_instr.params_found) {
let a;
const b = this.current_instr.instr.params[this.current_instr.params_found];
if (b instanceof ConstParam) {
a = MemoryCellType.Constant;
} else if (b instanceof RegisParam) {
a = MemoryCellType.Register;
} else if (b instanceof MemorParam) {
a = MemoryCellType.Memory;
}
if (a === undefined) {
throw new Error("Shouldn't");
}
this.events.dispatch(CpuEvent.MemoryByteParsed, {
type: a,
this.events.dispatch(CpuEvent.ParameterParsed, {
param: b,
pos: this.program_counter,
code: current_byte,
param: b,
});
this.current_instr.params[this.current_instr.params_found] = current_byte;
this.current_instr.params_found += 1;
if (this.current_instr.params.length !== this.current_instr.params_found) {
this.step_forward();
this.events.dispatch(CpuEvent.ClockCycle, null);
return;
}
}
@ -104,6 +89,7 @@ export class Computer {
if (execution_post_action_state.should_step) {
this.step_forward();
}
this.events.dispatch(CpuEvent.ClockCycle, null);
}
getMemory(address: u8): u8 {
@ -130,18 +116,30 @@ export class Computer {
this.program_counter = new_value;
}
pushCallStack(address: u8): boolean {
if (this.call_stack.length >= 8) return false;
this.call_stack.push(address);
return true;
}
popCallStack(): u8 | null {
return this.call_stack.pop() ?? null;
}
reset(): void {
this.events.dispatch(CpuEvent.Reset, null);
this.memory = new Uint8Array(256);
this.registers = new Uint8Array(8);
this.call_stack = [];
this.current_instr = null;
this.program_counter = 0;
this.events.dispatch(CpuEvent.Reset, null);
}
init_events(ui: UiEventHandler): void {
ui.listen(UiEvent.RequestCpuCycle, () => {
this.cycle();
ui.listen(UiEvent.RequestCpuCycle, (n) => {
for (let i = 0; i < n; i++) this.cycle();
});
ui.listen(UiEvent.RequestMemoryChange, ({ address, value }) => this.setMemory(address, value));
}
load_memory(program: Array<u8>): void {
@ -149,9 +147,8 @@ export class Computer {
const max_loop = Math.min(this.memory.length, program.length);
for (let i = 0; i < max_loop; i++) {
// Don't fire event if no change is made
if (this.memory[i] === program[i]) {
continue;
}
if (this.memory[i] === program[i]) continue;
this.memory[i] = program[i];
this.events.dispatch(CpuEvent.MemoryChanged, { address: i, value: program[i] });
}

View file

@ -1,4 +1,7 @@
import { UiEventHandler } from "./events";
// The u8 type represents an unsigned 8bit integer: byte. It does not add any safety, other than as a hint to the programmer.
// export type u8 = 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|126|127|128|129|130|131|132|133|134|135|136|137|138|139|140|141|142|143|144|145|146|147|148|149|150|151|152|153|154|155|156|157|158|159|160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175|176|177|178|179|180|181|182|183|184|185|186|187|188|189|190|191|192|193|194|195|196|197|198|199|200|201|202|203|204|205|206|207|208|209|210|211|212|213|214|215|216|217|218|219|220|221|222|223|224|225|226|227|228|229|230|231|232|233|234|235|236|237|238|239|240|241|242|243|244|245|246|247|248|249|250|251|252|253|254|255;
export type u8 = number;
// Jquery lite

View file

@ -27,7 +27,7 @@ export class EventHandler<T> {
const event = new Event<T>(identifier);
this.events.push(event);
}
dispatch(identifier: T, event_data: unknown): void {
dispatch(identifier: T, event_data?: unknown): void {
const event = this.events.find((e) => e.identifier === identifier);
if (event === undefined) {
throw new Error("Event not found");

View file

@ -6,18 +6,14 @@ export enum CpuEvent {
MemoryChanged,
RegisterChanged,
ProgramCounterChanged,
MemoryByteParsed,
InstructionParsed,
ParameterParsed,
InvalidParsed,
InstructionExecuted,
ClockCycle,
Print,
Reset,
}
export enum MemoryCellType {
Instruction,
InvalidInstruction,
Register,
Memory,
Constant,
Halt,
}
// Handily explained in https://www.cgjennings.ca/articles/typescript-events/
@ -25,8 +21,12 @@ interface CpuEventMap {
[CpuEvent.MemoryChanged]: { address: u8; value: u8 };
[CpuEvent.RegisterChanged]: { register_no: u8; value: u8 };
[CpuEvent.ProgramCounterChanged]: { counter: u8 };
[CpuEvent.Halt]: null;
[CpuEvent.Reset]: null;
[CpuEvent.MemoryByteParsed]: { type: MemoryCellType; pos: u8; code: u8; param?: ParameterType; instr?: Instruction };
[CpuEvent.ClockCycle]: null;
[CpuEvent.InstructionParsed]: { pos: u8; code: u8; instr: Instruction };
[CpuEvent.ParameterParsed]: { pos: u8; code: u8; param: ParameterType };
[CpuEvent.InvalidParsed]: { pos: u8; code: u8 };
[CpuEvent.InstructionExecuted]: { instr: Instruction };
[CpuEvent.Print]: string;
}
@ -38,10 +38,12 @@ export interface CpuEventHandler extends EventHandler<CpuEvent> {
export enum UiEvent {
RequestCpuCycle,
RequestMemoryChange,
}
interface UiEventMap {
[UiEvent.RequestCpuCycle]: null;
[UiEvent.RequestCpuCycle]: number;
[UiEvent.RequestMemoryChange]: { address: u8; value: u8 };
}
export interface UiEventHandler extends EventHandler<UiEvent> {

View file

@ -4,9 +4,28 @@ import { ISA } from "./instructionSet";
import { generate_isa } from "./isaGenerator";
import { UI } from "./ui";
import "./style.scss";
function main(): void {
// const program = [
// 0x2f, 0x00, 0xf0, 0x20, 0x07, 0x00, 0x50, 0x05, 0x06, 0x07, 0x11, 0x00, 0x05, 0xfe, 0x07, 0x30, 0x00, 0x10, 0x03,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57,
// 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0a, 0x00, 0x00, 0x00,
// ];
const program = [
0x2f, 0x00, 0xf0, 0x20, 0x07, 0x00, 0x50, 0x05, 0x06, 0x07, 0x11, 0x00, 0x05, 0xfe, 0x07, 0x30, 0x00, 0x10, 0x03,
0x01, 0x00, 0x01, 0x00, 0xa0, 0x10, 0xff, 0x00, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xa1,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -22,28 +41,25 @@ function main(): void {
0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0a, 0x00, 0x00, 0x00,
];
const container = $("container");
if (container === null) {
throw new Error("no");
}
const computer = new Computer();
const ui = new UI(container);
const ui = new UI();
ui.init_events(computer.events);
computer.load_memory(program);
computer.init_events(ui.events);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>window).comp = computer;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>window).ui = ui;
$("ISA").textContent = generate_isa(ISA);
// eslint-disable-next-line prefer-arrow-callback
$("binary_upload").addEventListener("change", function (e) {
$("binary_upload").addEventListener("change", (e) => {
if (e.target === null) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-destructuring
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const file: File = (<any>e.target).files[0];
const reader = new FileReader();
console.log(file);

View file

@ -1,16 +1,36 @@
import { CpuEvent, CpuEventHandler } from "./events";
import { format_hex, u8 } from "./etc";
export class ParameterType {
export enum ParamType {
Const,
Register,
Memory,
}
export abstract class ParameterType {
readonly desc: string;
constructor(description: string) {
readonly type: ParamType;
constructor(description: string, p_type: ParamType) {
this.desc = description;
this.type = p_type;
}
}
export class ConstParam extends ParameterType {}
export class RegisParam extends ParameterType {}
export class MemorParam extends ParameterType {}
class ConstParam extends ParameterType {
constructor(d: string) {
super(d, ParamType.Const);
}
}
class RegisParam extends ParameterType {
constructor(d: string) {
super(d, ParamType.Register);
}
}
class MemorParam extends ParameterType {
constructor(d: string) {
super(d, ParamType.Memory);
}
}
interface GenericComputer {
getMemory: (address: u8) => u8;
@ -19,6 +39,8 @@ interface GenericComputer {
getProgramCounter: () => u8;
getRegister: (number: u8) => u8;
setRegister: (number: u8, value: u8) => void;
pushCallStack: (address: u8) => boolean;
popCallStack: () => u8 | null;
}
interface AfterExecutionComputerAction {
@ -31,7 +53,11 @@ export interface Instruction {
readonly name: string;
readonly desc: string;
readonly params: Array<ParameterType>;
execute: (computer_reference: GenericComputer, parameters: Uint8Array, a: AfterExecutionComputerAction) => void;
readonly execute: (
computer_reference: GenericComputer,
parameters: Uint8Array,
a: AfterExecutionComputerAction
) => void;
}
export class InstructionSet {
@ -55,6 +81,8 @@ export class InstructionSet {
export const ISA = new InstructionSet();
// The definitions for actual instructions.
ISA.insertInstruction(0x00, {
name: "NoOp",
desc: "No operation; do nothing",
@ -86,7 +114,7 @@ ISA.insertInstruction(0x20, {
ISA.insertInstruction(0x21, {
name: "SaveToMemory",
desc: "Writes the byte in register (P1) to the memory cell (P2)",
params: [new RegisParam(""), new MemorParam("")],
params: [new RegisParam("Write the byte in this register"), new MemorParam("To this memory address")],
execute(c, p) {
const [register_no, mem_address] = p;
c.setMemory(mem_address, c.getRegister(register_no));
@ -115,6 +143,7 @@ ISA.insertInstruction(0x11, {
}
},
});
ISA.insertInstruction(0x30, {
name: "Increment",
desc: "Increments the value within register (P1) by 1",
@ -122,7 +151,8 @@ ISA.insertInstruction(0x30, {
execute(c, p) {
const register_no = p[0];
const current_value = c.getRegister(register_no);
c.setRegister(register_no, current_value + 1);
const new_value = (current_value + 1) % 256;
c.setRegister(register_no, new_value);
},
});
@ -133,14 +163,18 @@ ISA.insertInstruction(0x31, {
execute(c, p) {
const register_no = p[0];
const current_value = c.getRegister(register_no);
c.setRegister(register_no, current_value - 1);
let new_value = current_value - 1;
if (new_value === -1) {
new_value = 255;
}
c.setRegister(register_no, new_value);
},
});
ISA.insertInstruction(0x40, {
name: "Add",
desc: "Adds the contents of (P1) and (P2) and stores result to register (P1). (Overflow will be taken mod 256)",
params: [new RegisParam(""), new RegisParam("")],
params: [new RegisParam("set this register to"), new RegisParam("it's sum with the value in this register")],
execute(c, p) {
const [register_no_1, register_no_2] = p;
const new_value = (c.getRegister(register_no_1) + c.getRegister(register_no_2)) % 256;
@ -175,3 +209,81 @@ ISA.insertInstruction(0xfe, {
a.dispatch(CpuEvent.Print, char);
},
});
ISA.insertInstruction(0x48, {
name: "Bitwise And",
desc: "Takes each bit in register (P1) and compares to the respective bit in register (P2). Each bit in register (P1) is set to 1 if the respective bit in both registers are 1",
params: [new RegisParam(""), new RegisParam("")],
execute(c, p) {
const [register_no_1, register_no_2] = p;
const new_value = c.getRegister(register_no_1) & c.getMemory(register_no_2);
c.setRegister(register_no_1, new_value);
},
});
ISA.insertInstruction(0xff, {
name: "Print",
desc: "Prints the byte in register (P1) to console as base 10",
params: [new RegisParam("Register to print from")],
execute(c, p, a) {
const register_no = p[0];
const byte = c.getRegister(register_no);
a.dispatch(CpuEvent.Print, byte.toString(10));
},
});
ISA.insertInstruction(0xfd, {
name: "Print 16 bit",
desc: "Prints the byte in register (P1) as the upper half and the byte in register (P2) as the lower half of a 16 bit number. Formats to decimal",
params: [new RegisParam("Upper 8 bits of number"), new RegisParam("Lower 8 bits of number")],
execute(c, p, a) {
const [upper_register_no, lower_register_no] = p;
const upper = c.getRegister(upper_register_no);
const lower = c.getRegister(lower_register_no);
const sum = upper * 16 * 16 + lower;
a.dispatch(CpuEvent.Print, sum.toString(10));
},
});
ISA.insertInstruction(0x66, {
name: "Halt and Catch Fire",
desc: "Stops program execu..... Fire! FIRE EVERYWHERE!",
params: [],
execute(c, p, a) {
a.dispatch(CpuEvent.Halt, null);
},
});
ISA.insertInstruction(0xa0, {
name: "Call",
desc: "",
params: [new ConstParam("the subroutine at this memory address")],
execute(c, p, a) {
const current_address = c.getProgramCounter();
const success = c.pushCallStack(current_address);
// TODO Handle success/failure
const new_address = p[0];
c.setProgramCounter(new_address);
a.noStep();
},
});
ISA.insertInstruction(0xa1, {
name: "Return",
desc: "",
params: [],
execute(c, p, a) {
const new_address = c.popCallStack();
if (new_address === null) {
throw new Error("TODO handle this");
}
c.setProgramCounter(new_address + 1);
a.noStep();
},
});

View file

@ -3,15 +3,14 @@ import { Instruction, InstructionSet } from "./instructionSet";
export function generate_isa(iset: InstructionSet): string {
const instructions: Array<[u8, Instruction]> = [];
for (const kv of iset.instructions.entries()) {
instructions.push(kv);
}
for (const kv of iset.instructions.entries()) instructions.push(kv);
let output_string = "INSTRUCTIONS\n";
let max_instr_name_len = 0;
for (const instruction of instructions) {
const short_description = instruction[1].name;
max_instr_name_len = Math.max(max_instr_name_len, short_description.length);
}
const max_instr_name_len = instructions.map((i) => i[1].name.length).reduce((acc, p) => Math.max(p, acc), 0);
instructions.sort((a, b) => a[0] - b[0]);
for (const instruction of instructions) {
const hex_code = format_hex(instruction[0]);
const short_description = instruction[1].name.padEnd(max_instr_name_len, " ");

192
src/style.scss Normal file
View file

@ -0,0 +1,192 @@
* {
box-sizing: border-box;
}
pre {
font-size: 0.5em;
}
:root {
--Border: Yellow;
--mem-instruction: greenyellow;
--mem-register: Purple;
--mem-constant: lightgray;
--mem-memory: lightgray;
--mem-invalid: red;
}
body {
color: gray;
background-color: black;
font-size: 2.5em;
font-family: monospace;
}
#labelcontainer {
column-gap: 18px;
font-size: 0.85em;
display: flex;
align-items: center;
user-select: none;
grid-area: regmemlabel;
}
#main {
justify-content: center;
display: grid;
grid-template-columns: min-content max-content max-content 10px 500px;
grid-template-rows: min-content 1.5fr 10px 2fr 2fr min-content;
grid-template-areas:
"cycles registers regmemlabel . explainer "
"title . . . explainer "
"title . . . ."
"title . . . printout "
"title . . . printout "
". buttons buttons buttons buttons";
#memory {
grid-column: 2/4;
grid-row: 2/6;
}
#cycles {
grid-area: cycles;
text-align: left;
align-self: center;
font-size: 0.48em;
}
#title {
grid-area: title;
writing-mode: vertical-lr;
text-align: left;
user-select: none;
transform: scale(-1, -1);
}
}
#printout {
border: 4px dashed yellow;
grid-area: printout;
padding: 10px;
word-wrap: break-word;
word-break: break-all;
max-height: 200px;
overflow-x: scroll;
}
#instruction_explainer {
grid-area: explainer;
display: flex;
flex-direction: column;
gap: 5px;
border: 5px dashed yellow;
#expl_box {
padding-inline: 20px;
padding-block-start: 10px;
}
#expl_text {
font-size: 0.6em;
text-align: center;
vertical-align: center;
}
#expl_icon {
margin-inline-end: 0.5em;
font-size: 30px;
padding: 5px;
color: var(--color);
outline: 3px dashed var(--color);
}
}
.invalid {
--color: var(--mem-invalid);
}
.constant {
--color: var(--mem-constant);
}
.register {
--color: var(--mem-register);
}
.memory {
--color: var(--mem-memory);
}
.instruction {
--color: var(--mem-instruction);
}
#memory {
grid-area: memory;
display: grid;
grid-template-columns: repeat(16, min-content);
gap: 5px;
padding: 10px;
border: 5px solid yellow;
div {
text-align: center;
color: var(--color);
}
.program_counter {
outline: 3px solid orange;
}
.instruction_argument {
outline: 3px dashed purple;
}
.current_instruction {
outline: 3px dashed greenyellow;
}
.invalid {
&::after {
user-select: none;
float: right;
position: relative;
right: 0.5em;
width: 0px;
font-size: 0.5em;
content: "!";
}
}
}
#registers {
grid-area: registers;
border: 5px solid yellow;
border-bottom: none;
grid-template-columns: repeat(8, min-content);
max-width: fit-content;
display: grid;
column-gap: 5px;
color: lightgray;
padding: 10px;
div {
max-height: min-content;
// margin-block: auto;
}
}
button,
label.button {
border: 4px solid yellow;
color: gray;
margin: 10px;
margin-inline: 0px;
padding: 10px;
font-size: 0.8em;
font-family: monospace;
background-color: transparent;
cursor: pointer;
user-select: none;
}
button:hover,
label.button:hover {
color: white;
}
#controls_bar {
grid-area: buttons;
display: flex;
gap: 10px;
}

165
src/ui.ts
View file

@ -1,41 +1,40 @@
import { ComputerState } from "./computer";
import { CpuEvent, CpuEventHandler, MemoryCellType, UiEvent, UiEventHandler } from "./events";
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "./events";
import { $, el, format_hex, u8 } from "./etc";
import { Instruction, ParameterType } from "./instructionSet";
import { EventHandler } from "./eventHandler";
import { InstructionExplainer } from "./ui/instructionExplainer";
import { MemoryView } from "./ui/memoryView";
import { ParamType } from "./instructionSet";
import { frequencyIndicator } from "./ui/frequencyIndicator";
// Certainly the messiest portion of this program
// Needs to be broken into components
let delay = 100;
export class UI {
container: HTMLElement;
program_memory: HTMLElement;
program_memory_cells: Array<HTMLElement> = [];
registers: HTMLElement;
printout: HTMLElement;
instruction_explainer: HTMLElement;
register_cells: Array<HTMLElement> = [];
instruction_parsing_addresses: Array<u8> = [];
program_counter: u8 = 0;
auto_running: boolean;
events: UiEventHandler = new EventHandler<UiEvent>() as UiEventHandler;
constructor(parent: HTMLElement) {
frequencyIndicator: frequencyIndicator;
memory: MemoryView;
instruction_explainer: InstructionExplainer;
constructor() {
for (const [, e_type] of Object.entries(UiEvent)) {
this.events.register_event(e_type as UiEvent);
}
this.events.seal();
this.container = parent;
this.memory = new MemoryView($("memory"));
this.frequencyIndicator = new frequencyIndicator($("cycles"));
this.instruction_explainer = new InstructionExplainer($("instruction_explainer"));
this.printout = $("printout");
this.instruction_explainer = $("instruction_explainer");
const program_mem = $("memory");
for (let i = 0; i < 256; i++) {
const mem_cell = el("div", `p_${i}`);
mem_cell.textContent = "00";
program_mem.appendChild(mem_cell);
this.program_memory_cells.push(mem_cell);
}
this.program_memory_cells[0].classList.add("div", "program_counter");
this.program_memory = program_mem;
const registers = $("registers");
for (let i = 0; i < 8; i++) {
@ -66,21 +65,24 @@ export class UI {
this.stop_auto();
}
this.events.dispatch(UiEvent.RequestCpuCycle, null);
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
});
$("delay_range").addEventListener("input", (e) => {
delay = parseInt((e.target as HTMLInputElement).value, 10);
// console.log(delay);
});
}
init_events(cpu_events: CpuEventHandler): void {
cpu_events.listen(CpuEvent.MemoryChanged, ({ address, value }) => {
this.program_memory_cells[address].textContent = format_hex(value);
this.memory.set_cell_value(address, value);
});
cpu_events.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => {
this.register_cells[register_no].textContent = format_hex(value);
});
cpu_events.listen(CpuEvent.ProgramCounterChanged, ({ counter }) => {
this.program_memory_cells[this.program_counter].classList.remove("program_counter");
this.program_memory_cells[counter].classList.add("program_counter");
this.program_counter = counter;
this.memory.set_program_counter(counter);
});
cpu_events.listen(CpuEvent.Print, (char) => {
this.printout.textContent = (this.printout.textContent ?? "") + char;
@ -89,78 +91,51 @@ export class UI {
this.reset();
});
const map: Map<MemoryCellType, string> = new Map();
this.frequencyIndicator.init_cpu_events(cpu_events);
this.memory.init_cpu_events(cpu_events);
this.instruction_explainer.init_cpu_events(cpu_events);
map.set(MemoryCellType.Constant, "constant");
map.set(MemoryCellType.Instruction, "instruction");
map.set(MemoryCellType.InvalidInstruction, "invalid_instruction");
map.set(MemoryCellType.Memory, "memory");
map.set(MemoryCellType.Register, "register");
cpu_events.listen(CpuEvent.MemoryByteParsed, (e) => {
const { type, pos, code } = e;
const css_class = map.get(type);
if (css_class === undefined) {
throw new Error("Something went wrong");
cpu_events.listen(CpuEvent.ParameterParsed, ({ param, code, pos }) => {
this.memory.add_cell_class(pos, "instruction_argument");
this.instruction_explainer.add_param(param, pos, code);
const t = param.type;
this.memory.remove_cell_class(pos, "constant", "register", "memory", "instruction", "invalid");
let name: string = "";
if (t === ParamType.Const) {
name = "constant";
} else if (t === ParamType.Memory) {
name = "memory";
} else if (t === ParamType.Register) {
name = "register";
} else {
throw new Error("unreachable");
}
for (const other_class of map.values()) {
if (other_class === css_class) continue;
this.program_memory_cells[pos].classList.remove(other_class);
}
if (type === MemoryCellType.Instruction) {
while (this.instruction_parsing_addresses.length > 0) {
const num = this.instruction_parsing_addresses.pop();
if (num === undefined) {
throw new Error("Shouldn't happen");
}
this.program_memory_cells[num].classList.remove("instruction_argument");
this.program_memory_cells[num].classList.remove("current_instruction");
}
this.instruction_explainer.innerHTML = "";
const { instr } = e as { instr: Instruction };
this.program_memory_cells[pos].classList.add("current_instruction");
this.instruction_parsing_addresses.push(pos);
const instr_box = el("div", "expl_box");
const instr_icon = el("span", "expl_icon");
instr_icon.classList.add(css_class);
instr_icon.setAttribute("title", css_class.toUpperCase());
instr_icon.textContent = format_hex(code);
const instr_box_text = el("span", "expl_text");
instr_box_text.textContent = `${instr.name}`;
instr_box.appendChild(instr_icon);
instr_box.appendChild(instr_box_text);
this.instruction_explainer.appendChild(instr_box);
} else if (type !== MemoryCellType.InvalidInstruction) {
const { param } = e as { param: ParameterType };
this.program_memory_cells[pos].classList.add("instruction_argument");
this.instruction_parsing_addresses.push(pos);
const instr_box = el("div", "expl_box");
const instr_icon = el("span", "expl_icon");
instr_icon.classList.add(css_class);
instr_icon.setAttribute("title", css_class.toUpperCase());
instr_icon.textContent = format_hex(code);
const instr_box_text = el("span", "expl_text");
instr_box_text.textContent = `${param.desc}`;
instr_box.appendChild(instr_icon);
instr_box.appendChild(instr_box_text);
this.instruction_explainer.appendChild(instr_box);
}
this.program_memory_cells[pos].classList.add(css_class);
this.memory.add_cell_class(pos, name);
});
cpu_events.listen(CpuEvent.InstructionParsed, ({ instr, code, pos }) => {
this.instruction_explainer.add_instruction(instr, pos, code);
cpu_events.listen(CpuEvent.InstructionExecuted, ({ instr }) => {});
this.memory.remove_all_cell_class("instruction_argument");
this.memory.remove_all_cell_class("current_instruction");
this.memory.add_cell_class(pos, "current_instruction");
this.memory.remove_cell_class(pos, "constant", "register", "memory", "invalid");
this.memory.add_cell_class(pos, "instruction");
});
cpu_events.listen(CpuEvent.InvalidParsed, ({ code, pos }) => {
this.memory.remove_cell_class(pos, "constant", "register", "memory", "instruction");
this.memory.add_cell_class(pos, "invalid");
this.instruction_explainer.add_invalid(pos, code);
});
}
reset(): void {
this.stop_auto();
this.program_memory_cells.forEach((c) => {
c.className = "";
c.textContent = "00";
});
this.register_cells.forEach((r) => {
r.textContent = "00";
});
this.program_counter = 0;
this.program_memory_cells[0].classList.add("program_counter");
this.frequencyIndicator.reset();
this.instruction_explainer.reset();
this.memory.reset();
this.printout.textContent = "";
}
@ -173,9 +148,8 @@ export class UI {
if (this.auto_running === false) {
return;
}
this.events.dispatch(UiEvent.RequestCpuCycle, null);
setTimeout(loop, speed);
// requestAnimationFrame(loop);
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
setTimeout(loop, delay);
};
loop();
}
@ -183,15 +157,4 @@ export class UI {
stop_auto(): void {
this.auto_running = false;
}
state_update_event(state: ComputerState): void {
const current_instr = state.current_instruction;
if (current_instr !== null) {
this.program_memory_cells[current_instr.pos].classList.add("current_instruction");
for (let i = 0; i < current_instr.params_found; i++) {
const offset = i + 1 + current_instr.pos;
this.program_memory_cells[offset].classList.add("instruction_argument");
}
}
}
}

122
src/ui/editableHex.ts Normal file
View file

@ -0,0 +1,122 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function set_caret(el: any, pos: number): boolean {
const selection = window.getSelection();
const range = document.createRange();
if (selection === null) {
return false;
}
selection.removeAllRanges();
range.selectNode(el);
range.setStart(el, pos);
range.setEnd(el, pos);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
el.focus();
return true;
}
function get_caret(el: HTMLElement): null | number {
const sel = window.getSelection();
if (sel === null) {
return null;
}
const pos = sel.getRangeAt(0).startOffset;
const endPos = pos + Array.from(el.innerHTML.slice(0, pos)).length - el.innerHTML.slice(0, pos).split("").length;
return endPos;
}
const hex_characters = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
function replace_non_hex(c: string): string {
if (hex_characters.includes(c)) {
return c;
}
return "0";
}
function editable_constraints(e: Event): boolean {
const target = e.target as HTMLDivElement;
const text = target.innerHTML ?? "";
if (text.length !== 2) {
const pos = get_caret(target);
const new_str = [...(target.textContent ?? "").substring(0, 2).padStart(2, "0").toUpperCase()]
.map(replace_non_hex)
.join("");
target.innerHTML = "";
// For the caret selection to work right, each character must be its own node, complicating this greatly
target.append(new_str.substring(0, 1), new_str.substring(1));
if (pos !== null) {
if (pos >= 2) {
return true;
}
set_caret(target, pos);
}
}
return false;
}
function at<T>(l: Array<T>, i: number): T | null {
if (i < 0) {
return null;
}
if (i >= l.length) {
return null;
}
return l[i];
}
export function make_editable(
list: Array<HTMLElement>,
width: number,
height: number,
on_edit: (n: number, value: string) => void
): void {
for (const [i, cell] of list.entries()) {
cell.setAttribute("contenteditable", "true");
cell.setAttribute("spellcheck", "false");
const next: null | HTMLElement = at(list, i + 1);
const prev: null | HTMLElement = at(list, i - 1);
const up: null | HTMLElement = at(list, i - width);
const down: null | HTMLElement = at(list, i + width);
cell.addEventListener("keydown", (e) => {
const caret_position = get_caret(cell);
const k = e.key;
if (k === "ArrowUp") {
(up ?? prev)?.focus();
cell.blur();
} else if (k === "ArrowDown") {
(down ?? next)?.focus();
cell.blur();
} else if ((k === "ArrowLeft" || k === "Backspace") && caret_position === 0) {
prev?.focus();
cell.blur();
} else if (k === "ArrowRight" && caret_position === 1) {
next?.focus();
cell.blur();
} else if (k === "Enter") {
cell.blur();
} else if (k === "Escape") {
cell.blur();
return;
} else {
return;
}
e.preventDefault();
});
let previous_text = cell.textContent ?? "";
cell.addEventListener("input", (e) => {
const current_text = cell.textContent ?? "";
if (current_text !== previous_text) {
previous_text = cell.textContent ?? "";
on_edit(i, current_text);
}
if (editable_constraints(e) === true) {
next?.focus();
cell.blur();
}
});
}
}

View file

@ -0,0 +1,52 @@
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../events";
import { UiComponent } from "./uiComponent";
export class frequencyIndicator implements UiComponent {
element: HTMLElement;
private running: number | null = null;
private count: number = 0;
private last_value: number = 0;
private last_time: number = 0;
constructor(element: HTMLElement) {
this.element = element;
this.start();
}
start(): void {
if (this.running !== null) {
throw new Error("Tried starting frequencyIndicator twice!");
}
setInterval(this.update_indicator.bind(this), 1000);
}
stop(): void {
if (this.running === null) return;
clearInterval(this.running);
this.running = null;
}
update_indicator(): void {
if (this.last_value !== this.count) {
this.element.textContent = `${this.count}hz`;
this.last_value = this.count;
}
this.count = 0;
}
init_events(eh: UiEventHandler): void {
this;
}
clock_cycle(): void {
this.count += 1;
}
reset(): void {
this.stop();
this.count = 0;
this.last_value = 0;
}
init_cpu_events(c: CpuEventHandler) {
c.listen(CpuEvent.ClockCycle, () => {
this.count += 1;
});
}
}

View file

@ -0,0 +1,55 @@
import { el, format_hex, u8 } from "../etc";
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../events";
import { Instruction, ParamType, ParameterType } from "../instructionSet";
import { UiComponent } from "./uiComponent";
export class InstructionExplainer implements UiComponent {
element: HTMLElement;
constructor(element: HTMLElement) {
this.element = element;
}
add_instruction(instr: Instruction, pos: u8, byte: u8): void {
this.reset();
this.add_box(format_hex(byte), instr.name, "instruction");
}
private add_box(box_icon_text: string, name: string, css_class: string): void {
const instr_box = el("div", "expl_box");
const instr_icon = el("span", "expl_icon");
instr_icon.classList.add(css_class);
instr_icon.setAttribute("title", css_class.toUpperCase());
instr_icon.textContent = box_icon_text;
const instr_box_text = el("span", "expl_text");
instr_box_text.textContent = name;
instr_box.appendChild(instr_icon);
instr_box.appendChild(instr_box_text);
this.element.appendChild(instr_box);
}
add_param(param: ParameterType, pos: u8, byte: u8): void {
const t = param.type;
let name;
if (t === ParamType.Const) {
name = "constant";
} else if (t === ParamType.Memory) {
name = "memory";
} else if (t === ParamType.Register) {
name = "register";
} else {
throw new Error("unreachable");
}
this.add_box(format_hex(byte), param.desc, name);
}
add_invalid(pos: u8, byte: u8) {
this.reset();
this.add_box(format_hex(byte), "Invalid Instruction", "invalid");
}
init_events(eh: UiEventHandler): void {}
init_cpu_events(c: CpuEventHandler) {}
reset(): void {
this.element.innerHTML = "";
}
}

78
src/ui/memoryView.ts Normal file
View file

@ -0,0 +1,78 @@
import { el, format_hex, u8 } from "../etc";
import { CpuEventHandler, UiEventHandler } from "../events";
import { UiComponent } from "./uiComponent";
export enum CellTag {
ProgramCounter = "program_counter",
}
type MemoryCell = {
el: HTMLElement;
};
export class MemoryView implements UiComponent {
element: HTMLElement;
cells: Array<MemoryCell> = [];
program_counter: number = 0;
constructor(element: HTMLElement) {
this.element = element;
for (let i = 0; i < 256; i++) {
const mem_cell_el = el("div");
mem_cell_el.textContent = "00";
element.appendChild(mem_cell_el);
const mem_cell = { el: mem_cell_el, tags: [] };
this.cells.push(mem_cell);
}
this.set_program_counter(0);
}
add_cell_class(address: u8, ...css_class: string[]): void {
for (const str of css_class) {
this.cells[address].el.classList.add(str);
}
}
remove_cell_class(address: u8, ...css_class: string[]): void {
for (const str of css_class) {
this.cells[address].el.classList.remove(str);
}
}
remove_all_cell_class(css_class: string): void {
for (const cell of this.cells) {
cell.el.classList.remove(css_class);
}
}
add_cell_class_exclusive(address: u8, css_class: string): void {
this.remove_all_cell_class(css_class);
this.add_cell_class(address, css_class);
}
set_cell_value(address: u8, value: u8): void {
this.cells[address].el.textContent = format_hex(value);
}
set_program_counter(position: number): void {
this.cells[this.program_counter].el.classList.remove("program_counter");
this.cells[position].el.classList.add("program_counter");
this.program_counter = position;
}
reset(): void {
this.element.innerHTML = "";
for (let i = 0; i < 256; i++) {
const mem_cell_el = el("div");
mem_cell_el.textContent = "00";
this.element.appendChild(mem_cell_el);
const mem_cell = { el: mem_cell_el };
this.cells.push(mem_cell);
}
this.set_program_counter(0);
}
init_events(eh: UiEventHandler): void {
this;
}
init_cpu_events(c: CpuEventHandler) {}
}

9
src/ui/uiComponent.ts Normal file
View file

@ -0,0 +1,9 @@
import { u8 } from "../etc";
import { CpuEventHandler, UiEventHandler } from "../events";
export interface UiComponent {
element: HTMLElement;
reset: () => void;
init_events: (ui: UiEventHandler) => void;
init_cpu_events: (c: CpuEventHandler) => void;
}

136
style.css
View file

@ -1,136 +0,0 @@
* {
box-sizing: border-box;
}
#memory {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-gap: 5px;
padding: 10px;
border: 5px solid yellow;
/* color: lightgray; */
}
pre {
font-size: 0.5em;
}
#memory .instruction {
color: greenyellow;
}
#memory .constant {
color: purple;
}
#memory .register {
color: orange;
}
#memory .memory {
color: pink;
}
#memory .invalid_instruction {
color: maroon;
}
#memory div {
aspect-ratio: 1;
text-align: center;
margin: auto;
}
body {
color: gray;
background-color: black;
font-size: 2.5em;
font-family: monospace;
}
#registers div {
max-height: min-content;
margin-block: auto;
}
#subcontainer {
display: flex;
flex-direction: row;
column-gap: 15px;
}
#labelcontainer {
column-gap: 18px;
font-size: 0.85em;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
#main {
display: flex;
flex-direction: row;
}
#title {
writing-mode: vertical-lr;
text-align: left;
user-select: none;
transform: scale(-1, -1);
}
#printout {
border: 4px dashed yellow;
width: 1000px;
padding: 10px;
margin-left: 20px;
word-wrap: break-word;
word-break: break-all;
}
#memory div.program_counter {
outline: 3px solid orange;
}
#memory div.instruction_argument {
outline: 3px dashed purple;
}
#memory div.current_instruction {
outline: 3px dashed greenyellow;
}
#registers {
border: 5px solid yellow;
border-bottom: none;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
max-width: fit-content;
display: grid;
column-gap: 5px;
color: lightgray;
padding: 10px;
}
#container {
font-family: monospace;
max-width: min-content;
max-height: min-content;
}
button,
label.button {
border: 4px solid yellow;
color: gray;
margin: 10px;
margin-inline: 8px;
padding: 10px;
font-size: 0.8em;
font-family: monospace;
background-color: transparent;
cursor: pointer;
user-select: none;
}
button:hover,
label.button:hover {
color: white;
}
#controls_bar {
margin-left: 1.12em;
display: flex;
}

View file

@ -11,12 +11,23 @@ const config = {
},
{
test: /\.html/,
type: "asset/resource"
}
type: "asset/resource",
},
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
],
},
resolve: {
extensions: [".ts"],
extensions: [".ts", ".scss", ".css"],
},
output: {
filename: "main.js",
@ -32,4 +43,4 @@ module.exports = (env, argv) => {
}
return config;
};
};