diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..e7a7095 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Running pre-push checks..." + +echo "==> QA (biome ci)" +npm run qa + +echo "==> Build" +npm run build + +echo "==> Tests" +npm test + +echo "All pre-push checks passed." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c04058..565902e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,10 @@ jobs: repository: FluffyLabs/jamtestvectors path: "./jamtestvectors" ref: ba76542dbf7a0c72d414a87ad2e30ce4da380448 # New test vectors. - - run: npm run test:w3f ./jamtestvectors/pvm/programs/*.json + - name: Run W3F tests (WASM) + run: npm run test:w3f ./jamtestvectors/pvm/programs/*.json + - name: Run W3F tests (portable JS) + run: npm run test:w3f-portable ./jamtestvectors/pvm/programs/*.json build-and-test: runs-on: ubuntu-latest @@ -42,5 +45,3 @@ jobs: - run: npm run qa - run: npm run build - run: npm test --if-present - - diff --git a/assembly/api-debugger.ts b/assembly/api-debugger.ts index e4767df..082e286 100644 --- a/assembly/api-debugger.ts +++ b/assembly/api-debugger.ts @@ -6,7 +6,7 @@ import { Interpreter, Status } from "./interpreter"; import { MaybePageFault, MemoryBuilder } from "./memory"; import { Access, PAGE_SIZE } from "./memory-page"; import { deblob, extractCodeAndMetadata, liftBytes } from "./program"; -import { NO_OF_REGISTERS, REG_SIZE_BYTES, Registers } from "./registers"; +import { NO_OF_REGISTERS, newRegisters, REG_SIZE_BYTES, Registers } from "./registers"; import { decodeSpi } from "./spi"; let interpreter: Interpreter | null = null; @@ -30,7 +30,7 @@ export function resetGeneric(program: u8[], flatRegisters: u8[], initialGas: Gas const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); const p = deblob(code); - const registers: Registers = new StaticArray(NO_OF_REGISTERS); + const registers: Registers = newRegisters(); fillRegisters(registers, flatRegisters); const int = new Interpreter(p, registers); int.gas.set(initialGas); @@ -53,7 +53,7 @@ export function resetGenericWithMemory( const code = hasMetadata ? extractCodeAndMetadata(liftBytes(program)).code : liftBytes(program); const p = deblob(code); - const registers: Registers = new StaticArray(NO_OF_REGISTERS); + const registers: Registers = newRegisters(); fillRegisters(registers, flatRegisters); const builder = new MemoryBuilder(); @@ -115,7 +115,7 @@ export function getExitArg(): u32 { export function getGasLeft(): i64 { if (interpreter === null) { - return 0; + return i64(0); } const int = interpreter; return int.gas.get(); @@ -139,8 +139,8 @@ export function getRegisters(): Uint8Array { let val = int.registers[i]; for (let j = 0; j < REG_SIZE_BYTES; j++) { const index = i * REG_SIZE_BYTES + j; - flat[index] = (val & 0xff); - val = val >> 8; + flat[index] = (val & u64(0xff)); + val = val >> u64(8); } } @@ -249,10 +249,10 @@ function fillRegisters(registers: Registers, flat: u8[]): void { } for (let i = 0; i < registers.length; i++) { - let num: u64 = 0; + let num: u64 = u64(0); for (let j: u8 = 0; j < REG_SIZE_BYTES; j++) { const index = i * REG_SIZE_BYTES + j; - num |= (flat[index]) << (j * 8); + num |= (flat[index]) << u64(j * 8); } registers[i] = num; } diff --git a/assembly/api-internal.ts b/assembly/api-internal.ts index 034d579..d53bb6a 100644 --- a/assembly/api-internal.ts +++ b/assembly/api-internal.ts @@ -4,6 +4,7 @@ import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; import { Interpreter, Status } from "./interpreter"; import { MaybePageFault, Memory, MemoryBuilder } from "./memory"; import { Access, PAGE_SIZE, RESERVED_MEMORY } from "./memory-page"; +import { portable } from "./portable"; import { decodeArguments, Program, resolveArguments } from "./program"; export function getAssembly(p: Program): string { @@ -25,7 +26,7 @@ export function getAssembly(p: Program): string { v += "\n"; v += `${i}: `; - v += changetype(iData.namePtr); + v += iData.name; v += `(${instruction})`; const skipBytes = p.mask.skipBytesToNextInstruction(i); @@ -144,8 +145,8 @@ function readResult(int: Interpreter): u8[] { } // JAM return convention - const ptr_start = u32(int.registers[7] & 0xffff_ffff); - const ptr_end = u32(int.registers[8] & 0xffff_ffff); + const ptr_start = u32(int.registers[7] & u64(0xffff_ffff)); + const ptr_end = u32(int.registers[8] & u64(0xffff_ffff)); // invalid output result if (ptr_start >= ptr_end) { @@ -176,7 +177,8 @@ function readResult(int: Interpreter): u8[] { function getOutputChunks(memory: Memory): InitialChunk[] { const chunks: InitialChunk[] = []; - const pages = memory.pages.keys(); + // @ts-ignore: AS returns T[], JS returns iterator - asArray handles both + const pages: u32[] = portable.asArray(memory.pages.keys()); let currentChunk: InitialChunk | null = null; for (let i = 0; i < pages.length; i++) { const pageIdx = pages[i]; diff --git a/assembly/api-types.ts b/assembly/api-types.ts index 36d6977..13be7c3 100644 --- a/assembly/api-types.ts +++ b/assembly/api-types.ts @@ -25,7 +25,7 @@ export class VmRunOptions { export class VmInput { pc: u32 = 0; - gas: i64 = 0; + gas: i64 = i64(0); constructor( public readonly program: Program, @@ -39,7 +39,7 @@ export class VmPause { exitCode: u32 = 0; pc: u32 = 0; nextPc: u32 = 0; - gas: i64 = 0; + gas: i64 = i64(0); registers: u64[] = []; } @@ -47,7 +47,7 @@ export class VmOutput { status: Status = Status.OK; exitCode: u32 = 0; pc: u32 = 0; - gas: i64 = 0; + gas: i64 = i64(0); result: u8[] = []; registers: u64[] = []; memory: InitialChunk[] = []; diff --git a/assembly/api-utils.ts b/assembly/api-utils.ts index 3125141..6ffd0c5 100644 --- a/assembly/api-utils.ts +++ b/assembly/api-utils.ts @@ -3,8 +3,9 @@ import { InitialChunk, InitialPage, VmInput, VmOutput, VmPause, VmRunOptions } f import { BlockGasCost, computeGasCosts } from "./gas-costs"; import { Interpreter } from "./interpreter"; import { MaybePageFault, MemoryBuilder } from "./memory"; +import { portable } from "./portable"; import { deblob, extractCodeAndMetadata, liftBytes } from "./program"; -import { NO_OF_REGISTERS, Registers } from "./registers"; +import { NO_OF_REGISTERS, newRegisters, Registers } from "./registers"; import { decodeSpi, StandardProgram } from "./spi"; export enum InputKind { @@ -20,7 +21,8 @@ export enum HasMetadata { export function getGasCosts(input: u8[], kind: InputKind, withMetadata: HasMetadata): BlockGasCost[] { const program = prepareProgram(kind, withMetadata, input, [], [], [], [], 0); - return computeGasCosts(program.program).values(); + // @ts-ignore: AS returns T[], JS returns iterator - asArray handles both + return portable.asArray(computeGasCosts(program.program).values()); } export function disassemble(input: u8[], kind: InputKind, withMetadata: HasMetadata): string { @@ -59,7 +61,9 @@ export function prepareProgram( if (hasMetadata === HasMetadata.Yes) { const data = extractCodeAndMetadata(code); + // @ts-ignore: TS 5.9 Uint8Array generic parameter mismatch code = data.code; + // @ts-ignore: TS 5.9 Uint8Array generic parameter mismatch metadata = data.metadata; } @@ -69,7 +73,7 @@ export function prepareProgram( const builder = new MemoryBuilder(preallocateMemoryPages); const memory = buildMemory(builder, initialPageMap, initialMemory); - const registers: Registers = new StaticArray(NO_OF_REGISTERS); + const registers: Registers = newRegisters(); const safeLen = initialRegisters.length < NO_OF_REGISTERS ? initialRegisters.length : NO_OF_REGISTERS; for (let r = 0; r < safeLen; r++) { registers[r] = initialRegisters[r]; @@ -100,7 +104,7 @@ export function runProgram( dumpMemory: boolean = false, ): VmOutput { const vmInput = new VmInput(program.program, program.memory, program.registers); - vmInput.gas = initialGas; + vmInput.gas = i64(initialGas); vmInput.pc = programCounter; const vmOptions = new VmRunOptions(); diff --git a/assembly/arguments.ts b/assembly/arguments.ts index e1cc964..b371546 100644 --- a/assembly/arguments.ts +++ b/assembly/arguments.ts @@ -1,4 +1,5 @@ import { minI32 } from "./math"; +import { portable } from "./portable"; export enum Arguments { Zero = 0, @@ -53,7 +54,7 @@ export class Args { type ArgsDecoder = (args: Args, code: u8[], offset: u32, end: u32) => Args; function twoImm(args: Args, code: u8[], offset: u32, end: u32): Args { - const low = lowNibble(unchecked(code[offset])); + const low = lowNibble(portable.arrayAt(code, offset)); const split = minI32(4, low) + 1; const first = decodeI32(code, offset + 1, offset + split); const second = decodeI32(code, offset + split, end); @@ -155,7 +156,7 @@ function decodeI32(input: u8[], start: u32, end: u32): u32 { for (let i: u32 = 0; i < len; i++) { num |= u32(input[start + i]) << (i * 8); } - const msb = unchecked(input[start + len - 1]) & 0x80; + const msb = portable.arrayAt(input, start + len - 1) & 0x80; if (len < 4 && msb > 0) { num |= 0xffff_ffff << (len * 8); } @@ -167,5 +168,5 @@ function decodeU32(data: u8[], offset: u32): u32 { num |= u32(data[offset + 1]) << 8; num |= u32(data[offset + 2]) << 16; num |= u32(data[offset + 3]) << 24; - return num; + return portable.asU32(num); } diff --git a/assembly/codec.ts b/assembly/codec.ts index e047095..b1258b7 100644 --- a/assembly/codec.ts +++ b/assembly/codec.ts @@ -119,7 +119,8 @@ export function decodeVarU32(data: Uint8Array): ValOffset { } export function encodeVarU32(v: u64): Uint8Array { - if (v === 0) { + v = u64(v); + if (v === u64(0)) { return new Uint8Array(1); } @@ -134,27 +135,27 @@ export function encodeVarU32(v: u64): Uint8Array { } // let's look for the correct range - let minEncoded = maxEncoded >> 7; + let minEncoded = maxEncoded >> u64(7); for (let l = 7; l >= 0; l -= 1) { if (v >= minEncoded) { const dest = new Uint8Array(l + 1); // encode the first byte - const maxVal = 2 ** (8 * l); - const byte = 2 ** 8 - 2 ** (8 - l) + v / maxVal; + const maxVal = u64(2 ** (8 * l)); + const byte = (u32(2 ** 8 - 2 ** (8 - l)) + u32(v / maxVal)) & 0xffff_ffff; dest[0] = u8(byte); // now encode the rest of bytes of len `l` let rest = v % maxVal; for (let i = 1; i < 1 + l; i += 1) { dest[i] = u8(rest); - rest >>= 8; + rest >>= u64(8); } return dest; } // move one power down maxEncoded = minEncoded; - minEncoded >>= 7; + minEncoded >>= u64(7); } throw new Error(`Unhandled number encoding: ${v}`); diff --git a/assembly/gas-costs.ts b/assembly/gas-costs.ts index c7e0e8e..0ab9298 100644 --- a/assembly/gas-costs.ts +++ b/assembly/gas-costs.ts @@ -1,9 +1,10 @@ import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; +import { portable } from "./portable"; import { Program } from "./program"; export class BlockGasCost { pc: u32 = 0; - gas: u64 = 0; + gas: u64 = u64(0); } export function computeGasCosts(p: Program): Map { @@ -29,7 +30,7 @@ export function computeGasCosts(p: Program): Map { if (currentBlock !== null) { // add gas for current instruction - currentBlock.gas += iData.gas; + currentBlock.gas = portable.u64_add(currentBlock.gas, iData.gas); } // move forward diff --git a/assembly/gas.ts b/assembly/gas.ts index 835691a..7eb1af5 100644 --- a/assembly/gas.ts +++ b/assembly/gas.ts @@ -25,7 +25,7 @@ class GasCounterU64 implements GasCounter { constructor(private gas: Gas) {} set(g: Gas): void { - this.gas = g; + this.gas = i64(g); } get(): Gas { @@ -33,9 +33,9 @@ class GasCounterU64 implements GasCounter { } sub(g: Gas): boolean { - this.gas = this.gas - g; - if (this.gas < 0) { - this.gas = 0; + this.gas = this.gas - i64(g); + if (this.gas < i64(0)) { + this.gas = i64(0); return true; } return false; diff --git a/assembly/index-shared.ts b/assembly/index-shared.ts new file mode 100644 index 0000000..324480f --- /dev/null +++ b/assembly/index-shared.ts @@ -0,0 +1,4 @@ +export * from "./api-debugger"; +export { getAssembly } from "./api-internal"; +export * from "./api-utils"; +export { wrapAsProgram } from "./program-build"; diff --git a/assembly/index.ts b/assembly/index.ts index f1fde66..fb0e7d1 100644 --- a/assembly/index.ts +++ b/assembly/index.ts @@ -1,4 +1 @@ -export * from "./api-debugger"; -export * from "./api-internal"; -export * from "./api-utils"; -export * from "./program-build"; +export * from "./index-shared"; diff --git a/assembly/instructions.ts b/assembly/instructions.ts index a5d38f7..e60a7f9 100644 --- a/assembly/instructions.ts +++ b/assembly/instructions.ts @@ -1,19 +1,18 @@ import { Arguments } from "./arguments"; import { Gas } from "./gas"; -@unmanaged export class Instruction { - namePtr: usize = 0; + name: string = ""; kind: Arguments = Arguments.Zero; - gas: Gas = 0; + gas: Gas = i64(0); isTerminating: boolean = false; } function instruction(name: string, kind: Arguments, gas: Gas, isTerminating: boolean = false): Instruction { const i = new Instruction(); - i.namePtr = changetype(name); + i.name = name; i.kind = kind; - i.gas = gas; + i.gas = i64(gas); i.isTerminating = isTerminating; return i; } diff --git a/assembly/instructions/bit.ts b/assembly/instructions/bit.ts index 60bc9ac..99b457f 100644 --- a/assembly/instructions/bit.ts +++ b/assembly/instructions/bit.ts @@ -1,39 +1,40 @@ +import { portable } from "../portable"; import { InstructionRun, ok } from "./outcome"; import { reg, u8SignExtend, u16SignExtend } from "./utils"; // COUNT_SET_BITS_64 export const count_set_bits_64: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = popcnt(regs[reg(args.a)]); + regs[reg(args.b)] = portable.popcnt_u64(regs[reg(args.a)]); return ok(r); }; // COUNT_SET_BITS_32 export const count_set_bits_32: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = popcnt(u32(regs[reg(args.a)])); + regs[reg(args.b)] = u64(portable.popcnt_u32(u32(regs[reg(args.a)]))); return ok(r); }; // LEADING_ZERO_BITS_64 export const leading_zero_bits_64: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = clz(regs[reg(args.a)]); + regs[reg(args.b)] = portable.clz_u64(regs[reg(args.a)]); return ok(r); }; // LEADING_ZERO_BITS_32 export const leading_zero_bits_32: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = clz(u32(regs[reg(args.a)])); + regs[reg(args.b)] = u64(portable.clz_u32(u32(regs[reg(args.a)]))); return ok(r); }; // TRAILING_ZERO_BITS_64 export const trailing_zero_bits_64: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = ctz(regs[reg(args.a)]); + regs[reg(args.b)] = portable.ctz_u64(regs[reg(args.a)]); return ok(r); }; // TRAILING_ZERO_BITS_32 export const trailing_zero_bits_32: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = ctz(u32(regs[reg(args.a)])); + regs[reg(args.b)] = u64(portable.ctz_u32(u32(regs[reg(args.a)]))); return ok(r); }; @@ -57,6 +58,6 @@ export const zero_extend_16: InstructionRun = (r, args, regs) => { // REVERSE_BYTES export const reverse_bytes: InstructionRun = (r, args, regs) => { - regs[reg(args.b)] = bswap(regs[reg(args.a)]); + regs[reg(args.b)] = portable.bswap_u64(regs[reg(args.a)]); return ok(r); }; diff --git a/assembly/instructions/branch.ts b/assembly/instructions/branch.ts index f4e3e3e..1f4f189 100644 --- a/assembly/instructions/branch.ts +++ b/assembly/instructions/branch.ts @@ -57,7 +57,7 @@ export const branch_gt_u_imm: InstructionRun = (r, args, registers) => { // BRANCH_LT_S_IMM export const branch_lt_s_imm: InstructionRun = (r, args, registers) => { - if (i64(registers[reg(args.a)]) < u32SignExtend(args.b)) { + if (i64(registers[reg(args.a)]) < i64(u32SignExtend(args.b))) { return staticJump(r, args.c); } return ok(r); @@ -65,7 +65,7 @@ export const branch_lt_s_imm: InstructionRun = (r, args, registers) => { // BRANCH_LE_S_IMM export const branch_le_s_imm: InstructionRun = (r, args, registers) => { - if (i64(registers[reg(args.a)]) <= u32SignExtend(args.b)) { + if (i64(registers[reg(args.a)]) <= i64(u32SignExtend(args.b))) { return staticJump(r, args.c); } return ok(r); @@ -73,7 +73,7 @@ export const branch_le_s_imm: InstructionRun = (r, args, registers) => { // BRANCH_GE_S_IMM export const branch_ge_s_imm: InstructionRun = (r, args, registers) => { - if (i64(registers[reg(args.a)]) >= u32SignExtend(args.b)) { + if (i64(registers[reg(args.a)]) >= i64(u32SignExtend(args.b))) { return staticJump(r, args.c); } return ok(r); @@ -81,7 +81,7 @@ export const branch_ge_s_imm: InstructionRun = (r, args, registers) => { // BRANCH_GT_S_IMM export const branch_gt_s_imm: InstructionRun = (r, args, registers) => { - if (i64(registers[reg(args.a)]) > u32SignExtend(args.b)) { + if (i64(registers[reg(args.a)]) > i64(u32SignExtend(args.b))) { return staticJump(r, args.c); } return ok(r); diff --git a/assembly/instructions/jump.ts b/assembly/instructions/jump.ts index 34231d2..71400d5 100644 --- a/assembly/instructions/jump.ts +++ b/assembly/instructions/jump.ts @@ -1,3 +1,4 @@ +import { portable } from "../portable"; import { dJump, InstructionRun, staticJump } from "./outcome"; import { reg, u32SignExtend } from "./utils"; @@ -6,7 +7,7 @@ export const jump: InstructionRun = (r, args) => staticJump(r, args.a); // JUMP_IND export const jump_ind: InstructionRun = (r, args, registers) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.b)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.b))); return dJump(r, address); }; @@ -18,7 +19,7 @@ export const load_imm_jump: InstructionRun = (r, args, registers) => { // LOAD_IMM_JUMP_IND export const load_imm_jump_ind: InstructionRun = (r, args, registers) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.d)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.d))); registers[reg(args.b)] = u32SignExtend(args.c); return dJump(r, address); }; diff --git a/assembly/instructions/load.ts b/assembly/instructions/load.ts index fcde315..7b0a7e7 100644 --- a/assembly/instructions/load.ts +++ b/assembly/instructions/load.ts @@ -1,4 +1,5 @@ import { MaybePageFault } from "../memory"; +import { portable } from "../portable"; import { InstructionRun, ok, okOrFault } from "./outcome"; import { reg, u32SignExtend } from "./utils"; @@ -6,7 +7,7 @@ const faultRes: MaybePageFault = new MaybePageFault(); // LOAD_IMM_64 export const load_imm_64: InstructionRun = (r, args, registers) => { - registers[reg(args.a)] = u64(args.b) + (u64(args.c) << 32); + registers[reg(args.a)] = portable.u64_add(u64(args.b), u64(args.c) << u64(32)); return ok(r); }; @@ -81,7 +82,7 @@ export const load_u64: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_U8 export const load_ind_u8: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); const result = memory.getU8(faultRes, address); if (!faultRes.isFault) { registers[reg(args.b)] = result; @@ -91,7 +92,7 @@ export const load_ind_u8: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_I8 export const load_ind_i8: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); const result = memory.getI8(faultRes, address); if (!faultRes.isFault) { registers[reg(args.b)] = result; @@ -101,7 +102,7 @@ export const load_ind_i8: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_U16 export const load_ind_u16: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); const result = memory.getU16(faultRes, address); if (!faultRes.isFault) { registers[reg(args.b)] = result; @@ -111,7 +112,7 @@ export const load_ind_u16: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_I16 export const load_ind_i16: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); const result = memory.getI16(faultRes, address); if (!faultRes.isFault) { registers[reg(args.b)] = result; @@ -121,8 +122,8 @@ export const load_ind_i16: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_U32 export const load_ind_u32: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); - const result = memory.getU32(faultRes, u32(address)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); + const result = memory.getU32(faultRes, address); if (!faultRes.isFault) { registers[reg(args.b)] = result; } @@ -131,8 +132,8 @@ export const load_ind_u32: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_I32 export const load_ind_i32: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); - const result = memory.getI32(faultRes, u32(address)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); + const result = memory.getI32(faultRes, address); if (!faultRes.isFault) { registers[reg(args.b)] = result; } @@ -141,7 +142,7 @@ export const load_ind_i32: InstructionRun = (r, args, registers, memory) => { // LOAD_IND_U64 export const load_ind_u64: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = u32(portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c))); const result = memory.getU64(faultRes, u32(address)); if (!faultRes.isFault) { registers[reg(args.b)] = result; diff --git a/assembly/instructions/logic.ts b/assembly/instructions/logic.ts index 6113cfe..2b37413 100644 --- a/assembly/instructions/logic.ts +++ b/assembly/instructions/logic.ts @@ -45,12 +45,12 @@ export const and_inv: InstructionRun = (r, args, registers) => { // OR_INV export const or_inv: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = registers[reg(args.b)] | ~registers[reg(args.a)]; + registers[reg(args.c)] = u64(registers[reg(args.b)] | ~registers[reg(args.a)]); return ok(r); }; // XNOR export const xnor: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = ~(registers[reg(args.b)] ^ registers[reg(args.a)]); + registers[reg(args.c)] = u64(~(registers[reg(args.b)] ^ registers[reg(args.a)])); return ok(r); }; diff --git a/assembly/instructions/math.ts b/assembly/instructions/math.ts index 15adf6c..193dccc 100644 --- a/assembly/instructions/math.ts +++ b/assembly/instructions/math.ts @@ -1,3 +1,4 @@ +import { portable } from "../portable"; import { InstructionRun, ok } from "./outcome"; import { mulUpperSigned, mulUpperSignedUnsigned, mulUpperUnsigned, reg, u32SignExtend } from "./utils"; @@ -5,39 +6,39 @@ import { mulUpperSigned, mulUpperSignedUnsigned, mulUpperUnsigned, reg, u32SignE export const add_imm_32: InstructionRun = (r, args, registers) => { const a = registers[reg(args.a)]; const c = u32SignExtend(args.c); - registers[reg(args.b)] = u32SignExtend(u32(a + c)); + registers[reg(args.b)] = u32SignExtend(u32(portable.u64_add(a, c))); return ok(r); }; // MUL_IMM_32 export const mul_imm_32: InstructionRun = (r, args, registers) => { - registers[reg(args.b)] = u32SignExtend(u32(registers[reg(args.a)] * args.c)); + registers[reg(args.b)] = u32SignExtend(u32(portable.u64_mul(registers[reg(args.a)], u64(args.c)))); return ok(r); }; // NEG_ADD_IMM_32 export const neg_add_imm_32: InstructionRun = (r, args, registers) => { - const sum = (u64(args.c) | 0x1_0000_0000) - registers[reg(args.a)]; + const sum = portable.u64_sub(u64(args.c) | u64(0x1_0000_0000), registers[reg(args.a)]); registers[reg(args.b)] = u32SignExtend(u32(sum)); return ok(r); }; // ADD_IMM export const add_imm: InstructionRun = (r, args, registers) => { - const sum: u64 = registers[reg(args.a)] + u32SignExtend(args.c); + const sum: u64 = portable.u64_add(registers[reg(args.a)], u32SignExtend(args.c)); registers[reg(args.b)] = sum; return ok(r); }; // MUL_IMM export const mul_imm: InstructionRun = (r, args, registers) => { - registers[reg(args.b)] = registers[reg(args.a)] * u32SignExtend(args.c); + registers[reg(args.b)] = portable.u64_mul(registers[reg(args.a)], u32SignExtend(args.c)); return ok(r); }; // NEG_ADD_IMM export const neg_add_imm: InstructionRun = (r, args, registers) => { - const sum = u32SignExtend(args.c) - registers[reg(args.a)]; + const sum = portable.u64_sub(u32SignExtend(args.c), registers[reg(args.a)]); registers[reg(args.b)] = sum; return ok(r); }; @@ -52,13 +53,15 @@ export const add_32: InstructionRun = (r, args, registers) => { // SUB_32 export const sub_32: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = u32SignExtend(u32(registers[reg(args.b)] + 2 ** 32 - u32(registers[reg(args.a)]))); + const a = registers[reg(args.b)]; + const b = u64(0x1_0000_0000 - u32(registers[reg(args.a)])); + registers[reg(args.c)] = u32SignExtend(u32(portable.u64_add(a, b))); return ok(r); }; // MUL_32 export const mul_32: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = u32SignExtend(u32(registers[reg(args.a)] * registers[reg(args.b)])); + registers[reg(args.c)] = u32SignExtend(u32(portable.u64_mul(registers[reg(args.a)], registers[reg(args.b)]))); return ok(r); }; @@ -76,14 +79,14 @@ export const div_u_32: InstructionRun = (r, args, registers) => { // DIV_S_32 export const div_s_32: InstructionRun = (r, args, registers) => { - const b = u32SignExtend(u32(registers[reg(args.b)])); - const a = u32SignExtend(u32(registers[reg(args.a)])); - if (a === 0) { + const b = i64(u32SignExtend(u32(registers[reg(args.b)]))); + const a = i64(u32SignExtend(u32(registers[reg(args.a)]))); + if (a === i64(0)) { registers[reg(args.c)] = u64.MAX_VALUE; - } else if (a === -1 && b === i32.MIN_VALUE) { - registers[reg(args.c)] = b; + } else if (a === i64(-1) && b === i64(i32.MIN_VALUE)) { + registers[reg(args.c)] = u64(b); } else { - registers[reg(args.c)] = b / a; + registers[reg(args.c)] = u64(b / a); } return ok(r); }; @@ -105,36 +108,42 @@ export const rem_s_32: InstructionRun = (r, args, registers) => { const b = i32(registers[reg(args.b)]); const a = i32(registers[reg(args.a)]); if (a === 0) { - registers[reg(args.c)] = i64(b); + registers[reg(args.c)] = u64(i64(b)); } else if (a === -1 && b === i32.MIN_VALUE) { - registers[reg(args.c)] = 0; + registers[reg(args.c)] = u64(0); } else { - registers[reg(args.c)] = i64(b) % i64(a); + registers[reg(args.c)] = u64(i64(b) % i64(a)); } return ok(r); }; // ADD_64 export const add_64: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = registers[reg(args.a)] + registers[reg(args.b)]; + const a = registers[reg(args.a)]; + const b = registers[reg(args.b)]; + registers[reg(args.c)] = portable.u64_add(a, b); return ok(r); }; // SUB export const sub: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = registers[reg(args.b)] - registers[reg(args.a)]; + const a = registers[reg(args.a)]; + const b = registers[reg(args.b)]; + registers[reg(args.c)] = portable.u64_sub(b, a); return ok(r); }; // MUL export const mul: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = registers[reg(args.a)] * registers[reg(args.b)]; + const a = registers[reg(args.a)]; + const b = registers[reg(args.b)]; + registers[reg(args.c)] = portable.u64_mul(a, b); return ok(r); }; // DIV_U export const div_u: InstructionRun = (r, args, registers) => { - if (registers[reg(args.a)] === 0) { + if (registers[reg(args.a)] === u64(0)) { registers[reg(args.c)] = u64.MAX_VALUE; } else { registers[reg(args.c)] = registers[reg(args.b)] / registers[reg(args.a)]; @@ -146,19 +155,19 @@ export const div_u: InstructionRun = (r, args, registers) => { export const div_s: InstructionRun = (r, args, registers) => { const b = i64(registers[reg(args.b)]); const a = i64(registers[reg(args.a)]); - if (a === 0) { + if (a === i64(0)) { registers[reg(args.c)] = u64.MAX_VALUE; - } else if (a === -1 && b === i64.MIN_VALUE) { - registers[reg(args.c)] = b; + } else if (a === i64(-1) && b === i64.MIN_VALUE) { + registers[reg(args.c)] = u64(b); } else { - registers[reg(args.c)] = b / a; + registers[reg(args.c)] = u64(b / a); } return ok(r); }; // REM_U export const rem_u: InstructionRun = (r, args, registers) => { - if (registers[reg(args.a)] === 0) { + if (registers[reg(args.a)] === u64(0)) { registers[reg(args.c)] = registers[reg(args.b)]; } else { registers[reg(args.c)] = registers[reg(args.b)] % registers[reg(args.a)]; @@ -170,12 +179,12 @@ export const rem_u: InstructionRun = (r, args, registers) => { export const rem_s: InstructionRun = (r, args, registers) => { const b = i64(registers[reg(args.b)]); const a = i64(registers[reg(args.a)]); - if (a === 0) { - registers[reg(args.c)] = b; - } else if (a === -1 && b === i64.MIN_VALUE) { - registers[reg(args.c)] = 0; + if (a === i64(0)) { + registers[reg(args.c)] = u64(b); + } else if (a === i64(-1) && b === i64.MIN_VALUE) { + registers[reg(args.c)] = u64(0); } else { - registers[reg(args.c)] = b % a; + registers[reg(args.c)] = u64(b % a); } return ok(r); }; @@ -202,7 +211,7 @@ export const mul_upper_s_u: InstructionRun = (r, args, registers) => { export const max: InstructionRun = (r, args, registers) => { const a = i64(registers[reg(args.a)]); const b = i64(registers[reg(args.b)]); - registers[reg(args.c)] = a < b ? b : a; + registers[reg(args.c)] = u64(a < b ? b : a); return ok(r); }; @@ -218,7 +227,7 @@ export const max_u: InstructionRun = (r, args, registers) => { export const min: InstructionRun = (r, args, registers) => { const a = i64(registers[reg(args.a)]); const b = i64(registers[reg(args.b)]); - registers[reg(args.c)] = a > b ? b : a; + registers[reg(args.c)] = u64(a > b ? b : a); return ok(r); }; diff --git a/assembly/instructions/mov.ts b/assembly/instructions/mov.ts index 170667d..f0cb96f 100644 --- a/assembly/instructions/mov.ts +++ b/assembly/instructions/mov.ts @@ -9,7 +9,7 @@ export const move_reg: InstructionRun = (r, args, registers) => { // CMOV_IZ_IMM export const cmov_iz_imm: InstructionRun = (r, args, registers) => { - if (registers[reg(args.a)] === 0) { + if (registers[reg(args.a)] === u64(0)) { registers[reg(args.b)] = u32SignExtend(args.c); } return ok(r); @@ -17,7 +17,7 @@ export const cmov_iz_imm: InstructionRun = (r, args, registers) => { // CMOV_NZ_IMM export const cmov_nz_imm: InstructionRun = (r, args, registers) => { - if (registers[reg(args.a)] !== 0) { + if (registers[reg(args.a)] !== u64(0)) { registers[reg(args.b)] = u32SignExtend(args.c); } return ok(r); @@ -25,7 +25,7 @@ export const cmov_nz_imm: InstructionRun = (r, args, registers) => { // CMOV_IZ export const cmov_iz: InstructionRun = (r, args, registers) => { - if (registers[reg(args.a)] === 0) { + if (registers[reg(args.a)] === u64(0)) { registers[reg(args.c)] = registers[reg(args.b)]; } return ok(r); @@ -33,7 +33,7 @@ export const cmov_iz: InstructionRun = (r, args, registers) => { // CMOV_NZ export const cmov_nz: InstructionRun = (r, args, registers) => { - if (registers[reg(args.a)] !== 0) { + if (registers[reg(args.a)] !== u64(0)) { registers[reg(args.c)] = registers[reg(args.b)]; } return ok(r); diff --git a/assembly/instructions/rot.ts b/assembly/instructions/rot.ts index 672caa6..2efddc2 100644 --- a/assembly/instructions/rot.ts +++ b/assembly/instructions/rot.ts @@ -1,3 +1,4 @@ +import { portable } from "../portable"; import { InstructionRun, ok } from "./outcome"; import { reg, u32SignExtend } from "./utils"; @@ -52,18 +53,18 @@ export const rot_r_32: InstructionRun = (r, args, regs) => { export namespace math { // @inline export function rot_r(v: u64, shift: u64): u64 { - return rotr(v, shift); + return portable.rotr_u64(v, shift); } // @inline export function rot_r_32(v: u32, shift: u32): u32 { - return rotr(v, shift); + return portable.rotr_u32(v, shift); } // @inline export function rot_l(v: u64, shift: u64): u64 { - return rotl(v, shift); + return portable.rotl_u64(v, shift); } // @inline export function rot_l_32(v: u32, shift: u32): u32 { - return rotl(v, shift); + return portable.rotl_u32(v, shift); } } diff --git a/assembly/instructions/set.ts b/assembly/instructions/set.ts index 7eb8bc3..777ab61 100644 --- a/assembly/instructions/set.ts +++ b/assembly/instructions/set.ts @@ -4,39 +4,39 @@ import { reg, u32SignExtend } from "./utils"; // SET_LT_U_IMM export const set_lt_u_imm: InstructionRun = (r, args, registers) => { const cond = registers[reg(args.a)] < u64(u32SignExtend(args.c)); - registers[reg(args.b)] = cond ? 1 : 0; + registers[reg(args.b)] = cond ? u64(1) : u64(0); return ok(r); }; // SET_LT_S_IMM export const set_lt_s_imm: InstructionRun = (r, args, registers) => { - const cond = i64(registers[reg(args.a)]) < u32SignExtend(args.c); - registers[reg(args.b)] = cond ? 1 : 0; + const cond = i64(registers[reg(args.a)]) < i64(u32SignExtend(args.c)); + registers[reg(args.b)] = cond ? u64(1) : u64(0); return ok(r); }; // SET_GT_U_IMM export const set_gt_u_imm: InstructionRun = (r, args, registers) => { const cond = registers[reg(args.a)] > u64(u32SignExtend(args.c)); - registers[reg(args.b)] = cond ? 1 : 0; + registers[reg(args.b)] = cond ? u64(1) : u64(0); return ok(r); }; // SET_GT_S_IMM export const set_gt_s_imm: InstructionRun = (r, args, registers) => { - const cond = i64(registers[reg(args.a)]) > u32SignExtend(args.c); - registers[reg(args.b)] = cond ? 1 : 0; + const cond = i64(registers[reg(args.a)]) > i64(u32SignExtend(args.c)); + registers[reg(args.b)] = cond ? u64(1) : u64(0); return ok(r); }; // SET_LT_U export const set_lt_u: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = registers[reg(args.b)] < registers[reg(args.a)] ? 1 : 0; + registers[reg(args.c)] = registers[reg(args.b)] < registers[reg(args.a)] ? u64(1) : u64(0); return ok(r); }; // SET_LT_S export const set_lt_s: InstructionRun = (r, args, registers) => { - registers[reg(args.c)] = i64(registers[reg(args.b)]) < i64(registers[reg(args.a)]) ? 1 : 0; + registers[reg(args.c)] = i64(registers[reg(args.b)]) < i64(registers[reg(args.a)]) ? u64(1) : u64(0); return ok(r); }; diff --git a/assembly/instructions/shift.ts b/assembly/instructions/shift.ts index 2e7dc08..43ee1e7 100644 --- a/assembly/instructions/shift.ts +++ b/assembly/instructions/shift.ts @@ -24,43 +24,43 @@ export const shlo_r_imm_32: InstructionRun = (r, args, registers) => { export const shar_r_imm_32: InstructionRun = (r, args, registers) => { const shift = u32(args.c % MAX_SHIFT_32); const value = u32SignExtend(u32(registers[reg(args.a)])); - registers[reg(args.b)] = value >> shift; + registers[reg(args.b)] = u64(i64(value) >> i64(shift)); return ok(r); }; // SHLO_L_IMM_ALT_32 export const shlo_l_imm_alt_32: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_32); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_32)); registers[reg(args.b)] = u32SignExtend(args.c << shift); return ok(r); }; // SHLO_R_IMM_ALT_32 export const shlo_r_imm_alt_32: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_32); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_32)); registers[reg(args.b)] = u32SignExtend(args.c >>> shift); return ok(r); }; // SHAR_R_IMM_ALT_32 export const shar_r_imm_alt_32: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_32); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_32)); const imm = u32SignExtend(args.c); - registers[reg(args.b)] = u32SignExtend(u32(imm >> shift)); + registers[reg(args.b)] = u32SignExtend(u32(i64(imm) >> i64(shift))); return ok(r); }; // SHLO_L_IMM export const shlo_l_imm: InstructionRun = (r, args, registers) => { const shift = u32(args.c % MAX_SHIFT_64); - registers[reg(args.b)] = registers[reg(args.a)] << shift; + registers[reg(args.b)] = u64(registers[reg(args.a)] << u64(shift)); return ok(r); }; // SHLO_R_IMM export const shlo_r_imm: InstructionRun = (r, args, registers) => { const shift = u32(args.c % MAX_SHIFT_64); - registers[reg(args.b)] = registers[reg(args.a)] >>> shift; + registers[reg(args.b)] = registers[reg(args.a)] >> u64(shift); return ok(r); }; @@ -68,35 +68,35 @@ export const shlo_r_imm: InstructionRun = (r, args, registers) => { export const shar_r_imm: InstructionRun = (r, args, registers) => { const shift = u32(args.c % MAX_SHIFT_64); const value = i64(registers[reg(args.a)]); - registers[reg(args.b)] = value >> shift; + registers[reg(args.b)] = u64(value >> i64(shift)); return ok(r); }; // SHLO_L_IMM_ALT export const shlo_l_imm_alt: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_64); - registers[reg(args.b)] = u32SignExtend(args.c) << shift; + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_64)); + registers[reg(args.b)] = u64(u32SignExtend(args.c) << i64(shift)); return ok(r); }; // SHLO_R_IMM_ALT export const shlo_r_imm_alt: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_64); - registers[reg(args.b)] = u32SignExtend(args.c) >>> shift; + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_64)); + registers[reg(args.b)] = u64(u32SignExtend(args.c)) >> u64(shift); return ok(r); }; // SHAR_R_IMM_ALT export const shar_r_imm_alt: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_64); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_64)); const value = u32SignExtend(args.c); - registers[reg(args.b)] = u32SignExtend(u32(value >> shift)); + registers[reg(args.b)] = u32SignExtend(u32(value >> i64(shift))); return ok(r); }; // SHLO_L_32 export const shlo_l_32: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_32); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_32)); const value = u32(registers[reg(args.b)]); registers[reg(args.c)] = u32SignExtend(value << shift); return ok(r); @@ -104,7 +104,7 @@ export const shlo_l_32: InstructionRun = (r, args, registers) => { // SHLO_R_32 export const shlo_r_32: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_32); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_32)); const value = u32(registers[reg(args.b)]); registers[reg(args.c)] = u32SignExtend(value >>> shift); return ok(r); @@ -112,29 +112,29 @@ export const shlo_r_32: InstructionRun = (r, args, registers) => { // SHAR_R_32 export const shar_r_32: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_32); + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_32)); const regValue = u32SignExtend(u32(registers[reg(args.b)])); - registers[reg(args.c)] = u32SignExtend(u32(regValue >> shift)); + registers[reg(args.c)] = u32SignExtend(u32(i64(regValue) >> i64(shift))); return ok(r); }; // SHLO_L export const shlo_l: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_64); - registers[reg(args.c)] = registers[reg(args.b)] << shift; + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_64)); + registers[reg(args.c)] = u64(registers[reg(args.b)] << u64(shift)); return ok(r); }; // SHLO_R export const shlo_r: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_64); - registers[reg(args.c)] = registers[reg(args.b)] >>> shift; + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_64)); + registers[reg(args.c)] = registers[reg(args.b)] >> u64(shift); return ok(r); }; // SHAR_R export const shar_r: InstructionRun = (r, args, registers) => { - const shift = u32(registers[reg(args.a)] % MAX_SHIFT_64); - registers[reg(args.c)] = i64(registers[reg(args.b)]) >> shift; + const shift = u32(registers[reg(args.a)] % u64(MAX_SHIFT_64)); + registers[reg(args.c)] = u64(i64(registers[reg(args.b)]) >> i64(shift)); return ok(r); }; diff --git a/assembly/instructions/store.ts b/assembly/instructions/store.ts index 6de0fcb..1775f96 100644 --- a/assembly/instructions/store.ts +++ b/assembly/instructions/store.ts @@ -1,9 +1,16 @@ import { MaybePageFault } from "../memory"; +import { portable } from "../portable"; +import { Registers } from "../registers"; import { InstructionRun, okOrFault } from "./outcome"; import { reg, u32SignExtend } from "./utils"; const faultRes = new MaybePageFault(); +// Helper function to compute effective address from base register and signed 32-bit offset +function effectiveAddress(registers: Registers, baseReg: u32, offset: u32): u32 { + return u32(portable.u64_add(registers[reg(u64(baseReg))], u32SignExtend(offset))); +} + // STORE_IMM_U8 export const store_imm_u8: InstructionRun = (r, args, _registers, memory) => { const address = args.a; @@ -34,13 +41,13 @@ export const store_imm_u64: InstructionRun = (r, args, _registers, memory) => { // STORE_U8 export const store_u8: InstructionRun = (r, args, registers, memory) => { - memory.setU8(faultRes, args.b, (registers[reg(args.a)] & 0xff)); + memory.setU8(faultRes, args.b, (registers[reg(args.a)] & u64(0xff))); return okOrFault(r, faultRes); }; // STORE_U16 export const store_u16: InstructionRun = (r, args, registers, memory) => { - memory.setU16(faultRes, args.b, (registers[reg(args.a)] & 0xff_ff)); + memory.setU16(faultRes, args.b, (registers[reg(args.a)] & u64(0xff_ff))); return okOrFault(r, faultRes); }; @@ -58,56 +65,56 @@ export const store_u64: InstructionRun = (r, args, registers, memory) => { // STORE_IMM_IND_U8 export const store_imm_ind_u8: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.b)); + const address = effectiveAddress(registers, args.a, args.b); memory.setU8(faultRes, address, (args.c & 0xff)); return okOrFault(r, faultRes); }; // STORE_IMM_IND_U16 export const store_imm_ind_u16: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.b)); + const address = effectiveAddress(registers, args.a, args.b); memory.setU16(faultRes, address, (args.c & 0xff_ff)); return okOrFault(r, faultRes); }; // STORE_IMM_IND_U32 export const store_imm_ind_u32: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.b)); + const address = effectiveAddress(registers, args.a, args.b); memory.setU32(faultRes, address, args.c); return okOrFault(r, faultRes); }; // STORE_IMM_IND_U64 export const store_imm_ind_u64: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.b)); + const address = effectiveAddress(registers, args.a, args.b); memory.setU64(faultRes, address, u32SignExtend(args.c)); return okOrFault(r, faultRes); }; // STORE_IND_U8 export const store_ind_u8: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); - memory.setU8(faultRes, address, (registers[reg(args.b)] & 0xff)); + const address = effectiveAddress(registers, args.a, args.c); + memory.setU8(faultRes, address, (registers[reg(args.b)] & u64(0xff))); return okOrFault(r, faultRes); }; // STORE_IND_U16 export const store_ind_u16: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); - memory.setU16(faultRes, address, (registers[reg(args.b)] & 0xff_ff)); + const address = effectiveAddress(registers, args.a, args.c); + memory.setU16(faultRes, address, (registers[reg(args.b)] & u64(0xff_ff))); return okOrFault(r, faultRes); }; // STORE_IND_U32 export const store_ind_u32: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = effectiveAddress(registers, args.a, args.c); memory.setU32(faultRes, address, u32(registers[reg(args.b)])); return okOrFault(r, faultRes); }; // STORE_IND_U64 export const store_ind_u64: InstructionRun = (r, args, registers, memory) => { - const address = u32(registers[reg(args.a)] + u32SignExtend(args.c)); + const address = effectiveAddress(registers, args.a, args.c); memory.setU64(faultRes, address, registers[reg(args.b)]); return okOrFault(r, faultRes); }; diff --git a/assembly/instructions/utils.ts b/assembly/instructions/utils.ts index 1cf1f62..31c4c95 100644 --- a/assembly/instructions/utils.ts +++ b/assembly/instructions/utils.ts @@ -1,11 +1,12 @@ +import { portable } from "../portable"; import { NO_OF_REGISTERS } from "../registers"; /** * Multiply two unsigned 64-bit numbers and take the upper 64-bits of the result. - * + * * The result of multiplication is a 128-bits number and we are only interested in the part that lands in the upper 64-bits. * For example (for 32-bit case) if we multiply `0xffffffff * 0xffffffff`, we get: - + * | 32-bits | 32-bits | * +------------+------------+ * | upper | lower | @@ -14,18 +15,24 @@ import { NO_OF_REGISTERS } from "../registers"; * So `0xfffffffe` is returned. */ export function mulUpperUnsigned(a: u64, b: u64): u64 { - const aHigh: u64 = a >> 32; - const aLow: u64 = a & 0xffff_ffff; - const bHigh: u64 = b >> 32; - const bLow: u64 = b & 0xffff_ffff; + const aHigh: u64 = a >> u64(32); + const aLow: u64 = a & u64(0xffff_ffff); + const bHigh: u64 = b >> u64(32); + const bLow: u64 = b & u64(0xffff_ffff); - const lowLow = aLow * bLow; - const lowHigh = aLow * bHigh; - const highLow = aHigh * bLow; - const highHigh = aHigh * bHigh; - const carry = (lowLow >> 32) + (lowHigh & 0xffff_ffff) + (highLow & 0xffff_ffff); + const lowLow = portable.u64_mul(aLow, bLow); + const lowHigh = portable.u64_mul(aLow, bHigh); + const highLow = portable.u64_mul(aHigh, bLow); + const highHigh = portable.u64_mul(aHigh, bHigh); + const carry = portable.u64_add( + portable.u64_add(lowLow >> u64(32), lowHigh & u64(0xffff_ffff)), + highLow & u64(0xffff_ffff), + ); - return highHigh + (lowHigh >> 32) + (highLow >> 32) + (carry >> 32); + return portable.u64_add( + portable.u64_add(portable.u64_add(highHigh, lowHigh >> u64(32)), highLow >> u64(32)), + carry >> u64(32), + ); } /** @@ -35,47 +42,48 @@ export function mulUpperSigned(a: i64, b: i64): u64 { let isResultNegative = false; let aAbs = a; let bAbs = b; - if (a < 0) { + if (a < i64(0)) { isResultNegative = !isResultNegative; - aAbs = ~a + 1; + aAbs = portable.u64_add(~a, i64(1)); } - if (b < 0) { + if (b < i64(0)) { isResultNegative = !isResultNegative; - bAbs = ~b + 1; + bAbs = portable.u64_add(~b, i64(1)); } if (isResultNegative) { const upper = mulUpperUnsigned(aAbs, bAbs); - const lower = aAbs * bAbs; - return ~upper + (lower === 0 ? 1 : 0); + const lower = portable.u64_mul(aAbs, bAbs); + return portable.u64_add(~upper, lower === u64(0) ? u64(1) : u64(0)); } return mulUpperUnsigned(aAbs, bAbs); } export function mulUpperSignedUnsigned(a: i64, b: u64): u64 { - if (a < 0) { - const aAbs: u64 = ~a + 1; + if (a < i64(0)) { + const aAbs: u64 = portable.u64_add(~a, u64(1)); const upper = mulUpperUnsigned(aAbs, b); - const lower = aAbs * b; - return ~upper + (lower === 0 ? 1 : 0); + const lower = portable.u64_mul(aAbs, b); + return portable.u64_add(~upper, lower === u64(0) ? u64(1) : u64(0)); } return mulUpperUnsigned(a, b); } // @inline export function u8SignExtend(v: u8): i64 { - return i64(i32(i16(i8(v)))); + // u64 wrap ensures unsigned representation in JS BigInt (no-op in AS) + return u64(i64(i32(i16(i8(v))))); } // @inline export function u16SignExtend(v: u16): i64 { - return i64(i32(i16(v))); + return u64(i64(i32(i16(v)))); } // @inline export function u32SignExtend(v: u32): i64 { - return i64(i32(v)); + return u64(i64(i32(v))); } // @inline diff --git a/assembly/interpreter.ts b/assembly/interpreter.ts index 9f8681c..03683a8 100644 --- a/assembly/interpreter.ts +++ b/assembly/interpreter.ts @@ -6,6 +6,7 @@ import { reg } from "./instructions/utils"; import { RUN } from "./instructions-exe"; import { Memory, MemoryBuilder } from "./memory"; import { PAGE_SIZE, PAGE_SIZE_SHIFT, RESERVED_MEMORY } from "./memory-page"; +import { portable } from "./portable"; import { BasicBlocks, decodeArguments, JumpTable, Program, ProgramCounter } from "./program"; import { Registers } from "./registers"; @@ -57,7 +58,7 @@ export class Interpreter { this.program = program; this.registers = registers; this.memory = memory; - this.gas = gasCounter(0); + this.gas = gasCounter(i64(0)); this.pc = 0; this.status = Status.OK; this.exitCode = 0; @@ -131,9 +132,12 @@ export class Interpreter { // additional gas cost of sbrk if (iData === SBRK && this.useSbrkGas) { - const alloc = u64(u32(this.registers[reg(args.a)])); - const gas = ((alloc + PAGE_SIZE - 1) >> PAGE_SIZE_SHIFT) * 16; - if (this.gas.sub(gas)) { + const alloc = u64(u32(this.registers[reg(u64(args.a))])); + const gas = portable.u64_mul( + portable.u64_sub(portable.u64_add(alloc, u64(PAGE_SIZE)), u64(1)) >> u64(PAGE_SIZE_SHIFT), + u64(16), + ); + if (this.gas.sub(gas)) { this.status = Status.OOG; return false; } @@ -240,13 +244,13 @@ function dJump(r: DjumpResult, jumpTable: JumpTable, address: u32): DjumpResult return r; } - const index = address / JUMP_ALIGMENT_FACTOR - 1; + const index = u32(address / JUMP_ALIGMENT_FACTOR) - 1; if (index >= jumpTable.jumps.length) { r.status = DjumpStatus.PANIC; return r; } - const newPc: u64 = unchecked(jumpTable.jumps[index]); + const newPc: u64 = portable.staticArrayAt(jumpTable.jumps, index); if (newPc >= MAX_U32) { r.status = DjumpStatus.PANIC; return r; @@ -257,4 +261,4 @@ function dJump(r: DjumpResult, jumpTable: JumpTable, address: u32): DjumpResult return r; } -const MAX_U32: u64 = 2 ** 32; +const MAX_U32: u64 = u64(0x1_0000_0000); diff --git a/assembly/memory-page.ts b/assembly/memory-page.ts index ff46d2f..5832565 100644 --- a/assembly/memory-page.ts +++ b/assembly/memory-page.ts @@ -1,3 +1,5 @@ +import { portable } from "./portable"; + export type PageIndex = u32; export type ArenaId = u32; @@ -58,7 +60,7 @@ export class Arena { this.extraPageIndex = pageCount; const data = new ArrayBuffer(this.arenaBytes); for (let i = 0; i < pageCount; i++) { - this.free.unshift(new RawPage(i, Uint8Array.wrap(data, i * PAGE_SIZE, PAGE_SIZE))); + this.free.unshift(new RawPage(i, portable.uint8ArrayView(data, i * PAGE_SIZE, PAGE_SIZE))); } } diff --git a/assembly/memory.ts b/assembly/memory.ts index d9011f2..5e13bb5 100644 --- a/assembly/memory.ts +++ b/assembly/memory.ts @@ -11,6 +11,7 @@ import { RESERVED_MEMORY, RESERVED_PAGES, } from "./memory-page"; +import { portable } from "./portable"; // @unmanaged export class MaybePageFault { @@ -154,11 +155,13 @@ export class Memory { return 0; } // Trigger lazy allocation if the backing buffer has not been created yet. + // @ts-ignore: dataStart is an AS-specific property on Uint8Array return page.raw.data.dataStart; } free(): void { - const pages = this.pages.values(); + // @ts-ignore: AS returns T[], JS returns iterator - asArray handles both + const pages: Page[] = portable.asArray(this.pages.values()); for (let i = 0; i < pages.length; i++) { this.arena.release(pages[i].raw); } @@ -166,20 +169,20 @@ export class Memory { } sbrk(faultRes: MaybePageFault, amount: u32): u64 { - const freeMemoryStart = this.sbrkAddress; + const freeMemoryStart = u64(this.sbrkAddress); if (amount === 0) { faultRes.isFault = false; return freeMemoryStart; } - const newSbrk = u64(this.sbrkAddress) + amount; + const newSbrk = portable.u64_add(freeMemoryStart, u64(amount)); if (newSbrk > this.maxHeapPointer) { faultRes.isFault = true; return freeMemoryStart; } this.sbrkAddress = u32(newSbrk); - const pageIdx = i32((newSbrk - 1) >> PAGE_SIZE_SHIFT); + const pageIdx = i32(portable.u64_sub(newSbrk, u64(1)) >> u64(PAGE_SIZE_SHIFT)); if (pageIdx === this.lastAllocatedPage) { return freeMemoryStart; } @@ -194,19 +197,19 @@ export class Memory { } getU8(faultRes: MaybePageFault, address: u32): u64 { - return u8(this.getBytesReversed(faultRes, Access.Read, address, 1)); + return u64(u8(this.getBytesReversed(faultRes, Access.Read, address, 1))); } getU16(faultRes: MaybePageFault, address: u32): u64 { - return bswap(u16(this.getBytesReversed(faultRes, Access.Read, address, 2))); + return u64(portable.bswap_u16(u16(this.getBytesReversed(faultRes, Access.Read, address, 2)))); } getU32(faultRes: MaybePageFault, address: u32): u64 { - return bswap(u32(this.getBytesReversed(faultRes, Access.Read, address, 4))); + return u64(portable.bswap_u32(u32(this.getBytesReversed(faultRes, Access.Read, address, 4)))); } getU64(faultRes: MaybePageFault, address: u32): u64 { - return bswap(this.getBytesReversed(faultRes, Access.Read, address, 8)); + return portable.bswap_u64(this.getBytesReversed(faultRes, Access.Read, address, 8)); } getI8(faultRes: MaybePageFault, address: u32): u64 { @@ -246,7 +249,7 @@ export class Memory { // first traverse memory and see if we don't page fault if (length > 0) { let nextAddress = address; - const pagesToCheck = i32((u64(length) + u64(PAGE_SIZE - 1)) >> PAGE_SIZE_SHIFT); + const pagesToCheck = i32(portable.u64_add(u64(length), u64(PAGE_SIZE - 1)) >> u64(PAGE_SIZE_SHIFT)); for (let page = 0; page < pagesToCheck; page++) { const pageData = this.pageResult; this.getPage(fault, pageData, Access.Read, nextAddress); @@ -411,38 +414,38 @@ export class Memory { return; } - let bytesLeft = value; + let bytesLeft = u64(value); // write to first page const firstPageEnd = minU32(PAGE_SIZE, r.firstPageOffset + bytes); for (let i: u32 = r.firstPageOffset; i < firstPageEnd; i++) { r.firstPageData[i] = u8(bytesLeft); - bytesLeft >>= 8; + bytesLeft >>= u64(8); } // write rest to the second page for (let i: u32 = 0; i < r.secondPageEnd; i++) { r.secondPageData[i] = u8(bytesLeft); - bytesLeft >>= 8; + bytesLeft >>= u64(8); } } private getBytesReversed(faultRes: MaybePageFault, access: Access, address: u32, bytes: u8): u64 { this.getChunks(faultRes, this.chunksResult, access, address, bytes); if (faultRes.isFault) { - return 0; + return u64(0); } // result (bytes in reverse order) - let r: u64 = 0; + let r: u64 = u64(0); const firstPageEnd = minU32(PAGE_SIZE, this.chunksResult.firstPageOffset + bytes); // read from first page for (let i: u32 = this.chunksResult.firstPageOffset; i < firstPageEnd; i++) { - r = (r << 8) | this.chunksResult.firstPageData[i]; + r = (r << u64(8)) | u64(this.chunksResult.firstPageData[i]); } // read from the second page for (let i: u32 = 0; i < this.chunksResult.secondPageEnd; i++) { - r = (r << 8) | this.chunksResult.secondPageData[i]; + r = (r << u64(8)) | u64(this.chunksResult.secondPageData[i]); } return r; diff --git a/assembly/portable.ts b/assembly/portable.ts new file mode 100644 index 0000000..00d09de --- /dev/null +++ b/assembly/portable.ts @@ -0,0 +1,354 @@ +// Runtime binding for TS decorator emit in portable JS builds. +const inline = (_target: i32 = 0, _propertyKey: i32 = 0, descriptor: i32 = 0): i32 => descriptor; + +export class portable { + // @ts-ignore: @inline is an AS-only decorator + @inline + static installPolyfills(): void { + if (ASC_TARGET !== 0) { + return; + } + + // @ts-ignore: JS runtime only + const g = globalThis as any; + if (g.__ananPortablePolyfillsInstalled) { + return; + } + g.__ananPortablePolyfillsInstalled = true; + + // StaticArray -> Array with AS-like zero-initialized length constructor. + g.StaticArray = function StaticArray(length: number): any { + const arr = new Array(length); + arr.fill(0); + return arr; + }; + + g.ASC_TARGET = 0; + + g.i8 = (value: any): any => { + const n = typeof value === "bigint" ? Number(value) : value; + return (n << 24) >> 24; + }; + + g.i16 = (value: any): any => { + const n = typeof value === "bigint" ? Number(value) : value; + return (n << 16) >> 16; + }; + + const i32Fn = (value: any): any => { + if (typeof value === "bigint") { + return Number(BigInt.asIntN(32, value)); + } + return value | 0; + }; + i32Fn.MIN_VALUE = -2147483648; + i32Fn.MAX_VALUE = 2147483647; + g.i32 = i32Fn; + + const i64Fn = (value: any): any => { + return BigInt.asIntN(64, BigInt(value)); + }; + i64Fn.MAX_VALUE = BigInt("9223372036854775807"); + i64Fn.MIN_VALUE = BigInt("-9223372036854775808"); + g.i64 = i64Fn; + + g.u8 = (v: any): any => { + if (typeof v === "bigint") { + return Number(v & BigInt(0xff)) >>> 0; + } + return (v & 0xff) >>> 0; + }; + + g.u16 = (v: any): any => { + if (typeof v === "bigint") { + return Number(v & BigInt(0xffff)) >>> 0; + } + return (v & 0xffff) >>> 0; + }; + + g.u32 = (v: any): any => { + if (typeof v === "bigint") { + return Number(v & BigInt(0xffff_ffff)) >>> 0; + } + return (v & 0xffff_ffff) >>> 0; + }; + + const u64Fn = (value: any): any => { + return BigInt.asUintN(64, BigInt(value)); + }; + u64Fn.MAX_VALUE = BigInt("18446744073709551615"); + u64Fn.MIN_VALUE = BigInt(0); + g.u64 = u64Fn; + + g.f32 = (v: any): any => Math.fround(v); + g.f64 = (v: any): any => +v; + g.bool = (v: any): any => !!v; + + const dataViewProto = DataView.prototype as any; + if (!dataViewProto.setUint64) { + dataViewProto.setUint64 = function (byteOffset: number, value: bigint, littleEndian?: boolean): void { + const high = Number((value >> BigInt(32)) & BigInt(0xffff_ffff)); + const low = Number(value & BigInt(0xffff_ffff)); + if (littleEndian) { + this.setUint32(byteOffset, low, true); + this.setUint32(byteOffset + 4, high, true); + } else { + this.setUint32(byteOffset, high, false); + this.setUint32(byteOffset + 4, low, false); + } + }; + } + + if (!dataViewProto.getUint64) { + dataViewProto.getUint64 = function (byteOffset: number, littleEndian?: boolean): bigint { + if (littleEndian) { + const low = BigInt(this.getUint32(byteOffset, true)); + const high = BigInt(this.getUint32(byteOffset + 4, true)); + return (high << BigInt(32)) | low; + } + const high = BigInt(this.getUint32(byteOffset, false)); + const low = BigInt(this.getUint32(byteOffset + 4, false)); + return (high << BigInt(32)) | low; + }; + } + + g.unchecked = (v: any): any => v; + g.inline = inline; + g.changetype = (v: any): any => v; + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + // @ts-ignore: parameter type differs between AS and JS + static asArray(v: T[]): T[] { + if (ASC_TARGET === 0) { + // @ts-ignore: JS runtime - v is an iterator, convert to array + return Array.from(v) as T[]; + } + return v; + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static arrayAt(v: T[], i: u32): T { + if (ASC_TARGET === 0) { + return v[i]; + } + return unchecked(v[i]); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static staticArrayAt(v: StaticArray, i: u32): T { + if (ASC_TARGET === 0) { + return v[i]; + } + return unchecked(v[i]); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static asU32(v: u32): u32 { + if (ASC_TARGET === 0) { + return v >>> 0; + } + return v; + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static uint8ArrayView(data: ArrayBuffer, offset: i32, length: i32): Uint8Array { + if (ASC_TARGET === 0) { + // @ts-ignore: JS runtime supports Uint8Array(buffer, offset, length) + return new Uint8Array(data, offset, length); + } + // @ts-ignore: Uint8Array.wrap is an AS-only API + return Uint8Array.wrap(data, offset, length); + } + + // --- bswap --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static bswap_u16(v: u16): u16 { + if (ASC_TARGET === 0) { + return u16(((v & 0xff) << 8) | ((v >> 8) & 0xff)); + } + return bswap(v); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static bswap_u32(v: u32): u32 { + if (ASC_TARGET === 0) { + return u32(((v & 0xff) << 24) | ((v & 0xff00) << 8) | ((v >> 8) & 0xff00) | ((v >> 24) & 0xff)); + } + return bswap(v); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static bswap_u64(v: u64): u64 { + if (ASC_TARGET === 0) { + const lo = u32(v); + const hi = u32(v >> u64(32)); + const sLo = portable.bswap_u32(lo); + const sHi = portable.bswap_u32(hi); + return u64((u64(sLo) << u64(32)) | u64(sHi)); + } + return bswap(v); + } + + // --- popcnt --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static popcnt_u32(v: u32): u32 { + if (ASC_TARGET === 0) { + v = v - ((v >>> 1) & 0x55555555); + v = (v & 0x33333333) + ((v >>> 2) & 0x33333333); + return (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24; + } + return popcnt(v); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static popcnt_u64(v: u64): u64 { + if (ASC_TARGET === 0) { + const lo = portable.popcnt_u32(u32(v)); + const hi = portable.popcnt_u32(u32(v >> u64(32))); + return u64(lo + hi); + } + return popcnt(v); + } + + // --- clz --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static clz_u32(v: u32): u32 { + if (ASC_TARGET === 0) { + // @ts-ignore: Math.clz32 exists in ES2015+ + return Math.clz32(v); + } + return clz(v); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static clz_u64(v: u64): u64 { + if (ASC_TARGET === 0) { + const hi = u32(v >> u64(32)); + if (hi !== 0) { + return u64(portable.clz_u32(hi)); + } + return u64(32 + portable.clz_u32(u32(v))); + } + return clz(v); + } + + // --- ctz --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static ctz_u32(v: u32): u32 { + if (ASC_TARGET === 0) { + if (v === 0) return 32; + // @ts-ignore: Math.clz32 exists in ES2015+ + return (31 - Math.clz32(v & -v)); + } + return ctz(v); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static ctz_u64(v: u64): u64 { + if (ASC_TARGET === 0) { + const lo = u32(v); + if (lo !== 0) { + return u64(portable.ctz_u32(lo)); + } + return u64(32 + portable.ctz_u32(u32(v >> u64(32)))); + } + return ctz(v); + } + + // --- rotr --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static rotr_u32(v: u32, shift: u32): u32 { + if (ASC_TARGET === 0) { + shift &= 31; + return u32((v >>> shift) | (v << (32 - shift))); + } + return rotr(v, shift); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static rotr_u64(v: u64, shift: u64): u64 { + if (ASC_TARGET === 0) { + shift &= u64(63); + return u64((v >> shift) | (v << (u64(64) - shift))); + } + return rotr(v, shift); + } + + // --- rotl --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static rotl_u32(v: u32, shift: u32): u32 { + if (ASC_TARGET === 0) { + shift &= 31; + return u32((v << shift) | (v >>> (32 - shift))); + } + return rotl(v, shift); + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static rotl_u64(v: u64, shift: u64): u64 { + if (ASC_TARGET === 0) { + shift &= u64(63); + return u64((v << shift) | (v >> (u64(64) - shift))); + } + return rotl(v, shift); + } + + // --- u64 wrapping arithmetic --- + + // @ts-ignore: @inline is an AS-only decorator + @inline + static u64_add(a: u64, b: u64): u64 { + if (ASC_TARGET === 0) { + // @ts-ignore: BigInt + return BigInt.asUintN(64, BigInt(a) + BigInt(b)); + } + return a + b; + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static u64_sub(a: u64, b: u64): u64 { + if (ASC_TARGET === 0) { + // @ts-ignore: BigInt + return BigInt.asUintN(64, BigInt(a) - BigInt(b)); + } + return a - b; + } + + // @ts-ignore: @inline is an AS-only decorator + @inline + static u64_mul(a: u64, b: u64): u64 { + if (ASC_TARGET === 0) { + // @ts-ignore: BigInt + return BigInt.asUintN(64, BigInt(a) * BigInt(b)); + } + return a * b; + } +} + +portable.installPolyfills(); diff --git a/assembly/program.ts b/assembly/program.ts index dac4611..f6a1e94 100644 --- a/assembly/program.ts +++ b/assembly/program.ts @@ -2,6 +2,7 @@ import { Args, Arguments, DECODERS, REQUIRED_BYTES } from "./arguments"; import { Decoder } from "./codec"; import { INSTRUCTIONS, MISSING_INSTRUCTION } from "./instructions"; import { reg, u32SignExtend } from "./instructions/utils"; +import { portable } from "./portable"; import { Registers } from "./registers"; export type ProgramCounter = u32; @@ -52,13 +53,13 @@ export function deblob(program: Uint8Array): Program { // the length of the code (in bytes). const codeLength = decoder.varU32(); - const jumpTableLengthInBytes = jumpTableLength * jumpTableItemLength; + const jumpTableLengthInBytes = i32(jumpTableLength * jumpTableItemLength); const rawJumpTable = decoder.bytes(jumpTableLengthInBytes); // NOTE [ToDr] we copy the code here, because indexing a raw // assembly script array is faster than going through `Uint8Array` API. const rawCode = lowerBytes(decoder.bytes(codeLength)); - const rawMask = decoder.bytes((codeLength + 7) / 8); + const rawMask = decoder.bytes(i32((codeLength + 7) / 8)); const mask = new Mask(rawMask, codeLength); const jumpTable = new JumpTable(jumpTableItemLength, rawJumpTable); @@ -98,11 +99,11 @@ export class Mask { } isInstruction(index: ProgramCounter): boolean { - if (index >= this.bytesToSkip.length) { + if (index >= u32(this.bytesToSkip.length)) { return false; } - return unchecked(this.bytesToSkip[u32(index)]) === 0; + return portable.staticArrayAt(this.bytesToSkip, u32(index)) === 0; } /** @@ -115,7 +116,7 @@ export class Mask { */ skipBytesToNextInstruction(i: u32): u32 { if (i + 1 < this.bytesToSkip.length) { - return unchecked(this.bytesToSkip[i + 1]); + return portable.staticArrayAt(this.bytesToSkip, i + 1); } return 0; @@ -175,7 +176,7 @@ export class BasicBlocks { isStart(newPc: u32): boolean { if (newPc < this.isStartOrEnd.length) { - return (unchecked(this.isStartOrEnd[newPc]) & BasicBlock.START) > 0; + return (portable.staticArrayAt(this.isStartOrEnd, newPc) & BasicBlock.START) > 0; } return false; } @@ -200,19 +201,19 @@ export class JumpTable { readonly jumps: StaticArray; constructor(itemBytes: u8, data: Uint8Array) { - const jumps = new StaticArray(itemBytes > 0 ? data.length / itemBytes : 0); + const jumps = new StaticArray(itemBytes > 0 ? i32(data.length / itemBytes) : 0); for (let i = 0; i < data.length; i += itemBytes) { - let num: u64 = 0; + let num: u64 = u64(0); for (let j: i32 = itemBytes - 1; j >= 0; j--) { - let nextNum: u64 = num << 8; + let nextNum: u64 = num << u64(8); let isOverflow = nextNum < num; - nextNum = nextNum + u64(data[i + j]); + nextNum = portable.u64_add(nextNum, u64(data[i + j])); isOverflow = isOverflow || nextNum < num; // handle overflow num = isOverflow ? u64.MAX_VALUE : nextNum; } - jumps[i / itemBytes] = num; + jumps[i32(i / itemBytes)] = num; } this.jumps = jumps; @@ -253,10 +254,10 @@ export function decodeArguments(args: Args, kind: Arguments, code: u8[], offset: } class ResolvedArguments { - a: i64 = 0; - b: i64 = 0; - c: i64 = 0; - d: i64 = 0; + a: i64 = i64(0); + b: i64 = i64(0); + c: i64 = i64(0); + d: i64 = i64(0); decoded: Args = new Args(); } @@ -290,47 +291,47 @@ export function resolveArguments( resolved.a = u32SignExtend(args.a); return resolved; case Arguments.OneRegOneImm: - resolved.a = registers[reg(args.a)]; + resolved.a = registers[reg(u64(args.a))]; resolved.b = u32SignExtend(args.b); return resolved; case Arguments.OneRegOneExtImm: - resolved.a = registers[reg(args.a)]; - resolved.b = (u64(args.a) << 32) + u64(args.b); + resolved.a = registers[reg(u64(args.a))]; + resolved.b = portable.u64_add(u64(args.a) << u64(32), u64(args.b)); return resolved; case Arguments.OneRegTwoImm: - resolved.a = registers[reg(args.a)]; + resolved.a = registers[reg(u64(args.a))]; resolved.b = u32SignExtend(args.b); resolved.c = u32SignExtend(args.c); return resolved; case Arguments.OneRegOneImmOneOff: - resolved.a = registers[reg(args.a)]; + resolved.a = registers[reg(u64(args.a))]; resolved.b = u32SignExtend(args.b); resolved.c = u32SignExtend(args.c); return resolved; case Arguments.TwoReg: - resolved.a = registers[reg(args.a)]; - resolved.b = registers[reg(args.b)]; + resolved.a = registers[reg(u64(args.a))]; + resolved.b = registers[reg(u64(args.b))]; return resolved; case Arguments.TwoRegOneImm: - resolved.a = registers[reg(args.a)]; - resolved.b = registers[reg(args.b)]; + resolved.a = registers[reg(u64(args.a))]; + resolved.b = registers[reg(u64(args.b))]; resolved.c = u32SignExtend(args.c); return resolved; case Arguments.TwoRegOneOff: - resolved.a = registers[reg(args.a)]; - resolved.b = registers[reg(args.b)]; + resolved.a = registers[reg(u64(args.a))]; + resolved.b = registers[reg(u64(args.b))]; resolved.c = u32SignExtend(args.c); return resolved; case Arguments.TwoRegTwoImm: - resolved.a = registers[reg(args.a)]; - resolved.b = registers[reg(args.b)]; + resolved.a = registers[reg(u64(args.a))]; + resolved.b = registers[reg(u64(args.b))]; resolved.c = u32SignExtend(args.c); resolved.d = u32SignExtend(args.d); return resolved; case Arguments.ThreeReg: - resolved.a = registers[reg(args.a)]; - resolved.b = registers[reg(args.b)]; - resolved.c = registers[reg(args.c)]; + resolved.a = registers[reg(u64(args.a))]; + resolved.b = registers[reg(u64(args.b))]; + resolved.c = registers[reg(u64(args.c))]; return resolved; default: throw new Error(`Unhandled arguments kind: ${kind}`); diff --git a/assembly/registers.ts b/assembly/registers.ts index 2c33e22..16f5483 100644 --- a/assembly/registers.ts +++ b/assembly/registers.ts @@ -6,5 +6,9 @@ export const REG_SIZE_BYTES = 8; export type Registers = StaticArray; export function newRegisters(): Registers { - return new StaticArray(NO_OF_REGISTERS); + const regs = new StaticArray(NO_OF_REGISTERS); + for (let i = 0; i < NO_OF_REGISTERS; i++) { + regs[i] = u64(0); + } + return regs; } diff --git a/assembly/spi.ts b/assembly/spi.ts index b2fd986..955a5cc 100644 --- a/assembly/spi.ts +++ b/assembly/spi.ts @@ -2,7 +2,7 @@ import { Decoder } from "./codec"; import { Memory, MemoryBuilder } from "./memory"; import { Access, PAGE_SIZE, PAGE_SIZE_SHIFT, SEGMENT_SIZE, SEGMENT_SIZE_SHIFT } from "./memory-page"; import { deblob, Program } from "./program"; -import { NO_OF_REGISTERS, Registers } from "./registers"; +import { newRegisters, Registers } from "./registers"; /** `Z_I`: https://graypaper.fluffylabs.dev/#/ab2cdbd/2daf002daf00?v=0.7.2 */ export const MAX_ARGS_LEN: u32 = 2 ** 24; @@ -67,7 +67,7 @@ export function decodeSpi(data: Uint8Array, args: Uint8Array, preallocateMemoryP const memory = builder.build(heapZerosStart + heapZerosLength, stackStart); // build registers - const registers: Registers = new StaticArray(NO_OF_REGISTERS); + const registers: Registers = newRegisters(); registers[0] = 0xffff_0000; registers[1] = STACK_SEGMENT_END; registers[7] = ARGS_SEGMENT_START; diff --git a/assembly/test.ts b/assembly/test.ts index 93797e5..0a5cbbe 100644 --- a/assembly/test.ts +++ b/assembly/test.ts @@ -28,8 +28,8 @@ export class Assert { isEqual(actual: T, expected: T, msg: string = ""): void { if (actual !== expected) { this.isOkay = false; - const actualDisplay = isInteger(actual) ? `${actual} (0x${actual.toString(16)})` : `${actual}`; - const expectDisplay = isInteger(expected) ? `${expected} (0x${expected.toString(16)})` : `${expected}`; + const actualDisplay = isInteger(actual) ? `${actual} (0x${i64(actual).toString(16)})` : `${actual}`; + const expectDisplay = isInteger(expected) ? `${expected} (0x${i64(expected).toString(16)})` : `${expected}`; this.errors.push(`Got: '${actualDisplay}', expected: '${expectDisplay}' @ ${msg}`); } } @@ -37,7 +37,7 @@ export class Assert { isNotEqual(actual: T, unexpected: T, msg: string = ""): void { if (actual === unexpected) { this.isOkay = false; - const actualDisplay = isInteger(actual) ? `${actual} (0x${actual.toString(16)})` : `${actual}`; + const actualDisplay = isInteger(actual) ? `${actual} (0x${i64(actual).toString(16)})` : `${actual}`; this.errors.push(`Expected value to differ from: '${actualDisplay}' @ ${msg}`); } } diff --git a/bin/src/trace-replay.ts b/bin/src/trace-replay.ts index a097be5..4150209 100644 --- a/bin/src/trace-replay.ts +++ b/bin/src/trace-replay.ts @@ -46,7 +46,7 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace const spiArgs = Array.from(extractSpiArgs(start, initialMemWrites)); const preparedProgram = useSpi - ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, 0) + ? prepareProgram(InputKind.SPI, hasMetadata, programInput, [], [], [], spiArgs, 128) : prepareProgram( InputKind.Generic, hasMetadata, @@ -55,7 +55,7 @@ export function replayTraceFile(filePath: string, options: ReplayOptions): Trace buildInitialPages(initialMemWrites), buildInitialChunks(initialMemWrites), [], - 0, + 128, ); const id = pvmStart(preparedProgram, true); diff --git a/biome.jsonc b/biome.jsonc index 12f8192..dd97fce 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.3/schema.json", "formatter": { "enabled": true, "formatWithErrors": false, @@ -52,6 +52,9 @@ "rules": { "correctness": { "noPrecisionLoss": "off" + }, + "suspicious": { + "noTsIgnore": "off" } } } diff --git a/package.json b/package.json index 7de8a52..b8f7ee5 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,11 @@ "qa": "biome ci", "qa-fix": "npm run format; npm run lint", "start": "tsx ./bin/index.ts", - "test": "npm run asbuild:test && tsx ./test/test-as.ts && tsx ./test/test-trace-replay.ts", - "tsbuild": "tsc", + "test": "npm run asbuild:test && tsx ./test/test-as.ts && tsx ./test/test-trace-replay.ts", + "tsbuild": "tsc && npm run tsbuild:portable", + "tsbuild:portable": "tsc --project portable/tsconfig.json && esbuild dist/build/js/portable/index.js --bundle --format=esm --platform=node --outfile=dist/build/js/portable-bundle.js", "test:w3f": "tsx ./test/test-w3f.ts", + "test:w3f-portable": "tsx ./test/test-w3f-portable.ts", "test:gas-cost": "tsx ./test/test-gas-cost.ts", "update-version": "tsx ./web/bump-version.ts $GITHUB_SHA", "web": "npx live-server ./web" @@ -47,10 +49,11 @@ "node": ">=18.3.0" }, "devDependencies": { - "@biomejs/biome": "^2.3.11", + "@biomejs/biome": "^2.4.3", "@typeberry/lib": "^0.5.1", "@types/node": "^25.0.0", "assemblyscript": "^0.28.8", + "esbuild": "^0.27.0", "json-bigint-patch": "^0.0.8", "tsx": "^4.20.6", "typescript": "^5.9.3" @@ -111,6 +114,10 @@ "./release.wasm": "./dist/build/release.wasm", "./release-mini.wasm": "./dist/build/release-mini.wasm", "./release-stub.wasm": "./dist/build/release-stub.wasm", - "./compiler.wasm": "./dist/build/compiler.wasm" + "./compiler.wasm": "./dist/build/compiler.wasm", + "./js": { + "import": "./dist/build/js/portable-bundle.js", + "types": "./dist/build/js/portable/index.d.ts" + } } } diff --git a/portable/bootstrap.ts b/portable/bootstrap.ts new file mode 100644 index 0000000..d0ff825 --- /dev/null +++ b/portable/bootstrap.ts @@ -0,0 +1,5 @@ +// Portable bootstrap for globals that must exist before assembly/portable executes. +const g = globalThis as Record; +if (g.ASC_TARGET === undefined) { + g.ASC_TARGET = 0; +} diff --git a/portable/index.ts b/portable/index.ts new file mode 100644 index 0000000..b1df23e --- /dev/null +++ b/portable/index.ts @@ -0,0 +1,7 @@ +// Portable JS entry point for anan-as +// Imports portable runtime glue before the AS portable runtime. +import "./bootstrap"; +import "../assembly/portable"; +import "assemblyscript/std/portable/index.js"; + +export * from "../assembly/index-shared"; diff --git a/portable/tsconfig.json b/portable/tsconfig.json new file mode 100644 index 0000000..4c5d0d7 --- /dev/null +++ b/portable/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["./index.ts", "./types/**/*.d.ts"], + "compilerOptions": { + "experimentalDecorators": true, + "module": "ESNext", + "target": "ES2020", + "moduleResolution": "bundler", + "outDir": "../dist/build/js", + "declaration": true, + "lib": ["ES2020", "DOM"], + "skipLibCheck": true, + "strict": false, + "strictNullChecks": false, + "noImplicitAny": false, + "noImplicitThis": false, + "noUnusedLocals": false, + "noUnusedParameters": false + } +} diff --git a/portable/types/assemblyscript.d.ts b/portable/types/assemblyscript.d.ts new file mode 100644 index 0000000..daba955 --- /dev/null +++ b/portable/types/assemblyscript.d.ts @@ -0,0 +1,89 @@ +// Custom AssemblyScript type declarations for the portable (JS) build. +// Uses permissive types to allow number/bigint mixing that AS allows natively. + +type ASNumber = number | bigint; +type ASPrimitive = ASNumber | boolean; + +// AS numeric types in portable runtime. +type i8 = number; +type i16 = number; +type i32 = number; +type i64 = number; +type u8 = number; +type u16 = number; +type u32 = number; +type u64 = number; +type isize = number; +type usize = number; +type f32 = number; +type f64 = number; +type bool = boolean; + +// AS built-in functions (provided by assemblyscript/std/portable at runtime) +declare function bswap(value: T): T; +declare function clz(value: T): T; +declare function ctz(value: T): T; +declare function popcnt(value: T): T; +declare function rotl(value: T, shift: T): T; +declare function rotr(value: T, shift: T): T; +declare function abs(value: T): T; +declare function max(left: T, right: T): T; +declare function min(left: T, right: T): T; +declare function select(ifTrue: T, ifFalse: T, condition: bool): T; +declare function unreachable(): never; +declare function changetype(value: unknown): T; +declare function unchecked(value: T): T; +declare function assert(isTrueish: T, message?: string): T; +declare const inline: MethodDecorator; + +// AS target constant +declare const ASC_TARGET: i32; + +// Type cast functions with static constants +declare function i8(value: ASPrimitive): i8; +declare function i16(value: ASPrimitive): i16; + +interface I32Cast { + (value: ASPrimitive): i32; + MIN_VALUE: i32; + MAX_VALUE: i32; +} +declare var i32: I32Cast; + +interface I64Cast { + (value: ASPrimitive): i64; + MIN_VALUE: i64; + MAX_VALUE: i64; +} +declare var i64: I64Cast; + +declare function u8(value: ASPrimitive): u8; +declare function u16(value: ASPrimitive): u16; +declare function u32(value: ASPrimitive): u32; + +interface U64Cast { + (value: ASPrimitive): u64; + MAX_VALUE: u64; + MIN_VALUE: u64; +} +declare var u64: U64Cast; + +declare function f32(value: ASPrimitive): f32; +declare function f64(value: ASPrimitive): f64; +declare function bool(value: ASPrimitive): bool; + +// StaticArray - alias for Array in JS +interface StaticArray extends Array {} +interface StaticArrayConstructor { + new (length: number): StaticArray; +} +declare const StaticArray: StaticArrayConstructor; + +// DataView extensions +interface DataView { + setUint64(byteOffset: number, value: u64, littleEndian?: boolean): void; + getUint64(byteOffset: number, littleEndian?: boolean): u64; +} + +// MapIterator - in AS this returns an array, in JS it's an iterator +type MapIterator = IterableIterator | T[]; diff --git a/test/test-w3f-common.ts b/test/test-w3f-common.ts new file mode 100644 index 0000000..08476f9 --- /dev/null +++ b/test/test-w3f-common.ts @@ -0,0 +1,207 @@ +import "json-bigint-patch"; +import * as assert from "node:assert"; +import { ERR, OK, read, run, TestOptions } from "../bin/src/test-json.js"; + +/** The subset of PVM exports needed by the test runner. */ +export interface PvmModule { + // biome-ignore lint/suspicious/noExplicitAny: both WASM and portable builds have different concrete types + disassemble: (program: any, kind: any, hasMetadata: any) => string; + // biome-ignore lint/suspicious/noExplicitAny: both WASM and portable builds have different concrete types + HasMetadata: { No: any }; + // biome-ignore lint/suspicious/noExplicitAny: both WASM and portable builds have different concrete types + InputKind: { Generic: any }; + // biome-ignore lint/suspicious/noExplicitAny: both WASM and portable builds have different concrete types + prepareProgram: (...args: any[]) => any; + // biome-ignore lint/suspicious/noExplicitAny: both WASM and portable builds have different concrete types + runProgram: (...args: any[]) => PvmResult; +} + +interface PvmResult { + status: number; + exitCode: number; + pc: number; + gas: bigint | number; + result: number[]; + registers: (bigint | number)[]; + memory: Chunk[]; +} + +type PvmTest = { + name: string; + "initial-regs": (bigint | number)[]; + "initial-pc": number; + "initial-page-map": Page[]; + "initial-memory": Chunk[]; + "initial-gas": bigint | number; + program: number[]; + "expected-regs": (bigint | number)[]; + "expected-pc": number; + "expected-gas": bigint | number; + "expected-status": string; + "expected-page-fault-address"?: number; + "expected-memory": Chunk[]; +}; + +const ignored = [ + "inst_load_u8_nok", + "inst_store_imm_indirect_u16_with_offset_nok", + "inst_store_imm_indirect_u32_with_offset_nok", + "inst_store_imm_indirect_u64_with_offset_nok", + "inst_store_imm_indirect_u8_with_offset_nok", + "inst_store_imm_u8_trap_inaccessible", + "inst_store_imm_u8_trap_read_only", + "inst_store_indirect_u16_with_offset_nok", + "inst_store_indirect_u32_with_offset_nok", + "inst_store_indirect_u64_with_offset_nok", + "inst_store_indirect_u8_with_offset_nok", + "inst_store_u8_trap_inaccessible", + "inst_store_u8_trap_read_only", +]; + +export function runW3fTests(pvm: PvmModule) { + const options: TestOptions = { + isDebug: false, + isSilent: false, + useSbrkGas: false, + }; + + run((data, opts, filePath) => processW3f(pvm, data, opts, filePath), options); +} + +function processW3f(pvm: PvmModule, data: PvmTest, options: TestOptions, _filePath?: string) { + if (options.isDebug) { + console.debug(`🤖 Running ${data.name}`); + } + + if (ignored.includes(data.name)) { + if (options.isDebug) { + console.info(`⏭️ Skipping ${data.name}`); + } + return data; + } + // input + const input = { + registers: read(data, "initial-regs").map((x: number | bigint) => BigInt(x)), + pc: read(data, "initial-pc"), + pageMap: asPageMap(read(data, "initial-page-map")), + memory: asChunks(read(data, "initial-memory")), + gas: BigInt(read(data, "initial-gas")), + program: read(data, "program"), + }; + + if (options.isDebug) { + const assembly = pvm.disassemble(input.program, pvm.InputKind.Generic, pvm.HasMetadata.No); + console.info("==========="); + console.info(assembly); + console.info("\n^^^^^^^^^^^\n"); + } + + const exe = pvm.prepareProgram( + pvm.InputKind.Generic, + pvm.HasMetadata.No, + input.program, + input.registers, + input.pageMap, + input.memory, + [], + 16, + ); + const result = pvm.runProgram(exe, input.gas, input.pc, options.isDebug, options.useSbrkGas, true); + const status = statusAsString(result.status); + + // Normalize registers to plain unsigned BigInt array for comparison. + // The portable build may store signed BigInts or plain numbers in registers. + result.registers = Array.from(result.registers).map((v: bigint | number) => + typeof v === "bigint" ? BigInt.asUintN(64, v) : BigInt(v), + ); + result.gas = typeof result.gas === "bigint" ? result.gas : BigInt(result.gas); + + // Normalize memory chunks to plain objects for comparison. + // The portable build returns class instances; WASM returns plain objects. + result.memory = result.memory.map((chunk: Chunk) => ({ + address: chunk.address, + data: Array.from(chunk.data), + })); + + // silent mode - just put our vals into expected (comparison done externally) + if (options.isSilent) { + data["expected-pc"] = result.pc; + data["expected-gas"] = result.gas; + data["expected-status"] = status; + data["expected-regs"] = result.registers; + data["expected-page-fault-address"] = result.exitCode; + + return data; + } + + // compare with expected values + const expected = { + // just copy JAM-output result field + result: result.result, + status: read(data, "expected-status"), + registers: read(data, "expected-regs").map((x: number | bigint) => BigInt(x)), + pc: read(data, "expected-pc"), + memory: asChunks(read(data, "expected-memory")), + gas: BigInt(read(data, "expected-gas")), + exitCode: read(data, "expected-page-fault-address", 0) as number, + }; + + try { + assert.deepStrictEqual( + { + ...result, + status, + }, + expected, + ); + console.log(`${OK} ${data.name}`); + } catch (e) { + console.log(`${ERR} ${data.name}`); + throw e; + } + return data; +} + +type Chunk = { + address: number; + contents?: number[]; + data: number[]; +}; + +function asChunks(chunks: Chunk[]) { + return chunks.map((chunk: Chunk) => { + chunk.data = read(chunk, "contents") as number[]; + delete chunk.contents; + return chunk; + }); +} + +type Page = { + address: number; + length: number; + "is-writable": boolean; + access: Access; +}; +enum Access { + Read = 1, + Write = 2, +} +function asPageMap(pages: Page[]) { + return pages.map((page: Page) => { + page.access = read(page, "is-writable") ? Access.Write : Access.Read; + return page; + }); +} + +function statusAsString(status: number) { + const map: Record = { + 255: "ok", + 0: "halt", + 1: "panic", + 2: "page-fault", + 3: "host", + 4: "oog", + }; + + return map[status] || `unknown(${status})`; +} diff --git a/test/test-w3f-portable.ts b/test/test-w3f-portable.ts new file mode 100644 index 0000000..9887b01 --- /dev/null +++ b/test/test-w3f-portable.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +// @ts-expect-error: portable bundle has no TS declarations for direct import +import * as pvm from "../dist/build/js/portable-bundle.js"; +import { runW3fTests } from "./test-w3f-common.js"; + +runW3fTests(pvm); diff --git a/test/test-w3f.ts b/test/test-w3f.ts index 51e5a3a..c284942 100755 --- a/test/test-w3f.ts +++ b/test/test-w3f.ts @@ -1,176 +1,6 @@ #!/usr/bin/env node -import "json-bigint-patch"; -import * as assert from "node:assert"; -import { ERR, OK, read, run, TestOptions } from "../bin/src/test-json.js"; -import { disassemble, HasMetadata, InputKind, prepareProgram, runProgram } from "../build/release.js"; +import * as pvm from "../build/release.js"; +import { runW3fTests } from "./test-w3f-common.js"; -type PvmTest = { - name: string; - "initial-regs": (bigint | number)[]; - "initial-pc": number; - "initial-page-map": Page[]; - "initial-memory": Chunk[]; - "initial-gas": bigint | number; - program: number[]; - "expected-regs": (bigint | number)[]; - "expected-pc": number; - "expected-gas": bigint | number; - "expected-status": string; - "expected-page-fault-address"?: number; - "expected-memory": Chunk[]; -}; - -const ignored = [ - "inst_load_u8_nok", - "inst_store_imm_indirect_u16_with_offset_nok", - "inst_store_imm_indirect_u32_with_offset_nok", - "inst_store_imm_indirect_u64_with_offset_nok", - "inst_store_imm_indirect_u8_with_offset_nok", - "inst_store_imm_u8_trap_inaccessible", - "inst_store_imm_u8_trap_read_only", - "inst_store_indirect_u16_with_offset_nok", - "inst_store_indirect_u32_with_offset_nok", - "inst_store_indirect_u64_with_offset_nok", - "inst_store_indirect_u8_with_offset_nok", - "inst_store_u8_trap_inaccessible", - "inst_store_u8_trap_read_only", -]; - -// Run the CLI application -main(); - -// Main function -function main() { - const options: TestOptions = { - isDebug: false, - isSilent: false, - useSbrkGas: false, - }; - - run(processW3f, options); -} - -function processW3f(data: PvmTest, options: TestOptions) { - if (options.isDebug) { - console.debug(`🤖 Running ${data.name}`); - } - - if (ignored.includes(data.name)) { - if (options.isDebug) { - console.info(`⏭️ Skipping ${data.name}`); - } - return data; - } - // input - const input = { - registers: read(data, "initial-regs").map((x: number | bigint) => BigInt(x)), - pc: read(data, "initial-pc"), - pageMap: asPageMap(read(data, "initial-page-map")), - memory: asChunks(read(data, "initial-memory")), - gas: BigInt(read(data, "initial-gas")), - program: read(data, "program"), - }; - - if (options.isDebug) { - const assembly = disassemble(input.program, InputKind.Generic, HasMetadata.No); - console.info("==========="); - console.info(assembly); - console.info("\n^^^^^^^^^^^\n"); - } - - const exe = prepareProgram( - InputKind.Generic, - HasMetadata.No, - input.program, - input.registers, - input.pageMap, - input.memory, - [], - 16, - ); - const result = runProgram(exe, input.gas, input.pc, options.isDebug, options.useSbrkGas, true); - const status = statusAsString(result.status); - - // silent mode - just put our vals into expected (comparison done externally) - if (options.isSilent) { - data["expected-pc"] = result.pc; - data["expected-gas"] = result.gas; - data["expected-status"] = status; - data["expected-regs"] = result.registers; - data["expected-page-fault-address"] = result.exitCode; - - return data; - } - - // compare with expected values - const expected = { - // just copy JAM-output result field - result: result.result, - status: read(data, "expected-status"), - registers: read(data, "expected-regs").map((x: number | bigint) => BigInt(x)), - pc: read(data, "expected-pc"), - memory: asChunks(read(data, "expected-memory")), - gas: BigInt(read(data, "expected-gas")), - exitCode: read(data, "expected-page-fault-address", 0) as number, - }; - - try { - assert.deepStrictEqual( - { - ...result, - status, - }, - expected, - ); - console.log(`${OK} ${data.name}`); - } catch (e) { - console.log(`${ERR} ${data.name}`); - throw e; - } - return data; -} - -type Chunk = { - address: number; - contents?: number[]; - data: number[]; -}; - -function asChunks(chunks: Chunk[]) { - return chunks.map((chunk: Chunk) => { - chunk.data = read(chunk, "contents") as number[]; - delete chunk.contents; - return chunk; - }); -} - -type Page = { - address: number; - length: number; - "is-writable": boolean; - access: Access; -}; -enum Access { - Read = 1, - Write = 2, -} -function asPageMap(pages: Page[]) { - return pages.map((page: Page) => { - page.access = read(page, "is-writable") ? Access.Write : Access.Read; - return page; - }); -} - -function statusAsString(status: number) { - const map: Record = { - 255: "ok", - 0: "halt", - 1: "panic", - 2: "page-fault", - 3: "host", - 4: "oog", - }; - - return map[status] || `unknown(${status})`; -} +runW3fTests(pvm);