Web-Ide mit aufgenommen
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import {
|
||||
CompareResultLengths,
|
||||
CompareResultLine,
|
||||
compareLines,
|
||||
} from "@nand2tetris/simulator/compare.js";
|
||||
import {
|
||||
KEYBOARD_OFFSET,
|
||||
SCREEN_OFFSET,
|
||||
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import {
|
||||
ASM,
|
||||
Asm,
|
||||
fillLabel,
|
||||
isAValueInstruction,
|
||||
translateInstruction,
|
||||
} from "@nand2tetris/simulator/languages/asm.js";
|
||||
import {
|
||||
CompilationError,
|
||||
Span,
|
||||
} from "@nand2tetris/simulator/languages/base.js";
|
||||
import { Action } from "@nand2tetris/simulator/types.js";
|
||||
import { bin } from "@nand2tetris/simulator/util/twos.js";
|
||||
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||
import { RunSpeed } from "src/runbar.js";
|
||||
import { useImmerReducer } from "../react.js";
|
||||
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||
|
||||
export interface TranslatorSymbol {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function defaultSymbols(): TranslatorSymbol[] {
|
||||
return [
|
||||
{ name: "R0", value: "0" },
|
||||
{ name: "R1", value: "1" },
|
||||
{ name: "R2", value: "2" },
|
||||
{ name: "...", value: "" }, // abbreviation of R3 - R14
|
||||
{ name: "R15", value: "15" },
|
||||
{ name: "SCREEN", value: SCREEN_OFFSET.toString() },
|
||||
{ name: "KBD", value: KEYBOARD_OFFSET.toString() },
|
||||
];
|
||||
}
|
||||
|
||||
interface HighlightInfo {
|
||||
resultHighlight: Span | undefined;
|
||||
sourceHighlight: Span | undefined;
|
||||
highlightMap: Map<Span, Span>;
|
||||
}
|
||||
|
||||
interface AsmVariable {
|
||||
name: string;
|
||||
isHidden: boolean;
|
||||
}
|
||||
|
||||
class Translator {
|
||||
asm: Asm = { instructions: [] };
|
||||
current = -1;
|
||||
done = false;
|
||||
symbols: TranslatorSymbol[] = [];
|
||||
private variables: Map<number, AsmVariable> = new Map();
|
||||
private lines: string[] = [];
|
||||
lineNumbers: number[] = [];
|
||||
|
||||
getResult() {
|
||||
return this.lines.join("\n");
|
||||
}
|
||||
|
||||
load(asm: Asm, lineNum: number): Result<void, CompilationError> {
|
||||
this.symbols = defaultSymbols();
|
||||
this.variables.clear();
|
||||
this.asm = asm;
|
||||
|
||||
const result = fillLabel(asm, (name, value, isVar) => {
|
||||
if (isVar) {
|
||||
this.variables.set(value, { name: name, isHidden: true });
|
||||
} else {
|
||||
this.symbols.push({ name: name, value: value.toString() });
|
||||
}
|
||||
});
|
||||
if (isErr(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
asm.instructions = asm.instructions.filter(({ type }) => type !== "L");
|
||||
|
||||
this.resolveLineNumbers(lineNum);
|
||||
this.reset();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
resolveLineNumbers(lineNum: number) {
|
||||
this.lineNumbers = Array(lineNum);
|
||||
let currentLine = 0;
|
||||
for (const instruction of this.asm.instructions) {
|
||||
if (
|
||||
(instruction.type === "A" || instruction.type === "C") &&
|
||||
instruction.span != undefined
|
||||
) {
|
||||
this.lineNumbers[instruction.span.line] = currentLine;
|
||||
currentLine += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step(highlightInfo: HighlightInfo) {
|
||||
if (this.current >= this.asm.instructions.length - 1) {
|
||||
return;
|
||||
}
|
||||
this.current += 1;
|
||||
const instruction = this.asm.instructions[this.current];
|
||||
if (instruction.type === "A" || instruction.type === "C") {
|
||||
highlightInfo.sourceHighlight = instruction.span;
|
||||
const result = translateInstruction(this.asm.instructions[this.current]);
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
this.lines.push(`${bin(result)}`);
|
||||
highlightInfo.resultHighlight = {
|
||||
start: this.current * 17,
|
||||
end: (this.current + 1) * 17,
|
||||
line: -1,
|
||||
};
|
||||
|
||||
if (highlightInfo.sourceHighlight) {
|
||||
highlightInfo.highlightMap.set(
|
||||
highlightInfo.sourceHighlight,
|
||||
highlightInfo.resultHighlight,
|
||||
);
|
||||
}
|
||||
|
||||
if (isAValueInstruction(instruction)) {
|
||||
const variable = this.variables.get(instruction.value);
|
||||
if (variable != undefined && variable.isHidden) {
|
||||
this.symbols.push({
|
||||
name: variable.name,
|
||||
value: instruction.value.toString(),
|
||||
});
|
||||
variable.isHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.current === this.asm.instructions.length - 1) {
|
||||
this.done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetSymbols() {
|
||||
for (const variable of this.variables.values()) {
|
||||
variable.isHidden = true;
|
||||
}
|
||||
|
||||
const variableNames = new Set(
|
||||
Array.from(this.variables.values()).map((v) => v.name),
|
||||
);
|
||||
this.symbols = this.symbols.filter(
|
||||
(symbol) => !variableNames.has(symbol.name),
|
||||
);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.current = -1;
|
||||
this.lines = [];
|
||||
this.done = false;
|
||||
this.resetSymbols();
|
||||
}
|
||||
}
|
||||
|
||||
export interface AsmPageState {
|
||||
asm: string;
|
||||
path: string | undefined;
|
||||
translating: boolean;
|
||||
current: number;
|
||||
resultHighlight: Span | undefined;
|
||||
sourceHighlight: Span | undefined;
|
||||
symbols: TranslatorSymbol[];
|
||||
result: string;
|
||||
compare: string;
|
||||
compareName: string | undefined;
|
||||
lineNumbers: number[];
|
||||
error?: CompilationError;
|
||||
compareError: boolean;
|
||||
title?: string;
|
||||
config: AsmPageConfig;
|
||||
}
|
||||
|
||||
export interface AsmPageConfig {
|
||||
speed: RunSpeed;
|
||||
}
|
||||
|
||||
export type AsmStoreDispatch = Dispatch<{
|
||||
action: keyof ReturnType<typeof makeAsmStore>["reducers"];
|
||||
payload?: unknown;
|
||||
}>;
|
||||
|
||||
export function makeAsmStore(
|
||||
fs: FileSystem,
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||
dispatch: MutableRefObject<AsmStoreDispatch>,
|
||||
upgraded: boolean,
|
||||
) {
|
||||
const translator = new Translator();
|
||||
const highlightInfo: HighlightInfo = {
|
||||
resultHighlight: undefined,
|
||||
sourceHighlight: undefined,
|
||||
highlightMap: new Map(),
|
||||
};
|
||||
let path: string | undefined;
|
||||
let animate = true;
|
||||
let compiled = false;
|
||||
let translating = false;
|
||||
let failure = false;
|
||||
|
||||
const reducers = {
|
||||
setAsm(
|
||||
state: AsmPageState,
|
||||
{ asm, path }: { asm: string; path: string | undefined },
|
||||
) {
|
||||
state.asm = asm;
|
||||
|
||||
if (path) {
|
||||
state.path = path;
|
||||
}
|
||||
},
|
||||
|
||||
setCmp(state: AsmPageState, { cmp, name }: { cmp: string; name: string }) {
|
||||
state.compare = cmp;
|
||||
state.compareName = name;
|
||||
setStatus("Loaded compare file");
|
||||
},
|
||||
|
||||
setError(state: AsmPageState, error?: CompilationError) {
|
||||
if (error) {
|
||||
setStatus({
|
||||
message: error.message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
}
|
||||
state.error = error;
|
||||
},
|
||||
|
||||
update(state: AsmPageState) {
|
||||
state.translating = translating;
|
||||
state.current = translator.current;
|
||||
state.result = translator.getResult();
|
||||
state.symbols = Array.from(translator.symbols);
|
||||
state.lineNumbers = Array.from(translator.lineNumbers);
|
||||
state.sourceHighlight = highlightInfo.sourceHighlight;
|
||||
state.resultHighlight = highlightInfo.resultHighlight;
|
||||
state.compareError = failure;
|
||||
},
|
||||
|
||||
compare(state: AsmPageState) {
|
||||
const comparison = compareLines(state.result, state.compare);
|
||||
|
||||
if ((comparison as CompareResultLengths).lenA) {
|
||||
failure = true;
|
||||
setStatus({
|
||||
message: "Comparison failed - different lengths",
|
||||
severity: "ERROR",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { line } = comparison as CompareResultLine;
|
||||
if (line) {
|
||||
setStatus({
|
||||
message: `Comparison failure: Line ${line}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
|
||||
failure = true;
|
||||
highlightInfo.resultHighlight = {
|
||||
start: line * 17,
|
||||
end: (line + 1) * 17,
|
||||
line: -1,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({
|
||||
message: "Comparison successful",
|
||||
severity: "SUCCESS",
|
||||
});
|
||||
},
|
||||
|
||||
setTitle(state: AsmPageState, title: string) {
|
||||
state.title = title;
|
||||
},
|
||||
|
||||
updateConfig(state: AsmPageState, config: Partial<AsmPageConfig>) {
|
||||
state.config = { ...state.config, ...config };
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async loadAsm(_path: string) {
|
||||
path = _path;
|
||||
const source = await fs.readFile(path);
|
||||
actions.setAsm(source, path);
|
||||
},
|
||||
|
||||
setAsm(asm: string, path?: string) {
|
||||
asm = asm.replace(/\r\n/g, "\n");
|
||||
dispatch.current({
|
||||
action: "setAsm",
|
||||
payload: { asm, path },
|
||||
});
|
||||
translating = false;
|
||||
this.saveAsm(asm);
|
||||
requestAnimationFrame(() => {
|
||||
this.compileAsm(asm);
|
||||
});
|
||||
},
|
||||
|
||||
saveAsm(asm: string) {
|
||||
if (path) {
|
||||
fs.writeFile(path, asm);
|
||||
}
|
||||
},
|
||||
|
||||
compileAsm(asm: string) {
|
||||
this.reset();
|
||||
const parseResult = ASM.parse(asm);
|
||||
if (isErr(parseResult)) {
|
||||
dispatch.current({
|
||||
action: "setError",
|
||||
payload: Err(parseResult),
|
||||
});
|
||||
compiled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const loadResult = translator.load(
|
||||
Ok(parseResult),
|
||||
asm.split("\n").length,
|
||||
);
|
||||
if (isErr(loadResult)) {
|
||||
dispatch.current({
|
||||
action: "setError",
|
||||
payload: Err(loadResult),
|
||||
});
|
||||
compiled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
compiled = translator.asm.instructions.length > 0;
|
||||
setStatus("");
|
||||
dispatch.current({ action: "setError" });
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
setAnimate(value: boolean) {
|
||||
animate = value;
|
||||
},
|
||||
|
||||
async step(): Promise<boolean> {
|
||||
if (compiled) {
|
||||
translating = true;
|
||||
}
|
||||
translator.step(highlightInfo);
|
||||
|
||||
if (animate || translator.done) {
|
||||
dispatch.current({ action: "update" });
|
||||
|
||||
if (path && upgraded) {
|
||||
await fs.writeFile(
|
||||
path.replace(/\.asm$/, ".hack"),
|
||||
translator.getResult(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (translator.done) {
|
||||
setStatus({
|
||||
message: "Translation done.",
|
||||
severity: "SUCCESS",
|
||||
});
|
||||
}
|
||||
return translator.done;
|
||||
},
|
||||
|
||||
compare() {
|
||||
dispatch.current({ action: "compare" });
|
||||
this.updateHighlight(highlightInfo.resultHighlight?.start ?? 0, false);
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
updateHighlight(index: number, fromSource: boolean) {
|
||||
if (failure) {
|
||||
return;
|
||||
}
|
||||
for (const [sourceSpan, resultSpan] of highlightInfo.highlightMap) {
|
||||
if (
|
||||
(fromSource &&
|
||||
sourceSpan.start <= index &&
|
||||
index <= sourceSpan.end) ||
|
||||
(!fromSource && resultSpan.start <= index && index <= resultSpan.end)
|
||||
) {
|
||||
highlightInfo.sourceHighlight = sourceSpan;
|
||||
highlightInfo.resultHighlight = resultSpan;
|
||||
}
|
||||
}
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
resetHighlightInfo() {
|
||||
highlightInfo.sourceHighlight = undefined;
|
||||
highlightInfo.resultHighlight = undefined;
|
||||
highlightInfo.highlightMap.clear();
|
||||
},
|
||||
|
||||
reset() {
|
||||
failure = false;
|
||||
translating = false;
|
||||
setStatus("Reset");
|
||||
translator.reset();
|
||||
this.resetHighlightInfo();
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.setAsm("");
|
||||
dispatch.current({ action: "setTitle", payload: undefined });
|
||||
dispatch.current({ action: "setCmp", payload: "" });
|
||||
this.reset();
|
||||
},
|
||||
|
||||
overrideState(state: AsmPageState) {
|
||||
this.resetHighlightInfo();
|
||||
this.setAsm(state.asm, state.path);
|
||||
dispatch.current({
|
||||
action: "setCmp",
|
||||
payload: { cmp: state.compare, name: state.compareName },
|
||||
});
|
||||
|
||||
if (state.translating) {
|
||||
for (let i = 0; i <= state.current; i++) {
|
||||
this.step();
|
||||
}
|
||||
}
|
||||
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
};
|
||||
|
||||
const initialState: AsmPageState = {
|
||||
asm: "",
|
||||
path: undefined,
|
||||
translating: false,
|
||||
current: -1,
|
||||
resultHighlight: undefined,
|
||||
sourceHighlight: undefined,
|
||||
symbols: [],
|
||||
result: "",
|
||||
compare: "",
|
||||
compareName: undefined,
|
||||
lineNumbers: [],
|
||||
compareError: false,
|
||||
config: {
|
||||
speed: 2,
|
||||
},
|
||||
};
|
||||
|
||||
return { initialState, reducers, actions };
|
||||
}
|
||||
|
||||
export function useAsmPageStore() {
|
||||
const { setStatus, fs, localFsRoot } = useContext(BaseContext);
|
||||
|
||||
const dispatch = useRef<AsmStoreDispatch>(() => undefined);
|
||||
|
||||
const { initialState, reducers, actions } = useMemo(
|
||||
() => makeAsmStore(fs, setStatus, dispatch, localFsRoot != undefined),
|
||||
[setStatus, dispatch, fs],
|
||||
);
|
||||
|
||||
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||
dispatch.current = dispatcher;
|
||||
|
||||
return { state, dispatch, actions };
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
FileSystem,
|
||||
LocalStorageFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Action, AsyncAction } from "@nand2tetris/simulator/types.js";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDialog } from "../dialog.js";
|
||||
import { cloneTree } from "../file_utils.js";
|
||||
import {
|
||||
FileSystemAccessFileSystemAdapter,
|
||||
openNand2TetrisDirectory,
|
||||
} from "./base/fs.js";
|
||||
import {
|
||||
attemptLoadAdapterFromIndexedDb,
|
||||
createAndStoreLocalAdapterInIndexedDB,
|
||||
removeLocalAdapterFromIndexedDB,
|
||||
} from "./base/indexDb.js";
|
||||
|
||||
export type StatusSeverity = "SUCCESS" | "WARNING" | "ERROR" | "INFO";
|
||||
|
||||
export interface BaseContext {
|
||||
fs: FileSystem;
|
||||
localFsRoot?: string;
|
||||
canUpgradeFs: boolean;
|
||||
upgradeFs: (force?: boolean, createFiles?: boolean) => Promise<void>;
|
||||
closeFs: () => void;
|
||||
status: { message: string; severity: StatusSeverity };
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>;
|
||||
storage: Record<string, string>;
|
||||
permissionPrompt: ReturnType<typeof useDialog>;
|
||||
requestPermission: AsyncAction<void>;
|
||||
loadFs: Action<void>;
|
||||
}
|
||||
|
||||
export function useBaseContext(): BaseContext {
|
||||
const localAdapter = useMemo(() => new LocalStorageFileSystemAdapter(), []);
|
||||
const [fs, setFs] = useState(new FileSystem(localAdapter));
|
||||
const [root, setRoot] = useState<string>();
|
||||
|
||||
const permissionPrompt = useDialog();
|
||||
|
||||
const setLocalFs = useCallback(
|
||||
async (handle: FileSystemDirectoryHandle, createFiles = false) => {
|
||||
// We will not mirror the changes in localStorage, since they will be saved in the user's file system
|
||||
const newFs = new FileSystem(
|
||||
new FileSystemAccessFileSystemAdapter(handle),
|
||||
);
|
||||
if (createFiles) {
|
||||
if (root) {
|
||||
const loaders = await import("@nand2tetris/projects/loader.js");
|
||||
await loaders.createFiles(newFs);
|
||||
} else {
|
||||
await cloneTree(fs, newFs, "/projects", (path: string) =>
|
||||
path.replace("/projects", "/").replace(/\/0*(\d+)/, "$1"),
|
||||
);
|
||||
}
|
||||
}
|
||||
setFs(newFs);
|
||||
setRoot(handle.name);
|
||||
},
|
||||
[setRoot, setFs],
|
||||
);
|
||||
|
||||
const requestPermission = async () => {
|
||||
attemptLoadAdapterFromIndexedDb().then(async (adapter) => {
|
||||
if (!adapter) return;
|
||||
await adapter.requestPermission({ mode: "readwrite" });
|
||||
});
|
||||
};
|
||||
|
||||
const loadFs = () => {
|
||||
attemptLoadAdapterFromIndexedDb().then(async (adapter) => {
|
||||
if (!adapter) return;
|
||||
setLocalFs(adapter);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (root) return;
|
||||
|
||||
if ("showDirectoryPicker" in window) {
|
||||
attemptLoadAdapterFromIndexedDb().then(async (adapter) => {
|
||||
if (!adapter) return;
|
||||
|
||||
const permissions = await adapter.queryPermission({
|
||||
mode: "readwrite",
|
||||
});
|
||||
|
||||
switch (permissions) {
|
||||
case "granted":
|
||||
setLocalFs(adapter);
|
||||
break;
|
||||
case "prompt":
|
||||
permissionPrompt.open();
|
||||
break;
|
||||
case "denied":
|
||||
setStatus({
|
||||
message:
|
||||
"Permission denied. Please allow access to your file system.",
|
||||
severity: "ERROR",
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [root, setLocalFs]);
|
||||
|
||||
const canUpgradeFs = `showDirectoryPicker` in window;
|
||||
|
||||
const upgradeFs = useCallback(
|
||||
async (force = false, createFiles = false) => {
|
||||
if (!canUpgradeFs || (root && !force)) return;
|
||||
const handler = await openNand2TetrisDirectory();
|
||||
if (root) {
|
||||
await removeLocalAdapterFromIndexedDB();
|
||||
}
|
||||
const adapter = await createAndStoreLocalAdapterInIndexedDB(handler);
|
||||
await setLocalFs(adapter, createFiles);
|
||||
},
|
||||
[root, setLocalFs],
|
||||
);
|
||||
|
||||
const closeFs = useCallback(async () => {
|
||||
if (!root) return;
|
||||
await removeLocalAdapterFromIndexedDB();
|
||||
setRoot(undefined);
|
||||
setFs(new FileSystem(localAdapter));
|
||||
}, [root]);
|
||||
|
||||
const [status, setStatusInternal] = useState<{
|
||||
message: string;
|
||||
severity: StatusSeverity;
|
||||
}>({ message: "", severity: "INFO" });
|
||||
|
||||
const setStatus = useCallback(
|
||||
(input: string | { message: string; severity?: StatusSeverity }) => {
|
||||
if (typeof input === "string") {
|
||||
setStatusInternal({ message: input, severity: "INFO" });
|
||||
} else {
|
||||
setStatusInternal({
|
||||
message: input.message,
|
||||
severity: input.severity || "INFO",
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
fs,
|
||||
localFsRoot: root,
|
||||
status,
|
||||
setStatus,
|
||||
storage: localStorage,
|
||||
canUpgradeFs,
|
||||
permissionPrompt,
|
||||
upgradeFs,
|
||||
requestPermission,
|
||||
closeFs,
|
||||
loadFs,
|
||||
};
|
||||
}
|
||||
|
||||
export const BaseContext = createContext<BaseContext>({
|
||||
fs: new FileSystem(new LocalStorageFileSystemAdapter()),
|
||||
canUpgradeFs: false,
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||
async upgradeFs() {},
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||
closeFs() {},
|
||||
status: { message: "", severity: "INFO" },
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||
setStatus() {},
|
||||
storage: {},
|
||||
permissionPrompt: {} as ReturnType<typeof useDialog>,
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||
async requestPermission() {},
|
||||
// biome-ignore lint/suspicious/noEmptyBlockStatements: abstract base
|
||||
loadFs() {},
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
basename,
|
||||
FileSystemAdapter,
|
||||
SEP,
|
||||
Stats,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
|
||||
function dirname(path: string): string {
|
||||
return path.split(SEP).slice(0, -1).join(SEP);
|
||||
}
|
||||
|
||||
export function openNand2TetrisDirectory(): Promise<FileSystemDirectoryHandle> {
|
||||
return window.showDirectoryPicker({
|
||||
id: "nand2tetris",
|
||||
mode: "readwrite",
|
||||
startIn: "documents",
|
||||
});
|
||||
}
|
||||
|
||||
export class FileSystemAccessFileSystemAdapter implements FileSystemAdapter {
|
||||
constructor(private baseDir: FileSystemDirectoryHandle) {}
|
||||
|
||||
async getFolder(
|
||||
path: string,
|
||||
create = false,
|
||||
): Promise<FileSystemDirectoryHandle> {
|
||||
let folder = this.baseDir;
|
||||
const parts = path
|
||||
.split(SEP)
|
||||
.slice(1)
|
||||
.filter((part) => part.trim() != "");
|
||||
for (const part of parts) {
|
||||
folder = await folder.getDirectoryHandle(part, { create });
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
async copyFile(from: string, to: string): Promise<void> {
|
||||
throw new Error(
|
||||
"unimplemented: FileSystemAccessFileSystemAdapter::copyFile",
|
||||
);
|
||||
}
|
||||
|
||||
async mkdir(path: string): Promise<void> {
|
||||
this.getFolder(path, true);
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<string> {
|
||||
const folder = await this.getFolder(dirname(path));
|
||||
const file = await (await folder.getFileHandle(basename(path))).getFile();
|
||||
return file.text();
|
||||
}
|
||||
|
||||
async writeFile(path: string, contents: string): Promise<void> {
|
||||
const folder = await this.getFolder(dirname(path), true);
|
||||
const file = await (
|
||||
await folder.getFileHandle(basename(path), { create: true })
|
||||
).createWritable();
|
||||
await file.write(contents);
|
||||
await file.close();
|
||||
}
|
||||
|
||||
async readdir(path: string): Promise<string[]> {
|
||||
const folder = await this.getFolder(path);
|
||||
const entries: string[] = [];
|
||||
for await (const [entry, _] of folder.entries()) {
|
||||
entries.push(entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
async scandir(path: string): Promise<Stats[]> {
|
||||
const folder = await this.getFolder(path);
|
||||
const entries: Stats[] = [];
|
||||
for await (const [name, handle] of folder.entries()) {
|
||||
entries.push({
|
||||
name,
|
||||
isDirectory() {
|
||||
return handle.kind == "directory";
|
||||
},
|
||||
isFile() {
|
||||
return handle.kind == "file";
|
||||
},
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
async stat(path: string): Promise<Stats> {
|
||||
const folder = await this.getFolder(dirname(path));
|
||||
for await (const [name, handle] of folder.entries()) {
|
||||
if (name == basename(path)) {
|
||||
return {
|
||||
name,
|
||||
isDirectory() {
|
||||
return handle.kind == "directory";
|
||||
},
|
||||
isFile() {
|
||||
return handle.kind == "file";
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: basename(path),
|
||||
isDirectory() {
|
||||
return false;
|
||||
},
|
||||
isFile() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async rm(path: string): Promise<void> {
|
||||
const folder = await this.getFolder(dirname(path), true);
|
||||
await folder.removeEntry(basename(path), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class ChainedFileSystemAdapter implements FileSystemAdapter {
|
||||
constructor(
|
||||
protected adapter: FileSystemAdapter,
|
||||
private nextAdapter?: FileSystemAdapter | undefined,
|
||||
) {}
|
||||
|
||||
stat(path: string): Promise<Stats> {
|
||||
return this.adapter.stat(path).catch((e) => {
|
||||
if (this.nextAdapter) {
|
||||
return this.nextAdapter.stat(path);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
readdir(path: string): Promise<string[]> {
|
||||
return this.adapter.readdir(path).catch((e) => {
|
||||
if (this.nextAdapter) {
|
||||
return this.nextAdapter.readdir(path);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
scandir(path: string): Promise<Stats[]> {
|
||||
return this.adapter.scandir(path).catch((e) => {
|
||||
if (this.nextAdapter) {
|
||||
return this.nextAdapter.scandir(path);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
async mkdir(path: string): Promise<void> {
|
||||
if (this.nextAdapter) await this.nextAdapter.mkdir(path);
|
||||
return this.adapter.mkdir(path);
|
||||
}
|
||||
async copyFile(from: string, to: string): Promise<void> {
|
||||
if (this.nextAdapter) await this.nextAdapter.copyFile(from, to);
|
||||
return this.adapter.copyFile(from, to);
|
||||
}
|
||||
readFile(path: string): Promise<string> {
|
||||
return this.adapter.readFile(path).catch((e) => {
|
||||
if (this.nextAdapter) {
|
||||
return this.nextAdapter.readFile(path);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
async writeFile(path: string, contents: string): Promise<void> {
|
||||
if (this.nextAdapter) await this.nextAdapter?.writeFile(path, contents);
|
||||
return this.adapter.writeFile(path, contents);
|
||||
}
|
||||
async rm(path: string): Promise<void> {
|
||||
if (this.nextAdapter) await this.nextAdapter.rm(path);
|
||||
return this.adapter.rm(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { assert } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
|
||||
const IDB_NAME = "NAND2TetrisIndexedDB";
|
||||
const IDB_VERSION = 1;
|
||||
const IDB_FS_ADAPTER_OBJECT_STORE = "FileSystemAccess";
|
||||
const IDB_FS_ADAPTER_KEY = "Handler";
|
||||
function openIndexedDb(): Promise<IDBDatabase> {
|
||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = window.indexedDB.open(IDB_NAME, IDB_VERSION);
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
request.onupgradeneeded = (e) => {
|
||||
request.result.createObjectStore(IDB_FS_ADAPTER_OBJECT_STORE);
|
||||
};
|
||||
});
|
||||
}
|
||||
export async function attemptLoadAdapterFromIndexedDb(): Promise<FileSystemDirectoryHandle | void> {
|
||||
const db = await openIndexedDb();
|
||||
return new Promise<FileSystemDirectoryHandle | void>((resolve, reject) => {
|
||||
const transaction = db.transaction(
|
||||
[IDB_FS_ADAPTER_OBJECT_STORE],
|
||||
"readonly",
|
||||
);
|
||||
const objectStore = transaction.objectStore(IDB_FS_ADAPTER_OBJECT_STORE);
|
||||
const handleRequest = objectStore.get(IDB_FS_ADAPTER_KEY);
|
||||
handleRequest.onsuccess = () => {
|
||||
const handle = handleRequest.result;
|
||||
if (handle === undefined) {
|
||||
resolve();
|
||||
} else {
|
||||
assert(
|
||||
handle instanceof FileSystemDirectoryHandle,
|
||||
`Retrieved ${IDB_FS_ADAPTER_KEY} in ${IDB_FS_ADAPTER_OBJECT_STORE} in ${IDB_NAME} is not a FileSystemDirectoryHandle`,
|
||||
);
|
||||
resolve(handle);
|
||||
}
|
||||
};
|
||||
transaction.onerror = () => {
|
||||
console.error("Error in loading FileSystemDirectoryHandle transaction", {
|
||||
err: transaction.error,
|
||||
});
|
||||
reject(transaction.error);
|
||||
};
|
||||
handleRequest.onerror = () => {
|
||||
console.error("Error in FileSystemDirectoryHandle handleRequest", {
|
||||
err: handleRequest.error,
|
||||
});
|
||||
reject(handleRequest.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAndStoreLocalAdapterInIndexedDB(
|
||||
handle: FileSystemDirectoryHandle,
|
||||
): Promise<FileSystemDirectoryHandle> {
|
||||
const db = await openIndexedDb();
|
||||
const transaction = db.transaction(
|
||||
[IDB_FS_ADAPTER_OBJECT_STORE],
|
||||
"readwrite",
|
||||
);
|
||||
transaction
|
||||
.objectStore(IDB_FS_ADAPTER_OBJECT_STORE)
|
||||
.add(handle, IDB_FS_ADAPTER_KEY);
|
||||
transaction.commit();
|
||||
return new Promise<FileSystemDirectoryHandle>((resolve, reject) => {
|
||||
transaction.oncomplete = () => {
|
||||
resolve(handle);
|
||||
};
|
||||
transaction.onerror = () => {
|
||||
reject(transaction.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeLocalAdapterFromIndexedDB() {
|
||||
const db = await openIndexedDb();
|
||||
const transaction = db.transaction(
|
||||
[IDB_FS_ADAPTER_OBJECT_STORE],
|
||||
"readwrite",
|
||||
);
|
||||
transaction
|
||||
.objectStore(IDB_FS_ADAPTER_OBJECT_STORE)
|
||||
.delete(IDB_FS_ADAPTER_KEY);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
transaction.oncomplete = () => {
|
||||
resolve();
|
||||
};
|
||||
transaction.onerror = () => {
|
||||
reject(transaction.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
FileSystem,
|
||||
ObjectFileSystemAdapter,
|
||||
} from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { cleanState } from "@davidsouther/jiffies/lib/esm/scope/state.js";
|
||||
import * as not from "@nand2tetris/projects/project_01/01_not.js";
|
||||
import { produce } from "immer";
|
||||
import { MutableRefObject } from "react";
|
||||
import { ImmPin } from "src/pinout.js";
|
||||
import { ChipStoreDispatch, makeChipStore } from "./chip.store.js";
|
||||
|
||||
function testChipStore(
|
||||
fs: Record<string, string> = {
|
||||
"projects/01/Not.hdl": not.hdl,
|
||||
"projects/01/Not.tst": not.tst,
|
||||
"projects/01/Not.cmp": not.cmp,
|
||||
},
|
||||
storage: Record<string, string> = {},
|
||||
) {
|
||||
const dispatch: MutableRefObject<ChipStoreDispatch> = { current: jest.fn() };
|
||||
|
||||
const setStatus = jest.fn();
|
||||
|
||||
const { initialState, actions, reducers } = makeChipStore(
|
||||
new FileSystem(new ObjectFileSystemAdapter(fs)),
|
||||
setStatus,
|
||||
storage,
|
||||
dispatch,
|
||||
false,
|
||||
);
|
||||
const store = { state: initialState, actions, reducers, dispatch, setStatus };
|
||||
dispatch.current = jest.fn().mockImplementation(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: covariants are hard
|
||||
(command: { action: keyof typeof reducers; payload: any }) => {
|
||||
store.state = produce(store.state, (draft: typeof initialState) => {
|
||||
reducers[command.action](draft, command.payload);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
describe("ChipStore", () => {
|
||||
describe("initialization", () => {
|
||||
it("loads chip", async () => {
|
||||
const store = testChipStore({
|
||||
"projects/01/Not.hdl": not.hdl,
|
||||
"projects/01/Not.tst": not.tst,
|
||||
"projects/01/Not.cmp": not.cmp,
|
||||
});
|
||||
|
||||
await store.actions.initialize();
|
||||
await store.actions.loadChip("projects/01/Not.hdl");
|
||||
|
||||
expect(store.state.controls.project).toBe("01");
|
||||
expect(store.state.controls.chipName).toBe("Not");
|
||||
expect(store.state.files.hdl).toBe(not.hdl);
|
||||
expect(store.state.files.tst).toBe(not.tst);
|
||||
expect(store.state.files.cmp).toBe("");
|
||||
expect(store.state.files.out).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("behavior", () => {
|
||||
const state = cleanState(async () => {
|
||||
const store = testChipStore({
|
||||
"projects/01/Not.hdl": not.hdl,
|
||||
"projects/01/Not.tst": not.tst,
|
||||
"projects/01/Not.cmp": not.cmp,
|
||||
});
|
||||
await store.actions.initialize();
|
||||
await store.actions.loadChip("projects/01/Not.hdl");
|
||||
return { store };
|
||||
}, beforeEach);
|
||||
|
||||
it.todo("loads projects and chips");
|
||||
|
||||
it("toggles bits", async () => {
|
||||
state.store.actions.toggle(state.store.state.sim.chip[0].in(), 0);
|
||||
expect(state.store.state.sim.chip[0].in().busVoltage).toBe(1);
|
||||
expect(state.store.dispatch.current).toHaveBeenCalledWith({
|
||||
action: "updateChip",
|
||||
payload: { pending: true },
|
||||
});
|
||||
expect(state.store.state.sim.pending).toBe(true);
|
||||
|
||||
state.store.actions.eval();
|
||||
expect(state.store.dispatch.current).toHaveBeenCalledWith({
|
||||
action: "updateChip",
|
||||
payload: { pending: false },
|
||||
});
|
||||
expect(state.store.state.sim.pending).toBe(false);
|
||||
expect(state.store.state.sim.chip[0].out().busVoltage).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("execution", () => {
|
||||
const state = cleanState(async () => {
|
||||
const store = testChipStore({
|
||||
"projects/01/Not.hdl": not.hdl,
|
||||
"projects/01/Not.tst": not.tst,
|
||||
"projects/01/Not.cmp": not.cmp,
|
||||
});
|
||||
await store.actions.initialize();
|
||||
await store.actions.loadChip("projects/01/Not.hdl");
|
||||
return { store };
|
||||
}, beforeEach);
|
||||
|
||||
it.todo("compiles chips");
|
||||
|
||||
it("steps tests", async () => {
|
||||
const bits = (pins: ImmPin[]) =>
|
||||
pins.map((pin) => pin.bits.map((bit) => bit[1]));
|
||||
|
||||
expect(bits(state.store.state.sim.inPins)).toEqual([[0]]);
|
||||
expect(bits(state.store.state.sim.outPins)).toEqual([[0]]);
|
||||
|
||||
await state.store.actions.toggleBuiltin();
|
||||
|
||||
expect(bits(state.store.state.sim.inPins)).toEqual([[0]]);
|
||||
expect(bits(state.store.state.sim.outPins)).toEqual([[1]]);
|
||||
|
||||
await state.store.actions.stepTest(); // Load, Compare To and Output List
|
||||
|
||||
await state.store.actions.stepTest(); // Set in 0
|
||||
expect(bits(state.store.state.sim.inPins)).toEqual([[0]]);
|
||||
expect(bits(state.store.state.sim.outPins)).toEqual([[1]]);
|
||||
|
||||
await state.store.actions.stepTest(); // Set in 1
|
||||
expect(bits(state.store.state.sim.inPins)).toEqual([[1]]);
|
||||
expect(bits(state.store.state.sim.outPins)).toEqual([[0]]);
|
||||
|
||||
await state.store.actions.stepTest(); // No change (after end)
|
||||
expect(bits(state.store.state.sim.inPins)).toEqual([[1]]);
|
||||
expect(bits(state.store.state.sim.outPins)).toEqual([[0]]);
|
||||
});
|
||||
|
||||
it("starts the cursor on the first instruction", () => {
|
||||
expect(state.store.state.files.tst).toBe(not.tst);
|
||||
expect(state.store.state.controls.span).toEqual({
|
||||
start: 167,
|
||||
end: 220,
|
||||
line: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves the cursor on the final character", async () => {
|
||||
// Not.tst has 3 commands
|
||||
await state.store.actions.stepTest();
|
||||
await state.store.actions.stepTest();
|
||||
await state.store.actions.stepTest();
|
||||
|
||||
// Past the end of the test
|
||||
await state.store.actions.stepTest();
|
||||
|
||||
expect(state.store.state.controls.span).toEqual({
|
||||
start: 269,
|
||||
end: 270,
|
||||
line: 16,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,647 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { display } from "@davidsouther/jiffies/lib/esm/display.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { Err, isErr, Ok } from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import {
|
||||
BUILTIN_CHIP_PROJECTS,
|
||||
CHIP_PROJECTS,
|
||||
sortChips,
|
||||
} from "@nand2tetris/projects/base.js";
|
||||
import { parse as parseChip } from "@nand2tetris/simulator/chip/builder.js";
|
||||
import { getBuiltinChip } from "@nand2tetris/simulator/chip/builtins/index.js";
|
||||
import {
|
||||
Chip,
|
||||
Low,
|
||||
Pin,
|
||||
Chip as SimChip,
|
||||
} from "@nand2tetris/simulator/chip/chip.js";
|
||||
import { Clock } from "@nand2tetris/simulator/chip/clock.js";
|
||||
import {
|
||||
CompilationError,
|
||||
Span,
|
||||
} from "@nand2tetris/simulator/languages/base.js";
|
||||
import { TST } from "@nand2tetris/simulator/languages/tst.js";
|
||||
import { ChipTest } from "@nand2tetris/simulator/test/chiptst.js";
|
||||
import { Action } from "@nand2tetris/simulator/types.js";
|
||||
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||
import { compare } from "../compare.js";
|
||||
import { sortFiles } from "../file_utils.js";
|
||||
import { ImmPin, reducePins } from "../pinout.js";
|
||||
import { useImmerReducer } from "../react.js";
|
||||
import { RunSpeed } from "../runbar.js";
|
||||
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||
|
||||
export const NO_SCREEN = "noScreen";
|
||||
|
||||
export const PROJECT_NAMES = [
|
||||
["01", `Project 1`],
|
||||
["02", `Project 2`],
|
||||
["03", `Project 3`],
|
||||
["05", `Project 5`],
|
||||
];
|
||||
|
||||
const TEST_NAMES: Record<string, string[]> = {
|
||||
CPU: ["CPU", "CPU-external"],
|
||||
Computer: ["ComputerAdd", "ComputerMax", "ComputerRect"],
|
||||
};
|
||||
|
||||
export function isBuiltinOnly(
|
||||
project: keyof typeof CHIP_PROJECTS,
|
||||
chipName: string,
|
||||
) {
|
||||
return BUILTIN_CHIP_PROJECTS[project].includes(chipName);
|
||||
}
|
||||
|
||||
function convertToBuiltin(name: string, hdl: string) {
|
||||
return hdl.replace(/PARTS:([\s\S]*?)\}/, `PARTS:\n\tBUILTIN ${name};`);
|
||||
}
|
||||
|
||||
export interface ChipPageState {
|
||||
title?: string;
|
||||
files: Files;
|
||||
sim: ChipSim;
|
||||
controls: ControlsState;
|
||||
config: ChipPageConfig;
|
||||
dir?: string;
|
||||
}
|
||||
|
||||
export interface ChipPageConfig {
|
||||
speed: RunSpeed;
|
||||
}
|
||||
|
||||
export interface ChipSim {
|
||||
clocked: boolean;
|
||||
inPins: ImmPin[];
|
||||
outPins: ImmPin[];
|
||||
internalPins: ImmPin[];
|
||||
chip: [Chip];
|
||||
pending: boolean;
|
||||
invalid: boolean;
|
||||
}
|
||||
|
||||
export interface Files {
|
||||
hdl: string;
|
||||
cmp: string;
|
||||
tst: string;
|
||||
out: string;
|
||||
}
|
||||
|
||||
export interface ControlsState {
|
||||
projects: string[];
|
||||
project: string;
|
||||
chips: string[];
|
||||
chipName: string;
|
||||
tests: string[];
|
||||
testName: string;
|
||||
usingBuiltin: boolean;
|
||||
runningTest: boolean;
|
||||
span?: Span;
|
||||
error?: CompilationError;
|
||||
visualizationParameters: Set<string>;
|
||||
}
|
||||
|
||||
export interface HDLFile {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function reduceChip(chip: SimChip, pending = false, invalid = false): ChipSim {
|
||||
return {
|
||||
clocked: chip.clocked,
|
||||
inPins: reducePins(chip.ins),
|
||||
outPins: reducePins(chip.outs),
|
||||
internalPins: reducePins(chip.pins),
|
||||
chip: [chip],
|
||||
pending,
|
||||
invalid,
|
||||
};
|
||||
}
|
||||
|
||||
const clock = Clock.get();
|
||||
|
||||
export type ChipStoreDispatch = Dispatch<{
|
||||
action: keyof ReturnType<typeof makeChipStore>["reducers"];
|
||||
payload?: unknown;
|
||||
}>;
|
||||
|
||||
export function makeChipStore(
|
||||
fs: FileSystem,
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||
storage: Record<string, string>,
|
||||
dispatch: MutableRefObject<ChipStoreDispatch>,
|
||||
upgraded: boolean,
|
||||
) {
|
||||
let _chipName = "";
|
||||
let _dir = "";
|
||||
let chip = new Low();
|
||||
let backupHdl = "";
|
||||
let tests: string[] = [];
|
||||
let test = new ChipTest();
|
||||
let usingBuiltin = false;
|
||||
let invalid = false;
|
||||
|
||||
const reducers = {
|
||||
setFiles(
|
||||
state: ChipPageState,
|
||||
{
|
||||
hdl = state.files.hdl,
|
||||
tst = state.files.tst,
|
||||
cmp = state.files.cmp,
|
||||
out = "",
|
||||
}: {
|
||||
hdl?: string;
|
||||
tst?: string;
|
||||
cmp?: string;
|
||||
out?: string;
|
||||
},
|
||||
) {
|
||||
state.files.hdl = hdl;
|
||||
state.files.tst = tst;
|
||||
state.files.cmp = cmp;
|
||||
state.files.out = out;
|
||||
},
|
||||
|
||||
updateChip(
|
||||
state: ChipPageState,
|
||||
payload?: {
|
||||
pending?: boolean;
|
||||
invalid?: boolean;
|
||||
chipName?: string;
|
||||
error?: CompilationError | undefined;
|
||||
},
|
||||
) {
|
||||
state.sim = reduceChip(
|
||||
chip,
|
||||
payload?.pending ?? state.sim.pending,
|
||||
payload?.invalid ?? state.sim.invalid,
|
||||
);
|
||||
state.controls.error = state.sim.invalid
|
||||
? (payload?.error ?? state.controls.error)
|
||||
: undefined;
|
||||
},
|
||||
|
||||
setProjects(state: ChipPageState, projects: string[]) {
|
||||
state.controls.projects = projects;
|
||||
},
|
||||
|
||||
setProject(state: ChipPageState, project: keyof typeof CHIP_PROJECTS) {
|
||||
state.controls.project = project;
|
||||
},
|
||||
|
||||
setChips(state: ChipPageState, chips: string[]) {
|
||||
state.controls.chips = chips;
|
||||
},
|
||||
|
||||
setChip(
|
||||
state: ChipPageState,
|
||||
{ chipName, dir }: { chipName: string; dir: string },
|
||||
) {
|
||||
_dir = dir;
|
||||
_chipName = chipName;
|
||||
state.controls.chipName = chipName;
|
||||
state.title = `${chipName}.hdl`;
|
||||
state.controls.tests = Array.from(tests);
|
||||
state.dir = dir;
|
||||
},
|
||||
|
||||
clearChip(state: ChipPageState) {
|
||||
_chipName = "";
|
||||
state.controls.chipName = "";
|
||||
state.title = undefined;
|
||||
state.controls.tests = [];
|
||||
|
||||
this.setFiles(state, { hdl: "", tst: "", cmp: "", out: "" });
|
||||
},
|
||||
|
||||
setTest(state: ChipPageState, testName: string) {
|
||||
state.controls.testName = testName;
|
||||
},
|
||||
|
||||
setVisualizationParams(state: ChipPageState, params: Set<string>) {
|
||||
state.controls.visualizationParameters = new Set(params);
|
||||
},
|
||||
|
||||
testRunning(state: ChipPageState) {
|
||||
state.controls.runningTest = true;
|
||||
},
|
||||
|
||||
testFinished(state: ChipPageState) {
|
||||
state.controls.runningTest = false;
|
||||
const passed = compare(state.files.cmp.trim(), state.files.out.trim());
|
||||
// For some reason, this is happening during a render but I can't track it down.
|
||||
Promise.resolve().then(() => {
|
||||
setStatus(
|
||||
passed
|
||||
? {
|
||||
message: `Simulation successful: The output file is identical to the compare file`,
|
||||
severity: "SUCCESS",
|
||||
}
|
||||
: {
|
||||
message: `Simulation error: The output file differs from the compare file`,
|
||||
severity: "ERROR",
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
updateTestStep(state: ChipPageState) {
|
||||
state.files.out = test?.log() ?? "";
|
||||
if (test?.currentStep?.span) {
|
||||
state.controls.span = test.currentStep.span;
|
||||
} else {
|
||||
if (test.done) {
|
||||
const end = state.files.tst.length;
|
||||
state.controls.span = {
|
||||
start: end - 1,
|
||||
end,
|
||||
line: state.files.tst.split("\n").length,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.updateChip(state, {
|
||||
pending: state.sim.pending,
|
||||
invalid: state.sim.invalid,
|
||||
});
|
||||
},
|
||||
|
||||
updateConfig(state: ChipPageState, config: Partial<ChipPageConfig>) {
|
||||
state.config = { ...state.config, ...config };
|
||||
},
|
||||
|
||||
updateUsingBuiltin(state: ChipPageState) {
|
||||
state.controls.usingBuiltin = usingBuiltin;
|
||||
},
|
||||
|
||||
displayBuiltin(state: ChipPageState) {
|
||||
backupHdl = state.files.hdl;
|
||||
this.setFiles(state, {
|
||||
hdl: convertToBuiltin(state.controls.chipName, state.files.hdl),
|
||||
});
|
||||
},
|
||||
|
||||
toggleBuiltin(state: ChipPageState) {
|
||||
state.controls.usingBuiltin = usingBuiltin;
|
||||
if (usingBuiltin) {
|
||||
this.displayBuiltin(state);
|
||||
} else {
|
||||
this.setFiles(state, { hdl: backupHdl });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async initialize() {
|
||||
const projectsFolder = upgraded ? "/" : "/projects";
|
||||
|
||||
const entries = await fs.scandir(projectsFolder);
|
||||
const hdlProjects = [];
|
||||
|
||||
for (const project of entries.filter((project) =>
|
||||
project.isDirectory(),
|
||||
)) {
|
||||
const items = await fs.scandir(`${projectsFolder}/${project.name}`);
|
||||
if (items.some((item) => item.isFile() && item.name.endsWith(".hdl"))) {
|
||||
hdlProjects.push(project);
|
||||
}
|
||||
}
|
||||
|
||||
const sortedNames = sortFiles(hdlProjects).map((project) => project.name);
|
||||
|
||||
dispatch.current({
|
||||
action: "setProjects",
|
||||
payload: sortedNames,
|
||||
});
|
||||
|
||||
if (hdlProjects.length > 0) {
|
||||
await actions.setProject(sortedNames[0]);
|
||||
} else {
|
||||
dispatch.current({ action: "setChips", payload: [] });
|
||||
}
|
||||
|
||||
dispatch.current({ action: "clearChip" });
|
||||
},
|
||||
|
||||
reset() {
|
||||
Clock.get().reset();
|
||||
chip.reset();
|
||||
test.reset();
|
||||
dispatch.current({ action: "setFiles", payload: {} });
|
||||
dispatch.current({ action: "updateChip" });
|
||||
},
|
||||
|
||||
async setProject(project: string) {
|
||||
storage["/chip/project"] = project;
|
||||
dispatch.current({ action: "setProject", payload: project });
|
||||
|
||||
const prefix = upgraded ? "/" : "/projects";
|
||||
|
||||
const chips = (await fs.scandir(`${prefix}/${project}`))
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".hdl"))
|
||||
.map((file) => file.name.replace(".hdl", ""));
|
||||
|
||||
const payload = sortChips(project, chips);
|
||||
|
||||
dispatch.current({ action: "setChips", payload });
|
||||
|
||||
if (chips.length > 0) {
|
||||
this.loadChip(`${prefix}/${project}/${chips[0]}.hdl`, true);
|
||||
}
|
||||
},
|
||||
|
||||
async loadChip(path: string, loadTests = true) {
|
||||
dispatch.current({ action: "updateUsingBuiltin", payload: false });
|
||||
|
||||
const hdl = await fs.readFile(path);
|
||||
|
||||
const parts = path.split("/");
|
||||
const name = assertExists(parts.pop()).replace(".hdl", "");
|
||||
const dir = parts.join("/");
|
||||
|
||||
await this.compileChip(hdl, dir, name);
|
||||
|
||||
if (loadTests) {
|
||||
await this.initializeTests(dir, name);
|
||||
}
|
||||
|
||||
dispatch.current({
|
||||
action: "setChip",
|
||||
payload: { chipName: name, dir: dir },
|
||||
});
|
||||
dispatch.current({ action: "setFiles", payload: { hdl } });
|
||||
|
||||
if (usingBuiltin) {
|
||||
this.loadBuiltin();
|
||||
dispatch.current({ action: "displayBuiltin" });
|
||||
}
|
||||
},
|
||||
|
||||
async compileChip(hdl: string, dir?: string, name?: string) {
|
||||
chip.remove();
|
||||
const maybeChip = await parseChip(hdl, dir, name, fs);
|
||||
if (isErr(maybeChip)) {
|
||||
const error = Err(maybeChip);
|
||||
setStatus({
|
||||
message: Err(maybeChip).message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
invalid = true;
|
||||
dispatch.current({
|
||||
action: "updateChip",
|
||||
payload: { invalid: true, error },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.replaceChip(Ok(maybeChip));
|
||||
},
|
||||
|
||||
replaceChip(nextChip: SimChip) {
|
||||
// Store current inPins
|
||||
const inPins = chip.ins;
|
||||
for (const [pin, { busVoltage }] of inPins) {
|
||||
const nextPin = nextChip.ins.get(pin);
|
||||
if (nextPin) {
|
||||
nextPin.busVoltage = busVoltage;
|
||||
}
|
||||
}
|
||||
clock.reset();
|
||||
nextChip.eval();
|
||||
chip = nextChip;
|
||||
chip.reset();
|
||||
dispatch.current({ action: "updateChip", payload: { invalid: false } });
|
||||
dispatch.current({ action: "updateTestStep" });
|
||||
},
|
||||
|
||||
async initializeTests(dir: string, chip: string) {
|
||||
tests = TEST_NAMES[chip] ?? [];
|
||||
this.loadTest(tests[0] ?? chip, dir);
|
||||
},
|
||||
|
||||
async loadTest(name: string, dir?: string) {
|
||||
if (!fs) return;
|
||||
try {
|
||||
dir ??= _dir;
|
||||
|
||||
const tst = await fs.readFile(`${dir}/${name}.tst`);
|
||||
|
||||
dispatch.current({ action: "setFiles", payload: { tst, cmp: "" } });
|
||||
dispatch.current({ action: "setTest", payload: name });
|
||||
this.compileTest(tst, dir);
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: `Could not find ${name}.tst. Please load test file separately.`,
|
||||
severity: "WARNING",
|
||||
});
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
compileTest(file: string, path: string) {
|
||||
if (!fs) return;
|
||||
dispatch.current({ action: "setFiles", payload: { tst: file } });
|
||||
const tst = TST.parse(file);
|
||||
if (isErr(tst)) {
|
||||
setStatus({
|
||||
message: `Failed to parse test ${Err(tst).message}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
invalid = true;
|
||||
return false;
|
||||
}
|
||||
const maybeTest = ChipTest.from(Ok(tst), {
|
||||
dir: path,
|
||||
setStatus: setStatus,
|
||||
loadAction: async (file) => {
|
||||
await this.loadChip(file, false);
|
||||
return chip;
|
||||
},
|
||||
compareTo: async (file) => {
|
||||
const cmp = await fs.readFile(`${_dir}/${file}`);
|
||||
dispatch.current({ action: "setFiles", payload: { cmp } });
|
||||
},
|
||||
});
|
||||
if (isErr(maybeTest)) {
|
||||
invalid = true;
|
||||
setStatus({
|
||||
message: Err(maybeTest).message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
test = Ok(maybeTest).with(chip).reset();
|
||||
test.setFileSystem(fs);
|
||||
dispatch.current({ action: "updateTestStep" });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
async updateFiles({
|
||||
hdl,
|
||||
tst,
|
||||
cmp,
|
||||
tstPath,
|
||||
}: {
|
||||
hdl?: string;
|
||||
tst?: string;
|
||||
cmp: string;
|
||||
tstPath?: string;
|
||||
}) {
|
||||
invalid = false;
|
||||
dispatch.current({ action: "setFiles", payload: { hdl, tst, cmp } });
|
||||
try {
|
||||
if (hdl) {
|
||||
await this.compileChip(hdl, _dir, _chipName);
|
||||
}
|
||||
if (tst) {
|
||||
this.compileTest(tst, tstPath ?? _dir);
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: display(e),
|
||||
severity: "ERROR",
|
||||
});
|
||||
}
|
||||
dispatch.current({ action: "updateChip", payload: { invalid: invalid } });
|
||||
if (!invalid) {
|
||||
setStatus(`HDL code: No syntax errors`);
|
||||
}
|
||||
},
|
||||
|
||||
async saveChip(hdl: string) {
|
||||
dispatch.current({ action: "setFiles", payload: { hdl } });
|
||||
const path = `${_dir}/${_chipName}.hdl`;
|
||||
if (fs && path) {
|
||||
await fs.writeFile(path, hdl);
|
||||
}
|
||||
},
|
||||
|
||||
toggle(pin: Pin, i: number | undefined) {
|
||||
if (i !== undefined) {
|
||||
pin.busVoltage = pin.busVoltage ^ (1 << i);
|
||||
} else {
|
||||
if (pin.width === 1) {
|
||||
pin.toggle();
|
||||
} else {
|
||||
pin.busVoltage += 1;
|
||||
}
|
||||
}
|
||||
dispatch.current({ action: "updateChip", payload: { pending: true } });
|
||||
},
|
||||
|
||||
eval() {
|
||||
chip.eval();
|
||||
dispatch.current({ action: "updateChip", payload: { pending: false } });
|
||||
},
|
||||
|
||||
clock() {
|
||||
clock.toggle();
|
||||
if (clock.isLow) {
|
||||
clock.frame();
|
||||
}
|
||||
dispatch.current({ action: "updateChip" });
|
||||
},
|
||||
|
||||
async loadBuiltin() {
|
||||
const builtinName = _chipName;
|
||||
const nextChip = await getBuiltinChip(builtinName);
|
||||
if (isErr(nextChip)) {
|
||||
setStatus({
|
||||
message: `Failed to load builtin ${builtinName}: ${display(Err(nextChip))}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.replaceChip(Ok(nextChip));
|
||||
},
|
||||
|
||||
async toggleBuiltin() {
|
||||
usingBuiltin = !usingBuiltin;
|
||||
dispatch.current({ action: "toggleBuiltin" });
|
||||
if (usingBuiltin) {
|
||||
await this.loadBuiltin();
|
||||
} else {
|
||||
await this.compileChip(backupHdl, _dir, _chipName);
|
||||
}
|
||||
},
|
||||
|
||||
tick(): Promise<boolean> {
|
||||
return this.stepTest();
|
||||
},
|
||||
|
||||
async stepTest(): Promise<boolean> {
|
||||
try {
|
||||
const done = await test.step();
|
||||
dispatch.current({ action: "updateTestStep" });
|
||||
if (done) {
|
||||
dispatch.current({ action: "testFinished" });
|
||||
}
|
||||
return done;
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: (e as Error).message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
async getProjectFiles() {
|
||||
console.log(_dir);
|
||||
|
||||
return (await fs.scandir(_dir))
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".hdl"))
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
content: fs.readFile(`${_dir}/${entry.name}`),
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
const initialState: ChipPageState = (() => {
|
||||
const controls: ControlsState = {
|
||||
projects: ["1", "2", "3", "5"],
|
||||
project: "1",
|
||||
chips: [],
|
||||
chipName: "",
|
||||
tests,
|
||||
testName: "",
|
||||
usingBuiltin: false,
|
||||
runningTest: false,
|
||||
error: undefined,
|
||||
visualizationParameters: new Set(),
|
||||
};
|
||||
|
||||
const sim = reduceChip(new Low());
|
||||
|
||||
return {
|
||||
controls,
|
||||
files: {
|
||||
hdl: "",
|
||||
cmp: "",
|
||||
tst: "",
|
||||
out: "",
|
||||
backupHdl: "",
|
||||
},
|
||||
sim,
|
||||
config: { speed: 2 },
|
||||
};
|
||||
})();
|
||||
|
||||
return { initialState, reducers, actions };
|
||||
}
|
||||
|
||||
export function useChipPageStore() {
|
||||
const { fs, setStatus, storage, localFsRoot } = useContext(BaseContext);
|
||||
|
||||
const dispatch = useRef<ChipStoreDispatch>(() => undefined);
|
||||
|
||||
const { initialState, reducers, actions } = useMemo(
|
||||
() =>
|
||||
makeChipStore(fs, setStatus, storage, dispatch, localFsRoot != undefined),
|
||||
[fs, setStatus, storage, dispatch],
|
||||
);
|
||||
|
||||
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||
dispatch.current = dispatcher;
|
||||
|
||||
return { state, dispatch, actions };
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { compile } from "@nand2tetris/simulator/jack/compiler.js";
|
||||
import { CompilationError } from "@nand2tetris/simulator/languages/base.js";
|
||||
import { Action } from "@nand2tetris/simulator/types.js";
|
||||
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||
import { useImmerReducer } from "../react.js";
|
||||
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||
|
||||
export interface CompiledFile {
|
||||
vm?: string;
|
||||
valid: boolean;
|
||||
error?: CompilationError;
|
||||
}
|
||||
|
||||
export interface CompilerPageState {
|
||||
fs?: FileSystem;
|
||||
files: Record<string, string>;
|
||||
compiled: Record<string, CompiledFile>;
|
||||
isValid: boolean;
|
||||
isCompiled: boolean;
|
||||
selected: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export type CompilerStoreDispatch = Dispatch<{
|
||||
action: keyof ReturnType<typeof makeCompilerStore>["reducers"];
|
||||
payload?: unknown;
|
||||
}>;
|
||||
|
||||
function classTemplate(name: string) {
|
||||
return `class ${name} {\n\n}\n`;
|
||||
}
|
||||
|
||||
export function makeCompilerStore(
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||
dispatch: MutableRefObject<CompilerStoreDispatch>,
|
||||
) {
|
||||
let fs: FileSystem | undefined;
|
||||
|
||||
const reducers = {
|
||||
setFs(state: CompilerPageState, fs: FileSystem) {
|
||||
state.fs = fs;
|
||||
},
|
||||
reset(state: CompilerPageState) {
|
||||
state.files = {};
|
||||
state.title = undefined;
|
||||
},
|
||||
|
||||
setFile(
|
||||
state: CompilerPageState,
|
||||
{ name, content }: { name: string; content: string },
|
||||
) {
|
||||
state.files[name] = content;
|
||||
state.isCompiled = false;
|
||||
this.compile(state);
|
||||
},
|
||||
|
||||
// the keys of 'files' have to be the full file path, not basename
|
||||
setFiles(state: CompilerPageState, files: Record<string, string>) {
|
||||
state.files = files;
|
||||
state.isCompiled = false;
|
||||
this.compile(state);
|
||||
},
|
||||
|
||||
compile(state: CompilerPageState) {
|
||||
const compiledFiles = compile(state.files);
|
||||
state.compiled = {};
|
||||
for (const [name, compiled] of Object.entries(compiledFiles)) {
|
||||
if (typeof compiled === "string") {
|
||||
state.compiled[name] = {
|
||||
valid: true,
|
||||
vm: compiled,
|
||||
};
|
||||
} else {
|
||||
state.compiled[name] = {
|
||||
valid: false,
|
||||
error: compiled,
|
||||
};
|
||||
}
|
||||
}
|
||||
state.isValid = Object.keys(state.files).every(
|
||||
(file) => state.compiled[file].valid,
|
||||
);
|
||||
},
|
||||
|
||||
writeCompiled(state: CompilerPageState) {
|
||||
if (Object.values(state.compiled).every((compiled) => compiled.valid)) {
|
||||
for (const [name, compiled] of Object.entries(state.compiled)) {
|
||||
if (compiled.vm) {
|
||||
fs?.writeFile(`${name}.vm`, compiled.vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.isCompiled = true;
|
||||
},
|
||||
|
||||
setSelected(state: CompilerPageState, selected: string) {
|
||||
state.selected = selected;
|
||||
},
|
||||
|
||||
setTitle(state: CompilerPageState, title: string) {
|
||||
state.title = title;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
async loadProject(_fs: FileSystem, title: string) {
|
||||
this.reset();
|
||||
fs = _fs;
|
||||
dispatch.current({ action: "setFs", payload: fs });
|
||||
dispatch.current({ action: "setTitle", payload: title });
|
||||
|
||||
const files: Record<string, string> = {};
|
||||
for (const file of (await fs.scandir("/")).filter(
|
||||
(entry) => entry.isFile() && entry.name.endsWith(".jack"),
|
||||
)) {
|
||||
files[file.name.replace(".jack", "")] = await fs.readFile(file.name);
|
||||
}
|
||||
this.loadFiles(files);
|
||||
},
|
||||
|
||||
async loadFiles(files: Record<string, string>) {
|
||||
dispatch.current({ action: "setFiles", payload: files });
|
||||
if (Object.entries(files).length > 0) {
|
||||
dispatch.current({
|
||||
action: "setSelected",
|
||||
payload: Object.keys(files)[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async writeFile(name: string, content?: string) {
|
||||
content ??= classTemplate(name);
|
||||
dispatch.current({ action: "setFile", payload: { name, content } });
|
||||
if (fs) {
|
||||
await fs.writeFile(`${name}.jack`, content);
|
||||
}
|
||||
},
|
||||
|
||||
async reset() {
|
||||
fs = undefined;
|
||||
dispatch.current({ action: "reset" });
|
||||
},
|
||||
|
||||
async compile() {
|
||||
dispatch.current({ action: "writeCompiled" });
|
||||
},
|
||||
};
|
||||
|
||||
const initialState: CompilerPageState = {
|
||||
files: {},
|
||||
compiled: {},
|
||||
selected: "",
|
||||
isCompiled: false,
|
||||
isValid: true,
|
||||
};
|
||||
|
||||
return { initialState, reducers, actions };
|
||||
}
|
||||
|
||||
export function useCompilerPageStore() {
|
||||
const { setStatus } = useContext(BaseContext);
|
||||
|
||||
const dispatch = useRef<CompilerStoreDispatch>(() => undefined);
|
||||
|
||||
const { initialState, reducers, actions } = useMemo(
|
||||
() => makeCompilerStore(setStatus, dispatch),
|
||||
[setStatus, dispatch],
|
||||
);
|
||||
|
||||
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||
dispatch.current = dispatcher;
|
||||
|
||||
return { state, dispatch, actions };
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
unwrap,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import {
|
||||
Format,
|
||||
KeyboardAdapter,
|
||||
MemoryAdapter,
|
||||
MemoryKeyboard,
|
||||
ROM,
|
||||
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import { Span } from "@nand2tetris/simulator/languages/base.js";
|
||||
import { TST } from "@nand2tetris/simulator/languages/tst.js";
|
||||
import { loadAsm, loadBlob, loadHack } from "@nand2tetris/simulator/loader.js";
|
||||
import { CPUTest } from "@nand2tetris/simulator/test/cputst.js";
|
||||
import { Action } from "@nand2tetris/simulator/types.js";
|
||||
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||
import { ScreenScales } from "src/chips/screen.js";
|
||||
import { RunSpeed } from "src/runbar.js";
|
||||
import { compare } from "../compare.js";
|
||||
import { loadTestFiles } from "../file_utils.js";
|
||||
import { useImmerReducer } from "../react.js";
|
||||
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||
import { ImmMemory } from "./imm_memory.js";
|
||||
|
||||
function makeTst() {
|
||||
return `repeat {
|
||||
ticktock;
|
||||
}`;
|
||||
}
|
||||
|
||||
export interface CpuSim {
|
||||
A: number;
|
||||
D: number;
|
||||
PC: number;
|
||||
RAM: MemoryAdapter;
|
||||
ROM: MemoryAdapter;
|
||||
Screen: MemoryAdapter;
|
||||
Keyboard: KeyboardAdapter;
|
||||
}
|
||||
|
||||
export interface CPUTestSim {
|
||||
name: string;
|
||||
tst: string;
|
||||
cmp: string;
|
||||
out: string;
|
||||
highlight: Span | undefined;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export interface CPUPageConfig {
|
||||
romFormat: Format;
|
||||
ramFormat: Format;
|
||||
screenScale: ScreenScales;
|
||||
speed: RunSpeed;
|
||||
testSpeed: RunSpeed;
|
||||
}
|
||||
|
||||
export interface CpuPageState {
|
||||
sim: CpuSim;
|
||||
test: CPUTestSim;
|
||||
path: string;
|
||||
tests: string[];
|
||||
title?: string;
|
||||
config: CPUPageConfig;
|
||||
}
|
||||
|
||||
function reduceCPUTest(
|
||||
cpuTest: CPUTest,
|
||||
dispatch: MutableRefObject<CpuStoreDispatch>,
|
||||
): CpuSim {
|
||||
const RAM = new ImmMemory(cpuTest.cpu.RAM, dispatch);
|
||||
const ROM = new ImmMemory(cpuTest.cpu.ROM, dispatch);
|
||||
const Screen = new ImmMemory(cpuTest.cpu.Screen, dispatch);
|
||||
const Keyboard = new MemoryKeyboard(new ImmMemory(cpuTest.cpu.RAM, dispatch));
|
||||
|
||||
return {
|
||||
A: cpuTest.cpu.A,
|
||||
D: cpuTest.cpu.D,
|
||||
PC: cpuTest.cpu.PC,
|
||||
RAM,
|
||||
ROM,
|
||||
Screen,
|
||||
Keyboard,
|
||||
};
|
||||
}
|
||||
|
||||
export type CpuStoreDispatch = Dispatch<{
|
||||
action: keyof ReturnType<typeof makeCpuStore>["reducers"];
|
||||
payload?: unknown;
|
||||
}>;
|
||||
|
||||
export function makeCpuStore(
|
||||
fs: FileSystem,
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||
storage: Record<string, string>,
|
||||
dispatch: MutableRefObject<CpuStoreDispatch>,
|
||||
) {
|
||||
let test = new CPUTest();
|
||||
let animate = true;
|
||||
let valid = true;
|
||||
let path = "";
|
||||
let tests: string[] = [];
|
||||
let tstName = "";
|
||||
let _title: string | undefined;
|
||||
|
||||
const reducers = {
|
||||
update(state: CpuPageState) {
|
||||
state.sim = reduceCPUTest(test, dispatch);
|
||||
state.test.highlight = test.currentStep?.span;
|
||||
state.test.valid = valid;
|
||||
state.path = path;
|
||||
state.tests = Array.from(tests);
|
||||
state.test.name = tstName;
|
||||
},
|
||||
|
||||
setTest(state: CpuPageState, { tst, cmp }: { tst?: string; cmp?: string }) {
|
||||
state.test.tst = tst ?? state.test.tst;
|
||||
state.test.cmp = cmp ?? state.test.cmp;
|
||||
state.test.out = "";
|
||||
},
|
||||
|
||||
testStep(state: CpuPageState) {
|
||||
state.test.out = test.log();
|
||||
this.update(state);
|
||||
},
|
||||
|
||||
testFinished(state: CpuPageState) {
|
||||
if (state.test.cmp.trim() === "") {
|
||||
return;
|
||||
}
|
||||
const passed = compare(state.test.cmp.trim(), test.log().trim());
|
||||
setStatus(
|
||||
passed
|
||||
? {
|
||||
message: `Simulation successful: The output file is identical to the compare file`,
|
||||
severity: "SUCCESS",
|
||||
}
|
||||
: {
|
||||
message: `Simulation error: The output file differs from the compare file`,
|
||||
severity: "ERROR",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
setTitle(state: CpuPageState, title?: string) {
|
||||
_title = title;
|
||||
state.title = title;
|
||||
if (title) {
|
||||
test.fileLoaded = true;
|
||||
}
|
||||
},
|
||||
|
||||
updateConfig(state: CpuPageState, config: Partial<CPUPageConfig>) {
|
||||
state.config = { ...state.config, ...config };
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
tick() {
|
||||
test.cpu.tick();
|
||||
},
|
||||
|
||||
setAnimate(value: boolean) {
|
||||
animate = value;
|
||||
},
|
||||
|
||||
async setPath(_path: string) {
|
||||
path = _path;
|
||||
|
||||
const dir = path.split("/").slice(0, -1).join("/");
|
||||
const files = await fs.scandir(dir);
|
||||
tests = files
|
||||
.filter((file) => file.name.endsWith(".tst"))
|
||||
.map((file) => file.name);
|
||||
|
||||
if (tests.length > 0) {
|
||||
this.loadTest(tests[0]);
|
||||
} else {
|
||||
tstName = "Default";
|
||||
this.compileTest(makeTst(), "");
|
||||
}
|
||||
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
async testStep() {
|
||||
try {
|
||||
const done = await test.step();
|
||||
if (animate || done) {
|
||||
dispatch.current({ action: "testStep" });
|
||||
}
|
||||
if (done) {
|
||||
dispatch.current({ action: "testFinished" });
|
||||
}
|
||||
return done;
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: (e as Error).message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
resetRAM() {
|
||||
test.cpu.RAM.loadBytes([]);
|
||||
dispatch.current({ action: "update" });
|
||||
setStatus("Reset RAM");
|
||||
},
|
||||
|
||||
toggleUseTest() {
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
reset() {
|
||||
test.reset();
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.replaceROM(new ROM());
|
||||
this.resetRAM();
|
||||
this.clearTest();
|
||||
this.reset();
|
||||
dispatch.current({ action: "setTitle", payload: undefined });
|
||||
},
|
||||
|
||||
clearTest() {
|
||||
tstName = "";
|
||||
this.compileTest(makeTst(), "");
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
|
||||
replaceROM(rom: ROM) {
|
||||
test = new CPUTest({ dir: path, rom });
|
||||
this.clearTest();
|
||||
},
|
||||
|
||||
compileTest(file: string, cmp?: string, _path?: string) {
|
||||
const tstPath = _path ?? path;
|
||||
dispatch.current({ action: "setTest", payload: { tst: file, cmp } });
|
||||
const tst = TST.parse(file);
|
||||
|
||||
if (isErr(tst)) {
|
||||
setStatus({
|
||||
message: `Failed to parse test - ${Err(tst).message}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
valid = false;
|
||||
dispatch.current({ action: "update" });
|
||||
return false;
|
||||
}
|
||||
valid = true;
|
||||
|
||||
const maybeTest = CPUTest.from(Ok(tst), {
|
||||
dir: tstPath,
|
||||
rom: test.cpu.ROM,
|
||||
doEcho: setStatus,
|
||||
doLoad: async (path) => {
|
||||
let file;
|
||||
try {
|
||||
file = await fs.readFile(path);
|
||||
} catch (_e) {
|
||||
throw new Error(`Cannot find ${path}`);
|
||||
}
|
||||
const loader = path.endsWith("hack")
|
||||
? loadHack
|
||||
: path.endsWith("asm")
|
||||
? loadAsm
|
||||
: loadBlob;
|
||||
const bytes = await loader(file);
|
||||
console.log(bytes);
|
||||
test.cpu.ROM.loadBytes(bytes);
|
||||
},
|
||||
compareTo: async (file) => {
|
||||
const dir = tstPath.split("/").slice(0, -1).join("/");
|
||||
const cmp = await fs.readFile(`${dir}/${file}`);
|
||||
dispatch.current({ action: "setTest", payload: { cmp } });
|
||||
},
|
||||
requireLoad: false,
|
||||
});
|
||||
|
||||
if (isErr(maybeTest)) {
|
||||
setStatus({
|
||||
message: Err(maybeTest).message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
test = Ok(maybeTest);
|
||||
test.fileLoaded = _title != undefined;
|
||||
dispatch.current({ action: "update" });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
async loadTest(name: string) {
|
||||
const dir = path.split("/").slice(0, -1).join("/");
|
||||
const files = await loadTestFiles(fs, `${dir}/${name}`);
|
||||
if (isErr(files)) {
|
||||
setStatus({
|
||||
message: `Failed to load test`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return;
|
||||
}
|
||||
tstName = name;
|
||||
const { tst } = unwrap(files);
|
||||
this.compileTest(tst, "");
|
||||
},
|
||||
};
|
||||
|
||||
const initialState: CpuPageState = {
|
||||
sim: reduceCPUTest(test, dispatch),
|
||||
test: {
|
||||
highlight: test.currentStep?.span,
|
||||
name: "",
|
||||
tst: makeTst(),
|
||||
cmp: "",
|
||||
out: "",
|
||||
valid: true,
|
||||
},
|
||||
path: "",
|
||||
tests: [],
|
||||
config: {
|
||||
romFormat: "asm",
|
||||
ramFormat: "dec",
|
||||
screenScale: 1,
|
||||
speed: 2,
|
||||
testSpeed: 2,
|
||||
},
|
||||
};
|
||||
|
||||
return { initialState, reducers, actions };
|
||||
}
|
||||
|
||||
export function useCpuPageStore() {
|
||||
const { fs, setStatus, storage } = useContext(BaseContext);
|
||||
|
||||
const dispatch = useRef<CpuStoreDispatch>(() => undefined);
|
||||
|
||||
const { initialState, reducers, actions } = useMemo(
|
||||
() => makeCpuStore(fs, setStatus, storage, dispatch),
|
||||
[fs, setStatus, storage, dispatch],
|
||||
);
|
||||
|
||||
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||
dispatch.current = dispatcher;
|
||||
|
||||
return { state, dispatch, actions };
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import { MemoryAdapter, SubMemory } from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import { MutableRefObject } from "react";
|
||||
|
||||
export class ImmMemory<
|
||||
Action extends { action: "update" },
|
||||
Dispatch extends (a: Action) => void,
|
||||
> extends SubMemory {
|
||||
constructor(
|
||||
parent: MemoryAdapter,
|
||||
private dispatch: MutableRefObject<Dispatch>,
|
||||
) {
|
||||
super(parent, parent.size, 0);
|
||||
}
|
||||
|
||||
override async load(fs: FileSystem, path: string): Promise<void> {
|
||||
await super.load(fs, path);
|
||||
this.dispatch.current({ action: "update" } as Action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js";
|
||||
import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js";
|
||||
import {
|
||||
Err,
|
||||
isErr,
|
||||
Ok,
|
||||
Result,
|
||||
unwrap,
|
||||
} from "@davidsouther/jiffies/lib/esm/result.js";
|
||||
import { FIBONACCI } from "@nand2tetris/projects/base.js";
|
||||
import {
|
||||
Format,
|
||||
KeyboardAdapter,
|
||||
MemoryAdapter,
|
||||
MemoryKeyboard,
|
||||
} from "@nand2tetris/simulator/cpu/memory.js";
|
||||
import {
|
||||
CompilationError,
|
||||
Span,
|
||||
} from "@nand2tetris/simulator/languages/base.js";
|
||||
import { TST } from "@nand2tetris/simulator/languages/tst.js";
|
||||
import { VM, VmInstruction } from "@nand2tetris/simulator/languages/vm.js";
|
||||
import { VMTest, VmFile } from "@nand2tetris/simulator/test/vmtst.js";
|
||||
import { Action } from "@nand2tetris/simulator/types.js";
|
||||
import { Vm, VmFrame } from "@nand2tetris/simulator/vm/vm.js";
|
||||
import { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react";
|
||||
import { ScreenScales } from "../chips/screen.js";
|
||||
import { compare } from "../compare.js";
|
||||
import { useImmerReducer } from "../react.js";
|
||||
import { RunSpeed } from "../runbar.js";
|
||||
import { BaseContext, StatusSeverity } from "./base.context.js";
|
||||
import { ImmMemory } from "./imm_memory.js";
|
||||
|
||||
export const DEFAULT_TEST = "repeat {\n\tvmstep;\n}";
|
||||
|
||||
export interface VmSim {
|
||||
RAM: MemoryAdapter;
|
||||
Screen: MemoryAdapter;
|
||||
Keyboard: KeyboardAdapter;
|
||||
Stack: VmFrame[];
|
||||
Prog: VmInstruction[];
|
||||
Statics: number[];
|
||||
Temp: number[];
|
||||
AddedSysInit: boolean;
|
||||
highlight: number;
|
||||
showHighlight: boolean;
|
||||
}
|
||||
|
||||
export interface VMTestSim {
|
||||
highlight: Span | undefined;
|
||||
}
|
||||
|
||||
export interface VmPageState {
|
||||
vm: VmSim;
|
||||
controls: ControlsState;
|
||||
test: VMTestSim;
|
||||
files: VMFiles;
|
||||
title?: string;
|
||||
config: VmPageConfig;
|
||||
}
|
||||
|
||||
export interface VmPageConfig {
|
||||
ram1Format: Format;
|
||||
ram2Format: Format;
|
||||
screenScale: ScreenScales;
|
||||
speed: RunSpeed;
|
||||
testSpeed: RunSpeed;
|
||||
}
|
||||
|
||||
export interface ControlsState {
|
||||
runningTest: boolean;
|
||||
exitCode: number | undefined;
|
||||
animate: boolean;
|
||||
valid: boolean;
|
||||
error?: CompilationError;
|
||||
}
|
||||
|
||||
export interface VMFiles {
|
||||
vm: string;
|
||||
tst: string;
|
||||
cmp: string;
|
||||
out: string;
|
||||
}
|
||||
|
||||
export type VmStoreDispatch = Dispatch<{
|
||||
action: keyof ReturnType<typeof makeVmStore>["reducers"];
|
||||
payload?: unknown;
|
||||
}>;
|
||||
|
||||
function reduceVMTest(
|
||||
vmTest: VMTest,
|
||||
dispatch: MutableRefObject<VmStoreDispatch>,
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||
showHighlight: boolean,
|
||||
): VmSim {
|
||||
const RAM = new ImmMemory(vmTest.vm.RAM, dispatch);
|
||||
const Screen = new ImmMemory(vmTest.vm.Screen, dispatch);
|
||||
const Keyboard = new MemoryKeyboard(new ImmMemory(vmTest.vm.RAM, dispatch));
|
||||
const highlight = vmTest.vm.derivedLine();
|
||||
|
||||
let stack: VmFrame[] = [];
|
||||
try {
|
||||
stack = vmTest.vm.vmStack().reverse();
|
||||
} catch (_e) {
|
||||
dispatch.current({
|
||||
action: "setError",
|
||||
payload: new Error("Runtime error: Invalid stack"),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
Keyboard,
|
||||
RAM,
|
||||
Screen,
|
||||
Stack: stack,
|
||||
Prog: vmTest.vm.program,
|
||||
Statics: [
|
||||
...vmTest.vm.memory.map((_, v) => v, 16, 16 + vmTest.vm.getStaticCount()),
|
||||
],
|
||||
Temp: [...vmTest.vm.memory.map((_, v) => v, 5, 13)],
|
||||
AddedSysInit: vmTest.vm.addedSysInit,
|
||||
highlight,
|
||||
showHighlight,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeVmStore(
|
||||
fs: FileSystem,
|
||||
setStatus: Action<string | { message: string; severity?: StatusSeverity }>,
|
||||
storage: Record<string, string>,
|
||||
dispatch: MutableRefObject<VmStoreDispatch>,
|
||||
) {
|
||||
const parsed = unwrap(VM.parse(FIBONACCI));
|
||||
let vm = unwrap(Vm.build(parsed.instructions));
|
||||
let test = new VMTest({ doEcho: setStatus }).with(vm);
|
||||
let useTest = false;
|
||||
let animate = true;
|
||||
let vmSource = "";
|
||||
let showHighlight = true;
|
||||
const reducers = {
|
||||
setVm(state: VmPageState, vm: string) {
|
||||
state.files.vm = vm;
|
||||
},
|
||||
setTst(state: VmPageState, { tst }: { tst: string }) {
|
||||
state.files.tst = tst;
|
||||
},
|
||||
setCmp(state: VmPageState, { cmp }: { cmp: string }) {
|
||||
state.files.cmp = cmp;
|
||||
},
|
||||
setExitCode(state: VmPageState, code: number | undefined) {
|
||||
state.controls.exitCode = code;
|
||||
},
|
||||
setValid(state: VmPageState, valid: boolean) {
|
||||
state.controls.valid = valid;
|
||||
},
|
||||
setShowHighlight(state: VmPageState, value: boolean) {
|
||||
state.vm.showHighlight = value;
|
||||
},
|
||||
setError(state: VmPageState, error?: CompilationError) {
|
||||
if (error) {
|
||||
state.controls.valid = false;
|
||||
setStatus({
|
||||
message: error?.message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
} else {
|
||||
state.controls.valid = true;
|
||||
}
|
||||
state.controls.error = error;
|
||||
},
|
||||
update(state: VmPageState) {
|
||||
state.vm = reduceVMTest(test, dispatch, setStatus, showHighlight);
|
||||
state.test.highlight = test.currentStep?.span;
|
||||
},
|
||||
setAnimate(state: VmPageState, value: boolean) {
|
||||
state.controls.animate = value;
|
||||
},
|
||||
testStep(state: VmPageState) {
|
||||
state.files.out = test.log();
|
||||
},
|
||||
testFinished(state: VmPageState) {
|
||||
const passed = compare(state.files.cmp.trim(), state.files.out);
|
||||
setStatus(
|
||||
passed
|
||||
? {
|
||||
message: `Simulation successful: The output file is identical to the compare file`,
|
||||
severity: "SUCCESS",
|
||||
}
|
||||
: {
|
||||
message: `Simulation error: The output file differs from the compare file`,
|
||||
severity: "ERROR",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
setTitle(state: VmPageState, title: string) {
|
||||
state.title = title;
|
||||
},
|
||||
|
||||
updateConfig(state: VmPageState, config: Partial<VmPageConfig>) {
|
||||
state.config = { ...state.config, ...config };
|
||||
},
|
||||
};
|
||||
const initialState: VmPageState = {
|
||||
vm: reduceVMTest(test, dispatch, setStatus, true),
|
||||
controls: {
|
||||
exitCode: undefined,
|
||||
runningTest: false,
|
||||
animate: true,
|
||||
valid: true,
|
||||
},
|
||||
test: {
|
||||
highlight: undefined,
|
||||
},
|
||||
files: {
|
||||
vm: "",
|
||||
tst: DEFAULT_TEST,
|
||||
cmp: "",
|
||||
out: "",
|
||||
},
|
||||
config: {
|
||||
ram1Format: "dec",
|
||||
ram2Format: "dec",
|
||||
screenScale: 1,
|
||||
speed: 2,
|
||||
testSpeed: 2,
|
||||
},
|
||||
};
|
||||
const actions = {
|
||||
async load(path: string) {
|
||||
const files: VmFile[] = [];
|
||||
let title: string;
|
||||
|
||||
if ((await fs.stat(path)).isFile()) {
|
||||
// single file
|
||||
files.push({
|
||||
name: assertExists(path.split("/").pop()).replace(".vm", ""),
|
||||
content: await fs.readFile(path),
|
||||
});
|
||||
title = path.split("/").pop() ?? "";
|
||||
} else {
|
||||
// folder
|
||||
for (const file of (await fs.scandir(path)).filter(
|
||||
(entry) => entry.isFile() && entry.name.endsWith(".vm"),
|
||||
)) {
|
||||
files.push({
|
||||
name: file.name.replace(".vm", ""),
|
||||
content: await fs.readFile(`${path}/${file.name}`),
|
||||
});
|
||||
}
|
||||
title = `${path.split("/").pop()} / *.vm`;
|
||||
}
|
||||
dispatch.current({ action: "setTitle", payload: title });
|
||||
this.loadVm(files);
|
||||
this.reset();
|
||||
setStatus("");
|
||||
},
|
||||
setVm(content: string) {
|
||||
showHighlight = false;
|
||||
dispatch.current({
|
||||
action: "setVm",
|
||||
payload: content,
|
||||
});
|
||||
if (vmSource == content) {
|
||||
return;
|
||||
}
|
||||
vmSource = content;
|
||||
|
||||
const parseResult = VM.parse(content);
|
||||
|
||||
if (isErr(parseResult)) {
|
||||
dispatch.current({ action: "setError", payload: Err(parseResult) });
|
||||
return false;
|
||||
}
|
||||
const instructions = unwrap(parseResult).instructions;
|
||||
const buildResult = Vm.build(instructions);
|
||||
return this.replaceVm(buildResult);
|
||||
},
|
||||
loadVm(files: VmFile[]) {
|
||||
showHighlight = false;
|
||||
|
||||
const content = files.map((f) => f.content).join("\n");
|
||||
dispatch.current({
|
||||
action: "setVm",
|
||||
payload: content,
|
||||
});
|
||||
|
||||
if (vmSource == content) {
|
||||
return;
|
||||
}
|
||||
vmSource = content;
|
||||
|
||||
const parsed = [];
|
||||
|
||||
let lineOffset = 0;
|
||||
for (const file of files) {
|
||||
const parseResult = VM.parse(file.content);
|
||||
|
||||
if (isErr(parseResult)) {
|
||||
dispatch.current({ action: "setError", payload: Err(parseResult) });
|
||||
return false;
|
||||
}
|
||||
const instructions = unwrap(parseResult).instructions;
|
||||
|
||||
for (const instruction of instructions) {
|
||||
if (instruction.span?.line != undefined) {
|
||||
instruction.span.line += lineOffset;
|
||||
}
|
||||
}
|
||||
lineOffset += file.content.split("\n").length;
|
||||
|
||||
parsed.push({
|
||||
name: file.name,
|
||||
instructions,
|
||||
});
|
||||
}
|
||||
const buildResult = Vm.buildFromFiles(parsed);
|
||||
return this.replaceVm(buildResult);
|
||||
},
|
||||
replaceVm(buildResult: Result<Vm, CompilationError>) {
|
||||
if (isErr(buildResult)) {
|
||||
dispatch.current({ action: "setError", payload: Err(buildResult) });
|
||||
return false;
|
||||
}
|
||||
dispatch.current({ action: "setError" });
|
||||
// setStatus("Compiled VM code successfully");
|
||||
|
||||
vm = unwrap(buildResult);
|
||||
test.vm = vm;
|
||||
dispatch.current({ action: "update" });
|
||||
return true;
|
||||
},
|
||||
|
||||
loadTest(path: string, source: string) {
|
||||
dispatch.current({ action: "setTst", payload: { tst: source } });
|
||||
const tst = TST.parse(source);
|
||||
|
||||
if (isErr(tst)) {
|
||||
dispatch.current({ action: "setValid", payload: false });
|
||||
setStatus({
|
||||
message: `Failed to parse test`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
dispatch.current({ action: "setValid", payload: true });
|
||||
setStatus(`Parsed tst`);
|
||||
|
||||
vm.reset();
|
||||
|
||||
const maybeTest = VMTest.from(unwrap(tst), {
|
||||
dir: path,
|
||||
doLoad: async (path) => {
|
||||
await this.load(path);
|
||||
},
|
||||
doEcho: setStatus,
|
||||
compareTo: async (file) => {
|
||||
const dir = path.split("/").slice(0, -1).join("/");
|
||||
const cmp = await fs.readFile(`${dir}/${file}`);
|
||||
dispatch.current({ action: "setCmp", payload: { cmp } });
|
||||
},
|
||||
});
|
||||
if (isErr(maybeTest)) {
|
||||
setStatus({
|
||||
message: Err(maybeTest).message,
|
||||
severity: "ERROR",
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
test = Ok(maybeTest).using(fs);
|
||||
test.vm = vm;
|
||||
dispatch.current({ action: "update" });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
setAnimate(value: boolean) {
|
||||
animate = value;
|
||||
dispatch.current({ action: "setAnimate", payload: value });
|
||||
},
|
||||
async testStep() {
|
||||
showHighlight = true;
|
||||
let done = false;
|
||||
try {
|
||||
done = await test.step();
|
||||
dispatch.current({ action: "testStep" });
|
||||
if (done) {
|
||||
dispatch.current({ action: "testFinished" });
|
||||
}
|
||||
if (animate) {
|
||||
dispatch.current({ action: "update" });
|
||||
}
|
||||
return done;
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: `Runtime error: ${(e as Error).message}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
dispatch.current({ action: "setError", payload: e });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
setPaused(paused = true) {
|
||||
vm.setPaused(paused);
|
||||
},
|
||||
step() {
|
||||
showHighlight = true;
|
||||
try {
|
||||
let done = false;
|
||||
|
||||
const exitCode = vm.step();
|
||||
if (exitCode !== undefined) {
|
||||
done = true;
|
||||
dispatch.current({ action: "setExitCode", payload: exitCode });
|
||||
}
|
||||
|
||||
if (animate) {
|
||||
dispatch.current({ action: "update" });
|
||||
}
|
||||
|
||||
return done;
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
message: `Runtime error: ${(e as Error).message}`,
|
||||
severity: "ERROR",
|
||||
});
|
||||
dispatch.current({ action: "setError", payload: e });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
showHighlight = true;
|
||||
test.reset();
|
||||
vm.reset();
|
||||
dispatch.current({ action: "update" });
|
||||
dispatch.current({ action: "setExitCode", payload: undefined });
|
||||
dispatch.current({ action: "setValid", payload: true });
|
||||
},
|
||||
toggleUseTest() {
|
||||
useTest = !useTest;
|
||||
dispatch.current({ action: "update" });
|
||||
},
|
||||
initialize() {
|
||||
dispatch.current({ action: "setTitle", payload: undefined });
|
||||
this.setVm(FIBONACCI);
|
||||
},
|
||||
};
|
||||
|
||||
return { initialState, reducers, actions };
|
||||
}
|
||||
|
||||
export function useVmPageStore() {
|
||||
const { fs, setStatus, storage } = useContext(BaseContext);
|
||||
|
||||
const dispatch = useRef<VmStoreDispatch>(() => undefined);
|
||||
|
||||
const { initialState, reducers, actions } = useMemo(
|
||||
() => makeVmStore(fs, setStatus, storage, dispatch),
|
||||
[fs, setStatus, storage, dispatch],
|
||||
);
|
||||
|
||||
const [state, dispatcher] = useImmerReducer(reducers, initialState);
|
||||
dispatch.current = dispatcher;
|
||||
|
||||
return { state, dispatch, actions };
|
||||
}
|
||||
Reference in New Issue
Block a user