diff --git a/components/src/chips/memory.tsx b/components/src/chips/memory.tsx index 4addea251..e9deb0284 100644 --- a/components/src/chips/memory.tsx +++ b/components/src/chips/memory.tsx @@ -192,6 +192,7 @@ export const Memory = forwardRef( fileSelect, showClear = true, onChange = undefined, + onClear = undefined, }: { name?: string; className?: string; @@ -210,6 +211,7 @@ export const Memory = forwardRef( fileSelect?: () => Promise<{ name: string; content: string }>; showClear?: boolean; onChange?: () => void; + onClear?: () => void; }, ref, ) => { @@ -275,6 +277,7 @@ export const Memory = forwardRef( const clear = () => { memory.reset(); onChange?.(); + onClear?.(); rerenderMemoryBlock(); }; diff --git a/components/src/stores/asm.store.ts b/components/src/stores/asm.store.ts index be8c5ccf0..b89d5db29 100644 --- a/components/src/stores/asm.store.ts +++ b/components/src/stores/asm.store.ts @@ -20,12 +20,12 @@ 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 } from "./base.context.js"; -import { Action } from "@nand2tetris/simulator/types.js"; export interface TranslatorSymbol { name: string; @@ -200,6 +200,7 @@ export function makeAsmStore( fs: FileSystem, setStatus: Action, dispatch: MutableRefObject, + upgraded: boolean, ) { const translator = new Translator(); const highlightInfo: HighlightInfo = { @@ -347,14 +348,23 @@ export function makeAsmStore( animate = value; }, - step(): boolean { + async step(): Promise { 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("Translation done."); } @@ -447,13 +457,13 @@ export function makeAsmStore( } export function useAsmPageStore() { - const { setStatus, fs } = useContext(BaseContext); + const { setStatus, fs, localFsRoot } = useContext(BaseContext); const dispatch = useRef(() => undefined); const { initialState, reducers, actions } = useMemo( - () => makeAsmStore(fs, setStatus, dispatch), - [setStatus, dispatch], + () => makeAsmStore(fs, setStatus, dispatch, localFsRoot != undefined), + [setStatus, dispatch, fs], ); const [state, dispatcher] = useImmerReducer(reducers, initialState); diff --git a/components/src/stores/base.context.ts b/components/src/stores/base.context.ts index de44b5411..bf37d634d 100644 --- a/components/src/stores/base.context.ts +++ b/components/src/stores/base.context.ts @@ -2,6 +2,7 @@ import { FileSystem, LocalStorageFileSystemAdapter, } from "@davidsouther/jiffies/lib/esm/fs.js"; +import { Action } from "@nand2tetris/simulator/types.js"; import { createContext, useCallback, @@ -10,7 +11,6 @@ import { useState, } from "react"; import { - ChainedFileSystemAdapter, FileSystemAccessFileSystemAdapter, openNand2TetrisDirectory, } from "./base/fs.js"; @@ -19,14 +19,13 @@ import { createAndStoreLocalAdapterInIndexedDB, removeLocalAdapterFromIndexedDB, } from "./base/indexDb.js"; -import { Action } from "@nand2tetris/simulator/types.js"; export interface BaseContext { fs: FileSystem; + localFsRoot?: string; canUpgradeFs: boolean; - upgradeFs: (force?: boolean) => void; + upgradeFs: (force?: boolean, createFiles?: boolean) => Promise; closeFs: () => void; - upgraded?: string; status: string; setStatus: Action; storage: Record; @@ -35,60 +34,65 @@ export interface BaseContext { export function useBaseContext(): BaseContext { const localAdapter = useMemo(() => new LocalStorageFileSystemAdapter(), []); const [fs, setFs] = useState(new FileSystem(localAdapter)); - const [upgraded, setUpgraded] = useState(); + const [root, setRoot] = useState(); - const replaceFs = useCallback( - (handle: FileSystemDirectoryHandle) => { + 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 ChainedFileSystemAdapter( - new FileSystemAccessFileSystemAdapter(handle), - localAdapter, - ), + new FileSystemAccessFileSystemAdapter(handle), ); - newFs.cd(fs.cwd()); + if (createFiles) { + const loaders = await import("@nand2tetris/projects/loader.js"); + await loaders.createFiles(newFs); + } setFs(newFs); - setUpgraded(handle.name); + setRoot(handle.name); }, - [setFs, setUpgraded], + [setRoot, setFs], ); useEffect(() => { - if (upgraded) return; - attemptLoadAdapterFromIndexedDb().then((adapter) => { - if (!adapter) return; - replaceFs(adapter); - }); - }, [upgraded, replaceFs]); + if (root) return; + + if ("showDirectoryPicker" in window) { + attemptLoadAdapterFromIndexedDb().then((adapter) => { + if (!adapter) return; + setLocalFs(adapter); + }); + } + }, [root, setLocalFs]); const canUpgradeFs = `showDirectoryPicker` in window; + const upgradeFs = useCallback( - async (force = false) => { - if (!canUpgradeFs || (upgraded && !force)) return; + async (force = false, createFiles = false) => { + if (!canUpgradeFs || (root && !force)) return; const handler = await openNand2TetrisDirectory(); const adapter = await createAndStoreLocalAdapterInIndexedDB(handler); - replaceFs(adapter); + await setLocalFs(adapter, createFiles); }, - [upgraded, replaceFs], + [root, setLocalFs], ); const closeFs = useCallback(async () => { - if (!upgraded) return; + if (!root) return; await removeLocalAdapterFromIndexedDB(); + setRoot(undefined); setFs(new FileSystem(localAdapter)); - setUpgraded(undefined); - }, [upgraded, localAdapter]); + }, [root]); const [status, setStatus] = useState(""); return { fs, + localFsRoot: root, status, setStatus, storage: localStorage, canUpgradeFs, upgradeFs, closeFs, - upgraded, }; } @@ -96,7 +100,7 @@ export const BaseContext = createContext({ fs: new FileSystem(new LocalStorageFileSystemAdapter()), canUpgradeFs: false, // eslint-disable-next-line @typescript-eslint/no-empty-function - upgradeFs() {}, + async upgradeFs() {}, // eslint-disable-next-line @typescript-eslint/no-empty-function closeFs() {}, status: "", diff --git a/components/src/stores/chip.store.test.ts b/components/src/stores/chip.store.test.ts index 962a14168..1d2c88c1a 100644 --- a/components/src/stores/chip.store.test.ts +++ b/components/src/stores/chip.store.test.ts @@ -4,7 +4,6 @@ import { } 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 * as bit from "@nand2tetris/projects/project_03/01_bit.js"; import { produce } from "immer"; import { MutableRefObject } from "react"; import { ImmPin } from "src/pinout.js"; @@ -27,6 +26,7 @@ function testChipStore( setStatus, storage, dispatch, + false, ); const store = { state: initialState, actions, reducers, dispatch, setStatus }; dispatch.current = jest.fn().mockImplementation( @@ -46,18 +46,7 @@ function testChipStore( describe("ChipStore", () => { describe("initialization", () => { - it("starts on project 01 not", async () => { - const { state } = testChipStore({}); - - expect(state.controls.project).toBe("01"); - expect(state.controls.chipName).toBe("Not"); - expect(state.files.hdl).toBe(""); - expect(state.files.tst).toBe(""); - expect(state.files.cmp).toBe(""); - expect(state.files.out).toBe(""); - }); - - it("reloads initial chip not", async () => { + it("loads chip", async () => { const store = testChipStore({ "projects/01/Not.hdl": not.hdl, "projects/01/Not.tst": not.tst, @@ -65,42 +54,32 @@ describe("ChipStore", () => { }); 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(not.cmp); + expect(store.state.files.cmp).toBe(""); expect(store.state.files.out).toBe(""); }); - - it("loads saved state", () => { - const { state } = testChipStore( - { - "projects/01/Not.hdl": not.hdl, - "projects/01/Not.tst": not.tst, - "projects/01/Not.cmp": not.cmp, - "projects/03/Bit.hdl": bit.hdl, - "projects/03/Bit.tst": bit.tst, - "projects/03/Bit.cmp": bit.cmp, - }, - { - "/chip/project": "03", - "/chip/chip": "Bit", - }, - ); - expect(state.controls.project).toBe("03"); - expect(state.controls.chipName).toBe("Bit"); - }); }); describe("behavior", () => { - const state = cleanState(() => ({ store: testChipStore() }), beforeEach); + 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 () => { - await state.store.actions.initialize(); 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({ @@ -127,6 +106,7 @@ describe("ChipStore", () => { "projects/01/Not.cmp": not.cmp, }); await store.actions.initialize(); + await store.actions.loadChip("projects/01/Not.hdl"); return { store }; }, beforeEach); @@ -139,12 +119,12 @@ describe("ChipStore", () => { expect(bits(state.store.state.sim.inPins)).toEqual([[0]]); expect(bits(state.store.state.sim.outPins)).toEqual([[0]]); - await state.store.actions.useBuiltin(); + 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(); // Output List + 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]]); @@ -154,7 +134,7 @@ describe("ChipStore", () => { 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 (afte end) + 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]]); }); diff --git a/components/src/stores/chip.store.ts b/components/src/stores/chip.store.ts index 530f4cb36..c24dec9c7 100644 --- a/components/src/stores/chip.store.ts +++ b/components/src/stores/chip.store.ts @@ -1,18 +1,10 @@ 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 { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react"; - import { BUILTIN_CHIP_PROJECTS, - CHIP_ORDER, CHIP_PROJECTS, } from "@nand2tetris/projects/base.js"; import { parse as parseChip } from "@nand2tetris/simulator/chip/builder.js"; -import { - getBuiltinChip, - REGISTRY, -} from "@nand2tetris/simulator/chip/builtins/index.js"; import { Chip, Low, @@ -24,17 +16,20 @@ 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 { Dispatch, MutableRefObject, useContext, useMemo, useRef } from "react"; import { ImmPin, reducePins } from "../pinout.js"; import { useImmerReducer } from "../react.js"; -import { assert } from "@davidsouther/jiffies/lib/esm/assert.js"; -import { RunSpeed } from "src/runbar.js"; +import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js"; +import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js"; +import { getBuiltinChip } from "@nand2tetris/simulator/chip/builtins/index.js"; +import { TST } from "@nand2tetris/simulator/languages/tst.js"; +import { Action } from "@nand2tetris/simulator/types.js"; import { compare } from "../compare.js"; +import { RunSpeed } from "../runbar.js"; import { BaseContext } from "./base.context.js"; -import { Action } from "@nand2tetris/simulator/types.js"; export const NO_SCREEN = "noScreen"; @@ -45,38 +40,10 @@ export const PROJECT_NAMES = [ ["05", `Project 5`], ]; -function getChips(project: keyof typeof CHIP_PROJECTS) { - return project in CHIP_ORDER - ? (CHIP_ORDER as Record)[project] - : BUILTIN_CHIP_PROJECTS[project].concat(CHIP_PROJECTS[project]); -} - -function findDropdowns(storage: Record) { - const project = - (storage["/chip/project"] as keyof typeof CHIP_PROJECTS) ?? "01"; - const chips = getChips(project); - const chipName = storage["/chip/chip"] ?? CHIP_PROJECTS[project][0]; - return { project, chips, chipName }; -} - -function makeHdl(name: string) { - return `CHIP ${name} { - IN in; - OUT out; - PARTS: -}`; -} - -function makeTst() { - return `repeat 10 { - tick, - tock; -}`; -} - -function makeCmp() { - return `| in|out|`; -} +const TEST_NAMES: Record = { + CPU: ["CPU", "CPU-external"], + Computer: ["ComputerAdd", "ComputerMax", "ComputerRect"], +}; export function isBuiltinOnly( project: keyof typeof CHIP_PROJECTS, @@ -85,43 +52,17 @@ export function isBuiltinOnly( return BUILTIN_CHIP_PROJECTS[project].includes(chipName); } -async function getTemplate( - project: keyof typeof CHIP_PROJECTS, - chipName: string, -) { - const { ChipProjects } = await import("@nand2tetris/projects/full.js"); - if (isBuiltinOnly(project, chipName)) { - return (ChipProjects[project].BUILTIN_CHIPS as Record)[ - chipName - ]; - } - - return (ChipProjects[project].CHIPS as Record)[ - `${chipName}.hdl` - ] as string; -} - -async function getBuiltinCode( - project: keyof typeof CHIP_PROJECTS, - chipName: string, -) { - const template = await getTemplate(project, chipName); - if (isBuiltinOnly(project, chipName)) { - return template; - } - const bodyComment = "//// Replace this comment with your code."; - const builtinLine = `BUILTIN ${chipName};`; - const builtinCode = template.includes(bodyComment) - ? template.replace(bodyComment, builtinLine) - : template.replace("PARTS:", `PARTS:\n ${builtinLine}`); - return builtinCode; +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 { @@ -146,13 +87,13 @@ export interface Files { } export interface ControlsState { - project: keyof typeof CHIP_PROJECTS; + projects: string[]; + project: string; chips: string[]; chipName: string; tests: string[]; testName: string; - hasBuiltin: boolean; - builtinOnly: boolean; + usingBuiltin: boolean; runningTest: boolean; span?: Span; error?: CompilationError; @@ -188,15 +129,15 @@ export function makeChipStore( setStatus: Action, storage: Record, dispatch: MutableRefObject, + upgraded: boolean, ) { - const dropdowns = findDropdowns(storage); - let { project, chipName } = dropdowns; - const { chips } = dropdowns; + let _chipName = ""; + let _dir = ""; let chip = new Low(); + let backupHdl = ""; let tests: string[] = []; let test = new ChipTest(); let usingBuiltin = false; - let builtinOnly = false; let invalid = false; const reducers = { @@ -239,25 +180,37 @@ export function makeChipStore( : undefined; }, + setProjects(state: ChipPageState, projects: string[]) { + state.controls.projects = projects; + }, + setProject(state: ChipPageState, project: keyof typeof CHIP_PROJECTS) { - const chips = getChips(project); - const chipName = - state.controls.chipName && chips.includes(state.controls.chipName) - ? state.controls.chipName - : chips[0]; state.controls.project = project; + }, + + setChips(state: ChipPageState, chips: string[]) { state.controls.chips = chips; - this.setChip(state, chipName); }, - setChip(state: ChipPageState, chipName: string) { + 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.controls.hasBuiltin = REGISTRY.has(chipName); - state.controls.builtinOnly = isBuiltinOnly( - state.controls.project, - chipName, - ); + 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) { @@ -308,47 +261,45 @@ export function makeChipStore( updateConfig(state: ChipPageState, config: Partial) { state.config = { ...state.config, ...config }; }, - }; - const actions = { - setProject(p: keyof typeof CHIP_PROJECTS) { - project = storage["/chip/project"] = p; - dispatch.current({ action: "setProject", payload: project }); - this.setChip(CHIP_PROJECTS[project][0]); + updateUsingBuiltin(state: ChipPageState) { + state.controls.usingBuiltin = usingBuiltin; }, - async setChip(chip: string, project = storage["/chip/project"] ?? "01") { - chipName = storage["/chip/chip"] = chip; - builtinOnly = isBuiltinOnly( - project as keyof typeof CHIP_PROJECTS, - chipName, - ); + displayBuiltin(state: ChipPageState) { + backupHdl = state.files.hdl; + this.setFiles(state, { + hdl: convertToBuiltin(state.controls.chipName, state.files.hdl), + }); + }, - if (builtinOnly) { - this.useBuiltin(); + toggleBuiltin(state: ChipPageState) { + state.controls.usingBuiltin = usingBuiltin; + if (usingBuiltin) { + this.displayBuiltin(state); } else { - await this.loadChip(project, chipName); - if (usingBuiltin) { - this.useBuiltin(); - } + this.setFiles(state, { hdl: backupHdl }); } - await this.initializeTest(chip); - dispatch.current({ action: "setChip", payload: chipName }); }, + }; - setTest(test: string) { - dispatch.current({ action: "setTest", payload: test }); - - dispatch.current({ - action: "setVisualizationParams", - payload: new Set( - test == "ComputerAdd.tst" || test == "ComputerMax.tst" - ? [NO_SCREEN] - : [], - ), - }); + const actions = { + async initialize() { + if (upgraded) { + dispatch.current({ + action: "setProjects", + payload: ["1", "2", "3", "5"], + }); + await actions.setProject("1"); + } else { + dispatch.current({ + action: "setProjects", + payload: ["01", "02", "03", "05"], + }); + await actions.setProject("01"); + } - this.loadTest(test); + dispatch.current({ action: "clearChip" }); }, reset() { @@ -359,36 +310,48 @@ export function makeChipStore( dispatch.current({ action: "updateChip" }); }, - async updateFiles({ - hdl, - tst, - cmp, - }: { - hdl?: string; - tst?: string; - cmp: string; - }) { - invalid = false; - dispatch.current({ action: "setFiles", payload: { hdl, tst, cmp } }); - try { - if (hdl) { - await this.compileChip(hdl); - } - if (tst) { - this.compileTest(tst); - } - } catch (e) { - setStatus(display(e)); + async setProject(project: string) { + project = storage["/chip/project"] = project; + dispatch.current({ action: "setProject", payload: project }); + + const chips = ( + await fs.scandir(upgraded ? `/${project}` : `/projects/${project}`) + ) + .filter((entry) => entry.isFile() && entry.name.endsWith(".hdl")) + .map((file) => file.name.replace(".hdl", "")); + dispatch.current({ action: "setChips", payload: chips }); + }, + + 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: "updateChip", payload: { invalid: invalid } }); - if (!invalid) { - setStatus(`HDL code: No syntax errors`); + + 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) { + async compileChip(hdl: string, dir?: string, name?: string) { chip.remove(); - const maybeChip = await parseChip(hdl, chipName); + const maybeChip = await parseChip(hdl, dir, name, fs); if (isErr(maybeChip)) { const error = Err(maybeChip); setStatus(Err(maybeChip).message); @@ -415,48 +378,102 @@ export function makeChipStore( nextChip.eval(); chip = nextChip; chip.reset(); - test = test.with(chip).reset(); dispatch.current({ action: "updateChip", payload: { invalid: false } }); dispatch.current({ action: "updateTestStep" }); }, - async loadChip(project: string, name: string) { - storage["/chip/chip"] = name; - const fsName = (ext: string) => `/projects/${project}/${name}.${ext}`; + async initializeTests(dir: string, chip: string) { + tests = TEST_NAMES[chip] ?? []; + this.loadTest(tests[0] ?? chip, dir); + }, - const hdl = await fs.readFile(fsName("hdl")).catch(() => makeHdl(name)); + async loadTest(name: string, dir?: string) { + if (!fs) return; + try { + dir ??= _dir; - dispatch.current({ action: "setFiles", payload: { hdl } }); - await this.compileChip(hdl); + 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( + `Could not find ${name}.tst. Please load test file separately.`, + ); + console.error(e); + } }, - async initializeTest(name: string) { - tests = (await fs.scandir(`/projects/${project}`)) - .filter( - (file) => file.name.startsWith(name) && file.name.endsWith(".tst"), - ) - .map((file) => file.name); - if (tests.length > 0) { - await this.setTest(tests[0]); + compileTest(file: string, path: string) { + if (!fs) return; + dispatch.current({ action: "setFiles", payload: { tst: file } }); + const tst = TST.parse(file); + if (isErr(tst)) { + setStatus(`Failed to parse test ${Err(tst).message}`); + 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(Err(maybeTest).message); + return false; + } else { + test = Ok(maybeTest).with(chip).reset(); + test.setFileSystem(fs); + dispatch.current({ action: "updateTestStep" }); + return true; } }, - async loadTest(test: string) { - const [tst, cmp] = await Promise.all([ - fs.readFile(`/projects/${project}/${test}`).catch(() => makeTst()), - fs - .readFile(`/projects/${project}/${test}`.replace(".tst", ".cmp")) - .catch(() => makeCmp()), - ]); - dispatch.current({ action: "setFiles", payload: { cmp, tst } }); - this.compileTest(tst); + async updateFiles({ + hdl, + tst, + cmp, + tstPath, + }: { + hdl?: string; + tst?: string; + cmp: string; + tstPath?: string; + }) { + invalid = false; + dispatch.current({ action: "setFiles", payload: { hdl, tst, cmp } }); + console.log("calling update files"); + try { + if (hdl) { + await this.compileChip(hdl, _dir, _chipName); + } + if (tst) { + this.compileTest(tst, tstPath ?? _dir); + } + } catch (e) { + setStatus(display(e)); + } + dispatch.current({ action: "updateChip", payload: { invalid: invalid } }); + if (!invalid) { + setStatus(`HDL code: No syntax errors`); + } }, - async saveChip(hdl: string, prj = project, name = chipName) { + async saveChip(hdl: string) { dispatch.current({ action: "setFiles", payload: { hdl } }); - const path = `/projects/${prj}/${name}.hdl`; - await fs.writeFile(path, hdl); - setStatus(`Saved ${path}`); + const path = `${_dir}/${_chipName}.hdl`; + if (fs && path) { + await fs.writeFile(path, hdl); + } }, toggle(pin: Pin, i: number | undefined) { @@ -485,18 +502,8 @@ export function makeChipStore( dispatch.current({ action: "updateChip" }); }, - async useBuiltin(doUseBuiltin = true, oldHdl?: string) { - if (!doUseBuiltin) { - if (!builtinOnly) { - usingBuiltin = false; - } - await this.loadChip(project, chipName); - return; - } - if (!builtinOnly) { - usingBuiltin = true; - } - const builtinName = chipName; + async loadBuiltin() { + const builtinName = _chipName; const nextChip = await getBuiltinChip(builtinName); if (isErr(nextChip)) { setStatus( @@ -504,49 +511,17 @@ export function makeChipStore( ); return; } - - // Save hdl code that will be overwritten by the switch - if (oldHdl) { - await this.saveChip(oldHdl, project, chipName); - } - - const hdl = await getBuiltinCode(project, builtinName); - dispatch.current({ action: "setFiles", payload: { hdl } }); this.replaceChip(Ok(nextChip)); }, - async initialize() { - await this.setChip(chipName, project); - }, - - compileTest(file: string) { - dispatch.current({ action: "setFiles", payload: { tst: file } }); - const tst = TST.parse(file); - - if (isErr(tst)) { - setStatus(`Failed to parse test ${Err(tst).message}`); - invalid = true; - return false; - } - - test = ChipTest.from(Ok(tst), setStatus).with(chip).reset(); - test.setFileSystem(fs); - dispatch.current({ action: "updateTestStep" }); - return true; - }, - - async runTest(file: string) { - if (!this.compileTest(file)) { - return; + async toggleBuiltin() { + usingBuiltin = !usingBuiltin; + dispatch.current({ action: "toggleBuiltin" }); + if (usingBuiltin) { + await this.loadBuiltin(); + } else { + await this.compileChip(backupHdl, _dir, _chipName); } - dispatch.current({ action: "testRunning" }); - - fs.pushd("/samples"); - await test.run(); - fs.popd(); - - dispatch.current({ action: "updateTestStep" }); - dispatch.current({ action: "testFinished" }); }, tick(): Promise { @@ -554,42 +529,29 @@ export function makeChipStore( }, async stepTest(): Promise { - assert(test.chipId === chip.id, "Test and chip out of sync"); - const done = await test.step(); - dispatch.current({ action: "updateTestStep" }); - if (done) { - dispatch.current({ action: "testFinished" }); + try { + const done = await test.step(); + dispatch.current({ action: "updateTestStep" }); + if (done) { + dispatch.current({ action: "testFinished" }); + } + return done; + } catch (e) { + setStatus((e as Error).message); + return true; } - return done; - }, - - async resetFile() { - const { ChipProjects } = await import("@nand2tetris/projects/full.js"); - const template = (ChipProjects[project].CHIPS as Record)[ - `${chipName}.hdl` - ]; - dispatch.current({ action: "setFiles", payload: { hdl: template } }); - }, - - async getProjectFiles() { - return await Promise.all( - CHIP_PROJECTS[project].map((chip) => ({ - name: `${chip}.hdl`, - content: fs.readFile(`/projects/${project}/${chip}.hdl`), - })), - ); }, }; const initialState: ChipPageState = (() => { const controls: ControlsState = { - project, - chips, - chipName, + projects: ["1", "2", "3", "5"], + project: "1", + chips: [], + chipName: "", tests, testName: "", - hasBuiltin: REGISTRY.has(chipName), - builtinOnly: isBuiltinOnly(project, chipName), + usingBuiltin: false, runningTest: false, error: undefined, visualizationParameters: new Set(), @@ -604,6 +566,7 @@ export function makeChipStore( cmp: "", tst: "", out: "", + backupHdl: "", }, sim, config: { speed: 2 }, @@ -614,12 +577,13 @@ export function makeChipStore( } export function useChipPageStore() { - const { fs, setStatus, storage } = useContext(BaseContext); + const { fs, setStatus, storage, localFsRoot } = useContext(BaseContext); const dispatch = useRef(() => undefined); const { initialState, reducers, actions } = useMemo( - () => makeChipStore(fs, setStatus, storage, dispatch), + () => + makeChipStore(fs, setStatus, storage, dispatch, localFsRoot != undefined), [fs, setStatus, storage, dispatch], ); diff --git a/components/src/stores/cpu.store.ts b/components/src/stores/cpu.store.ts index bc0b24379..74c3e6351 100644 --- a/components/src/stores/cpu.store.ts +++ b/components/src/stores/cpu.store.ts @@ -14,7 +14,9 @@ import { } 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"; @@ -23,7 +25,6 @@ import { loadTestFiles } from "../file_utils.js"; import { useImmerReducer } from "../react.js"; import { BaseContext } from "./base.context.js"; import { ImmMemory } from "./imm_memory.js"; -import { Action } from "@nand2tetris/simulator/types.js"; function makeTst() { return `repeat { @@ -104,6 +105,7 @@ export function makeCpuStore( let path = ""; let tests: string[] = []; let tstName = ""; + let _title: string | undefined; const reducers = { update(state: CpuPageState) { @@ -138,8 +140,12 @@ export function makeCpuStore( ); }, - setTitle(state: CpuPageState, title: string) { + setTitle(state: CpuPageState, title?: string) { + _title = title; state.title = title; + if (title) { + test.fileLoaded = true; + } }, updateConfig(state: CpuPageState, config: Partial) { @@ -176,14 +182,19 @@ export function makeCpuStore( }, async testStep() { - const done = await test.step(); - if (animate || done) { - dispatch.current({ action: "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((e as Error).message); + return true; } - if (done) { - dispatch.current({ action: "testFinished" }); - } - return done; }, resetRAM() { @@ -210,16 +221,18 @@ export function makeCpuStore( }, clearTest() { + tstName = ""; this.compileTest(makeTst(), ""); dispatch.current({ action: "update" }); }, replaceROM(rom: ROM) { - test = new CPUTest(rom); + test = new CPUTest({ dir: path, rom }); this.clearTest(); }, - compileTest(file: string, cmp?: string) { + compileTest(file: string, cmp?: string, _path?: string) { + const tstPath = _path ?? path; dispatch.current({ action: "setTest", payload: { tst: file, cmp } }); const tst = TST.parse(file); @@ -231,9 +244,43 @@ export function makeCpuStore( } valid = true; - test = CPUTest.from(Ok(tst), test.cpu.ROM, setStatus); - dispatch.current({ action: "update" }); - return 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(Err(maybeTest).message); + return false; + } else { + test = Ok(maybeTest); + test.fileLoaded = _title != undefined; + dispatch.current({ action: "update" }); + return true; + } }, async loadTest(name: string) { @@ -244,8 +291,8 @@ export function makeCpuStore( return; } tstName = name; - const { tst, cmp } = unwrap(files); - this.compileTest(tst, cmp ?? ""); + const { tst } = unwrap(files); + this.compileTest(tst, ""); }, }; diff --git a/components/src/stores/vm.store.ts b/components/src/stores/vm.store.ts index cafef4ccb..c8141f289 100644 --- a/components/src/stores/vm.store.ts +++ b/components/src/stores/vm.store.ts @@ -1,6 +1,8 @@ +import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js"; import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js"; import { Err, + Ok, Result, isErr, unwrap, @@ -128,7 +130,7 @@ export function makeVmStore( ) { const parsed = unwrap(VM.parse(FIBONACCI)); let vm = unwrap(Vm.build(parsed.instructions)); - let test = new VMTest(setStatus).with(vm); + let test = new VMTest({ doEcho: setStatus }).with(vm); let useTest = false; let animate = true; let vmSource = ""; @@ -137,9 +139,11 @@ export function makeVmStore( setVm(state: VmPageState, vm: string) { state.files.vm = vm; }, - setTst(state: VmPageState, { tst, cmp }: { tst: string; cmp?: string }) { + setTst(state: VmPageState, { tst }: { tst: string }) { state.files.tst = tst; - state.files.cmp = cmp ?? ""; + }, + setCmp(state: VmPageState, { cmp }: { cmp: string }) { + state.files.cmp = cmp; }, setExitCode(state: VmPageState, code: number | undefined) { state.controls.exitCode = code; @@ -216,6 +220,34 @@ export function makeVmStore( }, }; 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({ @@ -239,11 +271,7 @@ export function makeVmStore( }, loadVm(files: VmFile[]) { showHighlight = false; - for (const file of files) { - if (file.content.endsWith("\n")) { - file.content = file.content.slice(0, -1); - } - } + const content = files.map((f) => f.content).join("\n"); dispatch.current({ action: "setVm", @@ -288,15 +316,16 @@ export function makeVmStore( return false; } dispatch.current({ action: "setError" }); - setStatus("Compiled VM code successfully"); + // setStatus("Compiled VM code successfully"); vm = unwrap(buildResult); test.vm = vm; dispatch.current({ action: "update" }); return true; }, - loadTest(path: string, source: string, cmp?: string) { - dispatch.current({ action: "setTst", payload: { tst: source, cmp } }); + + loadTest(path: string, source: string) { + dispatch.current({ action: "setTst", payload: { tst: source } }); const tst = TST.parse(source); if (isErr(tst)) { @@ -308,17 +337,28 @@ export function makeVmStore( setStatus(`Parsed tst`); vm.reset(); - test = VMTest.from( - unwrap(tst), - path, - (files) => { - this.loadVm(files); + + const maybeTest = VMTest.from(unwrap(tst), { + dir: path, + doLoad: async (path) => { + await this.load(path); }, - setStatus, - ).using(fs); - test.vm = vm; - dispatch.current({ action: "update" }); - return true; + 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(Err(maybeTest).message); + return false; + } else { + test = Ok(maybeTest).using(fs); + test.vm = vm; + dispatch.current({ action: "update" }); + return true; + } }, setAnimate(value: boolean) { animate = value; diff --git a/extension/views/hdl/src/App.tsx b/extension/views/hdl/src/App.tsx index 21d11aa2a..f49317f38 100644 --- a/extension/views/hdl/src/App.tsx +++ b/extension/views/hdl/src/App.tsx @@ -1,3 +1,9 @@ +import { makeVisualizationsWithId } from "@nand2tetris/components/chips/visualizations.js"; +import { Clockface } from "@nand2tetris/components/clockface.js"; +import { FullPinout } from "@nand2tetris/components/pinout.js"; +import { useChipPageStore } from "@nand2tetris/components/stores/chip.store.js"; +import * as Not from "@nand2tetris/projects/project_01/01_not.js"; +import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; import { ReactNode, useCallback, @@ -6,22 +12,12 @@ import { useMemo, useState, } from "react"; -import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"; -import * as Not from "@nand2tetris/projects/project_01/01_not.js"; -import { makeVisualizationsWithId } from "@nand2tetris/components/chips/visualizations.js"; -import { Clockface } from "@nand2tetris/components/clockface.js"; -import { FullPinout } from "@nand2tetris/components/pinout.js"; -import { useChipPageStore } from "@nand2tetris/components/stores/chip.store.js"; import { VSCodeContext } from "./vscode"; function App() { const { state, actions, dispatch } = useChipPageStore(); const { api } = useContext(VSCodeContext); - useEffect(() => { - actions.initialize(); - }, [actions]); - const [hdl, setHdl] = useState(Not.hdl); const [loaded, setLoaded] = useState(false); @@ -30,14 +26,14 @@ function App() { setHdl(hdl); await actions.updateFiles({ hdl, tst: "// No test", cmp: "" }); }, - [setHdl, actions] + [setHdl, actions], ); const onMessage = useCallback( ( event: MessageEvent< Partial<{ nand2tetris: boolean; hdl: string; chipName: string }> - > + >, ) => { if (!event.data?.nand2tetris) return; if (event.data.hdl) compile(event.data.hdl ?? ""); @@ -50,7 +46,7 @@ function App() { }); setLoaded(true); }, - [compile, dispatch] + [compile, dispatch], ); useEffect(() => { @@ -70,7 +66,7 @@ function App() { compile(hdl); setUseBuiltin(false); } else { - actions.useBuiltin(); + actions.toggleBuiltin(); setUseBuiltin(true); } }; @@ -84,7 +80,7 @@ function App() { actions.reset(); }, }), - [actions] + [actions], ); const chipButtons = state.controls.error ? ( diff --git a/extension/views/hdl/src/index.tsx b/extension/views/hdl/src/index.tsx index 65901a53e..ca99a2466 100644 --- a/extension/views/hdl/src/index.tsx +++ b/extension/views/hdl/src/index.tsx @@ -10,11 +10,10 @@ import App from "./App"; const baseContext: BaseContext = { fs: new FileSystem( - new ObjectFileSystemAdapter({ "projects/01/Not.hdl": Not.hdl }) + new ObjectFileSystemAdapter({ "projects/01/Not.hdl": Not.hdl }), ), - upgraded: "true", canUpgradeFs: false, - upgradeFs() {}, + async upgradeFs() {}, closeFs() {}, storage: {}, status: "", @@ -25,12 +24,12 @@ const baseContext: BaseContext = { }; const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement + document.getElementById("root") as HTMLElement, ); root.render( - + , ); diff --git a/projects/src/full.ts b/projects/src/full.ts index 079b1cf83..08a272572 100644 --- a/projects/src/full.ts +++ b/projects/src/full.ts @@ -7,6 +7,7 @@ import * as project_04 from "./project_04/index.js"; import * as project_05 from "./project_05/index.js"; import * as project_07 from "./project_07/index.js"; import * as project_08 from "./project_08/index.js"; +import { reset } from "./reset.js"; import * as project_06 from "./samples/project_06/index.js"; export const ChipProjects = { @@ -32,10 +33,21 @@ const Projects = { 8: project_08, }; -let reset = false; +const ProjectFiles = { + "1": project_01.CHIPS, + "2": project_02.CHIPS, + "3": project_03.CHIPS, + "4": project_04.TESTS, + "5": project_05.CHIPS, + "6": project_06.FILES, + "7": project_07.VMS, + "8": project_08.VMS, +}; + +let resetFlag = false; export const resetFiles = async (fs: FileSystem, projects?: number[]) => { - if (reset) return; // React will double-render a call to resetFiles in useEffect. - reset = true; + if (resetFlag) return; // React will double-render a call to resetFiles in useEffect. + resetFlag = true; projects ??= [1, 2, 3, 4, 6, 5, 7, 8]; for (const project of projects) { if (!Object.keys(Projects).includes(project.toString())) { @@ -43,12 +55,12 @@ export const resetFiles = async (fs: FileSystem, projects?: number[]) => { } await Projects[project as keyof typeof Projects].resetFiles(fs); } - reset = false; + resetFlag = false; }; export const resetTests = async (fs: FileSystem, projects?: number[]) => { - if (reset) return; // React will double-render a call to resetTests in useEffect. - reset = true; + if (resetFlag) return; // React will double-render a call to resetTests in useEffect. + resetFlag = true; projects ??= [1, 2, 3, 4, 5, 7, 8]; for (const project of projects) { if (!Object.keys(Projects).includes(project.toString())) { @@ -56,7 +68,11 @@ export const resetTests = async (fs: FileSystem, projects?: number[]) => { } await Projects[project as keyof typeof Projects].resetTests(fs); } - reset = false; + resetFlag = false; +}; + +export const createFiles = async (fs: FileSystem) => { + await reset(fs, ProjectFiles); }; export const Assignments = { diff --git a/projects/src/loader.ts b/projects/src/loader.ts index e64ea5a06..c045590d6 100644 --- a/projects/src/loader.ts +++ b/projects/src/loader.ts @@ -8,6 +8,10 @@ export async function resetTests(fs: FileSystem, projects?: number[]) { (await import("./full.js")).resetTests(fs, projects); } +export async function createFiles(fs: FileSystem) { + await (await import("./full.js")).createFiles(fs); +} + export async function loadSamples(fs: FileSystem) { (await import("./samples/index.js")).loadSamples(fs); } diff --git a/projects/src/reset.ts b/projects/src/reset.ts index cc685ea6b..f423de30a 100644 --- a/projects/src/reset.ts +++ b/projects/src/reset.ts @@ -18,3 +18,15 @@ export async function resetBySuffix( } } } + +export async function reset(fs: FileSystem, tree: Tree, base?: string) { + for (const [key, value] of Object.entries(tree)) { + const path = `${base ? `${base}/` : ""}${key}`; + if (typeof value === "string") { + await fs.writeFile(path, value); + } else { + await fs.mkdir(path); + await reset(fs, value as Tree, path); + } + } +} diff --git a/simulator/src/chip/builder.test.ts b/simulator/src/chip/builder.test.ts index 7cf403af0..de8f2012b 100644 --- a/simulator/src/chip/builder.test.ts +++ b/simulator/src/chip/builder.test.ts @@ -123,7 +123,7 @@ describe("Chip Builder", () => { try { const chip = unwrap(await HDL.parse(USE_COPY_HDL)); - foo = unwrap(await build(chip, fs)); + foo = unwrap(await build({ parts: chip, dir: ".", fs })); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { @@ -147,7 +147,7 @@ describe("Chip Builder", () => { PARTS: Or8Way(in=in, out=out); }`), ); - const foo = await build(chip); + const foo = await build({ parts: chip }); expect(foo).toBeErr(); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -164,7 +164,7 @@ describe("Chip Builder", () => { PARTS: Not(in=in, out=out); }`), ); - const foo = await build(chip); + const foo = await build({ parts: chip }); expect(foo).toBeErr(); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -183,7 +183,7 @@ describe("Chip Builder", () => { Nand(a=in, b=myNand, out=myNand); }`), ); - const foo = await build(chip); + const foo = await build({ parts: chip }); expect(foo).toBeErr(); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -203,7 +203,7 @@ describe("Chip Builder", () => { Nand(a=in, b=c, out=b); }`), ); - const foo = await build(chip); + const foo = await build({ parts: chip }); expect(foo).toBeErr(); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/simulator/src/chip/builder.ts b/simulator/src/chip/builder.ts index 086575b36..c557e1447 100644 --- a/simulator/src/chip/builder.ts +++ b/simulator/src/chip/builder.ts @@ -29,29 +29,37 @@ function pinWidth(pin: PinParts): Result { export async function parse( code: string, + dir?: string, name?: string, + fs?: FileSystem, ): Promise> { const parsed = HDL.parse(code.toString()); if (isErr(parsed)) { return parsed; } - return build(Ok(parsed), undefined, name); + return build({ parts: Ok(parsed), dir, name, fs }); } export async function loadChip( name: string, + dir?: string, fs?: FileSystem, ): Promise> { if (hasBuiltinChip(name) || fs === undefined) { return await getBuiltinChip(name); } try { - const file = await fs.readFile(`${name}.hdl`); + const file = await fs.readFile(`${dir}/${name}.hdl`); const maybeParsedHDL = HDL.parse(file); let maybeChip: Result; if (isOk(maybeParsedHDL)) { - const maybeBuilt = await build(Ok(maybeParsedHDL), fs); + const maybeBuilt = await build({ + parts: Ok(maybeParsedHDL), + dir, + name, + fs, + }); if (isErr(maybeBuilt)) { maybeChip = Err(new Error(Err(maybeBuilt).message)); } else { @@ -68,11 +76,9 @@ export async function loadChip( } export async function build( - parts: HdlParse, - fs?: FileSystem, - name?: string, -): Promise> { - return await new ChipBuilder(parts, fs, name).build(); + ...args: Parameters +): Promise> { + return await ChipBuilder.build(...args); } interface InternalPin { @@ -177,6 +183,7 @@ function checkMultipleAssignments( class ChipBuilder { private parts: HdlParse; private fs?: FileSystem; + private dir?: string; private expectedName?: string; private chip: Chip; @@ -185,9 +192,29 @@ class ChipBuilder { private outPins: Map> = new Map(); private wires: WireData[] = []; - constructor(parts: HdlParse, fs?: FileSystem, name?: string) { + static build(options: { + parts: HdlParse; + fs?: FileSystem; + dir?: string; + name?: string; + }) { + return new ChipBuilder(options).build(); + } + + private constructor({ + parts, + fs, + dir, + name, + }: { + parts: HdlParse; + fs?: FileSystem; + dir?: string; + name?: string; + }) { this.parts = parts; this.expectedName = name; + this.dir = dir; this.fs = fs; this.chip = new Chip( parts.ins.map(({ pin, width }) => ({ pin: pin.toString(), width })), @@ -198,7 +225,7 @@ class ChipBuilder { ); } - async build() { + async build(): Promise> { if (this.expectedName && this.parts.name.value != this.expectedName) { return Err(createError(`Wrong chip name`, this.parts.name.span)); } @@ -230,8 +257,9 @@ class ChipBuilder { if (this.parts.parts === "BUILTIN") { return Ok(); } + for (const part of this.parts.parts) { - const builtin = await loadChip(part.name, this.fs); + const builtin = await loadChip(part.name, this.dir, this.fs); if (isErr(builtin)) { return Err(createError(`Undefined chip name: ${part.name}`, part.span)); } diff --git a/simulator/src/chip/builtins/all.test.ts b/simulator/src/chip/builtins/all.test.ts index b3a88436a..ac6f5a4a2 100644 --- a/simulator/src/chip/builtins/all.test.ts +++ b/simulator/src/chip/builtins/all.test.ts @@ -2,7 +2,7 @@ import { FileSystem, ObjectFileSystemAdapter, } from "@davidsouther/jiffies/lib/esm/fs.js"; -import { Ok } from "@davidsouther/jiffies/lib/esm/result.js"; +import { Ok, unwrap } from "@davidsouther/jiffies/lib/esm/result.js"; import { CHIP_PROJECTS } from "@nand2tetris/projects/base.js"; import { ChipProjects } from "@nand2tetris/projects/full.js"; import { Max } from "@nand2tetris/projects/samples/hack.js"; @@ -42,9 +42,11 @@ describe("All Projects", () => { const tst = TST.parse(tstFile); expect(tst).toBeOk(); - const chip = await build(Ok(hdl as Ok)); + const chip = await build({ parts: Ok(hdl as Ok) }); expect(chip).toBeOk(); - const test = ChipTest.from(Ok(tst as Ok)).with(Ok(chip as Ok)); + const test = unwrap(ChipTest.from(Ok(tst as Ok))).with( + Ok(chip as Ok), + ); if (project === "05") { test.setFileSystem( diff --git a/simulator/src/chip/chip.ts b/simulator/src/chip/chip.ts index e84787fc1..84fb378b4 100644 --- a/simulator/src/chip/chip.ts +++ b/simulator/src/chip/chip.ts @@ -148,13 +148,19 @@ export class OutSubBus extends Bus { this.connect(bus); } + override pull(voltage: Voltage, bit = 0) { + if (bit >= this.start && bit < this.start + this.width) { + this.bus.pull(voltage, bit - this.start); + } + } + override set busVoltage(voltage: number) { this.bus.busVoltage = (voltage & mask(this.width + this.start)) >> this.start; } override get busVoltage(): number { - return this.bus.busVoltage & mask(this.width); + return (this.bus.busVoltage >> this.start) & mask(this.width); } override connect(bus: Pin): void { diff --git a/simulator/src/languages/grammars/tst.ohm b/simulator/src/languages/grammars/tst.ohm index 91bb9870f..df5085b2c 100644 --- a/simulator/src/languages/grammars/tst.ohm +++ b/simulator/src/languages/grammars/tst.ohm @@ -32,7 +32,8 @@ Tst <: Base { TstEchoOperation = Echo String TstClearEchoOperation = ClearEcho - FileName = Name + filename = (alnum|underscore|dot|dollar|minus)+ + FileName = filename FileOperation = "load" | "output-file" | "compare-to" Set = "set" diff --git a/simulator/src/languages/grammars/tst.ohm.js b/simulator/src/languages/grammars/tst.ohm.js index 5e3b145ae..62f7abf99 100644 --- a/simulator/src/languages/grammars/tst.ohm.js +++ b/simulator/src/languages/grammars/tst.ohm.js @@ -33,7 +33,8 @@ Tst <: Base { TstEchoOperation = Echo String TstClearEchoOperation = ClearEcho - FileName = Name + filename = (alnum|underscore|dot|dollar|minus)+ + FileName = filename FileOperation = "load" | "output-file" | "compare-to" Set = "set" diff --git a/simulator/src/languages/tst.ts b/simulator/src/languages/tst.ts index 5c731ba05..fcdca7886 100644 --- a/simulator/src/languages/tst.ts +++ b/simulator/src/languages/tst.ts @@ -186,10 +186,10 @@ tstSemantics.addAttribute("operation", { op: "clear-echo", }; }, - TstLoadROMOperation(_r, _l, { name }) { + TstLoadROMOperation(_r, _l, name) { return { op: "loadRom", - file: name, + file: name.sourceString, }; }, TstFileOperation(op, file) { diff --git a/simulator/src/projects/all.test.ts b/simulator/src/projects/all.test.ts index d58aec11e..bb863e5e4 100644 --- a/simulator/src/projects/all.test.ts +++ b/simulator/src/projects/all.test.ts @@ -63,9 +63,9 @@ describe("Chip Projects", () => { const tst = TST.parse(tstFile); expect(tst).toBeOk(); - const chip = await build(Ok(hdl as Ok)); + const chip = await build({ parts: Ok(hdl as Ok) }); expect(chip).toBeOk(); - const test = ChipTest.from(Ok(tst as Ok)).with( + const test = unwrap(ChipTest.from(Ok(tst as Ok))).with( Ok(chip as Ok), ); @@ -143,7 +143,7 @@ describe("Vm Projects", () => { const vm = await Vm.build(unwrap(parsed).instructions); expect(vm).toBeOk(); - const test = VMTest.from(unwrap(tst)).with(unwrap(vm)); + const test = unwrap(VMTest.from(unwrap(tst))).with(unwrap(vm)); await test.run(); diff --git a/simulator/src/projects/runner.ts b/simulator/src/projects/runner.ts index 8735c5e61..b041f19f8 100644 --- a/simulator/src/projects/runner.ts +++ b/simulator/src/projects/runner.ts @@ -1,22 +1,22 @@ import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js"; import { - isOk, - Ok, Err, - isErr, + Ok, Result, + isErr, + isOk, } from "@davidsouther/jiffies/lib/esm/result.js"; import { AssignmentStubs, type Assignment, } from "@nand2tetris/projects/base.js"; -import type { Runner, RunResult } from "@nand2tetris/runner/types.js"; -import { HDL, HdlParse } from "../languages/hdl.js"; -import { Tst, TST } from "../languages/tst.js"; +import type { RunResult, Runner } from "@nand2tetris/runner/types.js"; import { build as buildChip } from "../chip/builder.js"; import { Chip } from "../chip/chip.js"; -import { ChipTest } from "../test/chiptst.js"; import { CompilationError } from "../languages/base.js"; +import { HDL, HdlParse } from "../languages/hdl.js"; +import { TST, Tst } from "../languages/tst.js"; +import { ChipTest } from "../test/chiptst.js"; export interface AssignmentFiles extends Assignment { hdl: string; @@ -63,7 +63,10 @@ export const maybeBuild = async (file: AssignmentParse): Promise => { let maybeChip: Result; if (isOk(file.maybeParsedHDL)) { - const maybeBuilt = await buildChip(Ok(file.maybeParsedHDL), fs); + const maybeBuilt = await buildChip({ + parts: Ok(file.maybeParsedHDL), + fs, + }); if (isErr(maybeBuilt)) { maybeChip = Err(new Error(Err(maybeBuilt).message)); } else { @@ -73,8 +76,9 @@ export const maybeBuild = maybeChip = Err(new Error("HDL Was not parsed")); } const maybeTest = isOk(file.maybeParsedTST) - ? Ok(ChipTest.from(Ok(file.maybeParsedTST))) + ? ChipTest.from(Ok(file.maybeParsedTST)) : Err(new Error("TST Was not parsed")); + return { ...file, maybeChip, maybeTest }; }; diff --git a/simulator/src/test/builder.ts b/simulator/src/test/builder.ts index 8327b2a79..71b0a333f 100644 --- a/simulator/src/test/builder.ts +++ b/simulator/src/test/builder.ts @@ -1,4 +1,5 @@ import { checkExhaustive } from "@davidsouther/jiffies/lib/esm/assert.js"; +import { Err, Ok, Result } from "@davidsouther/jiffies/lib/esm/result.js"; import { Span } from "../languages/base.js"; import { Tst, @@ -17,10 +18,12 @@ import { Condition, TestBreakInstruction, TestClearEchoInstruction, + TestCompareToInstruction, TestEchoInstruction, TestInstruction, TestLoadInstruction, TestLoadROMInstruction, + TestOutputFileInstruction, TestOutputInstruction, TestOutputListInstruction, TestRepeatInstruction, @@ -31,7 +34,7 @@ import { import { Test } from "./tst.js"; import { TestVMStepInstruction } from "./vmtst.js"; -function isTstCommand(line: TstStatement): line is TstCommand { +export function isTstCommand(line: TstStatement): line is TstCommand { return (line as TstCommand).op !== undefined; } @@ -67,20 +70,27 @@ function makeInstruction(inst: TstOperation) { case "load": return new TestLoadInstruction(inst.file); case "output-file": + return new TestOutputFileInstruction(inst.file); case "compare-to": - return undefined; + return new TestCompareToInstruction(inst.file); default: checkExhaustive(op, `Unknown tst operation ${op}`); } } -export function fill(test: T, tst: Tst): T { +export function fill( + test: T, + tst: Tst, + requireLoad = true, +): Result { let span: Span | undefined; let stepInstructions: TestInstruction[] = []; let base: T | TestWhileInstruction | TestRepeatInstruction = test; let commands: TstCommand[] = []; + let hasLoad = false; + for (const line of tst.lines) { if (isTstCommand(line)) { base = test; @@ -103,6 +113,9 @@ export function fill(test: T, tst: Tst): T { } for (const command of commands) { + if (command.op.op == "load") { + hasLoad = true; + } const inst = makeInstruction(command.op); if (inst !== undefined) { if (span === undefined) { @@ -129,7 +142,11 @@ export function fill(test: T, tst: Tst): T { } } + if (requireLoad && !hasLoad) { + return Err(new Error("A test script must have a load command")); + } + test.reset(); - return test; + return Ok(test); } diff --git a/simulator/src/test/chiptst.test.ts b/simulator/src/test/chiptst.test.ts index 96fd97845..9b393eb89 100644 --- a/simulator/src/test/chiptst.test.ts +++ b/simulator/src/test/chiptst.test.ts @@ -1,3 +1,4 @@ +import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js"; import { Computer } from "../chip/builtins/computer/computer.js"; import { Nand } from "../chip/builtins/logic/nand.js"; import { TstRepeat } from "../languages/tst.js"; @@ -180,7 +181,14 @@ describe("Chip Test", () => { }, }; - const test = ChipTest.from({ lines: [repeat] }); + const maybeTest = ChipTest.from( + { + lines: [repeat], + }, + { requireLoad: false }, + ); + expect(maybeTest).toBeOk(); + const test = unwrap(maybeTest); test.outputList([{ id: "time", style: "S", len: 4, lpad: 0, rpad: 0 }]); await test.run(); diff --git a/simulator/src/test/chiptst.ts b/simulator/src/test/chiptst.ts index 7d3f1273f..be1256855 100644 --- a/simulator/src/test/chiptst.ts +++ b/simulator/src/test/chiptst.ts @@ -1,3 +1,4 @@ +import { Result } from "@davidsouther/jiffies/lib/esm/result.js"; import { Bus, Chip, HIGH, Low, LOW } from "../chip/chip.js"; import { Clock } from "../chip/clock.js"; import { Tst } from "../languages/tst.js"; @@ -8,15 +9,42 @@ import { Test } from "./tst.js"; export class ChipTest extends Test { private chip: Chip = new Low(); + private doLoad?: (path: string) => Promise; + get chipId(): number { return this.chip.id; } private clock = Clock.get(); - static from(tst: Tst, setStatus?: Action): ChipTest { - const test = new ChipTest(setStatus); - return fill(test, tst); + static from( + tst: Tst, + options: { + dir?: string; + setStatus?: Action; + loadAction?: (path: string) => Promise; + compareTo?: Action; + requireLoad?: boolean; + } = {}, + ): Result { + const test = new ChipTest(options); + + return fill(test, tst, options.requireLoad); + } + + constructor({ + dir, + setStatus, + loadAction, + compareTo, + }: { + dir?: string; + setStatus?: Action; + loadAction?: (path: string) => Promise; + compareTo?: Action; + } = {}) { + super(dir, setStatus, compareTo); + this.doLoad = loadAction; } with(chip: Chip): this { @@ -24,6 +52,16 @@ export class ChipTest extends Test { return this; } + override async load(filename?: string): Promise { + if (!this.dir) return; + const chip = await this.doLoad?.( + filename ? `${this.dir}/${filename}` : this.dir, + ); + if (chip) { + this.chip = chip; + } + } + hasVar(variable: string | number): boolean { if (variable === "time") { return true; @@ -74,7 +112,7 @@ export class ChipTest extends Test { } override async loadROM(filename: string) { - await this.chip.load(this.fs, filename); + await this.chip.load(this.fs, [this.dir ?? "", filename].join("/")); } override async run() { diff --git a/simulator/src/test/cputst.ts b/simulator/src/test/cputst.ts index e7b02249e..c654e7d62 100644 --- a/simulator/src/test/cputst.ts +++ b/simulator/src/test/cputst.ts @@ -1,26 +1,77 @@ +import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js"; +import { Result } from "@davidsouther/jiffies/lib/esm/result.js"; import { CPU } from "../cpu/cpu.js"; import { ROM } from "../cpu/memory.js"; import { Tst } from "../languages/tst.js"; -import { Action } from "../types.js"; -import { fill } from "./builder.js"; +import { Action, AsyncAction } from "../types.js"; +import { fill, isTstCommand } from "./builder.js"; import { TestInstruction } from "./instruction.js"; import { Test } from "./tst.js"; export class CPUTest extends Test { - readonly cpu: CPU; + cpu: CPU; private ticks = 0; + private doLoad?: AsyncAction; + fileLoaded = false; + hasLoad = false; - static from(tst: Tst, rom?: ROM, doEcho?: Action): CPUTest { - const test = new CPUTest(rom, doEcho); - return fill(test, tst); + static from( + tst: Tst, + options: { + dir?: string; + rom?: ROM; + doEcho?: Action; + doLoad?: AsyncAction; + compareTo?: Action; + requireLoad?: boolean; + } = {}, + ): Result { + const test = new CPUTest(options); + + test.hasLoad = tst.lines.some( + (line) => isTstCommand(line) && line.op.op == "load", + ); + + return fill(test, tst, options.requireLoad); } - constructor(rom: ROM = new ROM(), doEcho?: Action) { - super(doEcho); + constructor({ + dir, + rom = new ROM(), + doEcho, + doLoad, + compareTo, + }: { + dir?: string; + rom?: ROM; + doEcho?: Action; + doLoad?: AsyncAction; + compareTo?: Action; + } = {}) { + super(dir, doEcho, compareTo); + this.doLoad = doLoad; this.cpu = new CPU({ ROM: rom }); this.reset(); } + override async step() { + if (!(this.hasLoad || this.fileLoaded)) { + throw new Error( + "Cannot execute the test without first loading an .asm or .hack file", + ); + } + return super.step(); + } + + override async load(filename?: string): Promise { + if (!filename && !this.dir) return; + const dir = assertExists(this.dir?.split("/").slice(0, -1).join("/")); + const rom = await this.doLoad?.(filename ? `${dir}/${filename}` : dir); + if (rom) { + this.cpu = new CPU({ ROM: rom }); + } + } + override reset(): this { super.reset(); this.cpu.reset(); diff --git a/simulator/src/test/instruction.ts b/simulator/src/test/instruction.ts index 5f50030af..df4d8311f 100644 --- a/simulator/src/test/instruction.ts +++ b/simulator/src/test/instruction.ts @@ -219,9 +219,7 @@ export class TestClearEchoInstruction implements TestInstruction { export class TestLoadROMInstruction implements TestInstruction { constructor(readonly file: string) {} async do(test: Test) { - test.fs.pushd("/samples"); await test.loadROM(this.file); - test.fs.popd(); } *steps() { @@ -241,6 +239,34 @@ export class TestLoadInstruction implements TestInstruction { } } +export class TestCompareToInstruction implements TestInstruction { + constructor(readonly file?: string) {} + + async do(test: Test) { + if (this.file) { + await test.compareTo(this.file); + } + } + + *steps() { + yield this; + } +} + +export class TestOutputFileInstruction implements TestInstruction { + constructor(readonly file?: string) {} + + async do(test: Test) { + if (this.file) { + test.outputFile(this.file); + } + } + + *steps() { + yield this; + } +} + export class TestBreakpointInstruction implements TestInstruction { constructor( readonly variable: string, diff --git a/simulator/src/test/tst.ts b/simulator/src/test/tst.ts index 96fdee932..be614f617 100644 --- a/simulator/src/test/tst.ts +++ b/simulator/src/test/tst.ts @@ -17,9 +17,18 @@ export abstract class Test { protected _log = ""; fs: FileSystem = new FileSystem(); protected doEcho?: Action; - - constructor(doEcho?: Action) { + protected doCompareTo?: Action; + protected dir?: string; + protected outputFileName?: string; + + constructor( + path?: string, + doEcho?: Action, + doCompareTo?: Action, + ) { this.doEcho = doEcho; + this.doCompareTo = doCompareTo; + this.dir = path; } setFileSystem(fs: FileSystem): this { @@ -42,11 +51,12 @@ export abstract class Test { async load(_filename?: string): Promise { return undefined; } - async compareTo(_filename: string): Promise { - return undefined; + + async compareTo(filename: string): Promise { + this.doCompareTo?.(filename); } - outputFile(_filename: string): void { - return undefined; + outputFile(filename: string): void { + this.outputFileName = filename; } private createOutputs(params: OutputParams[]): Output[] { diff --git a/simulator/src/test/vmtst.test.ts b/simulator/src/test/vmtst.test.ts index a31e2e68d..75fc18705 100644 --- a/simulator/src/test/vmtst.test.ts +++ b/simulator/src/test/vmtst.test.ts @@ -1,11 +1,11 @@ -import { VM_PROJECTS } from "@nand2tetris/projects/base.js"; -import { resetFiles } from "@nand2tetris/projects/full.js"; import { FileSystem, ObjectFileSystemAdapter, } from "@davidsouther/jiffies/lib/esm/fs.js"; -import { TST } from "../languages/tst.js"; import { unwrap } from "@davidsouther/jiffies/lib/esm/result.js"; +import { VM_PROJECTS } from "@nand2tetris/projects/base.js"; +import { resetFiles } from "@nand2tetris/projects/full.js"; +import { TST } from "../languages/tst.js"; import { VMTest } from "./vmtst.js"; async function prepare(project: "07" | "08", name: string): Promise { @@ -14,7 +14,7 @@ async function prepare(project: "07" | "08", name: string): Promise { fs.cd(`/projects/${project}/${name}`); const vm_tst = await fs.readFile(name + "VME.tst"); const tst = unwrap(TST.parse(vm_tst)); - const test = VMTest.from(tst).using(fs); + const test = unwrap(VMTest.from(tst)).using(fs); await test.load(); return test; } diff --git a/simulator/src/test/vmtst.ts b/simulator/src/test/vmtst.ts index 23c8e3c33..b3ecfe08c 100644 --- a/simulator/src/test/vmtst.ts +++ b/simulator/src/test/vmtst.ts @@ -1,12 +1,14 @@ +import { assertExists } from "@davidsouther/jiffies/lib/esm/assert.js"; import { FileSystem } from "@davidsouther/jiffies/lib/esm/fs.js"; +import { Result } from "@davidsouther/jiffies/lib/esm/result.js"; import { RAM } from "../cpu/memory.js"; import { Tst } from "../languages/tst.js"; import { Segment } from "../languages/vm.js"; +import { Action, AsyncAction } from "../types.js"; import { Vm } from "../vm/vm.js"; import { fill } from "./builder.js"; import { TestInstruction } from "./instruction.js"; import { Test } from "./tst.js"; -import { Action } from "../types.js"; export interface VmFile { name: string; @@ -16,21 +18,36 @@ export interface VmFile { export class VMTest extends Test { vm: Vm = new Vm(); - private loadAction?: (files: VmFile[]) => void; - private dir?: string; + private doLoad?: AsyncAction; static from( tst: Tst, - path?: string, - loadAction?: (files: VmFile[]) => void, - doEcho?: Action, - ): VMTest { - const test = new VMTest(doEcho); - test.dir = path?.split("/").slice(0, -1).join("/"); - test.loadAction = loadAction; + options: { + dir?: string; + doLoad?: AsyncAction; + doEcho?: Action; + compareTo?: Action; + } = {}, + ): Result { + const test = new VMTest(options); return fill(test, tst); } + constructor({ + dir, + doEcho, + doLoad, + compareTo, + }: { + dir?: string; + doEcho?: Action; + doLoad?: AsyncAction; + compareTo?: Action; + } = {}) { + super(dir, doEcho, compareTo); + this.doLoad = doLoad; + } + using(fs: FileSystem): this { this.fs = fs; return this; @@ -41,6 +58,15 @@ export class VMTest extends Test { return this; } + override async load(filename?: string): Promise { + if (!this.dir) return; + const dir = assertExists(this.dir?.split("/").slice(0, -1).join("/")); + const vm = await this.doLoad?.(filename ? `${dir}/${filename}` : dir); + if (vm) { + this.vm = vm; + } + } + hasVar(variable: string | number, index?: number): boolean { if (typeof variable !== "string") { index = variable; @@ -122,30 +148,6 @@ export class VMTest extends Test { vmstep(): void { this.vm.step(); } - - override async load(filename?: string) { - if (!this.loadAction) { - return; - } - if (filename) { - const file = await this.fs.readFile( - `${this.dir ? `${this.dir}/` : ""}${filename}`, - ); - this.loadAction?.([{ name: filename.replace(".vm", ""), content: file }]); - } else { - const stats = await this.fs.scandir(this.dir ?? "/"); - const files: VmFile[] = []; - for (const stat of stats) { - if (stat.isFile() && stat.name.endsWith(".vm")) { - const file = await this.fs.readFile( - `${this.dir ? `${this.dir}/` : ""}${stat.name}`, - ); - files.push({ name: stat.name.replace(".vm", ""), content: file }); - } - } - this.loadAction(files); - } - } } export interface VMTestInstruction extends TestInstruction { diff --git a/simulator/src/types.ts b/simulator/src/types.ts index 89e600fb7..babadae1e 100644 --- a/simulator/src/types.ts +++ b/simulator/src/types.ts @@ -1 +1,2 @@ export type Action = (value: T) => void; +export type AsyncAction = (value: T) => Promise; diff --git a/web/package.json b/web/package.json index edcbbbe28..b59955fab 100644 --- a/web/package.json +++ b/web/package.json @@ -99,7 +99,8 @@ }, "jest": { "moduleNameMapper": { - "^@nand2tetris/([^/]+)/(.*)": "/../node_modules/@nand2tetris/$1/build/$2" + "^@nand2tetris/([^/]+)/(.*)": "/../node_modules/@nand2tetris/$1/build/$2", + "\\.css$": "identity-obj-proxy" }, "transformIgnorePatterns": [ "node_modules/(?!@davidsouther)" diff --git a/web/src/App.tsx b/web/src/App.tsx index 632b90b65..8964f9ad4 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -46,6 +46,7 @@ function App() { }, []); useEffect(() => { + if (baseContext.localFsRoot) return; fs.stat("/projects/01/Not.hdl").catch(async () => { await loaders.resetFiles(fs); }); diff --git a/web/src/Page.context.tsx b/web/src/Page.context.tsx index 70078f547..307745028 100644 --- a/web/src/Page.context.tsx +++ b/web/src/Page.context.tsx @@ -1,11 +1,20 @@ import { useAsmPageStore } from "@nand2tetris/components/stores/asm.store"; +import { BaseContext } from "@nand2tetris/components/stores/base.context"; import { useChipPageStore } from "@nand2tetris/components/stores/chip.store"; import { useCompilerPageStore } from "@nand2tetris/components/stores/compiler.store"; import { useCpuPageStore } from "@nand2tetris/components/stores/cpu.store"; import { useVmPageStore } from "@nand2tetris/components/stores/vm.store"; -import { ReactNode, createContext, useEffect, useState } from "react"; +import { + ReactNode, + createContext, + useContext, + useEffect, + useState, +} from "react"; export function usePageContext() { + const { fs } = useContext(BaseContext); + const [title, setTitle] = useState(); const [tool, setTool] = useState(); @@ -17,7 +26,7 @@ export function usePageContext() { useEffect(() => { chip.actions.initialize(); - }, [chip.actions]); + }, [chip.actions, fs]); useEffect(() => { vm.actions.initialize(); @@ -25,6 +34,9 @@ export function usePageContext() { useEffect(() => { switch (tool) { + case "chip": + setTitle(chip.state.title); + break; case "cpu": setTitle(cpu.state.title); break; @@ -32,7 +44,6 @@ export function usePageContext() { setTitle(asm.state.title); break; case "vm": - ``; setTitle(vm.state.title); break; case "compiler": @@ -44,6 +55,7 @@ export function usePageContext() { } }, [ tool, + chip.state.title, cpu.state.title, asm.state.title, vm.state.title, diff --git a/web/src/pages/asm.tsx b/web/src/pages/asm.tsx index 86085503a..f47c78537 100644 --- a/web/src/pages/asm.tsx +++ b/web/src/pages/asm.tsx @@ -39,7 +39,7 @@ export const Asm = () => { override async tick(): Promise { sourceCursorPos.current = 0; resultCursorPos.current = 0; - return actions.step(); + return await actions.step(); } override reset(): void { actions.reset(); diff --git a/web/src/pages/chip.tsx b/web/src/pages/chip.tsx index 50cafe212..4048ab103 100644 --- a/web/src/pages/chip.tsx +++ b/web/src/pages/chip.tsx @@ -20,12 +20,7 @@ import { } from "@nand2tetris/components/pinout.js"; import { useStateInitializer } from "@nand2tetris/components/react.js"; import { BaseContext } from "@nand2tetris/components/stores/base.context.js"; -import { - Files, - PROJECT_NAMES, - isBuiltinOnly, -} from "@nand2tetris/components/stores/chip.store.js"; -import { CHIP_PROJECTS } from "@nand2tetris/projects/base.js"; +import { hasBuiltinChip } from "@nand2tetris/simulator/chip/builtins/index.js"; import { HDL } from "@nand2tetris/simulator/languages/hdl.js"; import { Timer } from "@nand2tetris/simulator/timer.js"; import { TestPanel } from "src/shell/test_panel"; @@ -33,18 +28,32 @@ import { AppContext } from "../App.context"; import { PageContext } from "../Page.context"; import { Editor } from "../shell/editor"; import { Accordian, Panel } from "../shell/panel"; -import { zip } from "../shell/zip"; + +interface CompileInput { + hdl: string; + tst: string; + cmp: string; + tstDir: string; +} export const Chip = () => { - const { setStatus } = useContext(BaseContext); + const { setStatus, localFsRoot } = useContext(BaseContext); const { stores, setTool } = useContext(PageContext); - const { tracking } = useContext(AppContext); + const { tracking, filePicker } = useContext(AppContext); const { state, actions, dispatch } = stores.chip; const [hdl, setHdl] = useStateInitializer(state.files.hdl); const [tst, setTst] = useStateInitializer(state.files.tst); const [cmp, setCmp] = useStateInitializer(state.files.cmp); const [out, setOut] = useStateInitializer(state.files.out); + const [tstDir, setTstDir] = useStateInitializer(state.dir); + const [tstPath, setTstPath] = useState(); + + useEffect(() => { + if (tstPath) { + setTstDir(tstPath?.split("/").slice(0, -1).join("/")); + } + }, [tstPath]); useEffect(() => { setTool("chip"); @@ -59,43 +68,31 @@ export const Chip = () => { tracking.trackEvent("action", "setChip", state.controls.chipName); }, []); - const setProject = useCallback( - (project: keyof typeof CHIP_PROJECTS) => { - actions.setProject(project); - tracking.trackEvent("action", "setProject", project); - }, - [actions, tracking], - ); - - const setChip = useCallback( - (chip: string) => { - actions.setChip(chip); - tracking.trackEvent("action", "setChip", chip); - pinResetDispatcher.reset(); - }, - [actions, tracking], - ); - const doEval = useCallback(() => { actions.eval(); tracking.trackEvent("action", "eval"); }, [actions, tracking]); - const compile = useRef<(files?: Partial) => void>(() => undefined); - compile.current = async (files: Partial = {}) => { - const hdlToCompile = - useBuiltin || state.controls.builtinOnly ? files.hdl : (files.hdl ?? hdl); + const compile = useRef<(files?: Partial) => void>( + () => undefined, + ); + compile.current = async (files: Partial = {}) => { + const hdlToCompile = state.controls.usingBuiltin + ? files.hdl + : files.hdl ?? hdl; + await actions.updateFiles({ hdl: hdlToCompile, tst: files.tst ?? tst, cmp: files.cmp ?? cmp, + tstPath: files.tstDir ?? tstDir, }); }; useEffect(() => { - compile.current({ tst, cmp }); + compile.current({ tst, cmp, tstDir }); actions.reset(); - }, [tst, cmp]); + }, [tst, cmp, tstDir]); const runner = useRef(); useEffect(() => { @@ -138,84 +135,52 @@ export const Chip = () => { [actions], ); - const downloadRef = useRef(null); - - const downloadProject = async () => { - if (!downloadRef.current) { - return; - } - - const files = await actions.getProjectFiles(); - const url = await zip(files); - downloadRef.current.href = url; - downloadRef.current.download = `${state.controls.project}`; - downloadRef.current.click(); - - URL.revokeObjectURL(url); - }; - - const [useBuiltin, setUseBuiltin] = useState(false); const toggleUseBuiltin = () => { - if (useBuiltin) { - setUseBuiltin(false); - actions.useBuiltin(false); - } else { - setUseBuiltin(true); - actions.useBuiltin(true, hdl); - } + actions.toggleBuiltin(); pinResetDispatcher.reset(); }; + const loadFile = async () => { + const path = await filePicker.select({ suffix: "hdl" }); + actions.loadChip(path); + }; + const selectors = ( <>
- - -
); @@ -225,20 +190,24 @@ export const Chip = () => { isEditorPanel={true} header={ <> -
HDL
-
+
+ HDL +
+ {hasBuiltinChip(state.controls.chipName) && ( -
+ )} {selectors} +
+ +
} > @@ -248,16 +217,14 @@ export const Chip = () => { error={state.controls.error} onChange={async (source) => { setHdl(source); - if (!useBuiltin) { - await actions.saveChip(source); + if (!state.controls.usingBuiltin) { + actions.saveChip(source); } - compile.current( - useBuiltin || state.controls.builtinOnly ? {} : { hdl: source }, - ); + compile.current(state.controls.usingBuiltin ? {} : { hdl: source }); }} grammar={HDL.parser} language={"hdl"} - disabled={useBuiltin || state.controls.builtinOnly} + disabled={state.controls.usingBuiltin} /> ); @@ -348,7 +315,7 @@ export const Chip = () => { sim={state.sim} toggle={actions.toggle} setInputValid={setInputValid} - hideInternal={state.controls.builtinOnly || useBuiltin} + hideInternal={state.controls.usingBuiltin} /> {visualizations.length > 0 && ( @@ -365,13 +332,12 @@ export const Chip = () => { 1 ? (