@specy/risc-v 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -102,6 +102,52 @@ Synchronous handlers never yield to the event loop: the simulation runs straight
102
102
  returned promise settles on a microtask. Overlapping calls are queued and run one after another,
103
103
  never concurrently.
104
104
 
105
+ Program time is a handler too, so a run can be given a clock of its own: `sleep` answers syscall 32
106
+ and `time` answers syscall 30. A live run resolves `sleep` on a timer and returns `Date.now()` from
107
+ `time`; a scripted run can settle `sleep` immediately, advance a virtual clock by the requested
108
+ milliseconds and return that clock instead, which keeps elapsed-time output reproducible.
109
+
110
+ ## Memory observers
111
+
112
+ A memory-mapped device - a framebuffer, a keyboard register - is modelled by observing the memory
113
+ the program reads and writes:
114
+
115
+ ```ts
116
+ // Every write in a framebuffer: (address, length, value), with the width of the store in bytes.
117
+ const frame = riscvSimulator.addMemoryWriteObserver(0x10010000, 0x10012ffc, (address, length, value) => {
118
+ screen.markDirty(address)
119
+ })
120
+
121
+ // One memory-mapped register: reads and writes, either of which may be null.
122
+ const receiver = riscvSimulator.addMemoryAccessObserver(
123
+ 0xffff0004,
124
+ () => keyboard.consumeCharacter(),
125
+ null
126
+ )
127
+
128
+ riscvSimulator.removeMemoryObserver(frame)
129
+ riscvSimulator.removeMemoryObservers()
130
+ ```
131
+
132
+ * Addresses must be word-aligned, `endAddress` is inclusive and covers its whole word, and a range
133
+ may not cross `0x80000000`; a registration that breaks any of these throws. Either form of a
134
+ high address is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
135
+ * Handlers are given signed 32 bit integers, as the guest holds them: the register at
136
+ `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative.
137
+ Apply `>>> 0` wherever the unsigned form is wanted.
138
+ * Handlers run synchronously inside the instruction that caused the access, so they must be cheap
139
+ and must not write back into their own range. A returned promise is ignored, unlike an IO
140
+ handler's.
141
+ * An observer is notified *after* the access, with the value the program read or stored. A
142
+ register whose value is consumed by reading it must therefore be reloaded from the handler,
143
+ with `setPeripheralWord`, for the next read.
144
+ * Observers live on the simulator's memory, which assembling and initializing only clear the
145
+ contents of, so a registration survives `assemble()` and `initialize()` and - like a registered
146
+ IO handler - is shared by every `JsRiscV` instance. Notifications start once a program has been
147
+ assembled.
148
+ * `undo()` restores memory through the same stores, so an observed range reports the restored
149
+ values as ordinary writes and a device that follows notifications alone stays in step.
150
+
105
151
  ## API
106
152
 
107
153
  ### `RISCV` Static Class
@@ -145,8 +191,14 @@ This interface provides methods to control and interact with a RISC-V simulator
145
191
  * `getConditionFlags(): number[]`: Gets the 8 condition flags (if applicable, typically related to floating-point or custom extensions).
146
192
  * `registerHandler<T extends HandlerName>(name: T, handler: (...args: HandlerMap[T]['in']) => HandlerMap[T]['out'] | Promise<HandlerMap[T]['out']>): void`: Registers a handler function for a specific event (e.g., syscalls). See `HandlerName`, `HandlerMap` and [IO handlers](#io-handlers) for details.
147
193
  * `getUndoStack(): JsBackStep[]`: Returns the undo stack, an array of `JsBackStep` objects representing simulation history.
148
- * `readMemoryBytes(address: number, length: number): number[]`: Reads `length` bytes from memory starting at `address`. Returns an array of byte values.
149
- * `setMemoryBytes(address: number, bytes: number[]): void`: Writes an array of `bytes` to memory starting at `address`.
194
+ * `readMemoryBytes(address: number, length: number): number[]`: Reads `length` bytes from memory starting at `address`. Returns an array of byte values. Notifies no memory observer: inspecting memory from the host is not the program reading it.
195
+ * `setMemoryBytes(address: number, bytes: number[]): void`: Writes an array of `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.
196
+ * `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).
197
+ * `addMemoryWriteObserver(startAddress: number, endAddress: number, handler): number`: Observes every write in an address range. See [memory observers](#memory-observers).
198
+ * `addMemoryAccessObserver(address: number, onRead, onWrite): number`: Observes reads and writes of one word. See [memory observers](#memory-observers).
199
+ * `removeMemoryObserver(handle: number): void`: Removes one registration.
200
+ * `removeMemoryObservers(): void`: Removes every registration.
201
+ * `countMemoryObservers(): number`: The number of live registrations.
150
202
  * `getCurrentStatementIndex(): number`: Returns the index of the current instruction in the list of assembled program statements.
151
203
  * `getNextStatement(): JsProgramStatement | null`: Returns the next `JsProgramStatement` to be executed, or `null` if at the end.
152
204
  * `getStatementAtAddress(address: number): JsProgramStatement | null`: Gets the program statement at the given memory `address`.
package/dist/index.d.mts CHANGED
@@ -119,10 +119,23 @@ type HandlerMap = {
119
119
  in: [l: string];
120
120
  out: void;
121
121
  };
122
+ /**
123
+ * Syscall 32: the program asks to be suspended for this many milliseconds. Return a promise
124
+ * that settles when the wait is over to suspend the simulation without blocking the host.
125
+ */
122
126
  sleep: {
123
127
  in: [milliseconds: number];
124
128
  out: void;
125
129
  };
130
+ /**
131
+ * Syscall 30: the program time in milliseconds, split by the syscall into a0 (low word) and
132
+ * a1 (high word). Answer with `Date.now()` for a live run, or with a virtual clock for a
133
+ * scripted one, so that elapsed-time output stays reproducible.
134
+ */
135
+ time: {
136
+ in: [];
137
+ out: number;
138
+ };
126
139
  stdIn: {
127
140
  in: [buffer: number[], length: number];
128
141
  out: void;
@@ -136,6 +149,27 @@ type HandlerMap = {
136
149
  out: void;
137
150
  };
138
151
  };
152
+ /**
153
+ * Notified after a write anywhere in an observed address range.
154
+ *
155
+ * `length` is the width of the access in bytes (4, 2 or 1) and `value` is what the program stored,
156
+ * so a byte or halfword store reports only the bytes it touched; a peripheral that mirrors whole
157
+ * words should re-read the containing word with `readMemoryBytes` rather than trust `value`.
158
+ *
159
+ * Both `address` and `value` are signed 32 bit integers, as the guest holds them: the register at
160
+ * `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative too.
161
+ * Apply `>>> 0` wherever the unsigned form is wanted.
162
+ */
163
+ type MemoryWriteObserver = (address: number, length: number, value: number) => void;
164
+ /**
165
+ * Notified after a read or a write of one observed word, with the same signed 32 bit numbers as
166
+ * `MemoryWriteObserver`. `value` is the value the program read or stored; on a read it is what
167
+ * memory held *before* the observer ran, so a register whose value is consumed by reading it must
168
+ * be reloaded from the handler for the next read.
169
+ */
170
+ type MemoryAccessObserver = (address: number, value: number) => void;
171
+ /** Identifies one registration, for `removeMemoryObserver`. */
172
+ type MemoryObserverHandle = number;
139
173
  type JsInstruction = {
140
174
  name: string;
141
175
  example: string;
@@ -459,6 +493,9 @@ interface JsRiscV {
459
493
  getUndoStack(): JsBackStep[];
460
494
  /**
461
495
  * Reads a sequence of bytes from memory.
496
+ *
497
+ * Reading through this method notifies no memory observer: inspecting memory from the host is
498
+ * not the program reading it, so a memory viewer never drives a memory-mapped register.
462
499
  * @param address The starting memory address.
463
500
  * @param length The number of bytes to read.
464
501
  * @returns An array of bytes read from memory.
@@ -466,10 +503,63 @@ interface JsRiscV {
466
503
  readMemoryBytes(address: number, length: number): number[];
467
504
  /**
468
505
  * Writes a sequence of bytes to memory.
506
+ *
507
+ * Unlike `readMemoryBytes`, this writes the way the program does: it notifies write observers
508
+ * and, while undo is enabled, records an undo step per byte. Use `setPeripheralWord` for a
509
+ * device keeping its own register up to date.
469
510
  * @param address The starting memory address.
470
511
  * @param bytes An array of bytes to write to memory.
471
512
  */
472
513
  setMemoryBytes(address: number, bytes: number[]): void;
514
+ /**
515
+ * Writes one word as a peripheral would: no observer is notified and no undo step is recorded,
516
+ * because the write is not the program acting. This is how a device model refreshes a
517
+ * memory-mapped register - a ready bit, a pending character - without feeding its own observer
518
+ * or consuming undo history.
519
+ * @param address The word address, which must be word-aligned. Either form of a high address
520
+ * is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
521
+ * @param value The 32 bit value to store, raw, without byte-order adjustment.
522
+ */
523
+ setPeripheralWord(address: number, value: number): void;
524
+ /**
525
+ * Observes every write in an address range, the shape a framebuffer wants.
526
+ *
527
+ * Both addresses must be word-aligned, `endAddress` is inclusive and covers its whole word, and
528
+ * the range may not cross 0x80000000 (split it in two registrations instead); a range that
529
+ * breaks any of these throws. Either form of a high address is accepted: `0xffff0000` and
530
+ * `0xffff0000 | 0` name the same word. The handler runs synchronously inside the storing instruction, so
531
+ * it must be cheap and must not write back into its own range; a returned promise is ignored.
532
+ *
533
+ * Observers live on the simulator's memory, which assembling and initializing only clear the
534
+ * contents of, so a registration survives `assemble()` and `initialize()`. For the same reason
535
+ * it is shared by every `JsRiscV` instance: register once per page, or remove the previous
536
+ * registration before registering again for a newly built program.
537
+ *
538
+ * Notifications only start once a program has been assembled, and undo notifies too: restoring
539
+ * memory during `undo()` goes through the same stores, so an observed range reports the
540
+ * restored values as ordinary writes.
541
+ * @returns A handle for `removeMemoryObserver`.
542
+ */
543
+ addMemoryWriteObserver(startAddress: number, endAddress: number, handler: MemoryWriteObserver): MemoryObserverHandle;
544
+ /**
545
+ * Observes reads and writes of a single word, the shape a memory-mapped register wants. The
546
+ * address must be word-aligned. Pass `null` for a direction you do not care about. The same
547
+ * lifetime and synchronous-handler rules as `addMemoryWriteObserver` apply.
548
+ * @returns A handle for `removeMemoryObserver`.
549
+ */
550
+ addMemoryAccessObserver(address: number, onRead: MemoryAccessObserver | null, onWrite: MemoryAccessObserver | null): MemoryObserverHandle;
551
+ /**
552
+ * Removes one registration. An unknown handle is ignored.
553
+ */
554
+ removeMemoryObserver(handle: MemoryObserverHandle): void;
555
+ /**
556
+ * Removes every registration.
557
+ */
558
+ removeMemoryObservers(): void;
559
+ /**
560
+ * The number of live registrations, across every `JsRiscV` instance.
561
+ */
562
+ countMemoryObservers(): number;
473
563
  /**
474
564
  * Gets the index of the current statement in the assembled program.
475
565
  * @returns The index of the current statement.
@@ -504,4 +594,4 @@ declare function makeRiscVfromSource(source: string): JsRiscV;
504
594
  declare function initializeRISCV(): void;
505
595
  declare function bigintToHighLow(value: bigint): [high: number, low: number];
506
596
 
507
- export { BackStepAction, ConfirmResult, DialogType, type HandlerMap, type HandlerMapFns, type JsBackStep, type JsInstruction, type JsInstructionToken, type JsProgramStatement, type JsRiscV, type JsRiscVStackFrame, RISCV, type RISCVAssembleError, type RISCVAssembleResult, RISCVRegisters, RISCV_REGISTERS, type RegisterName, type RiscvTokenizedLine, StopReason, bigintToHighLow, registerHandlers, unimplementedHandler };
597
+ export { BackStepAction, ConfirmResult, DialogType, type HandlerMap, type HandlerMapFns, type JsBackStep, type JsInstruction, type JsInstructionToken, type JsProgramStatement, type JsRiscV, type JsRiscVStackFrame, type MemoryAccessObserver, type MemoryObserverHandle, type MemoryWriteObserver, RISCV, type RISCVAssembleError, type RISCVAssembleResult, RISCVRegisters, RISCV_REGISTERS, type RegisterName, type RiscvTokenizedLine, StopReason, bigintToHighLow, registerHandlers, unimplementedHandler };
package/dist/index.d.ts CHANGED
@@ -119,10 +119,23 @@ type HandlerMap = {
119
119
  in: [l: string];
120
120
  out: void;
121
121
  };
122
+ /**
123
+ * Syscall 32: the program asks to be suspended for this many milliseconds. Return a promise
124
+ * that settles when the wait is over to suspend the simulation without blocking the host.
125
+ */
122
126
  sleep: {
123
127
  in: [milliseconds: number];
124
128
  out: void;
125
129
  };
130
+ /**
131
+ * Syscall 30: the program time in milliseconds, split by the syscall into a0 (low word) and
132
+ * a1 (high word). Answer with `Date.now()` for a live run, or with a virtual clock for a
133
+ * scripted one, so that elapsed-time output stays reproducible.
134
+ */
135
+ time: {
136
+ in: [];
137
+ out: number;
138
+ };
126
139
  stdIn: {
127
140
  in: [buffer: number[], length: number];
128
141
  out: void;
@@ -136,6 +149,27 @@ type HandlerMap = {
136
149
  out: void;
137
150
  };
138
151
  };
152
+ /**
153
+ * Notified after a write anywhere in an observed address range.
154
+ *
155
+ * `length` is the width of the access in bytes (4, 2 or 1) and `value` is what the program stored,
156
+ * so a byte or halfword store reports only the bytes it touched; a peripheral that mirrors whole
157
+ * words should re-read the containing word with `readMemoryBytes` rather than trust `value`.
158
+ *
159
+ * Both `address` and `value` are signed 32 bit integers, as the guest holds them: the register at
160
+ * `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative too.
161
+ * Apply `>>> 0` wherever the unsigned form is wanted.
162
+ */
163
+ type MemoryWriteObserver = (address: number, length: number, value: number) => void;
164
+ /**
165
+ * Notified after a read or a write of one observed word, with the same signed 32 bit numbers as
166
+ * `MemoryWriteObserver`. `value` is the value the program read or stored; on a read it is what
167
+ * memory held *before* the observer ran, so a register whose value is consumed by reading it must
168
+ * be reloaded from the handler for the next read.
169
+ */
170
+ type MemoryAccessObserver = (address: number, value: number) => void;
171
+ /** Identifies one registration, for `removeMemoryObserver`. */
172
+ type MemoryObserverHandle = number;
139
173
  type JsInstruction = {
140
174
  name: string;
141
175
  example: string;
@@ -459,6 +493,9 @@ interface JsRiscV {
459
493
  getUndoStack(): JsBackStep[];
460
494
  /**
461
495
  * Reads a sequence of bytes from memory.
496
+ *
497
+ * Reading through this method notifies no memory observer: inspecting memory from the host is
498
+ * not the program reading it, so a memory viewer never drives a memory-mapped register.
462
499
  * @param address The starting memory address.
463
500
  * @param length The number of bytes to read.
464
501
  * @returns An array of bytes read from memory.
@@ -466,10 +503,63 @@ interface JsRiscV {
466
503
  readMemoryBytes(address: number, length: number): number[];
467
504
  /**
468
505
  * Writes a sequence of bytes to memory.
506
+ *
507
+ * Unlike `readMemoryBytes`, this writes the way the program does: it notifies write observers
508
+ * and, while undo is enabled, records an undo step per byte. Use `setPeripheralWord` for a
509
+ * device keeping its own register up to date.
469
510
  * @param address The starting memory address.
470
511
  * @param bytes An array of bytes to write to memory.
471
512
  */
472
513
  setMemoryBytes(address: number, bytes: number[]): void;
514
+ /**
515
+ * Writes one word as a peripheral would: no observer is notified and no undo step is recorded,
516
+ * because the write is not the program acting. This is how a device model refreshes a
517
+ * memory-mapped register - a ready bit, a pending character - without feeding its own observer
518
+ * or consuming undo history.
519
+ * @param address The word address, which must be word-aligned. Either form of a high address
520
+ * is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
521
+ * @param value The 32 bit value to store, raw, without byte-order adjustment.
522
+ */
523
+ setPeripheralWord(address: number, value: number): void;
524
+ /**
525
+ * Observes every write in an address range, the shape a framebuffer wants.
526
+ *
527
+ * Both addresses must be word-aligned, `endAddress` is inclusive and covers its whole word, and
528
+ * the range may not cross 0x80000000 (split it in two registrations instead); a range that
529
+ * breaks any of these throws. Either form of a high address is accepted: `0xffff0000` and
530
+ * `0xffff0000 | 0` name the same word. The handler runs synchronously inside the storing instruction, so
531
+ * it must be cheap and must not write back into its own range; a returned promise is ignored.
532
+ *
533
+ * Observers live on the simulator's memory, which assembling and initializing only clear the
534
+ * contents of, so a registration survives `assemble()` and `initialize()`. For the same reason
535
+ * it is shared by every `JsRiscV` instance: register once per page, or remove the previous
536
+ * registration before registering again for a newly built program.
537
+ *
538
+ * Notifications only start once a program has been assembled, and undo notifies too: restoring
539
+ * memory during `undo()` goes through the same stores, so an observed range reports the
540
+ * restored values as ordinary writes.
541
+ * @returns A handle for `removeMemoryObserver`.
542
+ */
543
+ addMemoryWriteObserver(startAddress: number, endAddress: number, handler: MemoryWriteObserver): MemoryObserverHandle;
544
+ /**
545
+ * Observes reads and writes of a single word, the shape a memory-mapped register wants. The
546
+ * address must be word-aligned. Pass `null` for a direction you do not care about. The same
547
+ * lifetime and synchronous-handler rules as `addMemoryWriteObserver` apply.
548
+ * @returns A handle for `removeMemoryObserver`.
549
+ */
550
+ addMemoryAccessObserver(address: number, onRead: MemoryAccessObserver | null, onWrite: MemoryAccessObserver | null): MemoryObserverHandle;
551
+ /**
552
+ * Removes one registration. An unknown handle is ignored.
553
+ */
554
+ removeMemoryObserver(handle: MemoryObserverHandle): void;
555
+ /**
556
+ * Removes every registration.
557
+ */
558
+ removeMemoryObservers(): void;
559
+ /**
560
+ * The number of live registrations, across every `JsRiscV` instance.
561
+ */
562
+ countMemoryObservers(): number;
473
563
  /**
474
564
  * Gets the index of the current statement in the assembled program.
475
565
  * @returns The index of the current statement.
@@ -504,4 +594,4 @@ declare function makeRiscVfromSource(source: string): JsRiscV;
504
594
  declare function initializeRISCV(): void;
505
595
  declare function bigintToHighLow(value: bigint): [high: number, low: number];
506
596
 
507
- export { BackStepAction, ConfirmResult, DialogType, type HandlerMap, type HandlerMapFns, type JsBackStep, type JsInstruction, type JsInstructionToken, type JsProgramStatement, type JsRiscV, type JsRiscVStackFrame, RISCV, type RISCVAssembleError, type RISCVAssembleResult, RISCVRegisters, RISCV_REGISTERS, type RegisterName, type RiscvTokenizedLine, StopReason, bigintToHighLow, registerHandlers, unimplementedHandler };
597
+ export { BackStepAction, ConfirmResult, DialogType, type HandlerMap, type HandlerMapFns, type JsBackStep, type JsInstruction, type JsInstructionToken, type JsProgramStatement, type JsRiscV, type JsRiscVStackFrame, type MemoryAccessObserver, type MemoryObserverHandle, type MemoryWriteObserver, RISCV, type RISCVAssembleError, type RISCVAssembleResult, RISCVRegisters, RISCV_REGISTERS, type RegisterName, type RiscvTokenizedLine, StopReason, bigintToHighLow, registerHandlers, unimplementedHandler };