Skip to content

Commit

Permalink
invoke pvm unit tests (#269)
Browse files Browse the repository at this point in the history
* invoke pvm unit tests wip

* fix memory read

* fix memory write

* fix format

* fix vm execute param

* add sumToN test

* add basic invocation context test

* remove log

* remove extra equatable impl

* fix extra Data init
  • Loading branch information
qiweiii authored Jan 27, 2025
1 parent 65a0f46 commit e3ba107
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@ public class Invoke: HostCall {
let program = try ProgramCode(innerPvm.code)
let vm = VMState(program: program, pc: innerPvm.pc, registers: Registers(registers), gas: Gas(gas), memory: innerPvm.memory)
let engine = Engine(config: DefaultPvmConfig())
let exitReason = await engine.execute(program: program, state: vm)
let exitReason = await engine.execute(state: vm)

try state.writeMemory(address: startAddr, values: JamEncoder.encode(vm.getGas(), vm.getRegisters()))
context.pvms[pvmIndex]?.memory = vm.getMemoryUnsafe()
Expand Down
10 changes: 5 additions & 5 deletions PolkaVM/Sources/PolkaVM/Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ public class Engine {
self.invocationContext = invocationContext
}

public func execute(program: ProgramCode, state: VMState) async -> ExitReason {
public func execute(state: VMState) async -> ExitReason {
let context = ExecutionContext(state: state, config: config)
while true {
guard state.getGas() > GasInt(0) else {
return .outOfGas
}
if case let .exit(reason) = step(program: program, context: context) {
if case let .exit(reason) = step(program: state.program, context: context) {
switch reason {
case let .hostCall(callIndex):
if case let .exit(hostExitReason) = await hostCall(state: state, callIndex: callIndex) {
Expand All @@ -44,14 +44,14 @@ public class Engine {
case let .pageFault(address):
return .exit(.pageFault(address))
case let .hostCall(callIndexInner):
let pc = state.pc
let skip = state.program.skip(pc)
state.increasePC(skip + 1)
return await hostCall(state: state, callIndex: callIndexInner)
default:
return .exit(reason)
}
case .continued:
let pc = state.pc
let skip = state.program.skip(pc)
state.increasePC(skip + 1)
return .continued
}
}
Expand Down
2 changes: 1 addition & 1 deletion PolkaVM/Sources/PolkaVM/ExecOutcome.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
public enum ExitReason {
public enum ExitReason: Equatable {
public enum PanicReason {
case trap
case invalidInstructionIndex
Expand Down
35 changes: 17 additions & 18 deletions PolkaVM/Sources/PolkaVM/Memory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public protocol Memory {
func read(address: UInt32) throws -> UInt8
func read(address: UInt32, length: Int) throws -> Data
func write(address: UInt32, value: UInt8) throws
func write(address: UInt32, values: some Sequence<UInt8>) throws
func write(address: UInt32, values: Data) throws

func zero(pageIndex: UInt32, pages: Int) throws
func void(pageIndex: UInt32, pages: Int) throws
Expand Down Expand Up @@ -186,13 +186,13 @@ public class MemoryChunk {
guard startAddress <= address, address + UInt32(length) <= endAddress else {
throw .exceedChunkBoundary(address)
}
let startIndex = address - startAddress
let startIndex = Int(address - startAddress) + data.startIndex

if startIndex >= data.count {
if startIndex >= data.endIndex {
return Data(repeating: 0, count: length)
} else {
let validCount = min(length, data.count - Int(startIndex))
let dataToRead = data.count > 0 ? data[startIndex ..< startIndex + UInt32(validCount)] : Data()
let validCount = min(length, data.endIndex - startIndex)
let dataToRead = data.count > 0 ? data[startIndex ..< startIndex + validCount] : Data()

let zeroCount = max(0, length - validCount)
let zeros = Data(repeating: 0, count: zeroCount)
Expand All @@ -201,18 +201,17 @@ public class MemoryChunk {
}
}

public func write(address: UInt32, values: some Sequence<UInt8>) throws(MemoryError) {
let valuesData = Data(values)
guard startAddress <= address, address + UInt32(valuesData.count) <= endAddress else {
public func write(address: UInt32, values: Data) throws(MemoryError) {
guard startAddress <= address, address + UInt32(values.count) <= endAddress else {
throw .exceedChunkBoundary(address)
}

let startIndex = address - startAddress
let endIndex = startIndex + UInt32(valuesData.count)
let startIndex = Int(address - startAddress) + data.startIndex
let endIndex = startIndex + values.count

try zeroPad(until: startAddress + endIndex)
try zeroPad(until: startAddress + UInt32(endIndex))

data[startIndex ..< endIndex] = valuesData
data.replaceSubrange(startIndex ..< endIndex, with: values)
}

public func incrementEnd(size increment: UInt32) throws(MemoryError) {
Expand Down Expand Up @@ -335,11 +334,11 @@ public class StandardMemory: Memory {
guard isWritable(address: address, length: 1) else {
throw .notWritable(address)
}
try getChunk(address: address).write(address: address, values: [value])
try getChunk(address: address).write(address: address, values: Data([value]))
}

public func write(address: UInt32, values: some Sequence<UInt8>) throws(MemoryError) {
guard isWritable(address: address, length: values.underestimatedCount) else {
public func write(address: UInt32, values: Data) throws(MemoryError) {
guard isWritable(address: address, length: values.count) else {
throw .notWritable(address)
}
try getChunk(address: address).write(address: address, values: values)
Expand Down Expand Up @@ -488,11 +487,11 @@ public class GeneralMemory: Memory {
guard isWritable(address: address, length: 1) else {
throw .notWritable(address)
}
try getChunk(address: address).write(address: address, values: [value])
try getChunk(address: address).write(address: address, values: Data([value]))
}

public func write(address: UInt32, values: some Sequence<UInt8>) throws(MemoryError) {
guard isWritable(address: address, length: values.underestimatedCount) else {
public func write(address: UInt32, values: Data) throws(MemoryError) {
guard isWritable(address: address, length: values.count) else {
throw .notWritable(address)
}
try getChunk(address: address).write(address: address, values: values)
Expand Down
2 changes: 1 addition & 1 deletion PolkaVM/Sources/PolkaVM/ProgramCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public class ProgramCode {

var value: UInt32 = 0
if (beginIndex + 4) < bitmask.endIndex { // if enough bytes
value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex, as: UInt32.self) }
value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex - bitmask.startIndex, as: UInt32.self) }
} else {
let byte1 = UInt32(bitmask[beginIndex])
let byte2 = UInt32(bitmask[safe: beginIndex + 1] ?? 0xFF)
Expand Down
2 changes: 1 addition & 1 deletion PolkaVM/Sources/PolkaVM/VMState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public class VMState {
}

public func writeMemory(address: some FixedWidthInteger, values: some Sequence<UInt8>) throws {
try memory.write(address: UInt32(truncatingIfNeeded: address), values: values)
try memory.write(address: UInt32(truncatingIfNeeded: address), values: Data(values))
}

public func sbrk(_ increment: UInt32) throws -> UInt32 {
Expand Down
4 changes: 2 additions & 2 deletions PolkaVM/Sources/PolkaVM/invokePVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public func invokePVM(
pc: UInt32,
gas: Gas,
argumentData: Data?,
ctx: any InvocationContext
ctx: (any InvocationContext)?
) async -> (ExitReason, Gas, Data?) {
do {
let state = try VMState(standardProgramBlob: blob, pc: pc, gas: gas, argumentData: argumentData)
let engine = Engine(config: config, invocationContext: ctx)
let exitReason = await engine.execute(program: state.program, state: state)
let exitReason = await engine.execute(state: state)

switch exitReason {
case .outOfGas:
Expand Down
139 changes: 139 additions & 0 deletions PolkaVM/Tests/PolkaVMTests/InvokePVMTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import Foundation
import Testing
import Utils

@testable import PolkaVM

// standard programs
let empty = Data([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0])
let fibonacci = Data([
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 61, 0, 0, 0, 0, 0, 51, 128, 119, 0,
51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200,
152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 61, 7, 0, 0, 2, 0,
51, 8, 4, 51, 7, 0, 0, 2, 0, 1, 50, 0, 73, 154, 148, 170, 130, 4, 3,
])
let sumToN = Data([
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 38, 128, 119, 0,
51, 8, 0, 100, 121, 40, 3, 0, 200, 137, 8, 149, 153, 255, 86, 9, 250,
61, 8, 0, 0, 2, 0, 51, 8, 4, 51, 7, 0, 0, 2, 0, 1, 50, 0, 73, 77, 18,
36, 24,
])
let sumToNWithHostCall = Data([
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 40, 128, 119, 0,
51, 8, 0, 100, 121, 40, 3, 0, 200, 137, 8, 149, 153, 255, 86, 9, 250,
61, 8, 0, 0, 2, 0, 51, 8, 4, 51, 7, 0, 0, 2, 0, 10, 1, 1, 50, 0, 73,
77, 18, 36, 104,
])

struct InvokePVMTests {
@Test func testEmptyProgram() async throws {
let config = DefaultPvmConfig()
let (exitReason, gas, output) = await invokePVM(
config: config,
blob: empty,
pc: 0,
gas: Gas(1_000_000),
argumentData: Data(),
ctx: nil
)
#expect(exitReason == .panic(.trap))
#expect(gas == Gas(0))
#expect(output == nil)
}

@Test(arguments: [
(2, 2, 999_980),
(8, 34, 999_944),
(9, 55, 999_938),
])
func testFibonacci(testCase: (input: UInt8, output: UInt8, gas: UInt64)) async throws {
let config = DefaultPvmConfig()
let (exitReason, gas, output) = await invokePVM(
config: config,
blob: fibonacci,
pc: 0,
gas: Gas(1_000_000),
argumentData: Data([testCase.input]),
ctx: nil
)

let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0

switch exitReason {
case .halt:
#expect(value == testCase.output)
#expect(gas == Gas(testCase.gas))
default:
Issue.record("Expected halt, got \(exitReason)")
}
}

@Test(arguments: [
(1, 1, 999_988),
(4, 10, 999_979),
(5, 15, 999_976),
])
func testSumToN(testCase: (input: UInt8, output: UInt8, gas: UInt64)) async throws {
let config = DefaultPvmConfig()
let (exitReason, gas, output) = await invokePVM(
config: config,
blob: sumToN,
pc: 0,
gas: Gas(1_000_000),
argumentData: Data([testCase.input]),
ctx: nil
)

let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0

switch exitReason {
case .halt:
#expect(value == testCase.output)
#expect(gas == Gas(testCase.gas))
default:
Issue.record("Expected halt, got \(exitReason)")
}
}

@Test func testInvocationContext() async throws {
let config = DefaultPvmConfig()

struct TestInvocationContext: InvocationContext {
public typealias ContextType = Void

public var context: ContextType = ()

public func dispatch(index _: UInt32, state: VMState) async -> ExecOutcome {
// perform output * 2
do {
let (ouputAddr, len): (UInt32, UInt32) = state.readRegister(Registers.Index(raw: 7), Registers.Index(raw: 8))
let output = try state.readMemory(address: ouputAddr, length: Int(len))
let value = output.withUnsafeBytes { $0.load(as: UInt32.self) }
let newOutput = withUnsafeBytes(of: value << 1) { Data($0) }
try state.writeMemory(address: ouputAddr, values: newOutput)
return .continued
} catch {
return .exit(.panic(.trap))
}
}
}

let (exitReason, _, output) = await invokePVM(
config: config,
blob: sumToNWithHostCall,
pc: 0,
gas: Gas(1_000_000),
argumentData: Data([5]),
ctx: TestInvocationContext()
)

let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0

switch exitReason {
case .halt:
#expect(value == 30)
default:
Issue.record("Expected halt, got \(exitReason)")
}
}
}
10 changes: 5 additions & 5 deletions PolkaVM/Tests/PolkaVMTests/MemoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ enum MemoryTests {

@Test func write() throws {
let chunk = try MemoryChunk(startAddress: 0, endAddress: 10, data: Data())
try chunk.write(address: 0, values: [1])
try chunk.write(address: 0, values: Data([1]))
#expect(chunk.data == Data([1]))
try chunk.write(address: 1, values: Data([2]))
#expect(chunk.data == Data([1, 2]))
Expand Down Expand Up @@ -199,7 +199,7 @@ enum MemoryTests {
#expect(memory.isWritable(address: stackStart, length: Int(stackEnd - stackStart)) == true)
try memory.write(address: stackStart, value: 1)
#expect(try memory.read(address: stackStart, length: 2) == Data([1, 0]))
try memory.write(address: stackEnd - 2, values: [1, 2])
try memory.write(address: stackEnd - 2, values: Data([1, 2]))
#expect(try memory.read(address: stackEnd - 4, length: 4) == Data([0, 0, 1, 2]))

// argument
Expand Down Expand Up @@ -260,9 +260,9 @@ enum MemoryTests {
}

@Test func write() throws {
try memory.write(address: 2, values: [9, 8])
try memory.write(address: 2, values: Data([9, 8]))
#expect(try memory.read(address: 0, length: 4) == Data([1, 2, 9, 8]))
#expect(throws: MemoryError.notWritable(4096)) { try memory.write(address: 4096, values: [0]) }
#expect(throws: MemoryError.notWritable(4096)) { try memory.write(address: 4096, values: Data([0])) }
}

@Test func sbrk() throws {
Expand All @@ -271,7 +271,7 @@ enum MemoryTests {
#expect(memory.isWritable(address: oldEnd, length: 512) == true)
#expect(memory.isWritable(address: 0, length: Int(oldEnd)) == true)

try memory.write(address: oldEnd, values: [1, 2, 3])
try memory.write(address: oldEnd, values: Data([1, 2, 3]))
#expect(try memory.read(address: oldEnd - 1, length: 5) == Data([7, 1, 2, 3, 0]))
}

Expand Down
20 changes: 18 additions & 2 deletions PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ struct ProgramTests {
_ = try ProgramCode(data)
}

// TODO: add more Program parsing tests

@Test(arguments: [
(Data(), 0, 0),
(Data([0]), 0, 7),
Expand All @@ -66,4 +64,22 @@ struct ProgramTests {
func skip(testCase: (Data, UInt32, UInt32)) {
#expect(ProgramCode.skip(start: testCase.1, bitmask: testCase.0) == testCase.2)
}

@Test(arguments: [
// inst_branch_eq_imm_nok
Data([0, 0, 16, 51, 7, 210, 4, 81, 39, 211, 4, 6, 0, 51, 7, 239, 190, 173, 222, 17, 6]),
// inst_branch_greater_unsigned_imm_ok
Data([0, 0, 14, 51, 7, 246, 86, 23, 10, 5, 0, 51, 7, 239, 190, 173, 222, 137, 1]),
// fibonacci general program (from pvm debuger example)
Data([
0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138,
200, 152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0,
])
])
func parseProgramCode(testCase: Data) throws {
let program = try ProgramCode(testCase)
#expect(program.jumpTableEntrySize == 0)
#expect(program.jumpTable == Data())
#expect(program.code == testCase[3 ..< testCase[2] + 3])
}
}

0 comments on commit e3ba107

Please sign in to comment.