@specy/risc-v 3.7.0 → 4.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
@@ -23,6 +23,10 @@ Omission selects `rars`. `RISCV.assemblerProfiles` lists supported profiles; eve
23
23
 
24
24
  `getAddressOfLabel(name)` returns a defined label/alias address after successful assembly, or `-1` when absent. Profile state is per program; width, memory and execution state retain the existing single-instance constraint.
25
25
 
26
+ Floating point literals are read as Java 21 reads them, correctly rounded: `.double` stores
27
+ `Double.parseDouble` of its token and `.float` that double rounded to a float, as RARS does, while
28
+ the `gnu-compiler-v1` profile reads `.float` straight to single precision, as GNU as does.
29
+
26
30
  ⚠️**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.
27
31
 
28
32
  ```typescript
@@ -65,15 +69,15 @@ console.log(`t2: ${t2}`);
65
69
  const data = riscvSimulator.readMemoryBytes(0xffff0000, 4); // Read 4 bytes from address 0xffff0000
66
70
  riscvSimulator.setMemoryBytes(0xffff0000, [0x01, 0x02, 0x03, 0x04]); // Write 4 bytes to address 0xffff0000
67
71
 
68
- // Registering Handlers (for syscalls and other events):
69
- riscvSimulator.registerHandler("printInt", (value: number) => {
70
- console.log("printInt syscall called with:", value);
72
+ // Registering Handlers (for syscalls and other events): every print syscall writes its text here.
73
+ riscvSimulator.registerHandler("printString", (text: string) => {
74
+ process.stdout.write(text);
71
75
  });
72
76
 
73
77
  // Handlers may also be async: returning a promise suspends the simulation until it settles,
74
78
  // so IO can be backed by an async API without blocking the event loop.
75
79
  riscvSimulator.registerHandler("readInt", async () => {
76
- return await promptUserForANumber();
80
+ return await promptUserForALine(); // the syscall parses the line itself
77
81
  });
78
82
 
79
83
  // Accessing the undo stack:
@@ -106,7 +110,7 @@ Every syscall that needs to talk to the outside world goes through a handler you
106
110
  handler can return its result directly, or return a promise of it:
107
111
 
108
112
  ```typescript
109
- riscvSimulator.registerHandler("readInt", () => 42); // synchronous
113
+ riscvSimulator.registerHandler("readInt", () => "42"); // synchronous
110
114
  riscvSimulator.registerHandler("readString", () => fetchLine()); // asynchronous
111
115
  ```
112
116
 
@@ -116,17 +120,95 @@ promise settles, so nothing has to be shoehorned into a synchronous API. Because
116
120
  `simulateWithBreakpointsAndLimit` all return a promise. Everything else on `JsRiscV` (registers,
117
121
  memory, statements, the undo stack) stays synchronous.
118
122
 
119
- If a handler's promise rejects, the pending `step`/`simulate*` call rejects as well, with the
120
- rejection reason in the message.
123
+ If a handler throws, its promise rejects or it answers what its type does not allow, the pending
124
+ `step`/`simulate*` call rejects with a `RuntimeError` of kind `handler`, whose `cause` is what the
125
+ handler threw or rejected with (see [Running and stopping](#running-and-stopping)).
121
126
 
122
127
  Synchronous handlers never yield to the event loop: the simulation runs straight through, and the
123
128
  returned promise settles on a microtask. Overlapping calls are queued and run one after another,
124
129
  never concurrently.
125
130
 
126
- Program time is a handler too, so a run can be given a clock of its own: `sleep` answers syscall 32
127
- and `time` answers syscall 30. A live run resolves `sleep` on a timer and returns `Date.now()` from
128
- `time`; a scripted run can settle `sleep` immediately, advance a virtual clock by the requested
129
- milliseconds and return that clock instead, which keeps elapsed-time output reproducible.
131
+ The syscalls behave as RARS 1.6 does on Java 21, so handlers only move text and bytes:
132
+
133
+ * Every print syscall formats its value and writes the text through `printString`. Print float
134
+ and print double write what `Float.toString` and `Double.toString` write: `1.0`, `0.001`,
135
+ `1.0E-4`, `1.0E7`, `-0.0`, `NaN`, `Infinity`.
136
+ * `readInt`, `readFloat` and `readDouble` answer with the line typed. The syscall trims it and
137
+ parses it as `Integer.parseInt`, `Float.parseFloat` and `Double.parseDouble` do, hexadecimal
138
+ floats and an `f` or `d` suffix included, and stops the program on anything else:
139
+ `Runtime exception at 0x00400004: invalid integer input (syscall 5)`.
140
+ * `readChar` answers with the character typed, Enter as `"\n"`. The syscall takes the first
141
+ UTF-16 unit, and an empty answer stops the program with `invalid char input (syscall 12)`.
142
+ * `inputDialog` answers `null` when the user cancels, which dialogs 51 to 54 report to the
143
+ program as status -2; `confirm` answers 2 for Cancel. The program waits for `outputDialog` to
144
+ settle.
145
+ * Bytes cross as plain arrays of numbers from 0 to 255. `writeFile` answers with the number of
146
+ bytes written, or -1, and the program receives that count; `readFile` and `stdIn` answer
147
+ `[count, bytes]`, with a count of 0 at the end of the input and -1 for a failed read.
148
+
149
+ ```typescript
150
+ riscvSimulator.registerHandler("inputDialog", (message) => window.prompt(message)); // null on Cancel
151
+ riscvSimulator.registerHandler("writeFile", (descriptor, bytes) => files.write(descriptor, Uint8Array.from(bytes)));
152
+ riscvSimulator.registerHandler("stdIn", async (length) => {
153
+ const bytes = await terminal.readBytes(length);
154
+ return [bytes.length, Array.from(bytes)];
155
+ });
156
+ ```
157
+
158
+ | Handler | Syscalls | Receives | Answers |
159
+ | --- | --- | --- | --- |
160
+ | `printString` | 1 to 4, 11, 34 to 36 | the text to print | nothing |
161
+ | `readInt`, `readFloat`, `readDouble`, `readString` | 5 to 8 | nothing | the line typed |
162
+ | `readChar` | 12 | nothing | the character typed |
163
+ | `openFile` | 1024 | path, flags, whether to append | the file descriptor, or -1 |
164
+ | `readFile` | 63 | file descriptor, most bytes to read | `[count, bytes]` |
165
+ | `writeFile` | 64 | file descriptor, bytes | bytes written, or -1 |
166
+ | `closeFile` | 57 | file descriptor | nothing |
167
+ | `seekFile` | 62 | file descriptor, offset, whence | the new position, or -1 |
168
+ | `stdIn` | 63 on descriptor 0 | most bytes to read | `[count, bytes]` |
169
+ | `stdOut`, `stdErr` | 64 on descriptors 1 and 2 | bytes | nothing |
170
+ | `confirm` | 50 | message | `ConfirmResult` |
171
+ | `inputDialog` | 51 to 54 | message | the text entered, or `null` |
172
+ | `outputDialog` | 55, 56, 58 to 60 | message, `DialogType` | nothing |
173
+ | `sleep` | 32 | milliseconds | nothing |
174
+ | `time` | 30, and an instruction reading the `time` CSR | nothing | milliseconds |
175
+ | `randomSeed` (optional) | 41 to 44, a generator's first use | the generator's number | a seed from 0 to 2^48 - 1 |
176
+
177
+ Program time is a handler too, so a run can be given a clock of its own: `sleep` answers syscall 32,
178
+ and `time` answers syscall 30 and every read of the `time` CSR. A live run resolves `sleep` on a
179
+ timer and returns `Date.now()` from `time`; a scripted run can settle `sleep` immediately, advance a
180
+ virtual clock by the requested milliseconds and return that clock instead, which keeps elapsed-time
181
+ output reproducible.
182
+
183
+ ## Text
184
+
185
+ Text is UTF-8 throughout, as in RARS, so a string's text is the same in a literal, through print
186
+ string (4) and through write (64), and the same as the UTF-8 a C compiler emits:
187
+
188
+ * `.ascii`, `.asciz` and `.string` store a literal by code point, `\u` escapes included: four
189
+ hexadecimal digits naming one UTF-16 unit, and two units that make a surrogate pair name one
190
+ character. RARS encodes a literal one UTF-16 unit at a time, which stores a character outside the
191
+ Basic Multilingual Plane as `??`; here `"😀"` and `"\ud83d\ude00"` both store its four bytes.
192
+ * Print string (4), the path of open (1024) and the messages of the dialogs (50 to 60) are read
193
+ as UTF-8, the way Java 21 decodes it: a malformed sequence prints as U+FFFD. Message dialog double
194
+ (58), which RARS reads one byte per character, reads UTF-8 like the others.
195
+ * Read string (8) and input dialog string (54) store the text as UTF-8, measuring the buffer in
196
+ bytes: a buffer of n bytes holds at most n - 1 bytes of text, so a character can be
197
+ cut where the buffer ends. An unpaired surrogate is stored as `?`, as Java encodes it.
198
+ * Print char (11) prints the character numbered by the low byte of `a0`, and read char (12)
199
+ answers the first UTF-16 unit typed: 233 for `é`, 8364 for `€` and 0xd83d, a high surrogate,
200
+ for `😀`.
201
+
202
+ ## Corrections to RARS
203
+
204
+ * GetCWD (17) writes `/`, the root that relative and absolute file paths both resolve against.
205
+ RARS writes the JVM's working directory, which a browser does not have.
206
+ * Input dialog double (53) reads its message from `a0`, as every dialog does; RARS reads `x4`.
207
+ * The `time` CSR, and `timeh`, its upper half on RV32, read the program time in milliseconds
208
+ from the `time` handler when an instruction reads them, as the time service (30) does, so a
209
+ scripted run's virtual clock reaches them. RARS writes the host clock into the counter after
210
+ every instruction. The counter reads 0 until the program reads it, and Undo puts back the reading
211
+ before. `cycle` and `instret` count instructions as in RARS.
130
212
 
131
213
  ## Memory observers
132
214
 
@@ -169,6 +251,89 @@ riscvSimulator.removeMemoryObservers()
169
251
  * `undo()` restores memory through the same stores, so an observed range reports the restored
170
252
  values as ordinary writes and a device that follows notifications alone stays in step.
171
253
 
254
+ ## Random numbers
255
+
256
+ The random services draw from `java.util.Random`, implemented exactly, so a program that seeds a
257
+ generator with ecall 40 prints the numbers RARS prints: `nextInt` (41), `nextInt(bound)` with
258
+ Java's rejection of over-represented values (42), `nextFloat` (43) and `nextDouble` (44). Each
259
+ number in `a0` names a generator of its own.
260
+
261
+ A generator the program has not seeded starts, on its first use, from the seed the optional
262
+ `randomSeed` handler answers: a whole number from 0 to 2^48 - 1, as `new Random(seed)` takes it.
263
+ Without the handler it starts from host randomness, as in RARS; a scripted run answers with a fixed
264
+ seed instead and gets the same numbers every time. Handlers are shared by every `JsRiscV` instance,
265
+ and registering `undefined` removes one.
266
+
267
+ ```typescript
268
+ riscvSimulator.registerHandler("randomSeed", () => 42); // the same numbers on every run
269
+ riscvSimulator.registerHandler("randomSeed", undefined); // host randomness again
270
+ ```
271
+
272
+ The generators belong to the run: `initialize` forgets them, so each starts from a new seed. Undo
273
+ puts a generator back as it was before the service ran, so Undo then Step draws the same number.
274
+
275
+ ## Running and stopping
276
+
277
+ Every run call (`step`, `simulate`, `simulateWithLimit`, `simulateWithBreakpoints`,
278
+ `simulateWithBreakpointsAndLimit`) resolves to a `StopReason`, the same enum `@specy/mips` exports:
279
+
280
+ | `StopReason` | When |
281
+ | --- | --- |
282
+ | `MAX_STEPS` | the instruction limit was reached; a `step` ends on it |
283
+ | `BREAKPOINT` | the program counter reached a breakpoint address, whose instruction has not run, or an `ebreak` ran |
284
+ | `NORMAL_TERMINATION` | exit (10) or exit2 (93) ran |
285
+ | `CLIFF_TERMINATION` | the program ran off the end of its code; the call that runs the last instruction says so |
286
+
287
+ `terminated` is read from the program's state, so it is right after `undo()` too: it is true once
288
+ an exit has run or there is no statement at the program counter, and `getNextStatement()` then
289
+ returns `null`. A program that has exited runs nothing more: every run call resolves to
290
+ `NORMAL_TERMINATION` again until `undo()` or `initialize()`. `exitCode` is exit2's `a0` once it has
291
+ run and 0 otherwise, after exit and after running off the end too; `initialize` resets it and
292
+ undoing the exit puts back the code before it. `getStopReason()` reports the last run call's
293
+ reason, `NONE` after `initialize`.
294
+
295
+ A runtime failure rejects the run call with a `RuntimeError`, an `Error` with typed fields:
296
+
297
+ ```typescript
298
+ try {
299
+ await riscvSimulator.simulate();
300
+ } catch (error) {
301
+ if (isRuntimeError(error)) {
302
+ // error.kind: 'exception' | 'syscall' | 'handler' | 'internal'
303
+ // error.address, error.sourcePath, error.line (one-based, null without a statement)
304
+ console.log(`${error.sourcePath}:${error.line}: ${error.message}`);
305
+ // main.asm:7: Runtime exception at 0x00400018: invalid or unimplemented syscall service: 99
306
+ }
307
+ }
308
+ ```
309
+
310
+ `exception` is a RISC-V exception the program had no trap handler for (a misaligned or faulting
311
+ access, an illegal instruction, an instruction fetch outside the program), `syscall` a service that
312
+ refused its number, arguments or input, `handler` a host handler or memory observer that threw,
313
+ rejected or broke its contract (its `cause` is what it threw), and `internal` the simulator
314
+ failing. The message is RARS's own. A failure leaves `terminated` false and `getStopReason()` at
315
+ `EXCEPTION`; any other rejection, such as a run call on a program that did not assemble, is the
316
+ plain error it is.
317
+
318
+ ## Memory layout
319
+
320
+ Programs run in RARS's memory layout: text from 0x00400000, static data from 0x10010000, the heap
321
+ from 0x10040000, the stack down from 0x7fffeffc and memory-mapped IO from 0xffff0000. The data
322
+ segment, which static data and the heap share, ends at 0x10400000.
323
+
324
+ * A RARS-dialect program keeps this layout whatever its size, as in RARS: data past
325
+ 0x10040000 shares its addresses with the heap.
326
+ * A `gnu-compiler-v1` program's heap follows its static data (`.data`, `.rodata`, `.bss` and
327
+ common symbols): once they reach past 0x10040000, the heap starts at the first 4 KiB page after
328
+ them, where ld and a kernel put the break, and sbrk (9) hands out that address first. Static data
329
+ must fit in the data segment, 4,128,768 bytes from 0x10010000; a program with more fails to
330
+ assemble with `Static data ends at 0x10410000, past the end of the data segment at 0x10400000: ...`.
331
+ * Library members linked after a RARS-dialect program must end below 0x10040000, or the program
332
+ fails to assemble with `Static data reaches the heap at 0x10040000`.
333
+
334
+ `getHeapStart()` reports where the heap starts, and `initialize` empties it again. An allocator
335
+ built on sbrk follows the heap wherever it starts.
336
+
172
337
  ## API
173
338
 
174
339
  ### `RISCV` Static Class
@@ -193,13 +358,18 @@ This interface provides methods to control and interact with a RISC-V simulator
193
358
  * `canUndo: boolean` (Read-only): True if an undo operation can be performed.
194
359
  * `programCounter: number` (Read-only): The current value of the program counter (PC).
195
360
  * `stackPointer: number` (Read-only): The current value of the stack pointer (`$sp`).
196
- * `terminated: boolean` (Read-only): True if the simulation has terminated (e.g., via an exit syscall or error).
361
+ * `terminated: boolean` (Read-only): Whether the program has ended, by an exit or by running off the end, read from its state so that it is right after `undo()`.
362
+ * `exitCode: number` (Read-only): exit2's code once it has run, 0 otherwise.
197
363
 
198
364
  #### Methods
199
365
 
200
366
  * `assemble(): RISCVAssembleResult`: Assembles the program. Returns an object containing a report, error list, and an `hasErrors` flag.
201
367
  * `initialize(startAtMain: boolean): void`: Initializes the simulator state for execution. If `startAtMain` is true, execution begins at the `main` label; otherwise, it starts at the first instruction.
202
- * `step(): Promise<StopReason>`: Executes a single instruction. Resolves to the reason the simulation stopped.
368
+ * `step(): Promise<StopReason>`: Executes a single instruction. See [Running and stopping](#running-and-stopping) for every run call's `StopReason` and `RuntimeError`.
369
+ * `getStopReason(): StopReason`: Why the last run call stopped, `NONE` after `initialize`.
370
+ * `getHeapStart(): number`: Where the heap, and so sbrk's first block, starts: 0x10040000, or the
371
+ first page after static data in a `gnu-compiler-v1` program whose static data reaches past it.
372
+ See [Memory layout](#memory-layout).
203
373
  * `undo(): void`: Undoes the last instruction executed, if `canUndo` is true and undo is enabled.
204
374
  * `setUndoEnabled(enabled: boolean): void`: Enables or disables the undo feature.
205
375
  * `setUndoSize(size: number): void`: Sets the maximum number of steps kept in the undo history. Must be called before assembling.
@@ -220,7 +390,7 @@ This interface provides methods to control and interact with a RISC-V simulator
220
390
  * `removeMemoryObserver(handle: number): void`: Removes one registration.
221
391
  * `removeMemoryObservers(): void`: Removes every registration.
222
392
  * `countMemoryObservers(): number`: The number of live registrations.
223
- * `getNextStatement(): JsProgramStatement | null`: Returns the next `JsProgramStatement` to be executed, or `null` if at the end.
393
+ * `getNextStatement(): JsProgramStatement | null`: Returns the next `JsProgramStatement` to be executed, or `null` once the program has terminated.
224
394
  * `getStatementAtAddress(address: number): JsProgramStatement | null`: Gets the program statement at the given memory `address`.
225
395
  * `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.
226
396
  * `getCompiledStatements(): JsProgramStatement[]`: Returns all machine statements after a successful assembly.
@@ -240,9 +410,11 @@ This interface provides methods to control and interact with a RISC-V simulator
240
410
  * `RISCVSourceSet`: A read-only mapping from canonical virtual source paths to source strings.
241
411
  * `RISCVSourceLocation`: A source path and one-based source line.
242
412
  * `HandlerName`: Union type of all possible handler names (keys of `HandlerMap`).
413
+ * `StopReason`: Enum of the reasons a run call stops.
414
+ * `RuntimeError`: The typed error a run call rejects with when the program fails; `isRuntimeError(error)` tells it apart.
243
415
  * `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.
244
416
  * `HandlerMapFns`: An object type where keys are `HandlerName`s and values are the corresponding handler functions.
245
- * `DialogType`: Enum for message dialog types (`ERROR_MESSAGE`, `INFORMATION_MESSAGE`, `WARNING_MESSAGE`, `QUESTION_MESSAGE`).
417
+ * `DialogType`: Enum for message dialog types (`PLAIN_MESSAGE`, `ERROR_MESSAGE`, `INFORMATION_MESSAGE`, `WARNING_MESSAGE`, `QUESTION_MESSAGE`).
246
418
  * `ConfirmResult`: Enum for confirm dialog results (`YES`, `NO`, `CANCEL`).
247
419
  * `BackStepAction`: Enum representing types of actions that can be undone (e.g., `MEMORY_RESTORE_WORD`, `REGISTER_RESTORE`).
248
420
  * `JsBackStep`: Interface representing an entry in the undo stack, detailing the action, parameters, and PC value.
@@ -253,3 +425,24 @@ This interface provides methods to control and interact with a RISC-V simulator
253
425
  * `RISCVAssembleError`: Interface describing an assembly diagnostic with its source path, one-based line and column, warning status, and structured macro expansion trace.
254
426
  * `RISCVAssembleResult`: Interface for the result of `assemble()`, containing a report string, list of `RISCVAssembleError`s, and a `hasErrors` boolean.
255
427
  * `JsRiscvStackFrame`: Interface describing a frame on the call stack (PC, target address, SP, FP, register snapshot).
428
+
429
+ ### Dynamic instruction identities
430
+
431
+ `getCurrentInstructionSerial(): string | null` names the instruction currently executing. Read it
432
+ inside a host handler (including an awaited handler) to journal host effects. The eventual
433
+ `JsInstructionUndoGroup.serial` and each `JsBackStep.serial` carry exactly that identity. Core-only
434
+ services, exits, write-free instructions and failed instructions also receive their own identity.
435
+ Pokes have a distinct `JsPokeUndoGroup.serial`; the current-instruction getter remains null in a Poke.
436
+ Serials are opaque positive decimal strings, allocated monotonically for the lifetime of this module
437
+ across instances, Undo, initialize and reassembly. Do not convert them to JavaScript numbers. At
438
+ serial-space exhaustion execution fails rather than reusing an identity.
439
+
440
+ History keeps only the bounded restore stack. The Undo size still counts raw restore slots, with a
441
+ Poke taking one slot; oldest instructions are evicted whole. An instruction larger than the capacity
442
+ is discarded whole, so a retained group always represents a complete rollback. `initialize` clears
443
+ retained history and Poke journals. Executing an instruction or opening a Poke with recording
444
+ disabled clears older retained history, preventing Undo across an unrecorded gap. No-history
445
+ instructions still expose a serial inside handlers. Match and retain host frames by serial against
446
+ `getUndoGroups`/`getUndoGroupsRange`, never by PC; drop frames whose groups are no longer retained.
447
+
448
+ `initialize` is rejected while an instruction or Poke is active.