1+ //
2+ // SyntaxKitGenerator.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 Foundation
31+
32+ /// A generator that can compile and execute SyntaxKit DSL code from a string
33+ /// and return the generated Swift code.
34+ public struct SyntaxKitGenerator {
35+
36+ /// Generates Swift code from a string containing SyntaxKit DSL code.
37+ ///
38+ /// This method takes a string of SyntaxKit DSL code, compiles it using the Swift compiler,
39+ /// executes it, and returns the generated Swift code.
40+ ///
41+ /// - Parameters:
42+ /// - dslCode: A string containing valid SyntaxKit DSL code
43+ /// - outputVariable: The name of the variable that contains the generated code (default: "result")
44+ /// - Returns: The generated Swift code as a string
45+ /// - Throws: `SyntaxKitGeneratorError` if compilation or execution fails
46+ public static func generateCode( from dslCode: String , outputVariable: String = " result " ) throws -> String {
47+ let tempDir = FileManager . default. temporaryDirectory
48+ let uniqueID = UUID ( ) . uuidString
49+ let workDir = tempDir. appendingPathComponent ( " SyntaxKit- \( uniqueID) " )
50+
51+ // Create working directory
52+ try FileManager . default. createDirectory ( at: workDir, withIntermediateDirectories: true )
53+ defer {
54+ // Clean up working directory
55+ try ? FileManager . default. removeItem ( at: workDir)
56+ }
57+
58+ // Create the Sources directory structure
59+ let sourcesDir = workDir. appendingPathComponent ( " Sources " )
60+ let targetDir = sourcesDir. appendingPathComponent ( " SyntaxKitGenerator " )
61+ try FileManager . default. createDirectory ( at: targetDir, withIntermediateDirectories: true )
62+
63+ // Create the main Swift file with the DSL code
64+ let mainSwiftFile = targetDir. appendingPathComponent ( " main.swift " )
65+ let swiftCode = createSwiftCode ( dslCode: dslCode, outputVariable: outputVariable)
66+ try swiftCode. write ( to: mainSwiftFile, atomically: true , encoding: . utf8)
67+
68+ // Create Package.swift for dependencies
69+ let packageFile = workDir. appendingPathComponent ( " Package.swift " )
70+ let packageContent = createPackageSwift ( )
71+ try packageContent. write ( to: packageFile, atomically: true , encoding: . utf8)
72+
73+ // Compile and run the code
74+ let process = Process ( )
75+ process. executableURL = URL ( fileURLWithPath: " /usr/bin/swift " )
76+ process. arguments = [ " run " ]
77+ process. currentDirectoryURL = workDir
78+
79+ let outputPipe = Pipe ( )
80+ let errorPipe = Pipe ( )
81+ process. standardOutput = outputPipe
82+ process. standardError = errorPipe
83+
84+ try process. run ( )
85+ process. waitUntilExit ( )
86+
87+ if process. terminationStatus != 0 {
88+ let errorData = errorPipe. fileHandleForReading. readDataToEndOfFile ( )
89+ let errorOutput = String ( data: errorData, encoding: . utf8) ?? " Unknown error "
90+ throw SyntaxKitGeneratorError . compilationFailed ( errorOutput)
91+ }
92+
93+ let outputData = outputPipe. fileHandleForReading. readDataToEndOfFile ( )
94+ let output = String ( data: outputData, encoding: . utf8) ?? " "
95+
96+ return output. trimmingCharacters ( in: . whitespacesAndNewlines)
97+ }
98+
99+ private static func createSwiftCode( dslCode: String , outputVariable: String ) -> String {
100+ return """
101+ import Foundation
102+ import SyntaxKit
103+
104+ // DSL Code to execute
105+ \( dslCode)
106+
107+ // Print the generated code
108+ print( \( outputVariable) .generateCode())
109+ """
110+ }
111+
112+ private static func createPackageSwift( ) -> String {
113+ // Get the current project directory by looking for the Package.swift file
114+ let currentDirectory = getCurrentProjectDirectory ( )
115+
116+ return """
117+ // swift-tools-version: 5.9
118+ import PackageDescription
119+
120+ let package = Package(
121+ name: " SyntaxKitGenerator " ,
122+ platforms: [.macOS(.v13)],
123+ dependencies: [
124+ .package(path: " \( currentDirectory) " )
125+ ],
126+ targets: [
127+ .executableTarget(
128+ name: " SyntaxKitGenerator " ,
129+ dependencies: [ " SyntaxKit " ]
130+ )
131+ ]
132+ )
133+ """
134+ }
135+
136+ private static func getCurrentProjectDirectory( ) -> String {
137+ // Try multiple approaches to find the project directory
138+
139+ // Approach 1: Look for the Bundle's bundlePath (if running from a built app)
140+ let bundlePath = Bundle . main. bundlePath
141+ let bundleDir = ( bundlePath as NSString ) . deletingLastPathComponent
142+ if FileManager . default. fileExists ( atPath: ( bundleDir as NSString ) . appendingPathComponent ( " Package.swift " ) ) {
143+ return bundleDir
144+ }
145+
146+ // Approach 2: Use the current working directory and walk up
147+ var currentPath = FileManager . default. currentDirectoryPath
148+
149+ // Walk up the directory tree to find the project root
150+ while !currentPath. isEmpty && currentPath != " / " {
151+ let packagePath = ( currentPath as NSString ) . appendingPathComponent ( " Package.swift " )
152+ if FileManager . default. fileExists ( atPath: packagePath) {
153+ return currentPath
154+ }
155+ currentPath = ( currentPath as NSString ) . deletingLastPathComponent
156+ }
157+
158+ // Approach 3: Try to find the project by looking for Sources/SyntaxKit
159+ let possiblePaths = [
160+ " /Users/leo/Documents/Projects/SyntaxKit " , // Hardcoded fallback
161+ FileManager . default. currentDirectoryPath,
162+ ProcessInfo . processInfo. environment [ " PWD " ] ?? " "
163+ ]
164+
165+ for path in possiblePaths {
166+ let sourcesPath = ( path as NSString ) . appendingPathComponent ( " Sources/SyntaxKit " )
167+ if FileManager . default. fileExists ( atPath: sourcesPath) {
168+ return path
169+ }
170+ }
171+
172+ // Fallback: return current directory and let the compiler error out
173+ return FileManager . default. currentDirectoryPath
174+ }
175+ }
176+
177+ /// Errors that can occur during code generation
178+ public enum SyntaxKitGeneratorError : Error , LocalizedError {
179+ case compilationFailed( String )
180+ case executionFailed( String )
181+
182+ public var errorDescription : String ? {
183+ switch self {
184+ case . compilationFailed( let error) :
185+ return " Compilation failed: \( error) "
186+ case . executionFailed( let error) :
187+ return " Execution failed: \( error) "
188+ }
189+ }
190+ }
0 commit comments