@specy/risc-v 2.0.1 → 3.0.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
@@ -9,22 +9,33 @@ It is part of a family of javascript assembly interpreters/simulators:
9
9
 
10
10
  ## Usage
11
11
 
12
- First, create an instance of the simulator with the `RISCV.makeRiscvFromSource` function.
12
+ First, create an instance of the simulator with `makeRiscVFromFiles`. Supply a virtual source tree and the path of its entry file. Relative `.include` paths resolve from the file containing the directive, while paths beginning with `/` resolve from the virtual root.
13
13
 
14
14
  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.
15
15
 
16
16
  ⚠️**WARNING**⚠️ You must have only one instance of the simulator at a time. Memory, registers, and other state may be shared or behave unpredictably with multiple instances.
17
17
 
18
18
  ```typescript
19
- import {RISCV, JsRiscV, RegisterName, BackStepAction} from '@specy/risc-v';
20
- const sourceCode = `
21
- li t0, 5 # Load immediate value 5 into register t0
22
- li t1, 7 # Load immediate value 7 into register t1
23
- add t2, t0, t1 # Add t0 and t1, store result in t2
24
- `;
19
+ import {RISCV, makeRiscVFromFiles, JsRiscV, RegisterName, BackStepAction} from '@specy/risc-v';
20
+
21
+ const files = {
22
+ 'src/main.asm': `
23
+ .include "lib/math.asm"
24
+ .text
25
+ .globl main
26
+ main:
27
+ add_values(t2, 5, 7)
28
+ `,
29
+ 'src/lib/math.asm': `
30
+ .macro add_values(%result, %left, %right)
31
+ li %result, %left
32
+ addi %result, %result, %right
33
+ .end_macro
34
+ `,
35
+ } as const;
25
36
 
26
37
  // RISCV.setIs64Bit(true); // Set to 64-bit mode if needed
27
- const riscvSimulator: JsRiscV = RISCV.makeRiscVFromSource(sourceCode);
38
+ const riscvSimulator: JsRiscV = makeRiscVFromFiles(files, 'src/main.asm');
28
39
 
29
40
  riscvSimulator.assemble();
30
41
  riscvSimulator.initialize(true); // Start at 'main'
@@ -102,16 +113,62 @@ Synchronous handlers never yield to the event loop: the simulation runs straight
102
113
  returned promise settles on a microtask. Overlapping calls are queued and run one after another,
103
114
  never concurrently.
104
115
 
116
+ Program time is a handler too, so a run can be given a clock of its own: `sleep` answers syscall 32
117
+ and `time` answers syscall 30. A live run resolves `sleep` on a timer and returns `Date.now()` from
118
+ `time`; a scripted run can settle `sleep` immediately, advance a virtual clock by the requested
119
+ milliseconds and return that clock instead, which keeps elapsed-time output reproducible.
120
+
121
+ ## Memory observers
122
+
123
+ A memory-mapped device - a framebuffer, a keyboard register - is modelled by observing the memory
124
+ the program reads and writes:
125
+
126
+ ```ts
127
+ // Every write in a framebuffer: (address, length, value), with the width of the store in bytes.
128
+ const frame = riscvSimulator.addMemoryWriteObserver(0x10010000, 0x10012ffc, (address, length, value) => {
129
+ screen.markDirty(address)
130
+ })
131
+
132
+ // One memory-mapped register: reads and writes, either of which may be null.
133
+ const receiver = riscvSimulator.addMemoryAccessObserver(
134
+ 0xffff0004,
135
+ () => keyboard.consumeCharacter(),
136
+ null
137
+ )
138
+
139
+ riscvSimulator.removeMemoryObserver(frame)
140
+ riscvSimulator.removeMemoryObservers()
141
+ ```
142
+
143
+ * Addresses must be word-aligned, `endAddress` is inclusive and covers its whole word, and a range
144
+ may not cross `0x80000000`; a registration that breaks any of these throws. Either form of a
145
+ high address is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
146
+ * Handlers are given signed 32 bit integers, as the guest holds them: the register at
147
+ `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative.
148
+ Apply `>>> 0` wherever the unsigned form is wanted.
149
+ * Handlers run synchronously inside the instruction that caused the access, so they must be cheap
150
+ and must not write back into their own range. A returned promise is ignored, unlike an IO
151
+ handler's.
152
+ * An observer is notified *after* the access, with the value the program read or stored. A
153
+ register whose value is consumed by reading it must therefore be reloaded from the handler,
154
+ with `setPeripheralWord`, for the next read.
155
+ * Observers live on the simulator's memory, which assembling and initializing only clear the
156
+ contents of, so a registration survives `assemble()` and `initialize()` and - like a registered
157
+ IO handler - is shared by every `JsRiscV` instance. Notifications start once a program has been
158
+ assembled.
159
+ * `undo()` restores memory through the same stores, so an observed range reports the restored
160
+ values as ordinary writes and a device that follows notifications alone stays in step.
161
+
105
162
  ## API
106
163
 
107
164
  ### `RISCV` Static Class
108
165
 
109
166
  Provides static methods to initialize and create simulator instances.
110
167
 
111
- * `RISCV.makeRiscvFromSource(source: string): JsRiscv`
112
- Creates a new `JsRiscv` instance from RISC-V assembly source code. This also initializes the underlying RISC-V simulation environment if it hasn't been already.
168
+ * `makeRiscVFromFiles(files: RISCVSourceSet, entryFile: string): JsRiscV`
169
+ Creates a simulator from a snapshot of a virtual source tree and its entry file. Source paths are canonical, root-relative POSIX paths. The factory is also available as `RISCV.makeRiscVFromFiles` and initializes the simulation environment when needed.
113
170
  * `RISCV.initializeRISCV(): void`
114
- Initializes the core RISC-V simulation environment. Called internally by `makeRiscvFromSource`, but can be called explicitly if needed.
171
+ Initializes the core RISC-V simulation environment. Called internally by `makeRiscVFromFiles`, but can be called explicitly if needed.
115
172
  * `RISCV.getInstructionSet(): JsInstruction[]`
116
173
  Returns an array of objects, each describing a supported RISC-V instruction (name, example, description, tokens).
117
174
  * `RISCV.setIs64Bit(is64Bit: boolean): void`
@@ -145,15 +202,20 @@ This interface provides methods to control and interact with a RISC-V simulator
145
202
  * `getConditionFlags(): number[]`: Gets the 8 condition flags (if applicable, typically related to floating-point or custom extensions).
146
203
  * `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
204
  * `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`.
150
- * `getCurrentStatementIndex(): number`: Returns the index of the current instruction in the list of assembled program statements.
205
+ * `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.
206
+ * `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.
207
+ * `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).
208
+ * `addMemoryWriteObserver(startAddress: number, endAddress: number, handler): number`: Observes every write in an address range. See [memory observers](#memory-observers).
209
+ * `addMemoryAccessObserver(address: number, onRead, onWrite): number`: Observes reads and writes of one word. See [memory observers](#memory-observers).
210
+ * `removeMemoryObserver(handle: number): void`: Removes one registration.
211
+ * `removeMemoryObservers(): void`: Removes every registration.
212
+ * `countMemoryObservers(): number`: The number of live registrations.
151
213
  * `getNextStatement(): JsProgramStatement | null`: Returns the next `JsProgramStatement` to be executed, or `null` if at the end.
152
214
  * `getStatementAtAddress(address: number): JsProgramStatement | null`: Gets the program statement at the given memory `address`.
153
- * `getStatementAtSourceLine(line: number): JsProgramStatement | null`: Gets the program statement corresponding to the original source `line` number.
154
- * `getCompiledStatements(): JsProgramStatement[]`: Returns an array of all assembled program statements.
155
- * `getParsedStatements(): JsInstructionToken[]`: Returns an array of tokens from the initial parsing stage.
156
- * `getTokenizedLines(): RiscvTokenizedLine[]`: Returns an array of lines, each with its source string and corresponding tokens.
215
+ * `getStatementsAtSourceLocation(sourcePath: string, sourceLine: number): JsProgramStatement[]`: Returns every machine statement generated from an original one-based source location, in address order. Pseudo-instructions and macro calls can produce multiple results.
216
+ * `getCompiledStatements(): JsProgramStatement[]`: Returns all machine statements after a successful assembly.
217
+ * `getParsedStatements(): JsProgramStatement[]`: Returns the parsed statements before pseudo-instruction expansion.
218
+ * `getTokenizedLines(): RiscvTokenizedLine[]`: Returns the expanded source stream with each line's path, one-based line number, original source, processed source, and tokens. It is available after tokenization succeeds, including when a later assembly stage fails.
157
219
  * `getCallStack(): JsRiscvStackFrame[]`: Returns the current call stack as an array of stack frame objects.
158
220
  * `getLabelAtAddress(address: number): string | null`: Returns the label name at the given memory `address`, or `null` if no label exists there.
159
221
 
@@ -165,6 +227,8 @@ This interface provides methods to control and interact with a RISC-V simulator
165
227
  #### Types
166
228
 
167
229
  * `RegisterName`: Union type for RISC-V register names (e.g., `'ra'`, `'sp'`, `'a0'`, `'t0'`, etc.).
230
+ * `RISCVSourceSet`: A read-only mapping from canonical virtual source paths to source strings.
231
+ * `RISCVSourceLocation`: A source path and one-based source line.
168
232
  * `HandlerName`: Union type of all possible handler names (keys of `HandlerMap`).
169
233
  * `HandlerMap`: An object type mapping `HandlerName`s to their expected input argument types (`in`) and return type (`out`). This defines the signature for syscall/event handlers.
170
234
  * `HandlerMapFns`: An object type where keys are `HandlerName`s and values are the corresponding handler functions.
@@ -172,10 +236,10 @@ This interface provides methods to control and interact with a RISC-V simulator
172
236
  * `ConfirmResult`: Enum for confirm dialog results (`YES`, `NO`, `CANCEL`).
173
237
  * `BackStepAction`: Enum representing types of actions that can be undone (e.g., `MEMORY_RESTORE_WORD`, `REGISTER_RESTORE`).
174
238
  * `JsBackStep`: Interface representing an entry in the undo stack, detailing the action, parameters, and PC value.
175
- * `JsProgramStatement`: Interface representing an assembled instruction, including source line, address, binary/machine/assembly representations.
176
- * `JsInstructionToken`: Interface representing a token from the assembly source (value, type, source position).
239
+ * `JsProgramStatement`: Interface representing an assembled instruction, including its source path, source line, address, binary/machine/assembly representations, and full original source line.
240
+ * `JsInstructionToken`: Interface representing a token from the assembly source (value, type, and one-based source column).
177
241
  * `JsInstruction`: Interface describing a RISC-V instruction (name, example, description, token patterns).
178
- * `RiscvTokenizedLine`: An object containing the original line string and its parsed `JsInstructionToken`s.
179
- * `RISCVAssembleError`: Interface describing an assembly error (message, line/column, warning status, etc.).
242
+ * `RiscvTokenizedLine`: An object containing source identity, original and processed line strings, and parsed `JsInstructionToken`s.
243
+ * `RISCVAssembleError`: Interface describing an assembly diagnostic with its source path, one-based line and column, warning status, and structured macro expansion trace.
180
244
  * `RISCVAssembleResult`: Interface for the result of `assemble()`, containing a report string, list of `RISCVAssembleError`s, and a `hasErrors` boolean.
181
- * `JsRiscvStackFrame`: Interface describing a frame on the call stack (PC, target address, SP, FP, register snapshot).
245
+ * `JsRiscvStackFrame`: Interface describing a frame on the call stack (PC, target address, SP, FP, register snapshot).
package/dist/index.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  type JsInstructionToken = {
2
- sourceLine: number;
2
+ /** One-based column in the processed source line. */
3
3
  sourceColumn: number;
4
- originalSourceLine: number;
5
4
  value: string;
6
5
  type: string;
7
6
  };
@@ -119,10 +118,23 @@ type HandlerMap = {
119
118
  in: [l: string];
120
119
  out: void;
121
120
  };
121
+ /**
122
+ * Syscall 32: the program asks to be suspended for this many milliseconds. Return a promise
123
+ * that settles when the wait is over to suspend the simulation without blocking the host.
124
+ */
122
125
  sleep: {
123
126
  in: [milliseconds: number];
124
127
  out: void;
125
128
  };
129
+ /**
130
+ * Syscall 30: the program time in milliseconds, split by the syscall into a0 (low word) and
131
+ * a1 (high word). Answer with `Date.now()` for a live run, or with a virtual clock for a
132
+ * scripted one, so that elapsed-time output stays reproducible.
133
+ */
134
+ time: {
135
+ in: [];
136
+ out: number;
137
+ };
126
138
  stdIn: {
127
139
  in: [buffer: number[], length: number];
128
140
  out: void;
@@ -136,6 +148,27 @@ type HandlerMap = {
136
148
  out: void;
137
149
  };
138
150
  };
151
+ /**
152
+ * Notified after a write anywhere in an observed address range.
153
+ *
154
+ * `length` is the width of the access in bytes (4, 2 or 1) and `value` is what the program stored,
155
+ * so a byte or halfword store reports only the bytes it touched; a peripheral that mirrors whole
156
+ * words should re-read the containing word with `readMemoryBytes` rather than trust `value`.
157
+ *
158
+ * Both `address` and `value` are signed 32 bit integers, as the guest holds them: the register at
159
+ * `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative too.
160
+ * Apply `>>> 0` wherever the unsigned form is wanted.
161
+ */
162
+ type MemoryWriteObserver = (address: number, length: number, value: number) => void;
163
+ /**
164
+ * Notified after a read or a write of one observed word, with the same signed 32 bit numbers as
165
+ * `MemoryWriteObserver`. `value` is the value the program read or stored; on a read it is what
166
+ * memory held *before* the observer ran, so a register whose value is consumed by reading it must
167
+ * be reloaded from the handler for the next read.
168
+ */
169
+ type MemoryAccessObserver = (address: number, value: number) => void;
170
+ /** Identifies one registration, for `removeMemoryObserver`. */
171
+ type MemoryObserverHandle = number;
139
172
  type JsInstruction = {
140
173
  name: string;
141
174
  example: string;
@@ -144,16 +177,30 @@ type JsInstruction = {
144
177
  getIsRv64Only: () => boolean;
145
178
  };
146
179
  type RiscvTokenizedLine = {
147
- line: string;
180
+ sourcePath: string;
181
+ /** One-based line in `sourcePath`. */
182
+ sourceLine: number;
183
+ /** Exact line supplied in the source set. */
184
+ source: string;
185
+ /** Line after assembler substitutions such as `.eqv`. */
186
+ processedSource: string;
148
187
  tokens: JsInstructionToken[];
149
188
  };
189
+ type RISCVSourceLocation = {
190
+ sourcePath: string;
191
+ /** One-based line in `sourcePath`. */
192
+ sourceLine: number;
193
+ };
194
+ type RISCVSourceSet = Readonly<Record<string, string>>;
150
195
  type RISCVAssembleError = {
151
196
  isWarning: boolean;
152
197
  message: string;
153
- macroExpansionHistory: string;
154
- filename: string;
155
- lineNumber: number;
156
- columnNumber: number;
198
+ macroExpansionTrace: RISCVSourceLocation[];
199
+ sourcePath: string;
200
+ /** One-based line in `sourcePath`. */
201
+ sourceLine: number;
202
+ /** One-based column in `sourcePath`. Zero only for diagnostics without a source location. */
203
+ sourceColumn: number;
157
204
  };
158
205
  type RISCVAssembleResult = {
159
206
  report: string;
@@ -164,7 +211,7 @@ type RISCVAssembleResult = {
164
211
  hasWarnings: boolean;
165
212
  };
166
213
  declare class RISCV {
167
- static makeRiscVFromSource: typeof makeRiscVfromSource;
214
+ static makeRiscVFromFiles: typeof makeRiscVFromFiles;
168
215
  static initializeRISCV: typeof initializeRISCV;
169
216
  static getInstructionSet(): JsInstruction[];
170
217
  static setIs64Bit(is64Bit: boolean): void;
@@ -196,9 +243,9 @@ type JsRiscVStackFrame = {
196
243
  * Represents a statement in the assembled program.
197
244
  */
198
245
  interface JsProgramStatement {
199
- /**
200
- * The line number in the original source code.
201
- */
246
+ /** Canonical path of the original source file. */
247
+ readonly sourcePath: string;
248
+ /** The one-based line number in the original source file. */
202
249
  readonly sourceLine: number;
203
250
  /**
204
251
  * The memory address of the instruction.
@@ -348,11 +395,8 @@ interface JsRiscV {
348
395
  * @param address
349
396
  */
350
397
  getStatementAtAddress(address: number): JsProgramStatement;
351
- /**
352
- * Gets the statement at the given source line.
353
- * @param line
354
- */
355
- getStatementAtSourceLine(line: number): JsProgramStatement;
398
+ /** Gets every machine statement generated from an original source location. */
399
+ getStatementsAtSourceLocation(sourcePath: string, sourceLine: number): JsProgramStatement[];
356
400
  getTokenizedLines(): RiscvTokenizedLine[];
357
401
  /**
358
402
  * Checks if the simulation can be undone.
@@ -369,7 +413,7 @@ interface JsRiscV {
369
413
  * @returns An array of `JsProgramStatement` objects representing the compiled program.
370
414
  */
371
415
  getCompiledStatements(): JsProgramStatement[];
372
- getParsedStatements(): JsInstructionToken[];
416
+ getParsedStatements(): JsProgramStatement[];
373
417
  /**
374
418
  * Gets the label at the given address.
375
419
  * @param address The memory address.
@@ -459,6 +503,9 @@ interface JsRiscV {
459
503
  getUndoStack(): JsBackStep[];
460
504
  /**
461
505
  * Reads a sequence of bytes from memory.
506
+ *
507
+ * Reading through this method notifies no memory observer: inspecting memory from the host is
508
+ * not the program reading it, so a memory viewer never drives a memory-mapped register.
462
509
  * @param address The starting memory address.
463
510
  * @param length The number of bytes to read.
464
511
  * @returns An array of bytes read from memory.
@@ -466,15 +513,63 @@ interface JsRiscV {
466
513
  readMemoryBytes(address: number, length: number): number[];
467
514
  /**
468
515
  * Writes a sequence of bytes to memory.
516
+ *
517
+ * Unlike `readMemoryBytes`, this writes the way the program does: it notifies write observers
518
+ * and, while undo is enabled, records an undo step per byte. Use `setPeripheralWord` for a
519
+ * device keeping its own register up to date.
469
520
  * @param address The starting memory address.
470
521
  * @param bytes An array of bytes to write to memory.
471
522
  */
472
523
  setMemoryBytes(address: number, bytes: number[]): void;
473
524
  /**
474
- * Gets the index of the current statement in the assembled program.
475
- * @returns The index of the current statement.
525
+ * Writes one word as a peripheral would: no observer is notified and no undo step is recorded,
526
+ * because the write is not the program acting. This is how a device model refreshes a
527
+ * memory-mapped register - a ready bit, a pending character - without feeding its own observer
528
+ * or consuming undo history.
529
+ * @param address The word address, which must be word-aligned. Either form of a high address
530
+ * is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
531
+ * @param value The 32 bit value to store, raw, without byte-order adjustment.
532
+ */
533
+ setPeripheralWord(address: number, value: number): void;
534
+ /**
535
+ * Observes every write in an address range, the shape a framebuffer wants.
536
+ *
537
+ * Both addresses must be word-aligned, `endAddress` is inclusive and covers its whole word, and
538
+ * the range may not cross 0x80000000 (split it in two registrations instead); a range that
539
+ * breaks any of these throws. Either form of a high address is accepted: `0xffff0000` and
540
+ * `0xffff0000 | 0` name the same word. The handler runs synchronously inside the storing instruction, so
541
+ * it must be cheap and must not write back into its own range; a returned promise is ignored.
542
+ *
543
+ * Observers live on the simulator's memory, which assembling and initializing only clear the
544
+ * contents of, so a registration survives `assemble()` and `initialize()`. For the same reason
545
+ * it is shared by every `JsRiscV` instance: register once per page, or remove the previous
546
+ * registration before registering again for a newly built program.
547
+ *
548
+ * Notifications only start once a program has been assembled, and undo notifies too: restoring
549
+ * memory during `undo()` goes through the same stores, so an observed range reports the
550
+ * restored values as ordinary writes.
551
+ * @returns A handle for `removeMemoryObserver`.
552
+ */
553
+ addMemoryWriteObserver(startAddress: number, endAddress: number, handler: MemoryWriteObserver): MemoryObserverHandle;
554
+ /**
555
+ * Observes reads and writes of a single word, the shape a memory-mapped register wants. The
556
+ * address must be word-aligned. Pass `null` for a direction you do not care about. The same
557
+ * lifetime and synchronous-handler rules as `addMemoryWriteObserver` apply.
558
+ * @returns A handle for `removeMemoryObserver`.
559
+ */
560
+ addMemoryAccessObserver(address: number, onRead: MemoryAccessObserver | null, onWrite: MemoryAccessObserver | null): MemoryObserverHandle;
561
+ /**
562
+ * Removes one registration. An unknown handle is ignored.
563
+ */
564
+ removeMemoryObserver(handle: MemoryObserverHandle): void;
565
+ /**
566
+ * Removes every registration.
567
+ */
568
+ removeMemoryObservers(): void;
569
+ /**
570
+ * The number of live registrations, across every `JsRiscV` instance.
476
571
  */
477
- getCurrentStatementIndex(): number;
572
+ countMemoryObservers(): number;
478
573
  /**
479
574
  * Gets the next statement to be executed.
480
575
  * @returns The next `JsProgramStatement`.
@@ -493,15 +588,19 @@ interface JsRiscV {
493
588
  terminated: boolean;
494
589
  }
495
590
  /**
496
- * Creates a new RISCV simulator from the given source code.
497
- * @param source The source code to assemble.
591
+ * Creates a RISC-V simulator from a virtual source tree and its entry file.
592
+ *
593
+ * Source paths are canonical, root-relative, case-sensitive POSIX paths. The source set is
594
+ * snapshotted by this call; only the entry file and files reached through `.include` are assembled.
595
+ * @param files Source text keyed by canonical source path.
596
+ * @param entryFile Canonical path of the file from which include expansion starts.
498
597
  * @returns A new `JsRiscV` object.
499
598
  */
500
- declare function makeRiscVfromSource(source: string): JsRiscV;
599
+ declare function makeRiscVFromFiles(files: RISCVSourceSet, entryFile: string): JsRiscV;
501
600
  /**
502
601
  * Initializes the RISCV simulator.
503
602
  */
504
603
  declare function initializeRISCV(): void;
505
604
  declare function bigintToHighLow(value: bigint): [high: number, low: number];
506
605
 
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 };
606
+ 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, type RISCVSourceLocation, type RISCVSourceSet, RISCV_REGISTERS, type RegisterName, type RiscvTokenizedLine, StopReason, bigintToHighLow, makeRiscVFromFiles, registerHandlers, unimplementedHandler };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  type JsInstructionToken = {
2
- sourceLine: number;
2
+ /** One-based column in the processed source line. */
3
3
  sourceColumn: number;
4
- originalSourceLine: number;
5
4
  value: string;
6
5
  type: string;
7
6
  };
@@ -119,10 +118,23 @@ type HandlerMap = {
119
118
  in: [l: string];
120
119
  out: void;
121
120
  };
121
+ /**
122
+ * Syscall 32: the program asks to be suspended for this many milliseconds. Return a promise
123
+ * that settles when the wait is over to suspend the simulation without blocking the host.
124
+ */
122
125
  sleep: {
123
126
  in: [milliseconds: number];
124
127
  out: void;
125
128
  };
129
+ /**
130
+ * Syscall 30: the program time in milliseconds, split by the syscall into a0 (low word) and
131
+ * a1 (high word). Answer with `Date.now()` for a live run, or with a virtual clock for a
132
+ * scripted one, so that elapsed-time output stays reproducible.
133
+ */
134
+ time: {
135
+ in: [];
136
+ out: number;
137
+ };
126
138
  stdIn: {
127
139
  in: [buffer: number[], length: number];
128
140
  out: void;
@@ -136,6 +148,27 @@ type HandlerMap = {
136
148
  out: void;
137
149
  };
138
150
  };
151
+ /**
152
+ * Notified after a write anywhere in an observed address range.
153
+ *
154
+ * `length` is the width of the access in bytes (4, 2 or 1) and `value` is what the program stored,
155
+ * so a byte or halfword store reports only the bytes it touched; a peripheral that mirrors whole
156
+ * words should re-read the containing word with `readMemoryBytes` rather than trust `value`.
157
+ *
158
+ * Both `address` and `value` are signed 32 bit integers, as the guest holds them: the register at
159
+ * `0xffff000c` arrives as `-65524`, and a pixel word with its high bit set arrives negative too.
160
+ * Apply `>>> 0` wherever the unsigned form is wanted.
161
+ */
162
+ type MemoryWriteObserver = (address: number, length: number, value: number) => void;
163
+ /**
164
+ * Notified after a read or a write of one observed word, with the same signed 32 bit numbers as
165
+ * `MemoryWriteObserver`. `value` is the value the program read or stored; on a read it is what
166
+ * memory held *before* the observer ran, so a register whose value is consumed by reading it must
167
+ * be reloaded from the handler for the next read.
168
+ */
169
+ type MemoryAccessObserver = (address: number, value: number) => void;
170
+ /** Identifies one registration, for `removeMemoryObserver`. */
171
+ type MemoryObserverHandle = number;
139
172
  type JsInstruction = {
140
173
  name: string;
141
174
  example: string;
@@ -144,16 +177,30 @@ type JsInstruction = {
144
177
  getIsRv64Only: () => boolean;
145
178
  };
146
179
  type RiscvTokenizedLine = {
147
- line: string;
180
+ sourcePath: string;
181
+ /** One-based line in `sourcePath`. */
182
+ sourceLine: number;
183
+ /** Exact line supplied in the source set. */
184
+ source: string;
185
+ /** Line after assembler substitutions such as `.eqv`. */
186
+ processedSource: string;
148
187
  tokens: JsInstructionToken[];
149
188
  };
189
+ type RISCVSourceLocation = {
190
+ sourcePath: string;
191
+ /** One-based line in `sourcePath`. */
192
+ sourceLine: number;
193
+ };
194
+ type RISCVSourceSet = Readonly<Record<string, string>>;
150
195
  type RISCVAssembleError = {
151
196
  isWarning: boolean;
152
197
  message: string;
153
- macroExpansionHistory: string;
154
- filename: string;
155
- lineNumber: number;
156
- columnNumber: number;
198
+ macroExpansionTrace: RISCVSourceLocation[];
199
+ sourcePath: string;
200
+ /** One-based line in `sourcePath`. */
201
+ sourceLine: number;
202
+ /** One-based column in `sourcePath`. Zero only for diagnostics without a source location. */
203
+ sourceColumn: number;
157
204
  };
158
205
  type RISCVAssembleResult = {
159
206
  report: string;
@@ -164,7 +211,7 @@ type RISCVAssembleResult = {
164
211
  hasWarnings: boolean;
165
212
  };
166
213
  declare class RISCV {
167
- static makeRiscVFromSource: typeof makeRiscVfromSource;
214
+ static makeRiscVFromFiles: typeof makeRiscVFromFiles;
168
215
  static initializeRISCV: typeof initializeRISCV;
169
216
  static getInstructionSet(): JsInstruction[];
170
217
  static setIs64Bit(is64Bit: boolean): void;
@@ -196,9 +243,9 @@ type JsRiscVStackFrame = {
196
243
  * Represents a statement in the assembled program.
197
244
  */
198
245
  interface JsProgramStatement {
199
- /**
200
- * The line number in the original source code.
201
- */
246
+ /** Canonical path of the original source file. */
247
+ readonly sourcePath: string;
248
+ /** The one-based line number in the original source file. */
202
249
  readonly sourceLine: number;
203
250
  /**
204
251
  * The memory address of the instruction.
@@ -348,11 +395,8 @@ interface JsRiscV {
348
395
  * @param address
349
396
  */
350
397
  getStatementAtAddress(address: number): JsProgramStatement;
351
- /**
352
- * Gets the statement at the given source line.
353
- * @param line
354
- */
355
- getStatementAtSourceLine(line: number): JsProgramStatement;
398
+ /** Gets every machine statement generated from an original source location. */
399
+ getStatementsAtSourceLocation(sourcePath: string, sourceLine: number): JsProgramStatement[];
356
400
  getTokenizedLines(): RiscvTokenizedLine[];
357
401
  /**
358
402
  * Checks if the simulation can be undone.
@@ -369,7 +413,7 @@ interface JsRiscV {
369
413
  * @returns An array of `JsProgramStatement` objects representing the compiled program.
370
414
  */
371
415
  getCompiledStatements(): JsProgramStatement[];
372
- getParsedStatements(): JsInstructionToken[];
416
+ getParsedStatements(): JsProgramStatement[];
373
417
  /**
374
418
  * Gets the label at the given address.
375
419
  * @param address The memory address.
@@ -459,6 +503,9 @@ interface JsRiscV {
459
503
  getUndoStack(): JsBackStep[];
460
504
  /**
461
505
  * Reads a sequence of bytes from memory.
506
+ *
507
+ * Reading through this method notifies no memory observer: inspecting memory from the host is
508
+ * not the program reading it, so a memory viewer never drives a memory-mapped register.
462
509
  * @param address The starting memory address.
463
510
  * @param length The number of bytes to read.
464
511
  * @returns An array of bytes read from memory.
@@ -466,15 +513,63 @@ interface JsRiscV {
466
513
  readMemoryBytes(address: number, length: number): number[];
467
514
  /**
468
515
  * Writes a sequence of bytes to memory.
516
+ *
517
+ * Unlike `readMemoryBytes`, this writes the way the program does: it notifies write observers
518
+ * and, while undo is enabled, records an undo step per byte. Use `setPeripheralWord` for a
519
+ * device keeping its own register up to date.
469
520
  * @param address The starting memory address.
470
521
  * @param bytes An array of bytes to write to memory.
471
522
  */
472
523
  setMemoryBytes(address: number, bytes: number[]): void;
473
524
  /**
474
- * Gets the index of the current statement in the assembled program.
475
- * @returns The index of the current statement.
525
+ * Writes one word as a peripheral would: no observer is notified and no undo step is recorded,
526
+ * because the write is not the program acting. This is how a device model refreshes a
527
+ * memory-mapped register - a ready bit, a pending character - without feeding its own observer
528
+ * or consuming undo history.
529
+ * @param address The word address, which must be word-aligned. Either form of a high address
530
+ * is accepted: `0xffff0000` and `0xffff0000 | 0` name the same word.
531
+ * @param value The 32 bit value to store, raw, without byte-order adjustment.
532
+ */
533
+ setPeripheralWord(address: number, value: number): void;
534
+ /**
535
+ * Observes every write in an address range, the shape a framebuffer wants.
536
+ *
537
+ * Both addresses must be word-aligned, `endAddress` is inclusive and covers its whole word, and
538
+ * the range may not cross 0x80000000 (split it in two registrations instead); a range that
539
+ * breaks any of these throws. Either form of a high address is accepted: `0xffff0000` and
540
+ * `0xffff0000 | 0` name the same word. The handler runs synchronously inside the storing instruction, so
541
+ * it must be cheap and must not write back into its own range; a returned promise is ignored.
542
+ *
543
+ * Observers live on the simulator's memory, which assembling and initializing only clear the
544
+ * contents of, so a registration survives `assemble()` and `initialize()`. For the same reason
545
+ * it is shared by every `JsRiscV` instance: register once per page, or remove the previous
546
+ * registration before registering again for a newly built program.
547
+ *
548
+ * Notifications only start once a program has been assembled, and undo notifies too: restoring
549
+ * memory during `undo()` goes through the same stores, so an observed range reports the
550
+ * restored values as ordinary writes.
551
+ * @returns A handle for `removeMemoryObserver`.
552
+ */
553
+ addMemoryWriteObserver(startAddress: number, endAddress: number, handler: MemoryWriteObserver): MemoryObserverHandle;
554
+ /**
555
+ * Observes reads and writes of a single word, the shape a memory-mapped register wants. The
556
+ * address must be word-aligned. Pass `null` for a direction you do not care about. The same
557
+ * lifetime and synchronous-handler rules as `addMemoryWriteObserver` apply.
558
+ * @returns A handle for `removeMemoryObserver`.
559
+ */
560
+ addMemoryAccessObserver(address: number, onRead: MemoryAccessObserver | null, onWrite: MemoryAccessObserver | null): MemoryObserverHandle;
561
+ /**
562
+ * Removes one registration. An unknown handle is ignored.
563
+ */
564
+ removeMemoryObserver(handle: MemoryObserverHandle): void;
565
+ /**
566
+ * Removes every registration.
567
+ */
568
+ removeMemoryObservers(): void;
569
+ /**
570
+ * The number of live registrations, across every `JsRiscV` instance.
476
571
  */
477
- getCurrentStatementIndex(): number;
572
+ countMemoryObservers(): number;
478
573
  /**
479
574
  * Gets the next statement to be executed.
480
575
  * @returns The next `JsProgramStatement`.
@@ -493,15 +588,19 @@ interface JsRiscV {
493
588
  terminated: boolean;
494
589
  }
495
590
  /**
496
- * Creates a new RISCV simulator from the given source code.
497
- * @param source The source code to assemble.
591
+ * Creates a RISC-V simulator from a virtual source tree and its entry file.
592
+ *
593
+ * Source paths are canonical, root-relative, case-sensitive POSIX paths. The source set is
594
+ * snapshotted by this call; only the entry file and files reached through `.include` are assembled.
595
+ * @param files Source text keyed by canonical source path.
596
+ * @param entryFile Canonical path of the file from which include expansion starts.
498
597
  * @returns A new `JsRiscV` object.
499
598
  */
500
- declare function makeRiscVfromSource(source: string): JsRiscV;
599
+ declare function makeRiscVFromFiles(files: RISCVSourceSet, entryFile: string): JsRiscV;
501
600
  /**
502
601
  * Initializes the RISCV simulator.
503
602
  */
504
603
  declare function initializeRISCV(): void;
505
604
  declare function bigintToHighLow(value: bigint): [high: number, low: number];
506
605
 
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 };
606
+ 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, type RISCVSourceLocation, type RISCVSourceSet, RISCV_REGISTERS, type RegisterName, type RiscvTokenizedLine, StopReason, bigintToHighLow, makeRiscVFromFiles, registerHandlers, unimplementedHandler };