Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add local file system functionality to all tools #411

Merged
merged 51 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
6b249eb
Change hardware simulator to use local file system API
netalondon Jul 7, 2024
ad830c1
Handle chips with multiple test files
netalondon Jul 11, 2024
19caa16
Allow dash in comapre file names
netalondon Jul 11, 2024
30c87ab
Use local storage file system as fallback
netalondon Jul 11, 2024
7dcad41
Always use file picker component
netalondon Jul 11, 2024
8144f84
Fix test script dropdown
netalondon Jul 11, 2024
48a03ce
Only load compare files using compare-to
netalondon Jul 11, 2024
4781296
Add warning when can't find matching test file
netalondon Jul 14, 2024
fd48b0c
Track fs upgrade in assembler
netalondon Jul 14, 2024
3a2c88d
Save seembler result on disk
netalondon Jul 14, 2024
1166a4a
Add compare-to inst to cpu and vm tests
netalondon Jul 14, 2024
72dddc9
Implement loading custom chips
netalondon Jul 14, 2024
087db53
Fix ROM32K load inst in computer tests
netalondon Jul 21, 2024
addc194
Return chip selection dropdowns
netalondon Jul 21, 2024
6d6893b
Fix change local fs
netalondon Jul 21, 2024
9389f52
Cleanup old code
netalondon Jul 21, 2024
d83e9aa
Fix build errors
netalondon Jul 21, 2024
3c6235c
Fix tests and test scripts
netalondon Jul 21, 2024
5cb1a3e
CR
netalondon Jul 29, 2024
507839b
CR
netalondon Jul 30, 2024
5845000
Implement test script load command
netalondon Jul 30, 2024
2e2b444
Fix compilation error when loading single vm file
netalondon Jul 30, 2024
d2e273f
Fix bug in vm file loading
netalondon Jul 30, 2024
9cf4b42
Add async behavior to test script load command
netalondon Jul 30, 2024
0c63bdf
Add error message when loaded file is missing
netalondon Jul 30, 2024
abda515
Fix build and CI errors
netalondon Jul 30, 2024
2342739
CR
netalondon Jul 31, 2024
aaf2130
Remove console.logs
netalondon Jul 31, 2024
dcbc08e
Fix test
netalondon Jul 31, 2024
0aa7faf
Move Action<T> change to another PR
netalondon Jul 31, 2024
5e7dae9
Revert test panel ui changes
netalondon Jul 31, 2024
ed465be
Add newline in package.json
netalondon Jul 31, 2024
29b7da4
Remove commented out code
netalondon Jul 31, 2024
fe7a17c
Fix build errors
netalondon Jul 31, 2024
44fbe07
Reinstate reverted Action<string> cahnges
netalondon Aug 1, 2024
4e5963b
Add missing import
netalondon Aug 1, 2024
b346fed
Fix bug where switching chips while builtin will override new file co…
netalondon Aug 1, 2024
eb98d99
Merge remote-tracking branch 'upstream/main' into feat/local-fs
netalondon Aug 4, 2024
d9cfd3a
Require a load command in test scripts
netalondon Jul 31, 2024
906c6b0
Add file not found error message for load command
netalondon Aug 4, 2024
dfdfbea
Force CPU emulator test to either have load inst or a file already lo…
netalondon Aug 4, 2024
647ac42
Merge remote-tracking branch 'upstream/main' into feat/local-fs
netalondon Aug 5, 2024
057697c
Fix tests
netalondon Aug 5, 2024
4683e18
Override pull in OutSubBus
netalondon Aug 5, 2024
1552838
Add option to download project files from the IDE
netalondon Aug 27, 2024
a156e83
CR
netalondon Sep 8, 2024
c9d5b1f
Specify return type
netalondon Sep 8, 2024
d7b5b6f
Merge remote-tracking branch 'upstream/main' into feat/local-fs
netalondon Sep 8, 2024
1abfe6f
Fix build error
netalondon Sep 8, 2024
d3cc013
Fix files not loaded on initial entrance in firefox
netalondon Sep 8, 2024
346b057
Merge branch 'release/fs-ui' into feat/local-fs
DavidSouther Sep 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions components/src/chips/memory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export const Memory = forwardRef(
fileSelect,
showClear = true,
onChange = undefined,
onClear = undefined,
}: {
name?: string;
className?: string;
Expand All @@ -210,6 +211,7 @@ export const Memory = forwardRef(
fileSelect?: () => Promise<{ name: string; content: string }>;
showClear?: boolean;
onChange?: () => void;
onClear?: () => void;
},
ref,
) => {
Expand Down Expand Up @@ -275,6 +277,7 @@ export const Memory = forwardRef(
const clear = () => {
memory.reset();
onChange?.();
onClear?.();
rerenderMemoryBlock();
};

Expand Down
20 changes: 15 additions & 5 deletions components/src/stores/asm.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -200,6 +200,7 @@ export function makeAsmStore(
fs: FileSystem,
setStatus: Action<string>,
dispatch: MutableRefObject<AsmStoreDispatch>,
upgraded: boolean,
) {
const translator = new Translator();
const highlightInfo: HighlightInfo = {
Expand Down Expand Up @@ -347,14 +348,23 @@ export function makeAsmStore(
animate = value;
},

step(): boolean {
async step(): Promise<boolean> {
if (compiled) {
translating = true;
}
translator.step(highlightInfo);

if (animate || translator.done) {
dispatch.current({ action: "update" });

if (path && upgraded) {
await fs.writeFile(
path.replace(/\.asm$/, ".hack"),
translator.getResult(),
);
}
}

if (translator.done) {
setStatus("Translation done.");
}
Expand Down Expand Up @@ -447,13 +457,13 @@ export function makeAsmStore(
}

export function useAsmPageStore() {
const { setStatus, fs } = useContext(BaseContext);
const { setStatus, fs, localFsRoot } = useContext(BaseContext);

const dispatch = useRef<AsmStoreDispatch>(() => 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);
Expand Down
62 changes: 33 additions & 29 deletions components/src/stores/base.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
FileSystem,
LocalStorageFileSystemAdapter,
} from "@davidsouther/jiffies/lib/esm/fs.js";
import { Action } from "@nand2tetris/simulator/types.js";
import {
createContext,
useCallback,
Expand All @@ -10,7 +11,6 @@ import {
useState,
} from "react";
import {
ChainedFileSystemAdapter,
FileSystemAccessFileSystemAdapter,
openNand2TetrisDirectory,
} from "./base/fs.js";
Expand All @@ -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<void>;
closeFs: () => void;
upgraded?: string;
status: string;
setStatus: Action<string>;
storage: Record<string, string>;
Expand All @@ -35,68 +34,73 @@ export interface BaseContext {
export function useBaseContext(): BaseContext {
const localAdapter = useMemo(() => new LocalStorageFileSystemAdapter(), []);
const [fs, setFs] = useState(new FileSystem(localAdapter));
const [upgraded, setUpgraded] = useState<string | undefined>();
const [root, setRoot] = useState<string>();

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,
DavidSouther marked this conversation as resolved.
Show resolved Hide resolved
),
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,
};
}

export const BaseContext = createContext<BaseContext>({
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: "",
Expand Down
56 changes: 18 additions & 38 deletions components/src/stores/chip.store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,6 +26,7 @@ function testChipStore(
setStatus,
storage,
dispatch,
false,
);
const store = { state: initialState, actions, reducers, dispatch, setStatus };
dispatch.current = jest.fn().mockImplementation(
Expand All @@ -46,61 +46,40 @@ 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,
"projects/01/Not.cmp": not.cmp,
});

await store.actions.initialize();
await store.actions.loadChip("projects/01/Not.hdl");

expect(store.state.controls.project).toBe("01");
expect(store.state.controls.chipName).toBe("Not");
expect(store.state.files.hdl).toBe(not.hdl);
expect(store.state.files.tst).toBe(not.tst);
expect(store.state.files.cmp).toBe(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({
Expand All @@ -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);

Expand All @@ -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]]);
Expand All @@ -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]]);
});
Expand Down
Loading