├── .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 |
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 |
9 |
--------------------------------------------------------------------------------
/src/assets/sidebar/brick.svg:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/src/assets/sidebar/chip.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/sidebar/computers.svg:
--------------------------------------------------------------------------------
1 |
34 |
--------------------------------------------------------------------------------
/src/assets/sidebar/debugger.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/sidebar/logo.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/assets/sidebar/stack.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
112 | {isProgramInvalid && {programError || "Program is not valid"}}
113 |
114 |
115 | );
116 | };
117 |
--------------------------------------------------------------------------------
/src/components/SearchInput/index.tsx:
--------------------------------------------------------------------------------
1 | import { SearchIcon } from "lucide-react";
2 | import React from "react";
3 | import { INPUT_STYLES, InputProps } from "../ui/input";
4 | import { cn } from "@/lib/utils";
5 |
6 | export type SearchProps = React.InputHTMLAttributes;
7 |
8 | const Search = React.forwardRef(
9 | ({ className, inputClassName, ...props }, ref) => {
10 | return (
11 |
18 |
19 |
27 |
28 | );
29 | },
30 | );
31 |
32 | Search.displayName = "Search";
33 |
34 | export { Search };
35 |
--------------------------------------------------------------------------------
/src/components/WithHelp/WithHelp.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Popover, PopoverContent } from "../ui/popover";
3 | import { PopoverTrigger } from "@radix-ui/react-popover";
4 | import { InfoIcon } from "lucide-react";
5 |
6 | export function WithHelp({ help, children }: { help: string; children: ReactNode }) {
7 | return (
8 | <>
9 | {children}{" "}
10 |
11 |
15 |
16 |
17 |
18 | {help}
19 |
20 |
21 | >
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
3 | import { ChevronDown, ChevronUp } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const Accordion = AccordionPrimitive.Root;
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
14 | ));
15 | AccordionItem.displayName = "AccordionItem";
16 |
17 | const AccordionTrigger = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef & { isOpen: boolean }
20 | >(({ className, children, ...props }, ref) => {
21 | const { isOpen, ...restProps } = props;
22 | return (
23 | svg]:rotate-180",
26 | className,
27 | )}
28 | >
29 | {children}
30 |
31 |
32 | {isOpen ? (
33 |
34 | ) : (
35 |
36 | )}
37 |
38 |
39 | );
40 | });
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | brand:
12 | "border-transparent bg-brand-dark dark:bg-brand text-brand-light dark:text-secondary hover:bg-brand-dark/80",
13 | default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
14 | secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16 | outline: "text-foreground",
17 | },
18 | },
19 | defaultVariants: {
20 | variant: "default",
21 | },
22 | },
23 | );
24 |
25 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
26 |
27 | function Badge({ className, variant, ...props }: BadgeProps) {
28 | return ;
29 | }
30 |
31 | export { Badge };
32 |
--------------------------------------------------------------------------------
/src/components/ui/button-variants.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority";
2 |
3 | export const buttonVariants = cva(
4 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
5 | {
6 | variants: {
7 | variant: {
8 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
9 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
10 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
11 | outlineBrand:
12 | "border border-input border-brand-dark text-brand-dark dark:border-brand dark:text-brand hover:bg-accent hover:text-accent-foreground",
13 | secondary:
14 | "bg-secondary text-secondary-foreground hover:bg-black hover:text-background dark:hover:bg-brand dark:hover:text-background",
15 | ghost: "hover:bg-accent hover:text-accent-foreground",
16 | link: "text-primary underline-offset-4 hover:underline",
17 | },
18 | size: {
19 | default: "h-10 px-4 py-2",
20 | sm: "h-9 rounded-md px-3",
21 | lg: "h-11 rounded-md px-8",
22 | icon: "h-4 w-4 -mt-2",
23 | },
24 | },
25 | defaultVariants: {
26 | variant: "default",
27 | size: "default",
28 | },
29 | },
30 | );
31 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { type VariantProps } from "class-variance-authority";
6 | import { buttonVariants } from "./button-variants";
7 |
8 | export interface ButtonProps
9 | extends React.ButtonHTMLAttributes,
10 | VariantProps {
11 | asChild?: boolean;
12 | }
13 |
14 | export const Button = React.forwardRef(
15 | ({ className, variant, size, asChild = false, ...props }, ref) => {
16 | const Comp = asChild ? Slot : "button";
17 | return ;
18 | },
19 | );
20 | Button.displayName = "Button";
21 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef>(({ className, ...props }, ref) => (
6 |
7 | ));
8 | Card.displayName = "Card";
9 |
10 | const CardHeader = React.forwardRef>(
11 | ({ className, ...props }, ref) => (
12 |
13 | ),
14 | );
15 | CardHeader.displayName = "CardHeader";
16 |
17 | const CardTitle = React.forwardRef>(
18 | ({ className, ...props }, ref) => (
19 |
20 | ),
21 | );
22 | CardTitle.displayName = "CardTitle";
23 |
24 | const CardDescription = React.forwardRef>(
25 | ({ className, ...props }, ref) => (
26 |
27 | ),
28 | );
29 | CardDescription.displayName = "CardDescription";
30 |
31 | const CardContent = React.forwardRef>(
32 | ({ className, ...props }, ref) => ,
33 | );
34 | CardContent.displayName = "CardContent";
35 |
36 | const CardFooter = React.forwardRef>(
37 | ({ className, ...props }, ref) => (
38 |
39 | ),
40 | );
41 | CardFooter.displayName = "CardFooter";
42 |
43 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
44 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2 |
3 | const Collapsible = CollapsiblePrimitive.Root;
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent };
10 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as DialogPrimitive from "@radix-ui/react-dialog";
3 | import { X } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const Dialog = DialogPrimitive.Root;
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger;
10 |
11 | const DialogPortal = DialogPrimitive.Portal;
12 |
13 | const DialogClose = DialogPrimitive.Close;
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef & {
33 | hideClose?: boolean;
34 | }
35 | >(({ className, children, hideClose, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 | {!hideClose && (
48 |
49 |
50 | Close
51 |
52 | )}
53 |
54 |
55 | ));
56 | DialogContent.displayName = DialogPrimitive.Content.displayName;
57 |
58 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
59 |
60 | );
61 | DialogHeader.displayName = "DialogHeader";
62 |
63 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
64 |
65 | );
66 | DialogFooter.displayName = "DialogFooter";
67 |
68 | const DialogTitle = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
77 | ));
78 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
79 |
80 | const DialogDescription = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
85 | ));
86 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
87 |
88 | export {
89 | Dialog,
90 | DialogPortal,
91 | DialogOverlay,
92 | DialogClose,
93 | DialogTrigger,
94 | DialogContent,
95 | DialogHeader,
96 | DialogFooter,
97 | DialogTitle,
98 | DialogDescription,
99 | };
100 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Drawer as DrawerPrimitive } from "vaul";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps) => (
7 |
8 | );
9 | Drawer.displayName = "Drawer";
10 |
11 | const DrawerTrigger = DrawerPrimitive.Trigger;
12 |
13 | const DrawerPortal = DrawerPrimitive.Portal;
14 |
15 | const DrawerClose = DrawerPrimitive.Close;
16 |
17 | const DrawerOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
22 | ));
23 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
24 |
25 | const DrawerContent = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, children, ...props }, ref) => (
29 |
30 |
31 |
39 |
40 | {children}
41 |
42 |
43 | ));
44 | DrawerContent.displayName = "DrawerContent";
45 |
46 | const DrawerHeader = ({ className, ...props }: React.HTMLAttributes) => (
47 |
48 | );
49 | DrawerHeader.displayName = "DrawerHeader";
50 |
51 | const DrawerFooter = ({ className, ...props }: React.HTMLAttributes) => (
52 |
53 | );
54 | DrawerFooter.displayName = "DrawerFooter";
55 |
56 | const DrawerTitle = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
65 | ));
66 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
67 |
68 | const DrawerDescription = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
73 | ));
74 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
75 |
76 | export {
77 | Drawer,
78 | DrawerPortal,
79 | DrawerOverlay,
80 | DrawerTrigger,
81 | DrawerClose,
82 | DrawerContent,
83 | DrawerHeader,
84 | DrawerFooter,
85 | DrawerTitle,
86 | DrawerDescription,
87 | };
88 |
--------------------------------------------------------------------------------
/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const HoverCard = HoverCardPrimitive.Root;
7 |
8 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
9 |
10 | const HoverCardContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
24 | ));
25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
26 |
27 | export { HoverCard, HoverCardTrigger, HoverCardContent };
28 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps extends React.InputHTMLAttributes {}
6 | export const INPUT_STYLES =
7 | "flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50";
8 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
9 | return ;
10 | });
11 | Input.displayName = "Input";
12 |
13 | export { Input };
14 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
8 |
9 | const Label = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef & VariantProps
12 | >(({ className, ...props }, ref) => (
13 |
14 | ));
15 | Label.displayName = LabelPrimitive.Root.displayName;
16 |
17 | export { Label };
18 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PopoverPrimitive from "@radix-ui/react-popover";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Popover = PopoverPrimitive.Root;
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger;
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ));
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
28 |
29 | export { Popover, PopoverTrigger, PopoverContent };
30 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
3 | import { Circle } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return ;
12 | });
13 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
14 |
15 | const RadioGroupItem = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => {
19 | return (
20 |
28 |
29 |
30 |
31 |
32 | );
33 | });
34 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
35 |
36 | export { RadioGroup, RadioGroupItem };
37 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
10 |
17 | ));
18 | Separator.displayName = SeparatorPrimitive.Root.displayName;
19 |
20 | export { Separator };
21 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SwitchPrimitives from "@radix-ui/react-switch";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef & { variant?: string }
9 | >(({ className, variant, ...props }, ref) => (
10 |
21 |
27 |
28 | ));
29 | Switch.displayName = SwitchPrimitives.Root.displayName;
30 |
31 | export { Switch };
32 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Table = React.forwardRef>(
6 | ({ className, ...props }, ref) => (
7 |
10 | ),
11 | );
12 | Table.displayName = "Table";
13 |
14 | const TableHeader = React.forwardRef>(
15 | ({ className, ...props }, ref) => ,
16 | );
17 | TableHeader.displayName = "TableHeader";
18 |
19 | const TableBody = React.forwardRef>(
20 | ({ className, ...props }, ref) => (
21 |
22 | ),
23 | );
24 | TableBody.displayName = "TableBody";
25 |
26 | const TableFooter = React.forwardRef>(
27 | ({ className, ...props }, ref) => (
28 | tr]:last:border-b-0", className)} {...props} />
29 | ),
30 | );
31 | TableFooter.displayName = "TableFooter";
32 |
33 | const TableRow = React.forwardRef>(
34 | ({ className, ...props }, ref) => (
35 |
40 | ),
41 | );
42 | TableRow.displayName = "TableRow";
43 |
44 | const TableHead = React.forwardRef>(
45 | ({ className, ...props }, ref) => (
46 | |
54 | ),
55 | );
56 | TableHead.displayName = "TableHead";
57 |
58 | const TableCell = React.forwardRef>(
59 | ({ className, ...props }, ref) => (
60 | |
61 | ),
62 | );
63 | TableCell.displayName = "TableCell";
64 |
65 | const TableCaption = React.forwardRef>(
66 | ({ className, ...props }, ref) => (
67 |
68 | ),
69 | );
70 | TableCaption.displayName = "TableCaption";
71 |
72 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
73 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
19 | ));
20 | TabsList.displayName = TabsPrimitive.List.displayName;
21 |
22 | const TabsTrigger = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef
25 | >(({ className, ...props }, ref) => (
26 |
34 | ));
35 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
36 |
37 | const TabsContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, ...props }, ref) => (
41 |
49 | ));
50 | TabsContent.displayName = TabsPrimitive.Content.displayName;
51 |
52 | export { Tabs, TabsList, TabsTrigger, TabsContent };
53 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
8 | return (
9 |
17 | );
18 | });
19 | Textarea.displayName = "Textarea";
20 |
21 | export { Textarea };
22 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const TooltipPortal = TooltipPrimitive.Portal;
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, TooltipPortal };
31 |
--------------------------------------------------------------------------------
/src/context/NumeralSystem.tsx:
--------------------------------------------------------------------------------
1 | export enum NumeralSystem {
2 | DECIMAL,
3 | HEXADECIMAL,
4 | BINARY,
5 | }
6 |
--------------------------------------------------------------------------------
/src/context/NumeralSystemContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import { NumeralSystem } from "./NumeralSystem";
3 |
4 | export const NumeralSystemContext = createContext({
5 | numeralSystem: NumeralSystem.DECIMAL,
6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
7 | setNumeralSystem: (_: NumeralSystem) => {},
8 | });
9 |
--------------------------------------------------------------------------------
/src/context/NumeralSystemProvider.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from "react";
2 | import { NumeralSystem } from "./NumeralSystem";
3 | import { NumeralSystemContext } from "./NumeralSystemContext";
4 |
5 | export const NumeralSystemProvider = ({ children }: { children: ReactNode }) => {
6 | const [numeralSystem, setNumeralSystem] = useState(NumeralSystem.HEXADECIMAL);
7 |
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/globals.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
2 | @import url("https://fonts.googleapis.com/css2?family=Inconsolata&display=swap");
3 |
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | /* @font-face {
9 | font-family: Oswald;
10 | font-style: normal;
11 | font-weight: 200 700;
12 | font-display: swap;
13 | src: url("/fonts/Oswald.woff2") format("woff2");
14 | } */
15 |
16 | @layer base {
17 | :root {
18 | --title-foreground: 0 0% 56%;
19 | --title: 0 0% 97%;
20 | --title-secondary-foreground: 0 0% 70.2%;
21 |
22 | --brand-light: 175.56 100% 94.71%;
23 | --brand: 175.29 79.55% 65.49%;
24 | --brand-dark: 175.26 76.77% 38.82%;
25 | /* ShadCN colors */
26 | --background: 0 0% 100%;
27 | --foreground: 218 22% 10%;
28 | --card: var(--background);
29 | --card-foreground: var(--title-foreground);
30 | --popover: var(--background);
31 | --popover-foreground: var(--title-foreground);
32 | /* Buttons */
33 | --secondary-foreground: 0 0% 36%;
34 | --secondary: var(--title);
35 | --primary-foreground: 220 20% 97.25%;
36 | --primary: 0 0% 14.12%;
37 | /* */
38 | --muted: 220 20% 97%;
39 | --muted-foreground: 0 0% 72%;
40 | --accent: 0 0 97%;
41 | --accent-foreground: 175.26 100% 25.82%;
42 | /* Defaults */
43 | --destructive: 0 84.2% 60.2%;
44 | --destructive-foreground: 1 61% 56%;
45 | /* */
46 | --border: 0 0% 94%;
47 | --input: var(--border);
48 | --ring: var(--title-secondary-foreground);
49 | --radius: 0.5rem;
50 |
51 | --sidebar: var(--secondary);
52 | --sidebar-foreground: 0 0% 73%;
53 |
54 | --toastify-toast-width: 350px;
55 |
56 | --font-display: : "Poppins", "sans-serif";
57 | }
58 |
59 | .dark {
60 | --brand-light: 175.56 100% 94.71%;
61 | --brand: 175.29 79.55% 65.49%;
62 | --brand-dark: 174.46 100% 12.75%;
63 |
64 | --background: 0, 0%, 20%;
65 | --foreground: 0 0% 70%;
66 | --card: 0 0% 14%;
67 | --title: 0 0% 18%;
68 | --title-foreground: 0 0% 56%;
69 | --border: 0 0% 27%;
70 | --input: var(--border);
71 | --ring: var(--brand);
72 |
73 | --secondary: 0 0% 23%;
74 | --secondary-foreground: 0 0% 52%;
75 |
76 | --card-foreground: var(--title-foreground);
77 | --popover: 190 84% 4.9%;
78 | --popover-foreground: 210 40% 98%;
79 | --primary: 175 80% 65%;
80 | --primary-foreground: 190 47.4% 11.2%;
81 | --muted: 0 0% 0;
82 | --muted-foreground: var(--title-foreground);
83 | --accent: var(--background);
84 | --accent-foreground: var(--foreground);
85 | --destructive: 0 62.8% 30.6%;
86 | --destructive-foreground: 0.88 60.71% 56.08%;
87 |
88 | --sidebar: 0 0% 14%;
89 | --sidebar-foreground: 0 0% 32%;
90 | }
91 | }
92 |
93 | @layer base {
94 | * {
95 | @apply border-border;
96 | }
97 | body {
98 | @apply bg-background text-foreground;
99 | @apply font-poppins;
100 | }
101 |
102 | @font-face {
103 | font-family: Oswald;
104 | font-style: normal;
105 | font-weight: 200 700;
106 | font-display: swap;
107 | src: url("/fonts/Oswald.woff2") format("woff2");
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | #root {
2 | min-height: 100dvh;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | body {
8 | overflow: hidden;
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "react-json-view-compare";
2 |
3 | declare module "path-browserify" {
4 | import path from "path";
5 | export default path;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 | import { MemorySegment, SpiMemory } from "@typeberry/pvm-debugger-adapter";
4 | import { MemoryChunkItem, PageMapItem } from "@/types/pvm.ts";
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
10 | export const hexToRgb = (hex: string) => {
11 | const bigint = parseInt(hex.slice(1), 16);
12 | const r = (bigint >> 16) & 255;
13 | const g = (bigint >> 8) & 255;
14 | const b = bigint & 255;
15 | return `${r}, ${g}, ${b}`;
16 | };
17 |
18 | export const invertHexColor = (hex: string) => {
19 | hex = hex.replace(/^#/, "");
20 |
21 | if (hex.length === 3) {
22 | hex = hex
23 | .split("")
24 | .map((char) => char + char)
25 | .join("");
26 | }
27 |
28 | const invertedColor = (0xffffff ^ parseInt(hex, 16)).toString(16).padStart(6, "0");
29 |
30 | return `#${invertedColor}`;
31 | };
32 |
33 | export interface SerializedFile {
34 | name: string;
35 | size: number;
36 | type: string;
37 | content: string;
38 | }
39 |
40 | export const serializeFile = async (file: File) => {
41 | const encodeFileToBase64 = (file: File) => {
42 | return new Promise((resolve, reject) => {
43 | const reader = new FileReader();
44 | reader.onload = () => resolve(reader.result);
45 | reader.onerror = (error) => reject(error);
46 | reader.readAsDataURL(file); // Encodes the file content as Base64
47 | });
48 | };
49 |
50 | const base64 = await encodeFileToBase64(file);
51 | return {
52 | name: file.name,
53 | size: file.size,
54 | type: file.type,
55 | content: base64,
56 | };
57 | };
58 |
59 | export const deserializeFile = (fileData: SerializedFile) => {
60 | const byteString = atob(fileData.content.split(",")[1]);
61 | const mimeType = fileData.type;
62 | const arrayBuffer = new ArrayBuffer(byteString.length);
63 | const uint8Array = new Uint8Array(arrayBuffer);
64 |
65 | for (let i = 0; i < byteString.length; i++) {
66 | uint8Array[i] = byteString.charCodeAt(i);
67 | }
68 |
69 | return new File([uint8Array], fileData.name, { type: mimeType });
70 | };
71 |
72 | const PAGE_SHIFT = 12; // log_2(4096)
73 | const PAGE_SIZE = 1 << PAGE_SHIFT;
74 |
75 | function pageAlignUp(v: number) {
76 | return (((v + PAGE_SIZE - 1) >> PAGE_SHIFT) << PAGE_SHIFT) >>> 0;
77 | }
78 |
79 | function asChunks(mem: MemorySegment[]): MemoryChunkItem[] {
80 | const items = [];
81 | for (const segment of mem) {
82 | if (segment.data === null) {
83 | continue;
84 | }
85 | let data = segment.data;
86 | let address = segment.start;
87 | while (data.length > 0) {
88 | const pageOffset = address % PAGE_SIZE;
89 | const lenForPage = PAGE_SIZE - pageOffset;
90 | const contents = Array.from(data.subarray(0, Math.min(data.length, lenForPage)));
91 | items.push({
92 | address,
93 | contents,
94 | });
95 |
96 | // move data & address
97 | data = data.subarray(contents.length);
98 | address += contents.length;
99 | }
100 | }
101 | return items;
102 | }
103 |
104 | function asPageMap(mem: MemorySegment[], isWriteable: boolean): PageMapItem[] {
105 | const items = [];
106 | for (const segment of mem) {
107 | const pageOffset = segment.start % PAGE_SIZE;
108 | const pageStart = segment.start - pageOffset;
109 | const pageEnd = pageAlignUp(segment.end);
110 | for (let i = pageStart; i < pageEnd; i += PAGE_SIZE) {
111 | items.push({
112 | address: i,
113 | length: Math.min(PAGE_SIZE, pageEnd - i),
114 | "is-writable": isWriteable,
115 | });
116 | }
117 | }
118 | return items;
119 | }
120 |
121 | export function getAsPageMap(mem: SpiMemory): PageMapItem[] {
122 | return asPageMap(mem.readable, false).concat(asPageMap(mem.writeable, true));
123 | }
124 |
125 | export function getAsChunks(mem: SpiMemory): MemoryChunkItem[] {
126 | return asChunks(mem.readable).concat(asChunks(mem.writeable));
127 | }
128 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App.tsx";
3 | import "./index.css";
4 | import "./globals.css";
5 | import { NumeralSystemProvider } from "@/context/NumeralSystemProvider";
6 | import { Provider } from "react-redux";
7 | import { persistor, store } from "./store";
8 | import { HashRouter } from "react-router";
9 | import { PersistGate } from "redux-persist/integration/react";
10 | import { isDarkMode, setColorMode } from "./packages/ui-kit/DarkMode/utils.ts";
11 | import { TooltipProvider } from "@radix-ui/react-tooltip";
12 |
13 | setColorMode(isDarkMode());
14 |
15 | ReactDOM.createRoot(document.getElementById("root")!).render(
16 | // TODO: strict mode is disabled because of the App useEffect for init being called twice
17 | //
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ,
29 | // ,
30 | );
31 |
--------------------------------------------------------------------------------
/src/packages/host-calls/read.ts:
--------------------------------------------------------------------------------
1 | import { block, bytes, hash, read } from "@typeberry/pvm-debugger-adapter";
2 | import { Storage } from "../web-worker/types";
3 |
4 | export class ReadAccounts implements read.Accounts {
5 | constructor(data: Storage) {
6 | this.data = data;
7 | }
8 |
9 | public readonly data: Storage = new Map();
10 |
11 | async read(_serviceId: block.ServiceId, hash: hash.Blake2bHash): Promise {
12 | const d = this.data.get(hash.toString());
13 | if (d === undefined) {
14 | return null;
15 | }
16 |
17 | return d;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/packages/host-calls/write.ts:
--------------------------------------------------------------------------------
1 | import { write, hash, block, bytes } from "@typeberry/pvm-debugger-adapter";
2 | import { Storage } from "../web-worker/types";
3 |
4 | export class WriteAccounts implements write.Accounts {
5 | constructor(data: Storage) {
6 | this.data = data;
7 | }
8 |
9 | public readonly data: Storage = new Map();
10 | public isFull = false;
11 |
12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
13 | isStorageFull(_serviceId: block.ServiceId): Promise {
14 | return Promise.resolve(this.isFull);
15 | }
16 |
17 | write(_serviceId: block.ServiceId, hash: hash.Blake2bHash, data: bytes.BytesBlob | null): Promise {
18 | if (data === null) {
19 | this.data.delete(hash.toString());
20 | } else {
21 | this.data.set(hash.toString(), data);
22 | }
23 |
24 | return Promise.resolve();
25 | }
26 |
27 | readSnapshotLen(): Promise {
28 | return Promise.resolve(0);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/packages/pvm/jam-codec/decode-natural-number.ts:
--------------------------------------------------------------------------------
1 | import { LittleEndianDecoder } from "./little-endian-decoder";
2 |
3 | const MASKS = [0xff, 0xfe, 0xfc, 0xf8, 0xf0, 0xe0, 0xc0, 0x80];
4 |
5 | type Result = {
6 | bytesToSkip: number;
7 | value: bigint;
8 | };
9 |
10 | export function decodeNaturalNumber(bytes: Uint8Array): Result {
11 | const littleEndianDecoder = new LittleEndianDecoder();
12 | const l = decodeLengthAfterFirstByte(bytes[0]);
13 | const bytesToSkip = l + 1;
14 |
15 | if (l === 8) {
16 | return {
17 | value: littleEndianDecoder.decodeU64(bytes.subarray(1, 9)),
18 | bytesToSkip,
19 | };
20 | }
21 |
22 | if (l === 0) {
23 | return { value: BigInt(bytes[0]), bytesToSkip };
24 | }
25 |
26 | const restBytesValue = littleEndianDecoder.decodeU64(bytes.subarray(1, l + 1));
27 | const firstByteValue = BigInt(bytes[0]) + 2n ** (8n - BigInt(l)) - 2n ** 8n;
28 |
29 | return {
30 | value: restBytesValue + (firstByteValue << (8n * BigInt(l))),
31 | bytesToSkip,
32 | };
33 | }
34 |
35 | function decodeLengthAfterFirstByte(firstByte: number) {
36 | for (let i = 0; i < MASKS.length; i++) {
37 | if (firstByte >= MASKS[i]) {
38 | return 8 - i;
39 | }
40 | }
41 |
42 | return 0;
43 | }
44 |
--------------------------------------------------------------------------------
/src/packages/pvm/jam-codec/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./decode-natural-number";
2 |
--------------------------------------------------------------------------------
/src/packages/pvm/jam-codec/little-endian-decoder.test.ts:
--------------------------------------------------------------------------------
1 | import assert from "node:assert";
2 | import { describe, it } from "node:test";
3 |
4 | import { LittleEndianDecoder } from "./little-endian-decoder";
5 |
6 | describe("LittleEndianDecoder", () => {
7 | describe("LittleEndianDecoder.decodeU64", () => {
8 | it("Empty bytes array", () => {
9 | const decoder = new LittleEndianDecoder();
10 |
11 | const encodedBytes = new Uint8Array([]);
12 | const expectedValue = 0n;
13 |
14 | const result = decoder.decodeU64(encodedBytes);
15 |
16 | assert.strictEqual(result, expectedValue);
17 | });
18 |
19 | it("1 byte number", () => {
20 | const decoder = new LittleEndianDecoder();
21 |
22 | const encodedBytes = new Uint8Array([0xff]);
23 | const expectedValue = 255n;
24 |
25 | const result = decoder.decodeU64(encodedBytes);
26 |
27 | assert.strictEqual(result, expectedValue);
28 | });
29 |
30 | it("2 bytes number", () => {
31 | const decoder = new LittleEndianDecoder();
32 |
33 | const encodedBytes = new Uint8Array([0xff, 0x01]);
34 | const expectedValue = 511n;
35 |
36 | const result = decoder.decodeU64(encodedBytes);
37 |
38 | assert.strictEqual(result, expectedValue);
39 | });
40 |
41 | it("4 bytes number", () => {
42 | const decoder = new LittleEndianDecoder();
43 |
44 | const encodedBytes = new Uint8Array([0xff, 0x56, 0x34, 0x12]);
45 | const expectedValue = 305420031n;
46 |
47 | const result = decoder.decodeU64(encodedBytes);
48 |
49 | assert.strictEqual(result, expectedValue);
50 | });
51 |
52 | it("8 bytes number", () => {
53 | const decoder = new LittleEndianDecoder();
54 |
55 | const encodedBytes = new Uint8Array([0xff, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12]);
56 | const expectedValue = 1311768467463790335n;
57 |
58 | const result = decoder.decodeU64(encodedBytes);
59 |
60 | assert.strictEqual(result, expectedValue);
61 | });
62 | });
63 |
64 | describe("LittleEndianDecoder.decodeU32", () => {
65 | it("Empty bytes array", () => {
66 | const decoder = new LittleEndianDecoder();
67 |
68 | const encodedBytes = new Uint8Array([]);
69 | const expectedValue = 0;
70 |
71 | const result = decoder.decodeU32(encodedBytes);
72 |
73 | assert.strictEqual(result, expectedValue);
74 | });
75 |
76 | it("1 byte number", () => {
77 | const decoder = new LittleEndianDecoder();
78 |
79 | const encodedBytes = new Uint8Array([0xff]);
80 | const expectedValue = 255;
81 |
82 | const result = decoder.decodeU32(encodedBytes);
83 |
84 | assert.strictEqual(result, expectedValue);
85 | });
86 |
87 | it("2 bytes number", () => {
88 | const decoder = new LittleEndianDecoder();
89 |
90 | const encodedBytes = new Uint8Array([0xff, 0x01]);
91 | const expectedValue = 511;
92 |
93 | const result = decoder.decodeU32(encodedBytes);
94 |
95 | assert.strictEqual(result, expectedValue);
96 | });
97 |
98 | it("4 bytes number", () => {
99 | const decoder = new LittleEndianDecoder();
100 |
101 | const encodedBytes = new Uint8Array([0xff, 0x56, 0x34, 0x12]);
102 | const expectedValue = 305420031;
103 |
104 | const result = decoder.decodeU32(encodedBytes);
105 |
106 | assert.strictEqual(result, expectedValue);
107 | });
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/src/packages/pvm/jam-codec/little-endian-decoder.ts:
--------------------------------------------------------------------------------
1 | const BUFFER_SIZE = 8;
2 |
3 | export class LittleEndianDecoder {
4 | private buffer = new ArrayBuffer(BUFFER_SIZE);
5 | private u64ValueArray = new BigUint64Array(this.buffer);
6 | private u32ValueArray = new Uint32Array(this.buffer);
7 | private view = new DataView(this.buffer);
8 |
9 | private loadBytes(bytes: Uint8Array) {
10 | const n = bytes.length;
11 | const noOfBytes = Math.min(n, BUFFER_SIZE);
12 |
13 | for (let i = 0; i < noOfBytes; i++) {
14 | this.view.setUint8(i, bytes[i]);
15 | }
16 |
17 | for (let i = n; i < BUFFER_SIZE; i++) {
18 | this.view.setUint8(i, 0x00);
19 | }
20 | }
21 |
22 | decodeU64(bytes: Uint8Array) {
23 | this.loadBytes(bytes);
24 | return this.u64ValueArray[0];
25 | }
26 |
27 | decodeU32(bytes: Uint8Array) {
28 | this.loadBytes(bytes);
29 | return this.u32ValueArray[0];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/packages/pvm/jam-codec/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@typeberry/jam-codec",
3 | "version": "0.0.1",
4 | "description": "Serialization and deserialization codec for JAM.",
5 | "main": "index.ts",
6 | "scripts": {
7 | "test": "node --test --require ts-node/register $(find . -type f -name '*.test.ts' | tr '\\\\n' ' ')"
8 | },
9 | "author": "Fluffy Labs",
10 | "license": "MPL-2.0"
11 | }
12 |
--------------------------------------------------------------------------------
/src/packages/pvm/pvm/disassemblify.ts:
--------------------------------------------------------------------------------
1 | import { virtualTrapInstruction } from "@/utils/virtualTrapInstruction";
2 | import { byteToOpCodeMap } from "./assemblify";
3 | import {
4 | ProgramDecoder,
5 | ArgsDecoder,
6 | instructionArgumentTypeMap,
7 | createResults,
8 | ArgumentType,
9 | BasicBlocks,
10 | } from "@typeberry/pvm-debugger-adapter";
11 | import { Instruction } from "./instruction";
12 | import { CurrentInstruction } from "@/types/pvm";
13 |
14 | export function disassemblify(rawProgram: Uint8Array): CurrentInstruction[] {
15 | const programDecoder = new ProgramDecoder(rawProgram);
16 | const code = programDecoder.getCode();
17 | const mask = programDecoder.getMask();
18 | const blocks = new BasicBlocks();
19 | blocks.reset(code, mask);
20 | const argsDecoder = new ArgsDecoder();
21 | argsDecoder.reset(code, mask);
22 | let i = 0;
23 | const printableProgram = [];
24 | let address = 0;
25 | let currentBlockNumber = -1;
26 |
27 | while (i < code.length) {
28 | const currentInstruction = code[i];
29 | const isValidInstruction = Instruction[currentInstruction] !== undefined;
30 | const argumentType = isValidInstruction
31 | ? instructionArgumentTypeMap[currentInstruction]
32 | : ArgumentType.NO_ARGUMENTS;
33 | const args = createResults()[argumentType];
34 | const isBlockStart = blocks.isBeginningOfBasicBlock(i);
35 | const isBlockEnd = blocks.isBeginningOfBasicBlock(i + 1);
36 |
37 | try {
38 | argsDecoder.fillArgs(i, args);
39 | address = i;
40 | i += args.noOfBytesToSkip ?? 0;
41 | } catch (e) {
42 | printableProgram.push({
43 | instructionCode: currentInstruction,
44 | name: "Error",
45 | address,
46 | ...byteToOpCodeMap[currentInstruction],
47 | error: "Cannot get arguments from args decoder",
48 | block: {
49 | isStart: isBlockStart,
50 | isEnd: isBlockEnd,
51 | name: "block",
52 | number: currentBlockNumber,
53 | },
54 | });
55 | return printableProgram;
56 | }
57 |
58 | if (isBlockStart) {
59 | currentBlockNumber++;
60 | }
61 |
62 | const currentInstructionDebug = {
63 | instructionCode: currentInstruction,
64 | ...byteToOpCodeMap[currentInstruction],
65 | name: isValidInstruction ? Instruction[currentInstruction] : `INVALID(${currentInstruction})`,
66 | instructionBytes: code.slice(i - (args.noOfBytesToSkip ?? 0), i),
67 | address,
68 | args,
69 | block: {
70 | isStart: isBlockStart,
71 | isEnd: isBlockEnd,
72 | name: `block${currentBlockNumber}`,
73 | number: currentBlockNumber,
74 | },
75 | };
76 |
77 | printableProgram.push(currentInstructionDebug);
78 | }
79 | printableProgram.push({ ...virtualTrapInstruction, address: code.length });
80 |
81 | return printableProgram;
82 | }
83 |
--------------------------------------------------------------------------------
/src/packages/pvm/pvm/instruction.ts:
--------------------------------------------------------------------------------
1 | export enum Instruction {
2 | TRAP = 0,
3 | FALLTHROUGH = 1,
4 | ECALLI = 10,
5 | LOAD_IMM_64 = 20,
6 | STORE_IMM_U8 = 30,
7 | STORE_IMM_U16 = 31,
8 | STORE_IMM_U32 = 32,
9 | STORE_IMM_U64 = 33,
10 | JUMP = 40,
11 | JUMP_IND = 50,
12 | LOAD_IMM = 51,
13 | LOAD_U8 = 52,
14 | LOAD_I8 = 53,
15 | LOAD_U16 = 54,
16 | LOAD_I16 = 55,
17 | LOAD_U32 = 56,
18 | LOAD_I32 = 57,
19 | LOAD_U64 = 58,
20 | STORE_U8 = 59,
21 | STORE_U16 = 60,
22 | STORE_U32 = 61,
23 | STORE_U64 = 62,
24 | STORE_IMM_IND_U8 = 70,
25 | STORE_IMM_IND_U16 = 71,
26 | STORE_IMM_IND_U32 = 72,
27 | STORE_IMM_IND_U64 = 73,
28 | LOAD_IMM_JUMP = 80,
29 | BRANCH_EQ_IMM = 81,
30 | BRANCH_NE_IMM = 82,
31 | BRANCH_LT_U_IMM = 83,
32 | BRANCH_LE_U_IMM = 84,
33 | BRANCH_GE_U_IMM = 85,
34 | BRANCH_GT_U_IMM = 86,
35 | BRANCH_LT_S_IMM = 87,
36 | BRANCH_LE_S_IMM = 88,
37 | BRANCH_GE_S_IMM = 89,
38 | BRANCH_GT_S_IMM = 90,
39 | MOVE_REG = 100,
40 | SBRK = 101,
41 | COUNT_SET_BITS_64 = 102,
42 | COUNT_SET_BITS_32 = 103,
43 | LEADING_ZERO_BITS_64 = 104,
44 | LEADING_ZERO_BITS_32 = 105,
45 | TRAILING_ZERO_BITS_64 = 106,
46 | TRAILING_ZERO_BITS_32 = 107,
47 | SIGN_EXTEND_8 = 108,
48 | SIGN_EXTEND_16 = 109,
49 | ZERO_EXTEND_16 = 110,
50 | REVERSE_BYTES = 111,
51 | STORE_IND_U8 = 120,
52 | STORE_IND_U16 = 121,
53 | STORE_IND_U32 = 122,
54 | STORE_IND_U64 = 123,
55 | LOAD_IND_U8 = 124,
56 | LOAD_IND_I8 = 125,
57 | LOAD_IND_U16 = 126,
58 | LOAD_IND_I16 = 127,
59 | LOAD_IND_U32 = 128,
60 | LOAD_IND_I32 = 129,
61 | LOAD_IND_U64 = 130,
62 | ADD_IMM_32 = 131,
63 | AND_IMM = 132,
64 | XOR_IMM = 133,
65 | OR_IMM = 134,
66 | MUL_IMM_32 = 135,
67 | SET_LT_U_IMM = 136,
68 | SET_LT_S_IMM = 137,
69 | SHLO_L_IMM_32 = 138,
70 | SHLO_R_IMM_32 = 139,
71 | SHAR_R_IMM_32 = 140,
72 | NEG_ADD_IMM_32 = 141,
73 | SET_GT_U_IMM = 142,
74 | SET_GT_S_IMM = 143,
75 | SHLO_L_IMM_ALT_32 = 144,
76 | SHLO_R_IMM_ALT_32 = 145,
77 | SHAR_R_IMM_ALT_32 = 146,
78 | CMOV_IZ_IMM = 147,
79 | CMOV_NZ_IMM = 148,
80 | ADD_IMM_64 = 149,
81 | MUL_IMM_64 = 150,
82 | SHLO_L_IMM_64 = 151,
83 | SHLO_R_IMM_64 = 152,
84 | SHAR_R_IMM_64 = 153,
85 | NEG_ADD_IMM_64 = 154,
86 | SHLO_L_IMM_ALT_64 = 155,
87 | SHLO_R_IMM_ALT_64 = 156,
88 | SHAR_R_IMM_ALT_64 = 157,
89 | ROT_R_64_IMM = 158,
90 | ROT_R_64_IMM_ALT = 159,
91 | ROT_R_32_IMM = 160,
92 | ROT_R_32_IMM_ALT = 161,
93 | BRANCH_EQ = 170,
94 | BRANCH_NE = 171,
95 | BRANCH_LT_U = 172,
96 | BRANCH_LT_S = 173,
97 | BRANCH_GE_U = 174,
98 | BRANCH_GE_S = 175,
99 | LOAD_IMM_JUMP_IND = 180,
100 | ADD_32 = 190,
101 | SUB_32 = 191,
102 | MUL_32 = 192,
103 | DIV_U_32 = 193,
104 | DIV_S_32 = 194,
105 | REM_U_32 = 195,
106 | REM_S_32 = 196,
107 | SHLO_L_32 = 197,
108 | SHLO_R_32 = 198,
109 | SHAR_R_32 = 199,
110 | ADD_64 = 200,
111 | SUB_64 = 201,
112 | MUL_64 = 202,
113 | DIV_U_64 = 203,
114 | DIV_S_64 = 204,
115 | REM_U_64 = 205,
116 | REM_S_64 = 206,
117 | SHLO_L_64 = 207,
118 | SHLO_R_64 = 208,
119 | SHAR_R_64 = 209,
120 | AND = 210,
121 | XOR = 211,
122 | OR = 212,
123 | MUL_UPPER_S_S = 213,
124 | MUL_UPPER_U_U = 214,
125 | MUL_UPPER_S_U = 215,
126 | SET_LT_U = 216,
127 | SET_LT_S = 217,
128 | CMOV_IZ = 218,
129 | CMOV_NZ = 219,
130 | ROT_L_64 = 220,
131 | ROT_L_32 = 221,
132 | ROT_R_64 = 222,
133 | ROT_R_32 = 223,
134 | AND_INV = 224,
135 | OR_INV = 225,
136 | XNOR = 226,
137 | MAX = 227,
138 | MAX_U = 228,
139 | MIN = 229,
140 | MIN_U = 230,
141 | }
142 |
143 | export const HIGHEST_INSTRUCTION_NUMBER = Instruction.MIN_U;
144 |
--------------------------------------------------------------------------------
/src/packages/pvm/utils/debug.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A function to perform runtime assertions.
3 | *
4 | * We avoid using `node:assert` to keep compatibility with a browser environment.
5 | * Note the checks should not have any side effects, since we might decide
6 | * to remove all of them in a post-processing step.
7 | */
8 | export function check(condition: boolean, message?: string): asserts condition is true {
9 | if (!condition) {
10 | throw new Error(`Assertion failure: ${message || ""}`);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/packages/pvm/utils/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Utilities that are widely used across typeberry.
3 | *
4 | * BIG FAT NOTE: Please think twice or thrice before adding something here.
5 | * The package should really contain only things that are pretty much essential
6 | * and used everywhere.
7 | *
8 | * It might be much better to create a small package just for the thing you
9 | * are thinking about adding here. It's easier to later consolide smaller
10 | * things into this `utils` package than to split it into separate parts
11 | * as an afterthought.
12 | */
13 |
14 | export * from "./debug";
15 | export * from "./opaque";
16 |
--------------------------------------------------------------------------------
/src/packages/pvm/utils/opaque.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview `Opaque` constructs a unique type which is a subset of Type with a
3 | * specified unique token Token. It means that base type cannot be assigned to unique type by accident.
4 | * Good examples of opaque types include:
5 | * - JWTs or other tokens - these are special kinds of string used for authorization purposes.
6 | * If your app uses multiple types of tokens each should be a separate opaque type to avoid confusion
7 | * - Specific currencies - amount of different currencies shouldn't be mixed
8 | * - Bitcoin address - special kind of string
9 | *
10 | * `type GithubAccessToken = Opaque;`
11 | * `type USD = Opaque;`
12 | * `type PositiveNumber = Opaque;
13 | *
14 | * More: https://github.com/ts-essentials/ts-essentials/blob/master/lib/opaque/README.md
15 | *
16 | * Copyright (c) 2018-2019 Chris Kaczor (github.com/krzkaczor)
17 | */
18 |
19 | type StringLiteral = Type extends string ? (string extends Type ? never : Type) : never;
20 |
21 | declare const __OPAQUE_TYPE__: unique symbol;
22 |
23 | export type WithOpaque = {
24 | readonly [__OPAQUE_TYPE__]: Token;
25 | };
26 |
27 | export type Opaque = Token extends StringLiteral ? Type & WithOpaque : never;
28 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/AppsSidebar/icons/Brick.tsx:
--------------------------------------------------------------------------------
1 | export const Brick = () => {
2 | return (
3 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/AppsSidebar/icons/Computers.tsx:
--------------------------------------------------------------------------------
1 | export const Computers = () => {
2 | return (
3 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/AppsSidebar/icons/Debugger.tsx:
--------------------------------------------------------------------------------
1 | export const Debugger = () => {
2 | return (
3 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/AppsSidebar/icons/Logo.tsx:
--------------------------------------------------------------------------------
1 | export const Logo = () => {
2 | return (
3 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/AppsSidebar/icons/Stack.tsx:
--------------------------------------------------------------------------------
1 | export const Stack = () => {
2 | return (
3 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/AppsSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import { ToggleDarkModeIcon } from "../DarkMode/ToggleDarkMode";
2 | import { Stack } from "./icons/Stack";
3 | import { Debugger } from "./icons/Debugger";
4 | import { Computers } from "./icons/Computers";
5 | import { Chip } from "./icons/Chip";
6 | import { Logo } from "./icons/Logo";
7 | import { cn } from "@/lib/utils";
8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
9 | import { ReactNode } from "react";
10 |
11 | export const AppsSidebar = () => {
12 | return (
13 |
14 |
15 | } />
16 | } active />
17 | } />
18 | {/*}
22 | />*/}
23 | } />
24 | } />
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | type SidebarLinkProps = {
35 | name: string;
36 | href: string;
37 | active?: boolean;
38 | icon: React.ReactNode;
39 | };
40 | function SidebarLink({ name, href, icon, active = false }: SidebarLinkProps) {
41 | return (
42 |
43 |
59 | {icon}
60 |
61 |
62 | );
63 | }
64 |
65 | function WithTooltip({ tooltip, children }: { tooltip: string; children: ReactNode }) {
66 | return (
67 |
68 | {children}
69 | {tooltip}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/DarkMode/ToggleDarkMode.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Moon, Sun } from "lucide-react";
3 | import { useIsDarkMode, useToggleColorMode } from "./utils";
4 | import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
5 |
6 | export const ToggleDarkMode = ({ className }: { className: string }) => {
7 | const isDark = useIsDarkMode();
8 | const toggleColorMode = useToggleColorMode();
9 | const onClick = (val: string) => {
10 | if ((isDark && val === "light") || (!isDark && val === "dark")) {
11 | toggleColorMode();
12 | }
13 | };
14 |
15 | return (
16 |
45 | );
46 | };
47 |
48 | export const ToggleDarkModeIcon = () => {
49 | const isDark = useIsDarkMode();
50 | const toggleColorMode = useToggleColorMode();
51 | const onCLick = toggleColorMode;
52 |
53 | return (
54 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/packages/ui-kit/DarkMode/utils.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | const themeChangeEvent = new Event("themeChange");
4 |
5 | export const isDarkMode = () =>
6 | localStorage.theme === "dark" ||
7 | (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches);
8 |
9 | export const useIsDarkMode = () => {
10 | const [theme, setTheme] = useState(() => isDarkMode());
11 |
12 | useEffect(() => {
13 | const handleThemeChange = () => setTheme(isDarkMode());
14 |
15 | window.addEventListener("themeChange", handleThemeChange);
16 | return () => window.removeEventListener("themeChange", handleThemeChange);
17 | }, []);
18 | return theme;
19 | };
20 |
21 | // TODO force only dark mode manually. Change wehn dark mode is stable
22 |
23 | export const setColorMode = (isDark: boolean) => {
24 | document.documentElement.classList.toggle("dark", isDark);
25 | };
26 |
27 | export const useToggleColorMode = () => {
28 | const isDark = useIsDarkMode();
29 |
30 | return () => {
31 | localStorage.theme = isDark ? "light" : "dark";
32 | setColorMode(!isDark);
33 | window.dispatchEvent(themeChangeEvent);
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/src/packages/web-worker/command-handlers/index.ts:
--------------------------------------------------------------------------------
1 | import { runInit } from "./init";
2 | import { runLoad } from "./load";
3 | import { runStep } from "./step";
4 | import { runMemory } from "./memory";
5 | import { runHostCall } from "./host-call.ts";
6 |
7 | export default {
8 | runInit,
9 | runLoad,
10 | runStep,
11 | runMemory,
12 | runHostCall,
13 | };
14 |
--------------------------------------------------------------------------------
/src/packages/web-worker/command-handlers/init.ts:
--------------------------------------------------------------------------------
1 | import { InitialState } from "@/types/pvm";
2 | import { initPvm } from "../pvm";
3 | import { chunksAsUint8, getState, isInternalPvm, pageMapAsUint8, regsAsUint8 } from "../utils";
4 | import { logger } from "@/utils/loggerService";
5 | import { CommandStatus, PvmApiInterface } from "../types";
6 |
7 | export type InitParams = {
8 | pvm: PvmApiInterface | null;
9 | program: Uint8Array;
10 | initialState: InitialState;
11 | };
12 | export type InitResponse = {
13 | initialState: InitialState;
14 | status: CommandStatus;
15 | error?: unknown;
16 | };
17 |
18 | const init = async ({ pvm, program, initialState }: InitParams) => {
19 | if (!pvm) {
20 | throw new Error("PVM is uninitialized.");
21 | }
22 | if (isInternalPvm(pvm)) {
23 | logger.info("PVM init internal", pvm);
24 | initPvm(pvm, program, initialState);
25 | } else {
26 | logger.info("PVM init external", pvm);
27 | const gas = initialState.gas || 10000;
28 | if (pvm.resetGenericWithMemory) {
29 | pvm.resetGenericWithMemory(
30 | program,
31 | regsAsUint8(initialState.regs),
32 | pageMapAsUint8(initialState.pageMap),
33 | chunksAsUint8(initialState.memory),
34 | BigInt(gas),
35 | );
36 | } else if (pvm.resetGeneric) {
37 | console.warn("Ignoring memory initialization, because there is no resetGenericWithMemory");
38 | pvm.resetGeneric(program, regsAsUint8(initialState.regs), BigInt(gas));
39 | }
40 | pvm.setNextProgramCounter && pvm.setNextProgramCounter(initialState.pc ?? 0);
41 | pvm.setGasLeft && pvm.setGasLeft(BigInt(gas));
42 | pvm.nextStep();
43 | }
44 | };
45 |
46 | export const runInit = async ({ pvm, program, initialState }: InitParams): Promise => {
47 | if (program.length === 0) {
48 | console.warn("Skipping init, no program yet.");
49 | return {
50 | status: CommandStatus.SUCCESS,
51 | initialState: {},
52 | };
53 | }
54 |
55 | try {
56 | await init({ pvm, program, initialState });
57 |
58 | return {
59 | status: CommandStatus.SUCCESS,
60 | initialState: pvm ? await getState(pvm) : {},
61 | };
62 | } catch (error) {
63 | return {
64 | status: CommandStatus.ERROR,
65 | error,
66 | initialState: pvm ? await getState(pvm) : {},
67 | };
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/src/packages/web-worker/command-handlers/load.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@/utils/loggerService";
2 | import { getMemorySize, loadArrayBufferAsWasm } from "../utils";
3 | import { CommandStatus, PvmApiInterface, PvmTypes } from "../types";
4 | import { Pvm as InternalPvmInstance } from "@typeberry/pvm-debugger-adapter";
5 | import { deserializeFile, SerializedFile } from "@/lib/utils.ts";
6 | import loadWasmFromWebsockets from "../wasmFromWebsockets";
7 |
8 | export type LoadParams = {
9 | type: PvmTypes;
10 | params: { url?: string; file?: SerializedFile };
11 | };
12 | export type LoadResponse = {
13 | pvm: PvmApiInterface | null;
14 | memorySize: number | null;
15 | status: CommandStatus;
16 | error?: unknown;
17 | socket?: WebSocket;
18 | };
19 |
20 | const load = async (
21 | args: LoadParams,
22 | ): Promise<{
23 | pvm?: PvmApiInterface;
24 | socket?: WebSocket;
25 | }> => {
26 | if (args.type === PvmTypes.BUILT_IN) {
27 | return {
28 | pvm: new InternalPvmInstance(),
29 | };
30 | } else if (args.type === PvmTypes.WASM_FILE) {
31 | if (!args.params.file) {
32 | throw new Error("No PVM file");
33 | }
34 |
35 | const file = deserializeFile(args.params.file);
36 |
37 | logger.info("Load WASM from file", file);
38 | const bytes = await file.arrayBuffer();
39 | const pvm = await loadArrayBufferAsWasm(bytes);
40 |
41 | return {
42 | pvm,
43 | };
44 | } else if (args.type === PvmTypes.WASM_URL) {
45 | const url = args.params.url ?? "";
46 | const isValidUrl = Boolean(new URL(url));
47 |
48 | if (!isValidUrl) {
49 | throw new Error("Invalid PVM URL");
50 | }
51 |
52 | logger.info("Load WASM from URL", url);
53 | const response = await fetch(url);
54 | const bytes = await response.arrayBuffer();
55 |
56 | const pvm = await loadArrayBufferAsWasm(bytes);
57 |
58 | return {
59 | pvm,
60 | };
61 | } else if (args.type === PvmTypes.WASM_WS) {
62 | return await loadWasmFromWebsockets();
63 | }
64 |
65 | return {};
66 | };
67 |
68 | export const runLoad = async (args: LoadParams): Promise => {
69 | try {
70 | const { pvm, socket } = await load(args);
71 | const memorySize = await getMemorySize(pvm);
72 | if (pvm) {
73 | return { pvm, memorySize, status: CommandStatus.SUCCESS, socket };
74 | }
75 | } catch (error) {
76 | return { pvm: null, memorySize: null, status: CommandStatus.ERROR, error };
77 | }
78 |
79 | return { pvm: null, memorySize: null, status: CommandStatus.ERROR, error: new Error("Unknown PVM type") };
80 | };
81 |
--------------------------------------------------------------------------------
/src/packages/web-worker/command-handlers/memory.ts:
--------------------------------------------------------------------------------
1 | import { CommandStatus, PvmApiInterface } from "../types";
2 |
3 | // Max memory size defined by the Gray paper (4GB)
4 | const MAX_ADDRESS = Math.pow(2, 32);
5 |
6 | export type MemoryParams = {
7 | pvm: PvmApiInterface | null;
8 | startAddress: number;
9 | stopAddress: number;
10 | memorySize: number | null;
11 | };
12 |
13 | export type MemoryResponse = {
14 | memoryChunk: Uint8Array;
15 | status: CommandStatus;
16 | error?: unknown;
17 | };
18 |
19 | export const getMemoryPage = async (pageNumber: number, pvm: PvmApiInterface | null) => {
20 | if (!pvm) {
21 | return new Uint8Array();
22 | }
23 |
24 | return pvm.getPageDump(pageNumber) || new Uint8Array();
25 | };
26 |
27 | const getMemory = async ({
28 | pvm,
29 | startAddress,
30 | stopAddress,
31 | memorySize,
32 | }: {
33 | pvm: PvmApiInterface;
34 | startAddress: number;
35 | stopAddress: number;
36 | memorySize: number;
37 | }): Promise => {
38 | const memoryChunk = new Uint8Array(stopAddress - startAddress);
39 | let memoryIndex = 0;
40 | let address = startAddress;
41 |
42 | while (address < stopAddress) {
43 | const pageNumber = Math.floor(address / memorySize);
44 | const page = await getMemoryPage(pageNumber, pvm);
45 | let offset = address % memorySize;
46 |
47 | while (address < stopAddress && offset < memorySize) {
48 | memoryChunk[memoryIndex] = page[offset];
49 | memoryIndex++;
50 | address++;
51 | offset++;
52 | }
53 | }
54 |
55 | return memoryChunk;
56 | };
57 |
58 | export const runMemory = async (params: MemoryParams): Promise => {
59 | if (!params.pvm) {
60 | return {
61 | memoryChunk: new Uint8Array(),
62 | status: CommandStatus.ERROR,
63 | error: new Error("PVM is uninitialized."),
64 | };
65 | }
66 |
67 | if (!params.memorySize) {
68 | return {
69 | memoryChunk: new Uint8Array(),
70 | status: CommandStatus.ERROR,
71 | error: new Error("Memory size is not defined"),
72 | };
73 | }
74 |
75 | if (params.startAddress < 0 || params.stopAddress < 0) {
76 | return {
77 | memoryChunk: new Uint8Array(),
78 | status: CommandStatus.ERROR,
79 | error: new Error("Invalid memory address"),
80 | };
81 | }
82 |
83 | if (params.stopAddress > MAX_ADDRESS) {
84 | return {
85 | memoryChunk: new Uint8Array(),
86 | status: CommandStatus.ERROR,
87 | error: new Error("Memory range is out of bounds"),
88 | };
89 | }
90 |
91 | if (params.startAddress > params.stopAddress) {
92 | return {
93 | memoryChunk: new Uint8Array(),
94 | status: CommandStatus.ERROR,
95 | error: new Error("Invalid memory range"),
96 | };
97 | }
98 |
99 | try {
100 | const memoryChunk = await getMemory({
101 | pvm: params.pvm,
102 | startAddress: params.startAddress,
103 | stopAddress: params.stopAddress,
104 | memorySize: params.memorySize,
105 | });
106 |
107 | return {
108 | memoryChunk,
109 | status: CommandStatus.SUCCESS,
110 | };
111 | } catch (error) {
112 | return { memoryChunk: new Uint8Array(), status: CommandStatus.ERROR, error };
113 | }
114 | };
115 |
--------------------------------------------------------------------------------
/src/packages/web-worker/command-handlers/step.ts:
--------------------------------------------------------------------------------
1 | import { CurrentInstruction, ExpectedState, Status } from "@/types/pvm";
2 | import { nextInstruction } from "../pvm";
3 | import { getState } from "../utils";
4 | import { CommandStatus, PvmApiInterface, Storage } from "../types";
5 |
6 | export type StepParams = {
7 | program: Uint8Array;
8 | pvm: PvmApiInterface | null;
9 | stepsToPerform: number;
10 | storage: Storage | null;
11 | serviceId: number | null;
12 | };
13 | export type StepResponse = {
14 | status: CommandStatus;
15 | error?: unknown;
16 | result: CurrentInstruction | object;
17 | state: ExpectedState;
18 | exitArg: number;
19 | isFinished: boolean;
20 | };
21 |
22 | const step = async ({ pvm, program, stepsToPerform }: StepParams) => {
23 | if (!pvm) {
24 | throw new Error("PVM is uninitialized.");
25 | }
26 |
27 | let isFinished = stepsToPerform > 1 ? !pvm.nSteps(stepsToPerform) : !pvm.nextStep();
28 | const state = await getState(pvm);
29 |
30 | // It's not really finished if we're in host status
31 | if (isFinished && state.status === Status.HOST) {
32 | isFinished = false;
33 | }
34 |
35 | const result = nextInstruction(state.pc ?? 0, program) as unknown as CurrentInstruction;
36 |
37 | return { result, state, isFinished, exitArg: await pvm.getExitArg() };
38 | };
39 |
40 | export const runStep = async ({
41 | pvm,
42 | program,
43 | stepsToPerform,
44 | storage,
45 | serviceId,
46 | }: StepParams): Promise => {
47 | try {
48 | const data = await step({ pvm, program, stepsToPerform, storage, serviceId });
49 | return { status: CommandStatus.SUCCESS, ...data };
50 | } catch (error) {
51 | return { status: CommandStatus.ERROR, error, isFinished: true, result: {}, state: {}, exitArg: 0 };
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/packages/web-worker/goWasmExec.d.ts:
--------------------------------------------------------------------------------
1 | declare class Go {
2 | argv: string[];
3 | env: { [envKey: string]: string };
4 | exit: (code: number) => void;
5 | importObject: WebAssembly.Imports;
6 | exited: boolean;
7 | mem: DataView;
8 | run(instance: WebAssembly.Instance): Promise;
9 | }
10 |
--------------------------------------------------------------------------------
/src/packages/web-worker/pvm.ts:
--------------------------------------------------------------------------------
1 | import { InitialState, Pvm as InternalPvm, Status } from "@/types/pvm";
2 | import {
3 | createResults,
4 | instructionArgumentTypeMap,
5 | interpreter,
6 | ProgramDecoder,
7 | } from "@typeberry/pvm-debugger-adapter";
8 | import { ArgsDecoder, Registers } from "@typeberry/pvm-debugger-adapter";
9 | import { byteToOpCodeMap } from "../../packages/pvm/pvm/assemblify";
10 | import { Pvm as InternalPvmInstance } from "@typeberry/pvm-debugger-adapter";
11 |
12 | const { tryAsMemoryIndex, tryAsSbrkIndex, MemoryBuilder: InternalPvmMemoryBuilder } = interpreter;
13 |
14 | export const initPvm = async (pvm: InternalPvmInstance, program: Uint8Array, initialState: InitialState) => {
15 | const initialMemory = initialState.memory ?? [];
16 | const pageMap = initialState.pageMap ?? [];
17 |
18 | const memoryBuilder = new InternalPvmMemoryBuilder();
19 | for (const page of pageMap) {
20 | const startPageIndex = tryAsMemoryIndex(page.address);
21 | const endPageIndex = tryAsMemoryIndex(startPageIndex + page.length);
22 | const isWriteable = page["is-writable"];
23 |
24 | if (isWriteable) {
25 | memoryBuilder.setWriteablePages(startPageIndex, endPageIndex, new Uint8Array(page.length));
26 | } else {
27 | memoryBuilder.setReadablePages(startPageIndex, endPageIndex, new Uint8Array(page.length));
28 | }
29 | }
30 |
31 | for (const memoryChunk of initialMemory) {
32 | const idx = tryAsMemoryIndex(memoryChunk.address);
33 | memoryBuilder.setData(idx, new Uint8Array(memoryChunk.contents));
34 | }
35 |
36 | const pageSize = 2 ** 12;
37 | const maxAddressFromPageMap = Math.max(...pageMap.map((page) => page.address + page.length));
38 | const hasMemoryLayout = maxAddressFromPageMap >= 0;
39 | const heapStartIndex = tryAsMemoryIndex(hasMemoryLayout ? maxAddressFromPageMap + pageSize : 0);
40 | const heapEndIndex = tryAsSbrkIndex(2 ** 32 - 2 * 2 ** 16 - 2 ** 24);
41 |
42 | const memory = memoryBuilder.finalize(heapStartIndex, heapEndIndex);
43 |
44 | const registers = new Registers();
45 | registers.copyFrom(new BigUint64Array(initialState.regs!.map((x) => BigInt(x))));
46 | pvm.reset(new Uint8Array(program), initialState.pc ?? 0, initialState.gas ?? 0n, registers, memory);
47 | };
48 |
49 | export const runAllInstructions = (pvm: InternalPvm, program: Uint8Array) => {
50 | const programPreviewResult = [];
51 |
52 | do {
53 | const pc = pvm.getProgramCounter();
54 | const result = nextInstruction(pc, program);
55 | programPreviewResult.push(result);
56 | } while (pvm.nextStep());
57 |
58 | return {
59 | programRunResult: {
60 | pc: pvm.getProgramCounter(),
61 | regs: Array.from(pvm.getRegisters()),
62 | gas: pvm.getGasLeft(),
63 | pageMap: [],
64 | memory: [],
65 | status: pvm.getStatus() as Status,
66 | },
67 | programPreviewResult,
68 | };
69 | };
70 |
71 | export const nextInstruction = (programCounter: number, program: Uint8Array) => {
72 | const programDecoder = new ProgramDecoder(new Uint8Array(program));
73 | const code = programDecoder.getCode();
74 | const mask = programDecoder.getMask();
75 | const argsDecoder = new ArgsDecoder();
76 | argsDecoder.reset(code, mask);
77 | const currentInstruction = code[programCounter];
78 | const argumentType = instructionArgumentTypeMap[currentInstruction];
79 | const args = createResults()[argumentType];
80 |
81 | try {
82 | argsDecoder.fillArgs(programCounter, args);
83 |
84 | const currentInstructionDebug = {
85 | instructionCode: currentInstruction,
86 | ...byteToOpCodeMap[currentInstruction],
87 | args,
88 | };
89 | return currentInstructionDebug;
90 | } catch (e) {
91 | // The last iteration goes here since there's no instruction to proces and we didn't check if there's a next operation
92 | return null;
93 | }
94 | };
95 |
--------------------------------------------------------------------------------
/src/packages/web-worker/types.ts:
--------------------------------------------------------------------------------
1 | import { CurrentInstruction, ExpectedState, HostCallIdentifiers, InitialState } from "@/types/pvm";
2 | import { WasmPvmShellInterface } from "./wasmBindgenShell";
3 | import { Pvm as InternalPvm } from "@/types/pvm";
4 | import { bytes } from "@typeberry/pvm-debugger-adapter";
5 | import { SerializedFile } from "@/lib/utils.ts";
6 |
7 | type CommonWorkerResponseParams = { status: CommandStatus; error?: unknown; messageId: string };
8 |
9 | export type WorkerResponseParams = CommonWorkerResponseParams &
10 | (
11 | | { command: Commands.LOAD }
12 | | {
13 | command: Commands.INIT;
14 | payload: { initialState: InitialState };
15 | }
16 | | {
17 | command: Commands.STEP;
18 | payload: {
19 | state: ExpectedState;
20 | result: CurrentInstruction | object;
21 | isFinished: boolean;
22 | isRunMode: boolean;
23 | exitArg: number;
24 | };
25 | }
26 | | {
27 | command: Commands.RUN;
28 | payload: { state: ExpectedState; isFinished: boolean; isRunMode: boolean };
29 | }
30 | | { command: Commands.STOP; payload: { isRunMode: boolean } }
31 | | { command: Commands.MEMORY; payload: { memoryChunk: Uint8Array } }
32 | | { command: Commands.SET_STORAGE }
33 | | {
34 | command: Commands.HOST_CALL;
35 | payload:
36 | | {
37 | hostCallIdentifier: Exclude;
38 | }
39 | | {
40 | hostCallIdentifier: HostCallIdentifiers.WRITE;
41 | storage?: Storage;
42 | };
43 | }
44 | | { command: Commands.SET_SERVICE_ID }
45 | | { command: Commands.UNLOAD }
46 | );
47 |
48 | type CommonWorkerRequestParams = { messageId: string };
49 | export type CommandWorkerRequestParams =
50 | | {
51 | command: Commands.LOAD;
52 | payload: { type: PvmTypes; params: { url?: string; file?: SerializedFile } };
53 | }
54 | | { command: Commands.INIT; payload: { program: Uint8Array; initialState: InitialState } }
55 | | { command: Commands.STEP; payload: { program: Uint8Array; stepsToPerform: number } }
56 | | { command: Commands.RUN }
57 | | { command: Commands.STOP }
58 | | { command: Commands.MEMORY; payload: { startAddress: number; stopAddress: number } }
59 | | { command: Commands.SET_STORAGE; payload: { storage: Storage | null } }
60 | | { command: Commands.HOST_CALL; payload: { hostCallIdentifier: HostCallIdentifiers } }
61 | | { command: Commands.SET_SERVICE_ID; payload: { serviceId: number } }
62 | | { command: Commands.UNLOAD };
63 |
64 | export type WorkerRequestParams = CommonWorkerRequestParams & CommandWorkerRequestParams;
65 |
66 | export enum Commands {
67 | LOAD = "load",
68 | INIT = "init",
69 | STEP = "step",
70 | RUN = "run",
71 | STOP = "stop",
72 | MEMORY = "memory",
73 | SET_STORAGE = "set_storage",
74 | HOST_CALL = "host_call",
75 | SET_SERVICE_ID = "set_service_id",
76 | UNLOAD = "unload",
77 | }
78 |
79 | export enum PvmTypes {
80 | BUILT_IN = "built-in",
81 | WASM_URL = "wasm-url",
82 | WASM_FILE = "wasm-file",
83 | WASM_WS = "wasm-websocket",
84 | }
85 |
86 | export enum CommandStatus {
87 | SUCCESS = "success",
88 | ERROR = "error",
89 | }
90 |
91 | // TODO: unify the api
92 | export type PvmApiInterface = WasmPvmShellInterface | InternalPvm;
93 |
94 | export type Storage = Map;
95 |
--------------------------------------------------------------------------------
/src/packages/web-worker/wasmAsShell.ts:
--------------------------------------------------------------------------------
1 | import { WasmPvmShellInterface } from "@/packages/web-worker/wasmBindgenShell.ts";
2 |
3 | import { instantiate } from "./wasmAsInit";
4 |
5 | export async function createAssemblyScriptWasmPvmShell(module: WebAssembly.Module): Promise {
6 | const imports = {};
7 | const instance = await instantiate(module, imports);
8 | const {
9 | // memory,
10 | // InputKind,
11 | // HasMetadata,
12 | // disassemble,
13 | // runProgram,
14 | // runVm,
15 | // getAssembly,
16 | // wrapAsProgram,
17 | resetGeneric,
18 | resetGenericWithMemory,
19 | nextStep,
20 | nSteps,
21 | getProgramCounter,
22 | setNextProgramCounter,
23 | getStatus,
24 | getExitArg,
25 | getGasLeft,
26 | setGasLeft,
27 | getRegisters,
28 | setRegisters,
29 | getPageDump,
30 | setMemory,
31 | } = instance;
32 |
33 | return {
34 | __wbg_set_wasm: () => {},
35 | resetGeneric,
36 | resetGenericWithMemory,
37 | nextStep,
38 | nSteps,
39 | getProgramCounter,
40 | setNextProgramCounter,
41 | setGasLeft,
42 | getStatus,
43 | getExitArg,
44 | getGasLeft,
45 | getRegisters,
46 | setRegisters,
47 | getPageDump,
48 | setMemory,
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/packages/web-worker/wasmBindgenShell.ts:
--------------------------------------------------------------------------------
1 | import { Status } from "@/types/pvm.ts";
2 | import * as wasm from "./wasmBindgenInit";
3 |
4 | export interface WasmPvmShellInterface {
5 | __wbg_set_wasm(val: unknown): void;
6 | getProgramCounter(): number;
7 | setNextProgramCounter?(pc: number): void;
8 | getGasLeft(): bigint;
9 | setGasLeft?(gas: bigint): void;
10 | resetGeneric(program: Uint8Array, registers: Uint8Array, gas: bigint): void;
11 | resetGenericWithMemory?(
12 | program: Uint8Array,
13 | registers: Uint8Array,
14 | pageMap: Uint8Array,
15 | chunks: Uint8Array,
16 | gas: bigint,
17 | ): void;
18 | nextStep(): boolean;
19 | nSteps(steps: number): boolean;
20 | getExitArg(): number;
21 | getStatus(): Status;
22 | getRegisters(): Uint8Array;
23 | setRegisters(registers: Uint8Array): void;
24 | getPageDump(index: number): Uint8Array;
25 | setMemory(address: number, data: Uint8Array): void;
26 | }
27 |
28 | export function createWasmPvmShell(): WasmPvmShellInterface {
29 | const {
30 | __wbg_set_wasm,
31 | resetGeneric,
32 | resetGenericWithMemory,
33 | nextStep,
34 | nSteps,
35 | getProgramCounter,
36 | setNextProgramCounter,
37 | getExitArg,
38 | getStatus,
39 | getGasLeft,
40 | setGasLeft,
41 | getRegisters,
42 | setRegisters,
43 | getPageDump,
44 | setMemory,
45 | } = wasm;
46 | return {
47 | __wbg_set_wasm,
48 | resetGeneric,
49 | resetGenericWithMemory,
50 | nextStep,
51 | nSteps,
52 | getProgramCounter,
53 | setNextProgramCounter,
54 | getStatus,
55 | getExitArg,
56 | getGasLeft,
57 | setGasLeft,
58 | getRegisters,
59 | setRegisters,
60 | getPageDump,
61 | setMemory,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/src/packages/web-worker/wasmFromWebsockets.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@/utils/loggerService.tsx";
2 | import { PvmApiInterface } from "@/packages/web-worker/types.ts";
3 |
4 | const generateMessageId = () => Math.random().toString(36).substring(2);
5 |
6 | // TODO: remove this when found a workaround for BigInt support in JSON.stringify
7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
8 | // @ts-expect-error
9 | BigInt.prototype["toJSON"] = function () {
10 | return this.toString();
11 | };
12 |
13 | export default function wasmFromWebsockets(): Promise<{
14 | pvm: PvmApiInterface;
15 | socket: WebSocket;
16 | }> {
17 | return new Promise((resolve, reject) => {
18 | // Connect to WebSocket server
19 | const socket = new WebSocket("ws://localhost:8765");
20 |
21 | socket.addEventListener("open", () => {
22 | logger.info("📡 Connected to server");
23 |
24 | // Send a JSON-RPC request
25 | const request = {
26 | jsonrpc: "2.0",
27 | method: "load",
28 | id: 1,
29 | };
30 |
31 | socket.send(JSON.stringify(request));
32 | });
33 |
34 | socket.addEventListener("message", (message: { data: string }) => {
35 | logger.info("📡 Received response:", message);
36 |
37 | const jsonResponse = JSON.parse(message.data);
38 |
39 | if (jsonResponse.result === "load") {
40 | const supportedMethodNames = [
41 | "reset",
42 | "nextStep",
43 | "getProgramCounter",
44 | "getStatus",
45 | "getGasLeft",
46 | "getRegisters",
47 | "getPageDump",
48 | "getExitArg",
49 | "resetGeneric",
50 | "resetGenericWithMemory",
51 | "runMemory",
52 | ];
53 |
54 | const resolveObj = supportedMethodNames.reduce(
55 | (acc, method) => {
56 | acc[method] = (...params: unknown[]) => invokeMethodViaRpc(method, params);
57 | return acc;
58 | },
59 | {} as Record unknown>,
60 | );
61 |
62 | resolve({
63 | pvm: resolveObj as unknown as PvmApiInterface,
64 | socket,
65 | });
66 | }
67 | });
68 |
69 | socket.addEventListener("close", () => {
70 | logger.info("📡 Connection closed");
71 | });
72 |
73 | socket.addEventListener("error", (error) => {
74 | logger.error("📡 WebSocket error:", { error });
75 | reject();
76 | });
77 |
78 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
79 | const invokeMethodViaRpc = (method: string, ...params: any[]) => {
80 | const requestId = generateMessageId();
81 |
82 | const request = {
83 | jsonrpc: "2.0",
84 | method,
85 | params,
86 | id: requestId,
87 | };
88 |
89 | return new Promise((resolve) => {
90 | const messageHandler = (message: { data: string }) => {
91 | const jsonResponse = JSON.parse(message.data);
92 |
93 | if (jsonResponse.id === requestId) {
94 | logger.info("📡 Received response:", message);
95 |
96 | if (jsonResponse.method === "getRegisters") {
97 | resolve(new Uint8Array(jsonResponse.result));
98 | }
99 |
100 | resolve(jsonResponse.result);
101 | socket.removeEventListener("message", messageHandler);
102 | }
103 | };
104 |
105 | socket.addEventListener("message", messageHandler);
106 |
107 | if (method === "resetGenericWithMemory") {
108 | const newParams = request.params[0];
109 |
110 | request.params = [
111 | newParams[0] ? Array.from(newParams[0]) : [],
112 | newParams[1] ? Array.from(newParams[1]) : [],
113 | newParams[2] ? Array.from(newParams[2]) : [],
114 | newParams[3] ? Array.from(newParams[3]) : [],
115 | newParams[4],
116 | ];
117 | return socket.send(JSON.stringify(request));
118 | }
119 |
120 | socket.send(JSON.stringify(request));
121 | });
122 | };
123 | });
124 | }
125 |
--------------------------------------------------------------------------------
/src/packages/web-worker/wasmGoShell.ts:
--------------------------------------------------------------------------------
1 | import { WasmPvmShellInterface } from "@/packages/web-worker/wasmBindgenShell.ts";
2 |
3 | import * as wasm from "./wasmGoInit";
4 |
5 | export function createGoWasmPvmShell(): WasmPvmShellInterface {
6 | const { __wbg_set_wasm, reset, nextStep, getProgramCounter, getStatus, getGasLeft, getRegisters, getPageDump } = wasm;
7 |
8 | return {
9 | __wbg_set_wasm,
10 | resetGeneric: reset,
11 | nextStep,
12 | nSteps: (steps: number) => {
13 | for (let i = 0; i < steps; i++) {
14 | if (!nextStep()) {
15 | return false;
16 | }
17 | }
18 | return true;
19 | },
20 | getProgramCounter,
21 | getStatus,
22 | getExitArg: () => 0,
23 | getGasLeft,
24 | getRegisters,
25 | setRegisters: (/*_registers: Uint8Array*/) => {},
26 | getPageDump,
27 | setMemory: (/*_address: number, _data: Uint8Array*/) => {},
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/store/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import type { RootState, AppDispatch } from "./index";
3 |
4 | export const useAppDispatch = useDispatch.withTypes();
5 | export const useAppSelector = useSelector.withTypes();
6 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { setupListeners } from "@reduxjs/toolkit/query";
3 | import storage from "redux-persist/lib/storage";
4 | import { persistReducer, persistStore } from "redux-persist";
5 | import workersReducer from "./workers/workersSlice";
6 | import debuggerReducer, { debuggerSliceListenerMiddleware, DebuggerState } from "./debugger/debuggerSlice";
7 |
8 | const persistConfig = {
9 | key: "debugger",
10 | storage,
11 | whitelist: ["pvmOptions"],
12 | };
13 |
14 | export const store = configureStore({
15 | reducer: {
16 | debugger: persistReducer(persistConfig, debuggerReducer),
17 | workers: workersReducer,
18 | },
19 | middleware: (getDefaultMiddleware) =>
20 | getDefaultMiddleware({
21 | serializableCheck: false,
22 | }).prepend(debuggerSliceListenerMiddleware.middleware),
23 | });
24 |
25 | setupListeners(store.dispatch);
26 |
27 | export const persistor = persistStore(store);
28 |
29 | export type RootState = ReturnType;
30 | export type AppDispatch = typeof store.dispatch;
31 |
--------------------------------------------------------------------------------
/src/store/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WorkerResponseParams,
3 | CommandWorkerRequestParams,
4 | WorkerRequestParams,
5 | Commands,
6 | CommandStatus,
7 | Storage,
8 | } from "@/packages/web-worker/types";
9 | import { DebuggerEcalliStorage } from "@/types/pvm";
10 | import { logger } from "@/utils/loggerService";
11 | import { SerializedError } from "@reduxjs/toolkit";
12 | import { bytes } from "@typeberry/pvm-debugger-adapter";
13 |
14 | const RESPONSE_WAIT_TIMEOUT = 60000;
15 | const getMessageId = () => Math.random().toString(36).substring(7);
16 |
17 | export const asyncWorkerPostMessage = (
18 | id: string,
19 | worker: Worker,
20 | data: Extract,
21 | ) => {
22 | return new Promise>((resolve, reject) => {
23 | const messageId = getMessageId();
24 | const timeoutId = setTimeout(() => {
25 | reject(`PVM ${id} reached max timeout ${RESPONSE_WAIT_TIMEOUT}ms for ${messageId}`);
26 | }, RESPONSE_WAIT_TIMEOUT);
27 |
28 | const messageHandler = (event: MessageEvent) => {
29 | logger.info("📥 Debugger received message", event.data);
30 | if (event.data.messageId === messageId) {
31 | clearTimeout(timeoutId);
32 | worker.removeEventListener("message", messageHandler);
33 | resolve(event.data as Extract);
34 | }
35 | };
36 | worker.addEventListener("message", messageHandler);
37 |
38 | const request: WorkerRequestParams = { ...data, messageId };
39 | worker.postMessage(request);
40 | });
41 | };
42 |
43 | export const hasCommandStatusError = (resp: WorkerResponseParams): resp is WorkerResponseParams & { error: Error } => {
44 | return "status" in resp && resp.status === CommandStatus.ERROR && resp.error instanceof Error;
45 | };
46 |
47 | export const isSerializedError = (error: unknown): error is SerializedError => {
48 | return (
49 | Object.prototype.hasOwnProperty.call(error, "name") ||
50 | Object.prototype.hasOwnProperty.call(error, "message") ||
51 | Object.prototype.hasOwnProperty.call(error, "stack") ||
52 | Object.prototype.hasOwnProperty.call(error, "code")
53 | );
54 | };
55 |
56 | export const MEMORY_SPLIT_STEP = 8;
57 | // Keep multiplication of step to make chunking easier
58 | export const LOAD_MEMORY_CHUNK_SIZE = MEMORY_SPLIT_STEP * 200;
59 |
60 | export const toPvmStorage = (storage: DebuggerEcalliStorage): Storage => {
61 | const pvmStorage = new Map();
62 | storage.forEach((item) => {
63 | pvmStorage.set(item.keyHash, item.valueBlob);
64 | });
65 |
66 | return pvmStorage;
67 | };
68 |
69 | const valueBlobToString = (valueBlob: bytes.BytesBlob) =>
70 | "0x" +
71 | Array.from(valueBlob.raw)
72 | .map((byte) => byte.toString(16).padStart(2, "0")) // Convert to hex and pad with leading zero if necessary
73 | .join("");
74 |
75 | export const mergePVMAndDebuggerEcalliStorage = (
76 | pvmStorage: Storage,
77 | prevStorage: DebuggerEcalliStorage,
78 | ): DebuggerEcalliStorage => {
79 | const result = [...prevStorage];
80 | Array.from(pvmStorage.entries()).forEach(([keyHash, rawValue]) => {
81 | const valueBlob = bytes.BytesBlob.blobFrom(rawValue.raw);
82 | const prevValue = result.find((item) => item.keyHash === keyHash);
83 | if (prevValue && prevValue.keyHash === keyHash) {
84 | if (prevValue.valueBlob.asText() !== valueBlob.asText()) {
85 | prevValue.valueBlob = valueBlob;
86 | }
87 | } else {
88 | result.push({
89 | key: "",
90 | keyHash,
91 | value: valueBlobToString(valueBlob),
92 | valueBlob,
93 | });
94 | }
95 | });
96 |
97 | return result;
98 | };
99 |
--------------------------------------------------------------------------------
/src/types/pvm.ts:
--------------------------------------------------------------------------------
1 | import { StorageRow } from "@/components/HostCalls/trie-input";
2 | import { Args } from "@typeberry/pvm-debugger-adapter";
3 | export { Pvm } from "@typeberry/pvm-debugger-adapter";
4 |
5 | type GrowToSize = A["length"] extends N ? A : GrowToSize;
6 | type FixedArray = GrowToSize;
7 |
8 | export type RegistersArray = FixedArray;
9 |
10 | export type InitialState = {
11 | regs?: RegistersArray;
12 | pc?: number;
13 | pageMap?: PageMapItem[];
14 | memory?: MemoryChunkItem[];
15 | gas?: bigint;
16 | };
17 |
18 | export type MemoryChunkItem = {
19 | address: number;
20 | contents: number[];
21 | };
22 |
23 | export type PageMapItem = {
24 | address: number;
25 | length: number;
26 | "is-writable": boolean;
27 | };
28 |
29 | export enum Status {
30 | OK = 255,
31 | HALT = 0,
32 | PANIC = 1,
33 | FAULT = 2,
34 | HOST = 3,
35 | OOG = 4 /* out of gas */,
36 | }
37 |
38 | export type ExpectedState = InitialState & {
39 | status?: Status;
40 | };
41 |
42 | export type Block = {
43 | isStart: boolean;
44 | isEnd: boolean;
45 | name: string;
46 | number: number;
47 | };
48 |
49 | export type CurrentInstruction =
50 | | {
51 | address: number;
52 | args: Args;
53 | name: string;
54 | gas: number;
55 | block: Block;
56 | instructionCode: number;
57 | instructionBytes: Uint8Array;
58 | }
59 | | {
60 | address: number;
61 | error: string;
62 | name: string;
63 | gas: number;
64 | block: Block;
65 | instructionCode: number;
66 | };
67 |
68 | export enum AvailablePvms {
69 | TYPEBERRY = "typeberry",
70 | POLKAVM = "polkavm",
71 | ANANAS = "ananas",
72 | WASM_URL = "wasm-url",
73 | WASM_FILE = "wasm-file",
74 | WASM_WS = "wasm-websocket",
75 | }
76 |
77 | export enum HostCallIdentifiers {
78 | GAS = 0,
79 | LOOKUP = 1,
80 | READ = 2,
81 | WRITE = 3,
82 | }
83 |
84 | export type DebuggerEcalliStorage = StorageRow[];
85 |
--------------------------------------------------------------------------------
/src/types/type-guards.ts:
--------------------------------------------------------------------------------
1 | import { Args, ArgumentType } from "@typeberry/pvm-debugger-adapter";
2 | import { CurrentInstruction } from "./pvm";
3 |
4 | export function isInstructionError(
5 | instruction: CurrentInstruction,
6 | ): instruction is Extract {
7 | return "error" in instruction;
8 | }
9 |
10 | export function isOneImmediateArgs(args: Args): args is Extract {
11 | return args.type === ArgumentType.ONE_IMMEDIATE;
12 | }
13 |
--------------------------------------------------------------------------------
/src/utils/colors.ts:
--------------------------------------------------------------------------------
1 | import { Status } from "@/types/pvm";
2 |
3 | export const getStatusColor = (isDarkMode?: boolean, status?: Status) => {
4 | if (status === Status.HALT) {
5 | return isDarkMode
6 | ? {
7 | background: "#4E4917",
8 | color: "#FFFDCF",
9 | border: "#696721",
10 | }
11 | : {
12 | background: "#FFFDCF",
13 | color: "#D0A21D",
14 | border: "#E1E7B5",
15 | };
16 | }
17 |
18 | if (status === Status.PANIC || status === Status.FAULT || status === Status.OOG) {
19 | return isDarkMode
20 | ? {
21 | background: "#4E1717",
22 | color: "#D34D4B",
23 | border: "#692121",
24 | }
25 | : {
26 | background: "#FFCFCF",
27 | color: "#D34D4B",
28 | border: "#E7B5B5",
29 | };
30 | }
31 |
32 | // Highlight color / OK color
33 | return isDarkMode
34 | ? {
35 | background: "#00413B",
36 | color: "#00FFEB",
37 | border: "#0F6760",
38 | }
39 | : {
40 | background: "#E4FFFD",
41 | color: "#17AFA3",
42 | border: "#B5E7E7",
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/src/utils/loggerService.tsx:
--------------------------------------------------------------------------------
1 | import { throttle } from "lodash";
2 | import { toast } from "react-toastify";
3 |
4 | const errorToast = throttle((msg: string) => {
5 | toast.error(
6 |
7 | {msg}
8 |
9 | Check console for more information
10 |
,
11 | { autoClose: 3000 },
12 | );
13 | }, 6000);
14 | class Logger {
15 | constructor() {
16 | if (typeof window !== "undefined") {
17 | // eslint-disable-next-line no-console
18 | console.info(`
19 | .!YGBBPJ^
20 | :B@&57~!?P&@5
21 | ...:YBB@&: !@@. .~~!77! ~YY ^JY~ 7YJ.
22 | ^B&&&&@#!!7 ~@@PPY^ 7@@@J~^.@@@ !@@5 &@@:
23 | J@B: . . J57?G@&: 7@@@P5P:&@@.G&B 5:#@@B^7@@@J.G&G 5&B
24 | @@. @&. ^@& 7@@@. @@@.@@@.@@@ Y@@? .@@@ @@& &@@
25 | 5@G. GB5@@. Y@G ^P B&P ?YB#P !&&^ #&P JY@@&
26 | !&@#BBGJ .@@Y! .YBBB#&&J . ~5@&^
27 | .:^!&@&^ .@@ &@?^^^. ~&&&. .@@B .
28 | :@&JP@# .@@ .Y@@P. ?@@@. ^!!~~^^@@&!!^ .^!~^
29 | ~@&^!@@ @@ .@@~^&@^ 7@@@ ~@@P^@@#:@@#^@@&.#@@&7
30 | ^P#BY. :G@@P. &@5Y@@. 7@@@?777@@B!@@B:@@B^@@B.~#@@@^
31 | :@&:^@@. ~YY! ~~^~~: .^~:~~. :~~~^. :^~^.
32 | .&@5P@&
33 | ~JJ^
34 | Logger initialized watch console for logs
35 | `);
36 | }
37 | }
38 |
39 | error(msg: string, { error, hideToast }: { error: unknown; hideToast?: boolean }) {
40 | if (!hideToast) {
41 | errorToast(msg);
42 | }
43 |
44 | console.error("☢️", error);
45 | }
46 |
47 | warn(...msg: unknown[]) {
48 | console.warn("⚠️", ...msg);
49 | }
50 |
51 | info(...msg: unknown[]) {
52 | // eslint-disable-next-line no-console
53 | console.info("🪵", ...msg);
54 | }
55 |
56 | debug(...msg: unknown[]) {
57 | if (process.env.NODE_ENV === "development") {
58 | // eslint-disable-next-line no-console
59 | console.debug("💻 DEV LOG: \n", ...msg);
60 | }
61 | }
62 | }
63 |
64 | export const logger = new Logger();
65 |
--------------------------------------------------------------------------------
/src/utils/virtualTrapInstruction.ts:
--------------------------------------------------------------------------------
1 | import { CurrentInstruction } from "@/types/pvm";
2 |
3 | export const virtualTrapInstruction: CurrentInstruction = {
4 | args: { type: 0, noOfBytesToSkip: 0 },
5 | address: 0,
6 | name: "TRAP",
7 | gas: 0,
8 | instructionCode: 0,
9 | instructionBytes: new Uint8Array(0),
10 | block: {
11 | isStart: true,
12 | isEnd: true,
13 | name: "end of program",
14 | number: -1,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import tailwindcssAnimate from "tailwindcss-animate";
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | darkMode: ["class"],
6 | content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
7 | prefix: "",
8 | theme: {
9 | container: {
10 | center: true,
11 | padding: "2rem",
12 | screens: {
13 | "2xl": "1400px",
14 | },
15 | colors: {},
16 | },
17 | extend: {
18 | colors: {
19 | "title-secondary-foreground": "hsl(var(--title-secondary-foreground))",
20 | title: {
21 | DEFAULT: "hsl(var(--title))",
22 | foreground: "hsl(var(--title-foreground))",
23 | },
24 | brand: {
25 | DEFAULT: "hsl(var(--brand))",
26 | dark: "hsl(var(--brand-dark))",
27 | light: "hsl(var(--brand-light))",
28 | },
29 | border: "hsl(var(--border))",
30 | input: "hsl(var(--input))",
31 | ring: "hsl(var(--ring))",
32 | background: "hsl(var(--background))",
33 | foreground: "hsl(var(--foreground))",
34 | sidebar: {
35 | DEFAULT: "hsl(var(--sidebar))",
36 | foreground: "hsl(var(--sidebar-foreground))",
37 | },
38 | primary: {
39 | DEFAULT: "hsl(var(--primary))",
40 | foreground: "hsl(var(--primary-foreground))",
41 | },
42 | secondary: {
43 | DEFAULT: "hsl(var(--secondary))",
44 | foreground: "hsl(var(--secondary-foreground))",
45 | },
46 | destructive: {
47 | DEFAULT: "hsl(var(--destructive))",
48 | foreground: "hsl(var(--destructive-foreground))",
49 | },
50 | muted: {
51 | DEFAULT: "hsl(var(--muted))",
52 | foreground: "hsl(var(--muted-foreground))",
53 | },
54 | accent: {
55 | DEFAULT: "hsl(var(--accent))",
56 | foreground: "hsl(var(--accent-foreground))",
57 | },
58 | popover: {
59 | DEFAULT: "hsl(var(--popover))",
60 | foreground: "hsl(var(--popover-foreground))",
61 | },
62 | card: {
63 | DEFAULT: "hsl(var(--card))",
64 | foreground: "hsl(var(--card-foreground))",
65 | },
66 | },
67 | borderRadius: {
68 | lg: "var(--radius)",
69 | md: "calc(var(--radius) - 2px)",
70 | sm: "calc(var(--radius) - 4px)",
71 | },
72 | keyframes: {
73 | "accordion-down": {
74 | from: { height: "0" },
75 | to: { height: "var(--radix-accordion-content-height)" },
76 | },
77 | "accordion-up": {
78 | from: { height: "var(--radix-accordion-content-height)" },
79 | to: { height: "0" },
80 | },
81 | },
82 | animation: {
83 | "accordion-down": "accordion-down 0.2s ease-out",
84 | "accordion-up": "accordion-up 0.2s ease-out",
85 | },
86 |
87 | fontFamily: {
88 | poppins: ["Poppins", "sans-serif", "system-ui"], // Replace with your preferred font
89 | inconsolata: ["Inconsolata", "monospace"],
90 | },
91 | },
92 | },
93 | plugins: [tailwindcssAnimate],
94 | };
95 |
--------------------------------------------------------------------------------
/tests/memory-range.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test";
2 | import { openDebugger, openProgram, selectPVM, step } from "./utils/actions";
3 |
4 | test("Should modify memory ranges", async ({ page }) => {
5 | await openDebugger(page);
6 | await selectPVM(page, "@typeberry");
7 | await openProgram(page, "storeU16");
8 |
9 | await page.getByRole("tab", { name: "Ranges" }).click();
10 | await page.getByLabel("Start").click();
11 | await page.getByLabel("Start").fill("131072");
12 | await page.getByRole("button", { name: "Add" }).click();
13 |
14 | await expect(page.locator("[data-test-id='memory-cell']").first()).toContainText("00", { timeout: 2000 });
15 | await expect(page.locator("[data-test-id='memory-cell']").nth(1)).toContainText("00", { timeout: 2000 });
16 |
17 | await step(page);
18 |
19 | await expect(page.locator("[data-test-id='memory-cell']").first()).toContainText("78", { timeout: 2000 });
20 | await expect(page.locator("[data-test-id='memory-cell']").nth(1)).toContainText("56", { timeout: 2000 });
21 | });
22 |
23 | test("Should show interpretations", async ({ page }) => {
24 | await openDebugger(page);
25 | await selectPVM(page, "@typeberry");
26 | await openProgram(page, "storeU16");
27 |
28 | await page.getByRole("tab", { name: "Ranges" }).click();
29 | await page.getByLabel("Start").click();
30 | await page.getByLabel("Start").fill("131072");
31 | await page.getByRole("button", { name: "Add" }).click();
32 |
33 | await step(page);
34 |
35 | await page.getByText("78", { exact: true }).click();
36 |
37 | await expect(page.getByText("Open codec tooli8 : 0x78 0x56")).toBeVisible();
38 | });
39 |
40 | test("Should not show interpretations for longer memory chunks", async ({ page }) => {
41 | await openDebugger(page);
42 | await selectPVM(page, "@typeberry");
43 | await openProgram(page, "storeU16");
44 |
45 | await page.getByRole("tab", { name: "Ranges" }).click();
46 | await page.getByLabel("Start").click();
47 | await page.getByLabel("Start").fill("131072");
48 | await page.getByLabel("Length").click();
49 | await page.getByLabel("Length").fill("40");
50 |
51 | await page.getByRole("button", { name: "Add" }).click();
52 |
53 | await step(page);
54 |
55 | await page.getByText("78", { exact: true }).click();
56 | await expect(page.getByRole("dialog")).toContainText("Max memory for interpretations is 32 bytes");
57 | });
58 |
59 | test("Should show diffs between PVMs", async ({ page }) => {
60 | await openDebugger(page);
61 | await selectPVM(page, "typeberry");
62 | await openProgram(page, "storeU16");
63 |
64 | await page.getByRole("tab", { name: "Ranges" }).click();
65 | await page.getByLabel("Start").click();
66 | await page.getByLabel("Start").fill("131072");
67 | await page.getByLabel("Length").click();
68 | await page.getByLabel("Length").fill("40");
69 |
70 | await page.getByRole("button", { name: "Add" }).click();
71 |
72 | await step(page);
73 |
74 | await page.getByText("78", { exact: true }).click();
75 | await expect(page.getByRole("dialog")).toContainText("Max memory for interpretations is 32 bytes");
76 | });
77 |
--------------------------------------------------------------------------------
/tests/run-program.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect, Page } from "@playwright/test";
2 | import { openDebugger, openProgram, selectPVM } from "./utils/actions";
3 |
4 | async function runProgramTest(page: Page, pvmType: string) {
5 | await openDebugger(page);
6 | await selectPVM(page, pvmType);
7 | await openProgram(page, "fibonacci");
8 |
9 | const jumpIndInstruction = page.locator('span:has-text("JUMP_IND")');
10 | await expect(jumpIndInstruction).toBeVisible();
11 |
12 | // Test the "Run" button functionality
13 | await page.click('button:has-text("Run")');
14 |
15 | // Wait for execution to complete
16 | await page.waitForTimeout(5000);
17 |
18 | const programStatus = page.locator('[test-id="program-status"]');
19 | await expect(programStatus).toHaveText("HALT");
20 |
21 | // const jumpIndInstructionParent = page.locator('div[test-id="instruction-item"]:has(span:has-text("JUMP_IND"))');
22 | // await expect(jumpIndInstructionParent).toHaveCSS('background-color', 'rgb(76, 175, 80)');
23 | }
24 |
25 | test("Run program with typeberry PVM", async ({ page }) => {
26 | await runProgramTest(page, "@typeberry");
27 | });
28 |
29 | test("Run program with polkavm PVM", async ({ page }) => {
30 | await runProgramTest(page, "polkavm");
31 | });
32 |
--------------------------------------------------------------------------------
/tests/utils/actions.ts:
--------------------------------------------------------------------------------
1 | import { Page } from "@playwright/test";
2 |
3 | export const openDebugger = async (page: Page) => {
4 | await page.goto("/", { waitUntil: "domcontentloaded" });
5 | await page.evaluate(() => window.localStorage.clear());
6 | };
7 |
8 | export const openProgram = async (page: Page, name: string) => {
9 | await page.click(`div[id="${name}"]`);
10 |
11 | await page.waitForTimeout(1000);
12 | };
13 |
14 | export const step = async (page: Page) => {
15 | await page.getByRole("button", { name: "Step" }).click();
16 | };
17 |
18 | export const selectPVM = async (page: Page, pvmType: string) => {
19 | await page.waitForSelector('button[test-id="pvm-select"]');
20 | await page.click('button[test-id="pvm-select"]');
21 |
22 | await page.waitForSelector('[role="dialog"]');
23 |
24 | // Locate all options in the multi-select
25 | const allPvmOptions = await page.locator('div[role="option"]');
26 |
27 | // Check for selected options
28 | const selectedPvmOptions = allPvmOptions.locator(".bg-brand"); // Adjust the class name as needed
29 | const selectedPvmCount = await selectedPvmOptions.count();
30 |
31 | for (let i = 0; i < selectedPvmCount; i++) {
32 | const pvm = selectedPvmOptions.nth(i);
33 | await pvm.click(); // Click to unselect the checkbox
34 | }
35 |
36 | const pvmOption = page.locator('div[role="option"]', { hasText: new RegExp(pvmType, "i") });
37 | await pvmOption.waitFor({ state: "visible" });
38 | await pvmOption.click();
39 |
40 | await page.waitForTimeout(1000);
41 | await page.locator("html").click();
42 | };
43 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 |
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true,
25 |
26 | "baseUrl": ".",
27 | "paths": {
28 | "@/*": ["./src/*"]
29 | }
30 | },
31 | "include": ["src"]
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ],
11 | "compilerOptions": {
12 | "allowJs": true,
13 | "checkJs": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "@/*": ["./src/*"],
17 | "@typeberry/*": ["./node_modules/typeberry/packages/*"]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import wasm from "vite-plugin-wasm";
4 | import topLevelAwait from "vite-plugin-top-level-await";
5 | import path from "path";
6 | import packageJson from "./package.json";
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | plugins: [react(), wasm(), topLevelAwait()],
11 | resolve: {
12 | alias: {
13 | "@": path.resolve(__dirname, "./src"),
14 | },
15 | },
16 | define: {
17 | "import.meta.env.TYPEBERRY_PVM_VERSION": JSON.stringify(
18 | packageJson.dependencies["@typeberry/pvm-debugger-adapter"],
19 | ),
20 | },
21 | });
22 |
--------------------------------------------------------------------------------