Skip to content

Commit

Permalink
Unify file loading (#245)
Browse files Browse the repository at this point in the history
* Load both generic and SPI programs in the same file input

* Unify loading with test files

* Add json schema validation
  • Loading branch information
wkwiatek authored Dec 13, 2024
1 parent 7bda3fb commit ae1be4e
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 162 deletions.
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

0 comments on commit ae1be4e

Please sign in to comment.