diff --git a/vweb/src/lib.rs b/vweb/src/lib.rs index f51e441..1d2419b 100644 --- a/vweb/src/lib.rs +++ b/vweb/src/lib.rs @@ -114,6 +114,14 @@ impl WasmCPU { pub fn halted(&self) -> bool { self.0.halted() } + pub fn get_stack(&self) -> Vec { + self.0.get_stack().into_iter().map(i32::from).collect() + } + + pub fn get_call(&self) -> Vec { + self.0.get_call().into_iter().map(i32::from).collect() + } + pub fn set_pc(&mut self, val: u32) { self.0.set_pc(Word::from(val)) } @@ -122,6 +130,14 @@ impl WasmCPU { self.0.run_to_halt() } + pub fn safe_run(&mut self, max_ticks: usize) { + let mut current = 0; + while !self.0.halted() && current < max_ticks { + current += 1; + self.tick() + } + } + pub fn load(&mut self, rom: Vec) { for (i, b) in rom.iter().enumerate() { self.poke(0x400 + i as u32, *b) diff --git a/vweb/src/snippet_emulator.jsx b/vweb/src/snippet_emulator.jsx index ffacec3..393433e 100644 --- a/vweb/src/snippet_emulator.jsx +++ b/vweb/src/snippet_emulator.jsx @@ -3,6 +3,13 @@ import React, {useState, useCallback, useEffect} from 'react' import {WasmCPU, assemble_snippet, source_map, NovaForth} from '../pkg/vweb.js' export default function({ children }) { + // Use this by giving it some source as a body: + // + // {`.org 0x400 + // push 3 + // hlt`} + // + // The string-in-braces is so that jsx doesn't eat the newlines and whitespace if (React.Children.count(children) !== 1) { throw 'Expects a single text node as a child' } @@ -16,20 +23,30 @@ export default function({ children }) { const [cpu, setCpu] = useState(new WasmCPU()) // The actual CPU emulator itself const [activeLine, setActiveLine] = useState(null) // The (0-based) index of the + // Update which line is highlighted. This should be called in most button callbacks. const updateActiveLine = useCallback(() => { sourceMap && setActiveLine(sourceMap.get(cpu.pc())) + if (cpu.halted()) { setMessage('Halted') } }, [cpu, sourceMap]) + // Reset the CPU const reset = useCallback(() => { cpu.reset() - cpu.start() + cpu.start() // Reset leaves Vulcan in a stopped state but since we're going to step through anyway... updateActiveLine() }, [cpu, updateActiveLine]) + // Reset the CPU but also put in a message saying we did. This needs to be a separate fn because the + // rebuild handler calls the other one but it sets its own message which we don't want to overwrite + const resetBtn = useCallback(() => { + reset() + setMessage('Reset') + }, [reset]) + // A callback to build the binary from source const rebuild = useCallback((code) => { try { - const bin = assemble_snippet(code) + const bin = assemble_snippet(code) // Build the thing and its source map. This throws if there's a problem const sm = source_map(code) setBinary(bin) setSourceMap(sm.Ok || {}) @@ -40,11 +57,26 @@ export default function({ children }) { } }, []) + // Step forward one const step = useCallback(() => { cpu.tick() + setMessage('') updateActiveLine() }, [cpu, updateActiveLine]) + // Run all the lines at once, until the CPU halts + const runToHalt = useCallback(() => { + setTimeout(() => { + cpu.safe_run(100_000) // Safe run, 100k cycles and pause + updateActiveLine() + // If we're not halted then we're probably in an infinite loop. That might be fine, but stop running, in case + // it's not. If they just hit [run] again then it'll keep on going. For snippets it's likely this indicates + // a bug though. + if (!cpu.halted()) { setMessage('Paused after 100k cycles')} + }, 0) + setMessage('Running...') + }, [cpu, updateActiveLine]) + // On load, clean the source and build the stuff useEffect(() => { const lines = React.Children.toArray(children)[0].split('\n') @@ -78,10 +110,11 @@ export default function({ children }) {
{message}
[Step] - [Run] - [Reset] + [Run] + [Reset] { setEditing(true); setMessage('') }}>[Edit]
+ ) } @@ -99,5 +132,10 @@ function SourceDisplay({ src, activeLine }) { } function StackDisplay({ data, call }) { - + return ( +
+ {data &&
Data: {data.join(', ')}
} + {call &&
Call: {call.join(', ')}
} +
+ ) } \ No newline at end of file