diff --git a/vweb/src/forge_simulator.jsx b/vweb/src/forge_simulator.jsx new file mode 100644 index 0000000..763cadc --- /dev/null +++ b/vweb/src/forge_simulator.jsx @@ -0,0 +1,119 @@ +import React, { useState, useCallback, useRef } from 'react' +import ForgeEditor from "./forge_editor" +import EmulatorDisplay from "./emulator_display" +import { WasmCPU, assemble_snippet, compile_forge } from '../pkg/vweb.js' +import { undent } from './undent' + +export default function({ src: defaultSrc }) { + let [activeTab, setActiveTab] = useState('editor') // Support for the tabbar + const [assembly, setAssembly] = useState(null) // The compiled assembly code + const [binary, setBinary] = useState(null) // The assembled binary + const [cpu, _setCpu] = useState(() => new WasmCPU()) // The actual CPU emulator + const [errors, setErrors] = useState(null) // What's displayed on the compile errors tab + const [src, setSrc] = useState(() => undent(defaultSrc)) // The current Forge source + const [status, setStatus] = useState('') // The contents of the status bar + // Whether the emulator should be running. Has to be a ref because the CPU setTimeout loop won't ever see changes in it otherwise + const running = useRef(false) + + // Callback for the build button + const build = useCallback(() => { + try { + const asm = compile_forge(src) // Compile forge to asm + setAssembly(asm) // Store that on the asm tab for inspection + const bin = assemble_snippet(asm) // Assemble it into a binary + setBinary(bin) // Store that + setStatus(`Compiled ${bin.length} bytes`) // Success! + setErrors(null) // Clear the old error messages off the tab + } catch (err) { + if (err.message) { err = err.message } // How we get this differs between forge and asm + console.error(err) // Meh + setErrors(err) // Show full compiler errors + setStatus("Failed to compile") // Short status line message + } + }, [src]) + + // Callback for the run button + const run = useCallback(() => { + cpu.load(binary) + cpu.reset() + cpu.start() + running.current = true + setStatus('Running...') + const time_slice = () => { + cpu.safe_run(100_000) // We want a good number of cycles here: too short is too slow; too fast is nonresponsive + if (!cpu.halted() && running.current) { // Stop if we've hit the stop btn or if it's at a hlt + setTimeout(time_slice, 0) // Otherwise reschedule this for the end of the event loop + } else { + if (running.current) { // If this is true, we stopped from hlt, so update the status + // (if they hit the button then the status has already been updated) + setStatus('Terminated') + running.current = false + } + } + } + setTimeout(time_slice, 0) + }, [cpu, binary, running]) + + // Callback for the stop button + const stop = useCallback(() => { + running.current = false + setStatus('Stopped by user') + }, []) + + return ( + <> + + +
+ {activeTab === 'editor' && } + {activeTab === 'assembly' &&
{assembly}
} + {activeTab === 'display' && } + {activeTab === 'errors' &&
{errors}
} +
+ + + ) +} + +// A tabbar of the various pages in the simulator +function Tabbar({ activeTab, setActiveTab, anyErrors }) { + let onChangeTab = useCallback((event) => { + event.preventDefault() + setActiveTab(event.target.getAttribute('data-tab')) + }, [setActiveTab]) + + const classNames = name => (name === activeTab ? 'button active' : 'button') + return ( +
+ [Editor] + [Display] + [Assembly] + {anyErrors && [Errors]} +
+ ) +} + +// Toolbar of the controls for the simulator +function Toolbar({ activeTab, running, compile, build, run, stop }) { + let buildBtn, runBtn + // TODO: Make this assemble on assembly tab, make that editable + buildBtn = [Build] + + if (running) { + runBtn = [Stop] + } else { + runBtn = [Run] + } + + return ( +
+ {buildBtn} + {runBtn} +
+ ) +} + +// The short status line at the bottom +function Status({ message }) { + return
{message}
+} \ No newline at end of file