Skip to content

Commit e3ba107

Browse files
authored
invoke pvm unit tests (#269)
* 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
1 parent 65a0f46 commit e3ba107

File tree

10 files changed

+190
-36
lines changed

10 files changed

+190
-36
lines changed

Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,7 @@ public class Invoke: HostCall {
10031003
let program = try ProgramCode(innerPvm.code)
10041004
let vm = VMState(program: program, pc: innerPvm.pc, registers: Registers(registers), gas: Gas(gas), memory: innerPvm.memory)
10051005
let engine = Engine(config: DefaultPvmConfig())
1006-
let exitReason = await engine.execute(program: program, state: vm)
1006+
let exitReason = await engine.execute(state: vm)
10071007

10081008
try state.writeMemory(address: startAddr, values: JamEncoder.encode(vm.getGas(), vm.getRegisters()))
10091009
context.pvms[pvmIndex]?.memory = vm.getMemoryUnsafe()

PolkaVM/Sources/PolkaVM/Engine.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ public class Engine {
1313
self.invocationContext = invocationContext
1414
}
1515

16-
public func execute(program: ProgramCode, state: VMState) async -> ExitReason {
16+
public func execute(state: VMState) async -> ExitReason {
1717
let context = ExecutionContext(state: state, config: config)
1818
while true {
1919
guard state.getGas() > GasInt(0) else {
2020
return .outOfGas
2121
}
22-
if case let .exit(reason) = step(program: program, context: context) {
22+
if case let .exit(reason) = step(program: state.program, context: context) {
2323
switch reason {
2424
case let .hostCall(callIndex):
2525
if case let .exit(hostExitReason) = await hostCall(state: state, callIndex: callIndex) {
@@ -44,14 +44,14 @@ public class Engine {
4444
case let .pageFault(address):
4545
return .exit(.pageFault(address))
4646
case let .hostCall(callIndexInner):
47-
let pc = state.pc
48-
let skip = state.program.skip(pc)
49-
state.increasePC(skip + 1)
5047
return await hostCall(state: state, callIndex: callIndexInner)
5148
default:
5249
return .exit(reason)
5350
}
5451
case .continued:
52+
let pc = state.pc
53+
let skip = state.program.skip(pc)
54+
state.increasePC(skip + 1)
5555
return .continued
5656
}
5757
}

PolkaVM/Sources/PolkaVM/ExecOutcome.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public enum ExitReason {
1+
public enum ExitReason: Equatable {
22
public enum PanicReason {
33
case trap
44
case invalidInstructionIndex

PolkaVM/Sources/PolkaVM/Memory.swift

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public protocol Memory {
6767
func read(address: UInt32) throws -> UInt8
6868
func read(address: UInt32, length: Int) throws -> Data
6969
func write(address: UInt32, value: UInt8) throws
70-
func write(address: UInt32, values: some Sequence<UInt8>) throws
70+
func write(address: UInt32, values: Data) throws
7171

7272
func zero(pageIndex: UInt32, pages: Int) throws
7373
func void(pageIndex: UInt32, pages: Int) throws
@@ -186,13 +186,13 @@ public class MemoryChunk {
186186
guard startAddress <= address, address + UInt32(length) <= endAddress else {
187187
throw .exceedChunkBoundary(address)
188188
}
189-
let startIndex = address - startAddress
189+
let startIndex = Int(address - startAddress) + data.startIndex
190190

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

197197
let zeroCount = max(0, length - validCount)
198198
let zeros = Data(repeating: 0, count: zeroCount)
@@ -201,18 +201,17 @@ public class MemoryChunk {
201201
}
202202
}
203203

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

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

213-
try zeroPad(until: startAddress + endIndex)
212+
try zeroPad(until: startAddress + UInt32(endIndex))
214213

215-
data[startIndex ..< endIndex] = valuesData
214+
data.replaceSubrange(startIndex ..< endIndex, with: values)
216215
}
217216

218217
public func incrementEnd(size increment: UInt32) throws(MemoryError) {
@@ -335,11 +334,11 @@ public class StandardMemory: Memory {
335334
guard isWritable(address: address, length: 1) else {
336335
throw .notWritable(address)
337336
}
338-
try getChunk(address: address).write(address: address, values: [value])
337+
try getChunk(address: address).write(address: address, values: Data([value]))
339338
}
340339

341-
public func write(address: UInt32, values: some Sequence<UInt8>) throws(MemoryError) {
342-
guard isWritable(address: address, length: values.underestimatedCount) else {
340+
public func write(address: UInt32, values: Data) throws(MemoryError) {
341+
guard isWritable(address: address, length: values.count) else {
343342
throw .notWritable(address)
344343
}
345344
try getChunk(address: address).write(address: address, values: values)
@@ -488,11 +487,11 @@ public class GeneralMemory: Memory {
488487
guard isWritable(address: address, length: 1) else {
489488
throw .notWritable(address)
490489
}
491-
try getChunk(address: address).write(address: address, values: [value])
490+
try getChunk(address: address).write(address: address, values: Data([value]))
492491
}
493492

494-
public func write(address: UInt32, values: some Sequence<UInt8>) throws(MemoryError) {
495-
guard isWritable(address: address, length: values.underestimatedCount) else {
493+
public func write(address: UInt32, values: Data) throws(MemoryError) {
494+
guard isWritable(address: address, length: values.count) else {
496495
throw .notWritable(address)
497496
}
498497
try getChunk(address: address).write(address: address, values: values)

PolkaVM/Sources/PolkaVM/ProgramCode.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public class ProgramCode {
146146

147147
var value: UInt32 = 0
148148
if (beginIndex + 4) < bitmask.endIndex { // if enough bytes
149-
value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex, as: UInt32.self) }
149+
value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex - bitmask.startIndex, as: UInt32.self) }
150150
} else {
151151
let byte1 = UInt32(bitmask[beginIndex])
152152
let byte2 = UInt32(bitmask[safe: beginIndex + 1] ?? 0xFF)

PolkaVM/Sources/PolkaVM/VMState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class VMState {
6565
}
6666

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

7171
public func sbrk(_ increment: UInt32) throws -> UInt32 {

PolkaVM/Sources/PolkaVM/invokePVM.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ public func invokePVM(
1111
pc: UInt32,
1212
gas: Gas,
1313
argumentData: Data?,
14-
ctx: any InvocationContext
14+
ctx: (any InvocationContext)?
1515
) async -> (ExitReason, Gas, Data?) {
1616
do {
1717
let state = try VMState(standardProgramBlob: blob, pc: pc, gas: gas, argumentData: argumentData)
1818
let engine = Engine(config: config, invocationContext: ctx)
19-
let exitReason = await engine.execute(program: state.program, state: state)
19+
let exitReason = await engine.execute(state: state)
2020

2121
switch exitReason {
2222
case .outOfGas:
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Foundation
2+
import Testing
3+
import Utils
4+
5+
@testable import PolkaVM
6+
7+
// standard programs
8+
let empty = Data([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0])
9+
let fibonacci = Data([
10+
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 61, 0, 0, 0, 0, 0, 51, 128, 119, 0,
11+
51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138, 200,
12+
152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 61, 7, 0, 0, 2, 0,
13+
51, 8, 4, 51, 7, 0, 0, 2, 0, 1, 50, 0, 73, 154, 148, 170, 130, 4, 3,
14+
])
15+
let sumToN = Data([
16+
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 46, 0, 0, 0, 0, 0, 38, 128, 119, 0,
17+
51, 8, 0, 100, 121, 40, 3, 0, 200, 137, 8, 149, 153, 255, 86, 9, 250,
18+
61, 8, 0, 0, 2, 0, 51, 8, 4, 51, 7, 0, 0, 2, 0, 1, 50, 0, 73, 77, 18,
19+
36, 24,
20+
])
21+
let sumToNWithHostCall = Data([
22+
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 48, 0, 0, 0, 0, 0, 40, 128, 119, 0,
23+
51, 8, 0, 100, 121, 40, 3, 0, 200, 137, 8, 149, 153, 255, 86, 9, 250,
24+
61, 8, 0, 0, 2, 0, 51, 8, 4, 51, 7, 0, 0, 2, 0, 10, 1, 1, 50, 0, 73,
25+
77, 18, 36, 104,
26+
])
27+
28+
struct InvokePVMTests {
29+
@Test func testEmptyProgram() async throws {
30+
let config = DefaultPvmConfig()
31+
let (exitReason, gas, output) = await invokePVM(
32+
config: config,
33+
blob: empty,
34+
pc: 0,
35+
gas: Gas(1_000_000),
36+
argumentData: Data(),
37+
ctx: nil
38+
)
39+
#expect(exitReason == .panic(.trap))
40+
#expect(gas == Gas(0))
41+
#expect(output == nil)
42+
}
43+
44+
@Test(arguments: [
45+
(2, 2, 999_980),
46+
(8, 34, 999_944),
47+
(9, 55, 999_938),
48+
])
49+
func testFibonacci(testCase: (input: UInt8, output: UInt8, gas: UInt64)) async throws {
50+
let config = DefaultPvmConfig()
51+
let (exitReason, gas, output) = await invokePVM(
52+
config: config,
53+
blob: fibonacci,
54+
pc: 0,
55+
gas: Gas(1_000_000),
56+
argumentData: Data([testCase.input]),
57+
ctx: nil
58+
)
59+
60+
let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0
61+
62+
switch exitReason {
63+
case .halt:
64+
#expect(value == testCase.output)
65+
#expect(gas == Gas(testCase.gas))
66+
default:
67+
Issue.record("Expected halt, got \(exitReason)")
68+
}
69+
}
70+
71+
@Test(arguments: [
72+
(1, 1, 999_988),
73+
(4, 10, 999_979),
74+
(5, 15, 999_976),
75+
])
76+
func testSumToN(testCase: (input: UInt8, output: UInt8, gas: UInt64)) async throws {
77+
let config = DefaultPvmConfig()
78+
let (exitReason, gas, output) = await invokePVM(
79+
config: config,
80+
blob: sumToN,
81+
pc: 0,
82+
gas: Gas(1_000_000),
83+
argumentData: Data([testCase.input]),
84+
ctx: nil
85+
)
86+
87+
let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0
88+
89+
switch exitReason {
90+
case .halt:
91+
#expect(value == testCase.output)
92+
#expect(gas == Gas(testCase.gas))
93+
default:
94+
Issue.record("Expected halt, got \(exitReason)")
95+
}
96+
}
97+
98+
@Test func testInvocationContext() async throws {
99+
let config = DefaultPvmConfig()
100+
101+
struct TestInvocationContext: InvocationContext {
102+
public typealias ContextType = Void
103+
104+
public var context: ContextType = ()
105+
106+
public func dispatch(index _: UInt32, state: VMState) async -> ExecOutcome {
107+
// perform output * 2
108+
do {
109+
let (ouputAddr, len): (UInt32, UInt32) = state.readRegister(Registers.Index(raw: 7), Registers.Index(raw: 8))
110+
let output = try state.readMemory(address: ouputAddr, length: Int(len))
111+
let value = output.withUnsafeBytes { $0.load(as: UInt32.self) }
112+
let newOutput = withUnsafeBytes(of: value << 1) { Data($0) }
113+
try state.writeMemory(address: ouputAddr, values: newOutput)
114+
return .continued
115+
} catch {
116+
return .exit(.panic(.trap))
117+
}
118+
}
119+
}
120+
121+
let (exitReason, _, output) = await invokePVM(
122+
config: config,
123+
blob: sumToNWithHostCall,
124+
pc: 0,
125+
gas: Gas(1_000_000),
126+
argumentData: Data([5]),
127+
ctx: TestInvocationContext()
128+
)
129+
130+
let value = output?.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) } ?? 0
131+
132+
switch exitReason {
133+
case .halt:
134+
#expect(value == 30)
135+
default:
136+
Issue.record("Expected halt, got \(exitReason)")
137+
}
138+
}
139+
}

PolkaVM/Tests/PolkaVMTests/MemoryTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ enum MemoryTests {
8080

8181
@Test func write() throws {
8282
let chunk = try MemoryChunk(startAddress: 0, endAddress: 10, data: Data())
83-
try chunk.write(address: 0, values: [1])
83+
try chunk.write(address: 0, values: Data([1]))
8484
#expect(chunk.data == Data([1]))
8585
try chunk.write(address: 1, values: Data([2]))
8686
#expect(chunk.data == Data([1, 2]))
@@ -199,7 +199,7 @@ enum MemoryTests {
199199
#expect(memory.isWritable(address: stackStart, length: Int(stackEnd - stackStart)) == true)
200200
try memory.write(address: stackStart, value: 1)
201201
#expect(try memory.read(address: stackStart, length: 2) == Data([1, 0]))
202-
try memory.write(address: stackEnd - 2, values: [1, 2])
202+
try memory.write(address: stackEnd - 2, values: Data([1, 2]))
203203
#expect(try memory.read(address: stackEnd - 4, length: 4) == Data([0, 0, 1, 2]))
204204

205205
// argument
@@ -260,9 +260,9 @@ enum MemoryTests {
260260
}
261261

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

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

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

PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ struct ProgramTests {
4747
_ = try ProgramCode(data)
4848
}
4949

50-
// TODO: add more Program parsing tests
51-
5250
@Test(arguments: [
5351
(Data(), 0, 0),
5452
(Data([0]), 0, 7),
@@ -66,4 +64,22 @@ struct ProgramTests {
6664
func skip(testCase: (Data, UInt32, UInt32)) {
6765
#expect(ProgramCode.skip(start: testCase.1, bitmask: testCase.0) == testCase.2)
6866
}
67+
68+
@Test(arguments: [
69+
// inst_branch_eq_imm_nok
70+
Data([0, 0, 16, 51, 7, 210, 4, 81, 39, 211, 4, 6, 0, 51, 7, 239, 190, 173, 222, 17, 6]),
71+
// inst_branch_greater_unsigned_imm_ok
72+
Data([0, 0, 14, 51, 7, 246, 86, 23, 10, 5, 0, 51, 7, 239, 190, 173, 222, 137, 1]),
73+
// fibonacci general program (from pvm debuger example)
74+
Data([
75+
0, 0, 33, 51, 8, 1, 51, 9, 1, 40, 3, 0, 149, 119, 255, 81, 7, 12, 100, 138,
76+
200, 152, 8, 100, 169, 40, 243, 100, 135, 51, 8, 51, 9, 1, 50, 0, 73, 147, 82, 213, 0,
77+
])
78+
])
79+
func parseProgramCode(testCase: Data) throws {
80+
let program = try ProgramCode(testCase)
81+
#expect(program.jumpTableEntrySize == 0)
82+
#expect(program.jumpTable == Data())
83+
#expect(program.code == testCase[3 ..< testCase[2] + 3])
84+
}
6985
}

0 commit comments

Comments
 (0)