From 6b249eb53a93034ca9afcaf849b7cdc6fb07833c Mon Sep 17 00:00:00 2001 From: Neta London Date: Sun, 7 Jul 2024 17:12:57 +0300 Subject: [PATCH 01/47] Change hardware simulator to use local file system API --- components/src/stores/base.context.ts | 48 ++- components/src/stores/chip.store.test.ts | 8 +- components/src/stores/chip.store.ts | 412 ++++++++++++----------- components/src/stores/vm.store.ts | 2 +- extension/views/hdl/src/App.tsx | 28 +- extension/views/hdl/src/index.tsx | 2 +- simulator/src/chip/builder.ts | 1 + web/src/Page.context.tsx | 7 +- web/src/pages/chip.tsx | 242 ++++++------- web/src/shell/settings.tsx | 35 +- 10 files changed, 407 insertions(+), 378 deletions(-) diff --git a/components/src/stores/base.context.ts b/components/src/stores/base.context.ts index de44b5411..28059d954 100644 --- a/components/src/stores/base.context.ts +++ b/components/src/stores/base.context.ts @@ -10,7 +10,6 @@ import { useState, } from "react"; import { - ChainedFileSystemAdapter, FileSystemAccessFileSystemAdapter, openNand2TetrisDirectory, } from "./base/fs.js"; @@ -23,10 +22,11 @@ import { Action } from "@nand2tetris/simulator/types.js"; export interface BaseContext { fs: FileSystem; + localFs?: FileSystem; + localFsRoot?: string; canUpgradeFs: boolean; upgradeFs: (force?: boolean) => void; closeFs: () => void; - upgraded?: string; status: string; setStatus: Action; storage: Record; @@ -34,61 +34,57 @@ export interface BaseContext { export function useBaseContext(): BaseContext { const localAdapter = useMemo(() => new LocalStorageFileSystemAdapter(), []); - const [fs, setFs] = useState(new FileSystem(localAdapter)); - const [upgraded, setUpgraded] = useState(); + const fs = new FileSystem(localAdapter); + const [localFs, setLocalFs] = useState(); + const [root, setRoot] = useState(); - const replaceFs = useCallback( + const replaceLocalFs = useCallback( (handle: FileSystemDirectoryHandle) => { - const newFs = new FileSystem( - new ChainedFileSystemAdapter( - new FileSystemAccessFileSystemAdapter(handle), - localAdapter, - ), - ); - newFs.cd(fs.cwd()); - setFs(newFs); - setUpgraded(handle.name); + setLocalFs(new FileSystem(new FileSystemAccessFileSystemAdapter(handle))); + setRoot(handle.name); }, - [setFs, setUpgraded], + [setRoot, setLocalFs], ); useEffect(() => { - if (upgraded) return; + if (root) return; attemptLoadAdapterFromIndexedDb().then((adapter) => { if (!adapter) return; - replaceFs(adapter); + replaceLocalFs(adapter); }); - }, [upgraded, replaceFs]); + }, [root, replaceLocalFs]); const canUpgradeFs = `showDirectoryPicker` in window; + const upgradeFs = useCallback( async (force = false) => { - if (!canUpgradeFs || (upgraded && !force)) return; + if (!canUpgradeFs || (root && !force)) return; const handler = await openNand2TetrisDirectory(); const adapter = await createAndStoreLocalAdapterInIndexedDB(handler); - replaceFs(adapter); + replaceLocalFs(adapter); }, - [upgraded, replaceFs], + [root, replaceLocalFs], ); const closeFs = useCallback(async () => { - if (!upgraded) return; + if (!root) return; await removeLocalAdapterFromIndexedDB(); - setFs(new FileSystem(localAdapter)); - setUpgraded(undefined); - }, [upgraded, localAdapter]); + setRoot(undefined); + setLocalFs(undefined); + }, [root]); const [status, setStatus] = useState(""); return { fs, + localFs, + localFsRoot: root, status, setStatus, storage: localStorage, canUpgradeFs, upgradeFs, closeFs, - upgraded, }; } diff --git a/components/src/stores/chip.store.test.ts b/components/src/stores/chip.store.test.ts index 1b02204e8..c3c3e65d8 100644 --- a/components/src/stores/chip.store.test.ts +++ b/components/src/stores/chip.store.test.ts @@ -64,7 +64,7 @@ describe("ChipStore", () => { "projects/01/Not/Not.cmp": not.cmp, }); - await store.actions.initialize(); + // await store.actions.initialize(); expect(store.state.controls.project).toBe("01"); expect(store.state.controls.chipName).toBe("Not"); @@ -100,7 +100,7 @@ describe("ChipStore", () => { it.todo("loads projects and chips"); it("toggles bits", async () => { - await state.store.actions.initialize(); + // 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({ @@ -126,7 +126,7 @@ describe("ChipStore", () => { "projects/01/Not/Not.tst": not.tst, "projects/01/Not/Not.cmp": not.cmp, }); - await store.actions.initialize(); + // await store.actions.initialize(); return { store }; }, beforeEach); @@ -139,7 +139,7 @@ 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]]); diff --git a/components/src/stores/chip.store.ts b/components/src/stores/chip.store.ts index 077af9ee5..0893d7d6f 100644 --- a/components/src/stores/chip.store.ts +++ b/components/src/stores/chip.store.ts @@ -1,5 +1,4 @@ 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"; @@ -9,10 +8,6 @@ import { 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,15 +19,17 @@ 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 { 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 { 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 { compare } from "../compare.js"; +import { RunSpeed } from "../runbar.js"; import { BaseContext } from "./base.context.js"; import { Action } from "@nand2tetris/simulator/types.js"; @@ -59,25 +56,6 @@ function findDropdowns(storage: Record) { 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|`; -} - export function isBuiltinOnly( project: keyof typeof CHIP_PROJECTS, chipName: string, @@ -85,39 +63,12 @@ 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][`${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; @@ -151,8 +102,7 @@ export interface ControlsState { chipName: string; tests: string[]; testName: string; - hasBuiltin: boolean; - builtinOnly: boolean; + usingBuiltin: boolean; runningTest: boolean; span?: Span; error?: CompilationError; @@ -190,13 +140,13 @@ export function makeChipStore( dispatch: MutableRefObject, ) { const dropdowns = findDropdowns(storage); - let { project, chipName } = dropdowns; const { chips } = dropdowns; let chip = new Low(); - let tests: string[] = []; + let hdlPath: string; + let backupHdl = ""; + const tests: string[] = []; let test = new ChipTest(); let usingBuiltin = false; - let builtinOnly = false; let invalid = false; const reducers = { @@ -251,13 +201,15 @@ export function makeChipStore( }, setChip(state: ChipPageState, chipName: string) { + storage["/chip/chip"] = chipName; state.controls.chipName = chipName; - state.controls.tests = Array.from(tests); - state.controls.hasBuiltin = REGISTRY.has(chipName); - state.controls.builtinOnly = isBuiltinOnly( - state.controls.project, - 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, + // ); }, setTest(state: ChipPageState, testName: string) { @@ -308,48 +260,61 @@ 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]); - }, - - async setChip(chip: string, project = storage["/chip/project"] ?? "01") { - chipName = storage["/chip/chip"] = chip; - builtinOnly = isBuiltinOnly( - project as keyof typeof CHIP_PROJECTS, - chipName, - ); - - if (builtinOnly) { - this.useBuiltin(); + toggleBuiltin(state: ChipPageState) { + state.controls.usingBuiltin = usingBuiltin; + if (usingBuiltin) { + console.log("backing up", state.files.hdl); + backupHdl = state.files.hdl; + this.setFiles(state, { + hdl: convertToBuiltin(state.controls.chipName, state.files.hdl), + }); } 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] - : [], - ), - }); - - this.loadTest(test); - }, + 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]); + // }, + + // async setChip(chip: string, project = storage["/chip/project"] ?? "01") { + + // builtinOnly = isBuiltinOnly( + // project as keyof typeof CHIP_PROJECTS, + // chipName, + // ); + + // if (builtinOnly) { + // this.useBuiltin(); + // } else { + // await this.loadChip(project, chipName); + // if (usingBuiltin) { + // this.useBuiltin(); + // } + // } + // 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] + // : [], + // ), + // }); + + // this.loadTest(test); + // }, reset() { Clock.get().reset(); @@ -386,9 +351,9 @@ export function makeChipStore( } }, - async compileChip(hdl: string) { + async compileChip(hdl: string, name?: string) { chip.remove(); - const maybeChip = await parseChip(hdl, chipName); + const maybeChip = await parseChip(hdl, name ?? storage["/chip/chip"]); if (isErr(maybeChip)) { const error = Err(maybeChip); setStatus(Err(maybeChip).message); @@ -421,45 +386,45 @@ export function makeChipStore( }, async loadChip(project: string, name: string) { - storage["/chip/chip"] = name; - const fsName = (ext: string) => - `/projects/${project}/${name}/${name}.${ext}`; - - const hdl = await fs.readFile(fsName("hdl")).catch(() => makeHdl(name)); - - dispatch.current({ action: "setFiles", payload: { hdl } }); - await this.compileChip(hdl); - }, + // storage["/chip/chip"] = name; + // const fsName = (ext: string) => + // `/projects/${project}/${name}/${name}.${ext}`; + // const hdl = await fs.readFile(fsName("hdl")).catch(() => makeHdl(name)); + // dispatch.current({ action: "setFiles", payload: { hdl } }); + // await this.compileChip(hdl); + }, + + async initializeTest(project: string, chip: string) { + // TODO: CPU, Computer have test with different names + // tests = (await fs.scandir(`/projects/${project}/${name}`)) + // .filter((file) => file.name.endsWith(".tst")) + // .map((file) => file.name); + // if (tests.length > 0) { + // await this.setTest(tests[0]); + // } + }, + + async loadTest(project: string, name: string) { + if (!fs) return; + try { + const tst = await fs.readFile(`${project}/${name}.tst`); + const cmp = await fs.readFile(`${project}/${name}.cmp`); - async initializeTest(name: string) { - tests = (await fs.scandir(`/projects/${project}/${name}`)) - .filter((file) => file.name.endsWith(".tst")) - .map((file) => file.name); - if (tests.length > 0) { - await this.setTest(tests[0]); + dispatch.current({ action: "setFiles", payload: { cmp, tst } }); + this.compileTest(tst); + } catch (e) { + console.error(e); } - }, - - async loadTest(test: string) { - const [tst, cmp] = await Promise.all([ - fs - .readFile(`/projects/${project}/${chipName}/${test}`) - .catch(() => makeTst()), - fs - .readFile( - `/projects/${project}/${chipName}/${test}`.replace(".tst", ".cmp"), - ) - .catch(() => makeCmp()), - ]); - dispatch.current({ action: "setFiles", payload: { cmp, tst } }); - this.compileTest(tst); - }, - - async saveChip(hdl: string, prj = project, name = chipName) { - dispatch.current({ action: "setFiles", payload: { hdl } }); - const path = `/projects/${prj}/${name}/${name}.hdl`; - await fs.writeFile(path, hdl); - setStatus(`Saved ${path}`); + // const [tst, cmp] = await Promise.all([ + // fs + // .readFile(`/projects/${project}/${chipName}/${test}`) + // .catch(() => makeTst()), + // fs + // .readFile( + // `/projects/${project}/${chipName}/${test}`.replace(".tst", ".cmp"), + // ) + // .catch(() => makeCmp()), + // ]); }, toggle(pin: Pin, i: number | undefined) { @@ -488,18 +453,9 @@ 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 = storage["/chip/chip"]; + console.log(`loading builtin ${builtinName}`); const nextChip = await getBuiltinChip(builtinName); if (isErr(nextChip)) { setStatus( @@ -507,49 +463,48 @@ 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 } }); + // dispatch.current({ action: "setFiles", payload: { hdl } }); this.replaceChip(Ok(nextChip)); }, - async initialize() { - await this.setChip(chipName, project); + async toggleBuiltin() { + usingBuiltin = !usingBuiltin; + dispatch.current({ action: "toggleBuiltin" }); + if (usingBuiltin) { + await this.loadBuiltin(); + } else { + await this.compileChip(backupHdl); + } }, + // 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); + // test.setFileSystem(fs); dispatch.current({ action: "updateTestStep" }); return true; }, async runTest(file: string) { - if (!this.compileTest(file)) { - return; - } - dispatch.current({ action: "testRunning" }); - - fs.pushd("/samples"); - await test.run(); - fs.popd(); - - dispatch.current({ action: "updateTestStep" }); - dispatch.current({ action: "testFinished" }); + // if (!this.compileTest(file)) { + // return; + // } + // dispatch.current({ action: "testRunning" }); + // fs.pushd("/samples"); + // await test.run(); + // fs.popd(); + // dispatch.current({ action: "updateTestStep" }); + // dispatch.current({ action: "testFinished" }); }, tick(): Promise { @@ -567,32 +522,94 @@ export function makeChipStore( }, async resetFile() { - const { ChipProjects } = await import("@nand2tetris/projects/full.js"); - const template = ( - ChipProjects[project].CHIPS as Record> - )[chipName][`${chipName}.hdl`]; - dispatch.current({ action: "setFiles", payload: { hdl: template } }); + // const { ChipProjects } = await import("@nand2tetris/projects/full.js"); + // const template = ( + // ChipProjects[project].CHIPS as Record> + // )[chipName][`${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}/${chip}.hdl`), - })), - ); + // return await Promise.all( + // CHIP_PROJECTS[project].map((chip) => ({ + // name: `${chip}.hdl`, + // content: fs.readFile(`/projects/${project}/${chip}/${chip}.hdl`), + // })), + // ); + }, + + // TODO: optimize. Maybe create a mapping in initialization + async findPath( + fs: FileSystem, + name: string, + ): Promise { + if (!fs) return; + + async function findIn( + fs: FileSystem, + path: string[] = [], + ): Promise { + const fullPath = path.length == 0 ? "." : path.join("/"); + for (const entry of await fs.scandir(fullPath)) { + if (entry.isDirectory()) { + const found = await findIn(fs, [...path, entry.name]); + if (found) { + return [...path, ...found]; + } + } else { + if (entry.name == name) { + return [...path, name]; + } + } + } + return; + } + + return await findIn(fs); + }, + + // TODO: currently doesn't support 2 chips with the same name in different projects + async loadLocalChip(handle: FileSystemFileHandle) { + if (!fs) return; + const path = await this.findPath(fs, handle.name); + if (!path) { + // TODO: turn into warning? + setStatus(`${handle.name} is not inside the projects directory`); + return; + } + + usingBuiltin = false; + + const name = handle.name.replace(".hdl", ""); + const hdl = await fs.readFile(path.join("/")); + + hdlPath = path.join("/"); + dispatch.current({ action: "setChip", payload: name }); + dispatch.current({ action: "setFiles", payload: { hdl } }); + await this.compileChip(hdl, name); + + // TODO: replace this with initializeTests for chips with multiple tests + await this.loadTest(path[0], name); + }, + + async saveChip(hdl: string) { + dispatch.current({ action: "setFiles", payload: { hdl } }); + if (fs && hdlPath) { + await fs.writeFile(hdlPath, hdl); + } }, }; const initialState: ChipPageState = (() => { const controls: ControlsState = { - project, + project: "01", chips, - chipName, + chipName: "", tests, testName: "", - hasBuiltin: REGISTRY.has(chipName), - builtinOnly: isBuiltinOnly(project, chipName), + // hasBuiltin: false, + // builtinOnly: false, + usingBuiltin: false, runningTest: false, error: undefined, visualizationParameters: new Set(), @@ -607,6 +624,7 @@ export function makeChipStore( cmp: "", tst: "", out: "", + backupHdl: "", }, sim, config: { speed: 2 }, @@ -617,13 +635,13 @@ export function makeChipStore( } export function useChipPageStore() { - const { fs, setStatus, storage } = useContext(BaseContext); + const { localFs, setStatus, storage } = useContext(BaseContext); const dispatch = useRef(() => undefined); const { initialState, reducers, actions } = useMemo( - () => makeChipStore(fs, setStatus, storage, dispatch), - [fs, setStatus, storage, dispatch], + () => makeChipStore(localFs, setStatus, storage, dispatch), + [localFs, setStatus, storage, dispatch], ); const [state, dispatcher] = useImmerReducer(reducers, initialState); diff --git a/components/src/stores/vm.store.ts b/components/src/stores/vm.store.ts index cafef4ccb..d2b359d99 100644 --- a/components/src/stores/vm.store.ts +++ b/components/src/stores/vm.store.ts @@ -288,7 +288,7 @@ 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; diff --git a/extension/views/hdl/src/App.tsx b/extension/views/hdl/src/App.tsx index 21d11aa2a..2db5ebec1 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,21 +12,15 @@ 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]); + // useEffect(() => { + // actions.initialize(); + // }, [actions]); const [hdl, setHdl] = useState(Not.hdl); const [loaded, setLoaded] = useState(false); @@ -30,14 +30,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 +50,7 @@ function App() { }); setLoaded(true); }, - [compile, dispatch] + [compile, dispatch], ); useEffect(() => { @@ -70,7 +70,7 @@ function App() { compile(hdl); setUseBuiltin(false); } else { - actions.useBuiltin(); + actions.toggleBuiltin(); setUseBuiltin(true); } }; @@ -84,7 +84,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..d7132a6e6 100644 --- a/extension/views/hdl/src/index.tsx +++ b/extension/views/hdl/src/index.tsx @@ -12,7 +12,7 @@ const baseContext: BaseContext = { fs: new FileSystem( new ObjectFileSystemAdapter({ "projects/01/Not.hdl": Not.hdl }) ), - upgraded: "true", + // upgraded: "true", canUpgradeFs: false, upgradeFs() {}, closeFs() {}, diff --git a/simulator/src/chip/builder.ts b/simulator/src/chip/builder.ts index 5a7878bb6..c180d1535 100644 --- a/simulator/src/chip/builder.ts +++ b/simulator/src/chip/builder.ts @@ -200,6 +200,7 @@ class ChipBuilder { async build() { if (this.expectedName && this.parts.name.value != this.expectedName) { + console.log(this.parts.name.value, this.expectedName); return Err(createError(`Wrong chip name`, this.parts.name.span)); } diff --git a/web/src/Page.context.tsx b/web/src/Page.context.tsx index 70078f547..65cc160be 100644 --- a/web/src/Page.context.tsx +++ b/web/src/Page.context.tsx @@ -16,7 +16,7 @@ export function usePageContext() { const compiler = useCompilerPageStore(); useEffect(() => { - chip.actions.initialize(); + // chip.actions.initialize(); }, [chip.actions]); useEffect(() => { @@ -25,6 +25,9 @@ export function usePageContext() { useEffect(() => { switch (tool) { + case "chip": + setTitle(chip.state.title); + break; case "cpu": setTitle(cpu.state.title); break; @@ -32,7 +35,6 @@ export function usePageContext() { setTitle(asm.state.title); break; case "vm": - ``; setTitle(vm.state.title); break; case "compiler": @@ -44,6 +46,7 @@ export function usePageContext() { } }, [ tool, + chip.state.title, cpu.state.title, asm.state.title, vm.state.title, diff --git a/web/src/pages/chip.tsx b/web/src/pages/chip.tsx index 213fce2d7..e50fd6d3f 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 { Files } from "@nand2tetris/components/stores/chip.store.js"; import { HDL } from "@nand2tetris/simulator/languages/hdl.js"; import { Timer } from "@nand2tetris/simulator/timer.js"; import { TestPanel } from "src/shell/test_panel"; @@ -33,7 +28,6 @@ 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"; export const Chip = () => { const { setStatus } = useContext(BaseContext); @@ -59,22 +53,22 @@ 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 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(); @@ -83,8 +77,9 @@ export const Chip = () => { const compile = useRef<(files?: Partial) => void>(() => undefined); compile.current = async (files: Partial = {}) => { - const hdlToCompile = - useBuiltin || state.controls.builtinOnly ? files.hdl : files.hdl ?? hdl; + const hdlToCompile = state.controls.usingBuiltin + ? files.hdl + : files.hdl ?? hdl; await actions.updateFiles({ hdl: hdlToCompile, tst: files.tst ?? tst, @@ -138,107 +133,118 @@ export const Chip = () => { [actions], ); - const downloadRef = useRef(null); + // const downloadRef = useRef(null); - const downloadProject = async () => { - if (!downloadRef.current) { - return; - } + // 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(); + // 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); - }; + // URL.revokeObjectURL(url); + // }; - const [useBuiltin, setUseBuiltin] = useState(false); + // const [useBuiltin, setUseBuiltin] = useState(false); const toggleUseBuiltin = () => { - if (useBuiltin) { - setUseBuiltin(false); - actions.useBuiltin(false); - } else { - setUseBuiltin(true); - actions.useBuiltin(true, hdl); - } + actions.toggleBuiltin(); + // if (useBuiltin) { + // setUseBuiltin(false); + // actions.useBuiltin(false); + // } else { + // setUseBuiltin(true); + // actions.useBuiltin(true, hdl); + // } pinResetDispatcher.reset(); }; - const selectors = ( - <> -
- - - - - -
- - ); + const loadFile = async () => { + const [handle] = await window.showOpenFilePicker(); + actions.loadLocalChip(handle); + }; + + // const selectors = ( + // <> + //
+ // + // + // + + // + //
+ // + // ); const hdlPanel = ( -
HDL
-
- +
+ HDL +
+ {/*
*/} + + {/*
*/} +
+
- {selectors} + {/* {selectors} */} } > @@ -248,16 +254,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 +352,7 @@ export const Chip = () => { sim={state.sim} toggle={actions.toggle} setInputValid={setInputValid} - hideInternal={state.controls.builtinOnly || useBuiltin} + hideInternal={state.controls.usingBuiltin} /> {visualizations.length > 0 && ( @@ -371,7 +375,7 @@ export const Chip = () => { - Builtin - + {state.controls.chipName != "" && ( + + )} {/*
*/}
@@ -369,12 +371,13 @@ export const Chip = () => { 1 ? ( { - // setProject(value as keyof typeof CHIP_PROJECTS); - // }} - // data-testid="project-picker" - // > - // {PROJECT_NAMES.map(([number, label]) => ( - // - // ))} - // - // - // - - // - //
- // - // ); + const selectors = ( + <> +
+ + +
+ + ); const hdlPanel = ( {
HDL
- {/*
*/} - {state.controls.chipName != "" && ( + {hasBuiltinChip(state.controls.chipName) && ( )} - {/*
*/} + {selectors}
- {/* {selectors} */} } > diff --git a/web/src/shell/settings.tsx b/web/src/shell/settings.tsx index 0c98d4b04..4cd367942 100644 --- a/web/src/shell/settings.tsx +++ b/web/src/shell/settings.tsx @@ -212,14 +212,16 @@ export const Settings = () => { ) : ( <> )} - {/* - + )} + {/* */} From 6d6893bd376014063e244bf92d384bd2dd301717 Mon Sep 17 00:00:00 2001 From: Neta London Date: Sun, 21 Jul 2024 12:45:50 +0300 Subject: [PATCH 15/47] Fix change local fs --- components/src/stores/base.context.ts | 4 ++-- web/src/shell/settings.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/src/stores/base.context.ts b/components/src/stores/base.context.ts index 259a3c0d2..cf2da7706 100644 --- a/components/src/stores/base.context.ts +++ b/components/src/stores/base.context.ts @@ -24,7 +24,7 @@ export interface BaseContext { fs: FileSystem; localFsRoot?: string; canUpgradeFs: boolean; - upgradeFs: (force?: boolean) => void; + upgradeFs: (force?: boolean) => Promise; closeFs: () => void; status: string; setStatus: Action; @@ -89,7 +89,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/web/src/shell/settings.tsx b/web/src/shell/settings.tsx index 4cd367942..7bf799a1a 100644 --- a/web/src/shell/settings.tsx +++ b/web/src/shell/settings.tsx @@ -177,7 +177,7 @@ export const Settings = () => { onClick={async () => { setUpgrading(true); try { - await upgradeFs(); + await upgradeFs(localFsRoot != undefined); } catch (err) { console.error("Failed to upgrade FS", { err }); setStatus(t`Failed to load local file system.`); From 9389f52cb589cfc92ebb6f79db13065f3608ce75 Mon Sep 17 00:00:00 2001 From: Neta London Date: Sun, 21 Jul 2024 12:50:53 +0300 Subject: [PATCH 16/47] Cleanup old code --- components/src/stores/chip.store.ts | 305 ++++++++-------------------- web/src/pages/chip.tsx | 48 ----- 2 files changed, 85 insertions(+), 268 deletions(-) diff --git a/components/src/stores/chip.store.ts b/components/src/stores/chip.store.ts index bf07d2772..10e338d6e 100644 --- a/components/src/stores/chip.store.ts +++ b/components/src/stores/chip.store.ts @@ -316,6 +316,14 @@ export function makeChipStore( 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) { project = storage["/chip/project"] = project; dispatch.current({ action: "setProject", payload: project }); @@ -328,75 +336,23 @@ export function makeChipStore( dispatch.current({ action: "setChips", payload: chips }); }, - // async setChip(chip: string, project = storage["/chip/project"] ?? "01") { - - // builtinOnly = isBuiltinOnly( - // project as keyof typeof CHIP_PROJECTS, - // chipName, - // ); - - // if (builtinOnly) { - // this.useBuiltin(); - // } else { - // await this.loadChip(project, chipName); - // if (usingBuiltin) { - // this.useBuiltin(); - // } - // } - // 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] - // : [], - // ), - // }); - - // this.loadTest(test); - // }, + async loadChip(path: string) { + usingBuiltin = false; + dispatch.current({ action: "updateUsingBuiltin", payload: false }); - reset() { - Clock.get().reset(); - chip.reset(); - test.reset(); - dispatch.current({ action: "setFiles", payload: {} }); - dispatch.current({ action: "updateChip" }); - }, + const hdl = await fs.readFile(path); - 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(display(e)); - } - dispatch.current({ action: "updateChip", payload: { invalid: invalid } }); - if (!invalid) { - setStatus(`HDL code: No syntax errors`); - } + const parts = path.split("/"); + const name = parts.pop()!.replace(".hdl", ""); + const dir = parts.join("/"); + + await this.compileChip(hdl, dir, name); + await this.initializeTests(dir, name); + dispatch.current({ + action: "setChip", + payload: { chipName: name, dir: dir }, + }); + dispatch.current({ action: "setFiles", payload: { hdl } }); }, async compileChip(hdl: string, dir?: string, name?: string) { @@ -436,13 +392,6 @@ export function makeChipStore( async initializeTests(dir: string, chip: string) { tests = TEST_NAMES[chip] ?? []; this.loadTest(tests[0] ?? chip, dir); - - // tests = (await fs.scandir(`/projects/${project}/${name}`)) - // .filter((file) => file.name.endsWith(".tst")) - // .map((file) => file.name); - // if (tests.length > 0) { - // await this.setTest(tests[0]); - // } }, async loadTest(name: string, dir?: string) { @@ -452,8 +401,6 @@ export function makeChipStore( const tst = await fs.readFile(`${dir}/${name}.tst`); - // TODO: if not using local fs, either load cmp file here or add compare-to inst to local storage test scripts - dispatch.current({ action: "setFiles", payload: { tst, cmp: "" } }); dispatch.current({ action: "setTest", payload: name }); this.compileTest(tst, dir); @@ -463,16 +410,68 @@ export function makeChipStore( }); console.error(e); } - // const [tst, cmp] = await Promise.all([ - // fs - // .readFile(`/projects/${project}/${chipName}/${test}`) - // .catch(() => makeTst()), - // fs - // .readFile( - // `/projects/${project}/${chipName}/${test}`.replace(".tst", ".cmp"), - // ) - // .catch(() => makeCmp()), - // ]); + }, + + 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; + } + test = ChipTest.from( + Ok(tst), + setStatus, + async (file) => { + const cmp = await fs.readFile(`${_dir}/${file}`); + dispatch.current({ action: "setFiles", payload: { cmp } }); + }, + path, + ) + .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(display(e)); + } + 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) { @@ -510,7 +509,6 @@ export function makeChipStore( ); return; } - // dispatch.current({ action: "setFiles", payload: { hdl } }); this.replaceChip(Ok(nextChip)); }, @@ -524,47 +522,6 @@ export function makeChipStore( } }, - // async initialize() { - // await this.setChip(chipName, project); - // }, - - 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; - } - test = ChipTest.from( - Ok(tst), - setStatus, - async (file) => { - const cmp = await fs.readFile(`${_dir}/${file}`); - dispatch.current({ action: "setFiles", payload: { cmp } }); - }, - path, - ) - .with(chip) - .reset(); - test.setFileSystem(fs); - dispatch.current({ action: "updateTestStep" }); - return true; - }, - - async runTest(file: string) { - // if (!this.compileTest(file)) { - // return; - // } - // dispatch.current({ action: "testRunning" }); - // fs.pushd("/samples"); - // await test.run(); - // fs.popd(); - // dispatch.current({ action: "updateTestStep" }); - // dispatch.current({ action: "testFinished" }); - }, - tick(): Promise { return this.stepTest(); }, @@ -578,98 +535,6 @@ export function makeChipStore( } return done; }, - - async resetFile() { - // const { ChipProjects } = await import("@nand2tetris/projects/full.js"); - // const template = ( - // ChipProjects[project].CHIPS as Record> - // )[chipName][`${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}/${chip}.hdl`), - // })), - // ); - }, - - // // TODO: optimize. Maybe create a mapping in initialization - // async findPath( - // fs: FileSystem, - // name: string, - // ): Promise { - // if (!fs) return; - - // async function findIn( - // fs: FileSystem, - // path: string[] = [], - // ): Promise { - // const fullPath = path.length == 0 ? "." : path.join("/"); - // for (const entry of await fs.scandir(fullPath)) { - // if (entry.isDirectory()) { - // const found = await findIn(fs, [...path, entry.name]); - // if (found) { - // return [...path, ...found]; - // } - // } else { - // if (entry.name == name) { - // return [...path, name]; - // } - // } - // } - // return; - // } - - // return await findIn(fs); - // }, - - async loadChip(path: string) { - usingBuiltin = false; - dispatch.current({ action: "updateUsingBuiltin", payload: false }); - - // const name = handle.name.replace(".hdl", ""); - // const path = `/${dir}/${name}.hdl`; - const hdl = await fs.readFile(path); - - // hdlPath = path.join("/"); - const parts = path.split("/"); - const name = parts.pop()!.replace(".hdl", ""); - const dir = parts.join("/"); - - await this.compileChip(hdl, dir, name); - await this.initializeTests(dir, name); - dispatch.current({ - action: "setChip", - payload: { chipName: name, dir: dir }, - }); - dispatch.current({ action: "setFiles", payload: { hdl } }); - }, - - // // TODO: currently doesn't support 2 chips with the same name in different projects - // async loadLocalChip(handle: FileSystemFileHandle) { - // const path = await this.findPath(fs, handle.name); - - // if (!path) { - // // TODO: turn into warning? - // setStatus(`${handle.name} is not inside the projects directory`); - // return; - // } - - // console.log(path); - - // await this.loadChip(`/${path.join("/")}`); - // }, - - async saveChip(hdl: string) { - dispatch.current({ action: "setFiles", payload: { hdl } }); - const path = `${_dir}/${_chipName}.hdl`; - if (fs && path) { - await fs.writeFile(path, hdl); - } - }, }; const initialState: ChipPageState = (() => { diff --git a/web/src/pages/chip.tsx b/web/src/pages/chip.tsx index c5b181c33..3b26dd6ec 100644 --- a/web/src/pages/chip.tsx +++ b/web/src/pages/chip.tsx @@ -70,23 +70,6 @@ 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"); @@ -153,43 +136,14 @@ 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 = () => { actions.toggleBuiltin(); - // if (useBuiltin) { - // setUseBuiltin(false); - // actions.useBuiltin(false); - // } else { - // setUseBuiltin(true); - // actions.useBuiltin(true, hdl); - // } pinResetDispatcher.reset(); }; const loadFile = async () => { - // if (localFsRoot) { - // const [handle] = await window.showOpenFilePicker(); - // actions.loadLocalChip(handle); - // } else { const path = await filePicker.select({ suffix: "hdl" }); actions.loadChip(path); - // } }; const selectors = ( @@ -379,14 +333,12 @@ export const Chip = () => { 1 ? (