Skip to content

Commit

Permalink
Test vectors conformance (#5)
Browse files Browse the repository at this point in the history
* Fix JSON tests.

* Add workflow to run JSON tests.

* Fix linter.

* Update readme.
  • Loading branch information
tomusdrw authored Nov 29, 2024
1 parent 48a8655 commit 62c0f29
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 120 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ jobs:
- run: npm run qa
- run: npm run build --if-present
- run: npm test --if-present

- name: Checkout JAM test vectors
uses: actions/checkout@v4
with:
repository: FluffyLabs/jamtestvectors
path: "./jamtestvectors"
- run: npm start ./jamtestvectors/pvm/programs/*.json
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Assembly Script implementation of the JAM PVM (32bit).
#### Todo

- [x] Memory
- [ ] [JAM tests](https://github.com/w3f/jamtestvectors/pull/3) compatibility
- [x] [JAM tests](https://github.com/w3f/jamtestvectors/pull/3) compatibility
- [ ] 64-bit & new instructions ([GrayPaper v0.5.0](https://graypaper.fluffylabs.dev))

### Why?
Expand All @@ -30,11 +30,17 @@ $ npm ci
To build the WASM modules (in `./build/{release,debug}.wasm`):

```
$ npm run asbuild
$ npm build
```

To run the example in the browser at [http://localhost:3000](http://localhost:3000).

```
$ npm start
$ npm run web
```

To run JSON test vectors.

```
$ npm start ./path/to/tests/*.json
```
154 changes: 154 additions & 0 deletions assembly/api-generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { RELEVANT_ARGS } from "./arguments";
import { INSTRUCTIONS } from "./instructions";
import { Interpreter, Status } from "./interpreter";
import { Memory, MemoryBuilder } from "./memory";
import { Access, PAGE_SIZE } from "./memory-page";
import { Program, decodeArguments, decodeProgram } from "./program";
import { NO_OF_REGISTERS, Registers } from "./registers";

export class InitialPage {
address: u32 = 0;
length: u32 = 0;
access: Access = Access.None;
}
export class InitialChunk {
address: u32 = 0;
data: u8[] = [];
}

export class VmInput {
registers: u32[] = new Array<u32>(NO_OF_REGISTERS).fill(0);
pc: u32 = 0;
gas: i64 = 0;
program: u8[] = [];
pageMap: InitialPage[] = [];
memory: InitialChunk[] = [];
}

export class VmOutput {
status: Status = Status.OK;
registers: u32[] = [];
pc: u32 = 0;
memory: InitialChunk[] = [];
gas: i64 = 0;
}

export function getAssembly(p: Program): string {
let v = "";
const len = p.code.length;
for (let i = 0; i < len; i++) {
if (!p.mask.isInstruction(i)) {
throw new Error("We should iterate only over instructions!");
}
const instruction = p.code[i];
const iData = INSTRUCTIONS[instruction];
v += "\n";
v += changetype<string>(iData.namePtr);

const argsLen = p.mask.argsLen(i);
const end = i + 1 + argsLen;
if (end > len) {
const name = changetype<string>(iData.namePtr);
const intro = "Invalid program - code is not long enough";
throw new Error(`${intro} Expected: ${argsLen} for ${name} at ${i} (${end} > ${len})`);
}

const args = decodeArguments(iData.kind, p.code.subarray(i + 1, end));
const argsArray = [args.a, args.b, args.c, args.d];
const relevantArgs = <i32>RELEVANT_ARGS[iData.kind];
for (let i = 0; i < relevantArgs; i++) {
v += ` ${argsArray[i]}, `;
}
i += argsLen;
}
return v;
}

export function runVm(input: VmInput, logs: boolean = false): VmOutput {
const p = decodeProgram(input.program);

const registers: Registers = new StaticArray(NO_OF_REGISTERS);
for (let r = 0; r < registers.length; r++) {
registers[r] = input.registers[r];
}
const memory = buildMemory(input.pageMap, input.memory);

const int = new Interpreter(p, registers, memory);
int.nextPc = -1;
int.gas.set(input.gas);

let isOk = true;
for (;;) {
if (!isOk) {
if (logs) console.log(`Finished with status: ${int.status}`);
break;
}

if (logs) console.log(`PC = ${int.pc}`);
if (logs) console.log(`STATUS = ${int.status}`);
if (logs) console.log(`REGISTERS = ${registers.join(", ")}`);
if (logs && int.pc < u32(int.program.code.length)) {
const name = changetype<string>(INSTRUCTIONS[int.program.code[int.pc]].namePtr);
console.log(`INSTRUCTION = ${name} (${int.program.code[int.pc]})`);
}

isOk = int.nextStep();
}
const output = new VmOutput();
output.status = int.status;
output.registers = int.registers.slice(0);
output.pc = int.pc;
output.gas = int.gas.get();
output.memory = getOutputChunks(int.memory);
return output;
}

export function getOutputChunks(memory: Memory): InitialChunk[] {
const chunks: InitialChunk[] = [];
const pages = memory.pages.keys();
let currentChunk: InitialChunk | null = null;
for (let i = 0; i < pages.length; i++) {
const pageIdx = pages[i];
const page = memory.pages.get(pageIdx);

for (let n = 0; n < page.raw.data.length; n++) {
const v = page.raw.data[n];
if (v !== 0) {
if (currentChunk !== null) {
currentChunk.data.push(v);
} else {
currentChunk = new InitialChunk();
currentChunk.address = pageIdx * PAGE_SIZE + n;
currentChunk.data = [v];
}
} else if (currentChunk !== null) {
chunks.push(currentChunk);
currentChunk = null;
}
}
}
if (currentChunk !== null) {
chunks.push(currentChunk);
}
return chunks;
}

export function buildMemory(pages: InitialPage[], chunks: InitialChunk[]): Memory {
const builder = new MemoryBuilder();
for (let i = 0; i < pages.length; i++) {
const initPage = pages[i];
builder.setData(initPage.access, initPage.address, new Uint8Array(initPage.length));
}

for (let i = 0; i < chunks.length; i++) {
const initChunk = chunks[i];
// access should not matter now, since we created the pages already.
const data = new Uint8Array(initChunk.data.length);
for (let i = 0; i < data.length; i++) {
data[i] = initChunk.data[i];
}
builder.setData(Access.None, initChunk.address, data);
}

return builder.build(0);
}
43 changes: 7 additions & 36 deletions assembly/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { InitialChunk, InitialPage, buildMemory } from "./api-generic";
import { Decoder } from "./codec";
import { Gas } from "./gas";
import { Interpreter, Status } from "./interpreter";
import { Memory, MemoryBuilder } from "./memory";
import { Access, PAGE_SIZE } from "./memory-page";
import { decodeProgram } from "./program";
import { NO_OF_REGISTERS, Registers } from "./registers";
Expand All @@ -27,7 +27,8 @@ export function resetGenericWithMemory(
const p = decodeProgram(program);
const registers: Registers = new StaticArray(NO_OF_REGISTERS);
fillRegisters(registers, flatRegisters);
const memory = buildMemory(pageMap, chunks);

const memory = buildMemory(readPages(pageMap), readChunks(chunks));

const int = new Interpreter(p, registers, memory);
int.gas.set(initialGas);
Expand Down Expand Up @@ -140,16 +141,6 @@ function fillRegisters(registers: Registers, flat: u8[]): void {
}
}

class InitialPage {
address: u32 = 0;
length: u32 = 0;
access: Access = Access.None;
}
class InitialChunk {
address: u32 = 0;
data: Uint8Array = new Uint8Array(0);
}

function readPages(pageMap: Uint8Array): InitialPage[] {
const pages: InitialPage[] = [];
const codec = new Decoder(pageMap);
Expand All @@ -170,31 +161,11 @@ function readChunks(chunks: Uint8Array): InitialChunk[] {
const c = new InitialChunk();
c.address = codec.u32();
const len = codec.u32();
c.data = codec.bytes(len);
const data = codec.bytes(len);
for (let i: u32 = 0; i < len; i++) {
c.data.push(data[i]);
}
res.push(c);
}
return res;
}

function buildMemory(pageMap: Uint8Array, flatChunks: Uint8Array): Memory {
console.log(`Got page: ${pageMap}`);
console.log(`Got chunks: ${flatChunks}`);
const pages = readPages(pageMap);
const chunks = readChunks(flatChunks);

const builder = new MemoryBuilder();
for (let i = 0; i < pages.length; i++) {
const initPage = pages[i];
console.log(`setting page: ${initPage.address}..+${initPage.length}`);
builder.setData(initPage.access, initPage.address, new Uint8Array(initPage.length));
}

for (let i = 0; i < chunks.length; i++) {
const initChunk = chunks[i];
console.log(`setting initial chunk: ${initChunk.address}..+${initChunk.data.length}`);
// access should not matter now, since we created the pages already.
builder.setData(Access.None, initChunk.address, initChunk.data);
}

return builder.build(0);
}
32 changes: 9 additions & 23 deletions assembly/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
import { Interpreter } from "./interpreter";
import { decodeProgram, getAssembly } from "./program";
import { NO_OF_REGISTERS, Registers } from "./registers";
import { VmInput, getAssembly, runVm } from "./api-generic";
import { decodeProgram } from "./program";

export * from "./api";
export { runVm } from "./api-generic";

export function exampleGetAssembly(program: u8[]): string {
const p = decodeProgram(program);
console.log(`Got program: ${p.toString()}`);
return getAssembly(p);
}

export function exampleRun(program: u8[]): void {
const p = decodeProgram(program);
const registers: Registers = new StaticArray(NO_OF_REGISTERS);
registers[7] = 9;
const int = new Interpreter(p, registers);
int.gas.set(10_000);

let isOk = true;
for (;;) {
if (!isOk) {
console.log(`Finished with status: ${int.status}`);
break;
}

console.log(`PC = ${int.pc}`);
console.log(`STATUS = ${int.status}`);
console.log(`REGISTERS = ${registers.join(", ")}`);

isOk = int.nextStep();
}
const input = new VmInput();
input.registers[7] = 9;
input.gas = 10_000;
input.program = program;
const output = runVm(input, true);
console.log(`Finished with status: ${output.status}`);
}
Loading

0 comments on commit 62c0f29

Please sign in to comment.