Created
March 16, 2025 23:41
-
-
Save daltonclaybrook/98148224902217a9235e3eed0ab70e21 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// A macro that produces an expression to initialize an `IntegerPair` after validating that the bit | |
/// width of `Second` is a positive multiple of `First`. | |
@freestanding(expression) | |
public macro IntegerPair<First: UnsignedInteger, Second: UnsignedInteger>( | |
_ firstType: First.Type, | |
_ secondType: Second.Type | |
) -> IntegerPair<First, Second> = #externalMacro(module: "BitWidthMacros", type: "IntegerPairMacro") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftSyntax | |
import SwiftSyntaxBuilder | |
import SwiftSyntaxMacros | |
import SwiftSyntaxMacrosTestSupport | |
import XCTest | |
#if canImport(BitWidthMacros) | |
import BitWidthMacros | |
let testMacros: [String: Macro.Type] = [ | |
"IntegerPair": IntegerPairMacro.self | |
] | |
final class BitWidthTests: XCTestCase { | |
func test_ifSecondIsPositiveMultiple_macroSucceeds() { | |
assertMacroExpansion( | |
""" | |
let pair = #IntegerPair(UInt32.self, UInt64.self) | |
""", | |
expandedSource: """ | |
let pair = IntegerPair<UInt32, UInt64>._createUnsafelyByMacro() | |
""", | |
macros: testMacros | |
) | |
} | |
func test_ifTypesAreTheSame_macroFails() { | |
assertMacroExpansion( | |
""" | |
let pair = #IntegerPair(UInt32.self, UInt32.self) | |
""", | |
expandedSource: """ | |
let pair = #IntegerPair(UInt32.self, UInt32.self) | |
""", | |
diagnostics: [ | |
DiagnosticSpec(message: "Expected the second integer type (UInt32) to have a bit width that is a positive multiple of the first integer type (UInt32). The bit width of UInt32 is 32, and the bit width of UInt32 is 32.", line: 1, column: 12) | |
], | |
macros: testMacros | |
) | |
} | |
func test_ifSecondIsNotAPositiveMultiple_macroFails() { | |
assertMacroExpansion( | |
""" | |
let pair = #IntegerPair(UInt16.self, UInt8.self) | |
""", | |
expandedSource: """ | |
let pair = #IntegerPair(UInt16.self, UInt8.self) | |
""", | |
diagnostics: [ | |
DiagnosticSpec(message: "Expected the second integer type (UInt8) to have a bit width that is a positive multiple of the first integer type (UInt16). The bit width of UInt8 is 8, and the bit width of UInt16 is 16.", line: 1, column: 12) | |
], | |
macros: testMacros | |
) | |
} | |
func test_ifTypeAreUnsupported_macroFails() { | |
assertMacroExpansion( | |
""" | |
let pair = #IntegerPair(String.self, UInt64.self) | |
""", | |
expandedSource: """ | |
let pair = #IntegerPair(String.self, UInt64.self) | |
""", | |
diagnostics: [ | |
DiagnosticSpec(message: "The provided identifier is not supported: String", line: 1, column: 12) | |
], | |
macros: testMacros | |
) | |
} | |
} | |
#endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import BitWidth | |
let pair1 = #IntegerPair(UInt32.self, UInt64.self) | |
// Error: Expected the second integer type (UInt8) to have a bit width that is a positive multiple of the first integer type (UInt16). The bit width of UInt8 is 8, and the bit width of UInt16 is 16. | |
let pair2 = #IntegerPair(UInt16.self, UInt8.self) | |
// Error: Macro 'IntegerPair' requires that 'Bool' conform to 'UnsignedInteger' | |
let pair3 = #IntegerPair(String.self, Bool.self) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct IntegerPair<First: UnsignedInteger, Second: UnsignedInteger> { | |
fileprivate init() {} | |
} | |
extension IntegerPair { | |
/// This function is used by the macro implementation to create an `IntegerPair` after the types | |
/// have been validated. Do not call this function directly. | |
public static func _createUnsafelyByMacro() -> IntegerPair<First, Second> { | |
IntegerPair() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftCompilerPlugin | |
import SwiftDiagnostics | |
import SwiftSyntax | |
import SwiftSyntaxMacros | |
public struct IntegerPairMacro: ExpressionMacro { | |
public static func expansion( | |
of node: some FreestandingMacroExpansionSyntax, | |
in context: some MacroExpansionContext | |
) throws -> ExprSyntax { | |
let arguments = Array(node.arguments) | |
guard | |
arguments.count == 2, | |
let first = arguments[0].typeName, | |
let second = arguments[1].typeName | |
else { | |
throw IntegerPairMacroError.unexpectedArguments | |
} | |
let firstType = try unsignedIntTypeFromIdentifier(first) | |
let secondType = try unsignedIntTypeFromIdentifier(second) | |
guard secondType.bitWidth > firstType.bitWidth && secondType.bitWidth.isMultiple(of: firstType.bitWidth) else { | |
throw IntegerPairMacroError.secondBitWidthIsNotAMultipleOfFirst(first: firstType, second: secondType) | |
} | |
return "IntegerPair<\(firstType), \(secondType)>._createUnsafelyByMacro()" | |
} | |
// MARK: - Private helpers | |
private static func unsignedIntTypeFromIdentifier(_ identifier: Identifier) throws -> (any FixedWidthInteger.Type) { | |
switch identifier.name { | |
case "UInt8": | |
return UInt8.self | |
case "UInt16": | |
return UInt16.self | |
case "UInt32": | |
return UInt32.self | |
case "UInt64": | |
return UInt64.self | |
case "UInt128": | |
if #available(macOS 15.0, iOS 18.0, *) { | |
return UInt128.self | |
} else { | |
throw IntegerPairMacroError.unsupportedIdentifier(identifier) | |
} | |
default: | |
throw IntegerPairMacroError.unsupportedIdentifier(identifier) | |
} | |
} | |
} | |
enum IntegerPairMacroError: Error, DiagnosticMessage { | |
case unexpectedArguments | |
case unsupportedIdentifier(Identifier) | |
case secondBitWidthIsNotAMultipleOfFirst(first: any FixedWidthInteger.Type, second: any FixedWidthInteger.Type) | |
var message: String { | |
switch self { | |
case .unexpectedArguments: | |
return "This macro can only take arguments that are type identifier literals" | |
case .unsupportedIdentifier(let identifier): | |
return "The provided identifier is not supported: \(identifier.name)" | |
case .secondBitWidthIsNotAMultipleOfFirst(let first, let second): | |
return "Expected the second integer type (\(second)) to have a bit width that is a positive multiple of the first integer type (\(first)). The bit width of \(second) is \(second.bitWidth), and the bit width of \(first) is \(first.bitWidth)." | |
} | |
} | |
var diagnosticID: MessageID { | |
MessageID(domain: "BitWidthMacros", id: String(describing: self)) | |
} | |
var severity: DiagnosticSeverity { | |
.error | |
} | |
} | |
extension LabeledExprListSyntax.Element { | |
fileprivate var typeName: Identifier? { | |
expression.as(MemberAccessExprSyntax.self)?.base?.as(DeclReferenceExprSyntax.self)?.baseName.identifier | |
} | |
} | |
@main | |
struct BitWidthPlugin: CompilerPlugin { | |
let providingMacros: [Macro.Type] = [ | |
IntegerPairMacro.self | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment