diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d515e5da..23cc3752 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -14,7 +14,7 @@ jobs: coverage: name: Code Coverage runs-on: [self-hosted, linux] - timeout-minutes: 30 + timeout-minutes: 40 steps: - name: Checkout Code uses: actions/checkout@v4 diff --git a/Blockchain/Package.resolved b/Blockchain/Package.resolved index 96ecff90..dc062b09 100644 --- a/Blockchain/Package.resolved +++ b/Blockchain/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c4ca1f6a94c6e5be92063c6336c1ee0684b59733c988b20db7b76d32c3d1ea9d", + "originHash" : "9f6dc2b4da094b706ede8ef387ba9caa66ee0af06e5f7ec929286f624fd18481", "pins" : [ { "identity" : "blake2.swift", @@ -10,15 +10,6 @@ "version" : "0.2.0" } }, - { - "identity" : "lrucache", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/LRUCache.git", - "state" : { - "revision" : "542f0449556327415409ededc9c43a4bd0a397dc", - "version" : "1.0.7" - } - }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", diff --git a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift index df016f9b..85ee737a 100644 --- a/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift +++ b/Blockchain/Sources/Blockchain/VMInvocations/HostCall/HostCalls.swift @@ -1373,6 +1373,12 @@ public class Provide: HostCall { public class Log: HostCall { public static var identifier: UInt8 { 100 } + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy/MM/dd HH:mm:ss" + return formatter + }() + public func gasCost(state _: VMState) -> Gas { Gas(0) } @@ -1445,9 +1451,7 @@ public class Log: HostCall { let target = regs[1] == 0 && regs[2] == 0 ? nil : try? state.readMemory(address: regs[1], length: Int(regs[2])) let message = try? state.readMemory(address: regs[3], length: Int(regs[4])) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" - let time = dateFormatter.string(from: Date()) + let time = Self.dateFormatter.string(from: Date()) let details = Details( time: time, diff --git a/Codec/Sources/Codec/IntegerCodec.swift b/Codec/Sources/Codec/IntegerCodec.swift index 73f3b80e..a45b3b03 100644 --- a/Codec/Sources/Codec/IntegerCodec.swift +++ b/Codec/Sources/Codec/IntegerCodec.swift @@ -12,28 +12,39 @@ extension Collection where SubSequence == Self { IntegerCodec.decode { self.next() } } - // this is pretty inefficient - // so need to ensure the usage of this is minimal public mutating func decode(length: Int) -> T? { - IntegerCodec.decode(length: length) { self.next() } - } -} + guard length > 0, length <= count else { return nil } -public enum IntegerCodec { - public static func decode(length: Int, next: () throws -> UInt8?) rethrows -> T? { - guard length > 0 else { - return nil - } - var res: T = 0 - for l in 0 ..< length { - guard let byte = try next() else { - return nil + // fast path for Data + if let data = self as? Data { + let result: T? = data.withUnsafeBytes { buffer in + guard length <= buffer.count else { return nil } + let ptr = buffer.bindMemory(to: UInt8.self) + + var result: T = 0 + for i in 0 ..< length { + let byte = T(ptr[i]) + result |= byte << (8 * i) + } + return result } - res = res | T(byte) << (8 * l) + self = dropFirst(length) + return result + } + + // fallback + var result: T = 0 + for i in 0 ..< length { + let index = index(startIndex, offsetBy: i) + let byte = T(self[index]) + result |= byte << (8 * i) } - return res + self = dropFirst(length) + return result } +} +public enum IntegerCodec { public static func decode(next: () throws -> UInt8?) rethrows -> UInt64? { guard let firstByte = try next() else { return nil @@ -45,10 +56,12 @@ public enum IntegerCodec { let byteLength = (~firstByte).leadingZeroBitCount var res: UInt64 = 0 if byteLength > 0 { - guard let rest: UInt64 = try decode(length: byteLength, next: next) else { - return nil + for i in 0 ..< byteLength { + guard let byte = try next() else { + return nil + } + res |= UInt64(byte) << (8 * i) } - res = rest } let mask = UInt8(UInt(1) << (8 - byteLength) - 1) diff --git a/JAMTests/.swiftpm/xcode/xcshareddata/xcschemes/JAMTestsTests.xcscheme b/JAMTests/.swiftpm/xcode/xcshareddata/xcschemes/JAMTestsTests.xcscheme index e551faa1..e13d635f 100644 --- a/JAMTests/.swiftpm/xcode/xcshareddata/xcschemes/JAMTestsTests.xcscheme +++ b/JAMTests/.swiftpm/xcode/xcshareddata/xcschemes/JAMTestsTests.xcscheme @@ -1,16 +1,27 @@ + version = "2.2"> + + + + + + @@ -27,7 +38,7 @@ + + + + + buildConfiguration = "Release"> UInt32 { // In JIT mode, memory allocation would need to be handled by the JIT runtime // This would require coordination with the memory sandbox mechanism diff --git a/PolkaVM/Sources/PolkaVM/Instructions/Instructions+Helpers.swift b/PolkaVM/Sources/PolkaVM/Instructions/Instructions+Helpers.swift index e4e24516..3db44f76 100644 --- a/PolkaVM/Sources/PolkaVM/Instructions/Instructions+Helpers.swift +++ b/PolkaVM/Sources/PolkaVM/Instructions/Instructions+Helpers.swift @@ -75,34 +75,7 @@ extension Instructions { logger.trace("djump target data (\(targetAlignedData.map(\.self)))") #endif - var targetAligned: any UnsignedInteger - - switch entrySize { - case 1: - let u8: UInt8? = targetAlignedData.decode(length: entrySize) - guard let u8 else { - return .exit(.panic(.invalidDynamicJump)) - } - targetAligned = u8 - case 2: - let u16: UInt16? = targetAlignedData.decode(length: entrySize) - guard let u16 else { - return .exit(.panic(.invalidDynamicJump)) - } - targetAligned = u16 - case 3: - let u32: UInt32? = targetAlignedData.decode(length: entrySize) - guard let u32 else { - return .exit(.panic(.invalidDynamicJump)) - } - targetAligned = u32 - case 4: - let u32: UInt32? = targetAlignedData.decode(length: entrySize) - guard let u32 else { - return .exit(.panic(.invalidDynamicJump)) - } - targetAligned = u32 - default: + guard let targetAligned: UInt32 = targetAlignedData.decode(length: entrySize) else { return .exit(.panic(.invalidDynamicJump)) } @@ -110,11 +83,11 @@ extension Instructions { logger.trace("djump target decoded (\(targetAligned))") #endif - guard context.state.program.basicBlockIndices.contains(UInt32(targetAligned)) else { + guard context.state.program.basicBlockIndices.contains(targetAligned) else { return .exit(.panic(.invalidDynamicJump)) } - context.state.updatePC(UInt32(targetAligned)) + context.state.updatePC(targetAligned) return .continued } diff --git a/PolkaVM/Sources/PolkaVM/Instructions/Instructions.swift b/PolkaVM/Sources/PolkaVM/Instructions/Instructions.swift index e5cc05ab..82d39f40 100644 --- a/PolkaVM/Sources/PolkaVM/Instructions/Instructions.swift +++ b/PolkaVM/Sources/PolkaVM/Instructions/Instructions.swift @@ -90,7 +90,7 @@ extension CppHelper.Instructions.StoreImmU16: Instruction { } public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { - try context.state.writeMemory(address: address, values: value.encode(method: .fixedWidth(2))) + try context.state.writeMemory(address: address, values: value.encode()) return .continued } } @@ -102,7 +102,7 @@ extension CppHelper.Instructions.StoreImmU32: Instruction { } public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { - try context.state.writeMemory(address: address, values: value.encode(method: .fixedWidth(4))) + try context.state.writeMemory(address: address, values: value.encode()) return .continued } } @@ -114,7 +114,7 @@ extension CppHelper.Instructions.StoreImmU64: Instruction { } public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { - try context.state.writeMemory(address: address, values: value.encode(method: .fixedWidth(8))) + try context.state.writeMemory(address: address, values: value.encode()) return .continued } } @@ -289,7 +289,7 @@ extension CppHelper.Instructions.StoreU16: Instruction { public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { let value: UInt16 = context.state.readRegister(reg) - try context.state.writeMemory(address: address, values: value.encode(method: .fixedWidth(2))) + try context.state.writeMemory(address: address, values: value.encode()) return .continued } } @@ -303,7 +303,7 @@ extension CppHelper.Instructions.StoreU32: Instruction { public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { let value: UInt32 = context.state.readRegister(reg) - try context.state.writeMemory(address: address, values: value.encode(method: .fixedWidth(4))) + try context.state.writeMemory(address: address, values: value.encode()) return .continued } } @@ -317,7 +317,7 @@ extension CppHelper.Instructions.StoreU64: Instruction { public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { let value: UInt64 = context.state.readRegister(reg) - try context.state.writeMemory(address: address, values: value.encode(method: .fixedWidth(8))) + try context.state.writeMemory(address: address, values: value.encode()) return .continued } } @@ -345,7 +345,7 @@ extension CppHelper.Instructions.StoreImmIndU16: Instruction { public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { try context.state.writeMemory( address: context.state.readRegister(reg) &+ address, - values: value.encode(method: .fixedWidth(2)) + values: value.encode() ) return .continued } @@ -361,7 +361,7 @@ extension CppHelper.Instructions.StoreImmIndU32: Instruction { public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { try context.state.writeMemory( address: context.state.readRegister(reg) &+ address, - values: value.encode(method: .fixedWidth(4)) + values: value.encode() ) return .continued } @@ -377,7 +377,7 @@ extension CppHelper.Instructions.StoreImmIndU64: Instruction { public func _executeImpl(context: ExecutionContext) throws -> ExecOutcome { try context.state.writeMemory( address: context.state.readRegister(reg) &+ address, - values: value.encode(method: .fixedWidth(8)) + values: value.encode() ) return .continued } @@ -719,7 +719,7 @@ extension CppHelper.Instructions.StoreIndU16: Instruction { let value: UInt16 = context.state.readRegister(src) try context.state.writeMemory( address: context.state.readRegister(dest) &+ offset, - values: value.encode(method: .fixedWidth(2)) + values: value.encode() ) return .continued } @@ -736,7 +736,7 @@ extension CppHelper.Instructions.StoreIndU32: Instruction { let value: UInt32 = context.state.readRegister(src) try context.state.writeMemory( address: context.state.readRegister(dest) &+ offset, - values: value.encode(method: .fixedWidth(4)) + values: value.encode() ) return .continued } @@ -753,7 +753,7 @@ extension CppHelper.Instructions.StoreIndU64: Instruction { let value: UInt64 = context.state.readRegister(src) try context.state.writeMemory( address: context.state.readRegister(dest) &+ offset, - values: value.encode(method: .fixedWidth(8)) + values: value.encode() ) return .continued } diff --git a/PolkaVM/Sources/PolkaVM/Memory.swift b/PolkaVM/Sources/PolkaVM/Memory.swift deleted file mode 100644 index 1b3dffd3..00000000 --- a/PolkaVM/Sources/PolkaVM/Memory.swift +++ /dev/null @@ -1,703 +0,0 @@ -import Foundation -import LRUCache - -public enum MemoryError: Error, Equatable { - case zoneNotFound(UInt32) - case chunkNotFound(UInt32) - case invalidZone(UInt32) - case exceedZoneBoundary(UInt32) - case invalidChunk(UInt32) - case exceedChunkBoundary(UInt32) - case notReadable(UInt32) - case notWritable(UInt32) - case outOfMemory(UInt32) - case notAdjacent(UInt32) - - // align to page start address - private func alignToPageStart(address: UInt32) -> UInt32 { - let config = DefaultPvmConfig() - let pageSize = UInt32(config.pvmMemoryPageSize) - return (address / pageSize) * pageSize - } - - public var address: UInt32 { - switch self { - case let .zoneNotFound(address): - alignToPageStart(address: address) - case let .chunkNotFound(address): - alignToPageStart(address: address) - case let .invalidZone(address): - alignToPageStart(address: address) - case let .exceedZoneBoundary(address): - alignToPageStart(address: address) - case let .invalidChunk(address): - alignToPageStart(address: address) - case let .exceedChunkBoundary(address): - alignToPageStart(address: address) - case let .notReadable(address): - alignToPageStart(address: address) - case let .notWritable(address): - alignToPageStart(address: address) - case let .outOfMemory(address): - alignToPageStart(address: address) - case let .notAdjacent(address): - alignToPageStart(address: address) - } - } -} - -public enum PageAccess { - case readOnly - case readWrite - - public func isReadable() -> Bool { - switch self { - case .readOnly: - true - case .readWrite: - true - } - } - - public func isWritable() -> Bool { - switch self { - case .readWrite: - true - default: - false - } - } -} - -public protocol Memory { - var pageMap: PageMap { get } - - func isReadable(address: UInt32, length: Int) -> Bool - func isWritable(address: UInt32, length: Int) -> Bool - func isReadable(pageStart: UInt32, pages: Int) -> Bool - func isWritable(pageStart: UInt32, pages: Int) -> Bool - - 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: Data) throws - - func sbrk(_ increment: UInt32) throws -> UInt32 -} - -public class PageMap { - // TODO: consider SortedDictionary - private var pageTable: [UInt32: PageAccess] = [:] - private let config: PvmConfig - - // cache for multi page queries - // if the result is false, the page is the fault page, otherwise the page is the first page - private let isReadableCache: LRUCache, (result: Bool, page: UInt32)> - private let isWritableCache: LRUCache, (result: Bool, page: UInt32)> - - public init(pageMap: [(address: UInt32, length: UInt32, access: PageAccess)], config: PvmConfig) { - self.config = config - isReadableCache = .init(totalCostLimit: 0, countLimit: 1024) - isWritableCache = .init(totalCostLimit: 0, countLimit: 1024) - - for entry in pageMap { - let startIndex = entry.address / UInt32(config.pvmMemoryPageSize) - let pages = numberOfPagesToAccess(address: entry.address, length: Int(entry.length)) - - for i in startIndex ..< startIndex + pages { - pageTable[i] = entry.access - } - } - } - - private func numberOfPagesToAccess(address: UInt32, length: Int) -> UInt32 { - if length == 0 { - return 0 - } - let addressPageIndex = address / UInt32(config.pvmMemoryPageSize) - let endPageIndex = (address + UInt32(length) - 1) / UInt32(config.pvmMemoryPageSize) - return endPageIndex - addressPageIndex + 1 - } - - /// If the pages are readable, return (true, pageStart) - /// - /// If the pages are not readable, return (false, faultPageIndex). - public func isReadable(pageStart: UInt32, pages: Int) -> (result: Bool, page: UInt32) { - if pages == 0 { - return (pageTable[pageStart]?.isReadable() ?? false, pageStart) - } - let pageRange = pageStart ..< pageStart + UInt32(pages) - let cacheValue = isReadableCache.value(forKey: pageRange) - if let cacheValue { - return cacheValue - } - - var result = true - var page = pageStart - for i in pageRange { - let curResult = pageTable[i]?.isReadable() ?? false - if !curResult { - result = false - page = i - break - } - } - isReadableCache.setValue((result, page), forKey: pageRange) - return (result, page) - } - - /// If the pages are writable, return (true, address) - /// - /// If the pages are not writable, return (false, faultPage start address). - public func isReadable(address: UInt32, length: Int) -> (result: Bool, address: UInt32) { - let startPageIndex = address / UInt32(config.pvmMemoryPageSize) - let pages = numberOfPagesToAccess(address: address, length: length) - let (result, page) = isReadable(pageStart: startPageIndex, pages: Int(pages)) - return (result, page * UInt32(config.pvmMemoryPageSize)) - } - - /// If the pages are writable, return (true, pageStart) - /// - /// If the pages are not writable, return (false, faultPageIndex). - public func isWritable(pageStart: UInt32, pages: Int) -> (result: Bool, page: UInt32) { - if pages == 0 { - return (pageTable[pageStart]?.isWritable() ?? false, pageStart) - } - let pageRange = pageStart ..< pageStart + UInt32(pages) - let cacheValue = isWritableCache.value(forKey: pageRange) - if let cacheValue { - return cacheValue - } - - var result = true - var page = pageStart - for i in pageRange { - let curResult = pageTable[i]?.isWritable() ?? false - if !curResult { - result = false - page = i - break - } - } - isWritableCache.setValue((result, page), forKey: pageRange) - return (result, page) - } - - /// If the pages are writable, return (true, address) - /// - /// If the pages are not writable, return (false, faultPage start address). - public func isWritable(address: UInt32, length: Int) -> (result: Bool, address: UInt32) { - let startPageIndex = address / UInt32(config.pvmMemoryPageSize) - let pages = numberOfPagesToAccess(address: address, length: length) - let (result, page) = isWritable(pageStart: startPageIndex, pages: Int(pages)) - return (result, page * UInt32(config.pvmMemoryPageSize)) - } - - public func update(address: UInt32, length: Int, access: PageAccess) { - let startPageIndex = address / UInt32(config.pvmMemoryPageSize) - let pages = numberOfPagesToAccess(address: address, length: length) - let pageRange = startPageIndex ..< startPageIndex + pages - - for i in pageRange { - pageTable[i] = access - } - - isReadableCache.removeAllValues() - isWritableCache.removeAllValues() - } - - public func update(pageIndex: UInt32, pages: Int, access: PageAccess) { - if pages == 0 { - pageTable[pageIndex] = access - return - } - for i in pageIndex ..< pageIndex + UInt32(pages) { - pageTable[i] = access - } - isReadableCache.removeAllValues() - isWritableCache.removeAllValues() - } - - public func removeAccess(address: UInt32, length: Int) { - let startPageIndex = address / UInt32(config.pvmMemoryPageSize) - let pages = numberOfPagesToAccess(address: address, length: length) - let pageRange = startPageIndex ..< startPageIndex + UInt32(pages) - - for i in pageRange { - pageTable.removeValue(forKey: i) - } - isReadableCache.removeAllValues() - isWritableCache.removeAllValues() - } - - public func removeAccess(pageIndex: UInt32, pages: Int) { - if pages == 0 { - pageTable.removeValue(forKey: pageIndex) - return - } - for i in pageIndex ..< pageIndex + UInt32(pages) { - pageTable.removeValue(forKey: i) - } - isReadableCache.removeAllValues() - isWritableCache.removeAllValues() - } - - // find an inaccessible gap in page map if any - // return the first page index of the gap - public func findGapOrThrow(pages: Int) throws(MemoryError) -> UInt32 { - let sortedKeys = pageTable.keys.sorted() - - for i in 0 ..< sortedKeys.count { - let current = sortedKeys[i] - let next = sortedKeys[i + 1] - - if next - current >= pages { - return current + 1 - } - } - - throw .outOfMemory(0) - } -} - -/// MemoryZone is an isolated memory area, used for stack, heap, arguments, etc. -public class MemoryZone { - private let config: PvmConfig - public let startAddress: UInt32 - public private(set) var endAddress: UInt32 - - // TODO: could be optimized by using a more efficient data structure - public private(set) var chunks: [MemoryChunk] = [] - - public init(startAddress: UInt32, endAddress: UInt32, chunks: [MemoryChunk]) throws(MemoryError) { - guard startAddress <= endAddress, (chunks.isSorted { $0.endAddress < $1.startAddress }) else { - throw .invalidZone(startAddress) - } - - if let last = chunks.last, endAddress < last.endAddress { - throw .invalidZone(startAddress) - } - - self.startAddress = startAddress - self.endAddress = endAddress - self.chunks = chunks - config = DefaultPvmConfig() - } - - // binary search for the index containing the address or index to be inserted - private func searchChunk(for address: UInt32) -> (index: Int, found: Bool) { - var low = 0 - var high = chunks.endIndex - while low < high { - let mid = low + (high - low) / 2 - if chunks[mid].startAddress <= address, address < chunks[mid].endAddress { - return (mid, true) - } else if chunks[mid].startAddress > address { - high = mid - } else { - low = mid + 1 - } - } - return (low, false) - } - - /// Insert or update the chunks, overwrite overlapping data. - /// Return index of the chunk containing the address. - /// - /// Note this method assumes chunks are sorted by address. - /// Note caller should remember to handle corresponding page map updates - private func insertOrUpdate(address chunkStart: UInt32, data: Data) throws -> Int { - // use the longer one as the chunk end address - let chunkEnd = chunkStart + UInt32(data.count) - - // insert new chunk at the end - if chunkStart >= chunks.last?.endAddress ?? UInt32.max { - let chunk = try MemoryChunk(startAddress: chunkStart, data: data) - if chunks.last?.endAddress == chunkStart { - try chunks.last?.append(chunk: chunk) - return chunks.endIndex - 1 - } else { - chunks.append(chunk) - return chunks.endIndex - 1 - } - } - - // find overlapping chunks - var firstIndex = searchChunk(for: chunkStart).index - if firstIndex > 0, chunks[firstIndex - 1].endAddress > chunkStart { - firstIndex -= 1 - } - var lastIndex = firstIndex - while lastIndex < chunks.count, chunks[lastIndex].startAddress < chunkEnd { - lastIndex += 1 - } - - // no overlaps - if firstIndex == lastIndex { - try chunks.insert(MemoryChunk(startAddress: chunkStart, data: data), at: firstIndex) - return firstIndex - } - - // have overlaps - // calculate overlapping chunk boundaries - let startAddr = min(chunks[firstIndex].startAddress, chunkStart) - let endAddr = max(chunks[lastIndex - 1].endAddress, chunkEnd) - let newChunk = try MemoryChunk(startAddress: startAddr, data: Data()) - try newChunk.zeroExtend(until: endAddr) - // merge overlapping part into a new chunk - for i in firstIndex ..< lastIndex { - try newChunk.write(address: chunks[i].startAddress, values: chunks[i].data) - } - // overwrite overlapping part with input data - try newChunk.write(address: chunkStart, values: data) - // replace overlapping chunks - chunks.replaceSubrange(firstIndex ..< lastIndex, with: [newChunk]) - - return firstIndex - } - - public func read(address: UInt32, length: Int) throws -> Data { - guard length > 0 else { return Data() } - let readEnd = address &+ UInt32(length) - guard readEnd >= address else { throw MemoryError.outOfMemory(readEnd) } - guard endAddress >= readEnd else { throw MemoryError.exceedZoneBoundary(endAddress) } - - let (startIndex, _) = searchChunk(for: address) - var res = Data() - var curAddr = address - var curChunkIndex = startIndex - - while curAddr < readEnd, curChunkIndex < chunks.endIndex { - let chunk = chunks[curChunkIndex] - // handle gap before chunk - if curAddr < chunk.startAddress { - let gapSize = min(chunk.startAddress - curAddr, readEnd - curAddr) - res.append(Data(repeating: 0, count: Int(gapSize))) - curAddr += gapSize - continue - } - // handle chunk content - if curAddr >= chunk.endAddress { - curChunkIndex += 1 - continue - } - let chunkOffset = Int(curAddr - chunk.startAddress) - let bytesToRead = min(chunk.data.count - chunkOffset, Int(readEnd - curAddr)) - res.append(chunk.data[relative: chunkOffset ..< chunkOffset + bytesToRead]) - curAddr += UInt32(bytesToRead) - curChunkIndex += 1 - } - - // handle remaining space after last chunk - if curAddr < readEnd { - res.append(Data(repeating: 0, count: Int(readEnd - curAddr))) - } - return res - } - - public func write(address: UInt32, values: Data) throws { - _ = try insertOrUpdate(address: address, data: values) - } - - public func incrementEnd(size increment: UInt32) throws(MemoryError) { - guard endAddress <= UInt32.max - increment else { - throw .outOfMemory(endAddress) - } - endAddress += increment - } - - public func zero(pageIndex: UInt32, pages: Int) throws { - _ = try insertOrUpdate( - address: pageIndex * UInt32(config.pvmMemoryPageSize), - data: Data(repeating: 0, count: Int(pages * config.pvmMemoryPageSize)) - ) - } -} - -public class MemoryChunk { - public private(set) var startAddress: UInt32 - public var endAddress: UInt32 { - startAddress + UInt32(data.count) - } - - public private(set) var data: Data - - public init(startAddress: UInt32, data: Data) throws(MemoryError) { - let endAddress = startAddress + UInt32(data.count) - guard startAddress <= endAddress, endAddress - startAddress >= UInt32(data.count) else { - throw .invalidChunk(startAddress) - } - self.startAddress = startAddress - self.data = data - } - - // append another adjacent chunk - public func append(chunk: MemoryChunk) throws(MemoryError) { - guard endAddress == chunk.startAddress else { - throw .notAdjacent(chunk.startAddress) - } - guard chunk.endAddress <= UInt32.max else { - throw .outOfMemory(endAddress) - } - data.append(chunk.data) - } - - public func zeroExtend(until address: UInt32) throws(MemoryError) { - guard address <= UInt32.max else { - throw .outOfMemory(endAddress) - } - data.append(Data(repeating: 0, count: Int(address - startAddress) - data.count)) - } - - public func read(address: UInt32, length: Int) throws(MemoryError) -> Data { - guard startAddress <= address, address + UInt32(length) <= endAddress else { - throw .exceedChunkBoundary(address) - } - let startIndex = Int(address - startAddress) + data.startIndex - - return data[startIndex ..< startIndex + length] - } - - public func write(address: UInt32, values: Data) throws(MemoryError) { - guard startAddress <= address, address + UInt32(values.count) <= endAddress else { - throw .exceedChunkBoundary(address) - } - - let startIndex = Int(address - startAddress) + data.startIndex - let endIndex = startIndex + values.count - - data.replaceSubrange(startIndex ..< endIndex, with: values) - } -} - -/// Standard Program Memory -public class StandardMemory: Memory { - public let pageMap: PageMap - private let config: PvmConfig - - private let readOnly: MemoryZone - private let heap: MemoryZone - private let stack: MemoryZone - private let argument: MemoryZone - - public init(readOnlyData: Data, readWriteData: Data, argumentData: Data, heapEmptyPagesSize: UInt32, stackSize: UInt32) throws { - let config = DefaultPvmConfig() - let P = StandardProgram.alignToPageSize - let Z = StandardProgram.alignToZoneSize - let ZZ = UInt32(config.pvmProgramInitZoneSize) - - let readOnlyLen = UInt32(readOnlyData.count) - let readWriteLen = UInt32(readWriteData.count) - - let heapStart = 2 * ZZ + Z(readOnlyLen, config) - let heapDataPagesLen = P(readWriteLen, config) - - let stackPageAlignedSize = P(stackSize, config) - let stackStartAddr = UInt32(config.pvmProgramInitStackBaseAddress) - stackPageAlignedSize - - let argumentDataLen = UInt32(argumentData.count) - - readOnly = try MemoryZone( - startAddress: ZZ, - endAddress: ZZ + P(readOnlyLen, config), - chunks: [MemoryChunk(startAddress: ZZ, data: readOnlyData)] - ) - - heap = try MemoryZone( - startAddress: heapStart, - endAddress: heapStart + heapDataPagesLen + heapEmptyPagesSize, - chunks: [MemoryChunk(startAddress: heapStart, data: readWriteData)] - ) - stack = try MemoryZone( - startAddress: stackStartAddr, - endAddress: UInt32(config.pvmProgramInitStackBaseAddress), - chunks: [MemoryChunk(startAddress: stackStartAddr, data: Data(repeating: 0, count: Int(stackPageAlignedSize)))] - ) - argument = try MemoryZone( - startAddress: UInt32(config.pvmProgramInitInputStartAddress), - endAddress: UInt32(config.pvmProgramInitInputStartAddress) + P(argumentDataLen, config), - chunks: [MemoryChunk(startAddress: UInt32(config.pvmProgramInitInputStartAddress), data: argumentData)] - ) - - pageMap = PageMap(pageMap: [ - (ZZ, P(readOnlyLen, config), .readOnly), - (heapStart, heapDataPagesLen + heapEmptyPagesSize, .readWrite), - (stackStartAddr, stackPageAlignedSize, .readWrite), - (UInt32(config.pvmProgramInitInputStartAddress), P(argumentDataLen, config), .readOnly), - ], config: config) - - self.config = config - } - - private func getZone(address: UInt32) throws(MemoryError) -> MemoryZone { - if address >= readOnly.startAddress, address < readOnly.endAddress { - return readOnly - } else if address >= heap.startAddress, address < heap.endAddress { - return heap - } else if address >= stack.startAddress, address < stack.endAddress { - return stack - } else if address >= argument.startAddress, address < argument.endAddress { - return argument - } - throw .zoneNotFound(address) - } - - public func read(address: UInt32) throws -> UInt8 { - try ensureReadable(address: address, length: 1) - return try getZone(address: address).read(address: address, length: 1).first ?? 0 - } - - public func read(address: UInt32, length: Int) throws -> Data { - try ensureReadable(address: address, length: length) - return try getZone(address: address).read(address: address, length: length) - } - - public func write(address: UInt32, value: UInt8) throws { - try ensureWritable(address: address, length: 1) - try getZone(address: address).write(address: address, values: Data([value])) - } - - public func write(address: UInt32, values: Data) throws { - try ensureWritable(address: address, length: values.count) - try getZone(address: address).write(address: address, values: values) - } - - public func sbrk(_ size: UInt32) throws(MemoryError) -> UInt32 { - // NOTE: sbrk will be removed from GP - // NOTE: this impl aligns with w3f traces test vector README - - let prevHeapEnd = heap.endAddress - if size == 0 { - return prevHeapEnd - } - - let nextPageBoundary = StandardProgram.alignToPageSize(size: prevHeapEnd, config: config) - try heap.incrementEnd(size: size) - - if heap.endAddress > nextPageBoundary { - let finalBoundary = heap.endAddress - let start = nextPageBoundary / UInt32(config.pvmMemoryPageSize) - let end = finalBoundary / UInt32(config.pvmMemoryPageSize) - let count = Int(end - start) + 1 - pageMap.update(pageIndex: start, pages: count, access: .readWrite) - } - - return prevHeapEnd - } -} - -/// General Program Memory -public class GeneralMemory: Memory { - private let config: PvmConfig - public let pageMap: PageMap - - // general memory has a single zone - private let zone: MemoryZone - - public init(pageMap: [(address: UInt32, length: UInt32, writable: Bool)], chunks: [(address: UInt32, data: Data)]) throws { - let config = DefaultPvmConfig() - self.pageMap = PageMap( - pageMap: pageMap.map { (address: $0.address, length: $0.length, access: $0.writable ? .readWrite : .readOnly) }, - config: config - ) - - let memoryChunks = try chunks.map { chunk in - try MemoryChunk(startAddress: chunk.address, data: chunk.data) - } - - zone = try MemoryZone(startAddress: 0, endAddress: UInt32.max, chunks: memoryChunks) - self.config = config - } - - public func read(address: UInt32) throws -> UInt8 { - try ensureReadable(address: address, length: 1) - return try zone.read(address: address, length: 1).first ?? 0 - } - - public func read(address: UInt32, length: Int) throws -> Data { - try ensureReadable(address: address, length: length) - return try zone.read(address: address, length: length) - } - - public func write(address: UInt32, value: UInt8) throws { - try ensureWritable(address: address, length: 1) - try zone.write(address: address, values: Data([value])) - } - - public func write(address: UInt32, values: Data) throws { - try ensureWritable(address: address, length: values.count) - try zone.write(address: address, values: values) - } - - public func pages(pageIndex: UInt32, pages: Int, variant: UInt64) throws { - if variant == 0 { - pageMap.removeAccess(pageIndex: pageIndex, pages: pages) - } else if variant == 1 || variant == 3 { - pageMap.update(pageIndex: pageIndex, pages: pages, access: .readOnly) - } else if variant == 2 || variant == 4 { - pageMap.update(pageIndex: pageIndex, pages: pages, access: .readWrite) - } - - if variant < 3 { - try zone.zero(pageIndex: pageIndex, pages: pages) - } - } - - public func sbrk(_ size: UInt32) throws(MemoryError) -> UInt32 { - let pages = (Int(size) + config.pvmMemoryPageSize - 1) / config.pvmMemoryPageSize - let page = try pageMap.findGapOrThrow(pages: pages) - pageMap.update(pageIndex: page, pages: pages, access: .readWrite) - - return page * UInt32(config.pvmMemoryPageSize) - } -} - -extension Memory { - public func isReadable(address: UInt32, length: Int) -> Bool { - if length == 0 { return true } - return pageMap.isReadable(address: address, length: length).result - } - - public func isReadable(pageStart: UInt32, pages: Int) -> Bool { - pageMap.isReadable(pageStart: pageStart, pages: pages).result - } - - public func ensureReadable(address: UInt32, length: Int) throws(MemoryError) { - let (result, address) = pageMap.isReadable(address: address, length: length) - guard result else { - throw .notReadable(address) - } - } - - public func isWritable(address: UInt32, length: Int) -> Bool { - if length == 0 { return true } - return pageMap.isWritable(address: address, length: length).result - } - - public func isWritable(pageStart: UInt32, pages: Int) -> Bool { - pageMap.isWritable(pageStart: pageStart, pages: pages).result - } - - public func ensureWritable(address: UInt32, length: Int) throws(MemoryError) { - let (result, address) = pageMap.isWritable(address: address, length: length) - guard result else { - throw .notWritable(address) - } - } -} - -public class ReadonlyMemory { - private let memory: Memory - - public init(_ memory: Memory) { - self.memory = memory - } - - public func read(address: UInt32) throws -> UInt8 { - try memory.read(address: address) - } - - public func read(address: UInt32, length: Int) throws -> Data { - try memory.read(address: address, length: length) - } -} diff --git a/PolkaVM/Sources/PolkaVM/Memory/GeneralMemory.swift b/PolkaVM/Sources/PolkaVM/Memory/GeneralMemory.swift new file mode 100644 index 00000000..943c7789 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Memory/GeneralMemory.swift @@ -0,0 +1,316 @@ +import Foundation + +/// MemoryZone is an isolated memory area, used for stack, heap, arguments, etc. +final class MemoryZone { + private let config: PvmConfig + public let startAddress: UInt32 + public private(set) var endAddress: UInt32 + + // TODO: could be optimized by using a more efficient data structure + public private(set) var chunks: [MemoryChunk] = [] + + public init(startAddress: UInt32, endAddress: UInt32, chunks: [MemoryChunk]) throws(MemoryError) { + guard startAddress <= endAddress, (chunks.isSorted { $0.endAddress < $1.startAddress }) else { + throw .invalidZone(startAddress) + } + + if let last = chunks.last, endAddress < last.endAddress { + throw .invalidZone(startAddress) + } + + self.startAddress = startAddress + self.endAddress = endAddress + self.chunks = chunks + config = DefaultPvmConfig() + } + + // binary search for the index containing the address or index to be inserted + private func searchChunk(for address: UInt32) -> (index: Int, found: Bool) { + var low = 0 + var high = chunks.endIndex + while low < high { + let mid = low + (high - low) / 2 + if chunks[mid].startAddress <= address, address < chunks[mid].endAddress { + return (mid, true) + } else if chunks[mid].startAddress > address { + high = mid + } else { + low = mid + 1 + } + } + return (low, false) + } + + /// Insert or update the chunks, overwrite overlapping data. + /// + /// Note this method assumes chunks are sorted by address. + private func insertOrUpdate(address chunkStart: UInt32, data: Data) throws { + let chunkEnd = chunkStart + UInt32(data.count) + guard chunkEnd >= chunkStart else { throw MemoryError.outOfMemory(chunkEnd) } + + // fast search for exact chunk match + let (chunkIndex, found) = searchChunk(for: chunkStart) + if found, chunkIndex < chunks.count { + let chunk = chunks[chunkIndex] + if chunkStart >= chunk.startAddress, chunkEnd <= chunk.endAddress { + try chunk.write(address: chunkStart, values: data) + return + } + } + + // insert new chunk at the end + if chunkStart >= chunks.last?.endAddress ?? UInt32.max { + let chunk = try MemoryChunk(startAddress: chunkStart, data: data) + if chunks.last?.endAddress == chunkStart { + try chunks.last?.append(chunk: chunk) + return + } else { + chunks.append(chunk) + return + } + } + + // find overlapping chunks + var firstIndex = searchChunk(for: chunkStart).index + if firstIndex > 0, chunks[firstIndex - 1].endAddress > chunkStart { + firstIndex -= 1 + } + var lastIndex = firstIndex + while lastIndex < chunks.count, chunks[lastIndex].startAddress < chunkEnd { + lastIndex += 1 + } + + // no overlaps + if firstIndex == lastIndex { + try chunks.insert(MemoryChunk(startAddress: chunkStart, data: data), at: firstIndex) + return + } + + // have overlaps + // calculate overlapping chunk boundaries + let startAddr = min(chunks[firstIndex].startAddress, chunkStart) + let endAddr = max(chunks[lastIndex - 1].endAddress, chunkEnd) + let newChunk = try MemoryChunk(startAddress: startAddr, data: Data()) + try newChunk.zeroExtend(until: endAddr) + // merge overlapping part into a new chunk + for i in firstIndex ..< lastIndex { + try newChunk.write(address: chunks[i].startAddress, values: chunks[i].data) + } + // overwrite overlapping part with input data + try newChunk.write(address: chunkStart, values: data) + // replace overlapping chunks + if lastIndex - firstIndex == 1 { + // replacing exactly one chunk + chunks[firstIndex] = newChunk + } else { + chunks.replaceSubrange(firstIndex ..< lastIndex, with: [newChunk]) + } + } + + public func read(address: UInt32, length: Int) throws -> Data { + guard length > 0 else { return Data() } + let readEnd = address &+ UInt32(length) + guard readEnd >= address else { throw MemoryError.outOfMemory(readEnd) } + guard endAddress >= readEnd else { throw MemoryError.exceedZoneBoundary(endAddress) } + + let (startIndex, _) = searchChunk(for: address) + + var res = Data(count: length) + var resOffset = 0 + var curAddr = address + var curChunkIndex = startIndex + + res.withUnsafeMutableBytes { resBuffer in + let resPtr = resBuffer.bindMemory(to: UInt8.self).baseAddress! + + while curAddr < readEnd, curChunkIndex < chunks.endIndex { + let chunk = chunks[curChunkIndex] + + // handle gap before chunk + if curAddr < chunk.startAddress { + let gapSize = min(chunk.startAddress - curAddr, readEnd - curAddr) + // zero-fill gap using unsafe operations + memset(resPtr.advanced(by: resOffset), 0, Int(gapSize)) + resOffset += Int(gapSize) + curAddr += gapSize + continue + } + + // handle chunk content + if curAddr >= chunk.endAddress { + curChunkIndex += 1 + continue + } + + let chunkOffset = Int(curAddr - chunk.startAddress) + let bytesToRead = min(chunk.data.count - chunkOffset, Int(readEnd - curAddr)) + + // direct memory copy using unsafe operations + chunk.data.withUnsafeBytes { chunkBuffer in + let chunkPtr = chunkBuffer.bindMemory(to: UInt8.self).baseAddress! + memcpy(resPtr.advanced(by: resOffset), chunkPtr.advanced(by: chunkOffset), bytesToRead) + } + + resOffset += bytesToRead + curAddr += UInt32(bytesToRead) + curChunkIndex += 1 + } + + // handle remaining space after last chunk + if curAddr < readEnd { + let remainingSize = Int(readEnd - curAddr) + memset(resPtr.advanced(by: resOffset), 0, remainingSize) + } + } + + return res + } + + public func write(address: UInt32, values: Data) throws { + try insertOrUpdate(address: address, data: values) + } + + public func incrementEnd(size increment: UInt32) throws(MemoryError) { + guard endAddress <= UInt32.max - increment else { + throw .outOfMemory(endAddress) + } + endAddress += increment + } + + public func zero(pageIndex: UInt32, pages: Int) throws { + try insertOrUpdate( + address: pageIndex * UInt32(config.pvmMemoryPageSize), + data: Data(repeating: 0, count: Int(pages * config.pvmMemoryPageSize)) + ) + } +} + +final class MemoryChunk { + public private(set) var startAddress: UInt32 + public var endAddress: UInt32 { + startAddress + UInt32(data.count) + } + + public private(set) var data: Data + + public init(startAddress: UInt32, data: Data) throws(MemoryError) { + let endAddress = startAddress + UInt32(data.count) + guard startAddress <= endAddress, endAddress - startAddress >= UInt32(data.count) else { + throw .invalidChunk(startAddress) + } + self.startAddress = startAddress + self.data = data + } + + // append another adjacent chunk + public func append(chunk: MemoryChunk) throws(MemoryError) { + guard endAddress == chunk.startAddress else { + throw .notAdjacent(chunk.startAddress) + } + guard chunk.endAddress <= UInt32.max else { + throw .outOfMemory(endAddress) + } + data.append(chunk.data) + } + + public func zeroExtend(until address: UInt32) throws(MemoryError) { + guard address <= UInt32.max else { + throw .outOfMemory(endAddress) + } + data.append(Data(repeating: 0, count: Int(address - startAddress) - data.count)) + } + + public func read(address: UInt32, length: Int) throws(MemoryError) -> Data { + guard startAddress <= address, address + UInt32(length) <= endAddress else { + throw .exceedChunkBoundary(address) + } + + let offset = Int(address - startAddress) + data.startIndex + + return data[offset ..< (offset + length)] + } + + public func write(address: UInt32, values: Data) throws(MemoryError) { + guard startAddress <= address, address + UInt32(values.count) <= endAddress else { + throw .exceedChunkBoundary(address) + } + + let offset = Int(address - startAddress) + + if values.count > 0 { + data.withUnsafeMutableBytes { dataBuffer in + values.withUnsafeBytes { valuesBuffer in + let dataPtr = dataBuffer.bindMemory(to: UInt8.self).baseAddress! + let valuesPtr = valuesBuffer.bindMemory(to: UInt8.self).baseAddress! + memcpy(dataPtr.advanced(by: offset), valuesPtr, values.count) + } + } + } + } +} + +/// General Program Memory +public final class GeneralMemory: Memory { + private let config: PvmConfig + public let pageMap: PageMap + + // general memory has a single zone + private let zone: MemoryZone + + public init(pageMap: [(address: UInt32, length: UInt32, writable: Bool)], chunks: [(address: UInt32, data: Data)]) throws { + let config = DefaultPvmConfig() + self.pageMap = PageMap( + pageMap: pageMap.map { (address: $0.address, length: $0.length, access: $0.writable ? .readWrite : .readOnly) }, + config: config + ) + + let memoryChunks = try chunks.map { chunk in + try MemoryChunk(startAddress: chunk.address, data: chunk.data) + } + + zone = try MemoryZone(startAddress: 0, endAddress: UInt32.max, chunks: memoryChunks) + self.config = config + } + + public func read(address: UInt32) throws -> UInt8 { + try ensureReadable(address: address, length: 1) + return try zone.read(address: address, length: 1).first ?? 0 + } + + public func read(address: UInt32, length: Int) throws -> Data { + try ensureReadable(address: address, length: length) + return try zone.read(address: address, length: length) + } + + public func write(address: UInt32, value: UInt8) throws { + try ensureWritable(address: address, length: 1) + try zone.write(address: address, values: Data([value])) + } + + public func write(address: UInt32, values: Data) throws { + try ensureWritable(address: address, length: values.count) + try zone.write(address: address, values: values) + } + + public func pages(pageIndex: UInt32, pages: Int, variant: UInt64) throws { + if variant == 0 { + pageMap.removeAccess(pageIndex: pageIndex, pages: pages) + } else if variant == 1 || variant == 3 { + pageMap.update(pageIndex: pageIndex, pages: pages, access: .readOnly) + } else if variant == 2 || variant == 4 { + pageMap.update(pageIndex: pageIndex, pages: pages, access: .readWrite) + } + + if variant < 3 { + try zone.zero(pageIndex: pageIndex, pages: pages) + } + } + + public func sbrk(_ size: UInt32) throws(MemoryError) -> UInt32 { + let pages = (Int(size) + config.pvmMemoryPageSize - 1) / config.pvmMemoryPageSize + let page = try pageMap.findGapOrThrow(pages: pages) + pageMap.update(pageIndex: page, pages: pages, access: .readWrite) + + return page * UInt32(config.pvmMemoryPageSize) + } +} diff --git a/PolkaVM/Sources/PolkaVM/Memory/Memory.swift b/PolkaVM/Sources/PolkaVM/Memory/Memory.swift new file mode 100644 index 00000000..5edb11d5 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Memory/Memory.swift @@ -0,0 +1,112 @@ +import Foundation + +public enum MemoryError: Error, Equatable { + case zoneNotFound(UInt32) + case chunkNotFound(UInt32) + case invalidZone(UInt32) + case exceedZoneBoundary(UInt32) + case invalidChunk(UInt32) + case exceedChunkBoundary(UInt32) + case notReadable(UInt32) + case notWritable(UInt32) + case outOfMemory(UInt32) + case notAdjacent(UInt32) + + private static let pageSize = UInt32(DefaultPvmConfig().pvmMemoryPageSize) + + // align to page start address + private func alignToPageStart(address: UInt32) -> UInt32 { + (address / Self.pageSize) * Self.pageSize + } + + public var address: UInt32 { + switch self { + case let .zoneNotFound(address): + alignToPageStart(address: address) + case let .chunkNotFound(address): + alignToPageStart(address: address) + case let .invalidZone(address): + alignToPageStart(address: address) + case let .exceedZoneBoundary(address): + alignToPageStart(address: address) + case let .invalidChunk(address): + alignToPageStart(address: address) + case let .exceedChunkBoundary(address): + alignToPageStart(address: address) + case let .notReadable(address): + alignToPageStart(address: address) + case let .notWritable(address): + alignToPageStart(address: address) + case let .outOfMemory(address): + alignToPageStart(address: address) + case let .notAdjacent(address): + alignToPageStart(address: address) + } + } +} + +public protocol Memory { + var pageMap: PageMap { get } + + func isReadable(address: UInt32, length: Int) -> Bool + func isWritable(address: UInt32, length: Int) -> Bool + func isReadable(pageStart: UInt32, pages: Int) -> Bool + func isWritable(pageStart: UInt32, pages: Int) -> Bool + + 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: Data) throws + + func sbrk(_ increment: UInt32) throws -> UInt32 +} + +extension Memory { + public func isReadable(address: UInt32, length: Int) -> Bool { + if length == 0 { return true } + return pageMap.isReadable(address: address, length: length).result + } + + public func isReadable(pageStart: UInt32, pages: Int) -> Bool { + pageMap.isReadable(pageStart: pageStart, pages: pages).result + } + + public func ensureReadable(address: UInt32, length: Int) throws(MemoryError) { + let (result, address) = pageMap.isReadable(address: address, length: length) + guard result else { + throw .notReadable(address) + } + } + + public func isWritable(address: UInt32, length: Int) -> Bool { + if length == 0 { return true } + return pageMap.isWritable(address: address, length: length).result + } + + public func isWritable(pageStart: UInt32, pages: Int) -> Bool { + pageMap.isWritable(pageStart: pageStart, pages: pages).result + } + + public func ensureWritable(address: UInt32, length: Int) throws(MemoryError) { + let (result, address) = pageMap.isWritable(address: address, length: length) + guard result else { + throw .notWritable(address) + } + } +} + +public class ReadonlyMemory { + private let memory: Memory + + public init(_ memory: Memory) { + self.memory = memory + } + + public func read(address: UInt32) throws -> UInt8 { + try memory.read(address: address) + } + + public func read(address: UInt32, length: Int) throws -> Data { + try memory.read(address: address, length: length) + } +} diff --git a/PolkaVM/Sources/PolkaVM/Memory/PageMap.swift b/PolkaVM/Sources/PolkaVM/Memory/PageMap.swift new file mode 100644 index 00000000..a568ab59 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Memory/PageMap.swift @@ -0,0 +1,333 @@ +import Foundation + +public enum PageAccess { + case readOnly + case readWrite + + public func isReadable() -> Bool { + switch self { + case .readOnly: + true + case .readWrite: + true + } + } + + public func isWritable() -> Bool { + switch self { + case .readWrite: + true + default: + false + } + } +} + +public class PageMap { + private var readableBits: [UInt64] = [] + private var writableBits: [UInt64] = [] + private var maxPageIndex: UInt32 = 0 + + private let config: PvmConfig + + private let pageSize: UInt32 + private let pageSizeShift: UInt32 + + public init(pageMap: [(address: UInt32, length: UInt32, access: PageAccess)], config: PvmConfig) { + self.config = config + pageSize = UInt32(config.pvmMemoryPageSize) + pageSizeShift = UInt32(pageSize.trailingZeroBitCount) + + for entry in pageMap { + let startIndex = entry.address >> pageSizeShift + let pages = numberOfPagesToAccess(address: entry.address, length: Int(entry.length)) + let endIndex = startIndex + pages + maxPageIndex = max(maxPageIndex, endIndex) + } + + let bitsNeeded = (maxPageIndex + 63) / 64 + readableBits = Array(repeating: 0, count: Int(bitsNeeded)) + writableBits = Array(repeating: 0, count: Int(bitsNeeded)) + + for entry in pageMap { + let startIndex = entry.address >> pageSizeShift + let pages = numberOfPagesToAccess(address: entry.address, length: Int(entry.length)) + setPageAccessRange(startIndex: startIndex, pages: pages, access: entry.access) + } + } + + @inline(__always) + private func checkPagesInRange( + pageStart: UInt32, + pages: Int, + bits: [UInt64], + singlePageChecker: (UInt32) -> Bool + ) -> (result: Bool, page: UInt32) { + if pages == 0 { + return (singlePageChecker(pageStart), pageStart) + } + + let pageEnd = pageStart + UInt32(pages) + var currentPage = pageStart + + while currentPage < pageEnd { + let wordIndex = Int(currentPage / 64) + let bitIndex = Int(currentPage % 64) + let bitsInThisWord = min(64 - bitIndex, Int(pageEnd - currentPage)) + + let mask: UInt64 = if bitsInThisWord == 64 { + UInt64.max + } else { + (UInt64(1) << bitsInThisWord) - 1 + } + let shiftedMask = mask << bitIndex + + let wordValue = wordIndex < bits.count ? bits[wordIndex] : 0 + if (wordValue & shiftedMask) != shiftedMask { + for bit in 0 ..< bitsInThisWord { + let pageIndex = currentPage + UInt32(bit) + if !singlePageChecker(pageIndex) { + return (false, pageIndex) + } + } + } + + currentPage += UInt32(bitsInThisWord) + } + + return (true, pageStart) + } + + @inline(__always) + private func modifyBitsInRange( + startIndex: UInt32, + pages: UInt32, + modifier: (Int, UInt64) -> Void + ) { + let endIndex = startIndex + pages + var currentPage = startIndex + + while currentPage < endIndex { + let wordIndex = Int(currentPage / 64) + let bitIndex = Int(currentPage % 64) + let bitsInThisWord = min(64 - bitIndex, Int(endIndex - currentPage)) + + let mask: UInt64 = if bitsInThisWord == 64 { + UInt64.max + } else { + (UInt64(1) << bitsInThisWord) - 1 + } + let shiftedMask = mask << bitIndex + + modifier(wordIndex, shiftedMask) + currentPage += UInt32(bitsInThisWord) + } + } + + private func ensureCapacity(pageIndex: UInt32) { + if pageIndex >= maxPageIndex { + maxPageIndex = pageIndex + 1 + let bitsNeeded = (maxPageIndex + 63) / 64 + let currentSize = readableBits.count + + if Int(bitsNeeded) > currentSize { + readableBits.append(contentsOf: Array(repeating: 0, count: Int(bitsNeeded) - currentSize)) + writableBits.append(contentsOf: Array(repeating: 0, count: Int(bitsNeeded) - currentSize)) + } + } + } + + private func setPageAccessRange(startIndex: UInt32, pages: UInt32, access: PageAccess) { + if pages == 0 { return } + + let endIndex = startIndex + pages - 1 + if endIndex >= maxPageIndex { + ensureCapacity(pageIndex: endIndex) + } + + modifyBitsInRange(startIndex: startIndex, pages: pages) { wordIndex, shiftedMask in + switch access { + case .readOnly: + readableBits[wordIndex] |= shiftedMask + writableBits[wordIndex] &= ~shiftedMask + case .readWrite: + readableBits[wordIndex] |= shiftedMask + writableBits[wordIndex] |= shiftedMask + } + } + } + + private func setPageAccess(pageIndex: UInt32, access: PageAccess) { + ensureCapacity(pageIndex: pageIndex) + + let wordIndex = Int(pageIndex / 64) + let bitIndex = pageIndex % 64 + let mask = UInt64(1) << bitIndex + + switch access { + case .readOnly: + readableBits[wordIndex] |= mask + writableBits[wordIndex] &= ~mask + case .readWrite: + readableBits[wordIndex] |= mask + writableBits[wordIndex] |= mask + } + } + + private func isPageReadable(pageIndex: UInt32) -> Bool { + guard pageIndex < maxPageIndex else { return false } + let wordIndex = Int(pageIndex / 64) + let bitIndex = pageIndex % 64 + return (readableBits[wordIndex] & (UInt64(1) << bitIndex)) != 0 + } + + private func isPageWritable(pageIndex: UInt32) -> Bool { + guard pageIndex < maxPageIndex else { return false } + let wordIndex = Int(pageIndex / 64) + let bitIndex = pageIndex % 64 + return (writableBits[wordIndex] & (UInt64(1) << bitIndex)) != 0 + } + + private func clearPageAccess(pageIndex: UInt32) { + guard pageIndex < maxPageIndex else { return } + let wordIndex = Int(pageIndex / 64) + let bitIndex = pageIndex % 64 + let mask = UInt64(1) << bitIndex + readableBits[wordIndex] &= ~mask + writableBits[wordIndex] &= ~mask + } + + private func numberOfPagesToAccess(address: UInt32, length: Int) -> UInt32 { + if length == 0 { + return 0 + } + let addressPageIndex = address >> pageSizeShift + let endPageIndex = (address + UInt32(length) - 1) >> pageSizeShift + return endPageIndex - addressPageIndex + 1 + } + + public func isReadable(pageStart: UInt32, pages: Int) -> (result: Bool, page: UInt32) { + checkPagesInRange( + pageStart: pageStart, + pages: pages, + bits: readableBits, + singlePageChecker: isPageReadable + ) + } + + public func isReadable(address: UInt32, length: Int) -> (result: Bool, address: UInt32) { + let startPageIndex = address >> pageSizeShift + let pages = numberOfPagesToAccess(address: address, length: length) + let (result, page) = isReadable(pageStart: startPageIndex, pages: Int(pages)) + return (result, page << pageSizeShift) + } + + public func isWritable(pageStart: UInt32, pages: Int) -> (result: Bool, page: UInt32) { + checkPagesInRange( + pageStart: pageStart, + pages: pages, + bits: writableBits, + singlePageChecker: isPageWritable + ) + } + + public func isWritable(address: UInt32, length: Int) -> (result: Bool, address: UInt32) { + let startPageIndex = address >> pageSizeShift + let pages = numberOfPagesToAccess(address: address, length: length) + let (result, page) = isWritable(pageStart: startPageIndex, pages: Int(pages)) + return (result, page << pageSizeShift) + } + + public func update(address: UInt32, length: Int, access: PageAccess) { + let startPageIndex = address >> pageSizeShift + let pages = numberOfPagesToAccess(address: address, length: length) + setPageAccessRange(startIndex: startPageIndex, pages: pages, access: access) + } + + public func update(pageIndex: UInt32, pages: Int, access: PageAccess) { + if pages == 0 { + setPageAccess(pageIndex: pageIndex, access: access) + return + } + setPageAccessRange(startIndex: pageIndex, pages: UInt32(pages), access: access) + } + + public func removeAccess(address: UInt32, length: Int) { + let startPageIndex = address >> pageSizeShift + let pages = numberOfPagesToAccess(address: address, length: length) + + modifyBitsInRange(startIndex: startPageIndex, pages: pages) { wordIndex, shiftedMask in + if wordIndex < readableBits.count { + readableBits[wordIndex] &= ~shiftedMask + writableBits[wordIndex] &= ~shiftedMask + } + } + } + + public func removeAccess(pageIndex: UInt32, pages: Int) { + if pages == 0 { + clearPageAccess(pageIndex: pageIndex) + return + } + + modifyBitsInRange(startIndex: pageIndex, pages: UInt32(pages)) { wordIndex, shiftedMask in + if wordIndex < readableBits.count { + readableBits[wordIndex] &= ~shiftedMask + writableBits[wordIndex] &= ~shiftedMask + } + } + } + + public func findGapOrThrow(pages: Int) throws(MemoryError) -> UInt32 { + var currentGapStart: UInt32? + var gapSize: UInt32 = 0 + + let searchLimit = max(maxPageIndex, UInt32(pages) + maxPageIndex + 64) + let totalWords = (searchLimit + 63) / 64 + + for wordIdx in 0 ..< Int(totalWords) { + let readableWord = wordIdx < readableBits.count ? readableBits[wordIdx] : 0 + let writableWord = wordIdx < writableBits.count ? writableBits[wordIdx] : 0 + let occupiedWord = readableWord | writableWord + + if occupiedWord == 0 { + let pagesInWord = min(64, Int(searchLimit) - wordIdx * 64) + if currentGapStart == nil { + currentGapStart = UInt32(wordIdx * 64) + gapSize = UInt32(pagesInWord) + } else { + gapSize += UInt32(pagesInWord) + } + + if gapSize >= pages { + return currentGapStart! + } + continue + } + + let pagesInWord = min(64, Int(searchLimit) - wordIdx * 64) + for bitIdx in 0 ..< pagesInWord { + let pageIndex = UInt32(wordIdx * 64 + bitIdx) + let hasAccess = (occupiedWord & (UInt64(1) << bitIdx)) != 0 + + if !hasAccess { + if currentGapStart == nil { + currentGapStart = pageIndex + gapSize = 1 + } else { + gapSize += 1 + } + + if gapSize >= pages { + return currentGapStart! + } + } else { + currentGapStart = nil + gapSize = 0 + } + } + } + + throw .outOfMemory(0) + } +} diff --git a/PolkaVM/Sources/PolkaVM/Memory/StandardMemory.swift b/PolkaVM/Sources/PolkaVM/Memory/StandardMemory.swift new file mode 100644 index 00000000..26fb98d0 --- /dev/null +++ b/PolkaVM/Sources/PolkaVM/Memory/StandardMemory.swift @@ -0,0 +1,208 @@ +import Foundation + +/// Standard Program Memory +public final class StandardMemory: Memory { + public let pageMap: PageMap + private let config: PvmConfig + + private class Zone { + let startAddress: UInt32 + var endAddress: UInt32 + var data: Data + + init(startAddress: UInt32, endAddress: UInt32, data: Data) { + self.startAddress = startAddress + self.endAddress = endAddress + self.data = data + } + + func contains(_ address: UInt32) -> Bool { + address >= startAddress && address < endAddress + } + + func offset(for address: UInt32) -> Int { + Int(address - startAddress) + } + } + + private var readOnlyZone: Zone + private var heapZone: Zone + private var stackZone: Zone + private var argumentZone: Zone + + public init(readOnlyData: Data, readWriteData: Data, argumentData: Data, heapEmptyPagesSize: UInt32, stackSize: UInt32) throws { + let config = DefaultPvmConfig() + let P = StandardProgram.alignToPageSize + let Z = StandardProgram.alignToZoneSize + let ZZ = UInt32(config.pvmProgramInitZoneSize) + + let readOnlyLen = UInt32(readOnlyData.count) + let readWriteLen = UInt32(readWriteData.count) + + let heapStart = 2 * ZZ + Z(readOnlyLen, config) + let heapDataPagesLen = P(readWriteLen, config) + + let stackPageAlignedSize = P(stackSize, config) + let stackStartAddr = UInt32(config.pvmProgramInitStackBaseAddress) - stackPageAlignedSize + + let argumentDataLen = UInt32(argumentData.count) + + readOnlyZone = Zone( + startAddress: ZZ, + endAddress: ZZ + P(readOnlyLen, config), + data: readOnlyData + ) + + var heapData = readWriteData + let totalHeapSize = Int(heapDataPagesLen + heapEmptyPagesSize) + if heapData.count < totalHeapSize { + let oldSize = heapData.count + let additionalSize = totalHeapSize - oldSize + + // Resize and zero-fill efficiently + heapData.count = totalHeapSize + heapData.withUnsafeMutableBytes { bytes in + let zeroPtr = bytes.baseAddress!.advanced(by: oldSize) + memset(zeroPtr, 0, additionalSize) + } + } + heapZone = Zone( + startAddress: heapStart, + endAddress: heapStart + heapDataPagesLen + heapEmptyPagesSize, + data: heapData + ) + + stackZone = Zone( + startAddress: stackStartAddr, + endAddress: UInt32(config.pvmProgramInitStackBaseAddress), + data: { + var stackData = Data(count: Int(stackPageAlignedSize)) + _ = stackData.withUnsafeMutableBytes { bytes in + memset(bytes.baseAddress!, 0, Int(stackPageAlignedSize)) + } + return stackData + }() + ) + + argumentZone = Zone( + startAddress: UInt32(config.pvmProgramInitInputStartAddress), + endAddress: UInt32(config.pvmProgramInitInputStartAddress) + P(argumentDataLen, config), + data: argumentData + ) + + pageMap = PageMap(pageMap: [ + (ZZ, P(readOnlyLen, config), .readOnly), + (heapStart, heapDataPagesLen + heapEmptyPagesSize, .readWrite), + (stackStartAddr, stackPageAlignedSize, .readWrite), + (UInt32(config.pvmProgramInitInputStartAddress), P(argumentDataLen, config), .readOnly), + ], config: config) + + self.config = config + } + + private func getZone(for address: UInt32) throws -> Zone { + if address >= stackZone.startAddress, address < stackZone.endAddress { + return stackZone + } else if address >= heapZone.startAddress, address < heapZone.endAddress { + return heapZone + } else if address >= readOnlyZone.startAddress, address < readOnlyZone.endAddress { + return readOnlyZone + } else if address >= argumentZone.startAddress, address < argumentZone.endAddress { + return argumentZone + } + throw MemoryError.zoneNotFound(address) + } + + private func pad(zone: Zone, requiredSize: Int) { + if requiredSize > zone.data.count { + let oldSize = zone.data.count + let additionalSize = requiredSize - oldSize + + zone.data.count = requiredSize + + zone.data.withUnsafeMutableBytes { bytes in + let zeroPtr = bytes.baseAddress!.advanced(by: oldSize) + memset(zeroPtr, 0, additionalSize) + } + } + } + + public func read(address: UInt32) throws -> UInt8 { + try ensureReadable(address: address, length: 1) + let zone = try getZone(for: address) + let offset = zone.offset(for: address) + if offset < zone.data.count { + return zone.data[relative: offset] + } + return 0 + } + + public func read(address: UInt32, length: Int) throws -> Data { + guard length > 0 else { return Data() } + try ensureReadable(address: address, length: length) + let zone = try getZone(for: address) + let offset = zone.offset(for: address) + + var result = Data(count: length) + let availableBytes = max(0, zone.data.count - offset) + let bytesToCopy = min(length, availableBytes) + + if bytesToCopy > 0 { + result.withUnsafeMutableBytes { resultBytes in + zone.data.withUnsafeBytes { zoneBytes in + let sourcePtr = zoneBytes.baseAddress!.advanced(by: offset) + memcpy(resultBytes.baseAddress!, sourcePtr, bytesToCopy) + } + } + } + + return result + } + + public func write(address: UInt32, value: UInt8) throws { + try ensureWritable(address: address, length: 1) + let zone = try getZone(for: address) + let offset = zone.offset(for: address) + pad(zone: zone, requiredSize: offset + 1) + zone.data[zone.data.startIndex + offset] = value + } + + public func write(address: UInt32, values: Data) throws { + guard !values.isEmpty else { return } + try ensureWritable(address: address, length: values.count) + let zone = try getZone(for: address) + let offset = zone.offset(for: address) + + pad(zone: zone, requiredSize: offset + values.count) + zone.data.withUnsafeMutableBytes { destBytes in + values.withUnsafeBytes { sourceBytes in + let destPtr = destBytes.baseAddress!.advanced(by: offset) + let sourcePtr = sourceBytes.baseAddress! + memcpy(destPtr, sourcePtr, values.count) + } + } + } + + public func sbrk(_ size: UInt32) throws(MemoryError) -> UInt32 { + // NOTE: sbrk will be removed from GP + // NOTE: this impl aligns with w3f traces test vector README + + let prevHeapEnd = heapZone.endAddress + if size == 0 { + return prevHeapEnd + } + + let nextPageBoundary = StandardProgram.alignToPageSize(size: prevHeapEnd, config: config) + heapZone.endAddress += size + + if heapZone.endAddress > nextPageBoundary { + let finalBoundary = heapZone.endAddress + let start = nextPageBoundary / UInt32(config.pvmMemoryPageSize) + let end = finalBoundary / UInt32(config.pvmMemoryPageSize) + let count = Int(end - start + 1) + pageMap.update(pageIndex: start, pages: count, access: .readWrite) + } + + return prevHeapEnd + } +} diff --git a/PolkaVM/Sources/PolkaVM/ProgramCode.swift b/PolkaVM/Sources/PolkaVM/ProgramCode.swift index b312b231..2da95d31 100644 --- a/PolkaVM/Sources/PolkaVM/ProgramCode.swift +++ b/PolkaVM/Sources/PolkaVM/ProgramCode.swift @@ -26,8 +26,16 @@ public class ProgramCode { // parsed stuff public private(set) var basicBlockIndices: Set = [] - private var skipCache: [UInt32: UInt32] = [:] - private var instCache: [UInt32: Instruction] = [:] + + private final class InstRef { + let instruction: Instruction + init(_ instruction: Instruction) { + self.instruction = instruction + } + } + + private var instCache: [InstRef?] = [] + private var blockGasCosts: [UInt32: Gas] = [:] public init(_ blob: Data) throws(Error) { @@ -89,7 +97,6 @@ public class ProgramCode { while i < code.count { let skip = ProgramCode.skip(start: i, bitmask: bitmask) - skipCache[i] = skip let opcode = code[relative: Int(i)] currentBlockGasCost += gasFromOpcode(opcode) @@ -127,20 +134,26 @@ public class ProgramCode { } public func getInstructionAt(pc: UInt32) -> Instruction? { - if let cached = instCache[pc] { - return cached + let pcIndex = Int(pc) + + if instCache.count <= pcIndex { + instCache.append(contentsOf: Array(repeating: nil, count: pcIndex - instCache.count + 1)) + } + + if let cached = instCache[pcIndex] { + return cached.instruction } guard Int(pc) < code.count else { let trapInst = CppHelper.Instructions.Trap() - instCache[pc] = trapInst + instCache[pcIndex] = InstRef(trapInst) return trapInst } do { let skip = skip(pc) let inst = try parseInstruction(startIndex: code.startIndex + Int(pc), skip: skip) - instCache[pc] = inst + instCache[pcIndex] = InstRef(inst) return inst } catch { return nil @@ -152,13 +165,7 @@ public class ProgramCode { } public func skip(_ pc: UInt32) -> UInt32 { - if let cached = skipCache[pc] { - return cached - } - - let skip = ProgramCode.skip(start: pc, bitmask: bitmask) - skipCache[pc] = skip - return skip + ProgramCode.skip(start: pc, bitmask: bitmask) } public static func skip(start: UInt32, bitmask: Data) -> UInt32 { diff --git a/PolkaVM/Sources/PolkaVM/Registers.swift b/PolkaVM/Sources/PolkaVM/Registers.swift index c016a1e7..1ce92ab3 100644 --- a/PolkaVM/Sources/PolkaVM/Registers.swift +++ b/PolkaVM/Sources/PolkaVM/Registers.swift @@ -4,37 +4,41 @@ import Foundation public struct Registers: Equatable { public typealias Index = CppHelper.RegisterIndex - public var reg1: UInt64 = 0 - public var reg2: UInt64 = 0 - public var reg3: UInt64 = 0 - public var reg4: UInt64 = 0 - public var reg5: UInt64 = 0 - public var reg6: UInt64 = 0 - public var reg7: UInt64 = 0 - public var reg8: UInt64 = 0 - public var reg9: UInt64 = 0 - public var reg10: UInt64 = 0 - public var reg11: UInt64 = 0 - public var reg12: UInt64 = 0 - public var reg13: UInt64 = 0 + private var regs: (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64) = ( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ) public init() {} public init(_ values: [UInt64]) { assert(values.count == 13) - reg1 = values[0] - reg2 = values[1] - reg3 = values[2] - reg4 = values[3] - reg5 = values[4] - reg6 = values[5] - reg7 = values[6] - reg8 = values[7] - reg9 = values[8] - reg10 = values[9] - reg11 = values[10] - reg12 = values[11] - reg13 = values[12] + regs = ( + values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + values[10], + values[11], + values[12] + ) } /// standard program init registers @@ -45,69 +49,33 @@ public struct Registers: Equatable { self[Index(raw: 8)] = UInt64(argumentData?.count ?? 0) } + @inline(__always) public subscript(index: Index) -> UInt64 { get { - switch index.value { - case 0: - reg1 - case 1: - reg2 - case 2: - reg3 - case 3: - reg4 - case 4: - reg5 - case 5: - reg6 - case 6: - reg7 - case 7: - reg8 - case 8: - reg9 - case 9: - reg10 - case 10: - reg11 - case 11: - reg12 - case 12: - reg13 - default: - fatalError("unreachable: index out of bounds \(index.value)") + withUnsafePointer(to: regs) { ptr in + ptr.withMemoryRebound(to: UInt64.self, capacity: 13) { arrayPtr in + arrayPtr[Int(index.value)] + } } } set { - switch index.value { - case 0: - reg1 = newValue - case 1: - reg2 = newValue - case 2: - reg3 = newValue - case 3: - reg4 = newValue - case 4: - reg5 = newValue - case 5: - reg6 = newValue - case 6: - reg7 = newValue - case 7: - reg8 = newValue - case 8: - reg9 = newValue - case 9: - reg10 = newValue - case 10: - reg11 = newValue - case 11: - reg12 = newValue - case 12: - reg13 = newValue - default: - fatalError("unreachable: index out of bounds \(index.value)") + withUnsafeMutablePointer(to: ®s) { ptr in + ptr.withMemoryRebound(to: UInt64.self, capacity: 13) { arrayPtr in + arrayPtr[Int(index.value)] = newValue + } + } + } + } + + public static func == (lhs: Registers, rhs: Registers) -> Bool { + withUnsafePointer(to: lhs.regs) { lhsPtr in + withUnsafePointer(to: rhs.regs) { rhsPtr in + memcmp( + lhsPtr, + rhsPtr, + MemoryLayout<(UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64)> + .size + ) == 0 } } } @@ -187,37 +155,10 @@ extension Registers { public mutating func withUnsafeMutableRegistersPointer( _ body: (UnsafeMutablePointer) throws -> R ) rethrows -> R { - // Create a temporary array to hold register values - var tempRegisters: [UInt64] = [ - reg1, reg2, reg3, reg4, - reg5, reg6, reg7, reg8, - reg9, reg10, reg11, reg12, - reg13, - ] - - let result = try tempRegisters.withUnsafeMutableBufferPointer { bufferPointer -> R in - guard let baseAddress = bufferPointer.baseAddress else { - // This should ideally not happen with a non-empty array - fatalError("Could not get base address of temporary register buffer. This indicates a critical issue.") + try withUnsafeMutablePointer(to: ®s) { ptr in + try ptr.withMemoryRebound(to: UInt64.self, capacity: 13) { arrayPtr in + try body(arrayPtr) } - return try body(baseAddress) } - - // Copy values back from the temporary array - reg1 = tempRegisters[0] - reg2 = tempRegisters[1] - reg3 = tempRegisters[2] - reg4 = tempRegisters[3] - reg5 = tempRegisters[4] - reg6 = tempRegisters[5] - reg7 = tempRegisters[6] - reg8 = tempRegisters[7] - reg9 = tempRegisters[8] - reg10 = tempRegisters[9] - reg11 = tempRegisters[10] - reg12 = tempRegisters[11] - reg13 = tempRegisters[12] - - return result } } diff --git a/PolkaVM/Sources/PolkaVM/VMState.swift b/PolkaVM/Sources/PolkaVM/VMState.swift index a36e9dfa..2dbe7e04 100644 --- a/PolkaVM/Sources/PolkaVM/VMState.swift +++ b/PolkaVM/Sources/PolkaVM/VMState.swift @@ -30,6 +30,7 @@ public protocol VMState { func readMemory(address: some FixedWidthInteger, length: Int) throws -> Data func writeMemory(address: some FixedWidthInteger, value: UInt8) throws func writeMemory(address: some FixedWidthInteger, values: some Sequence) throws + func writeMemory(address: some FixedWidthInteger, values: Data) throws func sbrk(_ increment: UInt32) throws -> UInt32 // VM State Control diff --git a/PolkaVM/Sources/PolkaVM/VMStateInterpreter.swift b/PolkaVM/Sources/PolkaVM/VMStateInterpreter.swift index 7e031833..6fc17ce3 100644 --- a/PolkaVM/Sources/PolkaVM/VMStateInterpreter.swift +++ b/PolkaVM/Sources/PolkaVM/VMStateInterpreter.swift @@ -63,16 +63,16 @@ public class VMStateInterpreter: VMState { } public func readMemory(address: some FixedWidthInteger) throws -> UInt8 { - try validateAddress(address) - let res = try memory.read(address: UInt32(truncatingIfNeeded: address)) - return res + let addr = UInt32(truncatingIfNeeded: address) + try validateAddress(addr) + return try memory.read(address: addr) } public func readMemory(address: some FixedWidthInteger, length: Int) throws -> Data { if length == 0 { return Data() } - try validateAddress(address) - let res = try memory.read(address: UInt32(truncatingIfNeeded: address), length: length) - return res + let addr = UInt32(truncatingIfNeeded: address) + try validateAddress(addr) + return try memory.read(address: addr, length: length) } public func isMemoryWritable(address: some FixedWidthInteger, length: Int) -> Bool { @@ -80,15 +80,24 @@ public class VMStateInterpreter: VMState { } public func writeMemory(address: some FixedWidthInteger, value: UInt8) throws { - try validateAddress(address) - try memory.write(address: UInt32(truncatingIfNeeded: address), value: value) + let addr = UInt32(truncatingIfNeeded: address) + try validateAddress(addr) + try memory.write(address: addr, value: value) } public func writeMemory(address: some FixedWidthInteger, values: some Sequence) throws { let data = Data(values) guard !data.isEmpty else { return } - try validateAddress(address) - try memory.write(address: UInt32(truncatingIfNeeded: address), values: data) + let addr = UInt32(truncatingIfNeeded: address) + try validateAddress(addr) + try memory.write(address: addr, values: data) + } + + public func writeMemory(address: some FixedWidthInteger, values: Data) throws { + guard !values.isEmpty else { return } + let addr = UInt32(truncatingIfNeeded: address) + try validateAddress(addr) + try memory.write(address: addr, values: values) } public func sbrk(_ increment: UInt32) throws -> UInt32 { diff --git a/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift b/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift index 2ab7308d..ad6f3e40 100644 --- a/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift +++ b/PolkaVM/Tests/PolkaVMTests/MemoryTests.swift @@ -219,10 +219,13 @@ enum MemoryTests { } @Test func read() throws { - // readonly #expect(throws: MemoryError.notReadable(0)) { try memory.read(address: 0) } - #expect(throws: MemoryError.notReadable(readOnlyStart - 4096)) { try memory.read(address: readOnlyStart - 1) } + #expect(throws: MemoryError.notReadable(readOnlyStart - UInt32(config.pvmMemoryPageSize))) { + try memory.read(address: readOnlyStart - 1) + } #expect(memory.isReadable(address: 0, length: config.pvmProgramInitZoneSize) == false) + + // readonly zone #expect(try memory.read(address: readOnlyStart, length: 4) == Data([1, 2, 3, 0])) #expect(try memory.read(address: readOnlyStart, length: 4) == Data([1, 2, 3, 0])) #expect(throws: MemoryError.notReadable(readOnlyEnd)) { try memory.read( @@ -251,10 +254,14 @@ enum MemoryTests { } @Test func write() throws { - // readonly #expect(throws: MemoryError.notWritable(0)) { try memory.write(address: 0, value: 0) } - #expect(throws: MemoryError.notWritable(readOnlyStart - 4096)) { try memory.write(address: readOnlyStart - 1, value: 0) } + #expect(throws: MemoryError.notWritable(readOnlyStart - UInt32(config.pvmMemoryPageSize))) { try memory.write( + address: readOnlyStart - 1, + value: 0 + ) } #expect(memory.isWritable(address: 0, length: config.pvmProgramInitZoneSize) == false) + + // readonly zone #expect(throws: MemoryError.notWritable(readOnlyStart)) { try memory.write(address: readOnlyStart, value: 4) } #expect(try memory.read(address: readOnlyStart, length: 4) == Data([1, 2, 3, 0])) @@ -292,9 +299,7 @@ enum MemoryTests { #expect(newHeapEnd == initialHeapEnd) let finalBoundary = initialHeapEnd + allocSize - let start = initialHeapEnd / pageSize let end = (finalBoundary + pageSize - 1) / pageSize - let pages = end - start #expect(memory.isWritable(address: initialHeapEnd, length: Int(allocSize)) == true) @@ -357,9 +362,9 @@ enum MemoryTests { @Test func sbrk() throws { let oldEnd = try memory.sbrk(512) - #expect(oldEnd == UInt32(config.pvmMemoryPageSize)) + #expect(oldEnd == UInt32(config.pvmMemoryPageSize) * 2) #expect(memory.isWritable(address: oldEnd, length: config.pvmMemoryPageSize) == true) - #expect(memory.isWritable(address: 0, length: Int(oldEnd)) == true) + #expect(memory.isWritable(address: UInt32(config.pvmMemoryPageSize), length: Int(oldEnd)) == false) try memory.write(address: oldEnd, values: Data([1, 2, 3])) #expect(try memory.read(address: oldEnd - 1, length: 5) == Data([0, 1, 2, 3, 0])) diff --git a/Utils/Sources/Utils/Merklization/StateMerklization.swift b/Utils/Sources/Utils/Merklization/StateMerklization.swift index c76c24ef..7d0a79d4 100644 --- a/Utils/Sources/Utils/Merklization/StateMerklization.swift +++ b/Utils/Sources/Utils/Merklization/StateMerklization.swift @@ -41,10 +41,11 @@ public func stateMerklize(kv: [Data31: Data], i: Int = 0) throws(MerklizeError) /// bit at i, returns true if it is 1 func bit(_ data: Data, _ i: Int) throws(MerklizeError) -> Bool { - guard data.indices.contains(data.startIndex + (i / 8)) else { + let byteIndex = i / 8 + guard byteIndex < data.count else { throw MerklizeError.invalidIndex } - let byte = data[relative: i / 8] + let byte = data[data.startIndex + byteIndex] return (byte & (1 << (7 - (i % 8)))) != 0 } @@ -58,6 +59,9 @@ public func stateMerklize(kv: [Data31: Data], i: Int = 0) throws(MerklizeError) var l: [Data31: Data] = [:] var r: [Data31: Data] = [:] + l.reserveCapacity(kv.count / 2 + 1) + r.reserveCapacity(kv.count / 2 + 1) + for (k, v) in kv { if try bit(k.data, i) { r[k] = v diff --git a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index df1b92eb..db947f7d 100644 --- a/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/boka.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b24bc350cefbc4a11375aec1aa77c5f0b7f73e4db0612fec8566457591c3980d", + "originHash" : "da3a4145b71c3b5b1f41471cd34bba166de8cf00f6f7ec67b36e3b0a8ed35d34", "pins" : [ { "identity" : "async-channels", @@ -55,15 +55,6 @@ "version" : "1.23.0" } }, - { - "identity" : "lrucache", - "kind" : "remoteSourceControl", - "location" : "https://github.com/nicklockwood/LRUCache.git", - "state" : { - "revision" : "542f0449556327415409ededc9c43a4bd0a397dc", - "version" : "1.0.7" - } - }, { "identity" : "multipart-kit", "kind" : "remoteSourceControl",