@fluffylabs/anan-as 1.1.3-c154c6b → 1.1.3-c185e54
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 +47 -10
- package/dist/bin/index.js +50 -25
- package/dist/bin/{fuzz.js → src/fuzz.js} +3 -20
- package/dist/bin/src/trace-parse.js +315 -0
- package/dist/bin/src/trace-replay.js +125 -0
- package/dist/bin/src/tracer.js +64 -0
- package/dist/bin/src/utils.js +25 -0
- package/dist/bin/test.js +1 -3
- package/dist/build/compiler-inline.js +1 -1
- package/dist/build/compiler.wasm +0 -0
- package/dist/build/debug-inline.js +1 -1
- package/dist/build/debug-raw-inline.js +1 -1
- package/dist/build/debug-raw.d.ts +120 -42
- package/dist/build/debug-raw.js +106 -45
- package/dist/build/debug-raw.wasm +0 -0
- package/dist/build/debug.d.ts +120 -42
- package/dist/build/debug.js +116 -47
- package/dist/build/debug.wasm +0 -0
- package/dist/build/release-inline.js +1 -1
- package/dist/build/release-mini-inline.js +1 -1
- package/dist/build/release-mini.d.ts +120 -42
- package/dist/build/release-mini.js +116 -47
- package/dist/build/release-mini.wasm +0 -0
- package/dist/build/release-stub-inline.js +1 -1
- package/dist/build/release-stub.d.ts +120 -42
- package/dist/build/release-stub.js +116 -47
- package/dist/build/release-stub.wasm +0 -0
- package/dist/build/release.d.ts +120 -42
- package/dist/build/release.js +116 -47
- package/dist/build/release.wasm +0 -0
- package/dist/test/test-as.js +3 -0
- package/dist/{bin → test}/test-gas-cost.js +1 -1
- package/dist/test/test-trace-replay.js +19 -0
- package/dist/{bin → test}/test-w3f.js +1 -1
- package/package.json +5 -5
- /package/dist/bin/{test-json.js → src/test-json.js} +0 -0
package/README.md
CHANGED
|
@@ -48,9 +48,26 @@ import ananAs from '@fluffylabs/anan-as/release-mini';
|
|
|
48
48
|
// make sure to call GC after multiple independent runs
|
|
49
49
|
ananAs.__collect();
|
|
50
50
|
|
|
51
|
+
// Release build with stub host functions (for standalone testing)
|
|
52
|
+
import ananAs from '@fluffylabs/anan-as/release-stub';
|
|
53
|
+
|
|
54
|
+
// Compiler module (for PVM bytecode compilation)
|
|
55
|
+
import ananAs from '@fluffylabs/anan-as/compiler';
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Inline Builds
|
|
59
|
+
|
|
60
|
+
Inline builds bundle the WASM binary directly into the JavaScript module (base64 encoded),
|
|
61
|
+
eliminating the need to fetch a separate `.wasm` file:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
import ananAs from '@fluffylabs/anan-as/debug-inline';
|
|
65
|
+
import ananAs from '@fluffylabs/anan-as/release-inline';
|
|
66
|
+
import ananAs from '@fluffylabs/anan-as/release-mini-inline';
|
|
67
|
+
import ananAs from '@fluffylabs/anan-as/release-stub-inline';
|
|
51
68
|
```
|
|
52
69
|
|
|
53
|
-
|
|
70
|
+
### Raw Bindings
|
|
54
71
|
|
|
55
72
|
Raw bindings give you direct access to WebAssembly exports
|
|
56
73
|
without the JavaScript wrapper layer.
|
|
@@ -69,7 +86,6 @@ const module = await WebAssembly.instantiateStreaming(
|
|
|
69
86
|
imports
|
|
70
87
|
);
|
|
71
88
|
const ananAs = await instantiate(module);
|
|
72
|
-
|
|
73
89
|
```
|
|
74
90
|
|
|
75
91
|
## Version Tags
|
|
@@ -105,24 +121,33 @@ To run the example in the browser at [http://localhost:3000](http://localhost:30
|
|
|
105
121
|
npm run web
|
|
106
122
|
```
|
|
107
123
|
|
|
108
|
-
To run
|
|
124
|
+
To run tests:
|
|
109
125
|
|
|
110
126
|
```cmd
|
|
111
|
-
|
|
127
|
+
# Run AssemblyScript unit tests and trace replay tests
|
|
128
|
+
npm test
|
|
129
|
+
|
|
130
|
+
# Run W3F test vectors
|
|
131
|
+
npm run test:w3f
|
|
132
|
+
|
|
133
|
+
# Run gas cost tests
|
|
134
|
+
npm run test:gas-cost
|
|
112
135
|
```
|
|
113
136
|
|
|
114
137
|
## CLI Usage
|
|
115
138
|
|
|
116
|
-
The package includes a CLI tool for disassembling and
|
|
139
|
+
The package includes a CLI tool for disassembling, running, and replaying PVM bytecode:
|
|
117
140
|
|
|
118
141
|
```bash
|
|
119
142
|
# Disassemble bytecode to assembly
|
|
120
143
|
npx @fluffylabs/anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>
|
|
121
144
|
|
|
122
145
|
# Run JAM programs
|
|
123
|
-
npx @fluffylabs/anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin]
|
|
146
|
+
npx @fluffylabs/anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin or hex]
|
|
124
147
|
|
|
125
|
-
|
|
148
|
+
# Replay an ecalli trace
|
|
149
|
+
# Learn more: https://github.com/tomusdrw/JIPs/blob/td-jip6-ecalliloggin/JIP-6.md
|
|
150
|
+
npx @fluffylabs/anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>
|
|
126
151
|
|
|
127
152
|
# Show help
|
|
128
153
|
npx @fluffylabs/anan-as --help
|
|
@@ -130,18 +155,23 @@ npx @fluffylabs/anan-as disassemble --help
|
|
|
130
155
|
npx @fluffylabs/anan-as run --help
|
|
131
156
|
```
|
|
132
157
|
|
|
158
|
+
The `run` command executes PVM bytecode until it encounters a `halt` instruction or a host call.
|
|
159
|
+
The `replay-trace` command re-executes an ecalli trace, replaying recorded host call responses.
|
|
160
|
+
|
|
133
161
|
### Commands
|
|
134
162
|
|
|
135
163
|
- `disassemble`: Convert PVM bytecode to human-readable assembly
|
|
136
164
|
- `run`: Execute PVM bytecode and show results
|
|
165
|
+
- `replay-trace`: Re-execute an ecalli trace with recorded host call responses
|
|
137
166
|
|
|
138
167
|
### Flags
|
|
139
168
|
|
|
140
169
|
- `--spi`: Treat input as JAM SPI format instead of generic PVM
|
|
141
170
|
- `--no-metadata`: Input does not start with metadata
|
|
142
|
-
- `--no-logs`: Disable execution logs (run
|
|
171
|
+
- `--no-logs`: Disable execution logs (run and replay-trace commands)
|
|
172
|
+
- `--no-verify`: Skip verification against trace data (replay-trace only)
|
|
143
173
|
- `--pc <number>`: Set initial program counter (default: 0)
|
|
144
|
-
- `--gas <number>`: Set initial gas amount (default:
|
|
174
|
+
- `--gas <number>`: Set initial gas amount (default: 10,000)
|
|
145
175
|
- `--help`, `-h`: Show help information
|
|
146
176
|
|
|
147
177
|
### Examples
|
|
@@ -168,6 +198,13 @@ npx @fluffylabs/anan-as run --no-logs program.jam
|
|
|
168
198
|
# Run a JAM program with custom initial PC and gas
|
|
169
199
|
npx @fluffylabs/anan-as run --pc 100 --gas 10000 program.jam
|
|
170
200
|
|
|
171
|
-
# Run SPI program with arguments
|
|
201
|
+
# Run SPI program with arguments (file or hex)
|
|
172
202
|
npx @fluffylabs/anan-as run --spi program.spi args.bin
|
|
203
|
+
npx @fluffylabs/anan-as run --spi program.spi 0xdeadbeef
|
|
204
|
+
|
|
205
|
+
# Replay an ecalli trace
|
|
206
|
+
npx @fluffylabs/anan-as replay-trace trace.log
|
|
207
|
+
|
|
208
|
+
# Replay without verification
|
|
209
|
+
npx @fluffylabs/anan-as replay-trace --no-verify trace.log
|
|
173
210
|
```
|
package/dist/bin/index.js
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import minimist from "minimist";
|
|
4
4
|
import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram } from "../build/release.js";
|
|
5
|
+
import { replayTraceFile } from "./src/trace-replay.js";
|
|
6
|
+
import { hexDecode, hexEncode } from "./src/utils.js";
|
|
5
7
|
const HELP_TEXT = `Usage:
|
|
6
8
|
anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>
|
|
7
9
|
anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin or hex]
|
|
10
|
+
anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>
|
|
8
11
|
|
|
9
12
|
Commands:
|
|
10
13
|
disassemble Disassemble PVM bytecode to assembly
|
|
11
14
|
run Execute PVM bytecode
|
|
15
|
+
replay-trace Re-execute a ecalli IO trace
|
|
12
16
|
|
|
13
17
|
Flags:
|
|
14
18
|
--spi Treat input as JAM SPI format
|
|
15
19
|
--no-metadata Input does not contain metadata
|
|
16
|
-
--no-logs Disable execution logs
|
|
20
|
+
--no-logs Disable execution logs
|
|
21
|
+
--no-verify Skip verification against trace data (replay-trace only)
|
|
17
22
|
--pc <number> Set initial program counter (default: 0)
|
|
18
23
|
--gas <number> Set initial gas amount (default: 10_000)
|
|
19
24
|
--help, -h Show this help message`;
|
|
@@ -33,6 +38,9 @@ function main() {
|
|
|
33
38
|
case "run":
|
|
34
39
|
handleRun(args.slice(1));
|
|
35
40
|
break;
|
|
41
|
+
case "replay-trace":
|
|
42
|
+
handleReplayTrace(args.slice(1));
|
|
43
|
+
break;
|
|
36
44
|
default:
|
|
37
45
|
console.error(`Error: Unknown sub-command '${subCommand}'`);
|
|
38
46
|
console.error("");
|
|
@@ -148,6 +156,47 @@ function handleRun(args) {
|
|
|
148
156
|
process.exit(1);
|
|
149
157
|
}
|
|
150
158
|
}
|
|
159
|
+
function handleReplayTrace(args) {
|
|
160
|
+
const parsed = minimist(args, {
|
|
161
|
+
boolean: ["metadata", "verify", "logs", "help"],
|
|
162
|
+
alias: { h: "help" },
|
|
163
|
+
default: { metadata: true, logs: true, verify: true },
|
|
164
|
+
});
|
|
165
|
+
if (parsed.help) {
|
|
166
|
+
console.log(HELP_TEXT);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const files = parsed._;
|
|
170
|
+
if (files.length === 0) {
|
|
171
|
+
console.error("Error: No trace file provided for replay-trace command.");
|
|
172
|
+
console.error("Usage: anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
if (files.length > 1) {
|
|
176
|
+
console.error("Error: Only one trace file can be replayed at a time.");
|
|
177
|
+
console.error("Usage: anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] <trace.log>");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const file = files[0];
|
|
181
|
+
const hasMetadata = parsed.metadata ? HasMetadata.Yes : HasMetadata.No;
|
|
182
|
+
const verify = parsed.verify;
|
|
183
|
+
const logs = parsed.logs;
|
|
184
|
+
try {
|
|
185
|
+
const summary = replayTraceFile(file, {
|
|
186
|
+
logs,
|
|
187
|
+
hasMetadata,
|
|
188
|
+
verify,
|
|
189
|
+
});
|
|
190
|
+
console.log(`✅ Replay complete: ${summary.ecalliCount} ecalli entries`);
|
|
191
|
+
console.log(`Status: ${summary.termination.type}`);
|
|
192
|
+
console.log(`Program counter: ${summary.termination.pc}`);
|
|
193
|
+
console.log(`Gas remaining: ${summary.termination.gas}`);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error(`Error replaying trace ${file}:`, error);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
151
200
|
function parseGas(parsed) {
|
|
152
201
|
if (parsed.gas === undefined) {
|
|
153
202
|
return BigInt(10_000);
|
|
@@ -212,27 +261,3 @@ function parsePc(parsed) {
|
|
|
212
261
|
}
|
|
213
262
|
return pcValue;
|
|
214
263
|
}
|
|
215
|
-
function hexEncode(result) {
|
|
216
|
-
return `0x${result.map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
217
|
-
}
|
|
218
|
-
function hexDecode(data) {
|
|
219
|
-
if (!data.startsWith("0x")) {
|
|
220
|
-
throw new Error("hex input must start with 0x");
|
|
221
|
-
}
|
|
222
|
-
const hex = data.substring(2);
|
|
223
|
-
const len = hex.length;
|
|
224
|
-
if (len % 2 === 1) {
|
|
225
|
-
throw new Error("Odd number of nibbles");
|
|
226
|
-
}
|
|
227
|
-
const bytes = new Uint8Array(len / 2);
|
|
228
|
-
for (let i = 0; i < len; i += 2) {
|
|
229
|
-
const c = hex.substring(i, i + 2);
|
|
230
|
-
const byteIndex = i / 2;
|
|
231
|
-
const value = parseInt(c, 16);
|
|
232
|
-
if (Number.isNaN(value)) {
|
|
233
|
-
throw new Error(`hexDecode: invalid hex pair "${c}" in data "${data}" for bytes[${byteIndex}]`);
|
|
234
|
-
}
|
|
235
|
-
bytes[byteIndex] = value;
|
|
236
|
-
}
|
|
237
|
-
return bytes;
|
|
238
|
-
}
|
|
@@ -3,7 +3,7 @@ import "json-bigint-patch";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { tryAsGas } from "@typeberry/lib/pvm-interface";
|
|
5
5
|
import { Interpreter } from "@typeberry/lib/pvm-interpreter";
|
|
6
|
-
import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram, wrapAsProgram } from "
|
|
6
|
+
import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram, wrapAsProgram } from "../../build/release.js";
|
|
7
7
|
const runNumber = 0;
|
|
8
8
|
export function fuzz(data) {
|
|
9
9
|
const gas = 200n;
|
|
@@ -56,30 +56,13 @@ export function fuzz(data) {
|
|
|
56
56
|
throw e;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
import { hexEncode } from "./utils.js";
|
|
59
60
|
function programHex(program) {
|
|
60
|
-
return Array.from(program)
|
|
61
|
-
.map((x) => x.toString(16).padStart(2, "0"))
|
|
62
|
-
.join("");
|
|
61
|
+
return hexEncode(Array.from(program), false);
|
|
63
62
|
}
|
|
64
63
|
function linkTo(programHex) {
|
|
65
64
|
return `https://pvm.fluffylabs.dev/?program=0x${programHex}#/`;
|
|
66
65
|
}
|
|
67
|
-
const REGISTER_BYTE_WIDTH = 8;
|
|
68
|
-
function _decodeRegisters(value) {
|
|
69
|
-
if (value.length === 0) {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
if (value.length % REGISTER_BYTE_WIDTH !== 0) {
|
|
73
|
-
throw new Error(`Invalid register buffer size: ${value.length}`);
|
|
74
|
-
}
|
|
75
|
-
const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
76
|
-
const registerCount = value.length / REGISTER_BYTE_WIDTH;
|
|
77
|
-
const registers = new Array(registerCount);
|
|
78
|
-
for (let i = 0; i < registerCount; i++) {
|
|
79
|
-
registers[i] = view.getBigUint64(i * REGISTER_BYTE_WIDTH, true);
|
|
80
|
-
}
|
|
81
|
-
return registers;
|
|
82
|
-
}
|
|
83
66
|
function decodeRegistersFromTypeberry(vm) {
|
|
84
67
|
const registers = [];
|
|
85
68
|
// Try to get up to 13 registers (common register count)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const NO_OF_REGISTERS = 13;
|
|
2
|
+
// Access.Write = 2 from assembly/memory-page.ts
|
|
3
|
+
const ACCESS_WRITE = 2;
|
|
4
|
+
export const STATUS = {
|
|
5
|
+
OK: 255,
|
|
6
|
+
HALT: 0,
|
|
7
|
+
PANIC: 1,
|
|
8
|
+
FAULT: 2,
|
|
9
|
+
HOST: 3,
|
|
10
|
+
OOG: 4,
|
|
11
|
+
};
|
|
12
|
+
export const ARGS_SEGMENT_START = 0xfeff0000;
|
|
13
|
+
const LINE_PATTERN = /(program\s+0x|memwrite\s+0x|start\s+pc=|ecalli=\d+|memread\s+0x|setreg\s+r\d+|setgas\s+<-|HALT\s+pc=|OOG\s+pc=|PANIC=)/;
|
|
14
|
+
export function parseTrace(input) {
|
|
15
|
+
const lines = input.split(/\r?\n/);
|
|
16
|
+
let program = null;
|
|
17
|
+
const initialMemWrites = [];
|
|
18
|
+
let start = null;
|
|
19
|
+
const ecalliEntries = [];
|
|
20
|
+
let currentEntry = null;
|
|
21
|
+
let termination = null;
|
|
22
|
+
for (const rawLine of lines) {
|
|
23
|
+
const line = extractPayload(rawLine);
|
|
24
|
+
if (!line) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith("program ")) {
|
|
28
|
+
program = parseHexBytes(line.replace("program ", "").trim());
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (line.startsWith("memwrite ")) {
|
|
32
|
+
const memwrite = parseMemWrite(line);
|
|
33
|
+
if (currentEntry) {
|
|
34
|
+
currentEntry.memWrites.push(memwrite);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
initialMemWrites.push(memwrite);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (line.startsWith("start ")) {
|
|
42
|
+
start = parseStart(line);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith("ecalli=")) {
|
|
46
|
+
const entry = parseEcalli(line);
|
|
47
|
+
ecalliEntries.push(entry);
|
|
48
|
+
currentEntry = entry;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (line.startsWith("memread ")) {
|
|
52
|
+
if (!currentEntry) {
|
|
53
|
+
throw new Error(`memread without active ecalli: ${line}`);
|
|
54
|
+
}
|
|
55
|
+
currentEntry.memReads.push(parseMemRead(line));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (line.startsWith("setreg ")) {
|
|
59
|
+
if (!currentEntry) {
|
|
60
|
+
throw new Error(`setreg without active ecalli: ${line}`);
|
|
61
|
+
}
|
|
62
|
+
const setReg = parseSetReg(line);
|
|
63
|
+
currentEntry.setRegs.push(setReg);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (line.startsWith("setgas ")) {
|
|
67
|
+
if (!currentEntry) {
|
|
68
|
+
throw new Error(`setgas without active ecalli: ${line}`);
|
|
69
|
+
}
|
|
70
|
+
currentEntry.setGas = parseSetGas(line);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (line.startsWith("HALT ") || line.startsWith("OOG ") || line.startsWith("PANIC=")) {
|
|
74
|
+
termination = parseTermination(line);
|
|
75
|
+
currentEntry = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!program) {
|
|
79
|
+
throw new Error("Missing program line in trace");
|
|
80
|
+
}
|
|
81
|
+
if (!start) {
|
|
82
|
+
throw new Error("Missing start line in trace");
|
|
83
|
+
}
|
|
84
|
+
if (!termination) {
|
|
85
|
+
throw new Error("Missing termination line in trace");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
program,
|
|
89
|
+
initialMemWrites,
|
|
90
|
+
start,
|
|
91
|
+
ecalliEntries,
|
|
92
|
+
termination,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function extractPayload(line) {
|
|
96
|
+
const match = LINE_PATTERN.exec(line);
|
|
97
|
+
if (!match || match.index === undefined) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return line.slice(match.index).trim();
|
|
101
|
+
}
|
|
102
|
+
function parseStart(line) {
|
|
103
|
+
const match = /^start pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
104
|
+
if (!match) {
|
|
105
|
+
throw new Error(`Invalid start line: ${line}`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
pc: parseInt(match[1], 10),
|
|
109
|
+
gas: BigInt(match[2]),
|
|
110
|
+
registers: parseRegisterDump(match[3]),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function parseEcalli(line) {
|
|
114
|
+
const match = /^ecalli=(\d+) pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
115
|
+
if (!match) {
|
|
116
|
+
throw new Error(`Invalid ecalli line: ${line}`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
index: parseInt(match[1], 10),
|
|
120
|
+
pc: parseInt(match[2], 10),
|
|
121
|
+
gas: BigInt(match[3]),
|
|
122
|
+
registers: parseRegisterDump(match[4]),
|
|
123
|
+
memReads: [],
|
|
124
|
+
memWrites: [],
|
|
125
|
+
setRegs: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function parseMemWrite(line) {
|
|
129
|
+
const match = /^memwrite\s+0x([0-9a-f]+)\s+len=(\d+)\s+<-\s+0x([0-9a-f]+)$/i.exec(line);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw new Error(`Invalid memwrite line: ${line}`);
|
|
132
|
+
}
|
|
133
|
+
const address = parseInt(match[1], 16);
|
|
134
|
+
const data = parseHexBytes(`0x${match[3]}`);
|
|
135
|
+
const len = parseInt(match[2], 10);
|
|
136
|
+
if (data.length !== len) {
|
|
137
|
+
throw new Error(`memwrite length mismatch: expected ${len}, got ${data.length}`);
|
|
138
|
+
}
|
|
139
|
+
return { address, data };
|
|
140
|
+
}
|
|
141
|
+
function parseMemRead(line) {
|
|
142
|
+
const match = /^memread\s+0x([0-9a-f]+)\s+len=(\d+)\s+->\s+0x([0-9a-f]+)$/i.exec(line);
|
|
143
|
+
if (!match) {
|
|
144
|
+
throw new Error(`Invalid memread line: ${line}`);
|
|
145
|
+
}
|
|
146
|
+
const address = parseInt(match[1], 16);
|
|
147
|
+
const data = parseHexBytes(`0x${match[3]}`);
|
|
148
|
+
const len = parseInt(match[2], 10);
|
|
149
|
+
if (data.length !== len) {
|
|
150
|
+
throw new Error(`memread length mismatch: expected ${len}, got ${data.length}`);
|
|
151
|
+
}
|
|
152
|
+
return { address, data };
|
|
153
|
+
}
|
|
154
|
+
function parseSetReg(line) {
|
|
155
|
+
const match = /^setreg\s+r(\d+)\s+<-\s+0x([0-9a-f]+)$/i.exec(line);
|
|
156
|
+
if (!match) {
|
|
157
|
+
throw new Error(`Invalid setreg line: ${line}`);
|
|
158
|
+
}
|
|
159
|
+
const index = parseInt(match[1], 10);
|
|
160
|
+
if (index < 0 || index >= NO_OF_REGISTERS) {
|
|
161
|
+
throw new Error(`Register index out of range: ${index}`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
index,
|
|
165
|
+
value: BigInt(`0x${match[2]}`),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function parseSetGas(line) {
|
|
169
|
+
const match = /^setgas\s+<-\s+(\d+)$/i.exec(line);
|
|
170
|
+
if (!match) {
|
|
171
|
+
throw new Error(`Invalid setgas line: ${line}`);
|
|
172
|
+
}
|
|
173
|
+
return BigInt(match[1]);
|
|
174
|
+
}
|
|
175
|
+
function parseTermination(line) {
|
|
176
|
+
if (line.startsWith("HALT ")) {
|
|
177
|
+
const match = /^HALT pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
178
|
+
if (!match) {
|
|
179
|
+
throw new Error(`Invalid HALT line: ${line}`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
type: "HALT",
|
|
183
|
+
pc: parseInt(match[1], 10),
|
|
184
|
+
gas: BigInt(match[2]),
|
|
185
|
+
registers: parseRegisterDump(match[3]),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (line.startsWith("OOG ")) {
|
|
189
|
+
const match = /^OOG pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
190
|
+
if (!match) {
|
|
191
|
+
throw new Error(`Invalid OOG line: ${line}`);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
type: "OOG",
|
|
195
|
+
pc: parseInt(match[1], 10),
|
|
196
|
+
gas: BigInt(match[2]),
|
|
197
|
+
registers: parseRegisterDump(match[3]),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (line.startsWith("PANIC=")) {
|
|
201
|
+
const match = /^PANIC=([^\s]+) pc=(\d+) gas=(\d+)(.*)$/.exec(line);
|
|
202
|
+
if (!match) {
|
|
203
|
+
throw new Error(`Invalid PANIC line: ${line}`);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
type: "PANIC",
|
|
207
|
+
pc: parseInt(match[2], 10),
|
|
208
|
+
gas: BigInt(match[3]),
|
|
209
|
+
registers: parseRegisterDump(match[4]),
|
|
210
|
+
panicArg: parseNumber(match[1]),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Unknown termination line: ${line}`);
|
|
214
|
+
}
|
|
215
|
+
function parseRegisterDump(input) {
|
|
216
|
+
const dump = new Map();
|
|
217
|
+
const regex = /r(\d+)=0x([0-9a-f]+)/gi;
|
|
218
|
+
for (const match of input.matchAll(regex)) {
|
|
219
|
+
const index = parseInt(match[1], 10);
|
|
220
|
+
if (index < 0 || index >= NO_OF_REGISTERS) {
|
|
221
|
+
throw new Error(`Register index out of range: ${index}`);
|
|
222
|
+
}
|
|
223
|
+
const value = BigInt(`0x${match[2]}`);
|
|
224
|
+
dump.set(index, value);
|
|
225
|
+
}
|
|
226
|
+
return dump;
|
|
227
|
+
}
|
|
228
|
+
function parseHexBytes(hex) {
|
|
229
|
+
if (!hex.startsWith("0x")) {
|
|
230
|
+
throw new Error(`Hex value must start with 0x: ${hex}`);
|
|
231
|
+
}
|
|
232
|
+
const data = hex.slice(2);
|
|
233
|
+
if (data.length % 2 !== 0) {
|
|
234
|
+
throw new Error(`Hex value must have even length: ${hex}`);
|
|
235
|
+
}
|
|
236
|
+
const bytes = new Uint8Array(data.length / 2);
|
|
237
|
+
for (let i = 0; i < data.length; i += 2) {
|
|
238
|
+
const pair = data.slice(i, i + 2);
|
|
239
|
+
if (!/^[0-9a-fA-F]{2}$/.test(pair)) {
|
|
240
|
+
throw new Error(`Invalid hex pair "${pair}" in hex value: ${hex}`);
|
|
241
|
+
}
|
|
242
|
+
bytes[i / 2] = Number.parseInt(pair, 16);
|
|
243
|
+
}
|
|
244
|
+
return bytes;
|
|
245
|
+
}
|
|
246
|
+
export function encodeRegistersFromDump(dump) {
|
|
247
|
+
const registers = new Array(NO_OF_REGISTERS).fill(0n);
|
|
248
|
+
for (const [index, value] of dump) {
|
|
249
|
+
registers[index] = value;
|
|
250
|
+
}
|
|
251
|
+
return registers;
|
|
252
|
+
}
|
|
253
|
+
export function buildInitialPages(memWrites) {
|
|
254
|
+
return memWrites
|
|
255
|
+
.filter((write) => write.data.length > 0)
|
|
256
|
+
.map((write) => ({
|
|
257
|
+
address: write.address,
|
|
258
|
+
length: write.data.length,
|
|
259
|
+
access: ACCESS_WRITE,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
export function buildInitialChunks(memWrites) {
|
|
263
|
+
return memWrites
|
|
264
|
+
.filter((write) => write.data.length > 0)
|
|
265
|
+
.map((write) => ({
|
|
266
|
+
address: write.address,
|
|
267
|
+
data: Array.from(write.data),
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
export function isSpiTrace(start, memWrites) {
|
|
271
|
+
const r07 = start.registers.get(7);
|
|
272
|
+
if (r07 === BigInt(ARGS_SEGMENT_START)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return memWrites.some((write) => write.address === ARGS_SEGMENT_START);
|
|
276
|
+
}
|
|
277
|
+
export function extractSpiArgs(start, memWrites) {
|
|
278
|
+
const argLenBig = start.registers.get(8) ?? 0n;
|
|
279
|
+
// Validate bounds: must be non-negative and <= 1 MiB (2^20)
|
|
280
|
+
const MAX_ARG_LEN = 1n << 20n;
|
|
281
|
+
if (argLenBig < 0n || argLenBig > MAX_ARG_LEN) {
|
|
282
|
+
return new Uint8Array(0);
|
|
283
|
+
}
|
|
284
|
+
const argLen = Number(argLenBig);
|
|
285
|
+
if (argLen <= 0) {
|
|
286
|
+
return new Uint8Array(0);
|
|
287
|
+
}
|
|
288
|
+
const buffer = new Uint8Array(argLen);
|
|
289
|
+
for (const write of memWrites) {
|
|
290
|
+
if (write.address < ARGS_SEGMENT_START) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const offset = write.address - ARGS_SEGMENT_START;
|
|
294
|
+
if (offset >= buffer.length) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
buffer.set(write.data.subarray(0, buffer.length - offset), offset);
|
|
298
|
+
}
|
|
299
|
+
return buffer;
|
|
300
|
+
}
|
|
301
|
+
export function statusToTermination(status) {
|
|
302
|
+
if (status === STATUS.HALT) {
|
|
303
|
+
return "HALT";
|
|
304
|
+
}
|
|
305
|
+
if (status === STATUS.OOG) {
|
|
306
|
+
return "OOG";
|
|
307
|
+
}
|
|
308
|
+
return "PANIC";
|
|
309
|
+
}
|
|
310
|
+
function parseNumber(value) {
|
|
311
|
+
if (value.startsWith("0x")) {
|
|
312
|
+
return Number.parseInt(value, 16);
|
|
313
|
+
}
|
|
314
|
+
return Number.parseInt(value, 10);
|
|
315
|
+
}
|