Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", .upToNextMajor(from: "602.0.0")),
.package(url: "https://github.com/FlickerSoul/swift-binary-parsing", branch: "main"),
.package(
url: "https://github.com/apple/swift-collections.git",
.upToNextMinor(from: "1.1.0"),
),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.5"),
.package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.6.4"),
// .package(url: "https://github.com/stackotter/swift-macro-toolkit.git", from: "0.7.2"),
],
targets: [
.macro(
Expand All @@ -37,6 +35,7 @@ let package = Package(
.product(name: "BinaryParsing", package: "swift-binary-parsing"),
.product(name: "Collections", package: "swift-collections"),
.target(name: "BinaryParseKitCommons"),
// .product(name: "MacroToolkit", package: "swift-macro-toolkit"),
],
),
.target(
Expand Down Expand Up @@ -77,13 +76,13 @@ let package = Package(
"BinaryParseKitMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
.product(name: "MacroTesting", package: "swift-macro-testing"),
"BinaryParseKitCommons",
],
),
.testTarget(
name: "BinaryParseKitTests",
dependencies: [
"BinaryParseKit",
"BinaryParseKitCommons",
],
),
],
Expand Down
10 changes: 8 additions & 2 deletions Sources/BinaryParseKit/BinaryParseKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,10 @@ public macro parseRest(endianness: Endianness) = #externalMacro(
/// let header = try FileHeader(parsing: data)
/// ```
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
public macro ParseStruct() = #externalMacro(
public macro ParseStruct(
parsingAccessor: ExtensionAccessor = .follow,
printingAccessor: ExtensionAccessor = .follow,
) = #externalMacro(
module: "BinaryParseKitMacros",
type: "ConstructStructParseMacro",
)
Expand All @@ -300,7 +303,10 @@ public macro ParseStruct() = #externalMacro(
/// - Note: Only one `@matchDefault` case is allowed per enum, and has to be declared at the end of all other cases.
/// - Note: any `match` macro has to proceed `parse` and `skip` macros.
@attached(extension, conformances: BinaryParseKit.Parsable, BinaryParseKit.Printable, names: arbitrary)
public macro ParseEnum() = #externalMacro(
public macro ParseEnum(
parsingAccessor: ExtensionAccessor = .follow,
printingAccessor: ExtensionAccessor = .follow,
) = #externalMacro(
module: "BinaryParseKitMacros",
type: "ConstructEnumParseMacro",
)
Expand Down
42 changes: 42 additions & 0 deletions Sources/BinaryParseKitCommons/ExtensionAccessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// ExtensionAccessor.swift
// BinaryParseKit
//
// Created by Larry Zeng on 11/26/25.
//

public enum ExtensionAccessor: ExpressibleByUnicodeScalarLiteral, Sendable, Codable {
public typealias UnicodeScalarLiteralType = String

case `public`
case package
case `internal`
case `fileprivate`
case `private`
case follow
case unknown(String)

public static var allowedCases: [ExtensionAccessor] {
[.public, .package, .internal, .fileprivate, .private, .follow]
}

public var description: String {
switch self {
case .public: "public"
case .package: "package"
case .internal: "internal"
case .fileprivate: "fileprivate"
case .private: "private"
case .follow: "follow"
case let .unknown(value): "unknown(\(value))"
}
}

public init(unicodeScalarLiteral value: String) {
self = ExtensionAccessor
.allowedCases
.first { access in
access.description == value
} ?? .unknown(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// ExtensionAccessor+.swift
// BinaryParseKit
//
// Created by Larry Zeng on 11/26/25.
//

import BinaryParseKitCommons
import SwiftSyntax

extension ExtensionAccessor {
func getAccessorToken(defaultAccessor: TokenKind) -> TokenKind? {
switch self {
case .public: .keyword(.public)
case .package: .keyword(.package)
case .internal: .keyword(.internal)
case .fileprivate: .keyword(.fileprivate)
case .private: .keyword(.private)
case .follow: defaultAccessor
case .unknown: nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SwiftSyntaxMacros

public struct ConstructEnumParseMacro: ExtensionMacro {
public static func expansion(
of _: SwiftSyntax.AttributeSyntax,
of attributeNode: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo _: [SwiftSyntax.TypeSyntax],
Expand All @@ -23,6 +23,12 @@ public struct ConstructEnumParseMacro: ExtensionMacro {
throw ParseEnumMacroError.onlyEnumsAreSupported
}

let accessorInfo = try extractAccessor(
from: attributeNode,
attachedTo: enumDeclaration,
in: context,
)

let visitor = ParseEnumCase(context: context)
visitor.walk(enumDeclaration)
try visitor.validate()
Expand All @@ -31,12 +37,10 @@ public struct ConstructEnumParseMacro: ExtensionMacro {
throw ParseEnumMacroError.unexpectedError(description: "Macro analysis finished without info")
}

let modifiers = declaration.modifiers

let parsingExtension =
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.parsableProtocol)") {
try InitializerDeclSyntax(
"\(modifiers)init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
"\(accessorInfo.parsingAccessor) init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
) {
for caseParseInfo in parseInfo.caseParseInfo {
let toBeMatched = caseParseInfo.bytesToMatch(of: type)
Expand Down Expand Up @@ -102,7 +106,7 @@ public struct ConstructEnumParseMacro: ExtensionMacro {

let printerExtension =
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.printableProtocol)") {
try FunctionDeclSyntax("\(modifiers)func printerIntel() throws -> PrinterIntel") {
try FunctionDeclSyntax("\(accessorInfo.printingAccessor) func printerIntel() throws -> PrinterIntel") {
try SwitchExprSyntax("switch self") {
for caseParseInfo in parseInfo.caseParseInfo {
var parseSkipMacroInfo: [PrintableFieldInfo] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,32 @@ import SwiftSyntaxMacros

public struct ConstructStructParseMacro: ExtensionMacro {
public static func expansion(
of _: SwiftSyntax.AttributeSyntax,
of attributeNode: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo _: [SwiftSyntax.TypeSyntax],
in context: some SwiftSyntaxMacros.MacroExpansionContext,
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
guard let structDeclaration = declaration.as(StructDeclSyntax.self) else {
let error = ParseStructMacroError.onlyStructsAreSupported
throw error
throw ParseStructMacroError.onlyStructsAreSupported
}

let accessorInfo = try extractAccessor(
from: attributeNode,
attachedTo: structDeclaration,
in: context,
)

let structFieldInfo = ParseStructField(context: context)
structFieldInfo.walk(structDeclaration)
try structFieldInfo.validate(for: structDeclaration)

let type = TypeSyntax(type)
let modifiers = declaration.modifiers

let extensionSyntax =
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.parsableProtocol)") {
try InitializerDeclSyntax(
"\(modifiers)init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
"\(accessorInfo.parsingAccessor) init(parsing span: inout \(raw: Constants.BinaryParsing.parserSpan)) throws(\(raw: Constants.BinaryParsing.thrownParsingError))",
) {
for (variableName, variableInfo) in structFieldInfo.variables {
for action in variableInfo.parseActions {
Expand All @@ -53,7 +58,7 @@ public struct ConstructStructParseMacro: ExtensionMacro {

let printerExtension =
try ExtensionDeclSyntax("extension \(type): \(raw: Constants.Protocols.printableProtocol)") {
try FunctionDeclSyntax("\(modifiers)func printerIntel() throws -> PrinterIntel") {
try FunctionDeclSyntax("\(accessorInfo.printingAccessor) func printerIntel() throws -> PrinterIntel") {
var parseSkipMacroInfo: [PrintableFieldInfo] = []

for (variableName, variableInfo) in structFieldInfo.variables {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// MacroAccessorVisitor.swift
// BinaryParseKit
//
// Created by Larry Zeng on 11/26/25.
//

import BinaryParseKitCommons
import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros

enum MacroAccessorError: DiagnosticMessage, Error {
case invalidAccessor(String)
case moreThanOneModifier(modifiers: String)
case unknownAccessor

var message: String {
switch self {
case let .invalidAccessor(accessor):
#"Invalid ACL value: \#(accessor); Please use one of \#(ExtensionAccessor.allowedCases.map(\.description).joined(separator: ", ")); use it in string literal "public" or enum member access .public."#
case let .moreThanOneModifier(modifiers: modifiers):
"More than one modifier found: \(modifiers). Only one modifier is allowed."
case .unknownAccessor:
"You have used unknown accessor in `@ParseStruct` or `@ParseEnum`."
}
}

var diagnosticID: SwiftDiagnostics.MessageID {
.init(
domain: "BinaryParseKit.MacroACLError",
id: "\(self)",
)
}

var severity: SwiftDiagnostics.DiagnosticSeverity {
switch self {
case .invalidAccessor, .moreThanOneModifier, .unknownAccessor: .error
}
}
}

class MacroAccessorVisitor: SyntaxVisitor {
private static let defaultAccessor = ExtensionAccessor.follow

private(set) var printingAccessor: ExtensionAccessor = MacroAccessorVisitor.defaultAccessor
private(set) var parsingAccessor: ExtensionAccessor = MacroAccessorVisitor.defaultAccessor

private let context: any MacroExpansionContext

init(context: any MacroExpansionContext) {
self.context = context
super.init(viewMode: .sourceAccurate)
}

override func visit(_ node: LabeledExprSyntax) -> SyntaxVisitorContinueKind {
let labelText = node.label?.text
switch labelText {
case "printingAccessor":
setACL(to: \.printingAccessor, with: node)
case "parsingAccessor":
setACL(to: \.parsingAccessor, with: node)
default:
break
}
return .skipChildren
}

private func setACL(
to keypath: ReferenceWritableKeyPath<MacroAccessorVisitor, ExtensionAccessor>,
with node: LabeledExprSyntax,
) {
let acl = parseACL(from: node)
self[keyPath: keypath] = acl
if case let .unknown(value) = acl {
context.diagnose(
.init(
node: node,
message: MacroAccessorError.invalidAccessor(value),
),
)
}
}

private func parseACL(from node: LabeledExprSyntax) -> ExtensionAccessor {
let expression = node.expression

// FIXME: use macro toolkit
if let stringLiteralSyntax = expression.as(StringLiteralExprSyntax.self),
let stringLiteral = stringLiteralSyntax.segments.first?.as(StringSegmentSyntax.self)?.content.text {
return .init(unicodeScalarLiteral: stringLiteral)
} else if let memberAccessSyntax = expression.as(MemberAccessExprSyntax.self) {
let element = memberAccessSyntax.declName.baseName.text
return .init(unicodeScalarLiteral: element)
}

return .unknown(expression.description)
}
}

struct AccessorInfo {
let parsingAccessor: DeclModifierSyntax
let printingAccessor: DeclModifierSyntax
}

func extractAccessor(
from attributeNode: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext,
) throws(MacroAccessorError) -> AccessorInfo {
let modifiers = declaration.modifiers
guard modifiers.count < 2 else {
throw MacroAccessorError.moreThanOneModifier(modifiers: modifiers.map(\.name.text).joined(separator: ", "))
}
let modifierToken = modifiers.first?.name.tokenKind ?? .keyword(.internal)

let accessorVisitor = MacroAccessorVisitor(context: context)
accessorVisitor.walk(attributeNode)

guard let parsingAccessor = accessorVisitor.parsingAccessor.getAccessorToken(defaultAccessor: modifierToken),
let printingAccessor = accessorVisitor.printingAccessor.getAccessorToken(defaultAccessor: modifierToken)
else {
throw MacroAccessorError.unknownAccessor
}

return .init(
parsingAccessor: .init(
name: TokenSyntax(parsingAccessor, presence: .present),
),
printingAccessor: .init(
name: TokenSyntax(printingAccessor, presence: .present),
),
)
}
Loading
Loading