diff --git a/Makefile b/Makefile index de18717..5d5eb8d 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,9 @@ test: build @swift test \ --parallel \ --explicit-target-dependency-import-check error + @SWIFT_MMIO_FEATURE_INTERPOSABLE=1 swift test \ + --parallel \ + --explicit-target-dependency-import-check error clean: @echo "cleaning..." diff --git a/Package.swift b/Package.swift index 3b7bfad..ebb84e7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,10 @@ // swift-tools-version: 5.9 import CompilerPluginSupport +import Foundation import PackageDescription -let package = Package( +var package = Package( name: "swift-mmio", platforms: [ .macOS(.v10_15), @@ -51,3 +52,37 @@ let package = Package( .target(name: "MMIOVolatile"), ]) + +// Replace this with a native spm feature flag if/when supported +let interposable = "FEATURE_INTERPOSABLE" +try package.defineFeature(named: interposable, override: nil) { package in + let targetAllowSet = Set(["MMIO", "MMIOVolatile", "MMIOMacros"]) + package.targets = package.targets.filter { targetAllowSet.contains($0.name) } + package.targets.append( + .testTarget(name: "MMIOInterposableTests", dependencies: ["MMIO"])) + for target in package.targets { + target.swiftDefine(interposable) + } +} + +extension Package { + func defineFeature( + named featureName: String, + override: Bool?, + body: (Package) throws -> Void + ) throws { + let key = "SWIFT_MMIO_\(featureName)" + let environment = ProcessInfo.processInfo.environment[key] != nil + if override ?? environment { + try body(self) + } + } +} + +extension Target { + func swiftDefine(_ value: String) { + var swiftSettings = self.swiftSettings ?? [] + swiftSettings.append(.define(value)) + self.swiftSettings = swiftSettings + } +} diff --git a/Sources/MMIO/BitFieldProjectable.swift b/Sources/MMIO/BitFieldProjectable.swift index a81a868..06e61c5 100644 --- a/Sources/MMIO/BitFieldProjectable.swift +++ b/Sources/MMIO/BitFieldProjectable.swift @@ -13,32 +13,32 @@ public protocol BitFieldProjectable { static var bitWidth: Int { get } init(storage: Storage) - where Storage: FixedWidthInteger, Storage: UnsignedInteger + where Storage: FixedWidthInteger & UnsignedInteger func storage(_: Storage.Type) -> Storage - where Storage: FixedWidthInteger, Storage: UnsignedInteger + where Storage: FixedWidthInteger & UnsignedInteger } extension Never: BitFieldProjectable { public static var bitWidth: Int { fatalError() } public init(storage: Storage) - where Storage: FixedWidthInteger, Storage: UnsignedInteger { fatalError() } + where Storage: FixedWidthInteger & UnsignedInteger { fatalError() } public func storage(_: Storage.Type) -> Storage - where Storage: FixedWidthInteger, Storage: UnsignedInteger { fatalError() } + where Storage: FixedWidthInteger & UnsignedInteger { fatalError() } } extension Bool: BitFieldProjectable { public static let bitWidth = 1 public init(storage: Storage) - where Storage: FixedWidthInteger, Storage: UnsignedInteger { + where Storage: FixedWidthInteger & UnsignedInteger { self = storage != 0b0 } public func storage(_: Storage.Type) -> Storage - where Storage: FixedWidthInteger, Storage: UnsignedInteger { + where Storage: FixedWidthInteger & UnsignedInteger { self ? 0b1 : 0b0 } } diff --git a/Sources/MMIO/MMIOInterposer.swift b/Sources/MMIO/MMIOInterposer.swift new file mode 100644 index 0000000..c2b525c --- /dev/null +++ b/Sources/MMIO/MMIOInterposer.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if FEATURE_INTERPOSABLE +public protocol MMIOInterposer: AnyObject { + // FIXME: Documentation is wrong + /// Loads an instance of `value` from the address pointed to by pointer. + func load( + from pointer: UnsafePointer + ) -> Value where Value: FixedWidthInteger & UnsignedInteger & _RegisterStorage + + /// Stores an instance of `value` to the address pointed to by pointer. + func store( + _ value: Value, + to pointer: UnsafeMutablePointer + ) where Value: FixedWidthInteger & UnsignedInteger & _RegisterStorage +} +#endif diff --git a/Sources/MMIO/MMIOMacros.swift b/Sources/MMIO/MMIOMacros.swift index f3168c4..1e61c26 100644 --- a/Sources/MMIO/MMIOMacros.swift +++ b/Sources/MMIO/MMIOMacros.swift @@ -10,7 +10,7 @@ //===----------------------------------------------------------------------===// // RegisterBank macros -@attached(member, names: named(unsafeAddress), named(init)) +@attached(member, names: named(unsafeAddress), named(init), named(interposer)) public macro RegisterBank() = #externalMacro(module: "MMIOMacros", type: "RegisterBankMacro") diff --git a/Sources/MMIO/Register.swift b/Sources/MMIO/Register.swift index fd451da..64ba580 100644 --- a/Sources/MMIO/Register.swift +++ b/Sources/MMIO/Register.swift @@ -14,8 +14,12 @@ public struct Register where Value: RegisterValue { public let unsafeAddress: UInt + #if FEATURE_INTERPOSABLE + public var interposer: (any MMIOInterposer)? + #endif + @inlinable @inline(__always) - public init(unsafeAddress: UInt) { + static func preconditionAligned(unsafeAddress: UInt) { let alignment = MemoryLayout.alignment #if hasFeature(Embedded) // FIXME: Embedded doesn't have static interpolated strings yet @@ -27,8 +31,22 @@ public struct Register where Value: RegisterValue { unsafeAddress.isMultiple(of: UInt(alignment)), "Misaligned address '\(unsafeAddress)' for data of type '\(Value.self)'") #endif + } + + #if FEATURE_INTERPOSABLE + @inlinable @inline(__always) + public init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) { + Self.preconditionAligned(unsafeAddress: unsafeAddress) self.unsafeAddress = unsafeAddress + self.interposer = interposer } + #else + @inlinable @inline(__always) + public init(unsafeAddress: UInt) { + Self.preconditionAligned(unsafeAddress: unsafeAddress) + self.unsafeAddress = unsafeAddress + } + #endif } extension Register { @@ -39,12 +57,31 @@ extension Register { @inlinable @inline(__always) public func read() -> Value.Read { - Value.Read(Value.Raw(Value.Raw.Storage.load(from: self.pointer))) + let storage: Value.Raw.Storage + #if FEATURE_INTERPOSABLE + if let interposer = self.interposer { + storage = interposer.load(from: self.pointer) + } else { + storage = Value.Raw.Storage.load(from: self.pointer) + } + #else + storage = Value.Raw.Storage.load(from: self.pointer) + #endif + return Value.Read(Value.Raw(storage)) } @inlinable @inline(__always) public func write(_ newValue: Value.Write) { - Value.Raw.Storage.store(Value.Raw(newValue).storage, to: self.pointer) + let storage = Value.Raw(newValue).storage + #if FEATURE_INTERPOSABLE + if let interposer = self.interposer { + interposer.store(storage, to: self.pointer) + } else { + Value.Raw.Storage.store(storage, to: self.pointer) + } + #else + Value.Raw.Storage.store(storage, to: self.pointer) + #endif } @inlinable @inline(__always) @_disfavoredOverload diff --git a/Sources/MMIO/RegisterValue.swift b/Sources/MMIO/RegisterValue.swift index 9561b86..6a5f490 100644 --- a/Sources/MMIO/RegisterValue.swift +++ b/Sources/MMIO/RegisterValue.swift @@ -35,7 +35,7 @@ extension RegisterValueRead { /// manipulation of the register's bits. /// /// Mutation through the raw view are unchecked. The user is responsible for - /// ensuring the bit pattern is valid. + /// ensuring the bit pattern yielded back to the read view is valid. @_disfavoredOverload @inlinable @inline(__always) public var raw: Value.Raw { @@ -61,7 +61,7 @@ extension RegisterValueWrite { /// manipulation of the register's bits. /// /// Mutation through the raw view are unchecked. The user is responsible for - /// ensuring the bit pattern is valid. + /// ensuring the bit pattern yielded back to the write view is valid. @inlinable @inline(__always) public var raw: Value.Raw { _read { diff --git a/Sources/MMIOMacros/Macros/RegisterBankMacro.swift b/Sources/MMIOMacros/Macros/RegisterBankMacro.swift index 39aa83f..07be988 100644 --- a/Sources/MMIOMacros/Macros/RegisterBankMacro.swift +++ b/Sources/MMIOMacros/Macros/RegisterBankMacro.swift @@ -65,9 +65,23 @@ extension RegisterBankMacro: MMIOMemberMacro { return [ "\(acl)private(set) var unsafeAddress: UInt", """ + #if FEATURE_INTERPOSABLE + var interposer: (any MMIOInterposer)? + #endif + """, + """ + #if FEATURE_INTERPOSABLE + @inlinable @inline(__always) + \(acl)init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) { + self.unsafeAddress = unsafeAddress + self.interposer = interposer + } + #else + @inlinable @inline(__always) \(acl)init(unsafeAddress: UInt) { self.unsafeAddress = unsafeAddress } + #endif """, ] } diff --git a/Sources/MMIOMacros/Macros/RegisterBankOffsetMacro.swift b/Sources/MMIOMacros/Macros/RegisterBankOffsetMacro.swift index 2940ede..f22e2a1 100644 --- a/Sources/MMIOMacros/Macros/RegisterBankOffsetMacro.swift +++ b/Sources/MMIOMacros/Macros/RegisterBankOffsetMacro.swift @@ -121,7 +121,13 @@ extension RegisterBankOffsetMacro: MMIOAccessorMacro { return [ """ - @inlinable @inline(__always) get { .init(unsafeAddress: self.unsafeAddress + (\(raw: self.offset))) } + @inlinable @inline(__always) get { + #if FEATURE_INTERPOSABLE + return .init(unsafeAddress: self.unsafeAddress + (\(raw: self.$offset)), interposer: self.interposer) + #else + return .init(unsafeAddress: self.unsafeAddress + (\(raw: self.$offset))) + #endif + } """ ] } diff --git a/Sources/MMIOMacros/Macros/RegisterDescription.swift b/Sources/MMIOMacros/Macros/RegisterDescription.swift index f2c9817..e642c01 100644 --- a/Sources/MMIOMacros/Macros/RegisterDescription.swift +++ b/Sources/MMIOMacros/Macros/RegisterDescription.swift @@ -99,7 +99,8 @@ extension RegisterDescription { """ \(self.accessLevel)struct Raw: RegisterValueRaw { \(self.accessLevel)typealias Value = \(self.name) - \(self.accessLevel)var storage: UInt\(raw: self.bitWidth) + \(self.accessLevel)typealias Storage = UInt\(raw: self.bitWidth) + \(self.accessLevel)var storage: Storage \(self.accessLevel)init(_ storage: Storage) { self.storage = storage } diff --git a/Tests/MMIOInterposableTests/Extensions/StringInterpolation.swift b/Tests/MMIOInterposableTests/Extensions/StringInterpolation.swift new file mode 100644 index 0000000..655d875 --- /dev/null +++ b/Tests/MMIOInterposableTests/Extensions/StringInterpolation.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension String.StringInterpolation { + mutating func appendInterpolation(hexNibble value: UInt8) { + let ascii: UInt8 + switch value { + case 0..<10: + ascii = UInt8(ascii: "0") + value + case 10..<16: + ascii = UInt8(ascii: "a") + (value - 10) + default: + preconditionFailure("Invalid hexNibble \(value)") + } + let character = Character(UnicodeScalar(ascii)) + self.appendInterpolation(character) + } + + mutating func appendInterpolation( + hex value: Value, + bytes size: Int? = nil + ) where Value: FixedWidthInteger { + let valueSize = MemoryLayout.size + precondition((size ?? 0) <= valueSize) + let size = size ?? valueSize + let sizeIsEven = size.isMultiple(of: 2) + + // Big endian so we can iterate from high to low byte + var value = value.bigEndian + + let droppedBytes = valueSize - size + value >>= 8 * droppedBytes + + self.appendLiteral("0x") + for offset in 0..> 4 + let lowNibble = byte & 0xf + self.appendInterpolation(hexNibble: highNibble) + self.appendInterpolation(hexNibble: lowNibble) + value >>= 8 + } + } +} diff --git a/Tests/MMIOInterposableTests/Extensions/StringInterpolationTests.swift b/Tests/MMIOInterposableTests/Extensions/StringInterpolationTests.swift new file mode 100644 index 0000000..f32d640 --- /dev/null +++ b/Tests/MMIOInterposableTests/Extensions/StringInterpolationTests.swift @@ -0,0 +1,133 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest + +final class StringInterpolationTests: XCTestCase { + func test_appendInterpolation_hexNibble() { + XCTAssertEqual("\(hexNibble: 0x0)", "0") + XCTAssertEqual("\(hexNibble: 0x1)", "1") + XCTAssertEqual("\(hexNibble: 0x2)", "2") + XCTAssertEqual("\(hexNibble: 0x3)", "3") + XCTAssertEqual("\(hexNibble: 0x4)", "4") + XCTAssertEqual("\(hexNibble: 0x5)", "5") + XCTAssertEqual("\(hexNibble: 0x6)", "6") + XCTAssertEqual("\(hexNibble: 0x7)", "7") + XCTAssertEqual("\(hexNibble: 0x8)", "8") + XCTAssertEqual("\(hexNibble: 0x9)", "9") + XCTAssertEqual("\(hexNibble: 0xa)", "a") + XCTAssertEqual("\(hexNibble: 0xb)", "b") + XCTAssertEqual("\(hexNibble: 0xc)", "c") + XCTAssertEqual("\(hexNibble: 0xd)", "d") + XCTAssertEqual("\(hexNibble: 0xe)", "e") + XCTAssertEqual("\(hexNibble: 0xf)", "f") + } + + func test_appendInterpolation_hex() { + // Int8 + XCTAssertEqual("\(hex: Int8.min)", "0x80") + XCTAssertEqual("\(hex: Int8(-1))", "0xff") + XCTAssertEqual("\(hex: Int8(0))", "0x00") + XCTAssertEqual("\(hex: Int8(1))", "0x01") + XCTAssertEqual("\(hex: Int8.max)", "0x7f") + + // Int16 + XCTAssertEqual("\(hex: Int16.min)", "0x8000") + XCTAssertEqual("\(hex: Int16(-1))", "0xffff") + XCTAssertEqual("\(hex: Int16(0))", "0x0000") + XCTAssertEqual("\(hex: Int16(1))", "0x0001") + XCTAssertEqual("\(hex: Int16.max)", "0x7fff") + + // Int32 + XCTAssertEqual("\(hex: Int32.min)", "0x8000_0000") + XCTAssertEqual("\(hex: Int32(-1))", "0xffff_ffff") + XCTAssertEqual("\(hex: Int32(0))", "0x0000_0000") + XCTAssertEqual("\(hex: Int32(1))", "0x0000_0001") + XCTAssertEqual("\(hex: Int32.max)", "0x7fff_ffff") + + // Int64 + XCTAssertEqual("\(hex: Int64.min)", "0x8000_0000_0000_0000") + XCTAssertEqual("\(hex: Int64(-1))", "0xffff_ffff_ffff_ffff") + XCTAssertEqual("\(hex: Int64(0))", "0x0000_0000_0000_0000") + XCTAssertEqual("\(hex: Int64(1))", "0x0000_0000_0000_0001") + XCTAssertEqual("\(hex: Int64.max)", "0x7fff_ffff_ffff_ffff") + + // UInt8 + XCTAssertEqual("\(hex: UInt8.min)", "0x00") + XCTAssertEqual("\(hex: UInt8(1))", "0x01") + XCTAssertEqual("\(hex: UInt8.max)", "0xff") + + // UInt16 + XCTAssertEqual("\(hex: UInt16.min)", "0x0000") + XCTAssertEqual("\(hex: UInt16(1))", "0x0001") + XCTAssertEqual("\(hex: UInt16.max)", "0xffff") + + // UInt32 + XCTAssertEqual("\(hex: UInt32.min)", "0x0000_0000") + XCTAssertEqual("\(hex: UInt32(1))", "0x0000_0001") + XCTAssertEqual("\(hex: UInt32.max)", "0xffff_ffff") + + // UInt64 + XCTAssertEqual("\(hex: UInt64.min)", "0x0000_0000_0000_0000") + XCTAssertEqual("\(hex: UInt64(1))", "0x0000_0000_0000_0001") + XCTAssertEqual("\(hex: UInt64.max)", "0xffff_ffff_ffff_ffff") + } + + func test_appendInterpolation_hex_bytes() { + // Int8 + XCTAssertEqual("\(hex: Int8.min, bytes: 1)", "0x80") + XCTAssertEqual("\(hex: Int8(-1), bytes: 1)", "0xff") + XCTAssertEqual("\(hex: Int8(0), bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int8(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: Int8.max, bytes: 1)", "0x7f") + + // Int16 + XCTAssertEqual("\(hex: Int16.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int16(-1), bytes: 1)", "0xff") + XCTAssertEqual("\(hex: Int16(0), bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int16(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: Int16.max, bytes: 1)", "0xff") + + // Int32 + XCTAssertEqual("\(hex: Int32.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int32(-1), bytes: 1)", "0xff") + XCTAssertEqual("\(hex: Int32(0), bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int32(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: Int32.max, bytes: 1)", "0xff") + + // Int64 + XCTAssertEqual("\(hex: Int64.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int64(-1), bytes: 1)", "0xff") + XCTAssertEqual("\(hex: Int64(0), bytes: 1)", "0x00") + XCTAssertEqual("\(hex: Int64(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: Int64.max, bytes: 1)", "0xff") + + // UInt8 + XCTAssertEqual("\(hex: UInt8.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: UInt8(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: UInt8.max, bytes: 1)", "0xff") + + // UInt16 + XCTAssertEqual("\(hex: UInt16.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: UInt16(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: UInt16.max, bytes: 1)", "0xff") + + // UInt32 + XCTAssertEqual("\(hex: UInt32.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: UInt32(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: UInt32.max, bytes: 1)", "0xff") + + // UInt64 + XCTAssertEqual("\(hex: UInt64.min, bytes: 1)", "0x00") + XCTAssertEqual("\(hex: UInt64(1), bytes: 1)", "0x01") + XCTAssertEqual("\(hex: UInt64.max, bytes: 1)", "0xff") + } +} diff --git a/Tests/MMIOInterposableTests/MMIOInterposableTests.swift b/Tests/MMIOInterposableTests/MMIOInterposableTests.swift new file mode 100644 index 0000000..d9d0b06 --- /dev/null +++ b/Tests/MMIOInterposableTests/MMIOInterposableTests.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest + +@testable import MMIO + +final class MMIOInterposableTests: XCTestCase { + @RegisterBank + struct Example { + @RegisterBank(offset: 0x0) + var regA: Register + @RegisterBank(offset: 0x4) + var regB: Register + } + + @Register(bitWidth: 32) + struct RegA { + @ReadWrite(bits: 0..<1, as: Bool.self) + var en: EN + @Reserved(bits: 1..<32) + var reserved0: Reserved0 + } + + @Register(bitWidth: 16) + struct RegB { + @ReadOnly(bits: 0..<1, as: Bool.self) + var en: EN + @WriteOnly(bits: 1..<2, as: Bool.self) + var rst: RST + @Reserved(bits: 3..<16) + var reserved0: Reserved0 + } + + func test_registerBank_passesMmioInterposerChildren() { + let interposer = MMIOTracingInterposer() + let bank = Example(unsafeAddress: 0x1000, interposer: interposer) + XCTAssertTrue(bank.interposer === bank.regA.interposer) + } + + func test_tracingInterposer_producesExpectedTrace() { + let interposer = MMIOTracingInterposer() + let bank = Example(unsafeAddress: 0x1000, interposer: interposer) + _ = bank.regA.read() + bank.regA.modify { $0.en = true } + bank.regA.write(unsafeBitCast(UInt32(0x5a5a_a5a5), to: RegA.ReadWrite.self)) + + bank.regB.modify { r, w in + w.rst = true + w.raw.en = 1 + } + bank.regB.modify { r, w in + w.rst = false + } + XCTAssertMMIOInterposerTrace( + interposer: interposer, + trace: [ + .load(of: UInt32(0), from: 0x1000), + .load(of: UInt32(0), from: 0x1000), + .store(of: UInt32(1), to: 0x1000), + .store(of: UInt32(0x5a5a_a5a5), to: 0x1000), + .load(of: UInt16(0x0000), from: 0x1004), + .store(of: UInt16(0x0003), to: 0x1004), + .load(of: UInt16(0x0003), from: 0x1004), + .store(of: UInt16(0x0001), to: 0x1004), + ]) + } +} diff --git a/Tests/MMIOInterposableTests/MMIOTracingInterposer.swift b/Tests/MMIOInterposableTests/MMIOTracingInterposer.swift new file mode 100644 index 0000000..13d0349 --- /dev/null +++ b/Tests/MMIOInterposableTests/MMIOTracingInterposer.swift @@ -0,0 +1,180 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import MMIO +import XCTest + +class MMIOTracingInterposer { + // This could be made more efficient by mapping to 8 byte blocks. + // Note: memory is stored as little endian + var memory: [UInt: UInt8] + var trace: [MMIOTracingInterposerEvent] + + init() { + self.memory = .init() + self.trace = .init() + } +} + +extension MMIOTracingInterposer: MMIOInterposer { + func load( + from pointer: UnsafePointer + ) -> Value where Value: FixedWidthInteger & UnsignedInteger { + XCTAssertMMIOAlignment(pointer: pointer) + + let address = UInt(bitPattern: pointer) + let size = MemoryLayout.size + + var value = Value(0) + for offset in 0..( + _ value: Value, + to pointer: UnsafeMutablePointer + ) where Value: FixedWidthInteger & UnsignedInteger { + XCTAssertMMIOAlignment(pointer: pointer) + + let address = UInt(bitPattern: pointer) + let size = MemoryLayout.size + + var storedValue = value + for offset in 0..>= 8 + } + + self.trace.append( + .init( + load: false, + address: address, + size: size, + value: UInt64(value))) + } +} + +// swift-format-ignore: AlwaysUseLowerCamelCase +func XCTAssertMMIOAlignment( + pointer: UnsafePointer, + file: StaticString = #file, + line: UInt = #line +) where Value: FixedWidthInteger & UnsignedInteger { + let address = UInt(bitPattern: pointer) + let alignment = UInt(MemoryLayout.alignment) + if !address.isMultiple(of: alignment) { + XCTFail( + """ + Invalid load or store of type '\(Value.self)' from unaligned address \ + '\(hex: UInt(bitPattern: pointer))' + """, + file: file, + line: line) + } +} + +// swift-format-ignore: AlwaysUseLowerCamelCase +func XCTAssertMMIOInterposerTrace( + interposer: MMIOTracingInterposer, + trace: [MMIOTracingInterposerEvent], + file: StaticString = #file, + line: UInt = #line +) { + // Exit early if the actual trace matches the expected trace. + let actualTrace = interposer.trace + let expectedTrace = trace + guard actualTrace != expectedTrace else { return } + + let failureMessage = formatTraceDiff( + expectedTrace: expectedTrace, + actualTrace: actualTrace) + + XCTFail(failureMessage, file: file, line: line) +} + +func formatTraceDiff( + expectedTrace: [MMIOTracingInterposerEvent], + actualTrace: [MMIOTracingInterposerEvent], + simple: Bool = false +) -> String { + assert(expectedTrace != actualTrace) + + let failureMessage: String + + // Use `CollectionDifference` on supported platforms to get `diff`-like + // line-based output. On older platforms, fall back to simple string + // comparison. + if !simple, #available(macOS 10.15, *) { + let difference = actualTrace.difference(from: expectedTrace) + + var result = "" + + var insertions = [Int: MMIOTracingInterposerEvent]() + var removals = [Int: MMIOTracingInterposerEvent]() + + for change in difference { + switch change { + case .insert(let offset, let element, _): + insertions[offset] = element + case .remove(let offset, let element, _): + removals[offset] = element + } + } + + var expectedLine = 0 + var actualLine = 0 + + while expectedLine < expectedTrace.count || actualLine < actualTrace.count { + if let removal = removals[expectedLine] { + result += "-\(removal)\n" + expectedLine += 1 + } else if let insertion = insertions[actualLine] { + result += "+\(insertion)\n" + actualLine += 1 + } else { + result += " \(expectedTrace[expectedLine])\n" + expectedLine += 1 + actualLine += 1 + } + } + + result.removeLast() + failureMessage = """ + Actual trace (+) differed from expected trace (-): + \(result) + """ + } else { + // Fall back to simple message on platforms that don't support + // CollectionDifference. + failureMessage = """ + Actual trace differed from expected trace: + Actual: + \(actualTrace.map(\.description).joined(separator: "\n")) + + Expected: + \(expectedTrace.map(\.description).joined(separator: "\n")) + """ + } + + return failureMessage +} diff --git a/Tests/MMIOInterposableTests/MMIOTracingInterposerEvent.swift b/Tests/MMIOInterposableTests/MMIOTracingInterposerEvent.swift new file mode 100644 index 0000000..388521e --- /dev/null +++ b/Tests/MMIOInterposableTests/MMIOTracingInterposerEvent.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import MMIO + +struct MMIOTracingInterposerEvent { + var load: Bool + var address: UInt + var size: Int + var value: UInt64 +} + +extension MMIOTracingInterposerEvent { + static func load( + of value: Value, + from address: UInt + ) -> Self where Value: FixedWidthInteger & UnsignedInteger { + Self( + load: true, + address: address, + size: MemoryLayout.size, + value: UInt64(value)) + } + + static func store( + of value: Value, + to address: UInt + ) -> Self where Value: FixedWidthInteger & UnsignedInteger { + Self( + load: false, + address: address, + size: MemoryLayout.size, + value: UInt64(value)) + } +} + +extension MMIOTracingInterposerEvent: Equatable {} + +extension MMIOTracingInterposerEvent: CustomStringConvertible { + var description: String { + switch self.load { + case true: + "m[\(hex: self.address)] -> \(hex: self.value, bytes: self.size)" + case false: + "m[\(hex: self.address)] <- \(hex: self.value, bytes: self.size)" + } + } +} diff --git a/Tests/MMIOInterposableTests/MMIOTracingInterposerEventTests.swift b/Tests/MMIOInterposableTests/MMIOTracingInterposerEventTests.swift new file mode 100644 index 0000000..7a2a09f --- /dev/null +++ b/Tests/MMIOInterposableTests/MMIOTracingInterposerEventTests.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest + +final class MMIOTracingInterposerEventTests: XCTestCase { + func test_load() { + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt8(1), from: 0x10), + MMIOTracingInterposerEvent(load: true, address: 0x10, size: 1, value: 1)) + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt16(2), from: 0x10), + MMIOTracingInterposerEvent(load: true, address: 0x10, size: 2, value: 2)) + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt32(3), from: 0x10), + MMIOTracingInterposerEvent(load: true, address: 0x10, size: 4, value: 3)) + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt64(4), from: 0x10), + MMIOTracingInterposerEvent(load: true, address: 0x10, size: 8, value: 4)) + } + + func test_store() { + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt8(1), to: 0x10), + MMIOTracingInterposerEvent(load: false, address: 0x10, size: 1, value: 1)) + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt16(2), to: 0x10), + MMIOTracingInterposerEvent(load: false, address: 0x10, size: 2, value: 2)) + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt32(3), to: 0x10), + MMIOTracingInterposerEvent(load: false, address: 0x10, size: 4, value: 3)) + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt64(4), to: 0x10), + MMIOTracingInterposerEvent(load: false, address: 0x10, size: 8, value: 4)) + } + + func test_description() { + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt8(1), from: 0x10).description, + "m[0x0000_0000_0000_0010] -> 0x01") + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt16(2), from: 0x10).description, + "m[0x0000_0000_0000_0010] -> 0x0002") + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt32(3), from: 0x10).description, + "m[0x0000_0000_0000_0010] -> 0x0000_0003") + XCTAssertEqual( + MMIOTracingInterposerEvent.load(of: UInt64(4), from: 0x10).description, + "m[0x0000_0000_0000_0010] -> 0x0000_0000_0000_0004") + + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt8(1), to: 0x10).description, + "m[0x0000_0000_0000_0010] <- 0x01") + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt16(2), to: 0x10).description, + "m[0x0000_0000_0000_0010] <- 0x0002") + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt32(3), to: 0x10).description, + "m[0x0000_0000_0000_0010] <- 0x0000_0003") + XCTAssertEqual( + MMIOTracingInterposerEvent.store(of: UInt64(4), to: 0x10).description, + "m[0x0000_0000_0000_0010] <- 0x0000_0000_0000_0004") + } +} diff --git a/Tests/MMIOInterposableTests/MMIOTracingInterposerTests.swift b/Tests/MMIOInterposableTests/MMIOTracingInterposerTests.swift new file mode 100644 index 0000000..de1395a --- /dev/null +++ b/Tests/MMIOInterposableTests/MMIOTracingInterposerTests.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift MMIO open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest + +final class MMIOTracingInterposerTests: XCTestCase { + func test_memory_load() { + let interposer = MMIOTracingInterposer() + interposer.memory[0x10] = 0x5a + interposer.memory[0x11] = 0xa5 + let value: UInt16 = interposer.load(from: .init(bitPattern: 0x10)!) + XCTAssertEqual(value, 0xa55a) + + XCTAssertEqual( + interposer.trace, + [ + .load(of: UInt16(0xa55a), from: 0x10) + ]) + } + + func test_memory_store() { + let interposer = MMIOTracingInterposer() + interposer.store( + UInt16(0xa55a), + to: .init(bitPattern: 0x10)!) + XCTAssertEqual(interposer.memory[0x10], 0x5a) + XCTAssertEqual(interposer.memory[0x11], 0xa5) + XCTAssertEqual( + interposer.trace, + [ + .store(of: UInt16(0xa55a), to: 0x10) + ]) + } + + func test_trace_diff() { + XCTAssertEqual( + formatTraceDiff( + expectedTrace: [ + .store(of: UInt8(0xa5), to: 0x10), + .load(of: UInt8(0x5a), from: 0x20), + ], + actualTrace: [ + .load(of: UInt8(0x5a), from: 0x20), + .load(of: UInt8(0xa6), from: 0x30), + ]), + """ + Actual trace (+) differed from expected trace (-): + -m[0x0000_0000_0000_0010] <- 0xa5 + m[0x0000_0000_0000_0020] -> 0x5a + +m[0x0000_0000_0000_0030] -> 0xa6 + """) + + XCTAssertEqual( + formatTraceDiff( + expectedTrace: [ + .store(of: UInt8(0xa5), to: 0x10), + .load(of: UInt8(0x5a), from: 0x20), + ], + actualTrace: [ + .load(of: UInt8(0x5a), from: 0x20), + .load(of: UInt8(0xa6), from: 0x30), + ], + simple: true), + """ + Actual trace differed from expected trace: + Actual: + m[0x0000_0000_0000_0020] -> 0x5a + m[0x0000_0000_0000_0030] -> 0xa6 + + Expected: + m[0x0000_0000_0000_0010] <- 0xa5 + m[0x0000_0000_0000_0020] -> 0x5a + """) + } + + func test_XCTAssertMMIOAlignment_pass() { + XCTAssertMMIOAlignment(pointer: UnsafePointer(bitPattern: 0x2)!) + } + + func test_XCTAssertMMIOAlignment_fail() { + XCTExpectFailure("testing negative case") + XCTAssertMMIOAlignment(pointer: UnsafePointer(bitPattern: 0x1)!) + } + + func test_XCTAssertMMIOInterposerTrace_pass() { + let interposer = MMIOTracingInterposer() + interposer.trace = [ + .store(of: UInt8(0xa5), to: 0x10), + .load(of: UInt8(0x5a), from: 0x20), + ] + XCTAssertMMIOInterposerTrace( + interposer: interposer, + trace: [ + .store(of: UInt8(0xa5), to: 0x10), + .load(of: UInt8(0x5a), from: 0x20), + ]) + } + + func test_XCTAssertMMIOInterposerTrace_fail() { + XCTExpectFailure("testing negative case") + let interposer = MMIOTracingInterposer() + interposer.trace = [ + .store(of: UInt8(0xa5), to: 0x10), + .load(of: UInt8(0x5a), from: 0x20), + ] + XCTAssertMMIOInterposerTrace( + interposer: interposer, + trace: [ + .load(of: UInt8(0x5a), from: 0x20), + .load(of: UInt8(0xa6), from: 0x30), + ]) + } +} diff --git a/Tests/MMIOMacrosTests/Macros/RegisterBankAndOffsetMacroTests.swift b/Tests/MMIOMacrosTests/Macros/RegisterBankAndOffsetMacroTests.swift index 6e440de..31402af 100644 --- a/Tests/MMIOMacrosTests/Macros/RegisterBankAndOffsetMacroTests.swift +++ b/Tests/MMIOMacrosTests/Macros/RegisterBankAndOffsetMacroTests.swift @@ -38,20 +38,41 @@ final class RegisterBankAndOffsetMacroTests: XCTestCase { struct I2C { var control: Control { @inlinable @inline(__always) get { - .init(unsafeAddress: self.unsafeAddress + (0)) + #if FEATURE_INTERPOSABLE + return .init(unsafeAddress: self.unsafeAddress + (0x0), interposer: self.interposer) + #else + return .init(unsafeAddress: self.unsafeAddress + (0x0)) + #endif } } var dr: Register { @inlinable @inline(__always) get { - .init(unsafeAddress: self.unsafeAddress + (8)) + #if FEATURE_INTERPOSABLE + return .init(unsafeAddress: self.unsafeAddress + (0x8), interposer: self.interposer) + #else + return .init(unsafeAddress: self.unsafeAddress + (0x8)) + #endif } } private (set) var unsafeAddress: UInt + #if FEATURE_INTERPOSABLE + var interposer: (any MMIOInterposer)? + #endif + + #if FEATURE_INTERPOSABLE + @inlinable @inline(__always) + init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) { + self.unsafeAddress = unsafeAddress + self.interposer = interposer + } + #else + @inlinable @inline(__always) init(unsafeAddress: UInt) { self.unsafeAddress = unsafeAddress } + #endif } """, macros: Self.macros, diff --git a/Tests/MMIOMacrosTests/Macros/RegisterBankMacroTests.swift b/Tests/MMIOMacrosTests/Macros/RegisterBankMacroTests.swift index 08edbf7..d2b67a3 100644 --- a/Tests/MMIOMacrosTests/Macros/RegisterBankMacroTests.swift +++ b/Tests/MMIOMacrosTests/Macros/RegisterBankMacroTests.swift @@ -133,9 +133,22 @@ final class RegisterBankMacroTests: XCTestCase { private (set) var unsafeAddress: UInt + #if FEATURE_INTERPOSABLE + var interposer: (any MMIOInterposer)? + #endif + + #if FEATURE_INTERPOSABLE + @inlinable @inline(__always) + init(unsafeAddress: UInt, interposer: (any MMIOInterposer)?) { + self.unsafeAddress = unsafeAddress + self.interposer = interposer + } + #else + @inlinable @inline(__always) init(unsafeAddress: UInt) { self.unsafeAddress = unsafeAddress } + #endif } """, diagnostics: [], diff --git a/Tests/MMIOMacrosTests/Macros/RegisterBankOffsetMacroTests.swift b/Tests/MMIOMacrosTests/Macros/RegisterBankOffsetMacroTests.swift index 7025bc1..3ae82b2 100644 --- a/Tests/MMIOMacrosTests/Macros/RegisterBankOffsetMacroTests.swift +++ b/Tests/MMIOMacrosTests/Macros/RegisterBankOffsetMacroTests.swift @@ -250,7 +250,11 @@ final class RegisterBankOffsetMacroTests: XCTestCase { expandedSource: """ var a: Reg { @inlinable @inline(__always) get { - .init(unsafeAddress: self.unsafeAddress + (0)) + #if FEATURE_INTERPOSABLE + return .init(unsafeAddress: self.unsafeAddress + (0x0), interposer: self.interposer) + #else + return .init(unsafeAddress: self.unsafeAddress + (0x0)) + #endif } } """, @@ -266,7 +270,11 @@ final class RegisterBankOffsetMacroTests: XCTestCase { expandedSource: """ var a: Swift.Int { @inlinable @inline(__always) get { - .init(unsafeAddress: self.unsafeAddress + (0)) + #if FEATURE_INTERPOSABLE + return .init(unsafeAddress: self.unsafeAddress + (0x0), interposer: self.interposer) + #else + return .init(unsafeAddress: self.unsafeAddress + (0x0)) + #endif } } """, diff --git a/Tests/MMIOMacrosTests/Macros/RegisterMacroTests.swift b/Tests/MMIOMacrosTests/Macros/RegisterMacroTests.swift index c7f2e3f..0015b67 100644 --- a/Tests/MMIOMacrosTests/Macros/RegisterMacroTests.swift +++ b/Tests/MMIOMacrosTests/Macros/RegisterMacroTests.swift @@ -155,7 +155,8 @@ final class RegisterMacroTests: XCTestCase { struct Raw: RegisterValueRaw { typealias Value = S - var storage: UInt8 + typealias Storage = UInt8 + var storage: Storage init(_ storage: Storage) { self.storage = storage } @@ -233,7 +234,8 @@ final class RegisterMacroTests: XCTestCase { struct Raw: RegisterValueRaw { typealias Value = S - var storage: UInt8 + typealias Storage = UInt8 + var storage: Storage init(_ storage: Storage) { self.storage = storage } @@ -326,7 +328,8 @@ final class RegisterMacroTests: XCTestCase { struct Raw: RegisterValueRaw { typealias Value = S - var storage: UInt8 + typealias Storage = UInt8 + var storage: Storage init(_ storage: Storage) { self.storage = storage } @@ -415,7 +418,8 @@ final class RegisterMacroTests: XCTestCase { struct Raw: RegisterValueRaw { typealias Value = S - var storage: UInt8 + typealias Storage = UInt8 + var storage: Storage init(_ storage: Storage) { self.storage = storage } @@ -509,7 +513,8 @@ final class RegisterMacroTests: XCTestCase { struct Raw: RegisterValueRaw { typealias Value = S - var storage: UInt8 + typealias Storage = UInt8 + var storage: Storage init(_ storage: Storage) { self.storage = storage }