├── src ├── backend │ ├── BuildSystem │ │ ├── BuildPrograms │ │ │ ├── All.ts │ │ │ ├── FileOperations.ts │ │ │ ├── Linker.ts │ │ │ └── Assembler.ts │ │ ├── Program.ts │ │ └── ProjectBuilder.ts │ ├── Emulator │ │ ├── Devices │ │ │ ├── All.ts │ │ │ ├── ConsoleLog.ts │ │ │ ├── RAM32.ts │ │ │ └── ROM32.ts │ │ ├── Masters │ │ │ ├── All.ts │ │ │ ├── ZeroToZero.ts │ │ │ ├── SimpleCPU.ts │ │ │ └── RISCV32.ts │ │ ├── MachineSerializable.ts │ │ ├── Machine.ts │ │ └── Bus.ts │ ├── Assembler │ │ ├── AssemblerProgram.ts │ │ ├── Tokenizer.ts │ │ ├── Instructions.ts │ │ └── Compiler.ts │ ├── Logger.ts │ ├── ProjectManager.ts │ └── FileSystem.ts ├── components │ ├── editors │ │ ├── EditorProjectSettings.tsx │ │ ├── EditorRegistry.ts │ │ ├── EditorManager.tsx │ │ ├── EditorContainer.tsx │ │ └── EditorMonaco.tsx │ ├── ProjectSelect.module.css │ ├── Tabs.module.css │ ├── sidebars │ │ ├── FileBrowser.module.css │ │ ├── FileBrowserSideBar.tsx │ │ ├── SideBar.tsx │ │ └── FileBrowser.tsx │ ├── Tabs.tsx │ ├── Tab.module.css │ ├── Tab.tsx │ ├── simulator │ │ ├── TabbedSideBar.module.css │ │ ├── SimulatorSideBar.module.css │ │ ├── SimulatorControls.tsx │ │ ├── TabbedSideBar.tsx │ │ ├── MachineContext.tsx │ │ └── SimulatorSideBar.tsx │ ├── FileSystemContext.tsx │ ├── TabsContext.tsx │ ├── ProjectContext.tsx │ └── ProjectSelect.tsx ├── workers │ ├── testWorker.ts │ └── machineWorker.ts ├── colors.css ├── assets │ ├── child-arrow-line.svg │ ├── storage-line.svg │ ├── angle-double-line.svg │ ├── dashboard.svg │ ├── project.svg │ ├── text-line.svg │ └── info.svg ├── style.css └── index.tsx ├── .idea ├── .gitignore ├── watcherTasks.xml ├── vcs.xml ├── modules.xml └── riscv-edu-ide.iml ├── tests ├── test.s ├── simulator.test.ts └── assembler.test.ts ├── .gitignore ├── index.html ├── vite.config.ts ├── .github └── workflows │ ├── ci.yml │ └── static.yml ├── package.json ├── README.md ├── tsconfig.json └── public └── vite.svg /src/backend/BuildSystem/BuildPrograms/All.ts: -------------------------------------------------------------------------------- 1 | import "./FileOperations"; 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /src/backend/Emulator/Devices/All.ts: -------------------------------------------------------------------------------- 1 | // TODO: import all the devices 2 | 3 | import "./ConsoleLog"; 4 | import "./RAM32"; 5 | import "./ROM32"; -------------------------------------------------------------------------------- /src/backend/Emulator/Masters/All.ts: -------------------------------------------------------------------------------- 1 | // TODO: import all the masters 2 | 3 | import "./ZeroToZero"; 4 | // import "./SimpleCPU"; 5 | import "./RISCV32"; -------------------------------------------------------------------------------- /src/components/editors/EditorProjectSettings.tsx: -------------------------------------------------------------------------------- 1 | export function EditorProjectSettings() { 2 | return ( 3 |
Project settings
4 | ); 5 | } -------------------------------------------------------------------------------- /src/workers/testWorker.ts: -------------------------------------------------------------------------------- 1 | self.onmessage = (e: MessageEvent) => { 2 | setTimeout(() => postMessage(e.data * e.data), 5000); 3 | }; 4 | 5 | export {}; -------------------------------------------------------------------------------- /tests/test.s: -------------------------------------------------------------------------------- 1 | .offset 0x0 2 | 3 | # Выводит число 8 в регистр 1 4 | test: 5 | addi x1, x0, 10 6 | # beq x1, x0, 4 7 | addi x1, x1, -2 8 | loop: jal x0, 0 -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/ProjectSelect.module.css: -------------------------------------------------------------------------------- 1 | div.projectSel { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | align-items: center; 5 | gap: 4px; 6 | padding: 4px; 7 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Tabs.module.css: -------------------------------------------------------------------------------- 1 | div.tabs { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | align-items: stretch; 5 | overflow-x: auto; 6 | 7 | flex-grow: 1; 8 | gap: 4px; 9 | } -------------------------------------------------------------------------------- /src/components/sidebars/FileBrowser.module.css: -------------------------------------------------------------------------------- 1 | .entry { 2 | display: inline-block; 3 | cursor: pointer; 4 | width: 100%; 5 | 6 | &:hover { 7 | background-color: rgba(0, 0, 0, 0.1); 8 | } 9 | } -------------------------------------------------------------------------------- /src/components/editors/EditorRegistry.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "preact"; 2 | import { EditorMonaco } from "./EditorMonaco"; 3 | 4 | export const Editors = new Map>([ 5 | ["code", EditorMonaco] 6 | ]); -------------------------------------------------------------------------------- /src/backend/Emulator/MachineSerializable.ts: -------------------------------------------------------------------------------- 1 | export interface IMachineSerializable { 2 | serialize(): { name: string, uuid: string, context: any }; 3 | } 4 | 5 | export interface IMachineVisualizable { 6 | // svelteComponent: any | null; 7 | getState(): any; 8 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | coverage/ -------------------------------------------------------------------------------- /src/backend/BuildSystem/BuildPrograms/FileOperations.ts: -------------------------------------------------------------------------------- 1 | import {BuildPrograms, ProgramStatus} from "$lib/backend/BuildSystem/Program"; 2 | 3 | function runCp(...args: any[]): ProgramStatus { 4 | return ProgramStatus.Failure; 5 | } 6 | 7 | function runRm(...args: any[]): ProgramStatus { 8 | return ProgramStatus.Failure; 9 | } 10 | 11 | BuildPrograms["cp"] = { 12 | program: runCp 13 | }; 14 | 15 | BuildPrograms["rm"] = { 16 | program: runRm 17 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + Preact 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/backend/BuildSystem/Program.ts: -------------------------------------------------------------------------------- 1 | import { AssemblerProgram } from "../Assembler/AssemblerProgram"; 2 | import { Project } from "../ProjectManager"; 3 | 4 | export enum ProgramStatus { 5 | Success = 0, 6 | Failure 7 | } 8 | 9 | export type Program = (project: Project, ...args: any[]) => ProgramStatus; 10 | 11 | export const BuildPrograms = new Map([ 15 | ["asm", { program: AssemblerProgram }] 16 | ]); -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import preact from '@preact/preset-vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [preact()], 7 | base: '', 8 | resolve: { 9 | alias: { 10 | src: "/src", 11 | } 12 | }, 13 | // For SharedArrayBuffer: 14 | server: { 15 | headers: { 16 | "Cross-Origin-Embedder-Policy": "require-corp", 17 | "Cross-Origin-Opener-Policy": "same-origin", 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js environment 17 | uses: actions/setup-node@v4.0.1 18 | with: 19 | cache: "npm" 20 | 21 | - name: Install dependencies 22 | run: npm install --dev 23 | 24 | - name: Run tests 25 | run: npm test 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "test": "vitest --coverage" 9 | }, 10 | "dependencies": { 11 | "@preact/signals": "^2.0.4", 12 | "jszip": "^3.10.1", 13 | "monaco-editor": "^0.52.2", 14 | "preact": "^10.25.3", 15 | "uuid": "^11.1.0" 16 | }, 17 | "devDependencies": { 18 | "@preact/preset-vite": "^2.9.3", 19 | "@vitest/coverage-v8": "^3.2.3", 20 | "typescript": "^5.8.3", 21 | "vite": "^6.0.4", 22 | "vitest": "^3.2.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RISC-V Edu IDE 2 | 3 | This is my coursework for the Kuban State University. 4 | 5 | ## Developing 6 | 7 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a 8 | development server: 9 | 10 | ```bash 11 | npm run dev 12 | 13 | # or start the server and open the app in a new browser tab 14 | npm run dev -- --open 15 | ``` 16 | 17 | ## Building 18 | 19 | To create a production version of your app: 20 | 21 | ```bash 22 | npm run build 23 | ``` 24 | 25 | You can preview the production build with `npm run preview`. 26 | -------------------------------------------------------------------------------- /src/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-light-primary: #C2F18D; 3 | --color-light-primary-text: #000000; 4 | 5 | --color-light-secondary: #E4F0D7; 6 | --color-light-secondary-text: #000000; 7 | 8 | --color-light-tertiary: #FFFFFF; 9 | 10 | --color-light-highlight: #448000; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | /* TODO: dark theme */ 15 | :root { 16 | color: #ccc; 17 | background-color: #1a1a1a; 18 | } 19 | .resource { 20 | color: #ccc; 21 | background-color: #161616; 22 | } 23 | .resource:hover { 24 | border: 1px solid #bbb; 25 | } 26 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2024", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "noEmit": true, 7 | "allowJs": true, 8 | "checkJs": true, 9 | /* Preact Config */ 10 | "jsx": "react-jsx", 11 | "jsxImportSource": "preact", 12 | "skipLibCheck": true, 13 | "paths": { 14 | "react": [ 15 | "./node_modules/preact/compat/" 16 | ], 17 | "react-dom": [ 18 | "./node_modules/preact/compat/" 19 | ], 20 | "src/*": [ 21 | "./src/*" 22 | ] 23 | } 24 | }, 25 | "include": [ 26 | "node_modules/vite/client.d.ts", 27 | "**/*" 28 | ] 29 | } -------------------------------------------------------------------------------- /.idea/riscv-edu-ide.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { Tab } from "./Tab"; 2 | import style from "./Tabs.module.css"; 3 | import { closeTab, useTabs } from "./TabsContext"; 4 | 5 | export function Tabs() { 6 | const tabManager = useTabs(); 7 | 8 | return ( 9 |
10 | {tabManager.tabs.map((tab, index) => tabManager.setTabIndex(index)} 13 | onClose={() => closeTab(tabManager, tab.uri)} 14 | title={tab.uri} 15 | active={index === tabManager.currentTabIndex} />)} 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /src/components/sidebars/FileBrowserSideBar.tsx: -------------------------------------------------------------------------------- 1 | import { useProject } from "src/components/ProjectContext"; 2 | import { FileBrowser } from "src/components/sidebars/FileBrowser"; 3 | import { useFileSystem } from "src/components/FileSystemContext"; 4 | import { SideBar } from "./SideBar"; 5 | import { useEffect, useState } from "preact/hooks"; 6 | 7 | export function FileBrowserSideBar() { 8 | const projectContext = useProject(); 9 | 10 | return ( 11 | 12 | {projectContext.projectFolder && } 13 | 14 | ); 15 | } -------------------------------------------------------------------------------- /src/components/Tab.module.css: -------------------------------------------------------------------------------- 1 | div.tab { 2 | display: flex; 3 | flex-flow: row nowrap; 4 | gap: 4px; 5 | padding: 4px; 6 | align-items: center; 7 | width: 150px; 8 | 9 | >span { 10 | flex-grow: 1; 11 | text-overflow: ellipsis; 12 | user-select: none; 13 | } 14 | 15 | >button { 16 | aspect-ratio: 1; 17 | } 18 | 19 | &:hover { 20 | background-color: rgba(0, 0, 0, 0.05); 21 | } 22 | 23 | &.active { 24 | background-color: rgba(0, 0, 0, 0.1); 25 | 26 | box-shadow: inset 0px -3px 0 0px var(--color-light-highlight); 27 | 28 | &:hover { 29 | background-color: rgba(0, 0, 0, 0.15); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'preact'; 2 | import style from './Tab.module.css'; 3 | 4 | export interface ITab { 5 | uri: string; 6 | editor: FunctionComponent; 7 | context: any; 8 | } 9 | 10 | export function Tab(props: { active?: boolean, onClick: () => void, onClose: () => void, title: string }) { 11 | const active = props.active ?? false; 12 | return ( 13 |
14 | {props.title} 15 | 19 |
20 | ); 21 | } -------------------------------------------------------------------------------- /src/components/editors/EditorManager.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createContext } from "preact"; 2 | import { MutableRef, useContext, useRef } from "preact/hooks"; 3 | 4 | interface IEditorManager { 5 | saveFile: MutableRef<() => void>; 6 | } 7 | 8 | const EditorManagerContext = createContext({ 9 | saveFile: null, 10 | }); 11 | 12 | export function EditorManager({ children }: { children: ComponentChildren }) { 13 | const save = useRef<() => void>(null); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | export function useEditorManager() { 23 | return useContext(EditorManagerContext); 24 | } -------------------------------------------------------------------------------- /tests/simulator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Machine } from 'src/backend/Emulator/Machine'; 3 | import { ZeroToZero } from 'src/backend/Emulator/Masters/ZeroToZero'; 4 | import { ConsoleLog } from 'src/backend/Emulator/Devices/ConsoleLog'; 5 | 6 | describe('simulator', () => { 7 | let machine: Machine; 8 | it('runs with default setup', () => { 9 | expect(() => { 10 | machine = Machine.FromMasterDevices(ZeroToZero.fromContext(undefined), [ConsoleLog.fromContext(undefined)]); 11 | }).not.toThrowError(); 12 | }); 13 | it('can tick', () => { 14 | expect(() => { 15 | for (const i of Array(10)) { 16 | machine.doTick(); 17 | } 18 | }).not.toThrowError(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/backend/Assembler/AssemblerProgram.ts: -------------------------------------------------------------------------------- 1 | import { Program, ProgramStatus } from "../BuildSystem/Program"; 2 | import { ROM32 } from "../Emulator/Devices/ROM32"; 3 | import { assemble } from "./Compiler"; 4 | import { tokenize } from "./Tokenizer"; 5 | 6 | export const AssemblerProgram: Program = (project, ...args: any[]): ProgramStatus => { 7 | let program: Uint32Array = new Uint32Array(); 8 | try { 9 | program = new Uint32Array(assemble(tokenize(args[0])).buffer); 10 | } catch (ex) { 11 | console.error(ex); 12 | } 13 | 14 | for (const device of project.machine.masterBus.devices) { 15 | if (device.info.name === "rom32") { 16 | const rom = device as ROM32; 17 | for (let i = 0; i < program.length; i++) { 18 | rom.setDword(i * 4, program[i]); 19 | } 20 | break; 21 | } 22 | } 23 | 24 | return ProgramStatus.Success; 25 | }; -------------------------------------------------------------------------------- /src/components/simulator/TabbedSideBar.module.css: -------------------------------------------------------------------------------- 1 | div.t_header { 2 | display: flex; 3 | align-items: stretch; 4 | gap: 4px; 5 | } 6 | 7 | div.tabs { 8 | flex-grow: 1; 9 | display: flex; 10 | align-items: stretch; 11 | gap: 4px; 12 | 13 | >.tab { 14 | display: inline-block; 15 | padding: 4px; 16 | text-transform: uppercase; 17 | cursor: pointer; 18 | 19 | &:hover { 20 | box-shadow: inset 0px -3px 0 0px rgb(from var(--color-light-highlight) r g b / 0.5); 21 | } 22 | 23 | &.active { 24 | font-weight: bold; 25 | box-shadow: inset 0px -3px 0 0px var(--color-light-highlight); 26 | } 27 | } 28 | } 29 | 30 | div.limitedContent { 31 | position: relative; 32 | 33 | >* { 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | bottom: 0; 38 | left: 0; 39 | } 40 | } -------------------------------------------------------------------------------- /src/assets/child-arrow-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/FileSystemContext.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createContext } from 'preact'; 2 | import { useContext, useEffect, useState } from 'preact/hooks'; 3 | 4 | type FileSystemContextType = { 5 | rootDir: FileSystemDirectoryHandle | null; 6 | refresh: () => Promise; 7 | }; 8 | 9 | const FileSystemContext = createContext({ 10 | rootDir: null, 11 | refresh: async () => { }, 12 | }); 13 | 14 | export function FileSystemProvider({ children }: { children: ComponentChildren }) { 15 | const [rootDir, setRootDir] = useState(null); 16 | 17 | const init = async () => { 18 | const dir = await navigator.storage.getDirectory(); 19 | setRootDir(dir); 20 | }; 21 | 22 | useEffect(() => { 23 | init(); 24 | }, []); 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | } 32 | 33 | export function useFileSystem() { 34 | return useContext(FileSystemContext); 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/storage-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/simulator/SimulatorSideBar.module.css: -------------------------------------------------------------------------------- 1 | div.tabContent { 2 | padding: 4px; 3 | overflow-y: auto; 4 | display: flex; 5 | flex-flow: column nowrap; 6 | align-items: stretch; 7 | 8 | >h5:first-child { 9 | margin-top: 0; 10 | } 11 | 12 | .icon { 13 | width: 16px; 14 | height: 16px; 15 | aspect-ratio: 1; 16 | } 17 | 18 | .table3 { 19 | display: grid; 20 | grid-template-columns: min-content 1fr auto; 21 | grid-template-rows: 1fr; 22 | grid-column-gap: 2px; 23 | grid-row-gap: 2px; 24 | } 25 | 26 | .table4 { 27 | display: grid; 28 | grid-template-columns: repeat(4 1fr); 29 | grid-template-rows: 1fr; 30 | grid-column-gap: 2px; 31 | grid-row-gap: 2px; 32 | } 33 | 34 | span { 35 | font-size: 16px; 36 | 37 | &.value { 38 | text-align: right; 39 | opacity: 0.75; 40 | } 41 | 42 | &.hexValue { 43 | text-align: right; 44 | opacity: 0.75; 45 | font-family: monospace; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static content to Pages 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Node.js environment 28 | uses: actions/setup-node@v4.0.1 29 | with: 30 | cache: "npm" 31 | 32 | - run: npm install --include=dev 33 | 34 | - name: Build RISC-V Edu IDE 35 | env: 36 | BASE_PATH: "/${{ github.event.repository.name }}" 37 | run: npx vite build 38 | 39 | - name: Setup Pages 40 | uses: actions/configure-pages@v4 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | path: "dist/" 46 | 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v4 50 | -------------------------------------------------------------------------------- /src/assets/angle-double-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assembler.test.ts: -------------------------------------------------------------------------------- 1 | import { Token, tokenize } from 'src/backend/Assembler/Tokenizer'; 2 | import { assemble } from 'src/backend/Assembler/Compiler'; 3 | import { describe, it, expect } from 'vitest'; 4 | import { readFileSync } from 'node:fs'; 5 | 6 | describe('assembler', () => { 7 | const testS = readFileSync('./tests/test.s'); 8 | let tokens: Token[] = []; 9 | 10 | describe('tokenizer', () => { 11 | it('can tokenize an example file', () => { 12 | expect(() => { 13 | tokens = tokenize(testS); 14 | console.log(JSON.stringify(tokens)); 15 | return tokens; 16 | }).not.toThrowError(); 17 | }); 18 | }); 19 | 20 | describe('compiler', () => { 21 | it('can compile the parsed file', () => { 22 | expect(() => { 23 | const result = assemble(tokens); 24 | console.log(result); 25 | }).not.toThrowError(); 26 | }); 27 | }); 28 | 29 | // it('can compile an empty file', () => { 30 | // expect(true).toBe(true); 31 | // }); 32 | // it('can compile a simple loop', () => { 33 | // expect(true).toBe(true); 34 | // }); 35 | it('throws an error for a malformed file', () => { 36 | expect(() => { throw "penis"; }).toThrowError(); 37 | }); 38 | }); -------------------------------------------------------------------------------- /src/components/editors/EditorContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'preact/hooks'; 2 | 3 | import { useEditorManager } from './EditorManager'; 4 | import { createElement } from 'preact'; 5 | import { getCurrentTab, useTabs } from '../TabsContext'; 6 | 7 | export function EditorContainer() { 8 | const editorManager = useEditorManager(); 9 | 10 | const tabManager = useTabs(); 11 | const tab = getCurrentTab(tabManager); 12 | 13 | useEffect(() => { 14 | function onKeyDown(e: KeyboardEvent) { 15 | if (e.ctrlKey && e.code === 'KeyS') { 16 | e.preventDefault(); 17 | 18 | if (editorManager.saveFile.current !== undefined) 19 | editorManager.saveFile.current(); 20 | } 21 | } 22 | 23 | window.addEventListener('keydown', onKeyDown); 24 | 25 | return () => window.removeEventListener('keydown', onKeyDown); 26 | }, [editorManager]); 27 | 28 | return ( 29 |
30 | { 31 | tab !== null 32 | ? createElement(tab.editor, { context: tab.context, uri: tab.uri, key: tab.uri }) 33 | :
Откройте любой файл, чтобы начать работу.
34 | } 35 |
36 | ); 37 | } -------------------------------------------------------------------------------- /src/components/sidebars/SideBar.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import collapseIcon from "src/assets/angle-double-line.svg"; 4 | 5 | export function SideBar(props: { 6 | title: string, 7 | visibleByDefault?: boolean, 8 | isLeftSide: boolean, 9 | children: ComponentChildren 10 | }) { 11 | const [visible, setVisible] = useState(props.visibleByDefault ?? true); 12 | const classList = ["sidebar"]; 13 | if (visible) { 14 | classList.push("visible"); 15 | } 16 | 17 | return ( 18 |
19 |
20 | {visible && {props.title}} 21 | 29 |
30 |
31 | {visible && (<>{props.children})} 32 |
33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /src/backend/Emulator/Masters/ZeroToZero.ts: -------------------------------------------------------------------------------- 1 | import { Master, MasterBusMasterRegistry } from "src/backend/Emulator/Bus"; 2 | import { v4 } from "uuid"; 3 | 4 | const ZERO2ZERO_NAME: string = "z2z"; 5 | 6 | export class ZeroToZero extends Master { 7 | // public svelteComponent: any | null = null; 8 | 9 | public static fromContext(context: any, uuid?: string): ZeroToZero { 10 | const z2z = new ZeroToZero(); 11 | z2z.uuid = uuid ?? v4(); 12 | return z2z; 13 | } 14 | 15 | public static fromBuffer(state: SharedArrayBuffer, uuid: string): ZeroToZero { 16 | const z2z = new ZeroToZero(); 17 | z2z.uuid = uuid; 18 | z2z.state = state; 19 | return z2z; 20 | } 21 | 22 | protected DeviceTick(tick: number): void { 23 | console.log("z2z: tick"); 24 | } 25 | 26 | protected MasterIO(ioTick: number): void { 27 | this.bus.Write(ioTick, 0, 0); 28 | } 29 | 30 | public get info(): { name: string; uuid: string; } { 31 | return { name: ZERO2ZERO_NAME, uuid: this.uuid }; 32 | } 33 | 34 | public serialize(): any { 35 | return { name: ZERO2ZERO_NAME, uuid: this.uuid, context: undefined }; 36 | } 37 | 38 | getState(): any { 39 | } 40 | } 41 | 42 | MasterBusMasterRegistry.set(ZERO2ZERO_NAME, { fromContext: (ctx, uuid) => ZeroToZero.fromContext(ctx, uuid), fromBuffer: ZeroToZero.fromBuffer }); -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color: #222; 7 | background-color: #ffffff; 8 | 9 | font-synthesis: none; 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-text-size-adjust: 100%; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | height: 100vh; 19 | } 20 | 21 | #app { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | main { 27 | width: 100%; 28 | height: 100%; 29 | 30 | display: grid; 31 | grid-template-columns: min-content 1fr min-content; 32 | grid-template-rows: min-content 1fr; 33 | } 34 | 35 | nav { 36 | grid-column: span 3 / span 3; 37 | 38 | display: flex; 39 | flex-flow: row nowrap; 40 | 41 | height: 34px; 42 | background-color: var(--color-light-primary); 43 | color: var(--color-light-primary-text); 44 | } 45 | 46 | div.sidebar { 47 | display: flex; 48 | flex-flow: column nowrap; 49 | 50 | background-color: var(--color-light-secondary); 51 | color: var(--color-light-secondary-text); 52 | 53 | div.header { 54 | display: flex; 55 | flex-flow: row nowrap; 56 | 57 | &>span { 58 | flex-grow: 1; 59 | } 60 | } 61 | 62 | div.content { 63 | flex-grow: 1; 64 | } 65 | 66 | &.visible { 67 | min-width: min(25vw, 300px); 68 | } 69 | } 70 | 71 | div.editor { 72 | position: relative; 73 | 74 | >* { 75 | position: absolute; 76 | left: 0; 77 | top: 0; 78 | right: 0; 79 | bottom: 0; 80 | } 81 | } -------------------------------------------------------------------------------- /src/assets/dashboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/project.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/backend/Emulator/Devices/ConsoleLog.ts: -------------------------------------------------------------------------------- 1 | import { Device, MasterBusDeviceRegistry } from "src/backend/Emulator/Bus"; 2 | import { v4 } from "uuid"; 3 | 4 | const CONSOLELOG_NAME: string = "consolelog"; 5 | 6 | export class ConsoleLog extends Device { 7 | // public svelteComponent: any | null = null; 8 | 9 | public static fromContext(context: any, uuid?: string): ConsoleLog { 10 | const cl = new ConsoleLog(); 11 | cl.uuid = uuid ?? v4(); 12 | return cl; 13 | } 14 | 15 | public static fromBuffer(state: SharedArrayBuffer, uuid: string): ConsoleLog { 16 | const cl = new ConsoleLog(); 17 | cl.uuid = uuid; 18 | cl.state = state; 19 | return cl; 20 | } 21 | 22 | protected DeviceRead(ioTick: number, address: number): number | null { 23 | console.log(`READ: ${address.toString(16)}`); 24 | return null; 25 | } 26 | 27 | protected DeviceTick(tick: number): void { 28 | console.log("TICK"); 29 | } 30 | 31 | protected DeviceWrite(ioTick: number, address: number, data: number): void { 32 | console.log(`WRITE: ${address.toString(16)} = ${data.toString(16)}`); 33 | } 34 | 35 | public get info(): { name: string; uuid: string; } { 36 | return { name: CONSOLELOG_NAME, uuid: this.uuid }; 37 | } 38 | 39 | public serialize(): any { 40 | } 41 | 42 | public getState(): any { 43 | return undefined; 44 | } 45 | } 46 | 47 | MasterBusDeviceRegistry.set(CONSOLELOG_NAME, { fromContext: ConsoleLog.fromContext, fromBuffer: ConsoleLog.fromBuffer }); -------------------------------------------------------------------------------- /src/workers/machineWorker.ts: -------------------------------------------------------------------------------- 1 | import { IMachineWorkerMessage, IMachineWorkerMessageLoad, Machine, MachineWorkerMessageTypes } from "src/backend/Emulator/Machine"; 2 | 3 | let machine: Machine; 4 | let runner: number | null = null; 5 | let isRunning = false; 6 | 7 | self.onmessage = (e: MessageEvent) => { 8 | console.log("To worker:", e.data); 9 | 10 | switch (e.data.type) { 11 | case MachineWorkerMessageTypes.Load: 12 | { 13 | const exchange = (e.data as IMachineWorkerMessageLoad).machine; 14 | machine = Machine.FromWorkerExchange(exchange); 15 | } 16 | break; 17 | 18 | case MachineWorkerMessageTypes.Tick: 19 | { 20 | machine.doTick(); 21 | } 22 | break; 23 | 24 | case MachineWorkerMessageTypes.Run: 25 | { 26 | if (isRunning) break; 27 | 28 | isRunning = true; 29 | 30 | function run() { 31 | machine.doTick(); 32 | 33 | if (!isRunning) { 34 | clearInterval(runner); 35 | runner = null; 36 | } 37 | } 38 | 39 | runner = setInterval(run, 0); 40 | } 41 | break; 42 | 43 | case MachineWorkerMessageTypes.Stop: 44 | { 45 | isRunning = false; 46 | } 47 | break; 48 | 49 | default: 50 | throw `Unsupported message type ${e.data.type}`; 51 | } 52 | }; 53 | 54 | export { }; -------------------------------------------------------------------------------- /src/backend/BuildSystem/BuildPrograms/Linker.ts: -------------------------------------------------------------------------------- 1 | import {BuildPrograms, ProgramStatus} from "$lib/backend/BuildSystem/Program"; 2 | import type {FSFile} from "$lib/backend/FileSystem"; 3 | import {LoggerManager, LogLevel} from "$lib/backend/Logger"; 4 | 5 | export interface IObjectBase { 6 | type: string 7 | } 8 | 9 | export interface IObjectBinary extends IObjectBase { 10 | type: "binary", 11 | binary: Uint8Array 12 | } 13 | 14 | export interface IObjectLabel extends IObjectBase { 15 | type: "label", 16 | name: string 17 | } 18 | 19 | export enum ObjectInstructionLinkType { 20 | Invalid = 0, 21 | AbsHigh20, 22 | AbsLow12, 23 | RelHi20, 24 | RelLow12 25 | } 26 | 27 | export interface IObjectInstruction extends IObjectBase { 28 | type: "instruction", 29 | instruction: number, 30 | link: string, 31 | linkType: ObjectInstructionLinkType 32 | } 33 | 34 | export interface IObjectSection { 35 | base: number, 36 | contents: IObjectBase[] 37 | } 38 | 39 | export interface IObject { 40 | sections: { 41 | [name: string]: IObjectSection 42 | }; 43 | } 44 | 45 | export enum LinkerExportType { 46 | Binary = 0 47 | } 48 | 49 | class Linker { 50 | constructor() { 51 | } 52 | 53 | public link(files: FSFile[], outputPath: string, exportAs: LinkerExportType) { 54 | for (const file of files) { 55 | LoggerManager.The.getLogger("linked").log(`${file.contents}`, LogLevel.Info); 56 | } 57 | } 58 | } 59 | 60 | BuildPrograms["ld"] = { 61 | program: (logger, ...args) => { 62 | const linker = new Linker(); 63 | return ProgramStatus.Success; 64 | } 65 | }; -------------------------------------------------------------------------------- /src/components/simulator/SimulatorControls.tsx: -------------------------------------------------------------------------------- 1 | import { IMachineWorkerMessageRun, IMachineWorkerMessageStop, IMachineWorkerMessageTick, MachineWorkerMessageTypes } from "src/backend/Emulator/Machine"; 2 | import { reloadMachine, useMachine } from "./MachineContext"; 3 | import { useState } from "preact/hooks"; 4 | import { useProject } from "../ProjectContext"; 5 | import { AssemblerProgram } from "src/backend/Assembler/AssemblerProgram"; 6 | 7 | export function SimulatorControls() { 8 | const machine = useMachine(); 9 | const project = useProject(); 10 | 11 | if (!project.project) return (
); 12 | 13 | const [isRunning, setRunning] = useState(false); 14 | 15 | return ( 16 |
17 | 28 | 29 | 37 | 40 |
41 | ); 42 | } -------------------------------------------------------------------------------- /src/components/simulator/TabbedSideBar.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createElement, FunctionComponent } from "preact"; 2 | import { useState } from "preact/hooks"; 3 | import collapseIcon from "src/assets/angle-double-line.svg"; 4 | import style from "./TabbedSideBar.module.css"; 5 | 6 | export function TabbedSideBar(props: { 7 | tabs: { title: string, component: FunctionComponent }[], 8 | visibleByDefault?: boolean, 9 | isLeftSide: boolean 10 | }) { 11 | const [visible, setVisible] = useState(props.visibleByDefault ?? true); 12 | const [tabIndex, setTabIndex] = useState(0); 13 | const classList = ["sidebar", style.special]; 14 | if (visible) { 15 | classList.push("visible"); 16 | } 17 | 18 | return ( 19 |
20 |
21 | 29 | {visible &&
30 | {props.tabs.map((tab, index) => 31 | setTabIndex(index)}> 34 | {tab.title} 35 | 36 | )} 37 |
} 38 |
39 |
40 | {visible && (<>{createElement(props.tabs[tabIndex].component, {})})} 41 |
42 |
43 | ); 44 | } -------------------------------------------------------------------------------- /src/components/simulator/MachineContext.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createContext } from 'preact'; 2 | import { useContext, useEffect, useRef, useState } from 'preact/hooks'; 3 | import { IMachineWorkerMessage, IMachineWorkerMessageLoad, MachineWorkerMessageTypes } from 'src/backend/Emulator/Machine'; 4 | 5 | import MachineWorker from 'src/workers/machineWorker?worker'; 6 | import { ProjectContextType, useProject } from '../ProjectContext'; 7 | 8 | interface IMachineManager { 9 | sendMessage: (message: IMachineWorkerMessage) => void; 10 | lastMessage: IMachineWorkerMessage | null; 11 | } 12 | 13 | const MachineManagerContext = createContext(null); 14 | 15 | export function reloadMachine(machineContext: IMachineManager, projectContext: ProjectContextType) { 16 | if (projectContext.project !== null) { 17 | projectContext.project.resetMachine(); 18 | machineContext.sendMessage({ type: MachineWorkerMessageTypes.Load, machine: projectContext.project.machine.toWorkerExchange() } as IMachineWorkerMessageLoad); 19 | } 20 | } 21 | 22 | export function MachineProvider({ children }: { children: ComponentChildren }) { 23 | const worker = useRef(null); 24 | const [message, setMessage] = useState(null); 25 | 26 | useEffect(() => { 27 | const w = new MachineWorker(); 28 | worker.current = w; 29 | 30 | w.onmessage = (e) => { 31 | setMessage(e.data); 32 | }; 33 | 34 | return () => { 35 | worker.current.terminate(); 36 | }; 37 | }, []); 38 | 39 | const sendMessage = (msg: IMachineWorkerMessage) => { 40 | worker.current?.postMessage(msg); 41 | }; 42 | 43 | const project = useProject(); 44 | useEffect(() => { 45 | if (project.project !== null) 46 | project.project.machine.shareWith(worker.current); 47 | }, [project.project]); 48 | 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | } 55 | 56 | export function useMachine() { 57 | return useContext(MachineManagerContext); 58 | } -------------------------------------------------------------------------------- /src/assets/text-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'src/colors.css'; 2 | import 'src/style.css'; 3 | 4 | import { ComponentChildren, render } from 'preact'; 5 | import { signal } from '@preact/signals'; 6 | import { useEffect } from 'preact/hooks'; 7 | 8 | import MachineWorker from 'src/workers/machineWorker?worker'; 9 | import { IMachineWorkerMessageTick, Machine } from 'src/backend/Emulator/Machine'; 10 | import { ZeroToZero } from 'src/backend/Emulator/Masters/ZeroToZero'; 11 | import { ConsoleLog } from 'src/backend/Emulator/Devices/ConsoleLog'; 12 | import { ProjectSelect } from 'src/components/ProjectSelect'; 13 | import { Tabs } from 'src/components/Tabs'; 14 | import { FileSystemProvider, useFileSystem } from 'src/components/FileSystemContext'; 15 | import { SideBar } from './components/sidebars/SideBar'; 16 | import { ProjectProvider } from './components/ProjectContext'; 17 | import { FileBrowserSideBar } from './components/sidebars/FileBrowserSideBar'; 18 | import { EditorContainer } from './components/editors/EditorContainer'; 19 | import { EditorManager } from './components/editors/EditorManager'; 20 | import { TabsProvider } from './components/TabsContext'; 21 | import { SimulatorControls } from './components/simulator/SimulatorControls'; 22 | import { SimulatorSideBar } from './components/simulator/SimulatorSideBar'; 23 | import { MachineProvider } from './components/simulator/MachineContext'; 24 | 25 | export function App() { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | 52 | function LoadingBarrier(props: { children: ComponentChildren }) { 53 | const { rootDir } = useFileSystem(); 54 | return rootDir ? <>{props.children} :

Loading, please wait...

; 55 | } 56 | 57 | render(, document.getElementById('app')); 58 | -------------------------------------------------------------------------------- /src/assets/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/TabsContext.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createContext, FunctionComponent } from "preact"; 2 | import { Dispatch, StateUpdater, useContext, useEffect, useState } from "preact/hooks"; 3 | import { ITab } from "./Tab"; 4 | import { EditorProjectSettings } from "./editors/EditorProjectSettings"; 5 | import { EditorMonaco } from "./editors/EditorMonaco"; 6 | import { useProject } from "./ProjectContext"; 7 | 8 | interface TabsContextType { 9 | tabs: ITab[]; 10 | setTabs: Dispatch>; 11 | currentTabIndex: number; 12 | setTabIndex: Dispatch>; 13 | }; 14 | 15 | const TabsContext = createContext({ 16 | tabs: [], 17 | setTabs: null, 18 | currentTabIndex: -1, 19 | setTabIndex: null 20 | }); 21 | 22 | export function openTab(context: TabsContextType, uri: string) { 23 | for (const [index, tab] of context.tabs.entries()) { 24 | if (tab.uri === uri) { 25 | context.setTabIndex(index); 26 | return; 27 | } 28 | } 29 | 30 | const newTabs = [...context.tabs, { uri, editor: getEditor(uri), context: {} } as ITab]; 31 | context.setTabs(newTabs); 32 | context.setTabIndex(newTabs.length - 1); 33 | } 34 | 35 | export function closeTab(context: TabsContextType, uri: string) { 36 | let tabIndex = -1; 37 | for (const [index, tab] of context.tabs.entries()) { 38 | if (tab.uri === uri) { 39 | tabIndex = index; 40 | break; 41 | } 42 | } 43 | 44 | if (tabIndex == -1) 45 | return; 46 | 47 | const newTabs = context.tabs.filter((value, index) => index != tabIndex); 48 | context.setTabs(newTabs); 49 | console.log(context.currentTabIndex, tabIndex); 50 | if (tabIndex < context.currentTabIndex) { 51 | context.setTabIndex(context.currentTabIndex - 1); 52 | } else { 53 | context.setTabIndex(context.currentTabIndex); 54 | } 55 | } 56 | 57 | export function getEditor(uri: string): FunctionComponent { 58 | // if (uri.endsWith(".rvedu")) { 59 | // return EditorProjectSettings; 60 | // } 61 | 62 | return EditorMonaco; 63 | } 64 | 65 | export function getCurrentTab(context: TabsContextType): ITab | null { 66 | if (context.currentTabIndex >= 0 && context.currentTabIndex < context.tabs.length) { 67 | return context.tabs[context.currentTabIndex]; 68 | } 69 | 70 | return null; 71 | } 72 | 73 | export function TabsProvider({ children }: { children: ComponentChildren }) { 74 | const [tabs, setTabs] = useState([]); 75 | const [currentTabIndex, setTabIndex] = useState(-1); 76 | 77 | const project = useProject(); 78 | useEffect(() => { 79 | setTabs([]); 80 | }, [project.projectName]); 81 | 82 | return ( 83 | 84 | {children} 85 | 86 | ); 87 | } 88 | 89 | export function useTabs() { 90 | return useContext(TabsContext); 91 | } -------------------------------------------------------------------------------- /src/backend/Logger.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | Info = 0, 3 | Warning, 4 | Error 5 | } 6 | 7 | export interface LogEntry { 8 | message: string; 9 | severity: LogLevel; 10 | when: number; 11 | } 12 | 13 | export class NewLogEntryEvent extends Event { 14 | public entry: LogEntry; 15 | 16 | public constructor(entry: LogEntry) { 17 | super('log-event'); 18 | this.entry = entry; 19 | } 20 | } 21 | 22 | export class Logger extends EventTarget { 23 | public name: string; 24 | private _entries: LogEntry[] = []; 25 | 26 | public constructor(name: string) { 27 | super(); 28 | 29 | this.name = name; 30 | } 31 | 32 | public log(message: string, severity: LogLevel): void { 33 | const entry: LogEntry = { 34 | message: message, 35 | severity: severity, 36 | when: Date.now() 37 | }; 38 | this._entries.push(entry); 39 | this.dispatchEvent(new NewLogEntryEvent(entry)); 40 | } 41 | 42 | public get entries(): LogEntry[] { 43 | return this._entries; 44 | } 45 | 46 | public clear() { 47 | this._entries = []; 48 | } 49 | } 50 | 51 | export class LoggerManagerEvent extends Event { 52 | public id: string; 53 | public entry: LogEntry; 54 | 55 | public constructor(id: string, entry: LogEntry) { 56 | super('logger-event'); 57 | this.id = id; 58 | this.entry = entry; 59 | } 60 | } 61 | 62 | export class NewLoggerEvent extends Event { 63 | public id: string; 64 | 65 | public constructor(id: string) { 66 | super('logger-new'); 67 | this.id = id; 68 | } 69 | } 70 | 71 | // export class LoggerManager extends EventTarget { 72 | // private static _the: LoggerManager; 73 | // public loggers = writable<{ loggers: { [id: string]: Logger } }>({loggers: {}}); 74 | 75 | // private constructor() { 76 | // super(); 77 | 78 | // LoggerManager._the = this; 79 | // } 80 | 81 | // public static get The() { 82 | // return LoggerManager._the ?? new LoggerManager(); 83 | // } 84 | 85 | // public reset() { 86 | // this.loggers.set({loggers: {}}); 87 | // } 88 | 89 | // public getLogger(id: string): Logger { 90 | // return get(this.loggers).loggers[id] ?? this.createLogger(id); 91 | // } 92 | 93 | // public createLogger(id: string = `${Math.random()}`, name: string = "New log"): Logger { 94 | // console.log(get(this.loggers)); 95 | // let logger: Logger | undefined = get(this.loggers).loggers[id]; 96 | // if (logger !== undefined) { 97 | // logger.clear(); 98 | // return logger; 99 | // } 100 | 101 | // console.log(`Making a new logger "${name}"`); 102 | // logger = new Logger(name); 103 | // this.loggers.update((o) => { 104 | // o.loggers[id] = logger!; 105 | // return o; 106 | // }); 107 | // logger.addEventListener('log-event', e => { 108 | // if (e instanceof NewLogEntryEvent) 109 | // dispatchEvent(new LoggerManagerEvent(id, e.entry)) 110 | // }); 111 | // dispatchEvent(new NewLoggerEvent(id)); 112 | // return logger; 113 | // } 114 | // } -------------------------------------------------------------------------------- /src/backend/Emulator/Devices/RAM32.ts: -------------------------------------------------------------------------------- 1 | import { Device, MasterBusDeviceRegistry } from "src/backend/Emulator/Bus"; 2 | import { v4 } from "uuid"; 3 | 4 | const RAM32_NAME: string = "ram32"; 5 | 6 | export interface IRAM32Context { 7 | address: number; 8 | size: number; 9 | } 10 | 11 | export class RAM32 extends Device { 12 | protected _dataView: DataView; 13 | // protected array: Uint32Array; 14 | 15 | public get position(): number { 16 | return this._dataView.getUint32(0); 17 | } 18 | 19 | protected set position(v: number) { 20 | this._dataView.setUint32(0, v); 21 | } 22 | 23 | public get size(): number { 24 | return this._dataView.getUint32(4); 25 | } 26 | 27 | protected set size(v: number) { 28 | this._dataView.setUint32(4, v); 29 | } 30 | 31 | protected getDword(address: number): number { 32 | return this._dataView.getUint32(4 + 4 + address); 33 | } 34 | 35 | protected setDword(address: number, value: number) { 36 | this._dataView.setUint32(4 + 4 + address, value); 37 | } 38 | 39 | public static fromContext(context: IRAM32Context, uuid?: string): RAM32 { 40 | const ram = new RAM32(); 41 | const calcSize = (context.size + 3) >> 2; 42 | 43 | ram.uuid = uuid ?? v4(); 44 | ram.state = new SharedArrayBuffer(4 + 4 + calcSize); 45 | ram._dataView = new DataView(ram.state); 46 | ram.position = context.address; 47 | ram.size = calcSize; 48 | 49 | return ram; 50 | } 51 | 52 | public static fromBuffer(state: SharedArrayBuffer, uuid: string): RAM32 { 53 | const ram = new RAM32(); 54 | 55 | ram.uuid = uuid; 56 | ram.state = state; 57 | ram._dataView = new DataView(ram.state); 58 | 59 | return ram; 60 | } 61 | 62 | protected DeviceRead(ioTick: number, address: number): number | null { 63 | if (address < this.position || address >= this.position + this.size) 64 | return null; 65 | 66 | if (address % 4 != 0) 67 | console.error(`ram unaligned access`); 68 | 69 | let value = this.getDword(address - this.position); 70 | console.log(`ram read @ ${address.toString(16)} = ${value}`); 71 | return value; 72 | } 73 | 74 | protected DeviceTick(tick: number): void { 75 | } 76 | 77 | protected DeviceWrite(ioTick: number, address: number, data: number): void { 78 | if (address < this.position || address >= this.position + this.size) 79 | return undefined; 80 | 81 | console.log(`ram write @ ${address.toString(16)} = ${data.toString(16)}`); 82 | if (address % 4 == 0) 83 | this.setDword(((address - this.position) >> 2) << 2, data); 84 | else 85 | console.error(`ram unaligned access`); 86 | } 87 | 88 | public get info(): { name: string; uuid: string; } { 89 | return { name: RAM32_NAME, uuid: this.uuid }; 90 | } 91 | 92 | public serialize(): any { 93 | return { 94 | address: this.position, 95 | size: this.size 96 | }; 97 | } 98 | 99 | public getState(): Uint32Array { 100 | return new Uint32Array(this.state.slice(4 + 4)); 101 | } 102 | } 103 | 104 | // TODO: 105 | MasterBusDeviceRegistry.set(RAM32_NAME, { 106 | fromContext(context, uuid: string = v4()) { 107 | return RAM32.fromContext(context, uuid); 108 | }, 109 | fromBuffer(state, uuid) { 110 | return RAM32.fromBuffer(state, uuid); 111 | }, 112 | }); 113 | -------------------------------------------------------------------------------- /src/backend/BuildSystem/ProjectBuilder.ts: -------------------------------------------------------------------------------- 1 | import type { Project } from "src/backend/ProjectManager"; 2 | import { LogLevel } from "src/backend/Logger"; 3 | import { BuildPrograms, type Program, ProgramStatus } from "src/backend/BuildSystem/Program"; 4 | 5 | export interface IBuildStageProgram { 6 | name: string; 7 | arguments: any[] 8 | } 9 | 10 | export interface IBuildStage { 11 | name: string; 12 | inputFiles: string[]; 13 | outputFiles: string[]; 14 | programsSequence: IBuildStageProgram[]; 15 | } 16 | 17 | export interface BuildStageProgram { 18 | program: Program; 19 | arguments: any[] 20 | } 21 | 22 | export class BuildStage { 23 | public name: string; 24 | public inputFiles: string[]; 25 | public outputFiles: string[]; 26 | public programsSequence: BuildStageProgram[]; 27 | 28 | public constructor(name: string, inputFiles: string[], outputFiles: string[], programsSequence: BuildStageProgram[]) { 29 | this.name = name; 30 | this.inputFiles = inputFiles; 31 | this.outputFiles = outputFiles; 32 | this.programsSequence = programsSequence; 33 | } 34 | 35 | public get isUpToDate(): boolean { 36 | // TODO: check whether the output file is older than any of the input files 37 | return false; 38 | } 39 | 40 | public run(project: Project): boolean { 41 | for (const value of this.programsSequence) { 42 | try { 43 | const result = value.program(project, value.arguments); 44 | if (result != ProgramStatus.Success) { 45 | console.log(`Program exited with status ${result}`, LogLevel.Error); 46 | } 47 | } catch (exception) { 48 | console.log(`Program threw an exception: ${exception}`, LogLevel.Error); 49 | return false; 50 | } 51 | } 52 | 53 | return true; 54 | } 55 | 56 | public toJSON(): IBuildStage { 57 | return { 58 | name: this.name, 59 | inputFiles: this.inputFiles, 60 | outputFiles: this.outputFiles, 61 | programsSequence: [] // TODO: serialize program sequence 62 | }; 63 | } 64 | 65 | public static fromJSON(json: IBuildStage): BuildStage { 66 | return new BuildStage(json.name ?? "", 67 | json.inputFiles, 68 | json.outputFiles, 69 | json.programsSequence.map((value) => { 70 | return { 71 | program: BuildPrograms.get(value.name).program, 72 | arguments: value.arguments 73 | } as BuildStageProgram; 74 | })); 75 | } 76 | } 77 | 78 | export interface IProjectBuilder { 79 | buildStages: IBuildStage[]; 80 | } 81 | 82 | export class ProjectBuilder { 83 | private buildStages: BuildStage[]; 84 | 85 | public constructor(buildStages: BuildStage[]) { 86 | this.buildStages = buildStages; 87 | } 88 | 89 | public run(project: Project, force: boolean = false): boolean { 90 | for (const stage of this.buildStages) { 91 | if (force || !stage.isUpToDate) { 92 | const success = stage.run(project); 93 | if (!success) 94 | return false; 95 | } 96 | } 97 | 98 | return true; 99 | } 100 | 101 | public toJSON(): IProjectBuilder { 102 | return { 103 | buildStages: this.buildStages.map((bs) => bs.toJSON()) 104 | }; 105 | } 106 | 107 | public static FromJSON(json: IProjectBuilder): ProjectBuilder { 108 | return new ProjectBuilder(json.buildStages.map((ibs) => BuildStage.fromJSON(ibs))); 109 | } 110 | } -------------------------------------------------------------------------------- /src/components/sidebars/FileBrowser.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "preact/hooks"; 2 | import style from "./FileBrowser.module.css"; 3 | import { combinePath } from "src/backend/FileSystem"; 4 | import { openTab, useTabs } from "../TabsContext"; 5 | 6 | type Entry = { 7 | name: string; 8 | isFile: boolean; 9 | handle: FileSystemHandle; 10 | }; 11 | 12 | export function FileBrowser(props: { 13 | dirHandle: FileSystemDirectoryHandle, 14 | ancestors?: string[] 15 | }) { 16 | const ancestors = props.ancestors ?? []; 17 | const depth = ancestors.length; 18 | 19 | const tabManager = useTabs(); 20 | 21 | const [entries, setEntries] = useState([]); 22 | const [expandedDirs, setExpandedDirs] = useState>({}); 23 | 24 | const refresh = async () => { 25 | const out: Entry[] = []; 26 | for await (const [name, handle] of props.dirHandle.entries()) { 27 | out.push({ 28 | name, 29 | isFile: handle.kind === 'file', 30 | handle, 31 | }); 32 | } 33 | out.sort((a, b) => { 34 | if (a.isFile !== b.isFile) { 35 | return a.isFile ? 1 : -1; 36 | } 37 | return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); 38 | }); 39 | setEntries(out); 40 | }; 41 | 42 | useEffect(() => { 43 | refresh(); 44 | const id = setInterval(refresh, 2000); 45 | return () => clearInterval(id); 46 | }, [props.dirHandle]); 47 | 48 | const toggleDir = (name: string) => { 49 | setExpandedDirs(prev => ({ 50 | ...prev, 51 | [name]: !prev[name], 52 | })); 53 | }; 54 | 55 | const createFile = async () => { 56 | const name = prompt('Введите имя файла:'); 57 | if (!name) return; 58 | const handle = await props.dirHandle.getFileHandle(name, { create: true }); 59 | const writable = await handle.createWritable(); 60 | await writable.write(''); 61 | await writable.close(); 62 | await refresh(); 63 | }; 64 | 65 | const createDir = async () => { 66 | const name = prompt('Введите имя папки:'); 67 | if (!name) return; 68 | await props.dirHandle.getDirectoryHandle(name, { create: true }); 69 | await refresh(); 70 | }; 71 | 72 | return ( 73 |
74 | {depth === 0 && ( 75 |
76 | 77 | 78 |
79 | )} 80 |
    81 | {entries.map(ent => ( 82 |
  • 83 | {ent.isFile ? ( 84 | { 85 | openTab(tabManager, combinePath([...ancestors, ent.name])); 86 | }}>📄 {ent.name} 87 | ) : ( 88 |
    89 | toggleDir(ent.name)}> 90 | {expandedDirs[ent.name] ? '📂' : '📁'} {ent.name} 91 | 92 | {expandedDirs[ent.name] && ( 93 | 94 | )} 95 |
    96 | )} 97 |
  • 98 | ))} 99 |
100 |
101 | ); 102 | 103 | } -------------------------------------------------------------------------------- /src/backend/ProjectManager.ts: -------------------------------------------------------------------------------- 1 | import {Exists, FSFile, FSFolder, Open, SaveAll} from "src/backend/FileSystem"; 2 | import {rootFolder} from "src/backend/FileSystem"; 3 | import {Machine} from "src/backend/Emulator/Machine"; 4 | import type {ISystemConfiguration} from "src/backend/Emulator/Machine"; 5 | import {type IProjectBuilder, ProjectBuilder} from "src/backend/BuildSystem/ProjectBuilder"; 6 | import {signal} from '@preact/signals'; 7 | 8 | export class Project { 9 | public machineTick: number; 10 | protected _machine: Machine; 11 | protected _projectBuilder: ProjectBuilder; 12 | protected _initialSystemConfiguration: ISystemConfiguration; 13 | 14 | public constructor(systemConfiguration: ISystemConfiguration, projectBuilder: ProjectBuilder) { 15 | this._initialSystemConfiguration = systemConfiguration; 16 | this._machine = Machine.FromSystemConfiguration(systemConfiguration); 17 | this._projectBuilder = projectBuilder; 18 | } 19 | 20 | public static FromJSON(json: IProjectJson): Project { 21 | const project = new Project( 22 | json.systemConfiguration ?? {master: null, devices: []}, 23 | ProjectBuilder.FromJSON(json.projectBuilder) 24 | ); 25 | return project; 26 | } 27 | 28 | public get machine(): Machine { 29 | return this._machine; 30 | } 31 | 32 | public get builder(): ProjectBuilder { 33 | return this._projectBuilder; 34 | } 35 | 36 | public resetMachine(): void { 37 | this._machine = Machine.FromSystemConfiguration(this._initialSystemConfiguration); 38 | } 39 | 40 | public MachineTick(): void { 41 | this._machine.doTick(); 42 | this.machineTick = this._machine.tick; 43 | } 44 | 45 | public ToJSON(): IProjectJson { 46 | return { 47 | systemConfiguration: this._machine.toSystemConfiguration(), 48 | projectBuilder: this._projectBuilder.toJSON() 49 | }; 50 | } 51 | } 52 | 53 | export interface IProjectJson { 54 | systemConfiguration: ISystemConfiguration, 55 | projectBuilder: IProjectBuilder 56 | } 57 | 58 | export const PROJECT_EXTENSION: string = ".rvedu"; 59 | 60 | export function makeProject(folderName: string): void { 61 | SaveAll(); 62 | 63 | const exists = Exists(`/${folderName}/${PROJECT_EXTENSION}`); 64 | if (exists) 65 | throw `Project ${folderName} already exists`; 66 | 67 | let mainSFile = Open(`/${folderName}/main.s`, true, true); 68 | mainSFile?.write("start:\n\tnop\n\taddi x1, x0, $40\n\taddi x2, x0, $2\n\tadd x3, x1, x2\n\tsw x3, $128(x0)\n\tj $0"); 69 | 70 | updateAvailableProjects(); 71 | } 72 | 73 | function getJsonFromString(jsonString: string): IProjectJson | null { 74 | // TODO: exceptions 75 | const jsonProject: IProjectJson = JSON.parse(jsonString); 76 | 77 | return jsonProject; 78 | } 79 | 80 | export const allProjects = signal([]); 81 | 82 | export function initializeProjectManager() { 83 | rootFolder.addEventListener('fsnode-updated', updateAvailableProjects); 84 | updateAvailableProjects(); 85 | } 86 | 87 | function updateAvailableProjects(): void { 88 | allProjects.value = availableProjects(); 89 | } 90 | 91 | function availableProjects(): string[] { 92 | let projects = Array.of(); 93 | 94 | for (const child of (rootFolder as FSFolder).children) { 95 | if (!(child instanceof FSFolder)) 96 | continue; 97 | 98 | for (const fChild of (child as FSFolder).children) { 99 | if (fChild instanceof FSFile && fChild.name == PROJECT_EXTENSION) { 100 | try { 101 | let projectJson = getJsonFromString((fChild as FSFile).text); 102 | if (projectJson != null) 103 | projects = [...projects, child.name]; 104 | } catch (e) { 105 | console.error(`Error while looking for available projects: ${e}`); 106 | } 107 | 108 | break; 109 | } 110 | } 111 | } 112 | 113 | return projects; 114 | } -------------------------------------------------------------------------------- /src/backend/Emulator/Devices/ROM32.ts: -------------------------------------------------------------------------------- 1 | import { Device, MasterBusDeviceRegistry } from "src/backend/Emulator/Bus"; 2 | import { v4 } from "uuid"; 3 | 4 | const ROM32_NAME: string = "rom32"; 5 | 6 | export interface IROM32Context { 7 | address: number; 8 | contents: Iterable; 9 | readOnly: boolean; 10 | } 11 | 12 | export class ROM32 extends Device { 13 | protected _dataView: DataView; 14 | 15 | public get position(): number { 16 | return this._dataView.getUint32(0); 17 | } 18 | 19 | protected set position(v: number) { 20 | this._dataView.setUint32(0, v); 21 | } 22 | 23 | public get size(): number { 24 | return this._dataView.getUint32(4); 25 | } 26 | 27 | protected set size(v: number) { 28 | this._dataView.setUint32(4, v); 29 | } 30 | 31 | protected get readOnly(): boolean { 32 | return this._dataView.getUint32(4 + 4) !== 0; 33 | } 34 | 35 | protected set readOnly(v: boolean) { 36 | this._dataView.setUint32(4 + 4, v ? 1 : 0); 37 | } 38 | 39 | public getDword(address: number): number { 40 | return this._dataView.getUint32(4 + 4 + 4 + address); 41 | } 42 | 43 | public setDword(address: number, value: number) { 44 | this._dataView.setUint32(4 + 4 + 4 + address, value); 45 | } 46 | 47 | public static fromContext(context: IROM32Context, uuid?: string): ROM32 { 48 | const rom = new ROM32(); 49 | 50 | const arr = new Uint32Array(context.contents); 51 | 52 | rom.uuid = uuid ?? v4(); 53 | rom.state = new SharedArrayBuffer(4 + 4 + 4 + arr.byteLength); 54 | rom._dataView = new DataView(rom.state); 55 | rom.position = context.address; 56 | rom.readOnly = context.readOnly ?? true; 57 | rom.size = arr.byteLength; 58 | 59 | for (let i = 0; i < arr.length; i++) { 60 | rom._dataView.setUint32(4 + 4 + 4 + i * 4, arr[i]); 61 | } 62 | 63 | return rom; 64 | } 65 | 66 | public static fromBuffer(state: SharedArrayBuffer, uuid: string): ROM32 { 67 | const rom = new ROM32(); 68 | 69 | rom.uuid = uuid; 70 | rom.state = state; 71 | rom._dataView = new DataView(rom.state); 72 | 73 | return rom; 74 | } 75 | 76 | // constructor(position: number, contents: Uint32Array, readOnly: boolean) { 77 | // super(); 78 | 79 | // this.position = position ?? 0; 80 | // if (contents === undefined) 81 | // throw "Why would you make a ROM with no contents?"; 82 | // this.contents = contents; 83 | // this.readOnly = readOnly ?? true; 84 | // } 85 | 86 | protected DeviceRead(ioTick: number, address: number): number | null { 87 | if (address < this.position || address >= this.position + this.size) 88 | return null; 89 | 90 | if (address % 4 != 0) 91 | console.error(`rom unaligned access`); 92 | 93 | let value = this.getDword(address - this.position); 94 | console.log(`rom read @ ${address.toString(16)} = ${value}`); 95 | return value; 96 | } 97 | 98 | protected DeviceTick(tick: number): void { 99 | } 100 | 101 | protected DeviceWrite(ioTick: number, address: number, data: number): void { 102 | if (this.readOnly || address < this.position || address >= this.position + this.size) 103 | return undefined; 104 | 105 | console.log(`rom write @ ${address.toString(16)} = ${data.toString(16)}`); 106 | if (address % 4 == 0) 107 | this.setDword(((address - this.position) >> 2) << 2, data); 108 | else 109 | console.error(`rom unaligned access`); 110 | } 111 | 112 | public get info(): { name: string; uuid: string; } { 113 | return { name: ROM32_NAME, uuid: this.uuid }; 114 | } 115 | 116 | public serialize(): { name: string; uuid: string; context: IROM32Context } { 117 | return { 118 | name: ROM32_NAME, 119 | uuid: this.uuid, 120 | context: { 121 | address: this.position, 122 | contents: Array.from(new Uint32Array(this.state.slice(4 + 4 + 4))), 123 | readOnly: this.readOnly 124 | } 125 | }; 126 | } 127 | 128 | public getState(): Uint32Array { 129 | return new Uint32Array(this.state.slice(4 + 4 + 4)); 130 | } 131 | } 132 | 133 | MasterBusDeviceRegistry.set(ROM32_NAME, { 134 | fromContext(context: IROM32Context, uuid: string = v4()) { 135 | return ROM32.fromContext(context, uuid); 136 | }, 137 | fromBuffer(state, uuid) { 138 | return ROM32.fromBuffer(state, uuid); 139 | }, 140 | }); 141 | -------------------------------------------------------------------------------- /src/backend/Emulator/Machine.ts: -------------------------------------------------------------------------------- 1 | import { Bus, Device, Master, MasterBusDeviceRegistry, MasterBusMasterRegistry } from "src/backend/Emulator/Bus"; 2 | import "src/backend/Emulator/Devices/All"; 3 | import "src/backend/Emulator/Masters/All"; 4 | 5 | export interface ISystemConfiguration { 6 | master: { name: string; uuid?: string; context: any; }, 7 | devices: { name: string; uuid?: string; context: any; }[] 8 | } 9 | 10 | export interface IDeviceWorkerExchange { 11 | info: { name: string; uuid: string; }; 12 | state: SharedArrayBuffer; 13 | } 14 | 15 | export interface ISystemWorkerExchange { 16 | master: IDeviceWorkerExchange; 17 | bus: { 18 | exchange: IDeviceWorkerExchange; 19 | devices: IDeviceWorkerExchange[]; 20 | }; 21 | } 22 | 23 | export enum MachineWorkerMessageTypes { 24 | Load = "load", 25 | Tick = "tick", 26 | Run = "run", 27 | Stop = "stop" 28 | } 29 | 30 | export interface IMachineWorkerMessage { 31 | type: MachineWorkerMessageTypes; 32 | } 33 | 34 | export interface IMachineWorkerMessageLoad extends IMachineWorkerMessage { 35 | type: MachineWorkerMessageTypes.Load; 36 | machine: ISystemWorkerExchange; 37 | } 38 | 39 | export interface IMachineWorkerMessageTick extends IMachineWorkerMessage { 40 | type: MachineWorkerMessageTypes.Tick; 41 | } 42 | 43 | export interface IMachineWorkerMessageRun extends IMachineWorkerMessage { 44 | type: MachineWorkerMessageTypes.Run; 45 | } 46 | 47 | export interface IMachineWorkerMessageStop extends IMachineWorkerMessage { 48 | type: MachineWorkerMessageTypes.Stop; 49 | } 50 | 51 | export class Machine { 52 | _masterBus: Bus; 53 | _master: Master; 54 | _tick: number = 0; 55 | 56 | protected constructor() { } 57 | 58 | public static FromMasterDevices(master: Master, devices: Device[]): Machine { 59 | const machine = new Machine(); 60 | 61 | // let deviceInstances: Device[] = []; 62 | // for (let device of devices) { 63 | // deviceInstances.push(device); 64 | // } 65 | machine._masterBus = Bus.fromContext(devices); 66 | 67 | machine._master = master; 68 | machine._master.bus = machine._masterBus; 69 | 70 | return machine; 71 | } 72 | 73 | public static FromSystemConfiguration(systemConfiguration: ISystemConfiguration): Machine { 74 | let master: Master = MasterBusMasterRegistry 75 | .get(systemConfiguration.master.name) 76 | .fromContext(systemConfiguration.master.context, systemConfiguration.master.uuid); 77 | 78 | let devices: Device[] = Array.of>(); 79 | for (let deviceSerialized of systemConfiguration.devices) { 80 | let device = MasterBusDeviceRegistry.get(deviceSerialized.name).fromContext(deviceSerialized.context, deviceSerialized.uuid); 81 | devices = [...devices, device]; 82 | } 83 | 84 | return Machine.FromMasterDevices(master, devices); 85 | } 86 | 87 | public static FromWorkerExchange(workerExchange: ISystemWorkerExchange): Machine { 88 | let master: Master = MasterBusMasterRegistry 89 | .get(workerExchange.master.info.name) 90 | .fromBuffer(workerExchange.master.state, workerExchange.master.info.uuid); 91 | 92 | let devices: Device[] = Array.of>(); 93 | for (let deviceSerialized of workerExchange.bus.devices) { 94 | let device = MasterBusDeviceRegistry.get(deviceSerialized.info.name).fromBuffer(deviceSerialized.state, deviceSerialized.info.uuid); 95 | devices = [...devices, device]; 96 | } 97 | 98 | const machine = new Machine(); 99 | 100 | machine._masterBus = Bus.fromBuffer(devices, workerExchange.bus.exchange.info.uuid, workerExchange.bus.exchange.state); 101 | 102 | machine._master = master; 103 | machine._master.bus = machine._masterBus; 104 | 105 | return machine; 106 | } 107 | 108 | public toSystemConfiguration(): ISystemConfiguration { 109 | return { master: this._master.serialize(), devices: this._masterBus.devices.map((device) => device.serialize()) }; 110 | } 111 | 112 | public toWorkerExchange(): ISystemWorkerExchange { 113 | return { 114 | master: { info: this._master.info, state: this._master.state }, 115 | bus: { 116 | exchange: { info: this._masterBus.info, state: this._masterBus.state }, 117 | devices: this._masterBus.devices.map((device) => { return { info: device.info, state: device.state } }) 118 | } 119 | }; 120 | } 121 | 122 | public get masterBus() { 123 | return this._masterBus; 124 | } 125 | 126 | public get tick() { 127 | return this._tick; 128 | } 129 | 130 | public doTick(): void { 131 | this._master.DoIO(this.tick); 132 | this._master.Tick(this.tick); 133 | this._masterBus.Tick(this.tick); 134 | this._tick++; 135 | } 136 | 137 | public shareWith(worker: Worker): void { 138 | const message: IMachineWorkerMessageLoad = { type: MachineWorkerMessageTypes.Load, machine: this.toWorkerExchange() }; 139 | worker.postMessage(message); 140 | } 141 | } -------------------------------------------------------------------------------- /src/components/ProjectContext.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentChildren, createContext } from "preact"; 2 | import { Dispatch, StateUpdater, useContext, useEffect, useState } from "preact/hooks"; 3 | import { useFileSystem } from "src/components/FileSystemContext"; 4 | import { MakeADD, MakeADDI, MakeNOP, MakeSW, MakeJ } from "src/backend/Assembler/Instructions"; 5 | import { IProjectJson, Project, PROJECT_EXTENSION } from "src/backend/ProjectManager"; 6 | import { BuildStage, ProjectBuilder } from "src/backend/BuildSystem/ProjectBuilder"; 7 | 8 | export interface ProjectContextType { 9 | projectName: string; 10 | setProjectName: Dispatch>; 11 | project: Project; 12 | projectFolder: FileSystemDirectoryHandle; 13 | }; 14 | 15 | const ProjectContext = createContext({ 16 | projectName: 'new-project', 17 | project: null, 18 | setProjectName: null, 19 | projectFolder: null 20 | }); 21 | 22 | const EXAMPLE_ASSEMBLY = ` 23 | .offset 0x0 24 | 25 | # Выводит число 8 в регистр 1 26 | test: 27 | addi x1, x0, 10 28 | # beq x1, x0, 4 29 | addi x1, x1, -2 30 | loop: jal x0, 0 31 | `; 32 | 33 | export function ProjectProvider({ children }: { children: ComponentChildren }) { 34 | const [projectName, setProjectName] = useState('new-project'); 35 | const [projectFolder, setProjectFolder] = useState(null); 36 | const [project, setProject] = useState(null); 37 | 38 | const fs = useFileSystem(); 39 | useEffect(() => { 40 | (async () => { 41 | let tempProjectFolder: FileSystemDirectoryHandle; 42 | try { 43 | tempProjectFolder = await fs.rootDir.getDirectoryHandle(projectName); 44 | } catch (ex: any) { 45 | if (ex instanceof DOMException && ex.name == "NotFoundError") { 46 | tempProjectFolder = await fs.rootDir.getDirectoryHandle(projectName, { create: true }); 47 | 48 | async function writeFile(fileName: string, fileContent: string) { 49 | const fileHandle = await tempProjectFolder.getFileHandle(fileName, { create: true }); 50 | const fileWritable = await fileHandle.createWritable(); 51 | const fileWriter = fileWritable.getWriter(); 52 | await fileWriter.ready; 53 | const textEncoder = new TextEncoder(); 54 | const chunks = textEncoder.encode(fileContent); 55 | await fileWriter.write(chunks); 56 | await fileWriter.ready; 57 | fileWriter.releaseLock(); 58 | await fileWritable.close(); 59 | } 60 | 61 | const json = JSON.stringify({ 62 | systemConfiguration: { 63 | master: { 64 | name: "rv32", 65 | context: null 66 | }, 67 | devices: [ 68 | { 69 | name: "consolelog", 70 | context: null 71 | }, { 72 | name: "ram32", 73 | context: { address: 128, size: 128 } 74 | }, { 75 | name: "rom32", 76 | context: { 77 | address: 0, 78 | contents: [ 79 | MakeNOP(), 80 | MakeADDI(0, 1, 40), 81 | MakeADDI(0, 2, 2), 82 | MakeADD(1, 2, 3), 83 | MakeSW(0, 3, 128), 84 | MakeJ(0), 85 | ...Array(4 * 16) // Reserve some space 86 | ], 87 | readOnly: true 88 | } 89 | } 90 | ] 91 | }, 92 | projectBuilder: (new ProjectBuilder([ 93 | // TODO: 94 | new BuildStage("run", ["main.s"], ["main.s.o"], []) 95 | ])).toJSON() 96 | } as IProjectJson); 97 | await writeFile(PROJECT_EXTENSION, json); 98 | await writeFile("main.s", EXAMPLE_ASSEMBLY); 99 | } 100 | } 101 | 102 | const projectJsonHandle = await tempProjectFolder.getFileHandle(PROJECT_EXTENSION); 103 | const projectJsonFile = await projectJsonHandle.getFile(); 104 | const projectJson = JSON.parse(await projectJsonFile.text()) as IProjectJson; 105 | const proj = Project.FromJSON(projectJson); 106 | 107 | setProjectFolder(tempProjectFolder); 108 | setProject(proj); 109 | })(); 110 | }, [projectName]); 111 | 112 | return ( 113 | 114 | {children} 115 | 116 | ); 117 | } 118 | 119 | export function useProject() { 120 | return useContext(ProjectContext); 121 | } -------------------------------------------------------------------------------- /src/backend/Emulator/Masters/SimpleCPU.ts: -------------------------------------------------------------------------------- 1 | import {Master, MasterBusMasterRegistry} from "src/backend/Emulator/Bus"; 2 | import {v4} from "uuid"; 3 | 4 | enum CPUState { 5 | ReadInstruction, 6 | ProcessInstruction, 7 | ReadWordFromRAM, 8 | ReadWordFromRAMStage2, 9 | WriteWordToRAM 10 | } 11 | 12 | export enum CPUInstructions { 13 | Noop, 14 | LoadImmediate, 15 | AddToRegister, 16 | StoreAtAddress, 17 | LoadFromAddress 18 | } 19 | 20 | const SIMPLECPU_NAME: string = "simplecpu"; 21 | 22 | export interface ISimpleCPUState { 23 | state: string; 24 | ip: number; 25 | register: number; 26 | } 27 | 28 | export class SimpleCPU extends Master { 29 | public svelteComponent: any | null = null; 30 | 31 | private ip: number = 0; 32 | private dataRq: number = 0; 33 | private dword: number = 0; 34 | private state: CPUState = 0; 35 | 36 | private currentInstruction: CPUInstructions = 0; 37 | private register: number = 0; 38 | 39 | protected DeviceTick(tick: number): void { 40 | console.log(`scpu: tick, state ${CPUState[this.state]}`); 41 | 42 | switch (this.state) { 43 | case CPUState.ReadInstruction: 44 | break; 45 | 46 | case CPUState.ProcessInstruction: 47 | this.currentInstruction = this.dword 48 | switch (this.currentInstruction) { 49 | case CPUInstructions.Noop: 50 | console.log("scpu: noop"); 51 | this.ip += 4; 52 | this.state = CPUState.ReadInstruction; 53 | break; 54 | 55 | case CPUInstructions.LoadImmediate: 56 | case CPUInstructions.AddToRegister: 57 | case CPUInstructions.StoreAtAddress: 58 | case CPUInstructions.LoadFromAddress: 59 | this.state = CPUState.ReadWordFromRAM; 60 | this.dataRq = this.ip + 4; 61 | break; 62 | 63 | default: 64 | throw `Unhandled instruction ${this.currentInstruction}`; 65 | } 66 | break; 67 | 68 | case CPUState.ReadWordFromRAM: 69 | switch (this.currentInstruction) { 70 | case CPUInstructions.LoadImmediate: 71 | this.register = this.dword; 72 | this.ip += 8; 73 | this.state = CPUState.ReadInstruction; 74 | break; 75 | 76 | case CPUInstructions.AddToRegister: 77 | this.register += this.dword; 78 | this.ip += 8; 79 | this.state = CPUState.ReadInstruction; 80 | break; 81 | 82 | case CPUInstructions.StoreAtAddress: 83 | this.dataRq = this.dword; 84 | this.state = CPUState.WriteWordToRAM; 85 | break; 86 | 87 | case CPUInstructions.LoadFromAddress: 88 | this.dataRq = this.dword; 89 | this.state = CPUState.ReadWordFromRAMStage2; 90 | break; 91 | } 92 | break; 93 | 94 | case CPUState.ReadWordFromRAMStage2: 95 | switch (this.currentInstruction) { 96 | case CPUInstructions.LoadFromAddress: 97 | this.register = this.dword; 98 | this.state = CPUState.ReadInstruction; 99 | this.ip += 8; 100 | break; 101 | } 102 | break; 103 | 104 | case CPUState.WriteWordToRAM: 105 | switch (this.currentInstruction) { 106 | case CPUInstructions.StoreAtAddress: 107 | this.ip += 8; 108 | this.state = CPUState.ReadInstruction; 109 | break; 110 | } 111 | break; 112 | } 113 | 114 | console.log(`scpu: state: reg=${this.register}`); 115 | } 116 | 117 | protected MasterIO(ioTick: number): void { 118 | switch (this.state) { 119 | case CPUState.ReadInstruction: { 120 | let i = this.bus.Read(ioTick, this.ip); 121 | if (i == null) 122 | throw `no response @ ${this.ip}`; 123 | this.dword = i; 124 | this.state = CPUState.ProcessInstruction; 125 | 126 | break; 127 | } 128 | 129 | case CPUState.ReadWordFromRAM: 130 | case CPUState.ReadWordFromRAMStage2: { 131 | let i = this.bus.Read(ioTick, this.dataRq); 132 | if (i == null) 133 | throw `no response @ ${this.dataRq}`; 134 | this.dword = i; 135 | 136 | break; 137 | } 138 | 139 | case CPUState.WriteWordToRAM: { 140 | this.bus.Write(ioTick, this.dataRq, this.register); 141 | 142 | break; 143 | } 144 | } 145 | } 146 | 147 | public getState(): ISimpleCPUState { 148 | return { 149 | state: CPUState[this.state], 150 | register: this.register, 151 | ip: this.ip 152 | }; 153 | } 154 | 155 | public serialize(): { name: string; uuid: string; context: any } { 156 | return {name: SIMPLECPU_NAME, uuid: this.uuid, context: undefined}; 157 | } 158 | } 159 | 160 | MasterBusMasterRegistry[SIMPLECPU_NAME] = (context, uuid: string = v4()) => { 161 | const cpu = new SimpleCPU(); 162 | cpu.uuid = uuid; 163 | return cpu; 164 | } -------------------------------------------------------------------------------- /src/backend/BuildSystem/BuildPrograms/Assembler.ts: -------------------------------------------------------------------------------- 1 | import {BuildPrograms, ProgramStatus} from "$lib/backend/BuildSystem/Program"; 2 | import {LoggerManager} from "$lib/backend/Logger"; 3 | 4 | 5 | enum RvAssemblerTokens { 6 | INVALID = -1, 7 | LITERAL = 0, 8 | NUMBER = 1, 9 | COMMA = 2, 10 | COLON = 3, 11 | NEW_LINE = 4, 12 | } 13 | 14 | const RvAssemblerRegularExpressions = { 15 | label: /[a-zA-Zа-яёА-ЯЁ_\d]+:/, 16 | register: /%[a-zA-Z0-9]+/, 17 | literal: /[a-zA-Z_][a-zA-Z_\d]*/, 18 | number: /\$-?((0[xX])?[0-9A-Fa-f]+)/, 19 | comment: /(^#.*$)/, 20 | whitespace: /[ \t]*/ 21 | }; 22 | 23 | export class RvAssembler { 24 | public constructor() { 25 | } 26 | 27 | public compile(source: string) { 28 | LoggerManager.The.createLogger("assembler-compiler", "Build Log"); 29 | return this.#assemble(this.#parse(this.tokenize(source))); 30 | } 31 | 32 | private tokenize(source: string) { 33 | class Token { 34 | static get type() { 35 | return RvAssemblerTokens.INVALID; 36 | } 37 | 38 | constructor() { 39 | } 40 | 41 | static parse(string: string) { 42 | return {token: undefined, length: 0}; 43 | } 44 | } 45 | 46 | class Whitespace { 47 | static parse(string: string) { 48 | return {token: undefined, length: RvAssemblerRegularExpressions.whitespace.exec(string)![0].length}; // implying that this regexp would always match even when there are no whitespaces at all 49 | } 50 | } 51 | 52 | class Comment extends Token { 53 | static parse(string: string) { 54 | if (string[0] == ';') { 55 | let pos = 1; 56 | while (pos < string.length) { 57 | if (string[pos] == '\n') { 58 | return {token: undefined, length: pos}; 59 | } 60 | pos++; 61 | } 62 | 63 | return {token: undefined, length: pos}; 64 | } 65 | 66 | return super.parse(string); 67 | } 68 | } 69 | 70 | class SingleCharacterToken extends Token { 71 | static get character() { 72 | return undefined; 73 | } 74 | 75 | static parse(string: string) { 76 | if (string[0] == this.character) 77 | return {token: new this(), length: 1}; 78 | 79 | return super.parse(string); 80 | } 81 | } 82 | 83 | class NewLine extends SingleCharacterToken { 84 | static get type() { 85 | return RvAssemblerTokens.NEW_LINE; 86 | } 87 | 88 | static get character() { 89 | return '\n'; 90 | } 91 | } 92 | 93 | class Comma extends SingleCharacterToken { 94 | static get type() { 95 | return RvAssemblerTokens.COMMA; 96 | } 97 | 98 | static get character() { 99 | return ','; 100 | } 101 | } 102 | 103 | class Colon extends SingleCharacterToken { 104 | static get type() { 105 | return RvAssemblerTokens.COLON; 106 | } 107 | 108 | static get character() { 109 | return ':'; 110 | } 111 | } 112 | 113 | class TokenWithValue extends Token { 114 | value; 115 | 116 | constructor(value: any) { 117 | super(); 118 | 119 | this.value = value; 120 | } 121 | } 122 | 123 | class Literal extends TokenWithValue { 124 | static get type() { 125 | return RvAssemblerTokens.LITERAL; 126 | } 127 | 128 | static regexp = new RegExp(`^${RvAssemblerRegularExpressions.literal.source}`); 129 | 130 | static parse(s: string) { 131 | let exec = this.regexp.exec(s); 132 | if (exec) { 133 | return {token: new this(exec[0]), length: exec[0].length}; 134 | } 135 | 136 | return super.parse(s); 137 | } 138 | } 139 | 140 | const PARSE_ORDER: ((s: string) => { length: number, token: any })[] = [ 141 | (s) => Whitespace.parse(s), 142 | (s) => NewLine.parse(s), 143 | (s) => Comma.parse(s), 144 | (s) => Colon.parse(s), 145 | (s) => Comment.parse(s), 146 | (s) => Literal.parse(s), 147 | // TODO: (s) => Number.parse(s), 148 | ]; 149 | 150 | let tokens = []; 151 | let position = 0; 152 | let parser = 0; 153 | while (parser < PARSE_ORDER.length && position < source.length) { 154 | let result = PARSE_ORDER[parser](source.slice(position)); 155 | if (result && result.length != 0) { 156 | if (result.token) 157 | tokens.push(result.token); 158 | position += result.length; 159 | parser = 0; 160 | } else 161 | parser++; 162 | } 163 | if (position != source.length) 164 | throw 'asm.error.tokenizer.failed-to-parse'; 165 | 166 | return tokens; 167 | } 168 | 169 | #parse(tokens: any[]): any[] { 170 | console.log(tokens); 171 | 172 | throw 'Not implemented yet!'; 173 | } 174 | 175 | #assemble(ast: any[]) { 176 | throw 'Not implemented yet!'; 177 | } 178 | } 179 | 180 | BuildPrograms["asm"] = { 181 | program: (logger, ...args) => { 182 | const as = new RvAssembler(); 183 | return ProgramStatus.Success; 184 | } 185 | }; -------------------------------------------------------------------------------- /src/components/ProjectSelect.tsx: -------------------------------------------------------------------------------- 1 | import projectIcon from "src/assets/project.svg"; 2 | import style from "./ProjectSelect.module.css"; 3 | import { useProject } from "./ProjectContext"; 4 | import { useFileSystem } from "./FileSystemContext"; 5 | import { useEffect, useState } from "preact/hooks"; 6 | import JSZip from "jszip"; 7 | import { combinePath, PATH_SEPARATOR } from "src/backend/FileSystem"; 8 | 9 | export function ProjectSelect() { 10 | const projectContext = useProject(); 11 | const fs = useFileSystem(); 12 | 13 | const [projectsList, setProjectsList] = useState([]); 14 | useEffect(() => { 15 | (async () => { 16 | const folders = []; 17 | console.log("iterating folders"); 18 | for await (const [name, handle] of fs.rootDir.entries()) { 19 | console.log(name); 20 | if (handle.kind == "directory") { 21 | folders.push(name); 22 | } 23 | } 24 | setProjectsList(folders); 25 | })(); 26 | }, [projectContext.projectFolder]); 27 | 28 | console.log(projectsList); 29 | return ( 30 |
31 | 32 | 44 | 87 | 119 |
120 | ); 121 | } -------------------------------------------------------------------------------- /src/components/editors/EditorMonaco.tsx: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' 3 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' 4 | import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker' 5 | import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker' 6 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' 7 | import { useEffect, useRef, useState } from 'preact/hooks'; 8 | import { useProject } from '../ProjectContext'; 9 | import { getFile } from 'src/backend/FileSystem'; 10 | import { useEditorManager } from './EditorManager'; 11 | import { PROJECT_EXTENSION } from 'src/backend/ProjectManager'; 12 | 13 | self.MonacoEnvironment = { 14 | getWorker(_, label) { 15 | if (label === 'json') { 16 | return new jsonWorker() 17 | } 18 | if (label === 'css' || label === 'scss' || label === 'less') { 19 | return new cssWorker() 20 | } 21 | if (label === 'html' || label === 'handlebars' || label === 'razor') { 22 | return new htmlWorker() 23 | } 24 | if (label === 'typescript' || label === 'javascript') { 25 | return new tsWorker() 26 | } 27 | return new editorWorker() 28 | } 29 | } 30 | 31 | monaco.editor.defineTheme('riscv-edu-ide-theme', { 32 | base: 'vs', 33 | inherit: true, 34 | rules: [ 35 | // { token: '', foreground: '#313131' }, 36 | // { token: 'register', foreground: '#313131' }, 37 | // { token: 'instruction', foreground: '#313131', fontStyle: 'bold' }, 38 | { token: 'instruction', foreground: '#448000' }, 39 | { token: 'comment', foreground: '#00000054' }, 40 | { token: 'directive', foreground: '#806300' }, 41 | { token: 'number', foreground: '#28766e' }, 42 | { token: 'label', foreground: '#7D0080' } 43 | ], 44 | colors: { 45 | // 'editor.background': '#FAFEF6', 46 | // 'editor.foreground': '#313131' 47 | } 48 | }); 49 | 50 | monaco.languages.register({ id: 'asm' }); 51 | monaco.languages.setMonarchTokensProvider('asm', { 52 | defaultToken: '', 53 | tokenPostfix: '.asm', 54 | 55 | instructions: [ 56 | 'add', 'addi', 'sub', 'and', 'andi', 'or', 'ori', 'xor', 'xori', 'slt', 'slti', 'sltu', 'sltiu', 'sra', 'srai', 'srl', 'srli', 'sll', 'slli', 57 | 'lw', 'sw', 'lb', 'sb', 'lui', 'auipc', 'jal', 'jalr', 'beq', 'bne', 'blt', 'bge', 'bltu', 'bgeu' 58 | ], 59 | directives: ['.section', '.global', '.define', '.offset'], 60 | 61 | tokenizer: { 62 | root: [ 63 | [/#.*$/, "comment"], 64 | [/[a-zA-Zа-яёА-ЯЁ_\d]+:/, "label"], 65 | // [/%[a-zA-Z0-9]+/, "register"], 66 | [/\.[a-zA-Z_][a-zA-Z_\d]*/, { 67 | cases: { 68 | '@directives': 'directive', 69 | '@default': '' 70 | } 71 | }], 72 | [/[a-zA-Z_][a-zA-Z_\d]*/, { 73 | cases: { 74 | '@instructions': 'instruction', 75 | '@default': '' 76 | } 77 | }], 78 | [/-?((0[xX])?[0-9A-Fa-f]+)/, "number"], 79 | // [/[ \t]+/, "whitespace"] 80 | ] 81 | } 82 | }); 83 | 84 | interface IEditorMonacoContext { 85 | model: monaco.editor.ITextModel; 86 | cursorPosition?: monaco.Position; 87 | } 88 | 89 | export function EditorMonaco(props: { uri: string, context: IEditorMonacoContext }) { 90 | const projectManager = useProject(); 91 | const editorManager = useEditorManager(); 92 | const editorRef = useRef(null); 93 | const monacoRef = useRef(null); 94 | const [language, setLanguage] = useState('plaintext'); 95 | 96 | useEffect(() => { 97 | (async () => { 98 | const fileHandle = await getFile(props.uri, projectManager.projectFolder); 99 | let text = ""; 100 | if (fileHandle !== null) { 101 | const file = await fileHandle.getFile(); 102 | text = await file.text(); 103 | } 104 | 105 | // const lang = detectLanguage(name); 106 | // setLanguage(lang); 107 | 108 | if (editorRef.current) { 109 | props.context.model ??= monaco.editor.createModel(text, props.uri.endsWith(PROJECT_EXTENSION) ? 'json' : 'asm'); 110 | 111 | monacoRef.current = monaco.editor.create(editorRef.current, { 112 | model: props.context.model, 113 | theme: 'riscv-edu-ide-theme', 114 | automaticLayout: true, 115 | }); 116 | 117 | if (props.context.cursorPosition) { 118 | monacoRef.current.setPosition(props.context.cursorPosition); 119 | monacoRef.current.revealPositionInCenter(props.context.cursorPosition); 120 | } 121 | } 122 | })(); 123 | 124 | return () => { 125 | props.context.cursorPosition = monacoRef.current?.getPosition(); 126 | monacoRef.current?.dispose(); 127 | }; 128 | }, [props.uri]); 129 | 130 | useEffect(() => { 131 | async function saveFile() { 132 | if (props.context.model !== undefined) { 133 | const fileHandle = await getFile(props.uri, projectManager.projectFolder); 134 | const writable = await fileHandle.createWritable(); 135 | const textEncoder = new TextEncoder(); 136 | writable.write(textEncoder.encode(props.context.model.getValue())); 137 | await writable.close(); 138 | } 139 | }; 140 | editorManager.saveFile.current = saveFile; 141 | 142 | return () => { 143 | editorManager.saveFile.current = null; 144 | }; 145 | }, [editorManager]); 146 | 147 | return ( 148 |
149 | ); 150 | } -------------------------------------------------------------------------------- /src/backend/Assembler/Tokenizer.ts: -------------------------------------------------------------------------------- 1 | export enum SpecialSymbols { 2 | COMMENT = "#", 3 | LABEL = ":", 4 | COMMA = ",", 5 | DIRECTIVE = ".", 6 | PREPROCESSOR = "#", 7 | NEW_LINE = "\n", 8 | NEW_LINE_WINDOWS = "\r" 9 | } 10 | 11 | export abstract class Token { 12 | start: number; 13 | length: number; 14 | 15 | constructor(start: number, length: number) { 16 | this.start = start; 17 | this.length = length; 18 | } 19 | } 20 | 21 | export class Space extends Token { 22 | static TryParse(input: string, start: number): Space | null { 23 | let pos = start; 24 | while (pos < input.length && (input[pos] == " " || input[pos] == "\t")) { 25 | pos++; 26 | } 27 | 28 | if (pos != start) { 29 | return new Space(start, pos - start); 30 | } 31 | 32 | return null; 33 | } 34 | } 35 | 36 | export class Comment extends Token { 37 | static TryParse(input: string, start: number): Comment | null { 38 | if (start < input.length && input[start] === SpecialSymbols.COMMENT) { 39 | let position = start + 1; 40 | while (position < input.length && NewLine.TryParse(input, position) == null) { 41 | position++; 42 | } 43 | 44 | return new Comment(start, position - start); 45 | } 46 | 47 | return null; 48 | } 49 | } 50 | 51 | export class Colon extends Token { 52 | static TryParse(input: string, start: number): Colon | null { 53 | if (start < input.length && input[start] === SpecialSymbols.LABEL) { 54 | return new Colon(start, 1); 55 | } 56 | 57 | return null; 58 | } 59 | } 60 | 61 | export class Dot extends Token { 62 | static TryParse(input: string, start: number): Dot | null { 63 | if (start < input.length && input[start] === SpecialSymbols.DIRECTIVE) { 64 | return new Dot(start, 1); 65 | } 66 | 67 | return null; 68 | } 69 | } 70 | 71 | export class Comma extends Token { 72 | static TryParse(input: string, start: number): Comma | null { 73 | if (start < input.length && input[start] === SpecialSymbols.COMMA) { 74 | return new Comma(start, 1); 75 | } 76 | 77 | return null; 78 | } 79 | } 80 | 81 | export class NewLine extends Token { 82 | static TryParse(input: string, start: number): NewLine | null { 83 | if (start < input.length && (input[start] === SpecialSymbols.NEW_LINE || input[start] === SpecialSymbols.NEW_LINE_WINDOWS)) { 84 | return new NewLine(start, 1); 85 | } 86 | 87 | return null; 88 | } 89 | } 90 | 91 | export class Literal extends Token { 92 | static readonly ALPHA = "^[A-Za-zА-Яа-яЁё\\-_]"; 93 | static readonly ALPHANUMERIC = "^[0-9A-Za-zА-Яа-яЁё\\-_]"; 94 | 95 | value: string; 96 | 97 | constructor(value: string, start: number, length: number) { 98 | super(start, length); 99 | 100 | this.value = value; 101 | } 102 | 103 | static TryParse(input: string, start: number): Literal | null { 104 | if (start < input.length && new RegExp(Literal.ALPHA).test(input.substring(start))) { 105 | const alphaNumeric = new RegExp(Literal.ALPHANUMERIC); 106 | let endPosition = start + 1; 107 | 108 | while (endPosition < input.length && alphaNumeric.test(input.substring(endPosition))) { 109 | alphaNumeric.lastIndex = 0; 110 | endPosition++; 111 | } 112 | 113 | const length = endPosition - start; 114 | return new Literal(input.substring(start, endPosition), start, length); 115 | } 116 | 117 | return null; 118 | } 119 | } 120 | 121 | export class NumberToken extends Token { 122 | static readonly NUMBER = "^[+\\-]?(0x)?[0-9]+"; 123 | value: number; 124 | 125 | constructor(value: number, start: number, length: number) { 126 | super(start, length); 127 | 128 | this.value = value; 129 | } 130 | 131 | static TryParse(input: string, start: number): NumberToken | null { 132 | if (start < input.length) { 133 | const numberRegexp = new RegExp(NumberToken.NUMBER); 134 | const result = input.substring(start).match(numberRegexp); 135 | if (result !== null) { 136 | const length = result[0].length; 137 | const s = input.substring(start, start + length); 138 | let n: number = 0; 139 | if (s.startsWith("0x") || s.startsWith("-0x")) { 140 | n = parseInt(s.substring(s[0] === "-" ? 3 : 2), 16); 141 | } else { 142 | n = parseInt(s, 10); 143 | } 144 | return new NumberToken(n, start, length); 145 | } 146 | } 147 | return null; 148 | } 149 | } 150 | 151 | export function tokenize(input: string): Token[] { 152 | input = String(input); 153 | 154 | const tokens: Token[] = []; 155 | 156 | const parseOrder: ((input: string, start: number) => Token | null)[] = [ 157 | Space.TryParse, 158 | Comment.TryParse, 159 | Colon.TryParse, 160 | Dot.TryParse, 161 | Comma.TryParse, 162 | NewLine.TryParse, 163 | NumberToken.TryParse, 164 | Literal.TryParse 165 | ]; 166 | 167 | let position = 0; 168 | const inputLength = input.length; 169 | 170 | restartParsing: 171 | while (position < inputLength) { 172 | for (const parser of parseOrder) { 173 | const result = parser(input, position); 174 | if (result !== null) { 175 | if (result.length <= 0) throw `Something weird happened, length ${result.length} at test ${result.constructor.name} ${JSON.stringify(result)} : ${input.substring(position, input.indexOf("\n", position) - 1)}`; 176 | tokens.push(result); 177 | position += result.length; 178 | 179 | continue restartParsing; 180 | } 181 | } 182 | 183 | throw `Unable to parse line: ${input.substring(position, input.indexOf("\n", position) - 1)} @ ${position}`; 184 | } 185 | 186 | // TODO: Failsafe 187 | tokens.push(new NewLine(position, 0)); 188 | 189 | return tokens; 190 | } -------------------------------------------------------------------------------- /src/components/simulator/SimulatorSideBar.tsx: -------------------------------------------------------------------------------- 1 | import { TabbedSideBar } from "./TabbedSideBar"; 2 | import dashboard from "src/assets/dashboard.svg"; 3 | import info from "src/assets/info.svg"; 4 | import childArrowLine from "src/assets/child-arrow-line.svg"; 5 | import textLine from "src/assets/text-line.svg"; 6 | import storageLine from "src/assets/storage-line.svg"; 7 | import style from "./SimulatorSideBar.module.css"; 8 | import { useProject } from "../ProjectContext"; 9 | import { RV32_REGISTERS_COUNT, Rv32CPU } from "src/backend/Emulator/Masters/RISCV32"; 10 | import { useEffect, useState } from "preact/hooks"; 11 | import { RAM32 } from "src/backend/Emulator/Devices/RAM32"; 12 | import { ROM32 } from "src/backend/Emulator/Devices/ROM32"; 13 | 14 | function Overview() { 15 | const project = useProject(); 16 | 17 | if (!project?.project?.machine) return (
Loading...
); 18 | 19 | const [registers, setRegisters] = useState(Array(RV32_REGISTERS_COUNT)); 20 | const [bus, setBus] = useState({ address: '-', value: '-', direction: '-' }); 21 | 22 | function formatHex(n: number): string { 23 | return '0x' + Number(n).toString(16).padStart(8, '0'); 24 | } 25 | 26 | function getRegister(n: number): string { 27 | return formatHex((project.project.machine._master as Rv32CPU).getRegister(n)); 28 | } 29 | 30 | useEffect(() => { 31 | let animationFrameId: number; 32 | 33 | const check = () => { 34 | const newRegs = Array(RV32_REGISTERS_COUNT); 35 | for (let i = 0; i < RV32_REGISTERS_COUNT; i++) { 36 | newRegs[i] = getRegister(i); 37 | } 38 | setRegisters(newRegs); 39 | 40 | const newBus = { 41 | direction: (project.project.machine._masterBus.isRead !== null) ? (project.project.machine._masterBus.isRead ? "Read" : "Write") : "-", 42 | address: (project.project.machine._masterBus.address !== null) ? formatHex(project.project.machine._masterBus.address) : "-", 43 | value: (project.project.machine._masterBus.value !== null) ? formatHex(project.project.machine._masterBus.value) : "-" 44 | }; 45 | setBus(newBus); 46 | 47 | animationFrameId = requestAnimationFrame(check); 48 | }; 49 | 50 | animationFrameId = requestAnimationFrame(check); 51 | 52 | return () => cancelAnimationFrame(animationFrameId); 53 | }, []); 54 | 55 | return ( 56 |
57 |
Machine
58 |
59 | 60 | Clock speed 61 | 8kHz 62 | 63 | Machine ID/Version 64 | 243 v1 65 |
66 |
Registers
67 |
68 |
69 | {[...Array(16).keys()].map((r) => <>r{r}{registers[r]})} 70 |
71 |
72 | {[...Array(16).keys()].map((r) => <>r{r + 16}{registers[r + 16]})} 73 |
74 |
75 |
Bus
76 |
77 | 78 | Address 79 | {bus.address} 80 | 81 | 82 | Value 83 | {bus.value} 84 | 85 | 86 | Direction 87 | {bus.direction} 88 |
89 |
90 | ); 91 | } 92 | 93 | function Memory() { 94 | const project = useProject(); 95 | 96 | if (!project?.project?.machine) return (
Loading...
); 97 | 98 | const [memoryDevices, setMemoryDevices] = useState<{ name: string, address: string, contents: Uint32Array }[]>([]); 99 | 100 | function formatHex(n: number): string { 101 | return '0x' + Number(n).toString(16).padStart(8, '0'); 102 | } 103 | 104 | useEffect(() => { 105 | let animationFrameId: number; 106 | 107 | const check = () => { 108 | const memdev: { name: string, address: string, contents: Uint32Array }[] = []; 109 | project.project.machine._masterBus.devices.forEach((device) => { 110 | if (device instanceof RAM32) { 111 | const ram = device as RAM32; 112 | memdev.push({ name: "RAM32", address: formatHex(ram.position), contents: ram.getState() }); 113 | } else if (device instanceof ROM32) { 114 | const rom = device as ROM32; 115 | memdev.push({ name: "ROM32", address: formatHex(rom.position), contents: rom.getState() }); 116 | } 117 | }); 118 | setMemoryDevices(memdev); 119 | 120 | animationFrameId = requestAnimationFrame(check); 121 | }; 122 | 123 | animationFrameId = requestAnimationFrame(check); 124 | 125 | return () => cancelAnimationFrame(animationFrameId); 126 | }, []); 127 | 128 | if (!project?.project?.machine) return (
Loading...
); 129 | 130 | return ( 131 |
132 | {memoryDevices.map((memdev) => <> 133 |
{memdev.name} @ {memdev.address}
134 |
135 | { 136 | (() => { 137 | let arr = []; 138 | for (const value of memdev.contents) { 139 | arr.push({formatHex(value)}); 140 | } 141 | return arr; 142 | })() 143 | } 144 |
145 | )} 146 |
147 | ); 148 | } 149 | 150 | export function SimulatorSideBar() { 151 | return (); 164 | } 165 | -------------------------------------------------------------------------------- /src/backend/Emulator/Bus.ts: -------------------------------------------------------------------------------- 1 | import type { IMachineSerializable, IMachineVisualizable } from "src/backend/Emulator/MachineSerializable"; 2 | import { v4 } from "uuid"; 3 | 4 | export abstract class TickReceiver implements IMachineSerializable, IMachineVisualizable { 5 | // public abstract svelteComponent: any | null; 6 | 7 | public lastTick: number = -1; 8 | public uuid: string = v4(); 9 | public state: SharedArrayBuffer = null; 10 | 11 | protected constructor() { } 12 | 13 | public HasTicked(tick: number) { 14 | return this.lastTick == tick; 15 | } 16 | 17 | public Tick(tick: number): void { 18 | if (!this.HasTicked(tick)) { 19 | this.DeviceTick(tick); 20 | this.lastTick = tick; 21 | } 22 | } 23 | 24 | protected abstract DeviceTick(tick: number): void; 25 | 26 | // TODO: reset 27 | 28 | public abstract getState(): any; 29 | 30 | public abstract serialize(): { name: string; uuid: string; context: any }; 31 | } 32 | 33 | export abstract class Device extends TickReceiver { 34 | public lastIO: number = -1; 35 | protected _response: Data | null = null; 36 | 37 | public HasDoneIO(ioTick: number) { 38 | return this.lastIO == ioTick; 39 | } 40 | 41 | public Read(ioTick: number, address: Address): Data | null { 42 | if (!this.HasDoneIO(ioTick)) { 43 | this._response = this.DeviceRead(ioTick, address); 44 | this.lastIO = ioTick; 45 | } 46 | 47 | return this._response; 48 | } 49 | 50 | public Write(ioTick: number, address: Address, data: Data): void { 51 | if (!this.HasDoneIO(ioTick)) { 52 | this.DeviceWrite(ioTick, address, data); 53 | this.lastIO = ioTick; 54 | } 55 | } 56 | 57 | protected abstract DeviceRead(ioTick: number, address: Address): Data | null; 58 | 59 | protected abstract DeviceWrite(ioTick: number, address: Address, data: Data): void; 60 | 61 | public abstract get info(): { name: string; uuid: string; }; 62 | 63 | public abstract serialize(): { name: string; uuid: string; context: any }; 64 | } 65 | 66 | export interface IBusState { 67 | address: Address | null; 68 | value: Data | null; 69 | isRead: boolean | null; 70 | } 71 | 72 | export class Bus extends Device { 73 | public svelteComponent: any | null = null; 74 | protected _devices: Device[]; 75 | protected _dataView: DataView; 76 | 77 | get isRead(): boolean | null { 78 | const byte = this._dataView.getInt8(5); 79 | if (byte === -1) return null; 80 | 81 | return byte === 1; 82 | } 83 | 84 | private static booleanToByte(value: boolean | null): number { 85 | if (value === null) return -1; 86 | 87 | if (value === true) return 1; 88 | 89 | return 0; 90 | } 91 | 92 | set isRead(value: boolean | null) { 93 | this._dataView.setInt8(5, Bus.booleanToByte(value)); 94 | } 95 | 96 | get address(): Address | null { 97 | return this.isRead !== null ? this._dataView.getUint32(0) as Address : null; 98 | } 99 | 100 | set address(value: Address) { 101 | this._dataView.setUint32(0, value as number); 102 | } 103 | 104 | get value(): Data | null { 105 | return this.isRead !== null ? this._dataView.getUint32(4) as Data : null; 106 | } 107 | 108 | set value(value: Data) { 109 | this._dataView.setUint32(4, value as number); 110 | } 111 | 112 | public get devices() { 113 | return this._devices; 114 | } 115 | 116 | protected init(): void { 117 | this.isRead = null; 118 | this.uuid = v4(); 119 | } 120 | 121 | public static fromContext(devices: Device[]): Bus { 122 | const bus = new Bus(); 123 | bus._devices = devices; 124 | bus.state = new SharedArrayBuffer(4 + 4 + 1); 125 | bus._dataView = new DataView(bus.state); 126 | bus.init(); 127 | return bus; 128 | } 129 | 130 | public static fromBuffer(devices: Device[], uuid: string, state: SharedArrayBuffer): Bus { 131 | const bus = new Bus(); 132 | bus.uuid = uuid; 133 | bus._devices = devices; 134 | bus.state = state; 135 | bus._dataView = new DataView(state); 136 | return bus; 137 | } 138 | 139 | public AddDevice(device: Device): void { 140 | this._devices = [...this._devices, device]; 141 | } 142 | 143 | public DeviceRead(ioTick: number, address: Address): Data | null { 144 | let response: Data | null = null; 145 | let responseDevice: Device | null = null; 146 | for (const device of this._devices) { 147 | let deviceResponse = device.Read(ioTick, address); 148 | if (response == null) { 149 | response = deviceResponse; 150 | responseDevice = device; 151 | } else if (deviceResponse != null) { 152 | // Only one device should write back to the bus 153 | throw `Bus collision with device ${device} (${deviceResponse}) and ${responseDevice} (${response})!`; 154 | } 155 | } 156 | 157 | this.address = address; 158 | this.value = response; 159 | this.isRead = true; 160 | return response; 161 | } 162 | 163 | public DeviceWrite(ioTick: number, address: Address, data: Data): void { 164 | for (const device of this._devices) { 165 | device.Write(ioTick, address, data); 166 | } 167 | 168 | this.address = address; 169 | this.value = data; 170 | this.isRead = false; 171 | } 172 | 173 | public DeviceTick(tick: number): void { 174 | for (const device of this._devices) { 175 | device.Tick(tick); 176 | } 177 | 178 | if (this.lastIO != tick) { 179 | // Then no operations were done on this tick 180 | this.isRead = null; 181 | } 182 | } 183 | 184 | public get info(): { name: string; uuid: string; } { 185 | return { name: "bus", uuid: this.uuid }; 186 | } 187 | 188 | public serialize(): any { 189 | throw "Not implemented"; 190 | 191 | //return {name: "bus", context: undefined}; 192 | } 193 | 194 | public getState(): IBusState { 195 | return { isRead: this.isRead, address: this.address, value: this.value }; 196 | } 197 | } 198 | 199 | export abstract class Master extends TickReceiver { 200 | public lastIO: number = -1; 201 | public bus: Bus; 202 | 203 | public HasDoneIO(ioTick: number) { 204 | return this.lastIO == ioTick; 205 | } 206 | 207 | public DoIO(ioTick: number): void { 208 | if (!this.HasDoneIO(ioTick)) { 209 | this.MasterIO(ioTick); 210 | this.lastIO = ioTick; 211 | } 212 | } 213 | 214 | protected abstract MasterIO(ioTick: number): void; 215 | 216 | public abstract get info(): { name: string; uuid: string; }; 217 | 218 | public abstract serialize(): any; 219 | } 220 | 221 | export interface IMasterConstructor { 222 | fromContext: ((context: any, uuid?: string) => Master); 223 | fromBuffer: ((state: SharedArrayBuffer, uuid: string) => Master); 224 | } 225 | 226 | export interface IDeviceConstructor { 227 | fromContext: ((context: any, uuid?: string) => Device); 228 | fromBuffer: ((state: SharedArrayBuffer, uuid: string) => Device); 229 | } 230 | 231 | export const MasterBusDeviceRegistry = new Map(); 232 | export const MasterBusMasterRegistry = new Map(); -------------------------------------------------------------------------------- /src/backend/Assembler/Instructions.ts: -------------------------------------------------------------------------------- 1 | // RISC-V Spec v2.2, RV32I Base Instruction Set (page 104) 2 | export enum Rv32OpCodes { 3 | LUI = 0b0110111, 4 | AUIPC = 0b0010111, 5 | JAL = 0b1101111, 6 | JALR = 0b1100111, 7 | // BEQ = 0b1100011, 8 | // BNE = 0b1100011, 9 | // BLT = 0b1100011, 10 | // BGE = 0b1100011, 11 | // BLTU = 0b1100011, 12 | // BGEU = 0b1100011, 13 | BRANCH = 0b1100011, 14 | // LB = 0b0000011, 15 | // LH = 0b0000011, 16 | // LW = 0b0000011, 17 | // LBU = 0b0000011, 18 | // LHU = 0b0000011, 19 | LOAD = 0b0000011, 20 | // SB = 0b0100011, 21 | // SH = 0b0100011, 22 | // SW = 0b0100011, 23 | STORE = 0b0100011, 24 | // ADDI = 0b0010011, 25 | // SLTI = 0b0010011, 26 | // SLTIU = 0b0010011, 27 | // XORI = 0b0010011, 28 | // ORI = 0b0010011, 29 | // ANDI = 0b0010011, 30 | // SLLI = 0b0010011, 31 | // SRLI = 0b0010011, 32 | // SRAI = 0b0010011, 33 | OP_IMM = 0b0010011, 34 | // ADD = 0b0110011, 35 | // SUB = 0b0110011, 36 | // SLL = 0b0110011, 37 | // SLT = 0b0110011, 38 | // SLTU = 0b0110011, 39 | // XOR = 0b0110011, 40 | // SRL = 0b0110011, 41 | // SRA = 0b0110011, 42 | // OR = 0b0110011, 43 | // AND = 0b0110011, 44 | OP = 0b0110011, 45 | // FENCE = 0b0001111, 46 | // FENCE_I = 0b0001111, 47 | MISC_MEM = 0b0001111, 48 | // ECALL = 0b1110011, 49 | // EBREAK = 0b1110011, 50 | // CSRRW = 0b1110011, 51 | // CSRRS = 0b1110011, 52 | // CSRRC = 0b1110011, 53 | // CSRRWI = 0b1110011, 54 | // CSRRSI = 0b1110011, 55 | // CSRRCI = 0b1110011, 56 | SYSTEM = 0b1110011, 57 | } 58 | 59 | export enum Rv32Funct3OpImm { 60 | ADDI = 0b000, 61 | SLTI = 0b010, 62 | SLTIU = 0b011, 63 | XORI = 0b100, 64 | ORI = 0b110, 65 | ANDI = 0b111, 66 | SLLI = 0b001, 67 | SRLI = 0b101, 68 | SRAI = 0b101, 69 | } 70 | 71 | export enum Rv32Funct3Op { 72 | ADDSUB = 0b000, 73 | SLL = 0b001, 74 | SLT = 0b010, 75 | SLTU = 0b011, 76 | XOR = 0b100, 77 | SRLSRA = 0b101, 78 | OR = 0b110, 79 | AND = 0b111, 80 | } 81 | 82 | export enum Rv32Funct3Store { 83 | // 1 byte 84 | BYTE = 0b000, 85 | // 2 bytes 86 | HALF = 0b001, 87 | // 4 bytes 88 | WORD = 0b010 89 | } 90 | 91 | export enum Rv32Funct7Op { 92 | ADD = 0b0000000, 93 | SUB = 0b0100000, 94 | } 95 | 96 | export interface IRv32RInstruction { 97 | opcode: Rv32OpCodes; 98 | rd: number; 99 | funct3: number; 100 | rs1: number; 101 | rs2: number; 102 | funct7: number; 103 | } 104 | 105 | export interface IRv32IInstruction { 106 | opcode: Rv32OpCodes; 107 | rd: number; 108 | funct3: number; 109 | rs1: number; 110 | imm: number; 111 | } 112 | 113 | export interface IRv32SInstruction { 114 | opcode: Rv32OpCodes; 115 | funct3: number; 116 | rs1: number; 117 | rs2: number; 118 | imm: number; 119 | } 120 | 121 | export interface IRv32JInstruction { 122 | opcode: Rv32OpCodes; 123 | rd: number; 124 | imm: number; 125 | } 126 | 127 | export type IRv32Instruction = IRv32RInstruction | IRv32IInstruction | IRv32SInstruction | IRv32JInstruction; 128 | 129 | export function VerifyOpcode(opcode: number) { 130 | console.assert(opcode < 128); 131 | // 32-bit instruction's first 5 bits: bbb11 (where bbb != 111) (page 6) 132 | console.assert((opcode & 0b11100) != 0b11100); 133 | } 134 | 135 | export function VerifyRInstruction(i: IRv32RInstruction) { 136 | VerifyOpcode(i.opcode); 137 | 138 | console.assert(i.rd < 32); 139 | console.assert(i.funct3 < 8); 140 | console.assert(i.rs1 < 32); 141 | console.assert(i.rs2 < 32); 142 | console.assert(i.funct7 < 128); 143 | } 144 | 145 | export function VerifyIInstruction(i: IRv32IInstruction) { 146 | VerifyOpcode(i.opcode); 147 | 148 | console.assert(i.rd < 32); 149 | console.assert(i.funct3 < 8); 150 | console.assert(i.rs1 < 32); 151 | console.assert(i.imm < 4096); 152 | } 153 | 154 | export function VerifySInstruction(i: IRv32SInstruction) { 155 | VerifyOpcode(i.opcode); 156 | 157 | console.assert(i.funct3 < 8); 158 | console.assert(i.rs1 < 32); 159 | console.assert(i.rs2 < 32); 160 | console.assert(i.imm < 4096); 161 | } 162 | 163 | export function VerifyJInstruction(i: IRv32JInstruction) { 164 | VerifyOpcode(i.opcode); 165 | 166 | console.assert(i.rd < 32); 167 | console.assert(i.imm < 1048576); 168 | } 169 | 170 | export function MakeRInstruction(i: IRv32RInstruction): number { 171 | VerifyRInstruction(i); 172 | 173 | return ( 174 | i.opcode 175 | | i.rd << 7 176 | | i.funct3 << (7 + 5) 177 | | i.rs1 << (7 + 5 + 3) 178 | | i.rs2 << (7 + 5 + 3 + 5) 179 | | i.funct7 << (7 + 5 + 3 + 5 + 5) 180 | ); 181 | } 182 | 183 | export function MakeIInstruction(i: IRv32IInstruction): number { 184 | VerifyIInstruction(i); 185 | 186 | return ( 187 | i.opcode 188 | | i.rd << 7 189 | | i.funct3 << (7 + 5) 190 | | i.rs1 << (7 + 5 + 3) 191 | | i.imm << (7 + 5 + 3 + 5) 192 | ); 193 | } 194 | 195 | export function MakeSInstruction(i: IRv32SInstruction): number { 196 | VerifySInstruction(i); 197 | 198 | return ( 199 | i.opcode 200 | | (i.imm & 0b1_1111) << 7 201 | | i.funct3 << (7 + 5) 202 | | i.rs1 << (7 + 5 + 3) 203 | | i.rs2 << (7 + 5 + 3 + 5) 204 | | ((i.imm >> 5) & 0b111_1111) << (7 + 5 + 3 + 5 + 5) 205 | ); 206 | } 207 | 208 | export function MakeJInstruction(i: IRv32JInstruction): number { 209 | VerifyJInstruction(i); 210 | 211 | return ( 212 | i.opcode 213 | | i.rd << 7 214 | // imm[19|9:0|10|18:11] 215 | | ((i.imm >> 11) & 0b1111_1111) << (7 + 5) 216 | | ((i.imm >> 10) & 0b1) << (7 + 5 + 8) 217 | | (i.imm & 0b11_1111_1111) << (7 + 5 + 8 + 1) 218 | | ((i.imm >> 19) & 0b1) << (7 + 5 + 8 + 1 + 10) 219 | ); 220 | } 221 | 222 | export function MakeADD(firstRegister: number, secondRegister: number, destinationRegister: number): number { 223 | return MakeRInstruction({ 224 | opcode: Rv32OpCodes.OP, 225 | rd: destinationRegister, 226 | funct3: Rv32Funct3Op.ADDSUB, 227 | rs1: firstRegister, 228 | rs2: secondRegister, 229 | funct7: Rv32Funct7Op.ADD 230 | }); 231 | } 232 | 233 | export function MakeSUB(firstRegister: number, secondRegister: number, destinationRegister: number): number { 234 | return MakeRInstruction({ 235 | opcode: Rv32OpCodes.OP, 236 | rd: destinationRegister, 237 | funct3: Rv32Funct3Op.ADDSUB, 238 | rs1: firstRegister, 239 | rs2: secondRegister, 240 | funct7: Rv32Funct7Op.SUB 241 | }); 242 | } 243 | 244 | export function MakeADDI(destinationRegister: number, sourceRegister: number, immediate: number): number { 245 | return MakeIInstruction({ 246 | opcode: Rv32OpCodes.OP_IMM, 247 | rd: destinationRegister, 248 | funct3: Rv32Funct3OpImm.ADDI, 249 | rs1: sourceRegister, 250 | imm: immediate 251 | }); 252 | } 253 | 254 | // A canonical NOP instruction 255 | export function MakeNOP(): number { 256 | return MakeADDI(0, 0, 0); 257 | } 258 | 259 | export function MakeSW(addressRegister: number, valueRegister: number, offset: number): number { 260 | return MakeSInstruction({ 261 | opcode: Rv32OpCodes.STORE, 262 | funct3: Rv32Funct3Store.WORD, 263 | rs1: addressRegister, 264 | rs2: valueRegister, 265 | imm: offset 266 | }); 267 | } 268 | 269 | export function MakeJAL(destinationRegister: number, offset: number): number { 270 | return MakeJInstruction({ 271 | opcode: Rv32OpCodes.JAL, 272 | rd: destinationRegister, 273 | imm: offset 274 | }); 275 | } 276 | 277 | export function MakeJ(offset: number): number { 278 | return MakeJAL(0, offset); 279 | } -------------------------------------------------------------------------------- /src/backend/Assembler/Compiler.ts: -------------------------------------------------------------------------------- 1 | import { IRv32Instruction, MakeADDI, MakeJAL } from "./Instructions"; 2 | import { NewLine, Comment, Token, Dot, Literal, Space, NumberToken, Colon, Comma } from "./Tokenizer"; 3 | 4 | function skipStuff(tokens: Token[]) { 5 | while (tokens.length > 0) { 6 | const currentToken = tokens[0]; 7 | if (!(currentToken instanceof Comment) && !(currentToken instanceof NewLine) && !(currentToken instanceof Space)) 8 | break; 9 | tokens.shift(); 10 | } 11 | } 12 | 13 | function readDirective(tokens: Token[]): { type: "offset", number: number } | null { 14 | if (tokens.length < 2) return null; 15 | if (!(tokens[0] instanceof Dot)) return null; 16 | if (!(tokens[1] instanceof Literal)) return null; 17 | 18 | tokens.shift(); 19 | 20 | const literalName = tokens.shift() as Literal; 21 | switch (literalName.value) { 22 | case "offset": 23 | { 24 | if (!(tokens.shift() instanceof Space)) return null; 25 | const offset = tokens.shift(); 26 | if (!(offset instanceof NumberToken)) return null; 27 | return { type: "offset", number: (offset as NumberToken).value }; 28 | } 29 | break; 30 | 31 | default: 32 | throw `Unknown directive: ${literalName}`; 33 | } 34 | } 35 | 36 | function readLabel(tokens: Token[]): string | null { 37 | if (tokens.length < 2) return null; 38 | if (!(tokens[0] instanceof Literal)) return null; 39 | if (!(tokens[1] instanceof Colon)) return null; 40 | 41 | const label = tokens.shift() as Literal; 42 | tokens.shift(); 43 | 44 | return label.value; 45 | } 46 | 47 | const AbiRegisterNames = new Map([ 48 | ["zero", 0], 49 | ["ra", 1], 50 | ["rs", 2], 51 | ["gp", 3], 52 | ["tp", 4], 53 | ["t0", 5], 54 | ["t1", 6], 55 | ["t2", 7], 56 | ["s0", 8], 57 | ["fp", 8], 58 | ["s1", 9], 59 | ["a0", 10], 60 | ["a1", 11], 61 | ["a2", 12], 62 | ["a3", 13], 63 | ["a4", 14], 64 | ["a5", 15], 65 | ["a6", 16], 66 | ["a7", 17], 67 | ["s2", 18], 68 | ["s3", 19], 69 | ["s4", 20], 70 | ["s5", 21], 71 | ["s6", 22], 72 | ["s7", 23], 73 | ["s8", 24], 74 | ["s9", 25], 75 | ["s10", 26], 76 | ["s11", 27], 77 | ["t3", 28], 78 | ["t4", 29], 79 | ["t5", 30], 80 | ["t6", 31] 81 | ]); 82 | 83 | function readRegister(tokens: Token[]): number | null { 84 | if (tokens.length < 1) return null; 85 | if (!(tokens[0] instanceof Literal)) return null; 86 | 87 | const registerName = (tokens.shift() as Literal).value.toLowerCase(); 88 | if (registerName.startsWith("x")) { 89 | const n = parseInt(registerName.substring(1)); 90 | if (Number.isNaN(n) || n < 0 || n > 31) return null; 91 | 92 | return n; 93 | } 94 | 95 | if (AbiRegisterNames.has(registerName)) return AbiRegisterNames.get(registerName); 96 | 97 | return null; 98 | } 99 | 100 | function readNumber(tokens: Token[]): number | null { 101 | if (tokens.length < 1) return null; 102 | if (!(tokens[0] instanceof NumberToken)) return null; 103 | 104 | return (tokens.shift() as NumberToken).value; 105 | } 106 | 107 | function readComma(tokens: Token[]): boolean | null { 108 | if (tokens.length < 1) return null; 109 | if (!(tokens[0] instanceof Comma)) return null; 110 | 111 | tokens.shift(); 112 | return true; 113 | } 114 | 115 | function readLabelOrNumber(tokens: Token[]): string | number | null { 116 | if (tokens.length < 1) return null; 117 | if (tokens[0] instanceof NumberToken) return (tokens.shift() as NumberToken).value; 118 | if (tokens[0] instanceof Literal) return (tokens.shift() as Literal).value; 119 | 120 | return null; 121 | } 122 | 123 | const InstructionTable = new Map any)[]>([ 124 | ["addi", [readRegister, readRegister, readNumber]], 125 | ["ret", []], 126 | ["jal", [readRegister, readLabelOrNumber]] 127 | ]); 128 | 129 | function readInstruction(tokens: Token[]): any[] | null { 130 | if (tokens.length < 1) return null; 131 | if (!(tokens[0] instanceof Literal)) return null; 132 | const instructionName = (tokens.shift() as Literal).value.toLowerCase(); 133 | if (!InstructionTable.has(instructionName)) throw `Invalid instruction: ${instructionName}`; 134 | 135 | const argumentsToGet = [...InstructionTable.get(instructionName)]; 136 | const argumentCount = argumentsToGet.length; 137 | const instructionArguments: any[] = []; 138 | while (argumentsToGet.length > 0) { 139 | skipStuff(tokens); 140 | if (argumentsToGet.length !== argumentCount && !readComma(tokens)) { 141 | throw `Didn't find a comma for instruction ${instructionName}`; 142 | } 143 | skipStuff(tokens); 144 | 145 | const f = argumentsToGet.shift(); 146 | const val = f(tokens); 147 | if (val === null) throw `Cannot read argument ${f} of instruction ${instructionName}`; 148 | 149 | instructionArguments.push(val); 150 | } 151 | 152 | return [instructionName, ...instructionArguments]; 153 | } 154 | 155 | function doAssembly(tokens: Token[], result: ArrayBuffer, labelPositions: Map, dryRun: boolean = false) { 156 | const dv = new DataView(result); 157 | 158 | // Offset of instructions, always multiple of 4 159 | let currentOffset = 0; 160 | let resultOffset = 0; 161 | while (tokens.length > 0) { 162 | skipStuff(tokens); 163 | 164 | // Get the whole line 165 | const lineTokens = []; 166 | while (tokens.length > 0 && !(tokens[0] instanceof NewLine)) { 167 | lineTokens.push(tokens.shift()); 168 | } 169 | 170 | do { 171 | let directive; 172 | if ((directive = readDirective(lineTokens)), directive !== null) { 173 | console.log(`Change offset to ${directive.number}`); 174 | console.assert(directive.number >= 0); 175 | currentOffset = directive.number; 176 | break; 177 | } 178 | 179 | let labelName: string | null = null; 180 | while ((labelName = readLabel(lineTokens)), labelName !== null) { 181 | console.log(`found label ${labelName} @ ${currentOffset}`); 182 | if (dryRun) { 183 | if (labelPositions.has(labelName)) throw `Label ${labelName} already exists`; 184 | labelPositions.set(labelName, currentOffset); 185 | } 186 | skipStuff(lineTokens); 187 | } 188 | 189 | let instruction: any[] | null = null; 190 | if ((instruction = readInstruction(lineTokens)), instruction !== null) { 191 | console.log(`found instruction: ${instruction}`); 192 | if (!dryRun) result.resize(result.byteLength + 4); 193 | let instructionBytes: number; 194 | switch (instruction[0]) { 195 | case "addi": 196 | { 197 | instructionBytes = MakeADDI(instruction[1], instruction[2], instruction[3]); 198 | } 199 | break; 200 | 201 | case "jal": 202 | { 203 | let jumpOffset = 0; 204 | if (typeof instruction[2] === "number") jumpOffset = instruction[2]; 205 | else if (typeof instruction[2] === "string") { 206 | const labelName = String(instruction[2]); 207 | if (!labelPositions.has(labelName)) throw `Cannot find label ${labelName}`; 208 | 209 | jumpOffset = labelPositions.get(labelName) - currentOffset; 210 | } else { 211 | throw `JAL: unknown argument type ${typeof instruction[2]}`; 212 | } 213 | instructionBytes = MakeJAL(instruction[1], jumpOffset); 214 | } 215 | break; 216 | 217 | default: 218 | throw `Cannot handle instruction ${instruction[0]}`; 219 | } 220 | if (!dryRun) dv.setUint32(resultOffset, instructionBytes, true); 221 | resultOffset += 4; 222 | currentOffset += 4; 223 | } 224 | } while (false); 225 | 226 | skipStuff(lineTokens); 227 | 228 | if (lineTokens.length > 0) { 229 | throw `No tokens were consumed on this round: ${JSON.stringify(lineTokens)}`; 230 | } 231 | } 232 | } 233 | 234 | export function assemble(tokens: Token[]): Uint8Array { 235 | const result = new ArrayBuffer(0, { maxByteLength: 256 }); 236 | 237 | let labelPositions = new Map(); 238 | // Dry run to mark all the labels 239 | doAssembly([...tokens], result, labelPositions, true); 240 | doAssembly([...tokens], result, labelPositions); 241 | 242 | return new Uint8Array(result); 243 | } -------------------------------------------------------------------------------- /src/backend/FileSystem.ts: -------------------------------------------------------------------------------- 1 | export const PATH_SEPARATOR: string = "/"; 2 | const PATH_PREFIX: string = "file:"; 3 | 4 | export async function getFile(path: string, root: FileSystemDirectoryHandle, create?: boolean): Promise { 5 | const crumbs = path.split(PATH_SEPARATOR); 6 | if (path === "" || crumbs.length < 1 || crumbs.at(-1) == "") { 7 | return null; 8 | } 9 | 10 | let currentDir = root; 11 | 12 | // The last crumb is the file name 13 | while (crumbs.length > 1) { 14 | const element = crumbs.shift(); 15 | try { 16 | currentDir = await currentDir.getDirectoryHandle(element); 17 | } catch (ex) { 18 | console.error(`Cannot find directory ${element} (${ex})`); 19 | return null; 20 | } 21 | } 22 | 23 | const filename = crumbs.shift(); 24 | let file: FileSystemFileHandle; 25 | try { 26 | file = await currentDir.getFileHandle(filename, {create: create ?? false}); 27 | } catch (ex) { 28 | console.error(`Cannot find file ${filename} (${ex})`); 29 | return null; 30 | } 31 | 32 | return file; 33 | } 34 | 35 | export function combinePath(crumbs: string[]): string { 36 | return crumbs.join(PATH_SEPARATOR); 37 | } 38 | 39 | interface IFSNodeJson { 40 | type: string; 41 | name: string; 42 | changed: number; 43 | } 44 | 45 | interface IFSFolderJson extends IFSNodeJson { 46 | children: string[]; 47 | } 48 | 49 | interface IFSFileJson extends IFSNodeJson { 50 | contentsUri: string; 51 | } 52 | 53 | export class FsNodeUpdated extends Event { 54 | public node: FSNode; 55 | 56 | constructor(node: FSNode) { 57 | super('fsnode-updated'); 58 | this.node = node; 59 | } 60 | } 61 | 62 | export class FsNodeDeleted extends Event { 63 | public node: FSNode; 64 | 65 | constructor(node: FSNode) { 66 | super('fsnode-deleted'); 67 | this.node = node; 68 | } 69 | } 70 | 71 | export abstract class FSNode extends EventTarget { 72 | public name: string; 73 | protected changed: number; 74 | public parent: FSFolder | undefined; // TODO: hide it. I tried setting it to protected but the TS has a problem with that 75 | private valid: boolean = true; 76 | 77 | protected constructor(name: string, changed?: number, parent?: FSFolder) { 78 | super(); 79 | 80 | this.name = name; 81 | this.changed = changed ?? Date.now(); 82 | this.parent = parent; 83 | } 84 | 85 | private AssertValid(): void { 86 | if (!this.valid) 87 | throw 'Invalid file'; 88 | } 89 | 90 | public Touch(notify: boolean = true): void { 91 | this.AssertValid(); 92 | 93 | this.changed = Date.now(); 94 | 95 | // TODO: should the touch events bubble up? 96 | // if (this.parent !== undefined) 97 | // this.parent.Touch(notify); 98 | 99 | if (notify) 100 | this.dispatchEvent(new FsNodeUpdated(this)); 101 | } 102 | 103 | private getParentChain(): FSFolder[] { 104 | let chain: FSFolder[] = new Array(); 105 | 106 | let currentParent: FSFolder | undefined = this.parent; 107 | while (currentParent !== undefined) { 108 | chain = [currentParent, ...chain]; 109 | currentParent = currentParent.parent; 110 | } 111 | 112 | return chain; 113 | } 114 | 115 | public FullPath(): string { 116 | return PATH_PREFIX + [...this.getParentChain(), this].map((f) => f.name).join(PATH_SEPARATOR); 117 | } 118 | 119 | public async Save(): Promise { 120 | this.AssertValid(); 121 | 122 | let context: string[] = this.getParentChain().map((f) => f.name); 123 | 124 | return this.SaveWithContext(context); 125 | } 126 | 127 | public Delete(): void { 128 | if (this.parent !== undefined) { 129 | const i = this.parent.children.indexOf(this); 130 | if (i != -1) 131 | delete this.parent.children[i]; 132 | } 133 | 134 | this.valid = false; 135 | this.dispatchEvent(new FsNodeDeleted(this)); 136 | this.parent?.dispatchEvent(new FsNodeUpdated(this.parent)); 137 | } 138 | 139 | public abstract SaveWithContext(context: string[]): Promise; 140 | } 141 | 142 | export class FSFolder extends FSNode { 143 | protected _children: FSNode[]; 144 | 145 | constructor(name: string, changed?: number, parent?: FSFolder, children?: FSNode[]) { 146 | super(name, changed, parent); 147 | 148 | this._children = children ?? new Array(); 149 | for (let child of this._children) { 150 | child.parent = this; 151 | } 152 | } 153 | 154 | public get children() { 155 | return this._children; 156 | } 157 | 158 | public NewFile(name: string): FSFile { 159 | const file = new FSFile(name); 160 | file.parent = this; 161 | this._children.push(file); 162 | this.Touch(); 163 | return file; 164 | } 165 | 166 | public NewFolder(name: string): FSFolder { 167 | const folder = new FSFolder(name); 168 | folder.parent = this; 169 | this._children.push(folder); 170 | this.Touch(); 171 | return folder; 172 | } 173 | 174 | public async SaveWithContext(context: string[]): Promise { 175 | let newContext = [...context, this.name]; 176 | let absolutePath = PATH_PREFIX + newContext.join(PATH_SEPARATOR); 177 | 178 | // Updating the JSON for this node 179 | let existingEntryJsonString = localStorage.getItem(absolutePath); 180 | let thisJson: IFSFolderJson = { 181 | type: "folder", 182 | name: this.name, 183 | changed: this.changed, 184 | children: this._children.map((c) => c.name) 185 | }; 186 | let thisJsonString = JSON.stringify(thisJson); 187 | if (existingEntryJsonString != null) { 188 | let existingEntryJson: IFSFolderJson = JSON.parse(existingEntryJsonString); 189 | if (existingEntryJson.changed !== thisJson.changed) 190 | localStorage.setItem(absolutePath, thisJsonString); 191 | } else { 192 | localStorage.setItem(absolutePath, thisJsonString); 193 | } 194 | 195 | for (let child of this._children) { 196 | await child.SaveWithContext(newContext); 197 | } 198 | } 199 | } 200 | 201 | export class FSFile extends FSNode { 202 | private _contents: Uint8Array; 203 | 204 | constructor(name: string, changed?: number | undefined, parent?: FSFolder | undefined, contents?: Uint8Array | undefined) { 205 | super(name, changed, parent); 206 | 207 | this._contents = contents ?? new Uint8Array(0); 208 | } 209 | 210 | public get contents(): Uint8Array { 211 | return this._contents; 212 | } 213 | 214 | /* 215 | * Throws an exception if the file is not a valid UTF-8 text 216 | */ 217 | public get text(): string { 218 | return new TextDecoder(undefined, { fatal: true }).decode(this._contents); 219 | } 220 | 221 | public write(contents: Uint8Array | string, notify: boolean = true): void { 222 | if (typeof contents === "string") 223 | contents = new TextEncoder().encode(contents); 224 | this._contents = contents; 225 | this.Touch(notify); 226 | } 227 | 228 | protected static async bytesToBase64DataUrl(bytes: Uint8Array, type = "application/octet-stream"): Promise { 229 | return await new Promise((resolve, reject) => { 230 | const reader = Object.assign(new FileReader(), { 231 | onload: () => resolve(reader.result?.toString() ?? ""), 232 | onerror: () => reject(reader.error), 233 | }); 234 | reader.readAsDataURL(new File([bytes], "", { type })); 235 | }); 236 | } 237 | 238 | public async SaveWithContext(context: string[]): Promise { 239 | let newContext = [...context, this.name]; 240 | let absolutePath = PATH_PREFIX + newContext.join(PATH_SEPARATOR); 241 | 242 | // Updating the JSON for this node 243 | let existingEntryJsonString = localStorage.getItem(absolutePath); 244 | if (existingEntryJsonString != null) { 245 | let existingEntryJson: IFSNodeJson = JSON.parse(existingEntryJsonString); 246 | if (existingEntryJson.changed === this.changed) 247 | return; 248 | } 249 | 250 | let thisJson: IFSFileJson = { 251 | type: "file", 252 | name: this.name, 253 | changed: this.changed, 254 | contentsUri: await FSFile.bytesToBase64DataUrl(this._contents) 255 | }; 256 | let thisJsonString = JSON.stringify(thisJson); 257 | localStorage.setItem(absolutePath, thisJsonString); 258 | } 259 | } 260 | 261 | async function parseNodesRecursive(path: string = PATH_PREFIX): Promise { 262 | let nodeJson: IFSFolderJson = JSON.parse(localStorage.getItem(path) ?? "{}"); 263 | switch (nodeJson.type) { 264 | case "folder": { 265 | let folderJson: IFSFolderJson = nodeJson as unknown as IFSFolderJson; 266 | 267 | let children: FSNode[] = new Array(); 268 | for (let childName of folderJson.children) { 269 | let newPath = path + PATH_SEPARATOR + childName; 270 | children.push(await parseNodesRecursive(newPath)); 271 | } 272 | 273 | // The folder construct will set the parent of the children 274 | return new FSFolder(folderJson.name, folderJson.changed, undefined, children); 275 | } 276 | 277 | case "file": { 278 | let fileJson: IFSFileJson = nodeJson as unknown as IFSFileJson; 279 | // https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer#comment124033543_49273187 280 | return new FSFile( 281 | fileJson.name, 282 | fileJson.changed, 283 | undefined, 284 | new Uint8Array( 285 | await (await fetch(fileJson.contentsUri)).arrayBuffer() 286 | ) 287 | ); 288 | } 289 | 290 | default: { 291 | throw `Unknown FS node type ${nodeJson.type} at "${path}"`; 292 | } 293 | } 294 | } 295 | 296 | // Get the root folder 297 | export let rootFolder: FSFolder; 298 | 299 | export async function initializeFS(): Promise { 300 | if (true) { 301 | let rf: FSNode = new FSFolder(""); 302 | try { 303 | rf = await parseNodesRecursive(); 304 | } catch (e) { 305 | console.error(e); 306 | } 307 | 308 | if (!(rf instanceof FSFolder)) 309 | throw `Wrong type of the root node (expected ${typeof FSFolder}, got ${typeof rf})`; 310 | 311 | rootFolder = rf as FSFolder; 312 | } 313 | } 314 | 315 | export async function SaveAll(): Promise { 316 | return rootFolder.Save(); 317 | } 318 | 319 | function OpenFile(currentFolder: FSFolder, path: string[], createFile: boolean, createFolders: boolean): FSFile | null { 320 | let nextPathElement = path.shift(); 321 | if (nextPathElement === undefined) 322 | return null; 323 | 324 | // The last element of the path is the file that we need 325 | if (path.length == 0) { 326 | for (let child of currentFolder.children) { 327 | if (child instanceof FSFile && child.name == nextPathElement) 328 | return child; 329 | } 330 | 331 | if (!createFile) 332 | return null; 333 | 334 | return currentFolder.NewFile(nextPathElement); 335 | } 336 | 337 | let nextFolder: FSFolder | null = null; 338 | for (let child of currentFolder.children) { 339 | if (child instanceof FSFolder && child.name == nextPathElement) 340 | nextFolder = child; 341 | } 342 | if (nextFolder == null) { 343 | if (!createFolders || !createFile) 344 | return null; 345 | 346 | nextFolder = currentFolder.NewFolder(nextPathElement); 347 | } 348 | 349 | return OpenFile(nextFolder, path, createFile, createFolders); 350 | } 351 | 352 | export function Open(path: string, createFile: boolean = false, createFolders: boolean = true): FSFile | null { 353 | let pathSplit = path.split(PATH_SEPARATOR); 354 | if (pathSplit.length == 0 || pathSplit[0] != "" || pathSplit[pathSplit.length - 1] == "") 355 | return null; 356 | 357 | pathSplit.shift(); 358 | return OpenFile(rootFolder, pathSplit, createFile, createFolders); 359 | } 360 | 361 | function ExistsNode(currentFolder: FSFolder, path: string[]): boolean { 362 | let nextPathElement = path.shift(); 363 | if (nextPathElement === undefined) 364 | return false; 365 | 366 | // The last element of the path is either a file or a folder 367 | if (path.length == 0) { 368 | for (let child of currentFolder.children) { 369 | if (child.name == nextPathElement) 370 | return true; 371 | } 372 | 373 | return false; 374 | } 375 | 376 | let nextFolder: FSFolder | null = null; 377 | for (let child of currentFolder.children) { 378 | if (child instanceof FSFolder && child.name == nextPathElement) 379 | nextFolder = child; 380 | } 381 | if (nextFolder == null) { 382 | return false; 383 | } 384 | 385 | return ExistsNode(nextFolder, path); 386 | } 387 | 388 | export function Exists(path: string): boolean { 389 | let pathSplit = path.split(PATH_SEPARATOR); 390 | if (pathSplit.length == 0 || pathSplit[0] != "") 391 | return false; 392 | 393 | pathSplit.shift(); 394 | return ExistsNode(rootFolder, pathSplit); 395 | } -------------------------------------------------------------------------------- /src/backend/Emulator/Masters/RISCV32.ts: -------------------------------------------------------------------------------- 1 | import { Master, MasterBusMasterRegistry } from "src/backend/Emulator/Bus"; 2 | import { v4 } from "uuid"; 3 | import { IRv32IInstruction, IRv32Instruction, IRv32JInstruction, IRv32RInstruction, IRv32SInstruction, Rv32Funct3Op, Rv32Funct3OpImm, Rv32Funct3Store, Rv32Funct7Op, Rv32OpCodes, VerifyOpcode } from "src/backend/Assembler/Instructions"; 4 | 5 | const RISCV32_NAME: string = "rv32"; 6 | export const RV32_REGISTERS_COUNT = 32; 7 | 8 | export interface IRv32State { 9 | registers: number[]; 10 | ip: number; 11 | } 12 | 13 | export function DecodeInstruction(instruction: number): IRv32Instruction | undefined { 14 | const opcode: Rv32OpCodes = instruction & 0b111_1111; 15 | VerifyOpcode(opcode); 16 | 17 | switch (opcode) { 18 | case Rv32OpCodes.LOAD: 19 | case Rv32OpCodes.OP_IMM: 20 | return { 21 | opcode: opcode, 22 | rd: (instruction >> 7) & 0b1_1111, 23 | funct3: (instruction >> (7 + 5)) & 0b111, 24 | rs1: (instruction >> (7 + 5 + 3)) & 0b1_1111, 25 | imm: (instruction >> (7 + 5 + 3 + 5)) & 0b1111_1111_1111, 26 | } as IRv32IInstruction; 27 | 28 | case Rv32OpCodes.OP: 29 | return { 30 | opcode: opcode, 31 | rd: (instruction >> 7) & 0b1_1111, 32 | funct3: (instruction >> (7 + 5)) & 0b111, 33 | rs1: (instruction >> (7 + 5 + 3)) & 0b1_1111, 34 | rs2: (instruction >> (7 + 5 + 3 + 5)) & 0b1_1111, 35 | funct7: (instruction >> (7 + 5 + 3 + 5 + 5)) & 0b111_1111, 36 | } as IRv32RInstruction; 37 | 38 | case Rv32OpCodes.STORE: 39 | return { 40 | opcode: opcode, 41 | funct3: (instruction >> (7 + 5)) & 0b111, 42 | rs1: (instruction >> (7 + 5 + 3)) & 0b1_1111, 43 | rs2: (instruction >> (7 + 5 + 3 + 5)) & 0b1_1111, 44 | imm: ((instruction >> 7) & 0b1_1111) | (((instruction >> (7 + 5 + 3 + 5 + 5)) & 0b111_1111) << 5), 45 | } as IRv32SInstruction; 46 | 47 | case Rv32OpCodes.JAL: { 48 | const immShuffled = instruction >> 7; 49 | return { 50 | opcode: opcode, 51 | rd: (instruction >> 7) & 0b1_1111, 52 | // imm[19|9:0|10|18:11] 53 | imm: (((immShuffled >> 19) & 0b1) << (10 + 1 + 8)) 54 | | ((immShuffled & 0b1111_1111) << (10 + 1)) 55 | | (((immShuffled >> 10) & 0b1) << 10) 56 | | ((immShuffled >> 9) & 0b11_1111_1111) 57 | } as IRv32JInstruction; 58 | } 59 | } 60 | 61 | return undefined; 62 | } 63 | 64 | function signExtend(n: number, bits: number): number { 65 | const sign = n >> (bits - 1); 66 | if (sign != 0) { 67 | n |= ~((1 << bits) - 1); 68 | } 69 | return n; 70 | } 71 | 72 | enum Rv32CPUState { 73 | Read, 74 | StoreToReg, 75 | StoreFromReg, 76 | Noop 77 | } 78 | 79 | enum Rv32IOState { 80 | Read, 81 | Write, 82 | Noop 83 | } 84 | 85 | export class Rv32CPU extends Master { 86 | protected _dataView: DataView; 87 | 88 | public getRegister(n: number): number { 89 | if (n === 0) 90 | return 0; 91 | 92 | if (n < 0 || n >= 32) throw "Too many registers"; 93 | 94 | return this._dataView.getUint32(4 + 4 + 4 + 4 + 4 + 4 + n * 4); 95 | } 96 | 97 | protected setRegister(n: number, v: number) { 98 | if (n === 0) 99 | return; 100 | 101 | if (n < 0 || n >= 32) throw "Too many registers"; 102 | 103 | this._dataView.setUint32(4 + 4 + 4 + 4 + 4 + 4 + n * 4, v); 104 | } 105 | 106 | protected get _ip(): number { 107 | return this._dataView.getUint32(0); 108 | } 109 | 110 | protected set _ip(value: number) { 111 | this._dataView.setUint32(0, value); 112 | } 113 | 114 | protected get _state(): Rv32CPUState { 115 | return this._dataView.getUint32(4) as Rv32CPUState; 116 | } 117 | 118 | protected set _state(value: Rv32CPUState) { 119 | this._dataView.setUint32(4, value as number); 120 | } 121 | 122 | protected get _op1(): number { 123 | return this._dataView.getUint32(4 + 4); 124 | } 125 | 126 | protected set _op1(value: number) { 127 | this._dataView.setUint32(4 + 4, value); 128 | } 129 | 130 | protected get _ioState(): Rv32IOState { 131 | return this._dataView.getUint32(4 + 4 + 4) as Rv32IOState; 132 | } 133 | 134 | protected set _ioState(value: Rv32IOState) { 135 | this._dataView.setUint32(4 + 4 + 4, value as number); 136 | } 137 | 138 | protected get _ioOp1(): number { 139 | return this._dataView.getUint32(4 + 4 + 4 + 4); 140 | } 141 | 142 | protected set _ioOp1(value: number) { 143 | this._dataView.setUint32(4 + 4 + 4 + 4, value); 144 | } 145 | 146 | protected get _ioOp2(): number { 147 | return this._dataView.getUint32(4 + 4 + 4 + 4 + 4); 148 | } 149 | 150 | protected set _ioOp2(value: number) { 151 | this._dataView.setUint32(4 + 4 + 4 + 4 + 4, value); 152 | } 153 | 154 | public static fromContext(context: any, uuid?: string): Rv32CPU { 155 | const rv32 = new Rv32CPU(); 156 | 157 | rv32.uuid = uuid ?? v4(); 158 | rv32.state = new SharedArrayBuffer(4 + 4 + 4 + 4 + 4 + 4 + 32 * 4); 159 | rv32._dataView = new DataView(rv32.state); 160 | 161 | rv32.init(); 162 | 163 | return rv32; 164 | } 165 | 166 | public static fromBuffer(state: SharedArrayBuffer, uuid: string): Rv32CPU { 167 | const rv32 = new Rv32CPU(); 168 | 169 | rv32.uuid = uuid; 170 | rv32.state = state; 171 | rv32._dataView = new DataView(rv32.state); 172 | 173 | return rv32; 174 | } 175 | 176 | protected init(): void { 177 | for (let i = 0; i < RV32_REGISTERS_COUNT; i++) 178 | this.setRegister(i, 0); 179 | } 180 | 181 | public get info(): { name: string; uuid: string; } { 182 | return { name: RISCV32_NAME, uuid: this.uuid }; 183 | } 184 | 185 | protected nextInstruction(nextAddress?: number): void { 186 | if (nextAddress === undefined) 187 | this._ip += 4; 188 | else 189 | this._ip = nextAddress; 190 | 191 | this._ioOp1 = this._ip; 192 | this._ioState = Rv32IOState.Read; 193 | this._state = Rv32CPUState.Read; 194 | } 195 | 196 | protected DeviceTick(tick: number): void { 197 | console.log("rv32: tick"); 198 | switch (this._state) { 199 | case Rv32CPUState.Read: 200 | const instruction = DecodeInstruction(this._ioOp2); 201 | if (instruction === undefined) { 202 | console.error(`rv32: invalid instruction ${this._ioOp2}`); 203 | return; 204 | } 205 | console.log(instruction); 206 | 207 | switch (instruction.opcode) { 208 | case Rv32OpCodes.OP_IMM: { 209 | const op_imm = instruction as IRv32IInstruction; 210 | switch (op_imm.funct3) { 211 | case Rv32Funct3OpImm.ADDI: 212 | { 213 | const immExt = signExtend(op_imm.imm, 12); 214 | console.log(`rv32: ADDI r${op_imm.rd} = r${op_imm.rs1} + 0x${immExt.toString(16)}`); 215 | this.setRegister(op_imm.rd, this.getRegister(op_imm.rs1) + immExt); 216 | } 217 | break; 218 | 219 | default: 220 | console.error(`rv32: unknown I funct3 ${op_imm.funct3}`); 221 | } 222 | 223 | this.nextInstruction(); 224 | break; 225 | } 226 | 227 | case Rv32OpCodes.OP: { 228 | const op = instruction as IRv32RInstruction; 229 | switch (op.funct3) { 230 | case Rv32Funct3Op.ADDSUB: { 231 | switch (op.funct7) { 232 | case Rv32Funct7Op.ADD: 233 | console.log(`rv32: ADD r${op.rd} = r${op.rs1} + r${op.rs2}`); 234 | this.setRegister(op.rd, this.getRegister(op.rs1) + this.getRegister(op.rs2)); 235 | break; 236 | 237 | case Rv32Funct7Op.SUB: 238 | console.log(`rv32: SUB r${op.rd} = r${op.rs1} - r${op.rs2}`); 239 | this.setRegister(op.rd, this.getRegister(op.rs1) - this.getRegister(op.rs2)); 240 | break; 241 | 242 | default: 243 | console.error(`rv32: unknown R funct7 ${op.funct7}`); 244 | break; 245 | } 246 | break; 247 | } 248 | 249 | default: 250 | console.error(`rv32: unknown R funct3 ${op.funct3}`); 251 | break; 252 | } 253 | 254 | this.nextInstruction(); 255 | break; 256 | } 257 | 258 | case Rv32OpCodes.LOAD: 259 | console.error(`not implemented ${instruction.opcode}`); 260 | break; 261 | 262 | case Rv32OpCodes.STORE: { 263 | const store = instruction as IRv32SInstruction; 264 | const offset = signExtend(store.imm, 12); 265 | switch (store.funct3) { 266 | case Rv32Funct3Store.WORD: 267 | console.log(`rv32: SW m[r${store.rs1} + 0x${offset.toString(16)}] = r${store.rs2}`); 268 | this._ioState = Rv32IOState.Write; 269 | this._ioOp1 = this.getRegister(store.rs1) + offset; 270 | this._ioOp2 = this.getRegister(store.rs2); 271 | this._state = Rv32CPUState.Noop; 272 | break; 273 | 274 | default: 275 | console.error(`rv32: unknown S funct3 ${store.funct3}`); 276 | } 277 | break; 278 | } 279 | 280 | case Rv32OpCodes.JAL: { 281 | const jal = instruction as IRv32JInstruction; 282 | const offset = signExtend(jal.imm, 20); 283 | console.log(`rv32: JAL r${jal.rd} = 0x${(this._ip + 4).toString(16)}`); 284 | this.setRegister(jal.rd, this._ip + 4); 285 | console.log(`rv32: JAL ip = 0x${(this._ip + offset).toString(16)}`); 286 | this.nextInstruction(this._ip + offset); 287 | break; 288 | } 289 | 290 | default: 291 | console.error(`unknown opcode ${instruction.opcode}`); 292 | break; 293 | } 294 | break; 295 | 296 | case Rv32CPUState.StoreToReg: 297 | console.error(`not implemented ${Rv32CPUState[this._state]}`); 298 | break; 299 | 300 | case Rv32CPUState.StoreFromReg: 301 | this.nextInstruction(); 302 | break; 303 | 304 | case Rv32CPUState.Noop: 305 | break; 306 | 307 | default: 308 | console.error(`rv32: unhandled state ${Rv32CPUState[this._state]}`); 309 | break; 310 | } 311 | } 312 | 313 | protected MasterIO(ioTick: number): void { 314 | switch (this._ioState) { 315 | case Rv32IOState.Read: { 316 | const read = this.bus.Read(ioTick, this._ioOp1); 317 | if (read == null) { 318 | console.error(`rv32: no response @ 0x${this._ioOp1.toString(16).padStart(8, "0")}`); 319 | this._ioOp2 = 0; 320 | break; 321 | } 322 | this._ioOp2 = read; 323 | this._ioState = Rv32IOState.Noop; 324 | 325 | break; 326 | } 327 | 328 | case Rv32IOState.Write: { 329 | if (this._ioOp1 % 4 == 0) { 330 | this.bus.Write(ioTick, this._ioOp1, this._ioOp2); 331 | } else { 332 | console.error(`rv32: not implemented unaligned access @ 0x${this._ioOp1.toString(16).padStart(8, "0")}`); 333 | } 334 | this._ioState = Rv32IOState.Noop; 335 | this._state = Rv32CPUState.StoreFromReg; 336 | break; 337 | } 338 | 339 | case Rv32IOState.Noop: 340 | break; 341 | 342 | default: 343 | console.error(`rv32: unhandled io state ${Rv32IOState[this._ioState]}`); 344 | } 345 | } 346 | 347 | public serialize(): { name: string; uuid: string; context: any } { 348 | return { name: RISCV32_NAME, uuid: this.uuid, context: undefined }; 349 | } 350 | 351 | public getState(): IRv32State { 352 | return { 353 | registers: [...Array(RV32_REGISTERS_COUNT).keys()].map((i) => this.getRegister(i)), 354 | ip: this._ip 355 | } 356 | } 357 | } 358 | 359 | MasterBusMasterRegistry.set(RISCV32_NAME, { 360 | fromContext(context: any, uuid: string = v4()) { 361 | return Rv32CPU.fromContext(context, uuid); 362 | }, 363 | fromBuffer(state, uuid) { 364 | return Rv32CPU.fromBuffer(state, uuid); 365 | }, 366 | }); --------------------------------------------------------------------------------