@fluffylabs/anan-as 1.1.3-cc868cf → 1.1.3-e01d5ea
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 +65 -7
- package/dist/bin/build-inline.js +70 -0
- package/dist/bin/fuzz.js +172 -0
- package/dist/bin/index.js +220 -0
- package/dist/bin/test-gas-cost.js +57 -0
- package/dist/bin/test-json.js +135 -0
- package/dist/bin/test-w3f.js +119 -0
- package/dist/bin/test.js +3 -0
- package/dist/build/compiler-inline.d.ts +11 -0
- package/dist/build/compiler-inline.js +22 -0
- package/dist/build/compiler.d.ts +26 -0
- package/dist/build/compiler.js +76 -0
- package/dist/build/compiler.wasm +0 -0
- package/{build → dist/build}/debug-inline.d.ts +1 -1
- package/dist/build/debug-inline.js +22 -0
- package/{build → dist/build}/debug-raw-inline.d.ts +1 -1
- package/dist/build/debug-raw-inline.js +22 -0
- package/{build → dist/build}/debug-raw.d.ts +100 -39
- package/{build → dist/build}/debug-raw.js +88 -37
- package/dist/build/debug-raw.wasm +0 -0
- package/{build → dist/build}/debug.d.ts +100 -39
- package/{build → dist/build}/debug.js +94 -39
- package/dist/build/debug.wasm +0 -0
- package/{build → dist/build}/release-inline.d.ts +1 -1
- package/dist/build/release-inline.js +22 -0
- package/{build → dist/build}/release-mini-inline.d.ts +1 -1
- package/dist/build/release-mini-inline.js +22 -0
- package/{build → dist/build}/release-mini.d.ts +100 -39
- package/{build → dist/build}/release-mini.js +94 -39
- package/dist/build/release-mini.wasm +0 -0
- package/{build → dist/build}/release-stub-inline.d.ts +1 -1
- package/dist/build/release-stub-inline.js +22 -0
- package/{build → dist/build}/release-stub.d.ts +100 -39
- package/{build → dist/build}/release-stub.js +94 -39
- package/dist/build/release-stub.wasm +0 -0
- package/{build → dist/build}/release.d.ts +100 -39
- package/{build → dist/build}/release.js +94 -39
- package/dist/build/release.wasm +0 -0
- package/{build → dist/build}/test-inline.d.ts +1 -1
- package/dist/build/test-inline.js +22 -0
- package/dist/build/test.wasm +0 -0
- package/dist/web/bump-version.js +7 -0
- package/package.json +46 -37
- package/build/debug-inline.js +0 -22
- package/build/debug-raw-inline.js +0 -22
- package/build/debug-raw.wasm +0 -0
- package/build/debug.wasm +0 -0
- package/build/release-inline.js +0 -22
- package/build/release-mini-inline.js +0 -22
- package/build/release-mini.wasm +0 -0
- package/build/release-stub-inline.js +0 -22
- package/build/release-stub.wasm +0 -0
- package/build/release.wasm +0 -0
- package/build/test-inline.js +0 -22
- package/build/test.wasm +0 -0
- /package/{build → dist/build}/test.d.ts +0 -0
- /package/{build → dist/build}/test.js +0 -0
package/README.md
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
AssemblyScript implementation of the JAM PVM (64-bit).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Gray Paper compatibility:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [x] 0.7.2
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- [x] [JAM tests](https://github.com/w3f/jamtestvectors/pull/3) compatibility
|
|
11
|
-
- [x] 64-bit & new instructions ([GrayPaper v0.5.0](https://graypaper.fluffylabs.dev))
|
|
12
|
-
- [x] GP 0.5.4 compatibility (ZBB extensions)
|
|
9
|
+
[Demo](https://todr.me/anan-as)
|
|
13
10
|
|
|
14
11
|
## Why?
|
|
15
12
|
|
|
@@ -19,7 +16,7 @@ AssemblyScript implementation of the JAM PVM (64-bit).
|
|
|
19
16
|
|
|
20
17
|
## Useful where?
|
|
21
18
|
|
|
22
|
-
-
|
|
19
|
+
- Main PVM backend of [`typeberry`](https://github.com/fluffylabs) JAM client.
|
|
23
20
|
- To test out the [PVM debugger](https://pvm.fluffylabs.dev).
|
|
24
21
|
|
|
25
22
|
## Installation
|
|
@@ -113,3 +110,64 @@ To run JSON test vectors.
|
|
|
113
110
|
```cmd
|
|
114
111
|
npm start ./path/to/tests/*.json
|
|
115
112
|
```
|
|
113
|
+
|
|
114
|
+
## CLI Usage
|
|
115
|
+
|
|
116
|
+
The package includes a CLI tool for disassembling and running PVM bytecode:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Disassemble bytecode to assembly
|
|
120
|
+
npx @fluffylabs/anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>
|
|
121
|
+
|
|
122
|
+
# Run JAM programs
|
|
123
|
+
npx @fluffylabs/anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin]
|
|
124
|
+
|
|
125
|
+
The `run` command executes PVM bytecode until it encounters a `halt` instruction or the first host call. For full execution including host call handling, use the disassemble command or other tooling.
|
|
126
|
+
|
|
127
|
+
# Show help
|
|
128
|
+
npx @fluffylabs/anan-as --help
|
|
129
|
+
npx @fluffylabs/anan-as disassemble --help
|
|
130
|
+
npx @fluffylabs/anan-as run --help
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Commands
|
|
134
|
+
|
|
135
|
+
- `disassemble`: Convert PVM bytecode to human-readable assembly
|
|
136
|
+
- `run`: Execute PVM bytecode and show results
|
|
137
|
+
|
|
138
|
+
### Flags
|
|
139
|
+
|
|
140
|
+
- `--spi`: Treat input as JAM SPI format instead of generic PVM
|
|
141
|
+
- `--no-metadata`: Input does not start with metadata
|
|
142
|
+
- `--no-logs`: Disable execution logs (run command only)
|
|
143
|
+
- `--pc <number>`: Set initial program counter (default: 0)
|
|
144
|
+
- `--gas <number>`: Set initial gas amount (default: 0)
|
|
145
|
+
- `--help`, `-h`: Show help information
|
|
146
|
+
|
|
147
|
+
### Examples
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Disassemble a JAM file (includes metadata by default)
|
|
151
|
+
npx @fluffylabs/anan-as disassemble program.jam
|
|
152
|
+
|
|
153
|
+
# Disassemble without metadata
|
|
154
|
+
npx @fluffylabs/anan-as disassemble --no-metadata program.jam
|
|
155
|
+
|
|
156
|
+
# Disassemble SPI program
|
|
157
|
+
npx @fluffylabs/anan-as disassemble --spi program.spi
|
|
158
|
+
|
|
159
|
+
# Run a JAM program with logs (includes metadata by default)
|
|
160
|
+
npx @fluffylabs/anan-as run program.jam
|
|
161
|
+
|
|
162
|
+
# Run a JAM program without metadata
|
|
163
|
+
npx @fluffylabs/anan-as run --no-metadata program.jam
|
|
164
|
+
|
|
165
|
+
# Run a JAM program quietly
|
|
166
|
+
npx @fluffylabs/anan-as run --no-logs program.jam
|
|
167
|
+
|
|
168
|
+
# Run a JAM program with custom initial PC and gas
|
|
169
|
+
npx @fluffylabs/anan-as run --pc 100 --gas 10000 program.jam
|
|
170
|
+
|
|
171
|
+
# Run SPI program with arguments
|
|
172
|
+
npx @fluffylabs/anan-as run --spi program.spi args.bin
|
|
173
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const projectRoot = resolve(__dirname, "..");
|
|
6
|
+
// Load asconfig.json
|
|
7
|
+
const asconfigPath = resolve(projectRoot, "asconfig.json");
|
|
8
|
+
const asconfig = JSON.parse(readFileSync(asconfigPath, "utf-8"));
|
|
9
|
+
console.log("Building inline JS files with base64-encoded WASM...\n");
|
|
10
|
+
// Process each target
|
|
11
|
+
for (const [targetName, config] of Object.entries(asconfig.targets)) {
|
|
12
|
+
const wasmPath = resolve(projectRoot, config.outFile);
|
|
13
|
+
try {
|
|
14
|
+
// Read the WASM file
|
|
15
|
+
const wasmBuffer = readFileSync(wasmPath);
|
|
16
|
+
// Base64 encode
|
|
17
|
+
const wasmBase64 = wasmBuffer.toString("base64");
|
|
18
|
+
// Generate the output JS file name
|
|
19
|
+
// e.g., "build/release.wasm" -> "build/release-inline.js"
|
|
20
|
+
const wasmFileName = basename(config.outFile, ".wasm");
|
|
21
|
+
const outputPath = resolve(projectRoot, dirname(config.outFile), `${wasmFileName}-inline.js`);
|
|
22
|
+
// Create the JS content
|
|
23
|
+
const jsContent = `// Auto-generated inline WASM module
|
|
24
|
+
// Target: ${targetName}
|
|
25
|
+
// Source: ${config.outFile}
|
|
26
|
+
|
|
27
|
+
import * as raw from './debug-raw.js';
|
|
28
|
+
|
|
29
|
+
export const wasmBase64 = "${wasmBase64}";
|
|
30
|
+
let compiledModulePromise = null;
|
|
31
|
+
|
|
32
|
+
// Helper function to decode and instantiate the module
|
|
33
|
+
export async function instantiate(imports) {
|
|
34
|
+
if (compiledModulePromise === null) {
|
|
35
|
+
compiledModulePromise = WebAssembly.compile(getWasmBytes());
|
|
36
|
+
}
|
|
37
|
+
const module = await compiledModulePromise;
|
|
38
|
+
return raw.instantiate(module, imports);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper function to just get the bytes
|
|
42
|
+
export function getWasmBytes() {
|
|
43
|
+
return Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0));
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
// Write the JS file
|
|
47
|
+
writeFileSync(outputPath, jsContent, "utf-8");
|
|
48
|
+
// Generate and write the .d.ts file
|
|
49
|
+
const dtsPath = outputPath.replace(/\.js$/, ".d.ts");
|
|
50
|
+
const dtsContent = `// Auto-generated type definitions for inline WASM module
|
|
51
|
+
// Target: ${targetName}
|
|
52
|
+
// Source: ${config.outFile}
|
|
53
|
+
|
|
54
|
+
import {__AdaptedExports} from "./debug-raw.d.ts";
|
|
55
|
+
|
|
56
|
+
export const wasmBase64: string;
|
|
57
|
+
|
|
58
|
+
export function instantiate(imports?: { env?: any }): Promise<typeof __AdaptedExports>;
|
|
59
|
+
|
|
60
|
+
export function getWasmBytes(): Uint8Array;
|
|
61
|
+
`;
|
|
62
|
+
writeFileSync(dtsPath, dtsContent, "utf-8");
|
|
63
|
+
console.log(`✓ ${targetName}: ${outputPath} (${Math.round(wasmBase64.length / 1024)} KB base64)`);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error(`✗ ${targetName}: Failed to process ${wasmPath}`);
|
|
67
|
+
console.error(` ${error instanceof Error ? error.message : error}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
console.log("\nDone!");
|
package/dist/bin/fuzz.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "json-bigint-patch";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { tryAsGas } from "@typeberry/lib/pvm-interface";
|
|
5
|
+
import { Interpreter } from "@typeberry/lib/pvm-interpreter";
|
|
6
|
+
import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram, wrapAsProgram } from "../build/release.js";
|
|
7
|
+
const runNumber = 0;
|
|
8
|
+
export function fuzz(data) {
|
|
9
|
+
const gas = 200n;
|
|
10
|
+
const pc = 0;
|
|
11
|
+
const vm = new Interpreter();
|
|
12
|
+
const program = wrapAsProgram(new Uint8Array(data));
|
|
13
|
+
if (program.length > 100) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
vm.resetGeneric(program, pc, tryAsGas(gas));
|
|
18
|
+
vm.runProgram();
|
|
19
|
+
const printDebugInfo = false;
|
|
20
|
+
const registers = Array(13)
|
|
21
|
+
.join(",")
|
|
22
|
+
.split(",")
|
|
23
|
+
.map(() => BigInt(0));
|
|
24
|
+
const exe = prepareProgram(InputKind.Generic, HasMetadata.No, Array.from(program), registers, [], [], []);
|
|
25
|
+
const output = runProgram(exe, gas, pc, printDebugInfo);
|
|
26
|
+
const vmRegisters = decodeRegistersFromTypeberry(vm);
|
|
27
|
+
collectErrors((assertFn) => {
|
|
28
|
+
assertFn(normalizeStatus(vm.getStatus()), normalizeStatus(output.status), "status");
|
|
29
|
+
assertFn(vm.gas.get(), output.gas, "gas");
|
|
30
|
+
assertFn(vmRegisters, output.registers, "registers");
|
|
31
|
+
assertFn(vm.getPC(), output.pc, "pc");
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
if (runNumber % 100000 === 0) {
|
|
35
|
+
writeTestCase(program, {
|
|
36
|
+
pc,
|
|
37
|
+
gas,
|
|
38
|
+
registers,
|
|
39
|
+
}, {
|
|
40
|
+
status: normalizeStatus(vm.getStatus()),
|
|
41
|
+
gasLeft: vm.gas.get(),
|
|
42
|
+
pc: vm.getPC(),
|
|
43
|
+
registers: vmRegisters,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
console.warn("Unable to write file", e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
const hex = programHex(program);
|
|
53
|
+
console.log(program);
|
|
54
|
+
console.log(linkTo(hex));
|
|
55
|
+
console.log(disassemble(Array.from(program), InputKind.Generic, HasMetadata.No));
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function programHex(program) {
|
|
60
|
+
return Array.from(program)
|
|
61
|
+
.map((x) => x.toString(16).padStart(2, "0"))
|
|
62
|
+
.join("");
|
|
63
|
+
}
|
|
64
|
+
function linkTo(programHex) {
|
|
65
|
+
return `https://pvm.fluffylabs.dev/?program=0x${programHex}#/`;
|
|
66
|
+
}
|
|
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
|
+
function decodeRegistersFromTypeberry(vm) {
|
|
84
|
+
const registers = [];
|
|
85
|
+
// Try to get up to 13 registers (common register count)
|
|
86
|
+
for (let i = 0; i < 13; i++) {
|
|
87
|
+
try {
|
|
88
|
+
registers.push(vm.registers.getU64(i));
|
|
89
|
+
}
|
|
90
|
+
catch (_e) {
|
|
91
|
+
// If we can't get a register, break
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return registers;
|
|
96
|
+
}
|
|
97
|
+
function normalizeStatus(status) {
|
|
98
|
+
if (status === 2) {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
return status;
|
|
102
|
+
}
|
|
103
|
+
function assert(tb, an, comment = "") {
|
|
104
|
+
let condition = tb !== an;
|
|
105
|
+
if (Array.isArray(tb) && Array.isArray(an)) {
|
|
106
|
+
condition = tb.toString() !== an.toString();
|
|
107
|
+
}
|
|
108
|
+
if (condition) {
|
|
109
|
+
const alsoAsHex = (f) => {
|
|
110
|
+
if (Array.isArray(f)) {
|
|
111
|
+
return `${f.map(alsoAsHex).join(", ")}`;
|
|
112
|
+
}
|
|
113
|
+
if (typeof f === "number" || typeof f === "bigint") {
|
|
114
|
+
if (BigInt(f) !== 0n) {
|
|
115
|
+
return `${f} | 0x${f.toString(16)}`;
|
|
116
|
+
}
|
|
117
|
+
return `${f}`;
|
|
118
|
+
}
|
|
119
|
+
return f;
|
|
120
|
+
};
|
|
121
|
+
throw new Error(`Diverging value: ${comment}
|
|
122
|
+
\t(typeberry) ${alsoAsHex(tb)}
|
|
123
|
+
\t(ananas) ${alsoAsHex(an)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function collectErrors(cb) {
|
|
127
|
+
const errors = [];
|
|
128
|
+
cb((tb, an, comment = "") => {
|
|
129
|
+
try {
|
|
130
|
+
assert(tb, an, comment);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
errors.push(`${e}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (errors.length > 0) {
|
|
137
|
+
throw new Error(errors.join("\n"));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function writeTestCase(program, initial, expected) {
|
|
141
|
+
const hex = programHex(program);
|
|
142
|
+
fs.mkdirSync(`../tests/length_${hex.length}`, { recursive: true });
|
|
143
|
+
fs.writeFileSync(`../tests/length_${hex.length}/${hex}.json`, JSON.stringify({
|
|
144
|
+
name: linkTo(hex),
|
|
145
|
+
"initial-regs": initial.registers,
|
|
146
|
+
"initial-pc": initial.pc,
|
|
147
|
+
"initial-page-map": [],
|
|
148
|
+
"initial-memory": [],
|
|
149
|
+
"initial-gas": initial.gas,
|
|
150
|
+
program: Array.from(program),
|
|
151
|
+
"expected-status": statusToStr(expected.status),
|
|
152
|
+
"expected-regs": Array.from(expected.registers),
|
|
153
|
+
"expected-pc": expected.pc,
|
|
154
|
+
"expected-gas": expected.gasLeft,
|
|
155
|
+
"expected-memory": [],
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
function statusToStr(status) {
|
|
159
|
+
if (status === 0) {
|
|
160
|
+
return "halt";
|
|
161
|
+
}
|
|
162
|
+
if (status === 1) {
|
|
163
|
+
return "trap";
|
|
164
|
+
}
|
|
165
|
+
if (status === 4) {
|
|
166
|
+
return "oog";
|
|
167
|
+
}
|
|
168
|
+
if (status === 3) {
|
|
169
|
+
return "host";
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`unexpected status: ${status}`);
|
|
172
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import minimist from "minimist";
|
|
4
|
+
import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram } from "../build/release.js";
|
|
5
|
+
const HELP_TEXT = `Usage:
|
|
6
|
+
anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>
|
|
7
|
+
anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
disassemble Disassemble PVM bytecode to assembly
|
|
11
|
+
run Execute PVM bytecode
|
|
12
|
+
|
|
13
|
+
Flags:
|
|
14
|
+
--spi Treat input as JAM SPI format
|
|
15
|
+
--no-metadata Input does not contain metadata
|
|
16
|
+
--no-logs Disable execution logs (run command only)
|
|
17
|
+
--pc <number> Set initial program counter (default: 0)
|
|
18
|
+
--gas <number> Set initial gas amount (default: 0)
|
|
19
|
+
--help, -h Show this help message`;
|
|
20
|
+
main();
|
|
21
|
+
function main() {
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
// Handle global help flags
|
|
24
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
25
|
+
console.log(HELP_TEXT);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const subCommand = args[0];
|
|
29
|
+
switch (subCommand) {
|
|
30
|
+
case "disassemble":
|
|
31
|
+
handleDisassemble(args.slice(1));
|
|
32
|
+
break;
|
|
33
|
+
case "run":
|
|
34
|
+
handleRun(args.slice(1));
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
console.error(`Error: Unknown sub-command '${subCommand}'`);
|
|
38
|
+
console.error("");
|
|
39
|
+
console.error(HELP_TEXT);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function handleDisassemble(args) {
|
|
44
|
+
const parsed = minimist(args, {
|
|
45
|
+
boolean: ["spi", "no-metadata", "help"],
|
|
46
|
+
alias: { h: "help" },
|
|
47
|
+
});
|
|
48
|
+
if (parsed.help) {
|
|
49
|
+
console.log(HELP_TEXT);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const files = parsed._;
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
console.error("Error: No file provided for disassemble command.");
|
|
55
|
+
console.error("Usage: anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (files.length > 1) {
|
|
59
|
+
console.error("Error: Only one file can be disassembled at a time.");
|
|
60
|
+
console.error("Usage: anan-as disassemble [--spi] [--no-metadata] <file.(jam|pvm|spi|bin)>");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const file = files[0];
|
|
64
|
+
// Validate file extension for disassemble command
|
|
65
|
+
const validExtensions = [".jam", ".pvm", ".spi", ".bin"];
|
|
66
|
+
const dotIndex = file.lastIndexOf(".");
|
|
67
|
+
if (dotIndex === -1) {
|
|
68
|
+
console.error(`Error: File '${file}' has no extension.`);
|
|
69
|
+
console.error("Supported extensions: .jam, .pvm, .spi, .bin");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const ext = file.substring(dotIndex);
|
|
73
|
+
if (!validExtensions.includes(ext)) {
|
|
74
|
+
console.error(`Error: Invalid file extension '${ext}' for disassemble command.`);
|
|
75
|
+
console.error("Supported extensions: .jam, .pvm, .spi, .bin");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const kind = parsed.spi ? InputKind.SPI : InputKind.Generic;
|
|
79
|
+
const hasMetadata = parsed["no-metadata"] ? HasMetadata.No : HasMetadata.Yes;
|
|
80
|
+
const f = readFileSync(file);
|
|
81
|
+
const name = kind === InputKind.Generic ? "generic PVM" : "JAM SPI";
|
|
82
|
+
console.log(`🤖 Assembly of ${file} (as ${name})`);
|
|
83
|
+
console.log(disassemble(Array.from(f), kind, hasMetadata));
|
|
84
|
+
}
|
|
85
|
+
function handleRun(args) {
|
|
86
|
+
const parsed = minimist(args, {
|
|
87
|
+
boolean: ["spi", "no-logs", "no-metadata", "help"],
|
|
88
|
+
string: ["pc", "gas"],
|
|
89
|
+
alias: { h: "help" },
|
|
90
|
+
});
|
|
91
|
+
if (parsed.help) {
|
|
92
|
+
console.log(HELP_TEXT);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const files = parsed._;
|
|
96
|
+
if (files.length === 0) {
|
|
97
|
+
console.error("Error: No file provided for run command.");
|
|
98
|
+
console.error("Usage: anan-as run [--spi] [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam> [spi-args.bin]");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const kind = parsed.spi ? InputKind.SPI : InputKind.Generic;
|
|
102
|
+
let programFile;
|
|
103
|
+
let spiArgsFile;
|
|
104
|
+
if (kind === InputKind.SPI) {
|
|
105
|
+
// For SPI programs, expect: <program.spi> [spi-args.bin]
|
|
106
|
+
if (files.length > 2) {
|
|
107
|
+
console.error("Error: Too many arguments for SPI run command.");
|
|
108
|
+
console.error("Usage: anan-as run --spi [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <program.spi> [spi-args.bin]");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
programFile = files[0];
|
|
112
|
+
spiArgsFile = files[1]; // optional
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// For generic programs, expect exactly one file
|
|
116
|
+
if (files.length > 1) {
|
|
117
|
+
console.error("Error: Only one file can be run at a time.");
|
|
118
|
+
console.error("Usage: anan-as run [--no-logs] [--no-metadata] [--pc <number>] [--gas <number>] <file.jam>");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
programFile = files[0];
|
|
122
|
+
}
|
|
123
|
+
// Validate program file extension
|
|
124
|
+
const expectedExt = kind === InputKind.SPI ? ".spi" : ".jam";
|
|
125
|
+
const dotIndex = programFile.lastIndexOf(".");
|
|
126
|
+
if (dotIndex === -1) {
|
|
127
|
+
console.error(`Error: File '${programFile}' has no extension.`);
|
|
128
|
+
console.error(`Expected: ${expectedExt}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const ext = programFile.substring(dotIndex);
|
|
132
|
+
if (ext !== expectedExt) {
|
|
133
|
+
console.error(`Error: Invalid file extension '${ext}' for run command.`);
|
|
134
|
+
console.error(`Expected: ${expectedExt}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
// Validate SPI args file if provided
|
|
138
|
+
let spiArgs;
|
|
139
|
+
if (spiArgsFile) {
|
|
140
|
+
const argsDotIndex = spiArgsFile.lastIndexOf(".");
|
|
141
|
+
if (argsDotIndex === -1) {
|
|
142
|
+
console.error(`Error: SPI args file '${spiArgsFile}' has no extension.`);
|
|
143
|
+
console.error(`Expected: .bin`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const argsExt = spiArgsFile.substring(argsDotIndex);
|
|
147
|
+
if (argsExt !== ".bin") {
|
|
148
|
+
console.error(`Error: SPI args file must have .bin extension, got '${argsExt}'.`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
spiArgs = new Uint8Array(readFileSync(spiArgsFile));
|
|
152
|
+
}
|
|
153
|
+
const logs = !parsed["no-logs"];
|
|
154
|
+
const hasMetadata = parsed["no-metadata"] ? HasMetadata.No : HasMetadata.Yes;
|
|
155
|
+
// Parse and validate PC and gas options
|
|
156
|
+
let initialPc = 0;
|
|
157
|
+
if (parsed.pc !== undefined) {
|
|
158
|
+
// Ensure it's a string/number, not boolean
|
|
159
|
+
if (typeof parsed.pc === "boolean") {
|
|
160
|
+
console.error("Error: --pc requires a value.");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const pcStr = String(parsed.pc);
|
|
164
|
+
// Reject floats and non-integer strings
|
|
165
|
+
if (pcStr.includes(".") || !/^-?\d+$/.test(pcStr)) {
|
|
166
|
+
console.error("Error: --pc must be a valid integer.");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const pcValue = parseInt(pcStr, 10);
|
|
170
|
+
if (!Number.isInteger(pcValue) || pcValue < 0 || pcValue > 0xffffffff) {
|
|
171
|
+
console.error("Error: --pc must be a non-negative integer <= 2^32-1.");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
initialPc = pcValue;
|
|
175
|
+
}
|
|
176
|
+
let initialGas = BigInt(0);
|
|
177
|
+
if (parsed.gas !== undefined) {
|
|
178
|
+
// Ensure it's a string/number, not boolean
|
|
179
|
+
if (typeof parsed.gas === "boolean") {
|
|
180
|
+
console.error("Error: --gas requires a value.");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const gasStr = String(parsed.gas);
|
|
184
|
+
// Reject floats and non-integer strings
|
|
185
|
+
if (gasStr.includes(".") || !/^-?\d+$/.test(gasStr)) {
|
|
186
|
+
console.error("Error: --gas must be a valid integer.");
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
let gasValue;
|
|
190
|
+
try {
|
|
191
|
+
gasValue = BigInt(gasStr);
|
|
192
|
+
}
|
|
193
|
+
catch (_e) {
|
|
194
|
+
console.error("Error: --gas must be a valid integer.");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const MAX_I64 = (1n << 63n) - 1n;
|
|
198
|
+
if (gasValue < 0n || gasValue > MAX_I64) {
|
|
199
|
+
console.error("Error: --gas must be a non-negative integer <= 2^63-1.");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
initialGas = gasValue;
|
|
203
|
+
}
|
|
204
|
+
const f = readFileSync(programFile);
|
|
205
|
+
const name = kind === InputKind.Generic ? "generic PVM" : "JAM SPI";
|
|
206
|
+
console.log(`🚀 Running ${programFile} (as ${name})`);
|
|
207
|
+
try {
|
|
208
|
+
const program = prepareProgram(kind, hasMetadata, Array.from(f), [], [], [], spiArgs ? Array.from(spiArgs) : []);
|
|
209
|
+
const result = runProgram(program, initialGas, initialPc, logs, false);
|
|
210
|
+
console.log(`Status: ${result.status}`);
|
|
211
|
+
console.log(`Exit code: ${result.exitCode}`);
|
|
212
|
+
console.log(`Program counter: ${result.pc}`);
|
|
213
|
+
console.log(`Gas remaining: ${result.gas}`);
|
|
214
|
+
console.log(`Registers: [${result.registers.join(", ")}]`);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.error(`Error running ${programFile}:`, error);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "json-bigint-patch";
|
|
3
|
+
import * as assert from "node:assert";
|
|
4
|
+
import { disassemble, getGasCosts, HasMetadata, InputKind } from "../build/release.js";
|
|
5
|
+
import { ERR, OK, read, run } from "./test-json.js";
|
|
6
|
+
// Run the CLI application
|
|
7
|
+
main();
|
|
8
|
+
// Main function
|
|
9
|
+
function main() {
|
|
10
|
+
const options = {
|
|
11
|
+
isDebug: false,
|
|
12
|
+
isSilent: false,
|
|
13
|
+
useSbrkGas: false,
|
|
14
|
+
};
|
|
15
|
+
run(processGasCost, options);
|
|
16
|
+
}
|
|
17
|
+
function processGasCost(data, options, filePath) {
|
|
18
|
+
if (options.isDebug) {
|
|
19
|
+
console.info(`🤖 reading ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
// input
|
|
22
|
+
const input = {
|
|
23
|
+
program: read(data, "program"),
|
|
24
|
+
blockGasCosts: read(data, "block_gas_costs"),
|
|
25
|
+
};
|
|
26
|
+
if (options.isDebug) {
|
|
27
|
+
const assembly = disassemble(input.program, InputKind.Generic, HasMetadata.No);
|
|
28
|
+
console.info("===========");
|
|
29
|
+
console.info(assembly);
|
|
30
|
+
console.info("\n^^^^^^^^^^^\n");
|
|
31
|
+
}
|
|
32
|
+
const result = asMap(getGasCosts(input.program, InputKind.Generic, HasMetadata.No));
|
|
33
|
+
// silent mode - just put our vals into expected (comparison done externally)
|
|
34
|
+
if (options.isSilent) {
|
|
35
|
+
data.block_gas_costs = result;
|
|
36
|
+
if (filePath !== "-") {
|
|
37
|
+
console.log(JSON.stringify(data, null, 2));
|
|
38
|
+
}
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
assert.deepStrictEqual(result, input.blockGasCosts);
|
|
43
|
+
console.log(`${OK} ${filePath}`);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
console.log(`${ERR} ${filePath}`);
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
function asMap(costs) {
|
|
52
|
+
const obj = {};
|
|
53
|
+
for (const { pc, gas } of costs) {
|
|
54
|
+
obj[pc] = Number(gas);
|
|
55
|
+
}
|
|
56
|
+
return obj;
|
|
57
|
+
}
|