# 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](https://github.com/dpetersanderson/MARS) simulator with [TeaVM](https://teavm.org/), 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](https://github.com/Specy/mars),  [npm package](https://www.npmjs.com/package/@specy/mips)
- RISC-V: [git repo](https://github.com/Specy/rars), [npm package](https://www.npmjs.com/package/@specy/risc-v)
- X86: [git repo](https://github.com/Specy/x86-js), [npm package](https://www.npmjs.com/package/@specy/x86)
- M68K: [git repo](https://github.com/Specy/s68k), [npm package](https://www.npmjs.com/package/@specy/s68k)

## Installation

```bash
npm install @specy/mips
```

## Usage

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. 

```typescript
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:

```typescript
mipsSimulator.registerHandler("readInt", () => 42);                  // synchronous
mipsSimulator.registerHandler("readString", () => fetchLine());      // asynchronous
```

When 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:

```ts
// 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, `endAddress` is inclusive and covers its whole word, and a range
    may not cross `0x80000000`; a registration that breaks any of these throws. Either form of a
    high address is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
*   Handlers are given signed 32 bit integers, as the guest holds them: the register at
    `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative.
    Apply `>>> 0` wherever 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()` and `initialize()` and - like a registered
    IO handler - is shared by every `JsMips` instance. 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. If `startAtMain` is true, execution begins at the `main` label; otherwise, it starts at the first instruction.
*   `step(): Promise<boolean>`: Executes a single instruction. Resolves to `true` if the execution is complete, `false` otherwise.
*   `simulateWithLimit(limit: number): Promise<boolean>`: Simulates the program for a maximum of `limit` instructions. Resolves to `true` if the execution is complete, `false` otherwise.
*   `simulateWithBreakpoints(breakpoints: number[]): Promise<boolean>`: Simulates the program until a breakpoint is reached.  `breakpoints` is an array of memory addresses. Resolves to `true` if the execution is complete, `false` otherwise.
*   `simulateWithBreakpointsAndLimit(breakpoints: number[], limit: number): Promise<boolean>`: Simulates the program until a breakpoint is reached or the limit is reached. Resolves to `true` if the execution is complete, `false` otherwise.
*   `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 the `HandlerName` type for possible event names. A handler may return its result directly or return a promise of it, see [IO handlers](#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[]`: Reads `length` bytes from memory starting at `address`. Notifies no memory observer: inspecting memory from the host is not the program reading it.
*   `setMemoryBytes(address: number, bytes: number[]): void`: Writes `bytes` to memory starting at `address`, 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](#memory-observers).
*   `addMemoryWriteObserver(startAddress: number, endAddress: number, handler): number`: Observes every write in an address range. See [memory observers](#memory-observers).
*   `addMemoryAccessObserver(address: number, onRead, onWrite): number`: Observes reads and writes of one word. See [memory observers](#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 next `JsProgramStatement` to be executed.
*   `setRegisterValue(register: RegisterName, value: number): void`: Sets the value of the specified register.
*   `hasTerminated(): boolean`: Returns `true` if the simulation has terminated, `false` otherwise.

#### 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.
