From ebb7a84a90c7fb504b14d9cda763af931680dd4c Mon Sep 17 00:00:00 2001 From: Xiliang Chen Date: Fri, 26 Jul 2024 15:00:04 +1200 Subject: [PATCH] PolkaVM (#41) * PolkVM Package and integer codec * Program parsing * program parse * more tests * memory wip * fix * pvm wip * pvm * fix * fix lint --- JAMTests/jamtestvectors | 2 +- PolkaVM/Package.resolved | 69 +++++++++ PolkaVM/Package.swift | 39 +++++ PolkaVM/Sources/PolkaVM/Engine.swift | 26 ++++ PolkaVM/Sources/PolkaVM/ExitReason.swift | 12 ++ PolkaVM/Sources/PolkaVM/Instruction.swift | 32 +++++ .../Sources/PolkaVM/InstructionTable.swift | 30 ++++ PolkaVM/Sources/PolkaVM/Instructions.swift | 134 ++++++++++++++++++ PolkaVM/Sources/PolkaVM/Memory.swift | 95 +++++++++++++ PolkaVM/Sources/PolkaVM/ProgramCode.swift | 100 +++++++++++++ PolkaVM/Sources/PolkaVM/Registers.swift | 17 +++ PolkaVM/Sources/PolkaVM/VMState.swift | 31 ++++ .../Tests/PolkaVMTests/InstructionTests.swift | 25 ++++ .../Tests/PolkaVMTests/ProgramCodeTests.swift | 69 +++++++++ .../xcschemes/UtilsTests.xcscheme | 54 +++++++ Utils/Package.resolved | 2 +- Utils/Sources/Utils/Collection+Codec.swift | 49 +++++++ Utils/Sources/Utils/Collection+Utils.swift | 30 ++++ .../Sources/Utils/UnsignedInteger+Codec.swift | 78 ++++++++++ .../Tests/UtilsTests/IntegerCodecTests.swift | 105 ++++++++++++++ boka.xcodeproj/project.pbxproj | 2 + .../xcshareddata/swiftpm/Package.resolved | 11 +- 22 files changed, 1009 insertions(+), 3 deletions(-) create mode 100644 PolkaVM/Package.resolved create mode 100644 PolkaVM/Package.swift create mode 100644 PolkaVM/Sources/PolkaVM/Engine.swift create mode 100644 PolkaVM/Sources/PolkaVM/ExitReason.swift create mode 100644 PolkaVM/Sources/PolkaVM/Instruction.swift create mode 100644 PolkaVM/Sources/PolkaVM/InstructionTable.swift create mode 100644 PolkaVM/Sources/PolkaVM/Instructions.swift create mode 100644 PolkaVM/Sources/PolkaVM/Memory.swift create mode 100644 PolkaVM/Sources/PolkaVM/ProgramCode.swift create mode 100644 PolkaVM/Sources/PolkaVM/Registers.swift create mode 100644 PolkaVM/Sources/PolkaVM/VMState.swift create mode 100644 PolkaVM/Tests/PolkaVMTests/InstructionTests.swift create mode 100644 PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift create mode 100644 Utils/.swiftpm/xcode/xcshareddata/xcschemes/UtilsTests.xcscheme create mode 100644 Utils/Sources/Utils/Collection+Codec.swift create mode 100644 Utils/Sources/Utils/Collection+Utils.swift create mode 100644 Utils/Sources/Utils/UnsignedInteger+Codec.swift create mode 100644 Utils/Tests/UtilsTests/IntegerCodecTests.swift diff --git a/JAMTests/jamtestvectors b/JAMTests/jamtestvectors index 15f1b2b3..2a633e73 160000 --- a/JAMTests/jamtestvectors +++ b/JAMTests/jamtestvectors @@ -1 +1 @@ -Subproject commit 15f1b2b31fdf05aab1ec92c7190a9a302d6ae37a +Subproject commit 2a633e7398a29f2ccd167653591343dc6fc2ad90 diff --git a/PolkaVM/Package.resolved b/PolkaVM/Package.resolved new file mode 100644 index 00000000..816eba58 --- /dev/null +++ b/PolkaVM/Package.resolved @@ -0,0 +1,69 @@ +{ + "originHash" : "83027e22d1c1e6e15dd98a1719eec3b0aa70fd540013d36d77223e0931d6f7a8", + "pins" : [ + { + "identity" : "blake2.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tesseract-one/Blake2.swift.git", + "state" : { + "revision" : "29c55c8fe42d6661e5a32cc5bbbad1fff64fd01e", + "version" : "0.2.0" + } + }, + { + "identity" : "scalecodec.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AcalaNetwork/ScaleCodec.swift.git", + "state" : { + "branch" : "main", + "revision" : "dac3e7161de34c60c82794d031de0231b5a5746e" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "46072478ca365fe48370993833cb22de9b41567f", + "version" : "3.5.2" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" + } + }, + { + "identity" : "swift-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-testing.git", + "state" : { + "branch" : "0.10.0", + "revision" : "69d59cfc76e5daf498ca61f5af409f594768eef9" + } + }, + { + "identity" : "tuples.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tesseract-one/Tuples.swift.git", + "state" : { + "revision" : "4d2cf7c64443cdf4df833d0bedd767bf9dbc49d9", + "version" : "0.1.3" + } + } + ], + "version" : 3 +} diff --git a/PolkaVM/Package.swift b/PolkaVM/Package.swift new file mode 100644 index 00000000..1fd98c39 --- /dev/null +++ b/PolkaVM/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "PolkaVM", + platforms: [ + .macOS(.v14), + ], + products: [ + .library( + name: "PolkaVM", + targets: ["PolkaVM"] + ), + ], + dependencies: [ + .package(path: "../Utils"), + .package(url: "https://github.com/apple/swift-testing.git", branch: "0.10.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), + ], + targets: [ + .target( + name: "PolkaVM", + dependencies: [ + "Utils", + .product(name: "Logging", package: "swift-log"), + ] + ), + .testTarget( + name: "PolkaVMTests", + dependencies: [ + "PolkaVM", + .product(name: "Testing", package: "swift-testing"), + ] + ), + ], + swiftLanguageVersions: [.version("6")] +) diff --git a/PolkaVM/Sources/PolkaVM/Engine.swift b/PolkaVM/Sources/PolkaVM/Engine.swift new file mode 100644 index 00000000..54e00f88 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Engine.swift @@ -0,0 +1,26 @@ +public class Engine { + public init() {} + + public func execute(program: ProgramCode, state: VMState) -> ExitReason { + while true { + guard state.gas > 0 else { + return .outOfGas + } + if let exitReason = step(program: program, state: state) { + return exitReason + } + } + } + + public func step(program: ProgramCode, state: VMState) -> ExitReason? { + let pc = state.pc + guard let skip = program.skip(state.pc) else { + return .halt(.invalidInstruction) + } + guard let inst = InstructionTable.parse(program.code[Int(pc) ..< Int(pc + 1 + skip)]) else { + return .halt(.invalidInstruction) + } + + return inst.execute(state: state, skip: skip) + } +} diff --git a/PolkaVM/Sources/PolkaVM/ExitReason.swift b/PolkaVM/Sources/PolkaVM/ExitReason.swift new file mode 100644 index 00000000..59128ec1 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/ExitReason.swift @@ -0,0 +1,12 @@ +public enum ExitReason { + public enum HaltReason { + case trap + case invalidInstruction + } + + case halt(HaltReason) + case panic + case outOfGas + case hostCall(UInt32) + case pageFault(UInt32) +} diff --git a/PolkaVM/Sources/PolkaVM/Instruction.swift b/PolkaVM/Sources/PolkaVM/Instruction.swift new file mode 100644 index 00000000..42db952b --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Instruction.swift @@ -0,0 +1,32 @@ +import Foundation + +public protocol Instruction { + static var opcode: UInt8 { get } + + init?(data: Data) + + func execute(state: VMState, skip: UInt32) -> ExitReason? + func executeImpl(state: VMState) -> ExitReason? + + func gasCost() -> UInt64 + func updatePC(state: VMState, skip: UInt32) +} + +extension Instruction { + public func execute(state: VMState, skip: UInt32) -> ExitReason? { + state.consumeGas(gasCost()) + let res = executeImpl(state: state) + if res == nil { + state.updatePC(state.pc + skip + 1) + } + return res + } + + public func gasCost() -> UInt64 { + 1 + } + + public func updatePC(state: VMState, skip: UInt32) { + state.increasePC(skip + 1) + } +} diff --git a/PolkaVM/Sources/PolkaVM/InstructionTable.swift b/PolkaVM/Sources/PolkaVM/InstructionTable.swift new file mode 100644 index 00000000..e1560bcc --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/InstructionTable.swift @@ -0,0 +1,30 @@ +import Foundation + +public class InstructionTable { + public static let table: [Instruction.Type?] = { + let insts: [Instruction.Type] = [ + Instructions.Trap.self, + Instructions.Fallthrough.self, + Instructions.Ecalli.self, + Instructions.StoreImmU8.self, + Instructions.StoreImmU16.self, + Instructions.StoreImmU32.self, + ] + var table: [Instruction.Type?] = Array(repeating: nil, count: 256) + for i in 0 ..< insts.count { + table[Int(insts[i].opcode)] = insts[i] + } + return table + }() + + public static func parse(_ data: Data) -> (any Instruction)? { + guard data.count >= 1 else { + return nil + } + let opcode = data[data.startIndex] + guard let instType = table[Int(opcode)] else { + return nil + } + return instType.init(data: data) + } +} diff --git a/PolkaVM/Sources/PolkaVM/Instructions.swift b/PolkaVM/Sources/PolkaVM/Instructions.swift new file mode 100644 index 00000000..79d28af8 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Instructions.swift @@ -0,0 +1,134 @@ +import Foundation +import Utils + +public enum Instructions { + static func decodeImmidate(_ data: Data) -> UInt32 { + let len = min(data.count, 4) + if len == 0 { + return 0 + } + var value: UInt32 = 0 + for i in 0 ..< len { + value = value | (UInt32(data[i]) << (8 * i)) + } + let shift = (4 - len) * 8 + // shift left so that the MSB is the sign bit + // and then do signed shift right to fill the empty bits using the sign bit + // and then convert back to UInt32 + return UInt32(bitPattern: Int32(bitPattern: value << shift) >> shift) + } + + static func decodeImmidate2(_ data: Data) -> (UInt32, UInt32)? { + do { + let lA = try Int(data.at(0) & 0b111) + let lX = min(4, lA) + let lY1 = min(4, max(0, data.count - Int(lA) - 1)) + let lY2 = min(lY1, 8 - lA) + let vX = try decodeImmidate(data.at(1 ..< lX)) + let vY = try decodeImmidate(data.at((1 + lA) ..< lY2)) + return (vX, vY) + } catch { + return nil + } + } + + // MARK: Instructions without Arguments + + public struct Trap: Instruction { + public static var opcode: UInt8 { 0 } + + public init(data _: Data) {} + + public func executeImpl(state _: VMState) -> ExitReason? { + .halt(.trap) + } + } + + public struct Fallthrough: Instruction { + public static var opcode: UInt8 { 1 } + + public init(data _: Data) {} + + public func executeImpl(state _: VMState) -> ExitReason? { + nil + } + } + + // MARK: Instructions with Arguments of One Immediate + + public struct Ecalli: Instruction { + public static var opcode: UInt8 { 78 } + + public let callIndex: UInt32 + + public init(data: Data) { + callIndex = Instructions.decodeImmidate(data) + } + + public func executeImpl(state _: VMState) -> ExitReason? { + .hostCall(callIndex) + } + } + + // MARK: Instructions with Arguments of Two Immediates + + public struct StoreImmU8: Instruction { + public static var opcode: UInt8 { 62 } + + public let address: UInt32 + public let value: UInt8 + + public init(data: Data) { + let (x, y) = Instructions.decodeImmidate2(data)! + address = x + value = UInt8(truncatingIfNeeded: y) + } + + public func executeImpl(state: VMState) -> ExitReason? { + if (try? state.memory.write(address: address, value: value)) != nil { + return nil + } + return .pageFault(address) + } + } + + public struct StoreImmU16: Instruction { + public static var opcode: UInt8 { 79 } + + public let address: UInt32 + public let value: UInt16 + + public init(data: Data) { + let (x, y) = Instructions.decodeImmidate2(data)! + address = x + value = UInt16(truncatingIfNeeded: y) + } + + public func executeImpl(state: VMState) -> ExitReason? { + if (try? state.memory.write(address: address, values: value.encode(method: .fixedWidth(2)))) != nil { + return nil + } + return .pageFault(address) + } + } + + public struct StoreImmU32: Instruction { + public static var opcode: UInt8 { 38 } + + public let address: UInt32 + public let value: UInt32 + + public init(data: Data) { + let (x, y) = Instructions.decodeImmidate2(data)! + address = x + value = y + } + + public func executeImpl(state: VMState) -> ExitReason? { + if (try? state.memory.write(address: address, values: value.encode(method: .fixedWidth(4)))) != nil { + return nil + } + return .pageFault(address) + } + } +} diff --git a/PolkaVM/Sources/PolkaVM/Memory.swift b/PolkaVM/Sources/PolkaVM/Memory.swift new file mode 100644 index 00000000..decaff71 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Memory.swift @@ -0,0 +1,95 @@ +import Foundation + +public class Memory { + public enum Error: Swift.Error { + case pageFault + case notWritable + } + + private let pageMap: [(address: UInt32, length: UInt32, writable: Bool)] + private var chunks: [(address: UInt32, data: Data)] + + public init(pageMap: [(address: UInt32, length: UInt32, writable: Bool)], chunks: [(address: UInt32, data: Data)]) { + self.pageMap = pageMap + self.chunks = chunks + } + + public func read(_ address: UInt32) throws(Error) -> UInt8 { + // TODO: optimize this + // check for chunks + for chunk in chunks { + if chunk.address <= address, address < chunk.address + UInt32(chunk.data.count) { + return chunk.data[Int(address - chunk.address)] + } + } + // check for page map + for page in pageMap { + if page.address <= address, address < page.address + page.length { + return 0 + } + } + throw Error.pageFault + } + + public func write(address: UInt32, value: UInt8) throws(Error) { + // TODO: optimize this + // check for chunks + for i in 0 ..< chunks.count { + var chunk = chunks[i] + if chunk.address <= address, address < chunk.address + UInt32(chunk.data.count) { + chunk.data[Int(address - chunk.address)] = value + chunks[i] = chunk + return + } + } + // check for page map + for page in pageMap { + if page.address <= address, address < page.address + page.length { + var newChunk = (address: address, data: Data(repeating: 0, count: Int(page.length))) + newChunk.data[Int(address - page.address)] = value + chunks.append(newChunk) + return + } + } + throw Error.notWritable + } + + public func write(address: UInt32, values: some Sequence) throws(Error) { + // TODO: optimize this + // check for chunks + for i in 0 ..< chunks.count { + var chunk = chunks[i] + if chunk.address <= address, address < chunk.address + UInt32(chunk.data.count) { + var idx = Int(address - chunk.address) + for v in values { + if idx == chunk.data.endIndex { + chunk.data.append(v) + } else { + chunk.data[idx] = v + } + idx += 1 + } + chunks[i] = chunk + return + } + } + // check for page map + for page in pageMap { + if page.address <= address, address < page.address + page.length { + var newChunk = (address: address, data: Data(repeating: 0, count: Int(page.length))) + var idx = Int(address - page.address) + for v in values { + if idx == newChunk.data.endIndex { + newChunk.data.append(v) + } else { + newChunk.data[idx] = v + } + idx += 1 + } + chunks.append(newChunk) + return + } + } + throw Error.notWritable + } +} diff --git a/PolkaVM/Sources/PolkaVM/ProgramCode.swift b/PolkaVM/Sources/PolkaVM/ProgramCode.swift new file mode 100644 index 00000000..3cd3f5d1 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/ProgramCode.swift @@ -0,0 +1,100 @@ +import Foundation +import Utils + +public class ProgramCode { + public enum Error: Swift.Error { + case invalidJumpTableEntriesCount + case invalidJumpTableEncodeSize + case invalidCodeLength + case invalidDataLength + } + + public enum Constants { + public static let maxJumpTableEntriesCount: UInt64 = 0x100000 + public static let maxEncodeSize: UInt8 = 8 + public static let maxCodeLength: UInt64 = 0x400000 + public static let maxInstructionLength: UInt32 = 24 + } + + public let blob: Data + public let jumpTableEntrySize: UInt8 + public let jumpTable: Data + public let code: Data + private let bitmask: Data + + public init(_ blob: Data) throws { + self.blob = blob + + var slice = Slice(base: blob, bounds: blob.startIndex ..< blob.endIndex) + guard let jumpTableEntriesCount = slice.decode(), jumpTableEntriesCount <= Constants.maxJumpTableEntriesCount else { + throw Error.invalidJumpTableEntriesCount + } + guard let encodeSize = slice.next(), encodeSize <= Constants.maxEncodeSize else { + throw Error.invalidJumpTableEncodeSize + } + guard let codeLength = slice.decode(), codeLength <= Constants.maxCodeLength else { + throw Error.invalidCodeLength + } + + jumpTableEntrySize = encodeSize + + let jumpTableSize = Int(jumpTableEntriesCount * UInt64(jumpTableEntrySize)) + let jumpTableEndIndex = slice.startIndex + jumpTableSize + + guard jumpTableEndIndex <= slice.endIndex else { + throw Error.invalidDataLength + } + + jumpTable = blob[slice.startIndex ..< jumpTableEndIndex] + + let codeEndIndex = jumpTableEndIndex + Int(codeLength) + guard codeEndIndex <= slice.endIndex else { + throw Error.invalidDataLength + } + + code = blob[jumpTableEndIndex ..< codeEndIndex] + + let expectedBitmaskSize = (codeLength + 7) / 8 + + guard expectedBitmaskSize == slice.endIndex - codeEndIndex else { + throw Error.invalidDataLength + } + + bitmask = blob[codeEndIndex ..< slice.endIndex] + } + + public static func skip(start: UInt32, bitmask: Data) -> UInt32? { + let start = start + 1 + let beginIndex = Int(start / 8) + guard beginIndex < bitmask.endIndex else { + return nil + } + + var value: UInt32 = 0 + if (beginIndex + 4) < bitmask.endIndex { // if enough bytes + value = bitmask.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: beginIndex, as: UInt32.self) } + } else { + let byte1 = UInt32(bitmask[beginIndex]) + let byte2 = UInt32(bitmask[safe: beginIndex + 1] ?? 0) + let byte3 = UInt32(bitmask[safe: beginIndex + 2] ?? 0) + let byte4 = UInt32(bitmask[safe: beginIndex + 3] ?? 0) + value = byte1 | (byte2 << 8) | (byte3 << 16) | (byte4 << 24) + } + + let offsetBits = start % 8 + + let idx = min(UInt32((value >> offsetBits).trailingZeroBitCount), Constants.maxInstructionLength) + + return idx + } + + public func skip(_ start: UInt32) -> UInt32? { + ProgramCode.skip(start: start, bitmask: bitmask) + } +} + +extension ProgramCode: Equatable { + public static func == (lhs: ProgramCode, rhs: ProgramCode) -> Bool { + lhs.blob == rhs.blob + } +} diff --git a/PolkaVM/Sources/PolkaVM/Registers.swift b/PolkaVM/Sources/PolkaVM/Registers.swift new file mode 100644 index 00000000..c0a60bad --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Registers.swift @@ -0,0 +1,17 @@ +public class Registers { + public var reg1: UInt32 = 0 + public var reg2: UInt32 = 0 + public var reg3: UInt32 = 0 + public var reg4: UInt32 = 0 + public var reg5: UInt32 = 0 + public var reg6: UInt32 = 0 + public var reg7: UInt32 = 0 + public var reg8: UInt32 = 0 + public var reg9: UInt32 = 0 + public var reg10: UInt32 = 0 + public var reg11: UInt32 = 0 + public var reg12: UInt32 = 0 + public var reg13: UInt32 = 0 + + public init() {} +} diff --git a/PolkaVM/Sources/PolkaVM/VMState.swift b/PolkaVM/Sources/PolkaVM/VMState.swift new file mode 100644 index 00000000..b7e878d5 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/VMState.swift @@ -0,0 +1,31 @@ +import Foundation + +public class VMState { + public let program: ProgramCode + + public private(set) var pc: UInt32 + + public private(set) var registers: Registers + public private(set) var gas: Int64 + public private(set) var memory: Memory + + public init(program: ProgramCode, pc: UInt32, registers: Registers, gas: UInt64, memory: Memory) { + self.program = program + self.pc = pc + self.registers = registers + self.gas = Int64(gas) + self.memory = memory + } + + public func consumeGas(_ amount: UInt64) { + gas -= Int64(amount) + } + + public func updatePC(_ pc: UInt32) { + self.pc = pc + } + + public func increasePC(_ amount: UInt32) { + pc += amount + } +} diff --git a/PolkaVM/Tests/PolkaVMTests/InstructionTests.swift b/PolkaVM/Tests/PolkaVMTests/InstructionTests.swift new file mode 100644 index 00000000..5d3bf470 --- /dev/null +++ b/PolkaVM/Tests/PolkaVMTests/InstructionTests.swift @@ -0,0 +1,25 @@ +import Foundation +import Testing + +@testable import PolkaVM + +struct InstructionTests { + @Test func decodeImmediate() { + #expect(Instructions.decodeImmidate(Data()) == 0) + #expect(Instructions.decodeImmidate(Data([0])) == 0) + #expect(Instructions.decodeImmidate(Data([0x07])) == 0x07) + #expect(Instructions.decodeImmidate(Data([0xFF])) == 0xFFFF_FFFF) + #expect(Instructions.decodeImmidate(Data([0xF0])) == 0xFFFF_FFF0) + #expect(Instructions.decodeImmidate(Data([0x23, 0x7F])) == 0x7F23) + #expect(Instructions.decodeImmidate(Data([0x12, 0x80])) == 0xFFFF_8012) + #expect(Instructions.decodeImmidate(Data([0x34, 0x12, 0x7F])) == 0x7F1234) + #expect(Instructions.decodeImmidate(Data([0x34, 0x12, 0x80])) == 0xFF80_1234) + #expect(Instructions.decodeImmidate(Data([0x12, 0x34, 0x56, 0x78])) == 0x7856_3412) + #expect(Instructions.decodeImmidate(Data([0x12, 0x34, 0x56, 0xFA])) == 0xFA56_3412) + } + + @Test func decodeImmiate2() { + #expect(Instructions.decodeImmidate2(Data()) == nil) + // TODO: add more tests + } +} diff --git a/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift b/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift new file mode 100644 index 00000000..ad5b857d --- /dev/null +++ b/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift @@ -0,0 +1,69 @@ +import Foundation +import Testing +import Utils + +@testable import PolkaVM + +struct ProgramTests { + @Test func empty() { + let blob = Data() + #expect(throws: ProgramCode.Error.invalidJumpTableEntriesCount) { try ProgramCode(blob) } + } + + @Test func invalidJumpTableEntriesCount() { + let highValue = Data(UInt64(0x1000000).encode(method: .variableWidth)) + let data = highValue + Data([0, 0]) + #expect(throws: ProgramCode.Error.invalidJumpTableEntriesCount) { try ProgramCode(data) } + } + + @Test func invalidJumpTableEncodeSize() { + let data = Data([1, 0xFF, 0, 0]) + #expect(throws: ProgramCode.Error.invalidJumpTableEncodeSize) { try ProgramCode(data) } + } + + @Test func invalidCodeLength() { + let highValue = Data(UInt64(0x1000000).encode(method: .variableWidth)) + let data = Data([0, 0]) + highValue + #expect(throws: ProgramCode.Error.invalidCodeLength) { try ProgramCode(data) } + } + + @Test func tooMuchData() throws { + let data = Data([0, 0, 2, 1, 2, 0, 0]) + #expect(throws: ProgramCode.Error.invalidDataLength) { try ProgramCode(data) } + } + + @Test func tooLittleData() throws { + let data = Data([0, 0, 2, 1, 2]) + #expect(throws: ProgramCode.Error.invalidDataLength) { try ProgramCode(data) } + } + + @Test func minimal() throws { + let data = Data([0, 0, 0]) + _ = try ProgramCode(data) + } + + @Test func simple() throws { + let data = Data([0, 0, 2, 1, 2, 0]) + _ = try ProgramCode(data) + } + + // TODO: add more Program parsing tests + + @Test(arguments: [ + (Data(), 0, nil), + (Data([0]), 0, 24), + (Data([0]), 8, nil), + (Data([0b0010_0000]), 0, 4), + (Data([0b0010_0000]), 3, 1), + (Data([0b0010_0000]), 6, 24), + (Data([0b0010_0000]), 7, nil), + (Data([0, 0, 0b0010_0000, 0b0000_0010]), 0, 20), + (Data([0, 0, 0b0010_0000, 0b0000_0010]), 2, 18), + (Data([0, 0, 0b0010_0000, 0b0000_0010]), 10, 10), + (Data([0, 0, 0b0010_0000, 0b0000_0010]), 22, 2), + (Data([0, 0, 0, 0b0000_0010]), 5, 19), + ] as[(Data, UInt32, UInt32?)]) + func skip(testCase: (Data, UInt32, UInt32?)) { + #expect(ProgramCode.skip(start: testCase.1, bitmask: testCase.0) == testCase.2) + } +} diff --git a/Utils/.swiftpm/xcode/xcshareddata/xcschemes/UtilsTests.xcscheme b/Utils/.swiftpm/xcode/xcshareddata/xcschemes/UtilsTests.xcscheme new file mode 100644 index 00000000..73b57da9 --- /dev/null +++ b/Utils/.swiftpm/xcode/xcshareddata/xcschemes/UtilsTests.xcscheme @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Utils/Package.resolved b/Utils/Package.resolved index 0c4fa126..cdc09355 100644 --- a/Utils/Package.resolved +++ b/Utils/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "be8f7fb2fd767eb2600004386301bc02dc2c521dd44b1ce46a357cdff6adcf1b", + "originHash" : "030db1e42e515a220efea2eb343018ec6d762cc8165e183ebe4aac4e53a4dcdb", "pins" : [ { "identity" : "blake2.swift", diff --git a/Utils/Sources/Utils/Collection+Codec.swift b/Utils/Sources/Utils/Collection+Codec.swift new file mode 100644 index 00000000..25cfb3b1 --- /dev/null +++ b/Utils/Sources/Utils/Collection+Codec.swift @@ -0,0 +1,49 @@ +import Foundation + +extension Collection where SubSequence == Self { + public mutating func next() -> UInt8? { + guard let byte = self[safe: startIndex] else { + return nil + } + let nextIndex = index(after: startIndex) + self = self[nextIndex ..< endIndex] + return byte + } + + // implements the general natural number serialization format + public mutating func decode() -> UInt64? { + guard let firstByte = next() else { + return nil + } + if firstByte == 0 { + return 0 + } + let byteLengh = (~firstByte).leadingZeroBitCount + var res: UInt64 = 0 + if byteLengh > 0 { + guard let rest = decode(length: byteLengh) else { + return nil + } + res = rest + } + + let mask = UInt8(UInt(1) << (8 - byteLengh) - 1) + let topBits = firstByte & mask + + return res + UInt64(topBits) << (8 * byteLengh) + } + + public mutating func decode(length: Int) -> UInt64? { + guard length > 0 else { + return nil + } + var res: UInt64 = 0 + for l in 0 ..< length { + guard let byte = next() else { + return nil + } + res = res | UInt64(byte) << (8 * l) + } + return res + } +} diff --git a/Utils/Sources/Utils/Collection+Utils.swift b/Utils/Sources/Utils/Collection+Utils.swift new file mode 100644 index 00000000..91a21d0a --- /dev/null +++ b/Utils/Sources/Utils/Collection+Utils.swift @@ -0,0 +1,30 @@ +public enum IndexOutOfBounds: Error { + case indexOutOfBounds +} + +extension Collection { + public subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } + + public subscript(safe range: Range) -> SubSequence? { + guard indices.contains(range.lowerBound), indices.contains(range.upperBound) else { + return nil + } + return self[range] + } + + public func at(_ index: Index) throws(IndexOutOfBounds) -> Element { + guard let element = self[safe: index] else { + throw IndexOutOfBounds.indexOutOfBounds + } + return element + } + + public func at(_ range: Range) throws(IndexOutOfBounds) -> SubSequence { + guard let subSequence = self[safe: range] else { + throw IndexOutOfBounds.indexOutOfBounds + } + return subSequence + } +} diff --git a/Utils/Sources/Utils/UnsignedInteger+Codec.swift b/Utils/Sources/Utils/UnsignedInteger+Codec.swift new file mode 100644 index 00000000..621f68d5 --- /dev/null +++ b/Utils/Sources/Utils/UnsignedInteger+Codec.swift @@ -0,0 +1,78 @@ +import Foundation + +struct IntegerEncoder: Sequence { + public typealias Element = UInt8 + + private var value: T + private let method: EncodeMethod + + public init(value: T, method: EncodeMethod) { + self.value = value + self.method = method + } + + public func makeIterator() -> Iterator { + Iterator(value: value, method: method) + } + + struct Iterator: IteratorProtocol { + let method: EncodeMethod + let value: T + var position: Int + var length: Int? + + init(value: T, method: EncodeMethod) { + self.value = value + self.method = method + position = 0 + } + + public mutating func next() -> UInt8? { + defer { position += 1 } + switch method { + case let .fixedWidth(width): + guard position < width else { + return nil + } + let byte = UInt8(value >> (position * 8) & 0xFF) + return byte + case .variableWidth: + if value == 0 { + return position == 0 ? 0 : nil + } + if position == 0 { + for l in 0 ..< 8 where value < (1 << (7 * (l + 1))) { + length = l + let prefix = UInt8(256 - 1 << (8 - l)) + let data = UInt8(value / (1 << (8 * l))) + return prefix + data + } + length = 8 + return 255 + } + guard let length else { + assertionFailure("length is not set. this should not be possible") + return nil + } + + guard position <= length else { + return nil + } + + let byte = UInt8(value >> ((position - 1) * 8) & 0xFF) + return byte + } + } + } +} + +public enum EncodeMethod: Sendable { + case fixedWidth(Int) + case variableWidth +} + +extension UnsignedInteger { + public func encode(method: EncodeMethod) -> some Sequence { + IntegerEncoder(value: self, method: method) + } +} diff --git a/Utils/Tests/UtilsTests/IntegerCodecTests.swift b/Utils/Tests/UtilsTests/IntegerCodecTests.swift new file mode 100644 index 00000000..d7cc94c2 --- /dev/null +++ b/Utils/Tests/UtilsTests/IntegerCodecTests.swift @@ -0,0 +1,105 @@ +import Foundation +import Testing + +@testable import Utils + +@Suite struct IntegerCodecTests { + static func fixedWidthTestCasesSimple() -> [(UInt8, EncodeMethod, [UInt8])] { + [ + (42, .fixedWidth(0), []), + (42, .fixedWidth(1), [42]), + (42, .fixedWidth(2), [42, 0]), + (42, .fixedWidth(3), [42, 0, 0]), + ] + } + + @Test(arguments: fixedWidthTestCasesSimple()) + func fixedWidthTestCasesSimple(testCase: (UInt8, EncodeMethod, [UInt8])) { + let (value, method, expected) = testCase + let array = Array(value.encode(method: method)) + #expect(array == expected) + } + + static func fixedWidthTestCasesComplex() -> [(UInt64, EncodeMethod, [UInt8])] { + [ + (0, .fixedWidth(0), []), + (1, .fixedWidth(1), [1]), + (255, .fixedWidth(1), [255]), + (256, .fixedWidth(2), [0, 1]), + (257, .fixedWidth(3), [1, 1, 0]), + (UInt64(1) << 56, .fixedWidth(8), [0, 0, 0, 0, 0, 0, 0, 1]), + (UInt64.max, .fixedWidth(8), [255, 255, 255, 255, 255, 255, 255, 255]), + ] + } + + @Test(arguments: fixedWidthTestCasesComplex()) + func fixedWidthTestCasesComplex(testCase: (UInt64, EncodeMethod, [UInt8])) { + let (value, method, expected) = testCase + let array = Array(value.encode(method: method)) + #expect(array == expected) + } + + @Test(arguments: [ + UInt64(0), + UInt64(1), + UInt64(2), + UInt64(1) << 7 - 1, + UInt64(1) << 7, + UInt64(1) << 7 + 1, + UInt64(1) << 14 - 1, + UInt64(1) << 14, + UInt64(1) << 14 + 1, + UInt64(1) << 21 - 1, + UInt64(1) << 21, + UInt64(1) << 21 + 1, + UInt64(1) << 28 - 1, + UInt64(1) << 28, + UInt64(1) << 28 + 1, + UInt64(1) << 35 - 1, + UInt64(1) << 35, + UInt64(1) << 35 + 1, + UInt64(1) << 42 - 1, + UInt64(1) << 42, + UInt64(1) << 42 + 1, + UInt64(1) << 49 - 1, + UInt64(1) << 49, + UInt64(1) << 49 + 1, + UInt64(1) << 56 - 1, + UInt64(1) << 56, + UInt64(1) << 56 + 1, + UInt64(1) << 63 - 1, + UInt64(1) << 63, + UInt64(1) << 63 + 1, + UInt64.max - 1, + UInt64.max, + ] as[UInt64]) + func variableWidth(testCase: UInt64) { + let array = Array(testCase.encode(method: .variableWidth)) + var slice = array[...] + #expect(slice.decode() == testCase) + } + + @Test(arguments: [ + UInt64(0), + UInt64(1), + UInt64(2), + UInt64(1) << 32 - 2, + UInt64(1) << 32 - 1, + UInt64(1) << 32, + UInt64(1) << 32 + 2, + UInt64.max - 2, + UInt64.max - 1, + UInt64.max, + ]) + func fixedWidth(testCase: UInt64) { + let array = Array(testCase.encode(method: .fixedWidth(8))) + var slice = array[...] + #expect(slice.decode(length: 8) == testCase) + + var slice2 = array[...] + #expect(slice2.decode(length: 4) == testCase & 0xFFFF_FFFF) + + var slice3 = array[...] + #expect(slice3.decode(length: 2) == testCase & 0xFFFF) + } +} diff --git a/boka.xcodeproj/project.pbxproj b/boka.xcodeproj/project.pbxproj index 939e1d92..18ff0122 100644 --- a/boka.xcodeproj/project.pbxproj +++ b/boka.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 066C926E2C095D67005DDE4F /* Boka */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Boka; sourceTree = ""; }; 066C926F2C095D76005DDE4F /* Blockchain */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Blockchain; sourceTree = ""; }; 066C92702C095D85005DDE4F /* Node */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Node; sourceTree = ""; }; + 06AAEF0C2C47746C0064995D /* PolkaVM */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PolkaVM; sourceTree = ""; }; 06F2335F2C0B306000A5E2E0 /* Database */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Database; sourceTree = ""; }; 06F233602C0C69F100A5E2E0 /* Utils */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Utils; sourceTree = ""; }; /* End PBXFileReference section */ @@ -19,6 +20,7 @@ 066C92652C095CE5005DDE4F = { isa = PBXGroup; children = ( + 06AAEF0C2C47746C0064995D /* PolkaVM */, 06F233602C0C69F100A5E2E0 /* Utils */, 06F2335F2C0B306000A5E2E0 /* Database */, 066C92702C095D85005DDE4F /* Node */, diff --git a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8c2201e2..21499750 100644 --- a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ed66da7fc53dbb1608f852db9eddc0e27b9e4ce008bb4d6f4b19c51005638d54", + "originHash" : "c70167db8b9aae420525a783ce4b7c6139e70ed2b5033d12314879a65299c15d", "pins" : [ { "identity" : "blake2.swift", @@ -37,6 +37,15 @@ "version" : "3.4.0" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl",