Skip to content
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
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"react-toastify": "^10.0.6",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.1"
"vaul": "^0.9.1",
"zod": "^3.24.1"
},
"devDependencies": {
"@playwright/test": "^1.47.2",
Expand Down
63 changes: 0 additions & 63 deletions src/components/ProgramLoader/BinaryFileUpload.tsx

This file was deleted.

18 changes: 0 additions & 18 deletions src/components/ProgramLoader/Bytecode.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/ProgramLoader/Examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const programs: {
export const Examples = ({ onProgramLoad }: { onProgramLoad: (val: ProgramUploadFileOutput) => void }) => {
return (
<div>
<p className="mb-2">Load example test file</p>
<h2 className="text-lg mb-4">Load an example test file</h2>
<RadioGroup
defaultValue="option-fibonacci"
onValueChange={(val) =>
Expand Down
24 changes: 8 additions & 16 deletions src/components/ProgramLoader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Button } from "../ui/button";
import { Bytecode } from "./Bytecode";
import { Examples } from "./Examples";
import { TextFileUpload } from "./TextFileUpload";
import { useState, useCallback, useEffect } from "react";
import { ProgramUploadFileOutput } from "./types";
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";
import { ProgramFileUpload } from "@/components/ProgramLoader/ProgramFileUpload.tsx";

export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) => void }) => {
const dispatch = useAppDispatch();
Expand Down Expand Up @@ -44,17 +43,19 @@ export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) =
<>
<Tabs className="flex-1 flex flex-col items-start overflow-auto" defaultValue="upload">
<TabsList>
<TabsTrigger value="upload">JSON tests</TabsTrigger>
<TabsTrigger value="examples">Examples</TabsTrigger>
<TabsTrigger value="bytecode">RAW bytecode</TabsTrigger>
<TabsTrigger value="upload">Upload file</TabsTrigger>
<TabsTrigger value="examples">Start with examples</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">
<TextFileUpload
<ProgramFileUpload
onFileUpload={(val) => {
setProgramLoad(val);
setIsSubmitted(false);
}}
onParseError={(error) => {
setError(error);
}}
/>
</TabsContent>
<TabsContent value="examples">
Expand All @@ -65,16 +66,7 @@ export const Loader = ({ setIsDialogOpen }: { setIsDialogOpen?: (val: boolean) =
}}
/>
</TabsContent>
<TabsContent value="bytecode">
<Bytecode
onProgramLoad={(val, error) => {
setProgramLoad(val);
setIsSubmitted(false);
setError(error);
}}
/>
</TabsContent>
{error && isSubmitted && <p className="text-red-500">{error}</p>}
{error && isSubmitted && <p className="text-red-500 whitespace-pre-line">{error}</p>}
</div>
</Tabs>
<Button className="mt-3" id="load-button" type="button" onClick={handleLoad}>
Expand Down
180 changes: 180 additions & 0 deletions src/components/ProgramLoader/ProgramFileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { ExternalLink } from "lucide-react";
import { Input } from "../ui/input";
import { ProgramUploadFileOutput } from "./types";
import { mapUploadFileInputToOutput } from "./utils";
import { decodeStandardProgram } from "@typeberry/pvm-debugger-adapter";
import { RegistersArray } from "@/types/pvm.ts";
import { SafeParseReturnType, z } from "zod";

const validateJsonTestCaseSchema = (json: unknown) => {
const pageMapSchema = z.object({
address: z.number(),
length: z.number(),
"is-writable": z.boolean(),
});

const memorySchema = z.object({
address: z.number(),
contents: z.array(z.number()),
});

const schema = z.object({
name: z.string(),
"initial-regs": z.array(z.number()).length(13),
"initial-pc": z.number(),
"initial-page-map": z.array(pageMapSchema),
"initial-memory": z.array(memorySchema),
"initial-gas": z.number(),
program: z.array(z.number()),
"expected-status": z.string(),
"expected-regs": z.array(z.number()),
"expected-pc": z.number(),
"expected-memory": z.array(memorySchema),
"expected-gas": z.number(),
});

return schema.safeParse(json);
};

const generateErrorMessageFromZodValidation = (result: SafeParseReturnType<unknown, unknown>) => {
if (!result.error) {
return false;
}

const formattedErrors = result.error.errors.map((err) => {
const path = err.path.join(" > ") || "root";
return `Field: "${path}" - ${err.message}`;
});

return `File validation failed with the following errors:\n\n${formattedErrors.join("\n")}`;
};

export const ProgramFileUpload = ({
onFileUpload,
onParseError,
close,
}: {
onFileUpload: (val: ProgramUploadFileOutput) => void;
onParseError: (error: string) => void;
close?: () => void;
}) => {
let fileReader: FileReader;

const handleFileRead = (e: ProgressEvent<FileReader>) => {
const arrayBuffer = e.target?.result;

if (arrayBuffer instanceof ArrayBuffer) {
// Try to parse file as a JSON first
try {
const stringContent = new TextDecoder("utf-8").decode(arrayBuffer);

const jsonFile = JSON.parse(stringContent);

const result = validateJsonTestCaseSchema(jsonFile);

if (!result.success) {
const errorMessage = generateErrorMessageFromZodValidation(result);
onParseError(errorMessage || "");
} else {
onFileUpload(mapUploadFileInputToOutput(jsonFile));
}
} catch (e) {
const uint8Array = new Uint8Array(arrayBuffer);

// Try to decode the program as an SPI
try {
const { code, /*memory,*/ registers } = decodeStandardProgram(uint8Array, new Uint8Array());

onFileUpload({
program: Array.from(code),
name: "custom",
initial: {
regs: Array.from(registers) as RegistersArray,
pc: 0,
pageMap: [],
// TODO: map memory properly
// memory: [...memory],
gas: 10000n,
},
});
} catch (e) {
// Try to load program as a Generic
onFileUpload({
program: Array.from(uint8Array),
name: "custom",
initial: {
regs: Array.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) as RegistersArray,
pc: 0,
pageMap: [],
gas: 10000n,
},
});
}
}
}
};

const handleProgramUpload = (file: Blob) => {
fileReader = new FileReader();
fileReader.onloadend = handleFileRead;
fileReader.readAsArrayBuffer(file);
};

return (
<div className="block pb-[100px]">
<h2 className="text-lg">Load a file in one of the following formats:</h2>
<ul className="list-disc p-4">
<li>
<p>JSON test file compatible with JAM TestVectors JSON</p>
<p>
<small>
Examples can be found in <a href="https://github.com/w3f/jamtestvectors">wf3/jamtestvectors</a> Github
repo&nbsp;
<a href="https://github.com/w3f/jamtestvectors/pull/3/files" target="_blank">
<ExternalLink className="inline w-4 mb-1 text-blue-600" />
</a>
</small>
</p>
</li>
<li>
<p>JAM SPI program</p>
<p>
<small>
SPI program definition can be found in
<a href="https://graypaper.fluffylabs.dev/#/5b732de/2a7e022a7e02" target="_blank">
&nbsp;a GrayPaper&nbsp;
<ExternalLink className="inline w-4 mb-1 text-blue-600" />
</a>
</small>
</p>
</li>
<li>
<p>Generic PVM program</p>
<p>
<small>
Generic program definition can be found in
<a href="https://graypaper.fluffylabs.dev/#/5b732de/23c60023c600" target="_blank">
&nbsp;a GrayPaper&nbsp;
<ExternalLink className="inline w-4 mb-1 text-blue-600" />
</a>
</small>
</p>
</li>
</ul>

<Input
className="mt-5 mr-3"
id="test-file"
type="file"
accept=".bin,.pvm,.json"
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
if (e.target.files?.length) {
handleProgramUpload(e.target.files[0]);
close?.();
}
}}
/>
</div>
);
};
Loading
Loading