diff --git a/PolkaVM/Sources/PolkaVM/Memory.swift b/PolkaVM/Sources/PolkaVM/Memory.swift new file mode 100644 index 00000000..25485352 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Memory.swift @@ -0,0 +1,56 @@ +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(Memory.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(Memory.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 + } +} diff --git a/PolkaVM/Sources/PolkaVM/Program.swift b/PolkaVM/Sources/PolkaVM/ProgramCode.swift similarity index 60% rename from PolkaVM/Sources/PolkaVM/Program.swift rename to PolkaVM/Sources/PolkaVM/ProgramCode.swift index 092dc8c5..2eacc422 100644 --- a/PolkaVM/Sources/PolkaVM/Program.swift +++ b/PolkaVM/Sources/PolkaVM/ProgramCode.swift @@ -1,7 +1,7 @@ import Foundation import Utils -public class Program { +public class ProgramCode { public enum Error: Swift.Error { case invalidJumpTableEntriesCount case invalidJumpTableEncodeSize @@ -13,6 +13,7 @@ public class Program { public static let maxJumpTableEntriesCount: UInt64 = 0x100000 public static let maxEncodeSize: UInt8 = 8 public static let maxCodeLength: UInt64 = 0x400000 + public static let maxInstructionLength: UInt = 24 } public let blob: Data @@ -61,10 +62,43 @@ public class Program { bitmask = Slice(base: blob, bounds: codeEndIndex ..< slice.endIndex) } + + public static func skipOffset(start: UInt, bitmask: D) -> UInt? where D: Collection, D.Element == UInt8, D.Index == Int, D: ContiguousBytes { + 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(UInt((value >> offsetBits).trailingZeroBitCount), Constants.maxInstructionLength) + + return idx + } + + public func skip(start: UInt) -> UInt? { + if let val = ProgramCode.skipOffset(start: start, bitmask: bitmask) { + val + start + 1 + } else { + nil + } + } } -extension Program: Equatable { - public static func == (lhs: Program, rhs: Program) -> Bool { +extension ProgramCode: Equatable { + public static func == (lhs: ProgramCode, rhs: ProgramCode) -> Bool { lhs.blob == rhs.blob } } diff --git a/PolkaVM/Sources/PolkaVM/VMState.swift b/PolkaVM/Sources/PolkaVM/VMState.swift new file mode 100644 index 00000000..fe08ea9f --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/VMState.swift @@ -0,0 +1,13 @@ +import Foundation + +public class VMState { + public let program: ProgramCode + + public private(set) var instructionCounter: UInt32 + + public private(set) var registers: ( + UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32 + ) // 13 registers + public private(set) var gas: UInt64 + public private(set) var memory: Data +} diff --git a/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift b/PolkaVM/Tests/PolkaVMTests/ProgramCodeTests.swift new file mode 100644 index 00000000..22d13970 --- /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, UInt, UInt?)]) + func skip(testCase: (Data, UInt, UInt?)) { + #expect(ProgramCode.skipOffset(start: testCase.1, bitmask: testCase.0) == testCase.2) + } +} diff --git a/PolkaVM/Tests/PolkaVMTests/ProgramTests.swift b/PolkaVM/Tests/PolkaVMTests/ProgramTests.swift deleted file mode 100644 index 350ba90b..00000000 --- a/PolkaVM/Tests/PolkaVMTests/ProgramTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import Testing -import Utils - -@testable import PolkaVM - -struct ProgramTests { - @Test func empty() { - let blob = Data() - #expect(throws: Program.Error.invalidJumpTableEntriesCount) { try Program(blob) } - } - - @Test func invalidJumpTableEntriesCount() { - let highValue = Data(UInt64(0x1000000).encode(method: .variableWidth)) - let data = highValue + Data([0, 0]) - #expect(throws: Program.Error.invalidJumpTableEntriesCount) { try Program(data) } - } - - @Test func invalidJumpTableEncodeSize() { - let data = Data([1, 0xFF, 0, 0]) - #expect(throws: Program.Error.invalidJumpTableEncodeSize) { try Program(data) } - } - - @Test func invalidCodeLength() { - let highValue = Data(UInt64(0x1000000).encode(method: .variableWidth)) - let data = Data([0, 0]) + highValue - #expect(throws: Program.Error.invalidCodeLength) { try Program(data) } - } - - @Test func tooMuchData() throws { - let data = Data([0, 0, 2, 1, 2, 0, 0]) - #expect(throws: Program.Error.invalidDataLength) { try Program(data) } - } - - @Test func tooLittleData() throws { - let data = Data([0, 0, 2, 1, 2]) - #expect(throws: Program.Error.invalidDataLength) { try Program(data) } - } - - @Test func minimal() throws { - let data = Data([0, 0, 0]) - _ = try Program(data) - } - - @Test func simple() throws { - let data = Data([0, 0, 2, 1, 2, 0]) - _ = try Program(data) - } - - // TODO: add more tests -} diff --git a/Utils/Sources/Utils/Collection+Utils.swift b/Utils/Sources/Utils/Collection+Utils.swift index cafcbc18..b578fbc4 100644 --- a/Utils/Sources/Utils/Collection+Utils.swift +++ b/Utils/Sources/Utils/Collection+Utils.swift @@ -1,5 +1,5 @@ extension Collection { - subscript(safe index: Index) -> Element? { + public subscript(safe index: Index) -> Element? { indices.contains(index) ? self[index] : nil } }