@specy/mips
v3.6.0
Published
The MARS mips interpreter in JS
Maintainers
Readme
Mips-js
A JavaScript library for simulating MIPS assembly code. This library provides an interface to assemble, execute, and inspect the state of a MIPS simulator. It's built by compiling the mars simulator with TeaVM, with some glue code on top to make it easier to use. It is part of a family of javascript assembly interpreters/simulators:
- MIPS: git repo, npm package
- RISC-V: git repo, npm package
- X86: git repo, npm package
- M68K: git repo, npm package
Installation
npm install @specy/mipsUsage
Create a simulator from a virtual source tree and the canonical path of its entry file. Assembly
starts at that file and expands .include directives transitively; files that are not reached are
ignored.
Before running the simulator, you must assemble and initialize it. You can then step through the program, simulate with breakpoints, or simulate with a limit.
⚠️WARNING⚠️ You must have only one instance of the simulator at a time. Memory, registers, and other state are shared between instances.
import { makeMipsFromFiles, JsMips, RegisterName, BackStepAction } from '@specy/mips';
const files = {
'src/main.asm': `
.include "lib/exit.asm"
.text
.globl main
main:
exit
`,
'src/lib/exit.asm': `
.macro exit
li $v0, 10
syscall
.end_macro
`,
} as const;
const mipsSimulator: JsMips = makeMipsFromFiles(files, 'src/main.asm');
mipsSimulator.assemble();
mipsSimulator.initialize(true); // Start at 'main'
while (!mipsSimulator.terminated) {
await mipsSimulator.step();
}
const pc = mipsSimulator.programCounter;
const v0 = mipsSimulator.getRegisterValue('$v0');
console.log(`Program Counter: ${pc}`);
console.log(`$v0: ${v0}`);
// Accessing memory:
const data = mipsSimulator.readMemoryBytes(0xffff0000, 4); // Read 4 bytes from address 0x1000
mipsSimulator.setMemoryBytes(0xffff0000, [0x01, 0x02, 0x03, 0x04]); // Write 4 bytes to address 0x1000
// Registering Handlers (for syscalls and other events):
mipsSimulator.registerHandler("printInt", (value: number) => {
console.log("printInt syscall called with:", value);
});
// Handlers may also be async: returning a promise suspends the simulation until it settles,
// so IO can be backed by an async API without blocking the event loop.
mipsSimulator.registerHandler("readInt", async () => {
return await promptUserForANumber();
});
// Accessing the undo stack:
const undoStack = mipsSimulator.getUndoStack();
undoStack.forEach(step => {
if (step.action === BackStepAction.REGISTER_RESTORE) {
console.log(`Register restored at PC ${step.pc}`);
}
});
// Simulating with breakpoints:
const breakpoints = [0x00400004, 0x00400008]; // Example breakpoint addresses
await mipsSimulator.simulateWithBreakpoints(breakpoints);
//Simulating with a limit
const limit = 100
await mipsSimulator.simulateWithLimit(limit);
//Simulating with breakpoints and a limit
await mipsSimulator.simulateWithBreakpointsAndLimit(breakpoints, limit);
//Setting Register Values:
mipsSimulator.setRegisterValue("$t0", 42);IO handlers
Every syscall that needs to talk to the outside world goes through a handler you register. A handler can return its result directly, or return a promise of it:
mipsSimulator.registerHandler("readInt", () => 42); // synchronous
mipsSimulator.registerHandler("readString", () => fetchLine()); // asynchronousWhen a handler returns a promise the simulation suspends at that instruction and resumes once the
promise settles, so nothing has to be shoehorned into a synchronous API. Because of that,
step, simulateWithLimit, simulateWithBreakpoints and simulateWithBreakpointsAndLimit all
return a promise. Everything else on JsMips (registers, memory, statements, the undo stack)
stays synchronous.
If a handler's promise rejects, the pending step/simulate* call rejects as well, with the
rejection reason in the message.
Program time is a handler too, so a run can be given a clock of its own: sleep answers syscall 32
and time answers syscall 30. A live run resolves sleep on a timer and returns Date.now() from
time; a scripted run can settle sleep immediately, advance a virtual clock by the requested
milliseconds and return that clock instead, which keeps elapsed-time output reproducible.
Memory observers
A memory-mapped device - a framebuffer, a keyboard register - is modelled by observing the memory the program reads and writes:
// Every write in a framebuffer: (address, length, value), with the width of the store in bytes.
const frame = mipsSimulator.addMemoryWriteObserver(0x10010000, 0x10012ffc, (address, length, value) => {
screen.markDirty(address)
})
// One memory-mapped register: reads and writes, either of which may be null.
const receiver = mipsSimulator.addMemoryAccessObserver(
0xffff0004,
() => keyboard.consumeCharacter(),
null
)
mipsSimulator.removeMemoryObserver(frame)
mipsSimulator.removeMemoryObservers()- Addresses must be word-aligned,
endAddressis inclusive and covers its whole word, and a range may not cross0x80000000; a registration that breaks any of these throws. Either form of a high address is accepted:0xffff0000and0xffff0000 | 0name the same word. - Handlers are given signed 32 bit integers, as the guest holds them: the register at
0xffff000carrives as-65524, and a pixel word with its high bit set arrives negative. Apply>>> 0wherever the unsigned form is wanted. - Handlers run synchronously inside the instruction that caused the access, so they must be cheap and must not write back into their own range. A returned promise is ignored, unlike an IO handler's.
- An observer is notified after the access, with the value the program read or stored. A
register whose value is consumed by reading it must therefore be reloaded from the handler,
with
setPeripheralWord, for the next read. - Observers live on the simulator's memory, which assembling and initializing only clear the
contents of, so a registration survives
assemble()andinitialize()and - like a registered IO handler - is shared by everyJsMipsinstance. Notifications start once a program has been assembled. undo()restores memory through the same stores, so an observed range reports the restored values as ordinary writes and a device that follows notifications alone stays in step.
API
makeMipsFromFiles(files: MIPSSourceSet, entryFile: string): JsMips
Creates a JsMips instance from a snapshotted virtual source tree. Source paths are canonical,
root-relative, case-sensitive POSIX paths such as src/main.asm. Relative includes resolve from the
including file, and includes beginning with / resolve from the virtual root.
JsMips Interface
Methods
assemble(): Assembles the program.initialize(startAtMain: boolean): Initializes the simulator. IfstartAtMainis true, execution begins at themainlabel; otherwise, it starts at the first instruction.step(): Promise<boolean>: Executes a single instruction. Resolves totrueif the execution is complete,falseotherwise.simulateWithLimit(limit: number): Promise<boolean>: Simulates the program for a maximum oflimitinstructions. Resolves totrueif the execution is complete,falseotherwise.simulateWithBreakpoints(breakpoints: number[]): Promise<boolean>: Simulates the program until a breakpoint is reached.breakpointsis an array of memory addresses. Resolves totrueif the execution is complete,falseotherwise.simulateWithBreakpointsAndLimit(breakpoints: number[], limit: number): Promise<boolean>: Simulates the program until a breakpoint is reached or the limit is reached. Resolves totrueif the execution is complete,falseotherwise.getRegisterValue(register: RegisterName): number: Returns the value of the specified register.registerHandler(name: HandlerName, handler: Function): void: Registers a handler function for a specific event (e.g., syscalls). See theHandlerNametype for possible event names. A handler may return its result directly or return a promise of it, see IO handlers.getStackPointer(): number: Returns the current value of the stack pointer.getProgramCounter(): number: Returns the current value of the program counter.getRegistersValues(): number[]: Returns an array of all register values.getUndoStack(): JsBackStep[]: Returns the undo stack, which contains information about previous simulation steps.readMemoryBytes(address: number, length: number): number[]: Readslengthbytes from memory starting ataddress. Notifies no memory observer: inspecting memory from the host is not the program reading it.setMemoryBytes(address: number, bytes: number[]): void: Writesbytesto memory starting ataddress, the way the program does: write observers are notified and, while undo is enabled, an undo step is recorded per byte.setPeripheralWord(address: number, value: number): void: Writes one word-aligned word as a peripheral would, notifying no observer and recording no undo step. See memory observers.addMemoryWriteObserver(startAddress: number, endAddress: number, handler): number: Observes every write in an address range. See memory observers.addMemoryAccessObserver(address: number, onRead, onWrite): number: Observes reads and writes of one word. See memory observers.removeMemoryObserver(handle: number): void: Removes one registration.removeMemoryObservers(): void: Removes every registration.countMemoryObservers(): number: The number of live registrations.getTokenizedLines(): MipsTokenizedLine[]: Returns the flattened tokenized lines with their original source paths and one-based line numbers.getParsedStatements(): JsProgramStatement[]: Returns the parsed source statements.getCompiledStatements(): JsProgramStatement[]: Returns every generated machine statement.getStatementsAtSourceLocation(sourcePath: string, sourceLine: number): JsProgramStatement[]: Returns every machine statement generated by one original source line, including pseudo-instruction and macro expansions.getNextStatement(): JsProgramStatement: Returns the nextJsProgramStatementto be executed.setRegisterValue(register: RegisterName, value: number): void: Sets the value of the specified register.hasTerminated(): boolean: Returnstrueif the simulation has terminated,falseotherwise.
Types
RegisterName: Type for MIPS register names (e.g.,$zero,$v0,$ra).BackStepAction: Enum representing the types of undo actions.MIPSSourceSet: Read-only mapping from canonical source paths to source text.MipsTokenizedLine: A tokenized line with its original and processed source text and source location.JsProgramStatement: Interface representing a statement in the assembled program.JsBackStep: Interface representing a back step in the simulation.HandlerName: Type representing the name of a handler function.
