├── .editorconfig ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── Package.swift ├── README.md ├── Sources ├── PlaygroundMacros │ ├── PlaygroundMacro.swift │ └── PlaygroundMacrosMain.swift └── Playgrounds │ ├── Discoverable │ ├── PlaygroundContent+Discoverable.swift │ ├── PlaygroundContent+Hashable.swift │ └── PlaygroundContent.swift │ ├── Playgrounds.swift │ └── ToolsAPI │ ├── EntryPoints │ └── SwiftPMEntryPoint.swift │ └── Playground.swift └── Tests ├── PlaygroundMacrosTests └── PlaygroundMacroExpansionTests.swift └── PlaygroundsTests └── PlaygroundsToolsAPITests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig documentation: https://editorconfig.org 2 | 3 | root = true 4 | 5 | # Default settings applied to every file type. 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.index-build 4 | /build 5 | /Packages 6 | /*.xcodeproj 7 | xcuserdata/ 8 | DerivedData/ 9 | .swiftpm/config/registries.json 10 | .swiftpm/xcode/package.xcworkspace/ 11 | .swiftpm/xcode/xcuserdata/ 12 | .netrc 13 | .vs 14 | .docc-build/ 15 | Package.resolved 16 | *.profraw 17 | .*.sw? 18 | /.vscode 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # 2 | # This source file is part of the Swift Play Experimental open source project 3 | # 4 | # Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | # Licensed under Apache License v2.0 with Runtime Library Exception 6 | # 7 | # See https://swift.org/LICENSE.txt for license information 8 | # 9 | 10 | * @chrismiles 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Swift Code of Conduct 2 | 3 | [Swift Code of Conduct](https://www.swift.org/code-of-conduct/) 4 | 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Swift Play Experimental 2 | 3 | ## Trying out #Playground with the “swift play” prototype 4 | 5 | An experimental Swift Package Manager branch implements a new "swift play" command, which provides an easy way to list and run `#Playground` instances found in package library targets. 6 | 7 | ### Swift Package Manager prototype branch build 8 | 9 | Checkout and build the swift-package-manager branch `eng/chrismiles/swift-play-prototype` first. 10 | 11 | $ git clone git@github.com:chrismiles/swift-package-manager.git 12 | $ cd swift-package-manager 13 | $ git checkout eng/chrismiles/swift-play-prototype 14 | $ swift build 15 | 16 | A successful build should produce a new `swift-play` executable. 17 | 18 | $ ls .build/arm64-apple-macosx/debug/swift-play 19 | .build/arm64-apple-macosx/debug/swift-play 20 | 21 | Note the path of the executable for later. 22 | 23 | ### Try out "swift play" 24 | 25 | Open any new or existing package containing a library target. 26 | 27 | Add a package dependency to swift-play-experimental branch `main`. 28 | 29 | dependencies: [ 30 | .package(url: "https://github.com/apple/swift-play-experimental", branch: "main"), 31 | ], 32 | 33 | Add a Playgrounds dependency to any library targets that you want to use `#Playground` in. 34 | 35 | dependencies: [ 36 | .product(name: "Playgrounds", package: "swift-play-experimental"), 37 | ] 38 | 39 | You may also need to set a minimum platform version. If so, you can add an entry like: 40 | 41 | platforms: [ 42 | .macOS(.v10_15) 43 | ], 44 | 45 | In a terminal, add the swiftpm debug products path to $PATH. Example: 46 | 47 | export PATH=~/swift-package-manager/.build/arm64-apple-macosx/debug:$PATH 48 | 49 | Change directory to the package and use `swift play --list` to verify everything works with no code modifications. The package should build and you should see “Found 0 Playgrounds”. Example: 50 | 51 | $ swift play --list 52 | Building for debugging... 53 | Build of product 'Dice__REPL' complete! (32.61s) 54 | Found 0 Playgrounds 55 | 56 | Problems at this point? 57 | 58 | * If you see a build error “error: no such module 'Playgrounds’” then the Playgrounds package dependency isn’t properly configured. 59 | * If you see any other build errors, make sure the package builds with the standard swift build first. If it only fails to build with swift play then submit a bug report. 60 | 61 | Now you can import the Playgrounds module and add some `#Playground` declarations to your code. For example: 62 | 63 | $ cat <> Sources/MyGame/Dice.swift 64 | import Playgrounds 65 | 66 | #Playground("d20") { 67 | let roll = Int.random(in: 1...20) 68 | print("d20 rolled \(roll)") 69 | } 70 | EOF 71 | $ swift play d20 72 | Building for debugging... 73 | ---- Running Playground "d20" - Hit ^C to quit ---- 74 | d20 rolled 6 75 | ^C 76 | 77 | Use `swift play ` to run a playground, as shown in the example above. 78 | 79 | 80 | ## Reporting issues 81 | 82 | Issues are tracked using the project's 83 | [GitHub Issue Tracker](https://github.com/apple/swift-play-experimental/issues). 84 | 85 | Fill in the fields of the relevant template form offered on that page when 86 | creating new issues. For bug report issues, please include a minimal example 87 | which reproduces the issue. Where possible, attach the example as a Swift 88 | package, or include a URL to the package hosted on GitHub or another public 89 | hosting service. 90 | 91 | ## Setting up the development environment 92 | 93 | First, clone the Swift Play Experimental repository from 94 | [https://github.com/apple/swift-play-experimental](https://github.com/apple/swift-play-experimental). 95 | 96 | If you're preparing to make a contribution, you should fork the repository first 97 | and clone the fork which will make opening PRs easier. 98 | 99 | ### Using Xcode (easiest) 100 | 101 | 1. Install Xcode 16 or newer from the [Apple Developer](https://developer.apple.com/xcode/) 102 | website. 103 | 1. Open the `Package.swift` file from the cloned Swift Play Experimental repository in 104 | Xcode. 105 | 1. Select the `swift-play` scheme (if not already selected) and the 106 | "My Mac" run destination. 107 | 1. Use Xcode to inspect, edit, build, or test the code. 108 | 109 | ### Using the command line 110 | 111 | If you are using macOS and have Xcode installed, you can use Swift from the 112 | command line immediately. 113 | 114 | If you aren't using macOS or do not have Xcode installed, you need to download 115 | and install a toolchain. 116 | 117 | #### Installing a toolchain 118 | 119 | 1. Download a toolchain. A recent **6.0 development snapshot** toolchain is 120 | required to build the playgrounds library. Visit 121 | [swift.org](http://swift.org/install) and download the most recent toolchain 122 | from the section titled **release/6.0** under **Development Snapshots** on 123 | the page for your platform. 124 | 125 | Be aware that development snapshot toolchains aren't intended for day-to-day 126 | development and may contain defects that affect the programs built with them. 127 | 1. Install the toolchain and confirm it can be located successfully: 128 | 129 | **macOS with Xcode installed**: 130 | 131 | ```bash 132 | $> export TOOLCHAINS=swift 133 | $> xcrun --find swift 134 | /Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin/swift 135 | ``` 136 | 137 | **Non-macOS or macOS without Xcode**: 138 | 139 | ```bash 140 | $> export PATH=/path/to/swift-toolchain/usr/bin:"${PATH}" 141 | $> which swift 142 | /path/to/swift-toolchain/usr/bin/swift 143 | ``` 144 | 145 | ## Local development 146 | 147 | With a Swift toolchain installed you are ready to make changes and test them locally. 148 | 149 | ### Building 150 | 151 | ```bash 152 | $> swift build 153 | ``` 154 | 155 | ### Testing 156 | 157 | ```bash 158 | $> swift test 159 | ``` 160 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | 205 | ## Runtime Library Exception to the Apache 2.0 License: ## 206 | 207 | 208 | As an exception, if you use this Software to compile your source code and 209 | portions of this Software are embedded into the binary product as a result, 210 | you may redistribute such product without providing attribution as would 211 | otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The Swift Play Experimental Project 3 | =================================== 4 | 5 | Please visit the Swift Play Experimental web site for more information: 6 | 7 | * https://github.com/apple/swift-play-experimental 8 | 9 | Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 10 | 11 | The Swift Project licenses this file to you under the Apache License, 12 | version 2.0 (the "License"); you may not use this file except in compliance 13 | with the License. You may obtain a copy of the License at: 14 | 15 | https://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 19 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 20 | License for the specific language governing permissions and limitations 21 | under the License. 22 | 23 | ------------------------------------------------------------------------------- 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | // 4 | // This source file is part of the Swift Play Experimental open source project 5 | // 6 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See https://swift.org/LICENSE.txt for license information 10 | // 11 | 12 | import PackageDescription 13 | import CompilerPluginSupport 14 | 15 | let package = Package( 16 | name: "swift-play-experimental", 17 | 18 | platforms: [ 19 | .macOS(.v10_15), 20 | .iOS(.v13), 21 | .watchOS(.v6), 22 | .tvOS(.v13), 23 | .macCatalyst(.v13), 24 | .visionOS(.v1), 25 | ], 26 | 27 | products: [ 28 | .library( 29 | name: "Playgrounds", 30 | targets: ["Playgrounds"] 31 | ) 32 | ], 33 | 34 | dependencies: [ 35 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "602.0.0-latest"), 36 | .package(url: "https://github.com/swiftlang/swift-testing.git", branch: "main"), 37 | ], 38 | 39 | targets: [ 40 | 41 | .target( 42 | name: "Playgrounds", 43 | dependencies: [ 44 | "PlaygroundMacros", 45 | .product(name: "_TestDiscovery", package: "swift-testing"), 46 | ], 47 | swiftSettings: .packageSettings 48 | ), 49 | 50 | .testTarget( 51 | name: "PlaygroundsTests", 52 | dependencies: [ 53 | "Playgrounds", 54 | ], 55 | swiftSettings: .packageSettings 56 | ), 57 | 58 | .macro( 59 | name: "PlaygroundMacros", 60 | dependencies: [ 61 | .product(name: "SwiftSyntax", package: "swift-syntax"), 62 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 63 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 64 | ], 65 | swiftSettings: .packageSettings 66 | ), 67 | ] 68 | ) 69 | 70 | // BUG: swift-package-manager-#6367 71 | #if !os(Windows) && !os(FreeBSD) && !os(OpenBSD) 72 | package.targets.append(contentsOf: [ 73 | .testTarget( 74 | name: "PlaygroundMacrosTests", 75 | dependencies: [ 76 | "Playgrounds", 77 | "PlaygroundMacros", 78 | .product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"), 79 | ], 80 | swiftSettings: .packageSettings 81 | ) 82 | ]) 83 | #endif 84 | 85 | extension Array where Element == PackageDescription.SwiftSetting { 86 | /// Settings intended to be applied to every Swift target in this package. 87 | /// Analogous to project-level build settings in an Xcode project. 88 | static var packageSettings: Self { 89 | [ 90 | // When building as a package, the macro plugin always builds as an 91 | // executable rather than a library. 92 | .define("SWP_NO_LIBRARY_MACRO_PLUGINS"), 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Play Experimental 2 | 3 | 11 | 12 | ## Feature overview 13 | 14 | The Swift Play Experimental project contains a Playgrounds library 15 | that provides a `#Playground` macro for declaring runnable blocks 16 | of code in any Swift file. Being in the same file allows the 17 | playground code to access private entities in the file. 18 | 19 | ```swift 20 | $ cat Sources/Fibonacci/Fibonacci.swift 21 | func fibonacci(_ n: Int) -> Int { 22 | ... 23 | } 24 | 25 | import Playgrounds 26 | 27 | #Playground("Fibonacci") { 28 | for n in 0..<10 { 29 | print("fibonacci(\(n)) = \(fibonacci(n))") 30 | } 31 | } 32 | ``` 33 | 34 | ### Tools API 35 | 36 | Swift tools and IDEs can use the provided tools-based API to 37 | enumerate and run playgrounds found in a process. 38 | 39 | ``` 40 | let foundPlaygrounds = __Playground.__allPlaygrounds() 41 | print("Found playgrounds:") 42 | for playground in foundPlaygrounds { 43 | print("* \(playground.__displayName)") 44 | } 45 | 46 | if let playground = __Playground.__getPlayground(named: "Fibonacci") { 47 | try await playground.__run() 48 | } 49 | ``` 50 | 51 | For example, a prototype Swift Package Manager branch adds a 52 | `swift play` command which can be used to list and execute 53 | playgrounds found in a package. 54 | 55 | For example: 56 | ``` 57 | $ swift play --list 58 | Building for debugging... 59 | Found 1 Playground 60 | * Fibonacci/Fibonacci.swift:23 "Fibonacci" 61 | 62 | $ swift play Fibonacci 63 | Building for debugging... 64 | ---- Running Playground "Fibonacci" - Hit ^C to quit ---- 65 | Fibonacci(7) = 21 66 | ^C 67 | ``` 68 | 69 | See CONTRIBUTING.md for details on trying out Swift Play 70 | Experimental with the Swift Package Manager "swift play" 71 | prototype. 72 | 73 | ### Cross-platform support 74 | 75 | macOS is supported. Other platforms to be supported soon. 76 | -------------------------------------------------------------------------------- /Sources/PlaygroundMacros/PlaygroundMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | public import SwiftSyntax 11 | public import SwiftSyntaxMacros 12 | 13 | struct PlaygroundDiagnostic: Error, CustomStringConvertible { 14 | var description: String 15 | } 16 | 17 | public enum Playground: DeclarationMacro, Sendable { 18 | /// DeclarationMacro entry point 19 | public static func expansion( 20 | of node: some FreestandingMacroExpansionSyntax, 21 | in context: some MacroExpansionContext 22 | ) throws -> [DeclSyntax] { 23 | // Determine the playground's name 24 | let name: any ExprSyntaxProtocol 25 | if let nameArg = node.arguments.first, 26 | let stringLiteral = nameArg.trimmed.expression.as(StringLiteralExprSyntax.self) 27 | { 28 | // The name of the playground was supplied as the argument to the macro 29 | name = stringLiteral 30 | } 31 | else { 32 | // Set the name to nil 33 | name = NilLiteralExprSyntax() 34 | } 35 | 36 | guard context.lexicalContext.isEmpty else { 37 | throw PlaygroundDiagnostic(description: "\(self) macro \(name): Must be at file scope") 38 | } 39 | 40 | // Define a function that will run the playground body 41 | let playgroundRunFuncName = context.makeUniqueName("PlaygroundRunFunc") 42 | let playgroundRunFuncDeclSyntax: DeclSyntax = 43 | """ 44 | @MainActor @Sendable private func \(playgroundRunFuncName)(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 45 | try await body() 46 | } 47 | 48 | """ 49 | 50 | // Define a function that will call the run function with arguments and any trailing closure preserved as written 51 | let playgroundEntryCallExpression = node.playgroundCallExpression(forFuncName: "\(playgroundRunFuncName)") 52 | let playgroundEntryName = context.makeUniqueName("PlaygroundEntry") 53 | let playgroundEntryDecl: DeclSyntax = 54 | """ 55 | @MainActor @Sendable private func \(playgroundEntryName)() async throws { 56 | struct \(markerTokenSyntax) { 57 | let utf8offset: Int 58 | @discardableResult init(utf8offset: Int) { 59 | self.utf8offset = utf8offset 60 | } 61 | } 62 | try await \(playgroundEntryCallExpression) 63 | } 64 | 65 | """ 66 | 67 | // The playground content record to be be looked up at runtime 68 | let playgroundContentRecordName = context.makeUniqueName("PlaygroundContentRecord") 69 | let playgroundContentRecordDecl: DeclSyntax = 70 | """ 71 | #if hasFeature(SymbolLinkageMarkers) 72 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 73 | @_section("__DATA_CONST,__swift5_tests") 74 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 75 | @_section("swift5_tests") 76 | #elseif os(Windows) 77 | @_section(".sw5test$B") 78 | #endif 79 | @_used 80 | #endif 81 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 82 | private let \(playgroundContentRecordName): Playgrounds.__PlaygroundsContentRecord = ( 83 | 0x706c6179, /* 'play' */ 84 | 0, 85 | { outValue, type, hint, _ in 86 | Playgrounds.__store( 87 | \(name), 88 | \(playgroundEntryName), 89 | at: outValue, 90 | asTypeAt: type, 91 | withHintAt: hint 92 | ) 93 | }, 94 | 0, 95 | 0 96 | ) 97 | 98 | """ 99 | 100 | // The playground content record container to be looked up at runtime 101 | let playgroundContentRecordContainerName = context.makeUniqueName("__🟡$PlaygroundContentRecordContainer") 102 | let playgroundContentRecordContainerDecl: DeclSyntax = 103 | """ 104 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 105 | enum \(playgroundContentRecordContainerName): Playgrounds.__PlaygroundsContentRecordContainer { 106 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 107 | \(playgroundContentRecordName) 108 | } 109 | } 110 | """ 111 | 112 | return [ 113 | playgroundRunFuncDeclSyntax, 114 | playgroundEntryDecl, 115 | playgroundContentRecordDecl, 116 | playgroundContentRecordContainerDecl 117 | ] 118 | } 119 | 120 | public static var formatMode: FormatMode { 121 | .disabled 122 | } 123 | 124 | fileprivate static let markerTokenSyntax = TokenSyntax(stringLiteral: "$__Marker") 125 | 126 | fileprivate enum BodyClosure { 127 | case trailingClosure(ClosureExprSyntax) 128 | case lastArgument(LabeledExprListSyntax.Element, ClosureExprSyntax) 129 | 130 | var closureSyntax: ClosureExprSyntax { 131 | switch self { 132 | case .trailingClosure(let trailingClosure): trailingClosure 133 | case .lastArgument(_, let lastArgumentClosure): lastArgumentClosure 134 | } 135 | } 136 | } 137 | } 138 | 139 | extension FreestandingMacroExpansionSyntax { 140 | // Creates a call expression from the macro, exactly preserving 141 | // arguments, trailing closures, etc, replacing the macro name with 142 | // the type to be instantiated. 143 | fileprivate func playgroundCallExpression(forFuncName funcNameExpr: ExprSyntax) -> FunctionCallExprSyntax { 144 | var callExpression = FunctionCallExprSyntax( 145 | calledExpression: funcNameExpr, 146 | leftParen: leftParen, 147 | arguments: arguments, 148 | rightParen: rightParen?.trimmed, 149 | trailingClosure: trailingClosure?.with(\.leadingTrivia, .space), 150 | additionalTrailingClosures: additionalTrailingClosures, 151 | trailingTrivia: trailingTrivia 152 | ) 153 | 154 | // Injects a marker call at the beginning of the closure body, so 155 | // that clients can calculate correct character offsets for logged 156 | // playground expression results. 157 | if let playgroundBodyClosure { 158 | var replacementClosure = playgroundBodyClosure.closureSyntax 159 | replacementClosure.injectMarkerCall(withMacroOffset: positionAfterSkippingLeadingTrivia.utf8Offset) 160 | switch playgroundBodyClosure { 161 | case .trailingClosure(_): 162 | replacementClosure.leftBrace.leadingTrivia = .space 163 | callExpression.trailingClosure = replacementClosure 164 | case .lastArgument(var lastArgument, _): 165 | lastArgument.expression = ExprSyntax(replacementClosure) 166 | callExpression.arguments = callExpression.arguments.dropLast() + [lastArgument] 167 | } 168 | } 169 | 170 | return callExpression 171 | } 172 | 173 | /// Returns a closure for the playground body if there is one, from either the 174 | /// trailing closure or the last argument. 175 | private var playgroundBodyClosure: Playground.BodyClosure? { 176 | if let trailingClosure { 177 | return .trailingClosure(trailingClosure) 178 | } else if let lastArgument = arguments.last, let closure = lastArgument.expression.as(ClosureExprSyntax.self) { 179 | return .lastArgument(lastArgument, closure) 180 | } 181 | return nil 182 | } 183 | } 184 | 185 | private extension ClosureExprSyntax { 186 | mutating func injectMarkerCall(withMacroOffset macroOffset: Int) { 187 | // Only inject a marker call if the passed block contains statements. 188 | guard var firstStatement = statements.first else { return } 189 | 190 | // The marker call has a single parameter: the UTF-8 offset from the 191 | // macro start position. This allows us to calculate the expected 192 | // location of the first statement in the original code containing 193 | // the macro. 194 | let firstStatementOffset = firstStatement.positionAfterSkippingLeadingTrivia.utf8Offset 195 | let markerOffset = firstStatementOffset - macroOffset 196 | var markerStatement = CodeBlockItemSyntax( 197 | stringLiteral: "\(Playground.markerTokenSyntax)(utf8offset: \(markerOffset));" 198 | ) 199 | 200 | // Make sure that the marker call ends up in the original location of 201 | // the first statement. 202 | let firstStatementLeadingTrivia = firstStatement.leadingTrivia 203 | markerStatement.leadingTrivia = firstStatementLeadingTrivia 204 | 205 | // The leading trivia of the first statement is cleared to make sure 206 | // the only character between the marker call and the first statement 207 | // is the inserted semicolon. 208 | firstStatement.leadingTrivia = [] 209 | 210 | // Update the first statement with the new trivia and insert the 211 | // marker call. 212 | statements = [firstStatement] + statements.dropFirst() 213 | statements.insert(markerStatement, at: statements.startIndex) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /Sources/PlaygroundMacros/PlaygroundMacrosMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | #if SWP_NO_LIBRARY_MACRO_PLUGINS 11 | import SwiftCompilerPlugin 12 | import SwiftSyntaxMacros 13 | 14 | @main 15 | struct PlaygroundMacroPlugin: CompilerPlugin { 16 | let providingMacros: [any Macro.Type] = [ 17 | Playground.self, 18 | ] 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Sources/Playgrounds/Discoverable/PlaygroundContent+Discoverable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | @_spi(ForToolsIntegrationOnly) private import _TestDiscovery 11 | 12 | /// Playgrounds uses the same runtime content discovery mechanism 13 | /// that Swift Testing uses. 14 | extension PlaygroundContent: DiscoverableAsTestContent { 15 | fileprivate static var testContentKind: TestContentKind { 16 | "play" 17 | } 18 | 19 | /// Use the hint type to avoid running code for _every_ playground in a binary 20 | /// in order to find the one that you care about. 21 | fileprivate typealias TestContentAccessorHint = Hint 22 | 23 | /// This property acts as a hint to ``DiscoverableAsTestContent`` so that it 24 | /// can skip over types whose names don't match the pattern used in the macro. 25 | fileprivate static var _testContentTypeNameHint: String { 26 | "__🟡$PlaygroundContentRecordContainer" 27 | } 28 | } 29 | 30 | extension PlaygroundContent { 31 | private static func allTypeMetadataBasedTestContentRecords() -> AnySequence> { 32 | allTypeMetadataBasedTestContentRecords { type, outRecord in 33 | guard let type = type as? any __PlaygroundsContentRecordContainer.Type else { 34 | return false 35 | } 36 | outRecord.withMemoryRebound(to: __PlaygroundsContentRecord.self) { outRecord in 37 | outRecord.baseAddress!.initialize(to: type.__playgroundsContentRecord) 38 | } 39 | return true 40 | } 41 | } 42 | 43 | /// All available playground instances in the process, according to the runtime. 44 | /// 45 | /// The order of values in this sequence is unspecified. 46 | static var all: some Sequence { 47 | var result = [Self]() 48 | 49 | result = PlaygroundContent.allTestContentRecords().compactMap { 50 | $0.load() 51 | } 52 | 53 | if result.isEmpty { 54 | // Fall back to type-based discovery. 55 | result = PlaygroundContent.allTypeMetadataBasedTestContentRecords().compactMap { 56 | $0.load() 57 | } 58 | } 59 | 60 | return Set(result) 61 | } 62 | 63 | /// The hint for finding a playground is its name or ID. 64 | enum Hint { 65 | case name(String) 66 | case id(ID) 67 | } 68 | 69 | /// Find the first playground in the current process with the given hint (name 70 | /// or ID.) 71 | static func find(withHint hint: Hint) -> Self? { 72 | var result = PlaygroundContent.allTestContentRecords().lazy 73 | .compactMap { $0.load(withHint: hint) } 74 | .first 75 | 76 | if result == nil { 77 | // Fall back to type-based discovery. 78 | result = PlaygroundContent.allTypeMetadataBasedTestContentRecords().lazy 79 | .compactMap { $0.load(withHint: hint) } 80 | .first 81 | } 82 | 83 | return result 84 | } 85 | } 86 | 87 | // MARK: - Content Records - 88 | 89 | /// The type of the accessor function used to access a playground content record. 90 | /// 91 | /// - Parameters: 92 | /// - outValue: A pointer to uninitialized memory large enough to contain the 93 | /// corresponding playground content record's value. 94 | /// - type: A pointer to the expected type of `outValue`. Use `load(as:)` to 95 | /// get the Swift type, not `unsafeBitCast(_:to:)`. 96 | /// - hint: An optional pointer to a hint value. 97 | /// - reserved: Reserved for future use. 98 | /// 99 | /// - Returns: Whether or not `outValue` was initialized. The caller is 100 | /// responsible for deinitializing `outValue` if it was initialized. 101 | /// 102 | /// - Warning: This type is used to implement the `#Playground` macro. Do not use it 103 | /// directly. 104 | public typealias __PlaygroundsContentRecordAccessor = @convention(c) ( 105 | _ outValue: UnsafeMutableRawPointer, 106 | _ type: UnsafeRawPointer, 107 | _ hint: UnsafeRawPointer?, 108 | _ reserved: UnsafeRawPointer? 109 | ) -> CBool 110 | 111 | /// The content of a playground content record. 112 | /// 113 | /// - Parameters: 114 | /// - kind: The kind of this record. 115 | /// - reserved1: Reserved for future use. 116 | /// - accessor: A function which, when called, produces the playground content. 117 | /// - context: Kind-specific context for this record. 118 | /// - reserved2: Reserved for future use. 119 | /// 120 | /// - Warning: This type is used to implement the `#Playground` macro. Do not use it 121 | /// directly. 122 | public typealias __PlaygroundsContentRecord = ( 123 | kind: UInt32, 124 | reserved1: UInt32, 125 | accessor: __PlaygroundsContentRecordAccessor?, 126 | context: UInt, 127 | reserved2: UInt 128 | ) 129 | 130 | /// Store a playground into the given memory. 131 | /// 132 | /// - Parameters: 133 | /// - name: The name of the playground. 134 | /// - fileID: The file identifier containing the playground. 135 | /// - line: The line number of the playground. 136 | /// - column: The line column number of the playground. 137 | /// - outValue: The uninitialized memory to store the playground into. 138 | /// - typeAddress: A pointer to the expected type of the playground as passed 139 | /// to the playground content record calling this function. 140 | /// - body: The body closure of the playground to store. 141 | /// 142 | /// - Returns: Whether or not a playground was stored into `outValue`. 143 | /// 144 | /// - Warning: This function is used to implement the `#Playground` macro. Do not use it 145 | /// directly. 146 | public func __store( 147 | _ name: String?, 148 | _ body: @escaping @Sendable @MainActor () async throws -> Void, 149 | _ fileID: String = #fileID, 150 | _ line: Int = #line, 151 | _ column: Int = #column, 152 | at outValue: UnsafeMutableRawPointer, 153 | asTypeAt typeAddress: UnsafeRawPointer, 154 | withHintAt hintAddress: UnsafeRawPointer? 155 | ) -> CBool { 156 | guard typeAddress.load(as: Any.Type.self) == PlaygroundContent.self else { 157 | return false 158 | } 159 | 160 | if let hint = hintAddress?.load(as: PlaygroundContent.Hint.self) { 161 | switch hint { 162 | case let .name(hintedName): 163 | if name != hintedName { 164 | // The caller provided a name as a hint but it didn't match, so exit early. 165 | return false 166 | } 167 | case let .id(hintedID): 168 | let id = PlaygroundContent.ID(__name: name, __fileID: fileID, __line: line, __column: column) 169 | if id != hintedID { 170 | // The caller provided an ID as a hint but it didn't match, so exit early. 171 | return false 172 | } 173 | } 174 | } 175 | 176 | outValue.initializeMemory( 177 | as: PlaygroundContent.self, 178 | to: PlaygroundContent( 179 | displayName: name, 180 | fileID: fileID, 181 | line: line, 182 | column: column, 183 | body: body 184 | ) 185 | ) 186 | return true 187 | } 188 | 189 | // MARK: - Content Records (Type-Based Discovery) - 190 | 191 | /// A protocol describing a type that contains a playground. 192 | /// 193 | /// - Warning: This protocol is used to implement the `#Playground` macro. Do 194 | /// not use it it directly. 195 | @_alwaysEmitConformanceMetadata 196 | public protocol __PlaygroundsContentRecordContainer { 197 | /// The playgrounds content record associated with this container. 198 | /// 199 | /// - Warning: This property is used to implement the `#Playground` macro. Do 200 | /// not use it it directly. 201 | nonisolated static var __playgroundsContentRecord: __PlaygroundsContentRecord { get } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/Playgrounds/Discoverable/PlaygroundContent+Hashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | extension PlaygroundContent: Equatable, Hashable { 11 | public static func ==(lhs: PlaygroundContent, rhs: PlaygroundContent) -> Bool { 12 | lhs.id == rhs.id 13 | } 14 | 15 | public func hash(into hasher: inout Hasher) { 16 | hasher.combine(id) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Playgrounds/Discoverable/PlaygroundContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | /// 11 | /// A type representing playground content in the process. 12 | /// 13 | struct PlaygroundContent: Sendable { 14 | var name: String? 15 | var fileID: String 16 | var line: Int 17 | var column: Int 18 | 19 | var body: @MainActor @Sendable () async throws -> Void 20 | 21 | init( 22 | displayName: String?, 23 | fileID: String, 24 | line: Int, 25 | column: Int, 26 | body: @escaping @MainActor @Sendable () async throws -> Void 27 | ) { 28 | self.name = displayName 29 | self.fileID = fileID 30 | self.line = line 31 | self.column = column 32 | self.body = body 33 | } 34 | } 35 | 36 | // MARK: - Identifiable 37 | 38 | extension PlaygroundContent: Identifiable { 39 | typealias ID = __Playground.__ID 40 | 41 | var id: ID { 42 | ID(__name: name, __fileID: fileID, __line: line, __column: column) 43 | } 44 | } 45 | 46 | // MARK: - CustomStringConvertible 47 | 48 | extension PlaygroundContent: CustomStringConvertible { 49 | var description: String { 50 | "" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Playgrounds/Playgrounds.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | @freestanding(declaration) 11 | public macro Playground( 12 | _ name: String? = nil, 13 | body: @escaping @Sendable () async throws -> Void 14 | ) = #externalMacro(module: "PlaygroundMacros", type: "Playground") 15 | -------------------------------------------------------------------------------- /Sources/Playgrounds/ToolsAPI/EntryPoints/SwiftPMEntryPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | import Foundation 11 | 12 | /// The entry point to the playgrounds library used by Swift Package Manager. 13 | /// 14 | /// - Parameters: 15 | /// - args: Command-line arguments to interpret. 16 | /// 17 | /// - Returns: The result of invoking the playgrounds library. The type of this 18 | /// value is subject to change. 19 | /// 20 | /// This function examines the command-line arguments represented by `args` and 21 | /// then invokes the playgrounds library in the current process. 22 | /// 23 | /// - Warning: This function is used by Swift Package Manager. Do not call it 24 | /// directly. 25 | public func __swiftPMEntryPoint(_ args: [String]) -> CInt { 26 | var listOption = false 27 | var oneShotOption = false 28 | var libPath = "" 29 | var playgroundDescription = "" 30 | 31 | // Parse arguments 32 | for arg in args { 33 | if arg == "--list" { 34 | listOption = true 35 | } 36 | else if arg == "--one-shot" { 37 | oneShotOption = true 38 | } 39 | else if arg == "--lib-path" { 40 | libPath = args[args.index(after: args.firstIndex(of: "--lib-path")!)] 41 | } 42 | else if arg != libPath { 43 | playgroundDescription = arg 44 | } 45 | } 46 | 47 | if libPath.isEmpty { 48 | print("Specify the path to a dylib with --lib-path ") 49 | return 1 50 | } 51 | 52 | // Load the specified dylib 53 | #if canImport(Darwin) 54 | let flags = RTLD_LAZY | RTLD_FIRST 55 | #else 56 | let flags = RTLD_LAZY 57 | #endif 58 | guard let image = dlopen(libPath, flags) else { 59 | let errorMessage: String = dlerror().flatMap { 60 | #if compiler(>=6) 61 | String(validatingCString: $0) 62 | #else 63 | String(validatingUTF8: $0) 64 | #endif 65 | } ?? "An unknown error occurred." 66 | fatalError("Failed to open target library at path \(libPath): \(errorMessage)") 67 | } 68 | defer { 69 | dlclose(image) 70 | } 71 | 72 | func printListOfPlaygrounds(_ playgrounds: [__Playground]) { 73 | for playground in playgrounds.sorted(by: playgroundSort) { 74 | let playgroundName = { 75 | if let name = playground.__name { 76 | return "\"\(name)\"" 77 | } 78 | return "(unnamed)" 79 | }() 80 | print("* \(playground.__id.__fileID):\(playground.__id.__line) \(playgroundName)") 81 | } 82 | } 83 | 84 | if listOption { 85 | // List playgrounds only 86 | let playgrounds = __Playground.__allPlaygrounds() 87 | print("Found \(playgrounds.count) Playground\(playgrounds.count==1 ? "" : "s")") 88 | printListOfPlaygrounds(playgrounds) 89 | } 90 | else { 91 | let playgrounds = __Playground.__findPlaygrounds(describedBy: playgroundDescription) 92 | guard playgrounds.count > 0 else { 93 | print("Unknown Playground \"\(playgroundDescription)\"") 94 | return 1 95 | } 96 | 97 | guard playgrounds.count == 1 else { 98 | print("Multiple playgrounds match the given description \"\(playgroundDescription)\"") 99 | printListOfPlaygrounds(playgrounds) 100 | return 1 101 | } 102 | 103 | let playground = playgrounds[0] 104 | 105 | print("---- Running Playground \"\(playground.__displayName)\" - Hit ^C to quit ----") 106 | Task { 107 | try await playground.__run() 108 | 109 | if oneShotOption { 110 | // Exit immediately after one-shot execution 111 | exit(0) 112 | } 113 | } 114 | 115 | // Wait forever so Playground can continue executing asynchronous calls 116 | RunLoop.main.run(until: Date.distantFuture) 117 | } 118 | 119 | return 0 120 | } 121 | 122 | /// Sort playgrounds by file/line/column. 123 | private func playgroundSort(_ lhs: __Playground, _ rhs: __Playground) -> Bool { 124 | if lhs.__id.__fileID == rhs.__id.__fileID { 125 | if lhs.__id.__line == rhs.__id.__line { 126 | return lhs.__id.__column < rhs.__id.__column 127 | } 128 | else { 129 | return lhs.__id.__line < rhs.__id.__line 130 | } 131 | } 132 | else { 133 | return lhs.__id.__fileID < rhs.__id.__fileID 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Playgrounds/ToolsAPI/Playground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | public struct __Playground: Sendable { 11 | // MARK: - For Tools Use: Identifying a playground - 12 | public struct __ID: Equatable, Hashable, CustomStringConvertible { 13 | public var __name: String? 14 | public var __fileID: String 15 | public var __line: Int 16 | public var __column: Int 17 | 18 | public init(__name: String? = nil, __fileID: String, __line: Int, __column: Int) { 19 | self.__name = __name 20 | self.__fileID = __fileID 21 | self.__line = __line 22 | self.__column = __column 23 | } 24 | 25 | public var description: String { 26 | let location = "\(__fileID):\(__line):\(__column)" 27 | if let name = __name { 28 | return "\(name) at \(location)" 29 | } 30 | return location 31 | } 32 | } 33 | 34 | // MARK: - For Tools Use: Public properties of a playground - 35 | 36 | public var __name: String? { playgroundContent.name } 37 | public var __id: __ID { playgroundContent.id } 38 | 39 | /// A name for the playground that is always displayable. 40 | public var __displayName: String { 41 | if let name = __name { 42 | return name 43 | } 44 | return String(describing: __id) 45 | } 46 | 47 | // MARK: - For Tools Use: Public function to run a playground - 48 | 49 | /// Executes the body of the playground 50 | public func __run() async throws { 51 | try await playgroundContent.body() 52 | } 53 | 54 | // MARK: - For Tools Use: Public functions to fetch playground records - 55 | 56 | /// Returns all playgrounds found at runtime 57 | public static func __allPlaygrounds() -> [__Playground] { 58 | PlaygroundContent.all.map { __Playground(from: $0) } 59 | } 60 | 61 | /// Returns a specific playground by name, if found at runtime 62 | public static func __getPlayground(named name: String) -> __Playground? { 63 | getPlayground(withHint: .name(name)) 64 | } 65 | 66 | /// Returns a specific playground by ID, if found at runtime 67 | public static func __getPlayground(identifiedBy id: __ID) -> __Playground? { 68 | getPlayground(withHint: .id(id)) 69 | } 70 | 71 | /// Returns all playgrounds matching the description, which can be either a playground 72 | /// name or a "filename:line:column" description string or subset of that description (like 73 | /// filename only to return all playgrounds in the given file). 74 | public static func __findPlaygrounds(describedBy description: String) -> [__Playground] { 75 | // If a playground name matches the description exactly, return it 76 | if let playground = __getPlayground(named: description) { 77 | return [playground] 78 | } 79 | 80 | guard let descriptionComponents = PlaygroundDescriptionComponents.parsePlaygroundDescription(description) else { 81 | return [] 82 | } 83 | 84 | let matchingContent = PlaygroundContent.all.filter { playgroundContent in 85 | descriptionComponents.matches(playgroundContent) 86 | } 87 | 88 | return matchingContent.map { __Playground(from: $0) } 89 | } 90 | 91 | // MARK: - Internal implementation details - 92 | 93 | internal init(from playgroundContent: PlaygroundContent) { 94 | self.playgroundContent = playgroundContent 95 | } 96 | 97 | internal let playgroundContent: PlaygroundContent 98 | 99 | /// Returns a specific playground by hint, if found at runtime 100 | internal static func getPlayground(withHint hint: PlaygroundContent.Hint) -> __Playground? { 101 | PlaygroundContent.find(withHint: hint).map { playground in 102 | __Playground(from: playground) 103 | } 104 | } 105 | 106 | } 107 | 108 | extension __Playground: CustomStringConvertible { 109 | public var description: String { 110 | let id = __id 111 | return "<__Playground name=\"\(__name ?? "(none)")\", fileID=\"\(id.__fileID)\", line=\(id.__line), column=\(id.__column)>" 112 | } 113 | } 114 | 115 | fileprivate struct PlaygroundDescriptionComponents { 116 | var fileID: String? 117 | var fileName: String? 118 | var lineNumber: Int? 119 | var columnNumber: Int? 120 | 121 | static func parsePlaygroundDescription(_ description: String) -> PlaygroundDescriptionComponents? { 122 | var descriptionComponents = PlaygroundDescriptionComponents() 123 | 124 | let components = description.split(separator: ":", maxSplits: 3) 125 | guard components.count >= 1 && components.count <= 3 else { 126 | return nil 127 | } 128 | 129 | descriptionComponents.fileID = String(components[0]) 130 | 131 | guard let fileName = components[0].split(separator: "/").last else { 132 | return nil 133 | } 134 | descriptionComponents.fileName = String(fileName) 135 | 136 | if components.count >= 2 { 137 | guard let lineNumber = Int(components[1]) else { return nil } 138 | descriptionComponents.lineNumber = lineNumber 139 | } 140 | 141 | if components.count == 3 { 142 | guard let columnNumber = Int(components[2]) else { return nil } 143 | descriptionComponents.columnNumber = columnNumber 144 | } 145 | 146 | return descriptionComponents 147 | } 148 | 149 | func matches(_ playgroundContent: PlaygroundContent) -> Bool { 150 | if let columnNumber { 151 | if playgroundContent.column != columnNumber { 152 | // Specified column number doesn't match 153 | return false 154 | } 155 | } 156 | 157 | if let lineNumber { 158 | if playgroundContent.line != lineNumber { 159 | // Specified line number doesn't match 160 | return false 161 | } 162 | } 163 | 164 | if let fileID, playgroundContent.fileID == fileID { 165 | // File ID matches exactly 166 | return true 167 | } 168 | 169 | if let fileName { 170 | if let contentFileName = playgroundContent.fileID.split(separator: "/").last, 171 | String(contentFileName) == fileName 172 | { 173 | // The file name matches 174 | return true 175 | } 176 | } 177 | 178 | return false 179 | } 180 | } 181 | 182 | -------------------------------------------------------------------------------- /Tests/PlaygroundMacrosTests/PlaygroundMacroExpansionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | import Testing 11 | @testable import PlaygroundMacros 12 | 13 | import SwiftDiagnostics 14 | import SwiftParser 15 | import SwiftSyntax 16 | import SwiftSyntaxBuilder 17 | import SwiftSyntaxMacros 18 | import SwiftSyntaxMacroExpansion 19 | import SwiftSyntaxMacrosGenericTestSupport 20 | 21 | @Suite("Playground Macro Expansion Tests") 22 | struct PlaygroundMacroExpansionTests { 23 | 24 | @Test("Named Playground with trailing closure expansion") 25 | func namedPlaygroundWithTrailingClosureExpansionTest() throws { 26 | assertMacroExpansion( 27 | """ 28 | #Playground("Named Playground with trailing closure") { 29 | let metadata = namedPlaygroundWithTrailingClosure 30 | let random = Int.random(in: 1...100) 31 | print("\\(metadata.displayName): random = \\(random)") 32 | metadata.wasExecuted = true 33 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 34 | } 35 | """, 36 | expandedSource: 37 | """ 38 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 39 | try await body() 40 | } 41 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 42 | struct $__Marker { 43 | let utf8offset: Int 44 | @discardableResult init(utf8offset: Int) { 45 | self.utf8offset = utf8offset 46 | } 47 | } 48 | try await __macro_local_17PlaygroundRunFuncfMu_("Named Playground with trailing closure") { 49 | $__Marker(utf8offset: 60);let metadata = namedPlaygroundWithTrailingClosure 50 | let random = Int.random(in: 1...100) 51 | print("\\(metadata.displayName): random = \\(random)") 52 | metadata.wasExecuted = true 53 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 54 | } 55 | } 56 | #if hasFeature(SymbolLinkageMarkers) 57 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 58 | @_section("__DATA_CONST,__swift5_tests") 59 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 60 | @_section("swift5_tests") 61 | #elseif os(Windows) 62 | @_section(".sw5test$B") 63 | #endif 64 | @_used 65 | #endif 66 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 67 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 68 | 0x706c6179, /* 'play' */ 69 | 0, 70 | { outValue, type, hint, _ in 71 | Playgrounds.__store( 72 | "Named Playground with trailing closure", 73 | __macro_local_15PlaygroundEntryfMu_, 74 | at: outValue, 75 | asTypeAt: type, 76 | withHintAt: hint 77 | ) 78 | }, 79 | 0, 80 | 0 81 | ) 82 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 83 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 84 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 85 | __macro_local_23PlaygroundContentRecordfMu_ 86 | } 87 | } 88 | """, 89 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 90 | ) 91 | { failure in 92 | Issue.record(Comment(rawValue: failure.message)) 93 | } 94 | } 95 | 96 | @Test("Unnamed playground with trailing closure") 97 | func unnamedPlaygroundWithTrailingClosureTest() throws { 98 | assertMacroExpansion( 99 | """ 100 | #Playground { 101 | let metadata = unnamedPlaygroundWithTrailingClosure 102 | let random = Int.random(in: 1...100) 103 | print("\\(metadata.displayName): random = \\(random)") 104 | metadata.wasExecuted = true 105 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 106 | } 107 | """, 108 | expandedSource: 109 | """ 110 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 111 | try await body() 112 | } 113 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 114 | struct $__Marker { 115 | let utf8offset: Int 116 | @discardableResult init(utf8offset: Int) { 117 | self.utf8offset = utf8offset 118 | } 119 | } 120 | try await __macro_local_17PlaygroundRunFuncfMu_ { 121 | $__Marker(utf8offset: 16);let metadata = unnamedPlaygroundWithTrailingClosure 122 | let random = Int.random(in: 1...100) 123 | print("\\(metadata.displayName): random = \\(random)") 124 | metadata.wasExecuted = true 125 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 126 | } 127 | } 128 | #if hasFeature(SymbolLinkageMarkers) 129 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 130 | @_section("__DATA_CONST,__swift5_tests") 131 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 132 | @_section("swift5_tests") 133 | #elseif os(Windows) 134 | @_section(".sw5test$B") 135 | #endif 136 | @_used 137 | #endif 138 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 139 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 140 | 0x706c6179, /* 'play' */ 141 | 0, 142 | { outValue, type, hint, _ in 143 | Playgrounds.__store( 144 | nil, 145 | __macro_local_15PlaygroundEntryfMu_, 146 | at: outValue, 147 | asTypeAt: type, 148 | withHintAt: hint 149 | ) 150 | }, 151 | 0, 152 | 0 153 | ) 154 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 155 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 156 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 157 | __macro_local_23PlaygroundContentRecordfMu_ 158 | } 159 | } 160 | """, 161 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 162 | ) 163 | { failure in 164 | Issue.record(Comment(rawValue: failure.message)) 165 | } 166 | } 167 | 168 | @Test("Named Playground with trailing closure containing `in` arg") 169 | func namedPlaygroundWithTrailingClosureContainingInArgTest() throws { 170 | assertMacroExpansion( 171 | """ 172 | #Playground("Named Playground with trailing closure containing in arg") { () -> Void in 173 | let metadata = namedPlaygroundWithTrailingClosureContainingInArg 174 | let random = Int.random(in: 1...100) 175 | print("\\(metadata.displayName) random = \\(random)") 176 | metadata.wasExecuted = true 177 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 178 | } 179 | """, 180 | expandedSource: 181 | """ 182 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 183 | try await body() 184 | } 185 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 186 | struct $__Marker { 187 | let utf8offset: Int 188 | @discardableResult init(utf8offset: Int) { 189 | self.utf8offset = utf8offset 190 | } 191 | } 192 | try await __macro_local_17PlaygroundRunFuncfMu_("Named Playground with trailing closure containing in arg") { () -> Void in 193 | $__Marker(utf8offset: 90);let metadata = namedPlaygroundWithTrailingClosureContainingInArg 194 | let random = Int.random(in: 1...100) 195 | print("\\(metadata.displayName) random = \\(random)") 196 | metadata.wasExecuted = true 197 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 198 | } 199 | } 200 | #if hasFeature(SymbolLinkageMarkers) 201 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 202 | @_section("__DATA_CONST,__swift5_tests") 203 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 204 | @_section("swift5_tests") 205 | #elseif os(Windows) 206 | @_section(".sw5test$B") 207 | #endif 208 | @_used 209 | #endif 210 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 211 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 212 | 0x706c6179, /* 'play' */ 213 | 0, 214 | { outValue, type, hint, _ in 215 | Playgrounds.__store( 216 | "Named Playground with trailing closure containing in arg", 217 | __macro_local_15PlaygroundEntryfMu_, 218 | at: outValue, 219 | asTypeAt: type, 220 | withHintAt: hint 221 | ) 222 | }, 223 | 0, 224 | 0 225 | ) 226 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 227 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 228 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 229 | __macro_local_23PlaygroundContentRecordfMu_ 230 | } 231 | } 232 | """, 233 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 234 | ) 235 | { failure in 236 | Issue.record(Comment(rawValue: failure.message)) 237 | } 238 | } 239 | 240 | @Test("Unnamed Playground with trailing closure containing `in` arg") 241 | func unnamedPlaygroundWithTrailingClosureContainingInArgTest() throws { 242 | assertMacroExpansion( 243 | """ 244 | #Playground { () -> Void in 245 | let metadata = unnamedPlaygroundWithTrailingClosureContainingInArg 246 | let random = Int.random(in: 1...100) 247 | print("\\(metadata.displayName) random = \\(random)") 248 | metadata.wasExecuted = true 249 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 250 | } 251 | """, 252 | expandedSource: 253 | """ 254 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 255 | try await body() 256 | } 257 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 258 | struct $__Marker { 259 | let utf8offset: Int 260 | @discardableResult init(utf8offset: Int) { 261 | self.utf8offset = utf8offset 262 | } 263 | } 264 | try await __macro_local_17PlaygroundRunFuncfMu_ { () -> Void in 265 | $__Marker(utf8offset: 30);let metadata = unnamedPlaygroundWithTrailingClosureContainingInArg 266 | let random = Int.random(in: 1...100) 267 | print("\\(metadata.displayName) random = \\(random)") 268 | metadata.wasExecuted = true 269 | print("\\(metadata.displayName) was executed: \\(metadata.wasExecuted)") 270 | } 271 | } 272 | #if hasFeature(SymbolLinkageMarkers) 273 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 274 | @_section("__DATA_CONST,__swift5_tests") 275 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 276 | @_section("swift5_tests") 277 | #elseif os(Windows) 278 | @_section(".sw5test$B") 279 | #endif 280 | @_used 281 | #endif 282 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 283 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 284 | 0x706c6179, /* 'play' */ 285 | 0, 286 | { outValue, type, hint, _ in 287 | Playgrounds.__store( 288 | nil, 289 | __macro_local_15PlaygroundEntryfMu_, 290 | at: outValue, 291 | asTypeAt: type, 292 | withHintAt: hint 293 | ) 294 | }, 295 | 0, 296 | 0 297 | ) 298 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 299 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 300 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 301 | __macro_local_23PlaygroundContentRecordfMu_ 302 | } 303 | } 304 | """, 305 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 306 | ) 307 | { failure in 308 | Issue.record(Comment(rawValue: failure.message)) 309 | } 310 | } 311 | 312 | @Test("Named playground with closure as body argument") 313 | func namedPlaygroundWithClosureAsBodyArgumentTest() throws { 314 | assertMacroExpansion( 315 | """ 316 | #Playground("Named playground with closure as body argument", body: { 317 | let metadata = namedPlaygroundWithClosureAsBodyArgument 318 | let random = Int.random(in: 1...100) 319 | print("\\(metadata.displayName): random = \\(random)") 320 | metadata.wasExecuted = true 321 | print("\\(metadata.displayName): was executed: \\(metadata.wasExecuted)") 322 | }) 323 | """, 324 | expandedSource: 325 | """ 326 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 327 | try await body() 328 | } 329 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 330 | struct $__Marker { 331 | let utf8offset: Int 332 | @discardableResult init(utf8offset: Int) { 333 | self.utf8offset = utf8offset 334 | } 335 | } 336 | try await __macro_local_17PlaygroundRunFuncfMu_("Named playground with closure as body argument", body: { 337 | $__Marker(utf8offset: 72);let metadata = namedPlaygroundWithClosureAsBodyArgument 338 | let random = Int.random(in: 1...100) 339 | print("\\(metadata.displayName): random = \\(random)") 340 | metadata.wasExecuted = true 341 | print("\\(metadata.displayName): was executed: \\(metadata.wasExecuted)") 342 | }) 343 | } 344 | #if hasFeature(SymbolLinkageMarkers) 345 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 346 | @_section("__DATA_CONST,__swift5_tests") 347 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 348 | @_section("swift5_tests") 349 | #elseif os(Windows) 350 | @_section(".sw5test$B") 351 | #endif 352 | @_used 353 | #endif 354 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 355 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 356 | 0x706c6179, /* 'play' */ 357 | 0, 358 | { outValue, type, hint, _ in 359 | Playgrounds.__store( 360 | "Named playground with closure as body argument", 361 | __macro_local_15PlaygroundEntryfMu_, 362 | at: outValue, 363 | asTypeAt: type, 364 | withHintAt: hint 365 | ) 366 | }, 367 | 0, 368 | 0 369 | ) 370 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 371 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 372 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 373 | __macro_local_23PlaygroundContentRecordfMu_ 374 | } 375 | } 376 | """, 377 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 378 | ) 379 | { failure in 380 | Issue.record(Comment(rawValue: failure.message)) 381 | } 382 | } 383 | 384 | @Test("Unnamed playground with closure as body argument") 385 | func unnamedPlaygroundWithClosureAsBodyArgumentTest() throws { 386 | assertMacroExpansion( 387 | """ 388 | #Playground(body: { 389 | let metadata = unnamedPlaygroundWithClosureAsBodyArgument 390 | let random = Int.random(in: 1...100) 391 | print("\\(metadata.displayName): random = \\(random)") 392 | metadata.wasExecuted = true 393 | print("\\(metadata.displayName): was executed: \\(metadata.wasExecuted)") 394 | }) 395 | """, 396 | expandedSource: 397 | """ 398 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 399 | try await body() 400 | } 401 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 402 | struct $__Marker { 403 | let utf8offset: Int 404 | @discardableResult init(utf8offset: Int) { 405 | self.utf8offset = utf8offset 406 | } 407 | } 408 | try await __macro_local_17PlaygroundRunFuncfMu_(body: { 409 | $__Marker(utf8offset: 22);let metadata = unnamedPlaygroundWithClosureAsBodyArgument 410 | let random = Int.random(in: 1...100) 411 | print("\\(metadata.displayName): random = \\(random)") 412 | metadata.wasExecuted = true 413 | print("\\(metadata.displayName): was executed: \\(metadata.wasExecuted)") 414 | }) 415 | } 416 | #if hasFeature(SymbolLinkageMarkers) 417 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 418 | @_section("__DATA_CONST,__swift5_tests") 419 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 420 | @_section("swift5_tests") 421 | #elseif os(Windows) 422 | @_section(".sw5test$B") 423 | #endif 424 | @_used 425 | #endif 426 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 427 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 428 | 0x706c6179, /* 'play' */ 429 | 0, 430 | { outValue, type, hint, _ in 431 | Playgrounds.__store( 432 | nil, 433 | __macro_local_15PlaygroundEntryfMu_, 434 | at: outValue, 435 | asTypeAt: type, 436 | withHintAt: hint 437 | ) 438 | }, 439 | 0, 440 | 0 441 | ) 442 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 443 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 444 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 445 | __macro_local_23PlaygroundContentRecordfMu_ 446 | } 447 | } 448 | """, 449 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 450 | ) 451 | { failure in 452 | Issue.record(Comment(rawValue: failure.message)) 453 | } 454 | } 455 | 456 | @Test("Named playground with function reference as body argument") 457 | func namedPlaygroundWithFunctionReferenceAsBodyArgumentTest() throws { 458 | assertMacroExpansion( 459 | """ 460 | #Playground("Named playground with function reference as body argument", body: namedPlaygroundWithFunctionReferenceAsBodyArgumentBody) 461 | """, 462 | expandedSource: 463 | """ 464 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 465 | try await body() 466 | } 467 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 468 | struct $__Marker { 469 | let utf8offset: Int 470 | @discardableResult init(utf8offset: Int) { 471 | self.utf8offset = utf8offset 472 | } 473 | } 474 | try await __macro_local_17PlaygroundRunFuncfMu_("Named playground with function reference as body argument", body: namedPlaygroundWithFunctionReferenceAsBodyArgumentBody) 475 | } 476 | #if hasFeature(SymbolLinkageMarkers) 477 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 478 | @_section("__DATA_CONST,__swift5_tests") 479 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 480 | @_section("swift5_tests") 481 | #elseif os(Windows) 482 | @_section(".sw5test$B") 483 | #endif 484 | @_used 485 | #endif 486 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 487 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 488 | 0x706c6179, /* 'play' */ 489 | 0, 490 | { outValue, type, hint, _ in 491 | Playgrounds.__store( 492 | "Named playground with function reference as body argument", 493 | __macro_local_15PlaygroundEntryfMu_, 494 | at: outValue, 495 | asTypeAt: type, 496 | withHintAt: hint 497 | ) 498 | }, 499 | 0, 500 | 0 501 | ) 502 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 503 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 504 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 505 | __macro_local_23PlaygroundContentRecordfMu_ 506 | } 507 | } 508 | """, 509 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 510 | ) 511 | { failure in 512 | Issue.record(Comment(rawValue: failure.message)) 513 | } 514 | } 515 | 516 | @Test("Unnamed playground with function reference as body argument") 517 | func unnamedPlaygroundWithFunctionReferenceAsBodyArgumentTest() throws { 518 | assertMacroExpansion( 519 | """ 520 | #Playground(body: unnamedPlaygroundWithFunctionReferenceAsBodyArgumentBody) 521 | """, 522 | expandedSource: 523 | """ 524 | @MainActor @Sendable private func __macro_local_17PlaygroundRunFuncfMu_(_ name: String? = nil, body: @MainActor @Sendable () async throws -> ()) async throws { 525 | try await body() 526 | } 527 | @MainActor @Sendable private func __macro_local_15PlaygroundEntryfMu_() async throws { 528 | struct $__Marker { 529 | let utf8offset: Int 530 | @discardableResult init(utf8offset: Int) { 531 | self.utf8offset = utf8offset 532 | } 533 | } 534 | try await __macro_local_17PlaygroundRunFuncfMu_(body: unnamedPlaygroundWithFunctionReferenceAsBodyArgumentBody) 535 | } 536 | #if hasFeature(SymbolLinkageMarkers) 537 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) 538 | @_section("__DATA_CONST,__swift5_tests") 539 | #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) 540 | @_section("swift5_tests") 541 | #elseif os(Windows) 542 | @_section(".sw5test$B") 543 | #endif 544 | @_used 545 | #endif 546 | @available(*, deprecated, message: "This property is an implementation detail of the playgrounds library. Do not use it directly.") 547 | private let __macro_local_23PlaygroundContentRecordfMu_: Playgrounds.__PlaygroundsContentRecord = ( 548 | 0x706c6179, /* 'play' */ 549 | 0, 550 | { outValue, type, hint, _ in 551 | Playgrounds.__store( 552 | nil, 553 | __macro_local_15PlaygroundEntryfMu_, 554 | at: outValue, 555 | asTypeAt: type, 556 | withHintAt: hint 557 | ) 558 | }, 559 | 0, 560 | 0 561 | ) 562 | @available(*, deprecated, message: "This type is an implementation detail of the playgrounds library. Do not use it directly.") 563 | enum __macro_local_36__🟡$PlaygroundContentRecordContainerfMu_: Playgrounds.__PlaygroundsContentRecordContainer { 564 | nonisolated static var __playgroundsContentRecord: Playgrounds.__PlaygroundsContentRecord { 565 | __macro_local_23PlaygroundContentRecordfMu_ 566 | } 567 | } 568 | """, 569 | macroSpecs: ["Playground" : MacroSpec(type: Playground.self, conformances: [])] 570 | ) 571 | { failure in 572 | Issue.record(Comment(rawValue: failure.message)) 573 | } 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /Tests/PlaygroundsTests/PlaygroundsToolsAPITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This source file is part of the Swift Play Experimental open source project 3 | // 4 | // Copyright (c) 2025 Apple Inc. and the Swift Play Experimental project authors 5 | // Licensed under Apache License v2.0 with Runtime Library Exception 6 | // 7 | // See https://swift.org/LICENSE.txt for license information 8 | // 9 | 10 | import Playgrounds 11 | import Testing 12 | 13 | // MARK: - Testable Playground Metadata - 14 | 15 | @MainActor class TestablePlaygroundMetadata { 16 | internal init(name: String? = nil, playgroundLineNumber: Int, playgroundColumnNumber: Int = 1, playgroundFile: String = #file) { 17 | self.name = name 18 | self.playgroundLineNumber = playgroundLineNumber 19 | self.playgroundColumnNumber = playgroundColumnNumber 20 | self.playgroundFile = playgroundFile 21 | } 22 | 23 | let name: String? 24 | var wasExecuted: Bool = false 25 | let playgroundLineNumber: Int 26 | let playgroundFile: String 27 | let playgroundColumnNumber: Int 28 | 29 | var displayName: String { 30 | name ?? "\(playgroundFile):\(playgroundLineNumber):\(playgroundColumnNumber)" 31 | } 32 | } 33 | 34 | 35 | // MARK: - Playgrounds to Test - 36 | 37 | /// Named playground with trailing closure 38 | @MainActor fileprivate var namedPlaygroundWithTrailingClosure = TestablePlaygroundMetadata(name: "Named Playground with trailing closure", playgroundLineNumber: #line + 1) 39 | #Playground("Named Playground with trailing closure") { 40 | let metadata = namedPlaygroundWithTrailingClosure 41 | let random = Int.random(in: 1...100) 42 | print("\(metadata.displayName): random = \(random)") 43 | metadata.wasExecuted = true 44 | print("\(metadata.displayName) was executed: \(metadata.wasExecuted)") 45 | } 46 | 47 | /// Unnamed playground with trailing closure 48 | @MainActor fileprivate var unnamedPlaygroundWithTrailingClosure = TestablePlaygroundMetadata(playgroundLineNumber: #line + 1) 49 | #Playground { 50 | let metadata = unnamedPlaygroundWithTrailingClosure 51 | let random = Int.random(in: 1...100) 52 | print("\(metadata.displayName): random = \(random)") 53 | metadata.wasExecuted = true 54 | print("\(metadata.displayName) was executed: \(metadata.wasExecuted)") 55 | } 56 | 57 | /// Named Playground with trailing closure containing in arg 58 | @MainActor fileprivate var namedPlaygroundWithTrailingClosureContainingInArg = TestablePlaygroundMetadata(name: "Named Playground with trailing closure containing in arg", playgroundLineNumber: #line + 1) 59 | #Playground("Named Playground with trailing closure containing in arg") { () -> Void in 60 | let metadata = namedPlaygroundWithTrailingClosureContainingInArg 61 | let random = Int.random(in: 1...100) 62 | print("\(metadata.displayName) random = \(random)") 63 | metadata.wasExecuted = true 64 | print("\(metadata.displayName) was executed: \(metadata.wasExecuted)") 65 | } 66 | 67 | /// Unnamed Playground with trailing closure containing in arg 68 | @MainActor fileprivate var unnamedPlaygroundWithTrailingClosureContainingInArg = TestablePlaygroundMetadata(playgroundLineNumber: #line + 1) 69 | #Playground { () -> Void in 70 | let metadata = unnamedPlaygroundWithTrailingClosureContainingInArg 71 | let random = Int.random(in: 1...100) 72 | print("\(metadata.displayName) random = \(random)") 73 | metadata.wasExecuted = true 74 | print("\(metadata.displayName) was executed: \(metadata.wasExecuted)") 75 | } 76 | 77 | /// Named playground with closure as body argument 78 | @MainActor fileprivate var namedPlaygroundWithClosureAsBodyArgument = TestablePlaygroundMetadata(name: "Named playground with closure as body argument", playgroundLineNumber: #line + 1) 79 | #Playground("Named playground with closure as body argument", body: { 80 | let metadata = namedPlaygroundWithClosureAsBodyArgument 81 | let random = Int.random(in: 1...100) 82 | print("\(metadata.displayName): random = \(random)") 83 | metadata.wasExecuted = true 84 | print("\(metadata.displayName): was executed: \(metadata.wasExecuted)") 85 | }) 86 | 87 | /// Unnamed playground with closure as body argument 88 | @MainActor fileprivate var unnamedPlaygroundWithClosureAsBodyArgument = TestablePlaygroundMetadata(playgroundLineNumber: #line + 1) 89 | #Playground(body: { 90 | let metadata = unnamedPlaygroundWithClosureAsBodyArgument 91 | let random = Int.random(in: 1...100) 92 | print("\(metadata.displayName): random = \(random)") 93 | metadata.wasExecuted = true 94 | print("\(metadata.displayName): was executed: \(metadata.wasExecuted)") 95 | }) 96 | 97 | /// Named playground with function reference as body argument 98 | @MainActor fileprivate var namedPlaygroundWithFunctionReferenceAsBodyArgument = TestablePlaygroundMetadata(name: "Named playground with function reference as body argument", playgroundLineNumber: #line + 8) 99 | @MainActor fileprivate func namedPlaygroundWithFunctionReferenceAsBodyArgumentBody() { 100 | let metadata = namedPlaygroundWithFunctionReferenceAsBodyArgument 101 | let random = Int.random(in: 1...100) 102 | print("\(metadata.displayName): random = \(random)") 103 | metadata.wasExecuted = true 104 | print("\(metadata.displayName): was executed: \(metadata.wasExecuted)") 105 | } 106 | #Playground("Named playground with function reference as body argument", body: namedPlaygroundWithFunctionReferenceAsBodyArgumentBody) 107 | 108 | /// Unnamed playground with function reference as body argument 109 | @MainActor fileprivate var unnamedPlaygroundWithFunctionReferenceAsBodyArgument = TestablePlaygroundMetadata(playgroundLineNumber: #line + 8) 110 | @MainActor fileprivate func unnamedPlaygroundWithFunctionReferenceAsBodyArgumentBody() { 111 | let metadata = unnamedPlaygroundWithFunctionReferenceAsBodyArgument 112 | let random = Int.random(in: 1...100) 113 | print("\(metadata.displayName): random = \(random)") 114 | metadata.wasExecuted = true 115 | print("\(metadata.displayName): was executed: \(metadata.wasExecuted)") 116 | } 117 | #Playground(body: unnamedPlaygroundWithFunctionReferenceAsBodyArgumentBody) 118 | 119 | 120 | // MARK: - All Testable Playground Metadata - 121 | 122 | @MainActor fileprivate var allTestablePlaygroundMetadata: [TestablePlaygroundMetadata] = [ 123 | namedPlaygroundWithTrailingClosure, 124 | unnamedPlaygroundWithTrailingClosure, 125 | namedPlaygroundWithTrailingClosureContainingInArg, 126 | unnamedPlaygroundWithTrailingClosureContainingInArg, 127 | namedPlaygroundWithClosureAsBodyArgument, 128 | unnamedPlaygroundWithClosureAsBodyArgument, 129 | namedPlaygroundWithFunctionReferenceAsBodyArgument, 130 | unnamedPlaygroundWithFunctionReferenceAsBodyArgument, 131 | ] 132 | 133 | 134 | // MARK: - Tests - 135 | 136 | @Suite("Playgrounds Tools API Tests", .serialized) 137 | struct PlaygroundsToolsAPITests { 138 | 139 | @Test("Playground definition variations", arguments: await allTestablePlaygroundMetadata) 140 | func testPlaygroundDefinitionVariations(metadata: TestablePlaygroundMetadata) async throws { 141 | let playground: __Playground 142 | 143 | if let playgroundName = metadata.name { 144 | playground = try #require(__Playground.__getPlayground(named: playgroundName)) 145 | } 146 | else { 147 | let identifier = __Playground.__ID(__fileID: metadata.playgroundFile, __line: metadata.playgroundLineNumber, __column: metadata.playgroundColumnNumber) 148 | playground = try #require(__Playground.__getPlayground(identifiedBy: identifier)) 149 | } 150 | 151 | print("[\(#function)] Running Playground \"\(playground.__displayName)\"") 152 | try await playground.__run() 153 | 154 | print("[\(#function)] Checking wasExecuted value: \(await metadata.wasExecuted)") 155 | #expect(await metadata.wasExecuted == true) 156 | 157 | #expect(playground.__id.__line == metadata.playgroundLineNumber) 158 | } 159 | 160 | @Test("Find Playground by ID") 161 | func testFindPlaygroundByID() async throws { 162 | let playground1 = try #require(__Playground.__allPlaygrounds().first) 163 | let playground2 = try #require(__Playground.__getPlayground(identifiedBy: playground1.__id)) 164 | #expect(playground1.__id == playground2.__id) 165 | } 166 | 167 | @MainActor 168 | @Test("Fetch all Playgrounds") 169 | func testFetchAllPlaygrounds() async throws { 170 | let playgrounds = __Playground.__allPlaygrounds() 171 | print("Found \(playgrounds.count) Playground records: \(playgrounds)") 172 | 173 | #expect(playgrounds.count == allTestablePlaygroundMetadata.count) 174 | 175 | let expectedNames = allTestablePlaygroundMetadata.map { $0.displayName }.sorted() 176 | let names = playgrounds.map { $0.__displayName }.sorted() 177 | #expect(names == expectedNames) 178 | 179 | let expectedFileIDs = allTestablePlaygroundMetadata.map { $0.playgroundFile }.sorted() 180 | let fileIDs = playgrounds.map(\.__id.__fileID).sorted() 181 | #expect(fileIDs == expectedFileIDs) 182 | 183 | let expectedLineNumbers = allTestablePlaygroundMetadata.map { $0.playgroundLineNumber }.sorted() 184 | let lineNumbers = playgrounds.map(\.__id.__line).sorted() 185 | #expect(lineNumbers == expectedLineNumbers) 186 | 187 | let expectedColumnNumbers = allTestablePlaygroundMetadata.map { $0.playgroundColumnNumber }.sorted() 188 | let columnNumbers = playgrounds.map(\.__id.__column).sorted() 189 | #expect(columnNumbers == expectedColumnNumbers) 190 | } 191 | 192 | } 193 | --------------------------------------------------------------------------------