Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify program editor to be available only in edit mode; improve error handling #242

Merged
merged 3 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -36,7 +42,7 @@ const DebuggerContent = () => {
program,
initialState,
isProgramEditMode,
isAsmError,
isProgramInvalid,
programPreviewResult,
clickedInstruction,
instructionMode,
Expand Down Expand Up @@ -78,12 +84,31 @@ const DebuggerContent = () => {
{!!program.length && (
<>
{isProgramEditMode && (
<div className="border-2 rounded-md h-full p-2 pt-8">
<Assembly
program={program}
onProgramLoad={debuggerActions.handleProgramLoad}
initialState={initialState}
/>
<div className="border-2 rounded-md h-full p-2">
{instructionMode === InstructionMode.ASM ? (
<Assembly
program={program}
onProgramLoad={debuggerActions.handleProgramLoad}
initialState={initialState}
/>
) : (
<ProgramTextLoader
program={program}
setProgram={(program, error) => {
if (error) {
dispatch(setIsProgramInvalid(true));
}

if (!error && program) {
debuggerActions.handleProgramLoad({
initial: initialState,
program: program || [],
name: "custom",
});
}
}}
/>
)}
</div>
)}

Expand Down Expand Up @@ -144,7 +169,6 @@ const DebuggerContent = () => {
<div className={`flex items-center space-x-2 ${!program.length ? "invisible" : "visible"}`}>
<Label htmlFor="instruction-mode">ASM</Label>
<Switch
disabled={isProgramEditMode}
id="instruction-mode"
checked={instructionMode === InstructionMode.BYTECODE}
onCheckedChange={(checked) =>
Expand All @@ -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) {
Expand All @@ -180,7 +204,7 @@ const DebuggerContent = () => {
};

function App() {
const { pvmInitialized, initialState, program } = useAppSelector((state) => state.debugger);
const { pvmInitialized } = useAppSelector((state) => state.debugger);

return (
<>
Expand Down Expand Up @@ -210,7 +234,7 @@ function App() {
) : (
<div className="col-span-12 flex justify-center h-[50vh] align-middle">
<div className="min-w-[50vw] max-md:w-[100%] min-h-[500px] h-[75vh] flex flex-col">
<Loader initialState={initialState} program={program} />
<Loader />
</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProgramLoader/BinaryFileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const BinaryFileUpload = ({

return (
<div className="block">
<p className="mt-10 mb-3">or upload program as a binary file</p>
<p className="mb-3">Upload program as a binary file</p>
<Input
className="my-6 mr-3"
id="test-file"
Expand Down
40 changes: 0 additions & 40 deletions src/components/ProgramLoader/Bytecode.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,17 @@
import { ProgramTextLoader } from "../ProgramTextLoader";
import { ProgramUploadFileOutput } from "./types";
import { BinaryFileUpload } from "@/components/ProgramLoader/BinaryFileUpload.tsx";
import { useState } from "react";

const initial = {
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 Bytecode = ({
onProgramLoad,
program,
}: {
onProgramLoad: (val?: ProgramUploadFileOutput, error?: string) => void;
program: number[];
}) => {
const [tempProgram, setTempProgram] = useState<number[] | undefined>(program);
const handleFileUpload = (val: ProgramUploadFileOutput) => {
setTempProgram(val.program);
onProgramLoad(val);
};

return (
<div className="h-full flex flex-col">
<p className="mb-3">Edit program code bytes</p>
<ProgramTextLoader
program={tempProgram}
setProgram={(program, error) => {
if (program) {
setTempProgram(program);
onProgramLoad({ initial, program, name: "custom" }, error);
} else {
onProgramLoad(undefined, error);
}
}}
/>
<BinaryFileUpload onFileUpload={handleFileUpload} />
</div>
);
Expand Down
28 changes: 28 additions & 0 deletions src/components/ProgramLoader/Examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -131,6 +153,12 @@ export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUpload
Store U16 instruction
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="empty" id="option-empty" />
<Label htmlFor="option-empty" className="cursor-pointer">
Empty
</Label>
</div>
</RadioGroup>
</div>
);
Expand Down
24 changes: 1 addition & 23 deletions src/components/ProgramLoader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -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<ProgramUploadFileOutput>();
const [error, setError] = useState<string>();
Expand Down Expand Up @@ -57,7 +47,6 @@ export const Loader = ({
<TabsTrigger value="upload">JSON tests</TabsTrigger>
<TabsTrigger value="examples">Examples</TabsTrigger>
<TabsTrigger value="bytecode">RAW bytecode</TabsTrigger>
<TabsTrigger value="assembly">Assembly</TabsTrigger>
</TabsList>
<div className="border-2 rounded p-4 flex-1 flex flex-col w-full h-full overflow-auto md:px-5">
<TabsContent value="upload">
Expand All @@ -83,17 +72,6 @@ export const Loader = ({
setIsSubmitted(false);
setError(error);
}}
program={program}
/>
</TabsContent>
<TabsContent value="assembly">
<Assembly
onProgramLoad={(val) => {
setProgramLoad(val);
setIsSubmitted(false);
}}
program={program}
initialState={initialState}
/>
</TabsContent>
{error && isSubmitted && <p className="text-red-500">{error}</p>}
Expand Down
83 changes: 71 additions & 12 deletions src/components/ProgramTextLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,45 +32,83 @@ 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<HTMLTextAreaElement>) => {
const newInput = e.target.value.trim();
setProgramInput(newInput);

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 (
<div className="h-full">
<div className={classNames("h-full flex-auto flex gap-1 flex-col border-2 rounded-md ")}>
<div className={classNames("h-full flex-auto flex gap-1 flex-col")}>
<p className="pb-2 mb-1">
<small>Edit program code bytes</small>
</p>
<Textarea
autoFocus
className={classNames("w-full flex-auto font-mono border-0 text-base")}
className={classNames("w-full flex-auto font-mono text-base border-2 rounded-md", {
"focus-visible:ring-3 focus-visible:outline-none active:outline-none border-red-500": isProgramInvalid,
})}
id="program"
placeholder="Paste the program as an array of numbers or hex string"
value={programInput}
onChange={handleOnChange}
/>
{isProgramInvalid && <span className="text-red-500">{programError || "Program is not valid"}</span>}
</div>
</div>
);
Expand Down
Loading
Loading