├── .editorconfig ├── .eslintrc.cjs ├── .github └── workflows │ ├── deploy.yml │ ├── lint.yml │ └── playwright.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── README.md ├── components.json ├── index.html ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── public └── favicon.ico ├── src ├── App.css ├── App.tsx ├── assets │ ├── arrow.png │ ├── brand.svg │ ├── github-mark-white.svg │ ├── logo.png │ ├── logo.svg │ ├── sidebar │ │ ├── Group 16.svg │ │ ├── brick.svg │ │ ├── chip.svg │ │ ├── computers.svg │ │ ├── debugger.svg │ │ ├── logo.svg │ │ └── stack.svg │ └── tool-name.svg ├── components │ ├── DebuggerControlls │ │ └── index.tsx │ ├── DebuggerSettings │ │ ├── Content.tsx │ │ └── index.tsx │ ├── DiffChecker.tsx │ ├── ErrorWarningTooltip │ │ └── index.tsx │ ├── Header │ │ └── index.tsx │ ├── HostCalls │ │ ├── HostCallsContent.tsx │ │ ├── index.tsx │ │ └── trie-input │ │ │ └── index.tsx │ ├── InitialLoadProgramCTA │ │ └── index.tsx │ ├── Instructions │ │ ├── InstructionItem.tsx │ │ ├── InstructionsTable.tsx │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils.tsx │ ├── KnowledgeBase │ │ ├── Mobile.tsx │ │ └── index.tsx │ ├── LoadingSpinner │ │ └── index.tsx │ ├── MemoryPreview │ │ ├── MemoryInfinite.tsx │ │ ├── MemoryRanges.tsx │ │ ├── MemoryRangesEmptyRow.tsx │ │ ├── MemoryRangesRow.tsx │ │ ├── index.tsx │ │ └── utils.ts │ ├── MobileDebuggerControlls │ │ ├── ControllsDrawer.tsx │ │ └── index.tsx │ ├── NumeralSystemSwitch │ │ └── index.tsx │ ├── ProgramEdit │ │ └── index.tsx │ ├── ProgramLoader │ │ ├── Assembly.tsx │ │ ├── Examples.tsx │ │ ├── Links.tsx │ │ ├── Loader.tsx │ │ ├── ProgramFileUpload.tsx │ │ ├── examplePrograms.ts │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils.ts │ ├── ProgramTextLoader │ │ └── index.tsx │ ├── PvmSelect │ │ └── index.tsx │ ├── Registers │ │ └── index.tsx │ ├── SearchInput │ │ └── index.tsx │ ├── WithHelp │ │ └── WithHelp.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── badge.tsx │ │ ├── button-variants.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── multi-select.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── context │ ├── NumeralSystem.tsx │ ├── NumeralSystemContext.tsx │ └── NumeralSystemProvider.tsx ├── globals.css ├── hooks │ └── useDebuggerActions.ts ├── index.css ├── index.d.ts ├── lib │ └── utils.ts ├── main.tsx ├── packages │ ├── host-calls │ │ ├── read.ts │ │ └── write.ts │ ├── pvm │ │ ├── jam-codec │ │ │ ├── decode-natural-number.test.ts │ │ │ ├── decode-natural-number.ts │ │ │ ├── index.ts │ │ │ ├── little-endian-decoder.test.ts │ │ │ ├── little-endian-decoder.ts │ │ │ └── package.json │ │ ├── pvm │ │ │ ├── args-decoder.ts │ │ │ ├── assemblify.ts │ │ │ ├── disassemblify.ts │ │ │ └── instruction.ts │ │ └── utils │ │ │ ├── debug.ts │ │ │ ├── index.ts │ │ │ └── opaque.ts │ ├── ui-kit │ │ ├── AppsSidebar │ │ │ ├── icons │ │ │ │ ├── Brick.tsx │ │ │ │ ├── Chip.tsx │ │ │ │ ├── Computers.tsx │ │ │ │ ├── Debugger.tsx │ │ │ │ ├── Logo.tsx │ │ │ │ └── Stack.tsx │ │ │ └── index.tsx │ │ ├── DarkMode │ │ │ ├── ToggleDarkMode.tsx │ │ │ └── utils.ts │ │ └── Header │ │ │ └── index.tsx │ └── web-worker │ │ ├── command-handlers │ │ ├── host-call.ts │ │ ├── index.ts │ │ ├── init.ts │ │ ├── load.ts │ │ ├── memory.ts │ │ └── step.ts │ │ ├── goWasmExec.d.ts │ │ ├── goWasmExec.js │ │ ├── pvm.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── wasmAsInit.ts │ │ ├── wasmAsShell.ts │ │ ├── wasmBindgenInit.ts │ │ ├── wasmBindgenShell.ts │ │ ├── wasmFromWebsockets.ts │ │ ├── wasmGoInit.ts │ │ ├── wasmGoShell.ts │ │ └── worker.ts ├── pages │ ├── DebuggerContent.tsx │ └── ProgramLoader.tsx ├── store │ ├── debugger │ │ └── debuggerSlice.ts │ ├── hooks.ts │ ├── index.ts │ ├── utils.ts │ └── workers │ │ └── workersSlice.ts ├── types │ ├── pvm.ts │ └── type-guards.ts ├── utils │ ├── colors.ts │ ├── instructionsKnowledgeBase.ts │ ├── loggerService.tsx │ └── virtualTrapInstruction.ts └── vite-env.d.ts ├── tailwind.config.js ├── tests ├── memory-range.spec.ts ├── run-program.spec.ts └── utils │ └── actions.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = off 12 | 13 | [CHANGELOG.md] 14 | indent_size = false 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], 5 | ignorePatterns: ["dist", ".eslintrc.cjs"], 6 | parser: "@typescript-eslint/parser", 7 | plugins: ["react-refresh"], 8 | rules: { 9 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], 10 | "no-console": ["warn", { allow: ["warn", "error"] }], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Deploy on release 6 | release: 7 | types: [published] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 20 37 | cache: "npm" 38 | - name: Install dependencies 39 | run: npm ci 40 | - name: Build 41 | run: npm run build 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v3 46 | with: 47 | # Upload dist folder 48 | path: "./dist" 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [22.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "npm" 28 | - run: npm ci 29 | - run: npm run lint 30 | - run: npm run format-check 31 | - run: npm run build --if-present 32 | - run: npm test --if-present 33 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | pull_request: 4 | branches: [main, master] 5 | 6 | jobs: 7 | test: 8 | timeout-minutes: 60 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: lts/* 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Install Playwright Browsers 18 | run: npx playwright install --with-deps 19 | - name: Waiting for 200 from the Netlify Preview 20 | uses: jakepartusch/wait-for-netlify-action@v1.2 21 | id: waitFor200 22 | with: 23 | site_name: "pvm-debugger" 24 | max_timeout: 360 # 6 Minutes, depends on your build pipeline duration 25 | - name: Run Playwright tests 26 | run: npx playwright test 27 | env: 28 | PLAYWRIGHT_TEST_BASE_URL: ${{ steps.waitFor200.outputs.url }} 29 | - uses: actions/upload-artifact@v4 30 | if: ${{ !cancelled() }} 31 | with: 32 | name: playwright-report 33 | path: playwright-report/ 34 | retention-days: 30 35 | -------------------------------------------------------------------------------- /.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 | /test-results/ 26 | /playwright-report/ 27 | /blob-report/ 28 | /playwright/.cache/ 29 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PVM disassembler & debugger 2 | 3 | - **Production @ https://pvm.fluffylabs.dev** 4 | - Beta @ https://pvm-debugger.netlify.app/ 5 | 6 | ## PVMs support 7 | 8 | We have the following PVMs integrated by default. 9 | 10 | - [x] [typeberry](https://github.com/fluffylabs/typeberry) - TypeScript implementation (Private) 11 | - [x] [anan-as](https://github.com/tomusdrw/anan-as) - AssemblyScript implementation (as WASM) 12 | - [x] [polkavm](https://github.com/paritytech/polkavm) - Rust implementation (as WASM) 13 | 14 | There are few ways how you can add your own PVM to execute the code. 15 | 16 | 1. Upload WASM (supported interfaces: [wasm-bindgen](https://github.com/FluffyLabs/pvm-debugger/blob/main/src/packages/web-worker/wasmBindgenShell.ts#L4), [assembly script](https://github.com/FluffyLabs/pvm-debugger/blob/main/src/packages/web-worker/wasmAsShell.ts#L5), [Go](https://github.com/FluffyLabs/pvm-debugger/blob/main/src/packages/web-worker/wasmGoShell.ts#L5)) 17 | 2. Point to an URL with metadata file (details in [#81](https://github.com/FluffyLabs/pvm-debugger/issues/81); example [pvm-metadata.json](https://github.com/tomusdrw/polkavm/blob/gh-pages/pvm-metadata.json)) 18 | 3. Connect to WebSocket interface: [example & docs](https://github.com/wkwiatek/pvm-ws-rpc). 19 | 20 | Details about the API requirements can be found in [#81](https://github.com/FluffyLabs/pvm-debugger/issues/81) 21 | 22 | ## Development 23 | 24 | ### Requirements 25 | 26 | ```bash 27 | $ node --version 28 | v 22.1.0 29 | ``` 30 | 31 | We recommend [NVM](https://github.com/nvm-sh/nvm) to install and manage different 32 | `node` versions. 33 | 34 | ### Installing dependencies 35 | 36 | ```bash 37 | $ npm ci 38 | ``` 39 | 40 | ### Start a development version 41 | 42 | ```bash 43 | $ npm run dev 44 | ``` 45 | 46 | ### Building 47 | 48 | ```bash 49 | $ npm run build 50 | ``` 51 | 52 | The static files with the website can then be found in `./dist` folder. 53 | 54 | You can display them for instance with `http-server`: 55 | 56 | ```bash 57 | $ npx http-server -o 58 | ``` 59 | 60 | ### Running formatting & linting 61 | 62 | ```bash 63 | $ npm run lint 64 | ``` 65 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PVM debugger | Fluffy Labs 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pvm-debugger", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "tsc -b && vite build", 9 | "format-check": "prettier --check 'src/**/*.{ts,tsx}'", 10 | "format": "prettier --write .", 11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "preview": "vite preview", 13 | "prepare": "husky" 14 | }, 15 | "dependencies": { 16 | "@radix-ui/react-accordion": "^1.2.3", 17 | "@radix-ui/react-collapsible": "^1.1.0", 18 | "@radix-ui/react-dialog": "^1.1.6", 19 | "@radix-ui/react-dropdown-menu": "^2.1.6", 20 | "@radix-ui/react-hover-card": "^1.1.1", 21 | "@radix-ui/react-label": "^2.1.0", 22 | "@radix-ui/react-popover": "^1.1.2", 23 | "@radix-ui/react-radio-group": "^1.2.0", 24 | "@radix-ui/react-select": "^2.1.6", 25 | "@radix-ui/react-separator": "^1.1.0", 26 | "@radix-ui/react-slot": "^1.1.0", 27 | "@radix-ui/react-switch": "^1.1.0", 28 | "@radix-ui/react-tabs": "^1.1.0", 29 | "@radix-ui/react-tooltip": "^1.1.7", 30 | "@reduxjs/toolkit": "^2.2.8", 31 | "@tanstack/react-virtual": "^3.10.9", 32 | "@typeberry/pvm-debugger-adapter": "0.1.0-f7a7185", 33 | "@typeberry/spectool-wasm": "0.20.8", 34 | "@uiw/react-codemirror": "^4.23.6", 35 | "blake2b": "^2.1.4", 36 | "class-variance-authority": "^0.7.1", 37 | "classnames": "^2.5.1", 38 | "clsx": "^2.1.1", 39 | "cmdk": "^1.0.0", 40 | "framer-motion": "^12.4.10", 41 | "lodash": "^4.17.21", 42 | "lucide-react": "^0.408.0", 43 | "path-browserify": "^1.0.1", 44 | "react": "^18.3.1", 45 | "react-dnd": "^16.0.1", 46 | "react-dnd-html5-backend": "^16.0.1", 47 | "react-dom": "^18.3.1", 48 | "react-dropzone": "^14.3.8", 49 | "react-intersection-observer": "^9.13.1", 50 | "react-katex": "^3.0.1", 51 | "react-number-format": "^5.4.2", 52 | "react-redux": "^9.1.2", 53 | "react-router": "^7.0.2", 54 | "react-toastify": "^10.0.6", 55 | "redux-persist": "^6.0.0", 56 | "tailwind-merge": "^2.6.0", 57 | "tailwindcss-animate": "^1.0.7", 58 | "vaul": "^0.9.1", 59 | "zod": "^3.24.1" 60 | }, 61 | "devDependencies": { 62 | "@playwright/test": "^1.47.2", 63 | "@types/blake2b": "^2.1.3", 64 | "@types/lodash": "^4.17.7", 65 | "@types/node": "^20.14.11", 66 | "@types/react": "^18.3.3", 67 | "@types/react-dom": "^18.3.0", 68 | "@types/react-katex": "^3.0.4", 69 | "@typescript-eslint/eslint-plugin": "^7.15.0", 70 | "@typescript-eslint/parser": "^7.15.0", 71 | "@vitejs/plugin-react": "^4.3.1", 72 | "autoprefixer": "^10.4.19", 73 | "esbuild": "^0.24.0", 74 | "eslint": "^8.57.0", 75 | "eslint-plugin-react-hooks": "^4.6.2", 76 | "eslint-plugin-react-refresh": "^0.4.7", 77 | "husky": "^9.1.1", 78 | "lint-staged": "^15.2.7", 79 | "postcss": "^8.4.39", 80 | "prettier": "3.3.3", 81 | "tailwindcss": "^3.4.6", 82 | "typescript": "^5.2.2", 83 | "vite": "^5.4.7", 84 | "vite-plugin-top-level-await": "^1.4.4", 85 | "vite-plugin-wasm": "^3.3.0" 86 | }, 87 | "lint-staged": { 88 | "**/*.{ts,tsx}": [ 89 | "npm run format", 90 | "npm run lint" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: "./tests", 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 0, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: "html", 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('/')`. */ 29 | baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://localhost:5173", 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: "on-first-retry", 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | { 38 | name: "chromium", 39 | use: { ...devices["Desktop Chrome"] }, 40 | }, 41 | 42 | { 43 | name: "firefox", 44 | use: { ...devices["Desktop Firefox"] }, 45 | }, 46 | 47 | { 48 | name: "webkit", 49 | use: { ...devices["Desktop Safari"] }, 50 | }, 51 | 52 | /* Test against mobile viewports. */ 53 | // { 54 | // name: 'Mobile Chrome', 55 | // use: { ...devices['Pixel 5'] }, 56 | // }, 57 | // { 58 | // name: 'Mobile Safari', 59 | // use: { ...devices['iPhone 12'] }, 60 | // }, 61 | 62 | /* Test against branded browsers. */ 63 | // { 64 | // name: 'Microsoft Edge', 65 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 66 | // }, 67 | // { 68 | // name: 'Google Chrome', 69 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 70 | // }, 71 | ], 72 | 73 | /* Run your local dev server before starting the tests */ 74 | // webServer: { 75 | // command: 'npm run start', 76 | // url: 'http://127.0.0.1:3000', 77 | // reuseExistingServer: !process.env.CI, 78 | // }, 79 | }); 80 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FluffyLabs/pvm-debugger/ce66addcc1fe85527b350da381d9faede89c70bf/public/favicon.ico -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import "katex/dist/katex.min.css"; 2 | 3 | .katex-html { 4 | text-align: left; 5 | } 6 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { ToastContainer } from "react-toastify"; 3 | import "react-toastify/dist/ReactToastify.css"; 4 | 5 | import { Header } from "@/components/Header"; 6 | import { useAppSelector } from "@/store/hooks.ts"; 7 | import { DebuggerControlls } from "./components/DebuggerControlls"; 8 | import DebuggerContent from "@/pages/DebuggerContent.tsx"; 9 | import ProgramLoader from "@/pages/ProgramLoader.tsx"; 10 | import { Navigate, Route, Routes } from "react-router"; 11 | import { AppsSidebar } from "./packages/ui-kit/AppsSidebar"; 12 | import { MobileDebuggerControls } from "./components/MobileDebuggerControlls"; 13 | 14 | function App() { 15 | const { pvmInitialized } = useAppSelector((state) => state.debugger); 16 | 17 | return ( 18 | <> 19 |
20 |
21 | 22 | 23 |
24 |
25 | {pvmInitialized ? ( 26 |
27 |
28 | 29 |
30 |
31 | ) : null} 32 | 33 | 34 | : } /> 35 | } /> 36 | 37 | 38 | {pvmInitialized ? ( 39 |
40 | 41 |
42 | ) : null} 43 |
44 |
45 |
46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /src/assets/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FluffyLabs/pvm-debugger/ce66addcc1fe85527b350da381d9faede89c70bf/src/assets/arrow.png -------------------------------------------------------------------------------- /src/assets/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FluffyLabs/pvm-debugger/ce66addcc1fe85527b350da381d9faede89c70bf/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/sidebar/brick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/sidebar/chip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/sidebar/computers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/sidebar/debugger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/sidebar/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/sidebar/stack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/DebuggerSettings/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog"; 2 | import { Settings } from "lucide-react"; 3 | import { useState } from "react"; 4 | import { DebuggerSettingsContent } from "./Content"; 5 | import { HostCallsContent } from "../HostCalls/HostCallsContent"; 6 | 7 | export const DebuggerSettings = ({ 8 | noTrigger, 9 | open, 10 | onOpenChange, 11 | }: { 12 | noTrigger?: boolean; 13 | open: boolean; 14 | onOpenChange: (open: boolean) => void; 15 | }) => { 16 | const [isStorageSettings, setIsStorageSettings] = useState(false); 17 | 18 | return ( 19 | 20 | {noTrigger ? null : ( 21 | 22 | 23 | 24 | )} 25 | 26 | { 28 | // Prevent closing when clicking inside the dialog 29 | if (e.target instanceof HTMLElement && e.target.closest('[role="dialog"]')) { 30 | e.preventDefault(); 31 | } 32 | }} 33 | className="p-0 pb-4 h-full sm:h-[700px] flex flex-col md:min-w-[680px] max-h-lvh" 34 | > 35 | {isStorageSettings ? ( 36 | setIsStorageSettings(false)} /> 37 | ) : ( 38 | setIsStorageSettings(true)} /> 39 | )} 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/DiffChecker.tsx: -------------------------------------------------------------------------------- 1 | import ReactJsonViewCompare from "react-json-view-compare"; 2 | 3 | export const DiffChecker = (args: { expected?: unknown; actual?: unknown }) => { 4 | if (!args.actual) return null; 5 | 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/ErrorWarningTooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipTrigger, TooltipContent } from "@radix-ui/react-tooltip"; 2 | import { TriangleAlertIcon } from "lucide-react"; 3 | import classNames from "classnames"; 4 | 5 | export const ErrorWarningTooltip = (props: { 6 | msg: string; 7 | classNames?: string; 8 | side?: "bottom" | "top" | "right" | "left" | undefined; 9 | variant: "dark" | "light"; 10 | }) => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 20 |

{props.msg}

21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Header as FluffyHeader } from "@/packages/ui-kit/Header"; 2 | import { DebuggerSettings } from "../DebuggerSettings"; 3 | import { PvmSelect } from "../PvmSelect"; 4 | import { NumeralSystemSwitch } from "../NumeralSystemSwitch"; 5 | import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuSeparator } from "../ui/dropdown-menu"; 6 | import { Button } from "../ui/button"; 7 | import { EllipsisVertical } from "lucide-react"; 8 | import { DropdownMenuItem, DropdownMenuLabel } from "@radix-ui/react-dropdown-menu"; 9 | import { useState } from "react"; 10 | 11 | const EndSlot = () => { 12 | const [dialogOpen, setDialogOpen] = useState(false); 13 | 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | const MobileMenu = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => { 34 | const [dropdownOpen, setDropdownOpen] = useState(false); 35 | 36 | // Function to handle opening the dialog and closing the dropdown 37 | const handleOpenDialog = () => { 38 | setDropdownOpen(false); // Close the dropdown first 39 | setTimeout(() => { 40 | onOpenChange(true); // Then open the dialog after a small delay 41 | }, 10); 42 | }; 43 | return ( 44 | <> 45 | 46 | 47 | 48 | 51 | 52 | 53 | { 56 | e.preventDefault(); // Prevent the dropdown from closing automatically 57 | handleOpenDialog(); // Handle opening the dialog 58 | }} 59 | > 60 |
61 | Settings 62 |
63 |
64 | 65 | Github 66 | 67 | window.open("https://github.com/FluffyLabs/pvm-debugger/issues/new", "_blank")} 69 | className="pl-3 pt-3" 70 | > 71 |
72 | Report an issue or suggestion 73 | Go to the issue creation page 74 |
75 |
76 | 77 | window.open("https://github.com/FluffyLabs/pvm-debugger", "_blank")} 79 | className="pl-3 pt-3" 80 | > 81 |
82 | Star us on Github to show support 83 | Visit our Github 84 |
85 |
86 | 87 | window.open("https://github.com/FluffyLabs/pvm-debugger/fork", "_blank")} 89 | className="pl-3 py-3" 90 | > 91 |
92 | Fork & contribute 93 | Opens the fork creation page 94 |
95 |
96 |
97 |
98 | 99 | ); 100 | }; 101 | export const Header = () => { 102 | return } />; 103 | }; 104 | -------------------------------------------------------------------------------- /src/components/HostCalls/HostCallsContent.tsx: -------------------------------------------------------------------------------- 1 | import { DialogHeader, DialogTitle } from "@/components/ui/dialog"; 2 | import { useAppDispatch, useAppSelector } from "@/store/hooks.ts"; 3 | import { handleHostCall, setAllWorkersStorage } from "@/store/workers/workersSlice"; 4 | import { TrieInput } from "./trie-input"; 5 | import { Button } from "../ui/button"; 6 | import { setStorage } from "@/store/debugger/debuggerSlice"; 7 | import { useEffect, useState } from "react"; 8 | import { DebuggerEcalliStorage } from "@/types/pvm"; 9 | import { isSerializedError } from "@/store/utils"; 10 | import { ChevronLeft } from "lucide-react"; 11 | import { Separator } from "../ui/separator"; 12 | import { WithHelp } from "../WithHelp/WithHelp"; 13 | 14 | const isEcalliWriteOrRead = (exitArg?: number) => { 15 | return exitArg === 2 || exitArg === 3; 16 | }; 17 | 18 | export const HostCallsContent = ({ onSetStorage }: { onSetStorage: () => void }) => { 19 | const dispatch = useAppDispatch(); 20 | const { storage } = useAppSelector((state) => state.debugger); 21 | const firstWorker = useAppSelector((state) => state.workers?.[0]); 22 | 23 | const [newStorage, setNewStorage] = useState(); 24 | const [error, setError] = useState(); 25 | const isOnEcalli = isEcalliWriteOrRead(firstWorker?.exitArg); 26 | 27 | useEffect(() => { 28 | setNewStorage(storage); 29 | }, [storage]); 30 | 31 | const onSubmit = async () => { 32 | setError(""); 33 | 34 | try { 35 | dispatch(setStorage({ storage: newStorage, isUserProvided: true })); 36 | await dispatch(setAllWorkersStorage({ storage: newStorage || null })).unwrap(); 37 | try { 38 | if (isOnEcalli) { 39 | await dispatch(handleHostCall({})).unwrap(); 40 | } 41 | 42 | onSetStorage(); 43 | } catch (e) { 44 | if (e instanceof Error || isSerializedError(e)) { 45 | setError(e.message); 46 | } 47 | } 48 | } catch (e) { 49 | if (e instanceof Error || isSerializedError(e)) { 50 | setError(e.message); 51 | } 52 | } 53 | }; 54 | 55 | return ( 56 | <> 57 |
58 | 59 | READ Host Call Storage 60 | 61 | {!isOnEcalli && ( 62 | <> 63 |
64 | 67 | Settings 68 |
69 | 70 | 71 | 72 | )} 73 | 74 |
75 | 76 | 77 | Service storage entries 78 | 79 | 80 |
81 | setNewStorage(v)} initialRows={storage} /> 82 |
83 |
84 | {error &&

{error}

} 85 |
86 |
87 | 95 |
96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/HostCalls/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 2 | import { useAppDispatch, useAppSelector } from "@/store/hooks.ts"; 3 | import { setHasHostCallOpen } from "@/store/debugger/debuggerSlice"; 4 | import { HostCallsContent } from "./HostCallsContent"; 5 | 6 | export const HostCalls = () => { 7 | const dispatch = useAppDispatch(); 8 | const { hasHostCallOpen } = useAppSelector((state) => state.debugger); 9 | 10 | return ( 11 | { 14 | if (!val) { 15 | dispatch(setHasHostCallOpen(false)); 16 | } 17 | }} 18 | > 19 | 20 | setHasHostCallOpen(false)} /> 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/InitialLoadProgramCTA/index.tsx: -------------------------------------------------------------------------------- 1 | import Arrow from "@/assets/arrow.png"; 2 | 3 | export const InitialLoadProgramCTA = () => { 4 | return ( 5 |
6 |
7 | 8 |
9 |

Start jamming with PVM.

10 | 11 |
12 | 13 |
14 |

Start jamming with PVM.

15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Instructions/index.tsx: -------------------------------------------------------------------------------- 1 | import { InstructionMode } from "@/components/Instructions/types"; 2 | import { CurrentInstruction, ExpectedState, Status } from "@/types/pvm"; 3 | import { InstructionsTable, ProgramRow } from "./InstructionsTable"; 4 | import { ProgramEdit } from "../ProgramEdit"; 5 | 6 | export interface InstructionsProps { 7 | programName: string; 8 | status?: Status; 9 | programPreviewResult?: CurrentInstruction[]; 10 | currentState: ExpectedState; 11 | instructionMode: InstructionMode; 12 | onInstructionClick: (row: ProgramRow) => void; 13 | onAddressClick: (address: number) => void; 14 | breakpointAddresses: (number | undefined)[]; 15 | } 16 | 17 | export const Instructions = (props: InstructionsProps) => { 18 | return ( 19 |
20 | {props.programName}} /> 21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Instructions/types.ts: -------------------------------------------------------------------------------- 1 | export enum InstructionMode { 2 | ASM, 3 | BYTECODE, 4 | } 5 | -------------------------------------------------------------------------------- /src/components/KnowledgeBase/Mobile.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Drawer, DrawerClose, DrawerContent, DrawerFooter } from "@/components/ui/drawer"; 3 | import { KnowledgeBase } from "."; 4 | import { CurrentInstruction } from "@/types/pvm"; 5 | import { useCallback } from "react"; 6 | 7 | type MobileKnowledgeBaseProps = { 8 | currentInstruction: CurrentInstruction | undefined; 9 | open: boolean; 10 | onClose: () => void; 11 | }; 12 | 13 | export const MobileKnowledgeBase = ({ currentInstruction, open, onClose }: MobileKnowledgeBaseProps) => { 14 | const onOpenChange = useCallback( 15 | (open: boolean) => { 16 | if (!open) { 17 | onClose(); 18 | } 19 | }, 20 | [onClose], 21 | ); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/KnowledgeBase/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | import { BlockMath } from "react-katex"; 3 | import { ExternalLink } from "lucide-react"; 4 | import { InstructionKnowledgeBaseEntry, instructionsKnowledgeBase } from "@/utils/instructionsKnowledgeBase.ts"; 5 | import { CurrentInstruction } from "@/types/pvm"; 6 | import { debounce } from "lodash"; 7 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; 8 | import { Search } from "../SearchInput"; 9 | 10 | const OPEN_VALUE = "item-1"; 11 | export const KnowledgeBase = ({ currentInstruction }: { currentInstruction: CurrentInstruction | undefined }) => { 12 | const [filteredInstructions, setFilteredInstructions] = useState([]); 13 | const [searchText, setSearchText] = useState(""); 14 | const [isOpen, setIsOpen] = useState(false); 15 | 16 | const setSearchLater = useMemo(() => { 17 | return debounce( 18 | (currentInstruction) => { 19 | if (currentInstruction) { 20 | setSearchText(currentInstruction.name || ""); 21 | } 22 | }, 23 | 10, 24 | { leading: true, trailing: true }, 25 | ); 26 | }, [setSearchText]); 27 | 28 | useEffect(() => { 29 | setSearchLater(currentInstruction); 30 | }, [currentInstruction, setSearchLater]); 31 | 32 | useEffect(() => { 33 | setFilteredInstructions( 34 | instructionsKnowledgeBase.filter((instruction) => 35 | instruction.name?.toUpperCase().includes(searchText.toUpperCase()), 36 | ), 37 | ); 38 | }, [searchText]); 39 | 40 | return ( 41 |
42 | setIsOpen(value === OPEN_VALUE)} 48 | > 49 | 50 | 51 | { 58 | setSearchText(e.target.value); 59 | }} 60 | onFocus={() => setIsOpen(true)} 61 | onBlur={() => setIsOpen(searchText === "" ? false : isOpen)} 62 | /> 63 | 64 | 65 |
66 |
67 | {filteredInstructions.length === 0 && ( 68 |
69 |

no instructions found

70 |
71 | )} 72 | {filteredInstructions.map((instruction, i) => { 73 | const currentInstructionFromKnowledgeBase = instructionsKnowledgeBase.find( 74 | (instructionFromKB) => instructionFromKB.name?.toUpperCase() === instruction.name?.toUpperCase(), 75 | ); 76 | return ( 77 |
78 |
79 |

{instruction?.name}

80 | 81 |
82 | {currentInstructionFromKnowledgeBase?.linkInGrayPaperReader && ( 83 | 84 | 85 | 86 | )} 87 |
88 |
89 |
90 | 91 |
92 |

{currentInstructionFromKnowledgeBase?.description}

93 |
94 | ); 95 | })} 96 |
97 |
98 |
99 |
100 |
101 |
102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /src/components/LoadingSpinner/index.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils.ts"; 2 | 3 | export interface ISVGProps extends React.SVGProps { 4 | size?: number; 5 | className?: string; 6 | } 7 | 8 | export const LoadingSpinner = ({ size = 24, className, ...props }: ISVGProps) => { 9 | return ( 10 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/MemoryPreview/MemoryRanges.tsx: -------------------------------------------------------------------------------- 1 | import { HTML5Backend } from "react-dnd-html5-backend"; 2 | import { DndProvider } from "react-dnd"; 3 | import { MemoryRangeRow } from "./MemoryRangesRow"; 4 | import { createEmptyRow, RangeRow } from "./MemoryRangesEmptyRow"; 5 | 6 | type MemoryRangesProps = { 7 | ranges: RangeRow[]; 8 | setRanges: (x: React.SetStateAction) => void; 9 | }; 10 | 11 | export function MemoryRanges({ ranges, setRanges }: MemoryRangesProps) { 12 | function handleAddNewRow(idx: number, start: number, length: number, rowId: string) { 13 | setRanges((prev) => { 14 | const copy = [...prev]; 15 | copy[idx] = { id: rowId, start, length, isEditing: false }; 16 | copy.push(createEmptyRow()); 17 | return copy; 18 | }); 19 | } 20 | 21 | function handleEditToggle(idx: number) { 22 | setRanges((prev) => { 23 | const copy = [...prev]; 24 | copy[idx].isEditing = !copy[idx].isEditing; 25 | return copy; 26 | }); 27 | } 28 | 29 | function handleChange(idx: number, start: number, length: number) { 30 | setRanges((prev) => { 31 | const copy = [...prev]; 32 | copy[idx].start = start; 33 | copy[idx].length = length; 34 | return copy; 35 | }); 36 | } 37 | 38 | function handleRemove(idx: number) { 39 | setRanges((prev) => { 40 | const copy = [...prev]; 41 | copy.splice(idx, 1); 42 | return copy; 43 | }); 44 | } 45 | function handleMove(dragIndex: number, hoverIndex: number) { 46 | setRanges((prev) => { 47 | const newArr = [...prev]; 48 | const [movedRow] = newArr.splice(dragIndex, 1); 49 | newArr.splice(hoverIndex, 0, movedRow); 50 | return newArr; 51 | }); 52 | } 53 | 54 | return ( 55 | 56 |
57 | {ranges.map((r, i) => { 58 | const isLast = i === ranges.length - 1; 59 | return ( 60 | handleChange(i, start, length)} 67 | onAddNew={(start, length, rowId) => handleAddNewRow(i, start, length, rowId)} 68 | onEditToggle={() => handleEditToggle(i)} 69 | onRemove={() => handleRemove(i)} 70 | onMove={handleMove} 71 | /> 72 | ); 73 | })} 74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/components/MemoryPreview/MemoryRangesEmptyRow.tsx: -------------------------------------------------------------------------------- 1 | import { MEMORY_SPLIT_STEP } from "@/store/utils"; 2 | 3 | export interface RangeRow { 4 | id: string; 5 | start: number; 6 | length: number; 7 | isEditing: boolean; 8 | } 9 | 10 | export const createEmptyRow = (): RangeRow => ({ 11 | id: crypto.randomUUID(), 12 | start: 0, 13 | length: 2 * MEMORY_SPLIT_STEP, 14 | isEditing: true, 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/MemoryPreview/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 2 | import { MemoryInfinite } from "./MemoryInfinite"; 3 | import { MemoryRanges } from "./MemoryRanges"; 4 | import { useState } from "react"; 5 | import { createEmptyRow } from "./MemoryRangesEmptyRow"; 6 | 7 | export const MemoryPreview = () => { 8 | const [ranges, setRanges] = useState([createEmptyRow()]); 9 | 10 | const triggerClass = 11 | "text-xs w-1/2 h-8 bg-title text-secondary-foreground dark:text-brand data-[state=active]:bg-black data-[state=active]:text-white dark:data-[state=active]:bg-brand dark:data-[state=active]:text-background rounded-se-none rounded-ee-none"; 12 | return ( 13 |
14 | 15 | 16 | 17 | Infinite 18 | 19 | 20 | Ranges 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/MobileDebuggerControlls/index.tsx: -------------------------------------------------------------------------------- 1 | import { DebuggerControlls } from "../DebuggerControlls"; 2 | import { NumeralSystemSwitch } from "../NumeralSystemSwitch"; 3 | import { Separator } from "../ui/separator"; 4 | import { ControllsDrawer } from "./ControllsDrawer"; 5 | import { useAppSelector } from "@/store/hooks"; 6 | 7 | export const MobileDebuggerControls = () => { 8 | const { initialState } = useAppSelector((state) => state.debugger); 9 | const workers = useAppSelector((state) => state.workers); 10 | const { currentState, previousState } = workers[0] || { 11 | currentState: initialState, 12 | previousState: initialState, 13 | }; 14 | 15 | return ( 16 |
17 | {/* Bottom Drawer */} 18 | 19 | 20 | {/* Main Controls */} 21 |
22 | 23 | 24 | 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/NumeralSystemSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/components/ui/label.tsx"; 2 | import { Switch } from "@/components/ui/switch.tsx"; 3 | import { useContext } from "react"; 4 | import { NumeralSystemContext } from "@/context/NumeralSystemContext"; 5 | import { NumeralSystem } from "@/context/NumeralSystem"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | export const NumeralSystemSwitch = ({ className }: { className: string }) => { 9 | const { setNumeralSystem, numeralSystem } = useContext(NumeralSystemContext); 10 | 11 | return ( 12 |
13 | 22 | setNumeralSystem(checked ? NumeralSystem.HEXADECIMAL : NumeralSystem.DECIMAL)} 27 | /> 28 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/ProgramEdit/index.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/components/ui/label.tsx"; 2 | import { Switch } from "@/components/ui/switch.tsx"; 3 | import { Pencil, PencilOff } from "lucide-react"; 4 | import { setInstructionMode, setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts"; 5 | import { useAppDispatch, useAppSelector } from "@/store/hooks"; 6 | import { InstructionMode } from "../Instructions/types"; 7 | import { useDebuggerActions } from "@/hooks/useDebuggerActions"; 8 | import cs from "classnames"; 9 | 10 | export const ProgramEdit = ({ startSlot, classNames }: { startSlot: JSX.Element; classNames?: string }) => { 11 | const dispatch = useAppDispatch(); 12 | const debuggerActions = useDebuggerActions(); 13 | const { program, programName, initialState, isProgramEditMode, isProgramInvalid, instructionMode } = useAppSelector( 14 | (state) => state.debugger, 15 | ); 16 | 17 | return ( 18 |
19 |
{startSlot}
20 |
21 | 47 |
48 | 57 | 62 | dispatch(setInstructionMode(checked ? InstructionMode.BYTECODE : InstructionMode.ASM)) 63 | } 64 | variant="secondary" 65 | /> 66 | 75 |
76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/Examples.tsx: -------------------------------------------------------------------------------- 1 | import { RegistersArray } from "@/types/pvm"; 2 | import { ProgramUploadFileOutput } from "./types"; 3 | import { Badge } from "@/components/ui/badge.tsx"; 4 | import { programs } from "./examplePrograms"; 5 | 6 | export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUploadFileOutput) => void }) => { 7 | return ( 8 |
9 | {Object.keys(programs).map((key) => { 10 | const program = programs[key]; 11 | return ( 12 | { 18 | onProgramLoad({ 19 | initial: { 20 | regs: program.regs.map((x) => BigInt(x)) as RegistersArray, 21 | pc: program.pc, 22 | pageMap: program.pageMap, 23 | memory: program.memory, 24 | gas: program.gas, 25 | }, 26 | program: programs[key].program, 27 | name: program.name, 28 | exampleName: key, 29 | }); 30 | }} 31 | > 32 | {programs[key].name} 33 | 34 | ); 35 | })} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/Links.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from "lucide-react"; 2 | 3 | export const Links = () => { 4 | return ( 5 |
    6 |
  • 7 |
    8 | 9 | 10 | 11 |
    12 |

    JSON test file compatible with JAM TestVectors JSON

    13 |

    14 | 15 | Examples can be found in{" "} 16 | 17 | wf3/jamtestvectors 18 | {" "} 19 | Github repo 20 | 21 |

    22 |
    23 |
    24 |
  • 25 | 26 |
  • 27 |
    28 | 29 | 30 | 31 |
    32 |

    JAM SPI program

    33 |

    34 | 35 | SPI program definition can be found in the{" "} 36 | 40 | GrayPaper 41 | 42 | 43 |

    44 |
    45 |
    46 |
  • 47 |
  • 48 |
    49 | 50 | 51 | 52 |
    53 |

    JSON test file compatible with JAM TestVectors JSON

    54 |

    55 | 56 | Generic program definition can be found in the{" "} 57 | 61 | GrayPaper 62 | 63 | 64 |

    65 |
    66 |
    67 |
  • 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "../ui/button"; 2 | import { Examples } from "./Examples"; 3 | import { useState, useCallback, useEffect } from "react"; 4 | import { ProgramUploadFileOutput } from "./types"; 5 | import { useDebuggerActions } from "@/hooks/useDebuggerActions"; 6 | import { useAppDispatch, useAppSelector } from "@/store/hooks.ts"; 7 | import { setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts"; 8 | import { selectIsAnyWorkerLoading } from "@/store/workers/workersSlice"; 9 | import { isSerializedError } from "@/store/utils"; 10 | import { ProgramFileUpload } from "@/components/ProgramLoader/ProgramFileUpload.tsx"; 11 | import { useNavigate } from "react-router"; 12 | import { Links } from "./Links"; 13 | import { Separator } from "../ui/separator"; 14 | import { TriangleAlert } from "lucide-react"; 15 | 16 | export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) => void }) => { 17 | const dispatch = useAppDispatch(); 18 | const [programLoad, setProgramLoad] = useState(); 19 | const [error, setError] = useState(); 20 | const [isSubmitted, setIsSubmitted] = useState(false); 21 | const debuggerActions = useDebuggerActions(); 22 | const isLoading = useAppSelector(selectIsAnyWorkerLoading); 23 | const navigate = useNavigate(); 24 | 25 | useEffect(() => { 26 | setError(""); 27 | }, [isLoading]); 28 | 29 | const handleLoad = useCallback( 30 | async (program?: ProgramUploadFileOutput) => { 31 | setIsSubmitted(true); 32 | 33 | if (!programLoad && !program) return; 34 | 35 | dispatch(setIsProgramEditMode(false)); 36 | 37 | try { 38 | await debuggerActions.handleProgramLoad(program || programLoad); 39 | setIsDialogOpen?.(false); 40 | navigate("/", { replace: true }); 41 | } catch (error) { 42 | if (error instanceof Error || isSerializedError(error)) { 43 | setError(error.message); 44 | } else { 45 | setError("Unknown error occurred"); 46 | } 47 | } 48 | }, 49 | [dispatch, programLoad, debuggerActions, setIsDialogOpen, navigate], 50 | ); 51 | 52 | const isProgramLoaded = programLoad !== undefined; 53 | 54 | return ( 55 |
56 |

57 | Start with an example program or upload your file 58 |

59 |
60 | { 62 | setProgramLoad(val); 63 | setIsSubmitted(false); 64 | handleLoad(val); 65 | }} 66 | /> 67 | 68 |
69 | { 71 | setProgramLoad(val); 72 | setIsSubmitted(false); 73 | // handleLoad(val); 74 | }} 75 | /> 76 |
77 | 78 | {error && isSubmitted && ( 79 |

80 | {error} 81 |

82 | )} 83 |
84 |
85 | 86 |
87 |
88 | 97 |
98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/examplePrograms.ts: -------------------------------------------------------------------------------- 1 | export const programs: { 2 | [key: string]: { 3 | name: string; 4 | program: number[]; 5 | regs: [number, number, number, number, number, number, number, number, number, number, number, number, number]; 6 | pc: number; 7 | pageMap: { address: number; length: number; "is-writable": boolean }[]; 8 | gas: bigint; 9 | memory: []; 10 | }; 11 | } = { 12 | fibonacci: { 13 | name: "Fibonacci sequence", 14 | program: [ 15 | 0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200, 152, 8, 100, 169, 40, 243, 100, 16 | 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0, 17 | ], 18 | regs: [4294901760, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0], 19 | pc: 0, 20 | pageMap: [], 21 | memory: [], 22 | gas: 100000n, 23 | }, 24 | gameOfLife: { 25 | name: "Conway's Game of Life", 26 | program: [ 27 | 0, 0, 129, 83, 30, 3, 3, 0, 2, 255, 0, 30, 3, 11, 0, 2, 255, 0, 30, 3, 19, 0, 2, 255, 0, 30, 3, 18, 0, 2, 255, 0, 28 | 30, 3, 9, 0, 2, 255, 0, 40, 22, 1, 51, 1, 255, 1, 149, 17, 1, 81, 17, 8, 12, 1, 51, 2, 255, 1, 149, 34, 1, 81, 18, 29 | 8, 241, 150, 19, 8, 149, 51, 0, 0, 2, 200, 35, 3, 40, 47, 149, 51, 128, 0, 124, 52, 132, 68, 1, 82, 20, 1, 14, 83, 30 | 21, 2, 25, 86, 21, 3, 21, 40, 8, 81, 21, 3, 6, 40, 11, 149, 51, 128, 70, 3, 255, 0, 40, 200, 149, 51, 128, 70, 3, 31 | 40, 193, 51, 5, 100, 52, 51, 8, 64, 149, 68, 255, 205, 132, 7, 149, 119, 0, 0, 2, 149, 119, 128, 0, 124, 118, 132, 32 | 102, 1, 200, 101, 5, 149, 68, 2, 205, 132, 7, 149, 119, 0, 0, 2, 149, 119, 128, 0, 124, 118, 132, 102, 1, 200, 33 | 101, 5, 149, 68, 247, 205, 132, 7, 149, 119, 0, 0, 2, 149, 119, 128, 0, 124, 118, 132, 102, 1, 200, 101, 5, 149, 34 | 68, 16, 205, 132, 7, 149, 119, 0, 0, 2, 149, 119, 128, 0, 124, 118, 132, 102, 1, 200, 101, 5, 149, 68, 1, 205, 35 | 132, 7, 149, 119, 0, 0, 2, 149, 119, 128, 0, 124, 118, 132, 102, 1, 200, 101, 5, 149, 68, 254, 205, 132, 7, 149, 36 | 119, 0, 0, 2, 149, 119, 128, 0, 124, 118, 132, 102, 1, 200, 101, 5, 149, 68, 240, 205, 132, 7, 149, 119, 0, 0, 2, 37 | 149, 119, 128, 0, 124, 118, 132, 102, 1, 200, 101, 5, 149, 68, 2, 205, 132, 7, 149, 119, 0, 0, 2, 149, 119, 128, 38 | 0, 124, 118, 132, 102, 1, 200, 101, 5, 40, 20, 255, 51, 1, 0, 0, 2, 1, 149, 19, 128, 0, 128, 18, 122, 50, 149, 17, 39 | 4, 81, 49, 100, 0, 2, 220, 254, 40, 238, 129, 64, 32, 16, 72, 38, 100, 34, 33, 69, 137, 136, 162, 68, 169, 74, 18, 40 | 162, 36, 9, 81, 146, 132, 40, 73, 66, 148, 36, 33, 74, 146, 16, 37, 73, 136, 146, 36, 68, 73, 194, 168, 4, 2, 41 | ], 42 | regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 43 | pc: 0, 44 | pageMap: [ 45 | { 46 | address: 2 ** 17, 47 | length: 4096, 48 | "is-writable": true, 49 | }, 50 | ], 51 | memory: [], 52 | gas: 10_000_000n, 53 | }, 54 | branch: { 55 | name: "Branch instruction", 56 | program: [0, 0, 16, 51, 7, 210, 4, 81, 39, 210, 4, 6, 0, 51, 7, 239, 190, 173, 222, 17, 6], 57 | regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 58 | pc: 0, 59 | pageMap: [], 60 | memory: [], 61 | gas: 10n, 62 | }, 63 | add: { 64 | name: "ADD instruction", 65 | program: [0, 0, 3, 200, 135, 9, 1], 66 | regs: [0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0], 67 | pc: 0, 68 | pageMap: [], 69 | memory: [], 70 | gas: 10000n, 71 | }, 72 | storeU16: { 73 | name: "Store U16 instruction", 74 | program: [0, 0, 5, 60, 7, 0, 0, 2, 1], 75 | regs: [0, 0, 0, 0, 0, 0, 0, 305419896, 0, 0, 0, 0, 0], 76 | pc: 0, 77 | pageMap: [ 78 | { 79 | address: 131072, 80 | length: 4096, 81 | "is-writable": true, 82 | }, 83 | ], 84 | memory: [], 85 | gas: 10000n, 86 | }, 87 | empty: { 88 | name: "Empty", 89 | program: [0, 0, 0], 90 | regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 91 | pc: 0, 92 | pageMap: [], 93 | memory: [], 94 | gas: 10000n, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; 2 | import { Button } from "@/components/ui/button.tsx"; 3 | import { useState } from "react"; 4 | import { InitialState } from "@/types/pvm"; 5 | import { Loader } from "./Loader.tsx"; 6 | import { Upload } from "lucide-react"; 7 | 8 | export const ProgramLoader = (props: { initialState: InitialState; program: number[]; onOpen: () => void }) => { 9 | const [isDialogOpen, setIsDialogOpen] = useState(false); 10 | 11 | return ( 12 | { 15 | setIsDialogOpen(val); 16 | if (val) { 17 | props.onOpen(); 18 | } 19 | }} 20 | > 21 | e.stopPropagation()}> 22 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/types.ts: -------------------------------------------------------------------------------- 1 | import { InitialState } from "@/types/pvm"; 2 | 3 | export type ProgramUploadFileOutput = { 4 | name: string; 5 | initial: InitialState; 6 | program: number[]; 7 | exampleName?: string; 8 | }; 9 | 10 | export type ProgramUploadFileInput = { 11 | name: string; 12 | program: number[]; 13 | 14 | "initial-regs": number[]; 15 | "initial-pc": number; 16 | "initial-page-map": { 17 | address: number; 18 | length: number; 19 | "is-writable": boolean; 20 | }[]; 21 | "initial-memory": { 22 | address: number; 23 | contents: number[]; 24 | }[]; 25 | "initial-gas": number; 26 | 27 | "expected-status": string; 28 | "expected-regs": number[]; 29 | "expected-pc": number; 30 | "expected-memory": { 31 | address: number; 32 | contents: number[]; 33 | }[]; 34 | "expected-gas": number; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/ProgramLoader/utils.ts: -------------------------------------------------------------------------------- 1 | import { mapKeys, camelCase, pickBy } from "lodash"; 2 | import { ProgramUploadFileInput, ProgramUploadFileOutput } from "./types"; 3 | import { RegistersArray } from "@/types/pvm"; 4 | 5 | export function mapUploadFileInputToOutput(data: ProgramUploadFileInput): ProgramUploadFileOutput { 6 | const camelCasedData = mapKeys(data, (_value: unknown, key: string) => camelCase(key)); 7 | 8 | const initial = pickBy(camelCasedData, (_value: unknown, key) => key.startsWith("initial")); 9 | // const expected = pickBy(camelCasedData, (_value: unknown, key) => key.startsWith("expected")); 10 | 11 | const result: ProgramUploadFileOutput = { 12 | name: data.name, 13 | initial: { 14 | ...(mapKeys(initial, (_value: unknown, key) => 15 | camelCase(key.replace("initial", "")), 16 | ) as ProgramUploadFileOutput["initial"]), 17 | // TODO is-writable has wrong case 18 | // pageMap: data["initial-page-map"].map((val) => ({ address: val.address, length: val.length, isWritable: val["is-writable"] })), 19 | }, 20 | program: data.program, 21 | // expected: mapKeys(expected, (_value: unknown, key) => camelCase(key.replace("expected", ""))) as unknown as ProgramUploadFileOutput["expected"], 22 | }; 23 | 24 | result.initial.regs = result.initial.regs?.map((x) => BigInt(x as number | bigint)) as RegistersArray; 25 | return result; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ProgramTextLoader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Textarea } from "@/components/ui/textarea.tsx"; 2 | import React, { useMemo, useState } from "react"; 3 | import classNames from "classnames"; 4 | import { bytes } from "@typeberry/pvm-debugger-adapter"; 5 | import { logger } from "@/utils/loggerService"; 6 | import { useAppSelector } from "@/store/hooks.ts"; 7 | import { selectIsProgramInvalid } from "@/store/debugger/debuggerSlice.ts"; 8 | import { ProgramEdit } from "../ProgramEdit"; 9 | 10 | const parseArrayLikeString = (input: string): (number | string)[] => { 11 | // Remove the brackets and split the string by commas 12 | const items = input 13 | .replace(/^\[|\]$/g, "") 14 | .split(",") 15 | .map((item) => item.trim()); 16 | 17 | // Process each item 18 | return items.map((item) => { 19 | if (/^0x[0-9a-fA-F]+$/i.test(item)) { 20 | return parseInt(item, 16); 21 | } else if (!isNaN(Number(item))) { 22 | return Number(item); 23 | } else { 24 | return item; 25 | } 26 | }); 27 | }; 28 | 29 | export const ProgramTextLoader = ({ 30 | program, 31 | setProgram, 32 | }: { 33 | program?: number[]; 34 | setProgram: (val?: number[], error?: string) => void; 35 | }) => { 36 | const defaultProgram = useMemo(() => { 37 | return program; 38 | }, [program]); 39 | 40 | const [programInput, setProgramInput] = useState(defaultProgram?.length ? JSON.stringify(defaultProgram) : ""); 41 | const [programError, setProgramError] = useState(""); 42 | const isProgramInvalid = useAppSelector(selectIsProgramInvalid); 43 | 44 | const handleOnChange = (e: React.ChangeEvent) => { 45 | const newInput = e.target.value.trim(); 46 | setProgramInput(newInput); 47 | 48 | if (!newInput.startsWith("[")) { 49 | try { 50 | const parsedBlob = bytes.BytesBlob.parseBlob(newInput); 51 | 52 | const parsedBlobArray = Array.prototype.slice.call(parsedBlob.raw); 53 | 54 | if (parsedBlobArray.length) { 55 | setProgram(parsedBlobArray); 56 | } 57 | 58 | setProgramError(""); 59 | } catch (error: unknown) { 60 | logger.error("Wrong binary program", { error, hideToast: true }); 61 | 62 | setProgram(undefined, "Wrong binary program"); 63 | 64 | if (error instanceof Error) { 65 | if (error?.message) { 66 | setProgramError(error.message); 67 | } 68 | } 69 | } 70 | } else { 71 | try { 72 | // Make sure that hex strings are parsed as strings for JSON.parse validation 73 | const parseTest = newInput.replace(/0x([a-fA-F0-9]+)/g, '"0x$1"'); 74 | // Parse it just to check if it's a valid JSON 75 | JSON.parse(parseTest); 76 | const parsedJson = parseArrayLikeString(newInput); 77 | const programArray = parsedJson.filter((item) => typeof item === "number") as number[]; 78 | 79 | if (programArray.length) { 80 | setProgram(programArray); 81 | } 82 | setProgramError(""); 83 | } catch (error) { 84 | logger.error("Wrong JSON", { error, hideToast: true }); 85 | 86 | setProgram(undefined, "Wrong JSON"); 87 | 88 | if (error) { 89 | setProgramError(error.toString()); 90 | } 91 | } 92 | } 93 | }; 94 | 95 | return ( 96 |
97 | Generic PVM program bytes} /> 98 |
99 |