From 0f154c26c1e5ef60fff16e2bfe03bc0a08be0bf2 Mon Sep 17 00:00:00 2001 From: happytomatoe <2893931+happytomatoe@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:53:26 +0200 Subject: [PATCH 1/6] Add breakpoints --- components/src/stores/vm.store.ts | 13 ++++++++----- package-lock.json | 11 +++++++++++ simulator/src/vm/vm.ts | 32 ++++++++++++++++++++++++------- web/package.json | 1 + web/src/pages/vm.tsx | 17 ++++++++++++---- web/src/shell/Monaco.tsx | 23 +++++++++++++++++++++- web/src/shell/editor.tsx | 4 ++++ 7 files changed, 84 insertions(+), 17 deletions(-) diff --git a/components/src/stores/vm.store.ts b/components/src/stores/vm.store.ts index cafef4ccb..4becc2c60 100644 --- a/components/src/stores/vm.store.ts +++ b/components/src/stores/vm.store.ts @@ -346,12 +346,12 @@ export function makeVmStore( setPaused(paused = true) { vm.setPaused(paused); }, - step() { + step(): VmStepResult { showHighlight = true; try { let done = false; - const exitCode = vm.step(); + const { exitCode, lineNumber } = vm.step(); if (exitCode !== undefined) { done = true; dispatch.current({ action: "setExitCode", payload: exitCode }); @@ -360,11 +360,11 @@ export function makeVmStore( if (animate) { dispatch.current({ action: "update" }); } - return done; + return { done, lineNumber }; } catch (e) { setStatus(`Runtime error: ${(e as Error).message}`); dispatch.current({ action: "setValid", payload: false }); - return true; + return { done: true, lineNumber: -1 }; } }, reset() { @@ -387,7 +387,10 @@ export function makeVmStore( return { initialState, reducers, actions }; } - +export interface VmStepResult { + done: boolean; + lineNumber: number; +} export function useVmPageStore() { const { fs, setStatus, storage } = useContext(BaseContext); diff --git a/package-lock.json b/package-lock.json index 3132a6955..2bbf57894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14494,6 +14494,16 @@ "version": "0.5.3", "license": "MIT" }, + "node_modules/monaco-breakpoints": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/monaco-breakpoints/-/monaco-breakpoints-0.2.0.tgz", + "integrity": "sha512-iNxvZH09ugBQmJ6h+fGG1mGt0MUy4iJruV0EdjVjdiMMK9+V8REQhX01Gt2IMDSaIJ7F2RnsJIM7TFd/kKefSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "monaco-editor": "^0.39.0" + } + }, "node_modules/monaco-editor": { "version": "0.51.0", "dev": true, @@ -22585,6 +22595,7 @@ "immer": "^10.1.1", "jszip": "^3.10.1", "make-plural": "^7.4.0", + "monaco-breakpoints": "^0.2.0", "ohm-js": "^17.1.0", "prettier": "^3.3.1", "raw-loader": "^4.0.2", diff --git a/simulator/src/vm/vm.ts b/simulator/src/vm/vm.ts index 901cea2c7..b31a6d229 100644 --- a/simulator/src/vm/vm.ts +++ b/simulator/src/vm/vm.ts @@ -48,6 +48,7 @@ export interface VmFunction { labels: Record; operations: VmInstruction[]; opBase: number; + lineNumberOffset: number; } interface VmFunctionInvocation { @@ -64,6 +65,8 @@ interface VmFunctionInvocation { thatInitialized: boolean; // The size of the memory block pointed to by the function's THIS (if exists) thisN?: number; + // Function line number offset + lineNumberOffset: number; } export const IMPLICIT = "__implicit"; @@ -73,6 +76,8 @@ export const SYS_INIT: VmFunction = { labels: {}, nVars: 0, opBase: 0, + //TODO: RL change? + lineNumberOffset: 0, operations: [ { op: "function", name: "Sys.init", nVars: 0 }, { op: "call", name: "Math.init", nArgs: 0 }, @@ -305,6 +310,7 @@ export class Vm { labels: {}, operations: [{ op: "function", name, nVars, span: instructions[i].span }], opBase: 0, + lineNumberOffset: i + 2 }; const declaredLabels: Set = new Set(); @@ -437,6 +443,7 @@ export class Vm { opPtr: 0, thisInitialized: false, thatInitialized: false, + lineNumberOffset: -3 //TODO: RL change? }; } return invocation; @@ -567,6 +574,7 @@ export class Vm { nArgs: 0, thisInitialized: false, thatInitialized: false, + lineNumberOffset: -2 //TODO: RL change }, ]; this.memory.reset(); @@ -631,12 +639,12 @@ export class Vm { this.os.paused = paused; } - step(): number | undefined { + step(): VmStepResult { if (this.os.sys.halted) { - return this.os.sys.exitCode; + return { exitCode: this.os.sys.exitCode, lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; } if (this.os.sys.blocked) { - return; + return { lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; } if (this.os.sys.released && this.operation?.op == "call") { const ret = this.os.sys.readReturnValue(); @@ -644,7 +652,7 @@ export class Vm { this.memory.set(sp, ret); this.memory.SP = sp + 1; this.invocation.opPtr += 1; - return; + return { lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; } if (this.operation == undefined) { @@ -748,11 +756,12 @@ export class Vm { frameBase: base, thisInitialized: false, thatInitialized: false, + lineNumberOffset: -1,//TODO: RL change? }); } else if (VM_BUILTINS[fnName]) { const ret = VM_BUILTINS[fnName].func(this.memory, this.os); if (this.os.sys.blocked) { - return; // we will handle the return when the OS is released + return { lineNumber: -1 }; // we will handle the return when the OS is released } const sp = this.memory.SP - operation.nArgs; this.memory.set(sp, ret); @@ -767,13 +776,16 @@ export class Vm { this.invocation.opPtr = ret; if (this.executionStack.length === 0) { this.returnLine = line; - return 0; + return { exitCode: 0, lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; } break; } } this.invocation.opPtr += 1; - return; + const e = this.currentFunction + const lineNumber = e.lineNumberOffset + this.invocation.opPtr - 1; + + return { lineNumber }; } private goto(label: string) { @@ -899,6 +911,12 @@ export class Vm { } } + +interface VmStepResult { + exitCode?: number; + lineNumber: number; +} + export function writeFrame(frame: VmFrame): string { return [ `Frame: ${frame.fn?.name ?? "Unknown Fn"} ARG:${frame.frame.ARG} LCL:${ diff --git a/web/package.json b/web/package.json index edcbbbe28..1d8cdca2e 100644 --- a/web/package.json +++ b/web/package.json @@ -31,6 +31,7 @@ "immer": "^10.1.1", "jszip": "^3.10.1", "make-plural": "^7.4.0", + "monaco-breakpoints": "^0.2.0", "ohm-js": "^17.1.0", "prettier": "^3.3.1", "raw-loader": "^4.0.2", diff --git a/web/src/pages/vm.tsx b/web/src/pages/vm.tsx index 197d9a421..357fc0120 100644 --- a/web/src/pages/vm.tsx +++ b/web/src/pages/vm.tsx @@ -48,7 +48,7 @@ const VM = () => { const { setTool, stores } = useContext(PageContext); const { state, actions, dispatch } = stores.vm; const { setStatus } = useContext(BaseContext); - + const [breakpoints, setBreakpoints] = useState([]); const [tst, setTst] = useStateInitializer(state.files.tst); const [out, setOut] = useStateInitializer(state.files.out); const [cmp, setCmp] = useStateInitializer(state.files.cmp); @@ -83,7 +83,12 @@ const VM = () => { useEffect(() => { vmRunner.current = new (class VMTimer extends Timer { override async tick() { - return actions.step(); + const {done, lineNumber} = actions.step(); + console.log("Breakpoints", breakpoints); + if(breakpoints.includes(lineNumber)){ + return true; + } + return done; } override finishFrame() { @@ -103,7 +108,10 @@ const VM = () => { testRunner.current = new (class TestTimer extends Timer { override async tick() { - return actions.testStep(); + const t = actions.testStep(); + actions.setPaused(!this.running); + dispatch.current({ action: "update" }); + return t; } override finishFrame() { @@ -127,7 +135,7 @@ const VM = () => { vmRunner.current?.stop(); testRunner.current?.stop(); }; - }, [actions, dispatch]); + }, [actions, dispatch, breakpoints]); const uploadRef = useRef(null); @@ -232,6 +240,7 @@ const VM = () => { : undefined } error={state.controls.error} + setBreakpoints={setBreakpoints} /> VM Structures}> diff --git a/web/src/shell/Monaco.tsx b/web/src/shell/Monaco.tsx index e1df96f05..6d5f9f9c8 100644 --- a/web/src/shell/Monaco.tsx +++ b/web/src/shell/Monaco.tsx @@ -1,6 +1,7 @@ import MonacoEditor, { type OnMount } from "@monaco-editor/react"; import { CompilationError, Span } from "@nand2tetris/simulator/languages/base"; import { Action } from "@nand2tetris/simulator/types"; +import { MonacoBreakpoint } from "monaco-breakpoints"; import * as monacoT from "monaco-editor/esm/vs/editor/editor.api"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { AppContext } from "../App.context"; @@ -78,6 +79,7 @@ export const Monaco = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, + setBreakpoints }: { value: string; onChange: Action; @@ -91,6 +93,7 @@ export const Monaco = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; + setBreakpoints?: (n: number[]) => void; }) => { const { theme } = useContext(AppContext); const monaco = useRef(); @@ -100,7 +103,21 @@ export const Monaco = ({ const decorations = useRef([]); const highlight = useRef(undefined); const customDecorations = useRef([]); - + const [instance, setInstace] = useState(); + const [b, setB] = useState(false); + const bCallback = useCallback((breakpoints: number[]) => { + console.log('breakpointChanged: ', breakpoints); + if(setBreakpoints!==undefined){ + setBreakpoints(breakpoints); + } + }, []); + useEffect(() => { + if (instance && !b) { + console.log("add callback for breakpoints") + instance.on('breakpointChanged', bCallback) + setB(true); + } + }, [instance, bCallback]) const codeTheme = useCallback(() => { const isDark = theme === "system" @@ -169,6 +186,9 @@ export const Monaco = ({ // Set options when mounting const onMount: OnMount = useCallback( (ed, mon) => { + if (instance === undefined) { + setInstace(new MonacoBreakpoint({ editor: ed })) + } monaco.current = mon; editor.current = ed; editor.current?.updateOptions({ @@ -189,6 +209,7 @@ export const Monaco = ({ quickSuggestions: { other: "inline", }, + glyphMargin: true, }); document.fonts.ready.then(() => { diff --git a/web/src/shell/editor.tsx b/web/src/shell/editor.tsx index 63ef60eef..b7162b71a 100644 --- a/web/src/shell/editor.tsx +++ b/web/src/shell/editor.tsx @@ -77,6 +77,7 @@ export const Editor = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, + setBreakpoints }: { className?: string; style?: CSSProperties; @@ -93,6 +94,7 @@ export const Editor = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; + setBreakpoints?: (n:number[]) => void }) => { const { monaco } = useContext(AppContext); @@ -116,6 +118,8 @@ export const Editor = ({ dynamicHeight={dynamicHeight} alwaysRecenter={alwaysRecenter} lineNumberTransform={lineNumberTransform} + setBreakpoints={setBreakpoints} + /> ) : ( From a44cbcb22dc6b841e47cd0c6966faabd81e1d3bf Mon Sep 17 00:00:00 2001 From: happytomatoe <2893931+happytomatoe@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:41:24 +0200 Subject: [PATCH 2/6] WIP --- web/src/pages/vm.tsx | 6 +++--- web/src/shell/Monaco.tsx | 14 +++++++------- web/src/shell/editor.tsx | 5 ++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/web/src/pages/vm.tsx b/web/src/pages/vm.tsx index 357fc0120..a086c068b 100644 --- a/web/src/pages/vm.tsx +++ b/web/src/pages/vm.tsx @@ -83,9 +83,9 @@ const VM = () => { useEffect(() => { vmRunner.current = new (class VMTimer extends Timer { override async tick() { - const {done, lineNumber} = actions.step(); + const { done, lineNumber } = actions.step(); console.log("Breakpoints", breakpoints); - if(breakpoints.includes(lineNumber)){ + if (breakpoints.includes(lineNumber)) { return true; } return done; @@ -417,7 +417,7 @@ function VMStackFrame({

Stack: - [{frame.stack.values.join(", ")}] + {/* [{frame.stack.values.join(", ")}] */}

{frame.usedSegments?.has("local") && (

diff --git a/web/src/shell/Monaco.tsx b/web/src/shell/Monaco.tsx index 6d5f9f9c8..da4460d26 100644 --- a/web/src/shell/Monaco.tsx +++ b/web/src/shell/Monaco.tsx @@ -79,7 +79,7 @@ export const Monaco = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, - setBreakpoints + setBreakpoints, }: { value: string; onChange: Action; @@ -106,18 +106,18 @@ export const Monaco = ({ const [instance, setInstace] = useState(); const [b, setB] = useState(false); const bCallback = useCallback((breakpoints: number[]) => { - console.log('breakpointChanged: ', breakpoints); - if(setBreakpoints!==undefined){ + console.log("breakpointChanged: ", breakpoints); + if (setBreakpoints !== undefined) { setBreakpoints(breakpoints); } }, []); useEffect(() => { if (instance && !b) { - console.log("add callback for breakpoints") - instance.on('breakpointChanged', bCallback) + console.log("add callback for breakpoints"); + instance.on("breakpointChanged", bCallback); setB(true); } - }, [instance, bCallback]) + }, [instance, bCallback]); const codeTheme = useCallback(() => { const isDark = theme === "system" @@ -187,7 +187,7 @@ export const Monaco = ({ const onMount: OnMount = useCallback( (ed, mon) => { if (instance === undefined) { - setInstace(new MonacoBreakpoint({ editor: ed })) + setInstace(new MonacoBreakpoint({ editor: ed })); } monaco.current = mon; editor.current = ed; diff --git a/web/src/shell/editor.tsx b/web/src/shell/editor.tsx index b7162b71a..cdd7ff9d3 100644 --- a/web/src/shell/editor.tsx +++ b/web/src/shell/editor.tsx @@ -77,7 +77,7 @@ export const Editor = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, - setBreakpoints + setBreakpoints, }: { className?: string; style?: CSSProperties; @@ -94,7 +94,7 @@ export const Editor = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; - setBreakpoints?: (n:number[]) => void + setBreakpoints?: (n: number[]) => void; }) => { const { monaco } = useContext(AppContext); @@ -119,7 +119,6 @@ export const Editor = ({ alwaysRecenter={alwaysRecenter} lineNumberTransform={lineNumberTransform} setBreakpoints={setBreakpoints} - /> ) : ( From 3eb158088d7e4d9e835495f3ef905672f28fd671 Mon Sep 17 00:00:00 2001 From: happytomatoe <2893931+happytomatoe@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:43:02 +0200 Subject: [PATCH 3/6] WIP --- simulator/src/vm/vm.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/simulator/src/vm/vm.ts b/simulator/src/vm/vm.ts index b31a6d229..b1ba90a69 100644 --- a/simulator/src/vm/vm.ts +++ b/simulator/src/vm/vm.ts @@ -310,7 +310,7 @@ export class Vm { labels: {}, operations: [{ op: "function", name, nVars, span: instructions[i].span }], opBase: 0, - lineNumberOffset: i + 2 + lineNumberOffset: i + 2, }; const declaredLabels: Set = new Set(); @@ -443,7 +443,7 @@ export class Vm { opPtr: 0, thisInitialized: false, thatInitialized: false, - lineNumberOffset: -3 //TODO: RL change? + lineNumberOffset: -3, //TODO: RL change? }; } return invocation; @@ -574,7 +574,7 @@ export class Vm { nArgs: 0, thisInitialized: false, thatInitialized: false, - lineNumberOffset: -2 //TODO: RL change + lineNumberOffset: -2, //TODO: RL change }, ]; this.memory.reset(); @@ -641,10 +641,15 @@ export class Vm { step(): VmStepResult { if (this.os.sys.halted) { - return { exitCode: this.os.sys.exitCode, lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; + return { + exitCode: this.os.sys.exitCode, + lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr, + }; } if (this.os.sys.blocked) { - return { lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; + return { + lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr, + }; } if (this.os.sys.released && this.operation?.op == "call") { const ret = this.os.sys.readReturnValue(); @@ -652,7 +657,9 @@ export class Vm { this.memory.set(sp, ret); this.memory.SP = sp + 1; this.invocation.opPtr += 1; - return { lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; + return { + lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr, + }; } if (this.operation == undefined) { @@ -756,7 +763,7 @@ export class Vm { frameBase: base, thisInitialized: false, thatInitialized: false, - lineNumberOffset: -1,//TODO: RL change? + lineNumberOffset: -1, //TODO: RL change? }); } else if (VM_BUILTINS[fnName]) { const ret = VM_BUILTINS[fnName].func(this.memory, this.os); @@ -776,13 +783,17 @@ export class Vm { this.invocation.opPtr = ret; if (this.executionStack.length === 0) { this.returnLine = line; - return { exitCode: 0, lineNumber: this.invocation.lineNumberOffset + this.invocation.opPtr }; + return { + exitCode: 0, + lineNumber: + this.invocation.lineNumberOffset + this.invocation.opPtr, + }; } break; } } this.invocation.opPtr += 1; - const e = this.currentFunction + const e = this.currentFunction; const lineNumber = e.lineNumberOffset + this.invocation.opPtr - 1; return { lineNumber }; @@ -911,7 +922,6 @@ export class Vm { } } - interface VmStepResult { exitCode?: number; lineNumber: number; From 905518945e0148e085c57ea3eb6d4e19b23f3e19 Mon Sep 17 00:00:00 2001 From: happytomatoe <2893931+happytomatoe@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:48:25 +0200 Subject: [PATCH 4/6] Hotfix for speed becoming default on breakpoint toogle --- web/src/pages/vm.tsx | 1 - web/src/shell/Monaco.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/pages/vm.tsx b/web/src/pages/vm.tsx index a086c068b..aae2cc7cd 100644 --- a/web/src/pages/vm.tsx +++ b/web/src/pages/vm.tsx @@ -84,7 +84,6 @@ const VM = () => { vmRunner.current = new (class VMTimer extends Timer { override async tick() { const { done, lineNumber } = actions.step(); - console.log("Breakpoints", breakpoints); if (breakpoints.includes(lineNumber)) { return true; } diff --git a/web/src/shell/Monaco.tsx b/web/src/shell/Monaco.tsx index da4460d26..16714124b 100644 --- a/web/src/shell/Monaco.tsx +++ b/web/src/shell/Monaco.tsx @@ -110,7 +110,7 @@ export const Monaco = ({ if (setBreakpoints !== undefined) { setBreakpoints(breakpoints); } - }, []); + },[]); useEffect(() => { if (instance && !b) { console.log("add callback for breakpoints"); From 9ab3207c5b0910cb9b2086e87153467ded5cabc6 Mon Sep 17 00:00:00 2001 From: happytomatoe <2893931+happytomatoe@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:48:46 +0200 Subject: [PATCH 5/6] Forgot to add file to previous commit --- components/src/runbar.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/src/runbar.tsx b/components/src/runbar.tsx index e956503c9..3d01391da 100644 --- a/components/src/runbar.tsx +++ b/components/src/runbar.tsx @@ -30,8 +30,14 @@ export const Runbar = (props: { 2: [16, 1], 3: [16, 16666], 4: [16, 16666 * 30], + }; + + useEffect(() => { + updateSpeed(); + }, []); + useEffect(() => { updateSpeed(); }, [speedValue]); From 7ec307352284d0d00763ffe0deec130c0525bb88 Mon Sep 17 00:00:00 2001 From: happytomatoe <2893931+happytomatoe@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:13:23 +0200 Subject: [PATCH 6/6] Fix bug when in long file setting a brekpoint causes editor to scroll up --- components/src/runbar.tsx | 31 ++++++---- components/src/stores/vm.store.ts | 15 +++++ components/src/timer.tsx | 4 ++ simulator/src/timer.ts | 3 + web/src/pages/vm.tsx | 99 ++++++++++++++++++------------- web/src/shell/Monaco.tsx | 40 ++++++------- web/src/shell/editor.tsx | 8 +-- 7 files changed, 120 insertions(+), 80 deletions(-) diff --git a/components/src/runbar.tsx b/components/src/runbar.tsx index 3d01391da..5d3c5398c 100644 --- a/components/src/runbar.tsx +++ b/components/src/runbar.tsx @@ -1,5 +1,5 @@ import { Timer } from "@nand2tetris/simulator/timer.js"; -import { ChangeEvent, ReactNode, useEffect, useRef } from "react"; +import { ChangeEvent, MutableRefObject, ReactNode, useEffect, useRef } from "react"; import { useStateInitializer } from "./react.js"; import { useTimer } from "./timer.js"; @@ -11,6 +11,17 @@ interface RunbarTooltipOverrides { } export type RunSpeed = 0 | 1 | 2 | 3 | 4; +interface TimerConfiguration { + speed: number; + steps: number; +} +const speedValues: Record = { + 0: { speed: 1000, steps: 1 }, + 1: { speed: 500, steps: 1 }, + 2: { speed: 16, steps: 1 }, + 3: { speed: 16, steps: 16666 }, + 4: { speed: 16, steps: 16666 * 30 }, +}; export const Runbar = (props: { runner: Timer; @@ -20,20 +31,11 @@ export const Runbar = (props: { children?: ReactNode; overrideTooltips?: Partial; onSpeedChange?: (speed: RunSpeed) => void; + breakpointsRef?: MutableRefObject; }) => { const runner = useTimer(props.runner); const [speedValue, setSpeed] = useStateInitializer(props.speed ?? 2); - const speedValues: Record = { - 0: [1000, 1], - 1: [500, 1], - 2: [16, 1], - 3: [16, 16666], - 4: [16, 16666 * 30], - - }; - - useEffect(() => { updateSpeed(); }, []); @@ -42,8 +44,9 @@ export const Runbar = (props: { updateSpeed(); }, [speedValue]); + const updateSpeed = () => { - const [speed, steps] = speedValues[speedValue]; + const {speed, steps} = speedValues[speedValue]; runner.dispatch({ action: "setSpeed", payload: speed }); runner.dispatch({ action: "setSteps", payload: steps }); }; @@ -88,11 +91,13 @@ export const Runbar = (props: { className="flex-0" ref={toggleRef} disabled={props.disabled} - onClick={() => + onClick={() =>{ + runner.actions.setBreakpoints( props.breakpointsRef?.current ?? []) runner.state.running ? runner.actions.stop() : runner.actions.start() } + } data-tooltip={ runner.state.running ? (props.overrideTooltips?.pause ?? `Pause`) diff --git a/components/src/stores/vm.store.ts b/components/src/stores/vm.store.ts index 4becc2c60..11935196f 100644 --- a/components/src/stores/vm.store.ts +++ b/components/src/stores/vm.store.ts @@ -391,6 +391,21 @@ export interface VmStepResult { done: boolean; lineNumber: number; } + +export interface VmPageStoreActions { + setVm(content: string): boolean | undefined; + loadVm(files: VmFile[]): boolean | undefined; + replaceVm(buildResult: Result): boolean; + loadTest(path: string, source: string, cmp?: string): boolean; + setAnimate(value: boolean): void; + testStep(): Promise; + setPaused(paused?: boolean): void; + step(): VmStepResult; + reset(): void; + toggleUseTest(): void; + initialize(): void; +} + export function useVmPageStore() { const { fs, setStatus, storage } = useContext(BaseContext); diff --git a/components/src/timer.tsx b/components/src/timer.tsx index 0e04a8cf0..d9450a090 100644 --- a/components/src/timer.tsx +++ b/components/src/timer.tsx @@ -49,6 +49,10 @@ const makeTimerStore = ( frame() { timer.frame(); }, + setBreakpoints(breakpoints: number[]) { + timer.setBreakpoints(breakpoints); + }, + start() { timer.start(); dispatch.current({ action: "update" }); diff --git a/simulator/src/timer.ts b/simulator/src/timer.ts index 4e25f67ee..11eca79c5 100644 --- a/simulator/src/timer.ts +++ b/simulator/src/timer.ts @@ -74,4 +74,7 @@ export abstract class Timer { this.#running = false; this.toggle(); } + setBreakpoints(breakpoints: number[]) { + + } } diff --git a/web/src/pages/vm.tsx b/web/src/pages/vm.tsx index aae2cc7cd..a5515c17e 100644 --- a/web/src/pages/vm.tsx +++ b/web/src/pages/vm.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useContext, useEffect, useRef, useState } from "react"; +import { ChangeEvent, MutableRefObject, useContext, useEffect, useRef, useState } from "react"; import { Trans, t } from "@lingui/macro"; import { Keyboard } from "@nand2tetris/components/chips/keyboard.js"; @@ -7,10 +7,11 @@ import { Screen } from "@nand2tetris/components/chips/screen.js"; import { useStateInitializer } from "@nand2tetris/components/react"; import { Runbar } from "@nand2tetris/components/runbar"; import { BaseContext } from "@nand2tetris/components/stores/base.context"; -import { DEFAULT_TEST } from "@nand2tetris/components/stores/vm.store.js"; +import { DEFAULT_TEST, VmPageStoreActions, VmStoreDispatch } from "@nand2tetris/components/stores/vm.store.js"; import { Timer } from "@nand2tetris/simulator/timer.js"; import { ERRNO, isSysError } from "@nand2tetris/simulator/vm/os/errors.js"; import { IMPLICIT, SYS_INIT, VmFrame } from "@nand2tetris/simulator/vm/vm.js"; +import { Action } from "@nand2tetris/simulator/types"; import { VmFile } from "@nand2tetris/simulator/test/vmtst"; import { PageContext } from "../Page.context"; @@ -43,12 +44,49 @@ const ERROR_MESSAGES: Record = { interface Rerenderable { rerender: () => void; } +class VMTimer extends Timer { + public dispatch: MutableRefObject; + public actions: VmPageStoreActions; + public breakpoints: number[]; + public setStatus: Action + constructor(actions: VmPageStoreActions, dispatch: MutableRefObject, + setStatus: Action, breakpoints: number[] = []) { + super(); + this.actions = actions; + this.breakpoints = breakpoints; + this.dispatch = dispatch; + this.setStatus = setStatus; + } + override setBreakpoints(breakpoints: number[]): void { + this.breakpoints=breakpoints; + } + + override async tick() { + const { done, lineNumber } = this.actions.step(); + if (this.breakpoints.includes(lineNumber)) { + return true; + } + return done; + } + + override finishFrame() { + this.dispatch.current({ action: "update" }); + } + override reset() { + this.setStatus("Reset"); + this.actions.reset(); + } + + override toggle() { + this.actions.setPaused(!this.running); + this.dispatch.current({ action: "update" }); + } +} const VM = () => { const { setTool, stores } = useContext(PageContext); const { state, actions, dispatch } = stores.vm; const { setStatus } = useContext(BaseContext); - const [breakpoints, setBreakpoints] = useState([]); const [tst, setTst] = useStateInitializer(state.files.tst); const [out, setOut] = useStateInitializer(state.files.out); const [cmp, setCmp] = useStateInitializer(state.files.cmp); @@ -68,42 +106,21 @@ const VM = () => { setStatus( state.controls.exitCode == 0 ? "Program halted" - : `Program exited with error code ${state.controls.exitCode}${ - isSysError(state.controls.exitCode) - ? `: ${ERROR_MESSAGES[state.controls.exitCode]}` - : "" - }`, + : `Program exited with error code ${state.controls.exitCode}${isSysError(state.controls.exitCode) + ? `: ${ERROR_MESSAGES[state.controls.exitCode]}` + : "" + }`, ); } }, [state.controls.exitCode]); - const vmRunner = useRef(); + const vmRunner = useRef(); + const breakpointsRef = useRef([]); const testRunner = useRef(); const [runnersAssigned, setRunnersAssigned] = useState(false); - useEffect(() => { - vmRunner.current = new (class VMTimer extends Timer { - override async tick() { - const { done, lineNumber } = actions.step(); - if (breakpoints.includes(lineNumber)) { - return true; - } - return done; - } - override finishFrame() { - dispatch.current({ action: "update" }); - } - - override reset() { - setStatus("Reset"); - actions.reset(); - } - - override toggle() { - actions.setPaused(!this.running); - dispatch.current({ action: "update" }); - } - })(); + useEffect(() => { + vmRunner.current = new VMTimer(actions, dispatch, setStatus); testRunner.current = new (class TestTimer extends Timer { override async tick() { @@ -134,7 +151,7 @@ const VM = () => { vmRunner.current?.stop(); testRunner.current?.stop(); }; - }, [actions, dispatch, breakpoints]); + }, [actions, dispatch]); const uploadRef = useRef(null); @@ -181,13 +198,12 @@ const VM = () => { return (

{ runner={vmRunner.current} disabled={!state.controls.valid} speed={state.config.speed} + breakpointsRef={breakpointsRef} onSpeedChange={(speed) => onSpeedChange(speed, false)} /> )} @@ -239,7 +256,7 @@ const VM = () => { : undefined } error={state.controls.error} - setBreakpoints={setBreakpoints} + breakpointsRef={breakpointsRef} /> VM Structures}> diff --git a/web/src/shell/Monaco.tsx b/web/src/shell/Monaco.tsx index 16714124b..dd7b3b6e6 100644 --- a/web/src/shell/Monaco.tsx +++ b/web/src/shell/Monaco.tsx @@ -3,7 +3,7 @@ import { CompilationError, Span } from "@nand2tetris/simulator/languages/base"; import { Action } from "@nand2tetris/simulator/types"; import { MonacoBreakpoint } from "monaco-breakpoints"; import * as monacoT from "monaco-editor/esm/vs/editor/editor.api"; -import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { MutableRefObject, useCallback, useContext, useEffect, useRef, useState } from "react"; import { AppContext } from "../App.context"; import { Decoration, HighlightType } from "./editor"; @@ -79,7 +79,7 @@ export const Monaco = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, - setBreakpoints, + breakpointsRef, }: { value: string; onChange: Action; @@ -93,7 +93,7 @@ export const Monaco = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; - setBreakpoints?: (n: number[]) => void; + breakpointsRef?: MutableRefObject; }) => { const { theme } = useContext(AppContext); const monaco = useRef(); @@ -103,21 +103,14 @@ export const Monaco = ({ const decorations = useRef([]); const highlight = useRef(undefined); const customDecorations = useRef([]); - const [instance, setInstace] = useState(); - const [b, setB] = useState(false); - const bCallback = useCallback((breakpoints: number[]) => { + const monacoBreakpoints = useRef(); + + const setBreakpoints = useCallback((breakpoints: number[]) => { console.log("breakpointChanged: ", breakpoints); - if (setBreakpoints !== undefined) { - setBreakpoints(breakpoints); - } - },[]); - useEffect(() => { - if (instance && !b) { - console.log("add callback for breakpoints"); - instance.on("breakpointChanged", bCallback); - setB(true); + if (breakpointsRef !== undefined) { + breakpointsRef.current = breakpoints; } - }, [instance, bCallback]); + }, []); const codeTheme = useCallback(() => { const isDark = theme === "system" @@ -139,9 +132,9 @@ export const Monaco = ({ highlight.current == lineCount ? (editor.current?.getModel()?.getValueLength() ?? 0) : (editor.current?.getModel()?.getOffsetAt({ - lineNumber: highlight.current + 1, - column: 0, - }) ?? 1) - 1; + lineNumber: highlight.current + 1, + column: 0, + }) ?? 1) - 1; newHighlight = { start: start, end: end, line: highlight.current }; } } else { @@ -186,9 +179,6 @@ export const Monaco = ({ // Set options when mounting const onMount: OnMount = useCallback( (ed, mon) => { - if (instance === undefined) { - setInstace(new MonacoBreakpoint({ editor: ed })); - } monaco.current = mon; editor.current = ed; editor.current?.updateOptions({ @@ -226,6 +216,12 @@ export const Monaco = ({ }); const model = editor.current?.getModel(); model?.setEOL(monacoT.editor.EndOfLineSequence.LF); + if (monacoBreakpoints.current !== undefined) { + monacoBreakpoints.current.clearBreakpoints(); + } + monacoBreakpoints.current = new MonacoBreakpoint({ editor: ed }); + monacoBreakpoints.current.on("breakpointChanged", setBreakpoints); + }, [codeTheme], ); diff --git a/web/src/shell/editor.tsx b/web/src/shell/editor.tsx index cdd7ff9d3..de70da022 100644 --- a/web/src/shell/editor.tsx +++ b/web/src/shell/editor.tsx @@ -1,6 +1,6 @@ import { Trans } from "@lingui/macro"; import { type Grammar } from "ohm-js"; -import { CSSProperties, lazy, Suspense, useContext, useState } from "react"; +import { CSSProperties, lazy, MutableRefObject, Suspense, useContext, useState } from "react"; import { AppContext } from "../App.context"; import { @@ -77,7 +77,7 @@ export const Editor = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, - setBreakpoints, + breakpointsRef, }: { className?: string; style?: CSSProperties; @@ -94,7 +94,7 @@ export const Editor = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; - setBreakpoints?: (n: number[]) => void; + breakpointsRef?: MutableRefObject; }) => { const { monaco } = useContext(AppContext); @@ -118,7 +118,7 @@ export const Editor = ({ dynamicHeight={dynamicHeight} alwaysRecenter={alwaysRecenter} lineNumberTransform={lineNumberTransform} - setBreakpoints={setBreakpoints} + breakpointsRef={breakpointsRef} /> ) : (