Skip to content

Commit 12b377f

Browse files
committed
create freestanding macro
1 parent e28e3e5 commit 12b377f

File tree

12 files changed

+354
-1
lines changed

12 files changed

+354
-1
lines changed

Macros/SKSampleMacro/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Macros/SKSampleMacro/Package.resolved

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Macros/SKSampleMacro/Package.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// swift-tools-version: 6.2
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
import CompilerPluginSupport
6+
7+
let package = Package(
8+
name: "SKSampleMacro",
9+
platforms: [
10+
.macOS(.v13),
11+
.iOS(.v13),
12+
.watchOS(.v6),
13+
.tvOS(.v13),
14+
.visionOS(.v1)
15+
],
16+
products: [
17+
// Products define the executables and libraries a package produces, making them visible to other packages.
18+
.library(
19+
name: "SKSampleMacro",
20+
targets: ["SKSampleMacro"]
21+
),
22+
.executable(
23+
name: "SKSampleMacroClient",
24+
targets: ["SKSampleMacroClient"]
25+
),
26+
],
27+
dependencies: [
28+
.package(path: "../.."),
29+
.package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1")
30+
],
31+
targets: [
32+
// Targets are the basic building blocks of a package, defining a module or a test suite.
33+
// Targets can depend on other targets in this package and products from dependencies.
34+
// Macro implementation that performs the source transformation of a macro.
35+
.macro(
36+
name: "SKSampleMacroMacros",
37+
dependencies: [
38+
.product(name: "SyntaxKit", package: "SyntaxKit"),
39+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
40+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
41+
]
42+
),
43+
44+
// Library that exposes a macro as part of its API, which is used in client programs.
45+
.target(name: "SKSampleMacro", dependencies: ["SKSampleMacroMacros"]),
46+
47+
// A client of the library, which is able to use the macro in its own code.
48+
.executableTarget(name: "SKSampleMacroClient", dependencies: ["SKSampleMacro"]),
49+
50+
// A test target used to develop the macro implementation.
51+
.testTarget(
52+
name: "SKSampleMacroTests",
53+
dependencies: [
54+
"SKSampleMacroMacros",
55+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
56+
]
57+
),
58+
]
59+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
/// A macro that produces both a value and a string containing the
5+
/// source code that generated the value. For example,
6+
///
7+
/// #stringify(x + y)
8+
///
9+
/// produces a tuple `(x + y, "x + y")`.
10+
@freestanding(expression)
11+
public macro stringify<T>(_ lhs: T, _ rhs: T) -> (T, String) = #externalMacro(module: "SKSampleMacroMacros", type: "StringifyMacro")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import SKSampleMacro
2+
3+
let a = 17
4+
let b = 25
5+
6+
let (result, code) = #stringify(a, b)
7+
8+
print("The value \(result) was produced by the code \"\(code)\"")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import SwiftCompilerPlugin
2+
import SwiftSyntax
3+
//import SwiftSyntaxBuilder
4+
import SwiftSyntaxMacros
5+
import SyntaxKit
6+
7+
/// Implementation of the `stringify` macro, which takes an expression
8+
/// of any type and produces a tuple containing the value of that expression
9+
/// and the source code that produced the value. For example
10+
///
11+
/// #stringify(x + y)
12+
///
13+
/// will expand to
14+
///
15+
/// (x + y, "x + y")
16+
public struct StringifyMacro: ExpressionMacro {
17+
public static func expansion(
18+
of node: some FreestandingMacroExpansionSyntax,
19+
in context: some MacroExpansionContext
20+
) -> ExprSyntax {
21+
let first = node.arguments.first?.expression
22+
let second = node.arguments.last?.expression
23+
guard let first, let second else {
24+
fatalError("compiler bug: the macro does not have any arguments")
25+
}
26+
27+
return Tuple{
28+
Infix("+") {
29+
VariableExp(first.description)
30+
VariableExp(second.description)
31+
}
32+
Literal.string("\(first.description) + \(second.description)")
33+
}.expr
34+
}
35+
}
36+
37+
@main
38+
struct SKSampleMacroPlugin: CompilerPlugin {
39+
let providingMacros: [Macro.Type] = [
40+
StringifyMacro.self,
41+
]
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
import SwiftSyntaxMacros
4+
import SwiftSyntaxMacrosTestSupport
5+
import XCTest
6+
7+
// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
8+
#if canImport(SKSampleMacroMacros)
9+
import SKSampleMacroMacros
10+
11+
let testMacros: [String: Macro.Type] = [
12+
"stringify": StringifyMacro.self,
13+
]
14+
#endif
15+
16+
final class SKSampleMacroTests: XCTestCase {
17+
func testMacro() throws {
18+
#if canImport(SKSampleMacroMacros)
19+
assertMacroExpansion(
20+
"""
21+
#stringify(a + b)
22+
""",
23+
expandedSource: """
24+
(a + b, "a + b")
25+
""",
26+
macros: testMacros
27+
)
28+
#else
29+
throw XCTSkip("macros are only supported when running tests for the host platform")
30+
#endif
31+
}
32+
33+
func testMacroWithStringLiteral() throws {
34+
#if canImport(SKSampleMacroMacros)
35+
assertMacroExpansion(
36+
#"""
37+
#stringify("Hello, \(name)")
38+
"""#,
39+
expandedSource: #"""
40+
("Hello, \(name)", #""Hello, \(name)""#)
41+
"""#,
42+
macros: testMacros
43+
)
44+
#else
45+
throw XCTSkip("macros are only supported when running tests for the host platform")
46+
#endif
47+
}
48+
}

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// CodeBlock+ExprSyntax.swift
3+
// SyntaxKit
4+
//
5+
// Created by Leo Dion.
6+
// Provides convenience for converting a CodeBlock into ExprSyntax when appropriate.
7+
//
8+
9+
import SwiftSyntax
10+
11+
extension CodeBlock {
12+
/// Attempts to treat this `CodeBlock` as an expression and return its `ExprSyntax` form.
13+
///
14+
/// If the underlying syntax already *is* an `ExprSyntax`, it is returned directly. If the
15+
/// underlying syntax is a bare `TokenSyntax` (commonly the case for `VariableExp` which
16+
/// produces an identifier token), we wrap it in a `DeclReferenceExprSyntax` so that it becomes
17+
/// a valid expression node. Any other kind of syntax results in a runtime error, because it
18+
/// cannot be represented as an expression (e.g. declarations or statements).
19+
public var expr: ExprSyntax {
20+
if let expr = self.syntax.as(ExprSyntax.self) {
21+
return expr
22+
}
23+
24+
if let token = self.syntax.as(TokenSyntax.self) {
25+
return ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(token.text)))
26+
}
27+
28+
fatalError("CodeBlock of type \(type(of: self.syntax)) cannot be represented as ExprSyntax")
29+
}
30+
}

Sources/SyntaxKit/Infix.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// Infix.swift
3+
// SyntaxKit
4+
//
5+
// Created by Leo Dion.
6+
// Copyright © 2025 BrightDigit.
7+
//
8+
// Permission is hereby granted, free of charge, to any person
9+
// obtaining a copy of this software and associated documentation
10+
// files (the “Software”), to deal in the Software without
11+
// restriction, including without limitation the rights to use,
12+
// copy, modify, merge, publish, distribute, sublicense, and/or
13+
// sell copies of the Software, and to permit persons to whom the
14+
// Software is furnished to do so, subject to the following
15+
// conditions:
16+
//
17+
// The above copyright notice and this permission notice shall be
18+
// included in all copies or substantial portions of the Software.
19+
//
20+
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
21+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27+
// OTHER DEALINGS IN THE SOFTWARE.
28+
//
29+
30+
import SwiftSyntax
31+
32+
/// A generic binary (infix) operator expression, e.g. `a + b`.
33+
public struct Infix: CodeBlock {
34+
private let op: String
35+
private let operands: [CodeBlock]
36+
37+
/// Creates an infix operator expression.
38+
/// - Parameters:
39+
/// - op: The operator symbol as it should appear in source (e.g. "+", "-", "&&").
40+
/// - content: A ``CodeBlockBuilder`` that supplies the two operand expressions.
41+
///
42+
/// Exactly two operands must be supplied – a left-hand side and a right-hand side.
43+
public init(_ op: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) {
44+
self.op = op
45+
self.operands = content()
46+
}
47+
48+
public var syntax: SyntaxProtocol {
49+
guard operands.count == 2 else {
50+
fatalError("Infix expects exactly two operands, got \(operands.count).")
51+
}
52+
53+
let left = operands[0].expr
54+
let right = operands[1].expr
55+
56+
let operatorExpr = ExprSyntax(
57+
BinaryOperatorExprSyntax(
58+
operator: .binaryOperator(op, leadingTrivia: .space, trailingTrivia: .space)
59+
)
60+
)
61+
62+
return SequenceExprSyntax(
63+
elements: ExprListSyntax([
64+
left,
65+
operatorExpr,
66+
right,
67+
])
68+
)
69+
}
70+
}

0 commit comments

Comments
 (0)