Skip to content

Commit 7bda3fb

Browse files
authored
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
1 parent 071e8f8 commit 7bda3fb

File tree

8 files changed

+157
-99
lines changed

8 files changed

+157
-99
lines changed

src/App.tsx

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ import { MobileRegisters } from "./components/MobileRegisters";
2121
import { MobileKnowledgeBase } from "./components/KnowledgeBase/Mobile";
2222
import { Assembly } from "./components/ProgramLoader/Assembly";
2323
import { useAppDispatch, useAppSelector } from "@/store/hooks.ts";
24-
import { setClickedInstruction, setInstructionMode, setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts";
24+
import {
25+
setClickedInstruction,
26+
setInstructionMode,
27+
setIsProgramEditMode,
28+
setIsProgramInvalid,
29+
} from "@/store/debugger/debuggerSlice.ts";
2530
import { MemoryPreview } from "@/components/MemoryPreview";
2631
import { DebuggerControlls } from "./components/DebuggerControlls";
2732
import { useDebuggerActions } from "./hooks/useDebuggerActions";
2833
import { Loader } from "./components/ProgramLoader/Loader";
2934
import classNames from "classnames";
3035
import { DebuggerSettings } from "./components/DebuggerSettings";
36+
import { ProgramTextLoader } from "@/components/ProgramTextLoader";
3137

3238
const DebuggerContent = () => {
3339
const dispatch = useAppDispatch();
@@ -36,7 +42,7 @@ const DebuggerContent = () => {
3642
program,
3743
initialState,
3844
isProgramEditMode,
39-
isAsmError,
45+
isProgramInvalid,
4046
programPreviewResult,
4147
clickedInstruction,
4248
instructionMode,
@@ -78,12 +84,31 @@ const DebuggerContent = () => {
7884
{!!program.length && (
7985
<>
8086
{isProgramEditMode && (
81-
<div className="border-2 rounded-md h-full p-2 pt-8">
82-
<Assembly
83-
program={program}
84-
onProgramLoad={debuggerActions.handleProgramLoad}
85-
initialState={initialState}
86-
/>
87+
<div className="border-2 rounded-md h-full p-2">
88+
{instructionMode === InstructionMode.ASM ? (
89+
<Assembly
90+
program={program}
91+
onProgramLoad={debuggerActions.handleProgramLoad}
92+
initialState={initialState}
93+
/>
94+
) : (
95+
<ProgramTextLoader
96+
program={program}
97+
setProgram={(program, error) => {
98+
if (error) {
99+
dispatch(setIsProgramInvalid(true));
100+
}
101+
102+
if (!error && program) {
103+
debuggerActions.handleProgramLoad({
104+
initial: initialState,
105+
program: program || [],
106+
name: "custom",
107+
});
108+
}
109+
}}
110+
/>
111+
)}
87112
</div>
88113
)}
89114

@@ -144,7 +169,6 @@ const DebuggerContent = () => {
144169
<div className={`flex items-center space-x-2 ${!program.length ? "invisible" : "visible"}`}>
145170
<Label htmlFor="instruction-mode">ASM</Label>
146171
<Switch
147-
disabled={isProgramEditMode}
148172
id="instruction-mode"
149173
checked={instructionMode === InstructionMode.BYTECODE}
150174
onCheckedChange={(checked) =>
@@ -158,7 +182,7 @@ const DebuggerContent = () => {
158182
variant="link"
159183
size="icon"
160184
className={!program.length ? "invisible" : "visible"}
161-
disabled={!program.length || isAsmError}
185+
disabled={!program.length || isProgramInvalid}
162186
title="Edit the code"
163187
onClick={() => {
164188
if (isProgramEditMode) {
@@ -180,7 +204,7 @@ const DebuggerContent = () => {
180204
};
181205

182206
function App() {
183-
const { pvmInitialized, initialState, program } = useAppSelector((state) => state.debugger);
207+
const { pvmInitialized } = useAppSelector((state) => state.debugger);
184208

185209
return (
186210
<>
@@ -210,7 +234,7 @@ function App() {
210234
) : (
211235
<div className="col-span-12 flex justify-center h-[50vh] align-middle">
212236
<div className="min-w-[50vw] max-md:w-[100%] min-h-[500px] h-[75vh] flex flex-col">
213-
<Loader initialState={initialState} program={program} />
237+
<Loader />
214238
</div>
215239
</div>
216240
)}

src/components/ProgramLoader/BinaryFileUpload.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const BinaryFileUpload = ({
4444

4545
return (
4646
<div className="block">
47-
<p className="mt-10 mb-3">or upload program as a binary file</p>
47+
<p className="mb-3">Upload program as a binary file</p>
4848
<Input
4949
className="my-6 mr-3"
5050
id="test-file"

src/components/ProgramLoader/Bytecode.tsx

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,17 @@
1-
import { ProgramTextLoader } from "../ProgramTextLoader";
21
import { ProgramUploadFileOutput } from "./types";
32
import { BinaryFileUpload } from "@/components/ProgramLoader/BinaryFileUpload.tsx";
4-
import { useState } from "react";
5-
6-
const initial = {
7-
regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] as [
8-
number,
9-
number,
10-
number,
11-
number,
12-
number,
13-
number,
14-
number,
15-
number,
16-
number,
17-
number,
18-
number,
19-
number,
20-
number,
21-
],
22-
pc: 0,
23-
pageMap: [],
24-
memory: [],
25-
gas: 10000n,
26-
};
273

284
export const Bytecode = ({
295
onProgramLoad,
30-
program,
316
}: {
327
onProgramLoad: (val?: ProgramUploadFileOutput, error?: string) => void;
33-
program: number[];
348
}) => {
35-
const [tempProgram, setTempProgram] = useState<number[] | undefined>(program);
369
const handleFileUpload = (val: ProgramUploadFileOutput) => {
37-
setTempProgram(val.program);
3810
onProgramLoad(val);
3911
};
4012

4113
return (
4214
<div className="h-full flex flex-col">
43-
<p className="mb-3">Edit program code bytes</p>
44-
<ProgramTextLoader
45-
program={tempProgram}
46-
setProgram={(program, error) => {
47-
if (program) {
48-
setTempProgram(program);
49-
onProgramLoad({ initial, program, name: "custom" }, error);
50-
} else {
51-
onProgramLoad(undefined, error);
52-
}
53-
}}
54-
/>
5515
<BinaryFileUpload onFileUpload={handleFileUpload} />
5616
</div>
5717
);

src/components/ProgramLoader/Examples.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ const programs: {
7979
memory: [],
8080
gas: 10000n,
8181
},
82+
empty: {
83+
program: [0, 0, 0],
84+
regs: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] as [
85+
number,
86+
number,
87+
number,
88+
number,
89+
number,
90+
number,
91+
number,
92+
number,
93+
number,
94+
number,
95+
number,
96+
number,
97+
number,
98+
],
99+
pc: 0,
100+
pageMap: [],
101+
memory: [],
102+
gas: 10000n,
103+
},
82104
};
83105

84106
export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUploadFileOutput) => void }) => {
@@ -131,6 +153,12 @@ export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUpload
131153
Store U16 instruction
132154
</Label>
133155
</div>
156+
<div className="flex items-center space-x-2">
157+
<RadioGroupItem value="empty" id="option-empty" />
158+
<Label htmlFor="option-empty" className="cursor-pointer">
159+
Empty
160+
</Label>
161+
</div>
134162
</RadioGroup>
135163
</div>
136164
);

src/components/ProgramLoader/Loader.tsx

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
11
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
22
import { Button } from "../ui/button";
3-
import { Assembly } from "./Assembly";
43
import { Bytecode } from "./Bytecode";
54
import { Examples } from "./Examples";
65
import { TextFileUpload } from "./TextFileUpload";
76
import { useState, useCallback, useEffect } from "react";
87
import { ProgramUploadFileOutput } from "./types";
9-
import { InitialState } from "@/types/pvm";
108
import { useDebuggerActions } from "@/hooks/useDebuggerActions";
119
import { useAppDispatch, useAppSelector } from "@/store/hooks.ts";
1210
import { setIsProgramEditMode } from "@/store/debugger/debuggerSlice.ts";
1311
import { selectIsAnyWorkerLoading } from "@/store/workers/workersSlice";
1412
import { isSerializedError } from "@/store/utils";
1513

16-
export const Loader = ({
17-
initialState,
18-
program,
19-
setIsDialogOpen,
20-
}: {
21-
initialState: InitialState;
22-
program: number[];
23-
setIsDialogOpen?: (val: boolean) => void;
24-
}) => {
14+
export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) => void }) => {
2515
const dispatch = useAppDispatch();
2616
const [programLoad, setProgramLoad] = useState<ProgramUploadFileOutput>();
2717
const [error, setError] = useState<string>();
@@ -57,7 +47,6 @@ export const Loader = ({
5747
<TabsTrigger value="upload">JSON tests</TabsTrigger>
5848
<TabsTrigger value="examples">Examples</TabsTrigger>
5949
<TabsTrigger value="bytecode">RAW bytecode</TabsTrigger>
60-
<TabsTrigger value="assembly">Assembly</TabsTrigger>
6150
</TabsList>
6251
<div className="border-2 rounded p-4 flex-1 flex flex-col w-full h-full overflow-auto md:px-5">
6352
<TabsContent value="upload">
@@ -83,17 +72,6 @@ export const Loader = ({
8372
setIsSubmitted(false);
8473
setError(error);
8574
}}
86-
program={program}
87-
/>
88-
</TabsContent>
89-
<TabsContent value="assembly">
90-
<Assembly
91-
onProgramLoad={(val) => {
92-
setProgramLoad(val);
93-
setIsSubmitted(false);
94-
}}
95-
program={program}
96-
initialState={initialState}
9775
/>
9876
</TabsContent>
9977
{error && isSubmitted && <p className="text-red-500">{error}</p>}

src/components/ProgramTextLoader/index.tsx

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
import { Textarea } from "@/components/ui/textarea.tsx";
2-
import React, { useEffect, useState } from "react";
2+
import React, { useMemo, useState } from "react";
33
import classNames from "classnames";
44
import { bytes } from "@typeberry/block";
55
import { logger } from "@/utils/loggerService";
6+
import { useAppSelector } from "@/store/hooks.ts";
7+
import { selectIsProgramInvalid } from "@/store/debugger/debuggerSlice.ts";
8+
9+
const parseArrayLikeString = (input: string): (number | string)[] => {
10+
// Remove the brackets and split the string by commas
11+
const items = input
12+
.replace(/^\[|\]$/g, "")
13+
.split(",")
14+
.map((item) => item.trim());
15+
16+
// Process each item
17+
return items.map((item) => {
18+
if (/^(?:0x)?[0-9a-fA-F]+$/i.test(item)) {
19+
return parseInt(item, 16);
20+
} else if (!isNaN(Number(item))) {
21+
return Number(item);
22+
} else {
23+
return item;
24+
}
25+
});
26+
};
627

728
export const ProgramTextLoader = ({
829
program,
@@ -11,45 +32,83 @@ export const ProgramTextLoader = ({
1132
program?: number[];
1233
setProgram: (val?: number[], error?: string) => void;
1334
}) => {
14-
const [programInput, setProgramInput] = useState(program?.length ? JSON.stringify(program) : "");
15-
useEffect(() => {
16-
setProgramInput(program?.length ? JSON.stringify(program) : "");
35+
const defaultProgram = useMemo(() => {
36+
return program;
1737
}, [program]);
1838

39+
const [programInput, setProgramInput] = useState(defaultProgram?.length ? JSON.stringify(defaultProgram) : "");
40+
const [programError, setProgramError] = useState("");
41+
const isProgramInvalid = useAppSelector(selectIsProgramInvalid);
42+
1943
const handleOnChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
2044
const newInput = e.target.value.trim();
2145
setProgramInput(newInput);
2246

2347
if (!newInput.startsWith("[")) {
2448
try {
2549
const parsedBlob = bytes.BytesBlob.parseBlob(newInput);
26-
setProgram(Array.prototype.slice.call(parsedBlob.raw));
27-
} catch (error) {
28-
logger.error("Wrong binary file", { error, hideToast: true });
29-
setProgram(undefined, "Wrong binary file");
50+
51+
const parsedBlobArray = Array.prototype.slice.call(parsedBlob.raw);
52+
53+
if (parsedBlobArray.length) {
54+
setProgram(parsedBlobArray);
55+
}
56+
57+
setProgramError("");
58+
} catch (error: unknown) {
59+
logger.error("Wrong binary program", { error, hideToast: true });
60+
61+
setProgram(undefined, "Wrong binary program");
62+
63+
if (error instanceof Error) {
64+
if (error?.message) {
65+
setProgramError(error.message);
66+
}
67+
}
3068
}
3169
} else {
3270
try {
33-
JSON.parse(newInput);
34-
setProgram(JSON.parse(newInput));
71+
// Make sure that hex strings are parsed as strings for JSON.parse validation
72+
const parseTest = newInput.replace(/0x([a-fA-F0-9]+)/g, '"0x$1"');
73+
// Parse it just to check if it's a valid JSON
74+
JSON.parse(parseTest);
75+
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("");
3583
} catch (error) {
3684
logger.error("Wrong JSON", { error, hideToast: true });
85+
3786
setProgram(undefined, "Wrong JSON");
87+
88+
if (error) {
89+
setProgramError(error.toString());
90+
}
3891
}
3992
}
4093
};
4194

4295
return (
4396
<div className="h-full">
44-
<div className={classNames("h-full flex-auto flex gap-1 flex-col border-2 rounded-md ")}>
97+
<div className={classNames("h-full flex-auto flex gap-1 flex-col")}>
98+
<p className="pb-2 mb-1">
99+
<small>Edit program code bytes</small>
100+
</p>
45101
<Textarea
46102
autoFocus
47-
className={classNames("w-full flex-auto font-mono border-0 text-base")}
103+
className={classNames("w-full flex-auto font-mono text-base border-2 rounded-md", {
104+
"focus-visible:ring-3 focus-visible:outline-none active:outline-none border-red-500": isProgramInvalid,
105+
})}
48106
id="program"
49107
placeholder="Paste the program as an array of numbers or hex string"
50108
value={programInput}
51109
onChange={handleOnChange}
52110
/>
111+
{isProgramInvalid && <span className="text-red-500">{programError || "Program is not valid"}</span>}
53112
</div>
54113
</div>
55114
);

0 commit comments

Comments
 (0)