@fluffylabs/anan-as 1.2.0-9723cd9 → 1.2.0-9f08a39

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/dist/bin/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
3
  import { parseArgs } from "node:util";
4
- import { disassemble, HasMetadata, InputKind, prepareProgram, pvmDestroy, pvmResume, pvmSetRegisters, pvmStart, } from "../build/release.js";
4
+ import { disassemble, HasMetadata, InputKind, prepareProgram, pvmDestroy, pvmReadMemory, pvmResume, pvmSetRegisters, pvmStart, } from "../build/release.js";
5
5
  import { LOG_GAS_COST, LOG_HOST_CALL_INDEX, printLogHostCall, WHAT } from "./src/log-host-call.js";
6
6
  import { STATUS } from "./src/trace-parse.js";
7
7
  import { replayTraceFile } from "./src/trace-replay.js";
8
8
  import { hexDecode, hexEncode } from "./src/utils.js";
9
+ // Page access modes (matches assembly/memory-page.ts Access enum)
10
+ const ACCESS_READ = 1;
11
+ const ACCESS_WRITE = 2;
9
12
  const HELP_TEXT = `Usage:
10
13
  anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>
11
- anan-as run [--spi] [--no-logs] [--no-metadata] [--no-log-host-call] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin or hex]
14
+ anan-as run [--spi] [--no-logs] [--no-metadata] [--no-log-host-call] [--pc <number>] [--gas <number>] [--regs <r0,r1,...,r12>] <file.jam> [spi-args.bin or hex]
12
15
  anan-as replay-trace [--no-metadata] [--no-verify] [--no-logs] [--no-log-host-call] <trace.log>
13
16
 
14
17
  Commands:
@@ -24,6 +27,10 @@ Flags:
24
27
  --no-verify Skip verification against trace data (replay-trace only)
25
28
  --pc <number> Set initial program counter (default: 0)
26
29
  --gas <number> Set initial gas amount (default: 10_000)
30
+ --regs <values> Set initial registers (comma-separated, 13 values: r0,r1,...,r12; supports decimal and 0x hex)
31
+ --pages <specs> Add memory pages (semicolon-separated: "addr:size;addr:size:ro"; append ":r" or ":ro" for read-only)
32
+ --mem <specs> Initialize memory (semicolon-separated: "addr:hex_bytes;addr:hex_bytes")
33
+ --dump <specs> Dump memory after execution (semicolon-separated: "addr:len;addr:len")
27
34
  --help, -h Show this help message`;
28
35
  main();
29
36
  function main() {
@@ -84,7 +91,7 @@ function handleDisassemble(args) {
84
91
  console.error("Supported extensions: .jam, .pvm, .spi, .bin");
85
92
  process.exit(1);
86
93
  }
87
- const ext = file.substring(dotIndex);
94
+ const ext = file.slice(dotIndex);
88
95
  if (!validExtensions.includes(ext)) {
89
96
  console.error(`Error: Invalid file extension '${ext}' for disassemble command.`);
90
97
  console.error("Supported extensions: .jam, .pvm, .spi, .bin");
@@ -109,6 +116,10 @@ function handleRun(args) {
109
116
  help: { type: "boolean", short: "h", default: false },
110
117
  pc: { type: "string" },
111
118
  gas: { type: "string" },
119
+ regs: { type: "string" },
120
+ pages: { type: "string" },
121
+ mem: { type: "string" },
122
+ dump: { type: "string" },
112
123
  },
113
124
  });
114
125
  if (values.help) {
@@ -150,13 +161,17 @@ function handleRun(args) {
150
161
  // Parse and validate PC and gas options
151
162
  const initialPc = parsePc(values.pc);
152
163
  const initialGas = parseGas(values.gas);
164
+ const initialRegisters = parseRegs(values.regs);
165
+ const initialPages = parsePages(values.pages);
166
+ const initialMemory = parseMem(values.mem);
167
+ const dumpRegions = parseDump(values.dump);
153
168
  const programCode = Array.from(readFileSync(programFile));
154
169
  const name = kind === InputKind.Generic ? "generic PVM" : "JAM SPI";
155
170
  console.log(`🚀 Running ${programFile} (as ${name})`);
156
171
  try {
157
172
  const preallocateMemoryPages = 128;
158
173
  const useBlockGas = true;
159
- const program = prepareProgram(kind, hasMetadata, programCode, [], [], [], spiArgs, preallocateMemoryPages, useBlockGas);
174
+ const program = prepareProgram(kind, hasMetadata, programCode, initialRegisters, initialPages, initialMemory, spiArgs, preallocateMemoryPages, useBlockGas);
160
175
  const id = pvmStart(program);
161
176
  let gas = initialGas;
162
177
  let pc = initialPc;
@@ -180,6 +195,24 @@ function handleRun(args) {
180
195
  break;
181
196
  }
182
197
  }
198
+ // Dump memory regions before destroying the VM
199
+ for (const region of dumpRegions) {
200
+ const data = pvmReadMemory(id, region.address, region.length);
201
+ const addrHex = `0x${region.address.toString(16)}`;
202
+ if (data) {
203
+ console.log(`\nMemory @ ${addrHex} (${region.length} bytes):`);
204
+ for (let off = 0; off < data.length; off += 16) {
205
+ const addr = region.address + off;
206
+ const slice = Array.from(data.slice(off, Math.min(off + 16, data.length)));
207
+ const hex = slice.map((b) => b.toString(16).padStart(2, "0")).join(" ");
208
+ const ascii = slice.map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
209
+ console.log(` ${addr.toString(16).padStart(8, "0")}: ${hex.padEnd(47)} ${ascii}`);
210
+ }
211
+ }
212
+ else {
213
+ console.log(`\nMemory @ ${addrHex}: <page fault>`);
214
+ }
215
+ }
183
216
  const result = pvmDestroy(id);
184
217
  console.log(`Status: ${result?.status}`);
185
218
  console.log(`Exit code: ${result?.exitCode}`);
@@ -293,3 +326,102 @@ function parsePc(pcStr) {
293
326
  }
294
327
  return pcValue;
295
328
  }
329
+ function parseRegs(regsStr) {
330
+ if (regsStr === undefined) {
331
+ return [];
332
+ }
333
+ const parts = regsStr.split(",");
334
+ if (parts.length !== 13) {
335
+ throw new Error(`--regs must have exactly 13 comma-separated values (got ${parts.length}).\nFormat: --regs r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12`);
336
+ }
337
+ return parts.map((s, i) => {
338
+ try {
339
+ return BigInt.asUintN(64, BigInt(s.trim()));
340
+ }
341
+ catch (_e) {
342
+ throw new Error(`--regs value at index ${i} ("${s.trim()}") is not a valid integer.`);
343
+ }
344
+ });
345
+ }
346
+ function parseNum(s) {
347
+ return Number(s.trim());
348
+ }
349
+ function parsePages(pagesStr) {
350
+ if (pagesStr === undefined) {
351
+ return [];
352
+ }
353
+ // Format: "addr:size;addr:size" — all pages are writable
354
+ // Or "addr:size:ro" (or "addr:size:r") for read-only
355
+ const specs = pagesStr.split(";").filter((s) => s.trim().length > 0);
356
+ return specs.map((spec, i) => {
357
+ const parts = spec.split(":");
358
+ if (parts.length < 2 || parts.length > 3) {
359
+ throw new Error(`--pages entry ${i} ("${spec}") must be "addr:size" or "addr:size:ro" (or "addr:size:r").`);
360
+ }
361
+ const address = parseNum(parts[0]);
362
+ const length = parseNum(parts[1]);
363
+ const flag = parts[2]?.trim();
364
+ const access = flag === "ro" || flag === "r" ? ACCESS_READ : ACCESS_WRITE;
365
+ if (Number.isNaN(address) || Number.isNaN(length) || length <= 0) {
366
+ throw new Error(`--pages entry ${i} ("${spec}") has invalid address or size.`);
367
+ }
368
+ return { address, length, access };
369
+ });
370
+ }
371
+ function parseMem(memStr) {
372
+ if (memStr === undefined) {
373
+ return [];
374
+ }
375
+ // Format: "addr:hexbytes;addr:hexbytes"
376
+ // Example: "0x20000:0500000000000000;0x20008:0300000000000000"
377
+ const specs = memStr.split(";").filter((s) => s.trim().length > 0);
378
+ return specs.map((spec, i) => {
379
+ const colonIdx = spec.indexOf(":");
380
+ if (colonIdx === -1) {
381
+ throw new Error(`--mem entry ${i} ("${spec}") must be "addr:hexbytes".`);
382
+ }
383
+ const addrStr = spec.slice(0, colonIdx).trim();
384
+ let hexStr = spec.slice(colonIdx + 1).trim();
385
+ const address = parseNum(addrStr);
386
+ if (Number.isNaN(address)) {
387
+ throw new Error(`--mem entry ${i} has invalid address "${addrStr}".`);
388
+ }
389
+ // Strip 0x prefix from hex data
390
+ if (hexStr.startsWith("0x") || hexStr.startsWith("0X")) {
391
+ hexStr = hexStr.slice(2);
392
+ }
393
+ if (hexStr.length % 2 !== 0) {
394
+ throw new Error(`--mem entry ${i} hex data has odd length.`);
395
+ }
396
+ const data = [];
397
+ for (let j = 0; j < hexStr.length; j += 2) {
398
+ const pair = hexStr.slice(j, j + 2);
399
+ if (!/^[0-9a-fA-F]{2}$/.test(pair)) {
400
+ throw new Error(`--mem entry ${i} has invalid hex byte at position ${j}: "${pair}".`);
401
+ }
402
+ const byte = parseInt(pair, 16);
403
+ data.push(byte);
404
+ }
405
+ return { address, data };
406
+ });
407
+ }
408
+ function parseDump(dumpStr) {
409
+ if (dumpStr === undefined) {
410
+ return [];
411
+ }
412
+ // Format: "addr:len;addr:len"
413
+ // Example: "0x20000:64;0x20100:32"
414
+ const specs = dumpStr.split(";").filter((s) => s.trim().length > 0);
415
+ return specs.map((spec, i) => {
416
+ const parts = spec.split(":");
417
+ if (parts.length !== 2) {
418
+ throw new Error(`--dump entry ${i} ("${spec}") must be "addr:len".`);
419
+ }
420
+ const address = parseNum(parts[0]);
421
+ const length = parseNum(parts[1]);
422
+ if (Number.isNaN(address) || Number.isNaN(length) || length <= 0) {
423
+ throw new Error(`--dump entry ${i} ("${spec}") has invalid address or length.`);
424
+ }
425
+ return { address, length };
426
+ });
427
+ }
@@ -1,3 +1,4 @@
1
+ // Trace format: https://github.com/FluffyLabs/jam-ecalli-trace/blob/main/ecalli-trace-jip.md
1
2
  const NO_OF_REGISTERS = 13;
2
3
  // Access.Write = 2 from assembly/memory-page.ts
3
4
  const ACCESS_WRITE = 2;
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import * as defaultPvm from "../../build/release.js";
3
3
  import { LOG_HOST_CALL_INDEX, printLogHostCall } from "./log-host-call.js";
4
- import { ARGS_SEGMENT_START, buildInitialChunks, buildInitialPages, encodeRegistersFromDump, extractSpiArgs, isSpiTrace, parseTrace, STATUS, statusToTermination, } from "./trace-parse.js";
4
+ import { buildInitialChunks, buildInitialPages, encodeRegistersFromDump, extractSpiArgs, isSpiTrace, parseTrace, STATUS, statusToTermination, } from "./trace-parse.js";
5
5
  import { ConsoleTracer } from "./tracer.js";
6
6
  import { hexEncode } from "./utils.js";
7
7
  export function replayTraceFile(filePath, options) {
@@ -25,11 +25,12 @@ export function replayTraceFile(filePath, options) {
25
25
  try {
26
26
  let gas = start.gas;
27
27
  let pc = start.pc;
28
- // Print start line
29
- tracer.start(pc, gas, start.registers);
30
- if (spiArgs.length > 0) {
31
- tracer.spiArgs(ARGS_SEGMENT_START, spiArgs);
28
+ // Print prelude: program, initial memwrites, start
29
+ tracer.program(program);
30
+ for (const write of initialMemWrites) {
31
+ tracer.memwrite(write.address, write.data);
32
32
  }
33
+ tracer.start(pc, gas, start.registers);
33
34
  for (;;) {
34
35
  const pause = pvmResume(id, gas, pc, options.logs);
35
36
  if (!pause) {
@@ -1,29 +1,29 @@
1
- import { hexEncode } from "./utils.js";
1
+ // Trace output format: https://github.com/FluffyLabs/jam-ecalli-trace/blob/main/ecalli-trace-jip.md
2
2
  export class ConsoleTracer {
3
+ program(data) {
4
+ console.log(`program ${formatHex(data)}`);
5
+ }
3
6
  start(pc, gas, registers) {
4
7
  console.log(`start pc=${pc} gas=${gas} ${formatRegisters(registers)}`);
5
8
  }
6
- spiArgs(address, data) {
7
- console.log(` memwrite ${address} len=${data.length} <- ${hexEncode(data)}`);
8
- }
9
9
  ecalli(index, pc, gas, registers) {
10
- console.log(`\necalli=${index} pc=${pc} gas=${gas} ${formatRegisters(registers)}`);
10
+ console.log(`ecalli=${index} pc=${pc} gas=${gas} ${formatRegisters(registers)}`);
11
11
  }
12
12
  memread(address, data) {
13
- console.log(` memread 0x${address.toString(16)} len=${data.length} -> ${formatHex(data)}`);
13
+ console.log(` memread ${formatAddress(address)} len=${data.length} -> ${formatHex(data)}`);
14
14
  }
15
15
  memwrite(address, data) {
16
- console.log(` memwrite 0x${address.toString(16)} len=${data.length} <- ${formatHex(data)}`);
16
+ console.log(` memwrite ${formatAddress(address)} len=${data.length} <- ${formatHex(data)}`);
17
17
  }
18
18
  setreg(index, value) {
19
- console.log(` setreg r${index.toString().padStart(2, "0")} <- 0x${value.toString(16)}`);
19
+ console.log(` setreg r${index.toString().padStart(2, "0")} <- 0x${value.toString(16)}`);
20
20
  }
21
21
  setgas(gas) {
22
- console.log(` setgas <- ${gas}`);
22
+ console.log(` setgas <- ${gas}`);
23
23
  }
24
24
  termination(type, exitCode, pc, gas, registers) {
25
- let termLine = `\n------\n${type}`;
26
- if (type === "PANIC" && exitCode !== 0) {
25
+ let termLine = `${type}`;
26
+ if (type === "PANIC") {
27
27
  termLine += `=${exitCode}`;
28
28
  }
29
29
  termLine += ` pc=${pc} gas=${gas} ${formatRegisters(registers)}`;
@@ -31,8 +31,8 @@ export class ConsoleTracer {
31
31
  }
32
32
  }
33
33
  export class NoOpTracer {
34
+ program() { }
34
35
  start() { }
35
- spiArgs() { }
36
36
  ecalli() { }
37
37
  memread() { }
38
38
  memwrite() { }
@@ -56,9 +56,12 @@ function formatRegisters(registers) {
56
56
  }
57
57
  return entries
58
58
  .sort((a, b) => a.idx - b.idx)
59
- .map((e) => `r${e.idx}=0x${e.val.toString(16)}`)
59
+ .map((e) => `r${e.idx.toString().padStart(2, "0")}=0x${e.val.toString(16)}`)
60
60
  .join(" ");
61
61
  }
62
+ function formatAddress(address) {
63
+ return `0x${Number(address).toString(16).padStart(8, "0")}`;
64
+ }
62
65
  function formatHex(data) {
63
66
  return `0x${Buffer.from(data).toString("hex")}`;
64
67
  }
@@ -6,14 +6,14 @@ export function hexDecode(data) {
6
6
  if (!data.startsWith("0x")) {
7
7
  throw new Error("hex input must start with 0x");
8
8
  }
9
- const hex = data.substring(2);
9
+ const hex = data.slice(2);
10
10
  const len = hex.length;
11
11
  if (len % 2 === 1) {
12
12
  throw new Error("Odd number of nibbles");
13
13
  }
14
14
  const bytes = new Uint8Array(len / 2);
15
15
  for (let i = 0; i < len; i += 2) {
16
- const c = hex.substring(i, i + 2);
16
+ const c = hex.slice(i, i + 2);
17
17
  const byteIndex = i / 2;
18
18
  if (!/^[0-9a-fA-F]{2}$/.test(c)) {
19
19
  throw new Error(`hexDecode: invalid hex pair "${c}" in data "${data}" for bytes[${byteIndex}]`);