diff --git a/components/src/runbar.tsx b/components/src/runbar.tsx index e956503c9..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,24 +31,22 @@ 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(); + }, []); useEffect(() => { 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 }); }; @@ -82,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 cafef4ccb..11935196f 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,6 +387,24 @@ export function makeVmStore( return { initialState, reducers, actions }; } +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/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/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/simulator/src/vm/vm.ts b/simulator/src/vm/vm.ts index 901cea2c7..b1ba90a69 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,17 @@ 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 +657,9 @@ 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 +763,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 +783,20 @@ 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 +922,11 @@ 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..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 [tst, setTst] = useStateInitializer(state.files.tst); const [out, setOut] = useStateInitializer(state.files.out); const [cmp, setCmp] = useStateInitializer(state.files.cmp); @@ -68,42 +106,28 @@ 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() { - return actions.step(); - } - - 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() { - return actions.testStep(); + const t = actions.testStep(); + actions.setPaused(!this.running); + dispatch.current({ action: "update" }); + return t; } override finishFrame() { @@ -174,13 +198,12 @@ const VM = () => { return (
{ runner={vmRunner.current} disabled={!state.controls.valid} speed={state.config.speed} + breakpointsRef={breakpointsRef} onSpeedChange={(speed) => onSpeedChange(speed, false)} /> )} @@ -232,6 +256,7 @@ const VM = () => { : undefined } error={state.controls.error} + breakpointsRef={breakpointsRef} /> VM Structures}> @@ -408,7 +433,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 e1df96f05..dd7b3b6e6 100644 --- a/web/src/shell/Monaco.tsx +++ b/web/src/shell/Monaco.tsx @@ -1,8 +1,9 @@ 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 { MutableRefObject, useCallback, useContext, useEffect, useRef, useState } from "react"; import { AppContext } from "../App.context"; import { Decoration, HighlightType } from "./editor"; @@ -78,6 +79,7 @@ export const Monaco = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, + breakpointsRef, }: { value: string; onChange: Action; @@ -91,6 +93,7 @@ export const Monaco = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; + breakpointsRef?: MutableRefObject; }) => { const { theme } = useContext(AppContext); const monaco = useRef(); @@ -100,7 +103,14 @@ export const Monaco = ({ const decorations = useRef([]); const highlight = useRef(undefined); const customDecorations = useRef([]); + const monacoBreakpoints = useRef(); + const setBreakpoints = useCallback((breakpoints: number[]) => { + console.log("breakpointChanged: ", breakpoints); + if (breakpointsRef !== undefined) { + breakpointsRef.current = breakpoints; + } + }, []); const codeTheme = useCallback(() => { const isDark = theme === "system" @@ -122,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 { @@ -189,6 +199,7 @@ export const Monaco = ({ quickSuggestions: { other: "inline", }, + glyphMargin: true, }); document.fonts.ready.then(() => { @@ -205,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 63ef60eef..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,6 +77,7 @@ export const Editor = ({ dynamicHeight = false, alwaysRecenter = true, lineNumberTransform, + breakpointsRef, }: { className?: string; style?: CSSProperties; @@ -93,6 +94,7 @@ export const Editor = ({ dynamicHeight?: boolean; alwaysRecenter?: boolean; lineNumberTransform?: (n: number) => string; + breakpointsRef?: MutableRefObject; }) => { const { monaco } = useContext(AppContext); @@ -116,6 +118,7 @@ export const Editor = ({ dynamicHeight={dynamicHeight} alwaysRecenter={alwaysRecenter} lineNumberTransform={lineNumberTransform} + breakpointsRef={breakpointsRef} /> ) : (