Building SyntaxKit with AI: A Developer's Journey

When Swift Macros were released in 2023, I thought it would be a great opportunity to augment existing Swift code effectively. However, working with SwiftSyntax quickly made me realize the challenge I was going to face. I had encountered SwiftSyntax before and knew exactly how difficult it was to use its various patterns for programmatic Swift code generation. In many ways, those of us not on the Apple payroll aren't the target audience for creating Macros. If only there were an easier way to create Swift code that was simpler than SwiftSyntax but more type-safe than creating mere strings.

This is the story of how I used AI tools like Cursor and Claude Code to build SyntaxKit - a Swift package that transforms SwiftSyntax's verbose approach into something that feels like writing Swift code.

πŸ“š View Documentation | πŸ™ GitHub Repository

The SwiftSyntax Challenge

SwiftSyntax is Apple's official Swift library that provides a source-accurate tree representation of Swift source code. It enables developers to parse, inspect, generate, and transform Swift code programmatically, making it the foundation for creating Swift macros and other code generation tools.

Key Concepts:

Documentation and Resources:

Swift Macros: The Primary Use Case

Swift macros are the biggest use case for SwiftSyntax, using it as their foundation to generate code at compile time. When Swift Macros were released in 2023, they opened up new possibilities for augmenting existing Swift code effectively. However, working with SwiftSyntax quickly revealed the challenges developers would face.

Swift Macros Documentation:

Working with SwiftSyntax is very procedural and verbose when you want to write Swift. Consider this simple example - creating a basic struct:

// SwiftSyntax - creating a simple struct
let structKeyword = TokenSyntax.keyword(.struct, trailingTrivia: .space)
let identifier = TokenSyntax.identifier("User", trailingTrivia: .space)
let leftBrace = TokenSyntax.leftBrace(leadingTrivia: .space, trailingTrivia: .newline)

let members = MemberDeclListSyntax([
    MemberDeclListSyntax.Element(
        decl: VariableDeclSyntax(
            bindingKeyword: .keyword(.let),
            bindings: PatternBindingListSyntax([
                PatternBindingSyntax(
                    pattern: IdentifierPatternSyntax(identifier: .identifier("id")),
                    typeAnnotation: TypeAnnotationSyntax(
                        type: SimpleTypeIdentifierSyntax(name: .identifier("UUID"))
                    )
                )
            ])
        )
    )
])

let structDecl = StructDeclSyntax(
    structKeyword: structKeyword,
    identifier: identifier,
    leftBrace: leftBrace,
    members: members,
    rightBrace: TokenSyntax.rightBrace(leadingTrivia: .newline)
)

That's just for creating a single property! The SwiftSyntax AST for even a simple User struct can be nearly 2,100 lines when prettified. It's precise, but it's not human-friendly.

To put this in perspective, here's what a simple User struct looks like in regular Swift:

struct User {
    let id: UUID
    let name: String
}

But to generate this same struct using SwiftSyntax requires the verbose 80+ lines of code shown above.

My Fascination with DSLs

Before diving into the solution, I should mention my fascination with Domain Specific Languages (DSLs). Ever since I saw Zach's presentation on DeckUI, I've been captivated by the power of Swift's result builders. SwiftUI itself is a perfect example - it makes UI creation feel natural and declarative. I've previously built PackageDSL to simplify creating large Swift packages using a similar approach.

Understanding Result Builders

Result builders (introduced with the @resultBuilder attribute) are a Swift feature that enables the creation of domain-specific languages (DSLs) by allowing functions to build up a result from a sequence of components. This is what makes SwiftUI's declarative syntax possible.

How Result Builders Work:

Documentation and Tutorials:

The elegance of these DSLs inspired me: what if we could bring that same declarative simplicity to Swift code generation?

Discovering AI as a Development Tool

Like many developers, I was initially skeptical of AI tools. I'd been burnt by trending technologies before - VR, Bitcoin, NFTs, and countless failed startups built on questionable foundations. So when AI development tools started gaining traction, I approached them with healthy skepticism.

However, after experiencing ChatGPT's capabilities firsthand, and later working with tools like Cursor and Claude Code, I began to see their potential. These weren't just fancy autocomplete tools - they were capable coding partners that could understand context, generate complex implementations, and iterate on solutions.

The turning point came when I realized I had a perfect use case: building SyntaxKit. I had three clear pieces:

  1. The desired Swift code output (what developers want to generate)
  2. The SwiftSyntax AST structure (what SwiftSyntax needs)
  3. My ideal API design (how it should feel to use SyntaxKit)

This was a perfect scenario for AI-assisted development.

Building SyntaxKit with AI Tools

The Failed LLM Approach

My first instinct was ambitious: create a custom LLM specifically trained to generate Swift Macro code. I was inspired by the Swift AST Explorer - a fantastic tool that visualizes Swift code as Abstract Syntax Trees. I spent considerable time researching fine-tuning approaches, mapping out training pipelines, and even discussing comprehensive training strategies with Claude or using the Claude Workbench to build prompts.

The Swift AST Explorer became crucial for understanding the relationship between Swift code and its SwiftSyntax representation. I could input simple Swift constructs and immediately see the corresponding AST structure, which seemed perfect for generating training data. The workflow was elegant: write Swift code β†’ feed it through the AST Explorer β†’ get the exact SwiftSyntax tree structure β†’ use that mapping to teach the LLM what SwiftSyntax code generates what Swift output.

This approach failed for several practical reasons:

This failure taught me an important lesson: sometimes the "AI solution" isn't the right solution. While LLMs excel at generating code from natural language, creating better development tools often requires traditional software engineering approaches.

Success with Cursor

My breakthrough came with Cursor. Instead of training a custom model, I used Cursor's interactive LLM capabilities to iteratively build SyntaxKit components.

The process was surprisingly effective:

  1. Extract the AST: I built a terminal console application that could generate clean JSON from SwiftSyntax ASTs, making them easily consumable by the LLM. This was inspired by the Swift AST Explorer's ability to show the relationship between Swift code and its AST representation. The tool would take Swift code as input and output a simplified JSON structure showing the exact SwiftSyntax nodes needed to recreate that code.
  2. Teach through examples: I provided the three key pieces - desired output, SwiftSyntax structure, and my API design
  3. Iterate and refine: Cursor helped me implement each SyntaxKit component, learning from previous patterns
Teaching Cursor how to do SwiftSyntax

The key was breaking the problem down into manageable pieces rather than trying to solve everything at once. You can see more examples of these here.

Transitioning to Claude Code

As SyntaxKit grew more complex, I transitioned from Cursor to Claude Code for more sophisticated project management and planning capabilities. Here's why: Cursor excels at editing specific pieces of code within an IDE - perfect for implementing individual components and making targeted changes. Claude Code is better for bigger changes that don't necessarily need IDE integration - ideal for architectural decisions, project-wide refactoring, and maintaining consistency across multiple components. Claude Code's ability to understand project context and maintain consistency across multiple components proved invaluable for the larger architectural decisions.

The Result: SyntaxKit

After weeks of AI-assisted development, the result was SyntaxKit - a Swift package that transforms SwiftSyntax's verbose approach into something that feels like writing Swift code:

// SyntaxKit - declarative and readable
let userModel = Struct("User") {
    Variable(.let, name: "id", type: "UUID")
    Variable(.let, name: "name", type: "String")
    Variable(.let, name: "email", type: "String")
}
.inherits("Equatable")

The difference is night and day - SyntaxKit reads like SwiftUI code for creating Swift code, while SwiftSyntax reads like UIKit code for building an interface. This addresses several key pain points in Swift macro and code generation development:

Lessons Learned Building with AI

Working with AI tools to build SyntaxKit taught me several important lessons:

Unit Tests: Where LLMs Shine

One area where AI tools particularly excel is generating comprehensive unit tests. I created tests not just for each SyntaxKit component, but for various scenarios - structs with generics, protocols with attributes, complex inheritance hierarchies.

These tests became crucial for validating that the generated SwiftSyntax code compiled correctly and produced the expected Swift output.

Plan and Break Down Projects

One of the most crucial lessons is the importance of planning and breaking projects into smaller, manageable pieces. AI tools work best when given specific, focused tasks rather than trying to generate entire systems at once. Taking time to plan the overall architecture and then breaking it down into discrete components makes AI assistance much more effective.

This is why tools like Cursor and Claude Code include built-in todo list functionality - they recognize that breaking work into smaller, trackable pieces is essential for effective AI-assisted development. There are even specialized tools like Task Master that focus specifically on creating detailed task breakdowns from Product Requirements Documents (PRDs), making the planning process more systematic and comprehensive.

Hold Your AI's Hand

While AI can build entire applications, unless you're creating a quick prototype, you're best served by guiding the AI through each step of implementation. AI-generated code often lacks the architectural decisions and patterns that make code maintainable and extensible over time. The iterative approach works much better than trying to generate everything at once - think of AI as a coding partner that needs clear direction and regular feedback to produce code you'll actually want to build upon.

Be Wary of Over-Engineering

LLMs can build APIs you don't need or overcomplicate simple problems. It's crucial to review generated code carefully and remove anything out of scope. Sometimes the AI wants to be "helpful" by adding features you never requested.

Context and Consistency Matter

Understanding context windows, pricing, and when to switch between models becomes important for larger projects. Maintaining consistency across components requires careful prompting and sometimes manual oversight.

Human Code Reviews Are Essential

AI-generated code still needs human review. While the tools are incredibly capable, they can introduce subtle bugs or architectural issues that only human experience can catch.

A Healthy Continuous Integration System Is Critical

When building with AI, having a robust continuous integration system becomes even more important. AI-generated code can introduce subtle issues that only surface during compilation or testing across different platforms. A healthy CI system acts as a safety net, catching problems early and ensuring that AI-assisted code changes don't break existing functionality. This is especially crucial when iterating quickly with AI tools, as the rapid pace of development can easily introduce regressions.

For Swift development specifically, I've created swift-build - a comprehensive GitHub Action that simplifies CI setup for Swift packages across multiple platforms, which I've detailed in my latest article on building Swift CI/CD with swift-build.

You can also integrate AI-powered code review tools like Claude Code or CodeRabbit into your CI pipeline for automated PR reviews. These tools can catch common issues, suggest improvements, and provide feedback on code quality. However, it's always important to have a human check your code - AI review tools are excellent supplements but should never replace human judgment and domain expertise. Understanding the fundamentals of continuous integration helps establish the right practices from the start.

Future Plans

SyntaxKit is just the beginning. I'm already exploring similar DSL approaches for other Swift development challenges:

The combination of thoughtful API design and AI-assisted implementation opens up exciting possibilities for developer tooling. Tools like Sosumi.ai for Apple API exploration and llm.codes for converting documentation to LLM-friendly formats will be invaluable for building more sophisticated Swift packages and developer tools that integrate deeply with Apple's ecosystem.

Getting Started with SyntaxKit

SyntaxKit is available as a Swift package and can be integrated into your macro projects today. The documentation includes examples for common use cases, from simple data structures to complex generic types with protocol conformances.

Whether you're building your first Swift macro or looking to simplify existing SwiftSyntax code, SyntaxKit provides a more approachable path to programmatic Swift code generation.


The journey from SwiftSyntax frustration to SyntaxKit success illustrates how AI tools, when used thoughtfully, can accelerate development without replacing good engineering practices. The key is understanding when to lean on AI assistance and when to rely on traditional software design principles.

SyntaxKit represents not just a better API for Swift code generation, but a new model for AI-assisted tool development that prioritizes developer experience while maintaining the precision and reliability that Swift developers expect.