Skip to content

Commit a67bf21

Browse files
committed
Add macros for System and Component
1 parent b9984c2 commit a67bf21

File tree

5 files changed

+207
-9
lines changed

5 files changed

+207
-9
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
import SwiftSyntax
9+
import SwiftSyntaxBuilder
10+
import SwiftSyntaxMacros
11+
12+
public enum ComponentMacroError: Error, CustomStringConvertible {
13+
case notStructOrClass
14+
15+
public var description: String {
16+
switch self {
17+
case .notStructOrClass:
18+
"@Component can only be attached to a struct or a class."
19+
}
20+
}
21+
}
22+
23+
public struct ECSComponentMacro: MemberMacro, ExtensionMacro {
24+
public static func expansion(
25+
of node: SwiftSyntax.AttributeSyntax,
26+
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
27+
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
28+
conformingTo protocols: [SwiftSyntax.TypeSyntax],
29+
in context: some SwiftSyntaxMacros.MacroExpansionContext
30+
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
31+
guard let name = declaration.as(ClassDeclSyntax.self)?.name.trimmed ?? declaration.as(StructDeclSyntax.self)?.name.trimmed else {
32+
throw ComponentMacroError.notStructOrClass
33+
}
34+
35+
var extensionDeclaration = "extension \(name)"
36+
if protocols.contains(where: {$0 == "GateEngine.Component"}) == false {
37+
extensionDeclaration += ": GateEngine.Component"
38+
}
39+
40+
return [
41+
try ExtensionDeclSyntax(SyntaxNodeString(stringLiteral: extensionDeclaration)) {
42+
"""
43+
public static let componentID: GateEngine.ComponentID = .init()
44+
"""
45+
}
46+
]
47+
}
48+
49+
// public static func expansion(
50+
// of node: SwiftSyntax.AttributeSyntax,
51+
// providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
52+
// in context: some SwiftSyntaxMacros.MacroExpansionContext
53+
// ) throws -> [SwiftSyntax.DeclSyntax] {
54+
// guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
55+
// throw SystemMacroError.notClass
56+
// }
57+
// let className = classDecl.name.trimmed
58+
// return [DeclSyntax(
59+
//"""
60+
//@_cdecl(\"eventHandler\")
61+
//fileprivate func _eventHandler(pointer: UnsafeMutableRawPointer!, event: System.Event, arg: CUnsignedInt) -> CInt {
62+
// return \(className)._eventHandler(pointer: pointer, event: event, arg: arg)
63+
//}
64+
//"""
65+
// )]
66+
// }
67+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
import SwiftSyntax
9+
import SwiftSyntaxBuilder
10+
import SwiftSyntaxMacros
11+
12+
public enum SystemMacroError: Error, CustomStringConvertible {
13+
case notClass
14+
15+
public var description: String {
16+
switch self {
17+
case .notClass:
18+
"@System(phase) can only be attached to a class."
19+
}
20+
}
21+
}
22+
23+
public struct ECSSystemMacro: MemberMacro, ExtensionMacro {
24+
public static func expansion(
25+
of node: AttributeSyntax,
26+
providingMembersOf declaration: some DeclGroupSyntax,
27+
conformingTo protocols: [TypeSyntax],
28+
in context: some MacroExpansionContext
29+
) throws -> [DeclSyntax] {
30+
31+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
32+
throw SystemMacroError.notClass
33+
}
34+
35+
var syntax = "override class var phase: System.Phase { \(node.arguments!.formatted()) }"
36+
37+
if let access = classDecl.modifiers.first(where: {
38+
let syntax = $0.trimmed.description
39+
return syntax == "public" || syntax == "open" || syntax == "package"
40+
}) {
41+
syntax = access.trimmedDescription + " " + syntax
42+
}
43+
44+
return [DeclSyntax(stringLiteral: syntax)]
45+
}
46+
47+
public static func expansion(
48+
of node: SwiftSyntax.AttributeSyntax,
49+
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
50+
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
51+
conformingTo protocols: [SwiftSyntax.TypeSyntax],
52+
in context: some SwiftSyntaxMacros.MacroExpansionContext
53+
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
54+
guard protocols.contains(where: {$0 == "GateEngine.System"}) == false else { return [] }
55+
56+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
57+
throw SystemMacroError.notClass
58+
}
59+
60+
let className = classDecl.name.trimmed
61+
62+
// var protocols: [ExtensionDeclSyntax] = [try ProtocolDeclSyntax("GateEngine.System")] + protocols
63+
// return protocols
64+
return [
65+
try ExtensionDeclSyntax(SyntaxNodeString("GateEngine.System"))
66+
]
67+
}
68+
69+
// public static func expansion(
70+
// of node: SwiftSyntax.AttributeSyntax,
71+
// providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
72+
// in context: some SwiftSyntaxMacros.MacroExpansionContext
73+
// ) throws -> [SwiftSyntax.DeclSyntax] {
74+
// guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
75+
// throw SystemMacroError.notClass
76+
// }
77+
// let className = classDecl.name.trimmed
78+
// return [DeclSyntax(
79+
//"""
80+
//@_cdecl(\"eventHandler\")
81+
//fileprivate func _eventHandler(pointer: UnsafeMutableRawPointer!, event: System.Event, arg: CUnsignedInt) -> CInt {
82+
// return \(className)._eventHandler(pointer: pointer, event: event, arg: arg)
83+
//}
84+
//"""
85+
// )]
86+
// }
87+
}

Macros/ECSMacros/Plugin.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
import SwiftCompilerPlugin
9+
import SwiftSyntaxMacros
10+
11+
@main
12+
struct ECSMacros: CompilerPlugin {
13+
var providingMacros: [Macro.Type] = [
14+
ECSSystemMacro.self,
15+
ECSComponentMacro.self
16+
]
17+
}

Package.swift

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
5+
import CompilerPluginSupport
56

67
let package = Package(
78
name: "GateEngine",
@@ -14,17 +15,20 @@ let package = Package(
1415
var packageDependencies: [Package.Dependency] = []
1516

1617
// Official
17-
#if os(Windows)
18-
packageDependencies.append(
18+
packageDependencies.append(contentsOf: {
19+
var official: [Package.Dependency] = []
20+
#if os(Windows)
1921
// Windows requires 1.2.0+
20-
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0"))
21-
)
22-
#else
23-
packageDependencies.append(
22+
official.append(.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0")))
23+
#else
2424
// swift-atomics must use extact 1.1.0 pending https://github.com/apple/swift/issues/69264
25-
.package(url: "https://github.com/apple/swift-atomics.git", exact: "1.1.0")
26-
)
27-
#endif
25+
official.append(.package(url: "https://github.com/apple/swift-atomics.git", exact: "1.1.0"))
26+
#endif
27+
official.append(.package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"))
28+
return official
29+
}())
30+
31+
2832
packageDependencies.append(
2933
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0"))
3034
)
@@ -51,6 +55,11 @@ let package = Package(
5155
.target(name: "GateEngine",
5256
dependencies: {
5357
var dependencies: [Target.Dependency] = []
58+
59+
dependencies.append(
60+
"ECSMacros"
61+
)
62+
5463
dependencies.append(contentsOf: [
5564
"GameMath",
5665
"Shaders",
@@ -266,6 +275,18 @@ let package = Package(
266275
}()),
267276
])
268277

278+
// MARK: - Macros
279+
targets.append(contentsOf: [
280+
.macro(
281+
name: "ECSMacros",
282+
dependencies: [
283+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
284+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
285+
],
286+
path: "Macros/ECSMacros"
287+
),
288+
])
289+
269290
// MARK: - Dependencies
270291

271292
targets.append(contentsOf: [

Sources/GateEngine/GateEngine.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import GameMath
2323
@_exported import func Foundation.sin
2424
@_exported import func Foundation.tan
2525

26+
@attached(member, names: named(phase), named(macroPhase))
27+
public macro System(_ macroPhase: GateEngine.System.Phase) = #externalMacro(module: "ECSMacros", type: "ECSSystemMacro")
28+
29+
@attached(extension, conformances: Component, names: named(componentID), named(`init`))
30+
public macro Component() = #externalMacro(module: "ECSMacros", type: "ECSComponentMacro")
31+
2632
#if canImport(WinSDK)
2733
import WinSDK
2834
#endif

0 commit comments

Comments
 (0)