Skip to content

Instantly share code, notes, and snippets.

@daltonclaybrook
Created March 16, 2025 23:41
Show Gist options
  • Save daltonclaybrook/98148224902217a9235e3eed0ab70e21 to your computer and use it in GitHub Desktop.
Save daltonclaybrook/98148224902217a9235e3eed0ab70e21 to your computer and use it in GitHub Desktop.
/// 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")
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
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)
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()
}
}
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