From 7bda3fbd38a012c2075065c736b349547d0978a3 Mon Sep 17 00:00:00 2001 From: Wojciech Kwiatek Date: Mon, 9 Dec 2024 17:32:09 +0100 Subject: [PATCH] Modify program editor to be available only in edit mode; improve error handling (#242) * Move program code input to edit mode * Handle errors in program text editor * Allow for hex in array as a program input --- src/App.tsx | 48 ++++++++--- .../ProgramLoader/BinaryFileUpload.tsx | 2 +- src/components/ProgramLoader/Bytecode.tsx | 40 --------- src/components/ProgramLoader/Examples.tsx | 28 +++++++ src/components/ProgramLoader/Loader.tsx | 24 +----- src/components/ProgramTextLoader/index.tsx | 83 ++++++++++++++++--- src/hooks/useDebuggerActions.ts | 19 +++-- src/store/debugger/debuggerSlice.ts | 12 +-- 8 files changed, 157 insertions(+), 99 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 92d8d66..10cbf5d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,13 +21,19 @@ import { MobileRegisters } from "./components/MobileRegisters"; import { MobileKnowledgeBase } from "./components/KnowledgeBase/Mobile"; import { Assembly } from "./components/ProgramLoader/Assembly"; import { useAppDispatch, useAppSelector } from "@/store/hooks.ts"; -import { setClickedInstruction, setInstructionMode, setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts"; +import { + setClickedInstruction, + setInstructionMode, + setIsProgramEditMode, + setIsProgramInvalid, +} from "@/store/debugger/debuggerSlice.ts"; import { MemoryPreview } from "@/components/MemoryPreview"; import { DebuggerControlls } from "./components/DebuggerControlls"; import { useDebuggerActions } from "./hooks/useDebuggerActions"; import { Loader } from "./components/ProgramLoader/Loader"; import classNames from "classnames"; import { DebuggerSettings } from "./components/DebuggerSettings"; +import { ProgramTextLoader } from "@/components/ProgramTextLoader"; const DebuggerContent = () => { const dispatch = useAppDispatch(); @@ -36,7 +42,7 @@ const DebuggerContent = () => { program, initialState, isProgramEditMode, - isAsmError, + isProgramInvalid, programPreviewResult, clickedInstruction, instructionMode, @@ -78,12 +84,31 @@ const DebuggerContent = () => { {!!program.length && ( <> {isProgramEditMode && ( -
- +
+ {instructionMode === InstructionMode.ASM ? ( + + ) : ( + { + if (error) { + dispatch(setIsProgramInvalid(true)); + } + + if (!error && program) { + debuggerActions.handleProgramLoad({ + initial: initialState, + program: program || [], + name: "custom", + }); + } + }} + /> + )}
)} @@ -144,7 +169,6 @@ const DebuggerContent = () => {
@@ -158,7 +182,7 @@ const DebuggerContent = () => { variant="link" size="icon" className={!program.length ? "invisible" : "visible"} - disabled={!program.length || isAsmError} + disabled={!program.length || isProgramInvalid} title="Edit the code" onClick={() => { if (isProgramEditMode) { @@ -180,7 +204,7 @@ const DebuggerContent = () => { }; function App() { - const { pvmInitialized, initialState, program } = useAppSelector((state) => state.debugger); + const { pvmInitialized } = useAppSelector((state) => state.debugger); return ( <> @@ -210,7 +234,7 @@ function App() { ) : (
- +
)} diff --git a/src/components/ProgramLoader/BinaryFileUpload.tsx b/src/components/ProgramLoader/BinaryFileUpload.tsx index bc3dab5..be9cf39 100644 --- a/src/components/ProgramLoader/BinaryFileUpload.tsx +++ b/src/components/ProgramLoader/BinaryFileUpload.tsx @@ -44,7 +44,7 @@ export const BinaryFileUpload = ({ return (
-

or upload program as a binary file

+

Upload program as a binary file

void; - program: number[]; }) => { - const [tempProgram, setTempProgram] = useState(program); const handleFileUpload = (val: ProgramUploadFileOutput) => { - setTempProgram(val.program); onProgramLoad(val); }; return (
-

Edit program code bytes

- { - if (program) { - setTempProgram(program); - onProgramLoad({ initial, program, name: "custom" }, error); - } else { - onProgramLoad(undefined, error); - } - }} - />
); diff --git a/src/components/ProgramLoader/Examples.tsx b/src/components/ProgramLoader/Examples.tsx index ad0355d..94f416e 100644 --- a/src/components/ProgramLoader/Examples.tsx +++ b/src/components/ProgramLoader/Examples.tsx @@ -79,6 +79,28 @@ const programs: { memory: [], gas: 10000n, }, + empty: { + program: [0, 0, 0], + regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] as [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + ], + pc: 0, + pageMap: [], + memory: [], + gas: 10000n, + }, }; export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUploadFileOutput) => void }) => { @@ -131,6 +153,12 @@ export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUpload Store U16 instruction
+
+ + +
); diff --git a/src/components/ProgramLoader/Loader.tsx b/src/components/ProgramLoader/Loader.tsx index 6c5ca05..097335c 100644 --- a/src/components/ProgramLoader/Loader.tsx +++ b/src/components/ProgramLoader/Loader.tsx @@ -1,27 +1,17 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Button } from "../ui/button"; -import { Assembly } from "./Assembly"; import { Bytecode } from "./Bytecode"; import { Examples } from "./Examples"; import { TextFileUpload } from "./TextFileUpload"; import { useState, useCallback, useEffect } from "react"; import { ProgramUploadFileOutput } from "./types"; -import { InitialState } from "@/types/pvm"; import { useDebuggerActions } from "@/hooks/useDebuggerActions"; import { useAppDispatch, useAppSelector } from "@/store/hooks.ts"; import { setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts"; import { selectIsAnyWorkerLoading } from "@/store/workers/workersSlice"; import { isSerializedError } from "@/store/utils"; -export const Loader = ({ - initialState, - program, - setIsDialogOpen, -}: { - initialState: InitialState; - program: number[]; - setIsDialogOpen?: (val: boolean) => void; -}) => { +export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) => void }) => { const dispatch = useAppDispatch(); const [programLoad, setProgramLoad] = useState(); const [error, setError] = useState(); @@ -57,7 +47,6 @@ export const Loader = ({ JSON tests Examples RAW bytecode - Assembly
@@ -83,17 +72,6 @@ export const Loader = ({ setIsSubmitted(false); setError(error); }} - program={program} - /> - - - { - setProgramLoad(val); - setIsSubmitted(false); - }} - program={program} - initialState={initialState} /> {error && isSubmitted &&

{error}

} diff --git a/src/components/ProgramTextLoader/index.tsx b/src/components/ProgramTextLoader/index.tsx index f47cf0d..9c53405 100644 --- a/src/components/ProgramTextLoader/index.tsx +++ b/src/components/ProgramTextLoader/index.tsx @@ -1,8 +1,29 @@ import { Textarea } from "@/components/ui/textarea.tsx"; -import React, { useEffect, useState } from "react"; +import React, { useMemo, useState } from "react"; import classNames from "classnames"; import { bytes } from "@typeberry/block"; import { logger } from "@/utils/loggerService"; +import { useAppSelector } from "@/store/hooks.ts"; +import { selectIsProgramInvalid } from "@/store/debugger/debuggerSlice.ts"; + +const parseArrayLikeString = (input: string): (number | string)[] => { + // Remove the brackets and split the string by commas + const items = input + .replace(/^\[|\]$/g, "") + .split(",") + .map((item) => item.trim()); + + // Process each item + return items.map((item) => { + if (/^(?:0x)?[0-9a-fA-F]+$/i.test(item)) { + return parseInt(item, 16); + } else if (!isNaN(Number(item))) { + return Number(item); + } else { + return item; + } + }); +}; export const ProgramTextLoader = ({ program, @@ -11,11 +32,14 @@ export const ProgramTextLoader = ({ program?: number[]; setProgram: (val?: number[], error?: string) => void; }) => { - const [programInput, setProgramInput] = useState(program?.length ? JSON.stringify(program) : ""); - useEffect(() => { - setProgramInput(program?.length ? JSON.stringify(program) : ""); + const defaultProgram = useMemo(() => { + return program; }, [program]); + const [programInput, setProgramInput] = useState(defaultProgram?.length ? JSON.stringify(defaultProgram) : ""); + const [programError, setProgramError] = useState(""); + const isProgramInvalid = useAppSelector(selectIsProgramInvalid); + const handleOnChange = (e: React.ChangeEvent) => { const newInput = e.target.value.trim(); setProgramInput(newInput); @@ -23,33 +47,68 @@ export const ProgramTextLoader = ({ if (!newInput.startsWith("[")) { try { const parsedBlob = bytes.BytesBlob.parseBlob(newInput); - setProgram(Array.prototype.slice.call(parsedBlob.raw)); - } catch (error) { - logger.error("Wrong binary file", { error, hideToast: true }); - setProgram(undefined, "Wrong binary file"); + + const parsedBlobArray = Array.prototype.slice.call(parsedBlob.raw); + + if (parsedBlobArray.length) { + setProgram(parsedBlobArray); + } + + setProgramError(""); + } catch (error: unknown) { + logger.error("Wrong binary program", { error, hideToast: true }); + + setProgram(undefined, "Wrong binary program"); + + if (error instanceof Error) { + if (error?.message) { + setProgramError(error.message); + } + } } } else { try { - JSON.parse(newInput); - setProgram(JSON.parse(newInput)); + // Make sure that hex strings are parsed as strings for JSON.parse validation + const parseTest = newInput.replace(/0x([a-fA-F0-9]+)/g, '"0x$1"'); + // Parse it just to check if it's a valid JSON + JSON.parse(parseTest); + + const parsedJson = parseArrayLikeString(newInput); + const programArray = parsedJson.filter((item) => typeof item === "number") as number[]; + + if (programArray.length) { + setProgram(programArray); + } + setProgramError(""); } catch (error) { logger.error("Wrong JSON", { error, hideToast: true }); + setProgram(undefined, "Wrong JSON"); + + if (error) { + setProgramError(error.toString()); + } } } }; return (
-
+
+

+ Edit program code bytes +