-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Observation and associated macros #63725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#===--- CMakeLists.txt - Macro support libraries ------------------------===# | ||
# | ||
# This source file is part of the Swift.org open source project | ||
# | ||
# Copyright (c) 2023 Apple Inc. and the Swift project authors | ||
# Licensed under Apache License v2.0 with Runtime Library Exception | ||
# | ||
# See https://swift.org/LICENSE.txt for license information | ||
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
# | ||
#===----------------------------------------------------------------------===# | ||
|
||
add_subdirectory(Sources/ObservationMacros) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
#===--- CMakeLists.txt - Observation macros library ----------------------===# | ||
# | ||
# This source file is part of the Swift.org open source project | ||
# | ||
# Copyright (c) 2023 Apple Inc. and the Swift project authors | ||
# Licensed under Apache License v2.0 with Runtime Library Exception | ||
# | ||
# See https://swift.org/LICENSE.txt for license information | ||
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
# | ||
#===----------------------------------------------------------------------===# | ||
|
||
if (SWIFT_SWIFT_PARSER) | ||
add_library(ObservationMacros SHARED | ||
ObservableMacro.swift) | ||
|
||
set_target_properties(ObservationMacros | ||
PROPERTIES | ||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/swift/host/plugins" | ||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/swift/host/plugins" | ||
) | ||
|
||
target_compile_options(ObservationMacros PRIVATE | ||
$<$<COMPILE_LANGUAGE:Swift>:-runtime-compatibility-version> | ||
$<$<COMPILE_LANGUAGE:Swift>:none>) | ||
|
||
# Set the appropriate target triple. | ||
if(SWIFT_HOST_VARIANT_SDK IN_LIST SWIFT_DARWIN_PLATFORMS) | ||
set(DEPLOYMENT_VERSION "${SWIFT_SDK_${SWIFT_HOST_VARIANT_SDK}_DEPLOYMENT_VERSION}") | ||
endif() | ||
|
||
if(SWIFT_HOST_VARIANT_SDK STREQUAL ANDROID) | ||
set(DEPLOYMENT_VERSION ${SWIFT_ANDROID_API_LEVEL}) | ||
endif() | ||
|
||
get_target_triple(target target_variant "${SWIFT_HOST_VARIANT_SDK}" "${SWIFT_HOST_VARIANT_ARCH}" | ||
MACCATALYST_BUILD_FLAVOR "" | ||
DEPLOYMENT_VERSION "${DEPLOYMENT_VERSION}") | ||
|
||
target_compile_options(ObservationMacros PRIVATE $<$<COMPILE_LANGUAGE:Swift>:-target;${target}>) | ||
|
||
# Workaround a cmake bug, see the corresponding function in swift-syntax | ||
function(force_target_macros_link_libraries TARGET) | ||
cmake_parse_arguments(ARGS "" "" "PUBLIC" ${ARGN}) | ||
|
||
foreach(DEPENDENCY ${ARGS_PUBLIC}) | ||
target_link_libraries(${TARGET} PRIVATE | ||
${DEPENDENCY} | ||
) | ||
add_dependencies(${TARGET} ${DEPENDENCY}) | ||
|
||
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/forced-${DEPENDENCY}-dep.swift | ||
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/forced-${DEPENDENCY}-dep.swift | ||
DEPENDS ${DEPENDENCY} | ||
) | ||
target_sources(${TARGET} PRIVATE | ||
${CMAKE_CURRENT_BINARY_DIR}/forced-${DEPENDENCY}-dep.swift | ||
) | ||
endforeach() | ||
endfunction() | ||
|
||
set(SWIFT_SYNTAX_MODULES | ||
SwiftSyntax | ||
SwiftSyntaxMacros | ||
) | ||
|
||
# Compute the list of SwiftSyntax targets | ||
list(TRANSFORM SWIFT_SYNTAX_MODULES PREPEND "SwiftSyntax::" | ||
OUTPUT_VARIABLE SWIFT_SYNTAX_TARGETS) | ||
|
||
# TODO: Change to target_link_libraries when cmake is fixed | ||
force_target_link_libraries(ObservationMacros PUBLIC | ||
${SWIFT_SYNTAX_TARGETS} | ||
) | ||
|
||
set(SWIFT_SYNTAX_LIBRARIES_SOURCE_DIR | ||
"${SWIFT_PATH_TO_EARLYSWIFTSYNTAX_BUILD_DIR}/lib/swift/host") | ||
set(SWIFT_SYNTAX_LIBRARIES_DEST_DIR | ||
"${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/swift/host") | ||
|
||
# Determine the SwiftSyntax shared library files that were built as | ||
# part of earlyswiftsyntax. | ||
list(TRANSFORM SWIFT_SYNTAX_MODULES PREPEND ${CMAKE_SHARED_LIBRARY_PREFIX} | ||
OUTPUT_VARIABLE SWIFT_SYNTAX_SHARED_LIBRARIES) | ||
list(TRANSFORM SWIFT_SYNTAX_SHARED_LIBRARIES APPEND | ||
${CMAKE_SHARED_LIBRARY_SUFFIX} | ||
OUTPUT_VARIABLE SWIFT_SYNTAX_SHARED_LIBRARIES) | ||
|
||
# Copy over all of the shared libraries from earlyswiftsyntax so they can | ||
# be found via RPATH. | ||
foreach (sharedlib ${SWIFT_SYNTAX_SHARED_LIBRARIES}) | ||
add_custom_command( | ||
TARGET ObservationMacros PRE_BUILD | ||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${SWIFT_SYNTAX_LIBRARIES_SOURCE_DIR}/${sharedlib} ${SWIFT_SYNTAX_LIBRARIES_DEST_DIR}/${sharedlib} | ||
COMMENT "Copying ${sharedlib}" | ||
) | ||
endforeach() | ||
|
||
Comment on lines
+89
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why we need this. ASTGen should already have dealt with whatever is needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. most of this was copy-pasta from ASTGen |
||
# Copy all of the Swift modules from earlyswiftsyntax so they can be found | ||
# in the same relative place within the build directory as in the final | ||
# toolchain. | ||
list(TRANSFORM SWIFT_SYNTAX_MODULES APPEND ".swiftmodule" | ||
OUTPUT_VARIABLE SWIFT_SYNTAX_MODULE_DIRS) | ||
foreach(module_dir ${SWIFT_SYNTAX_MODULE_DIRS}) | ||
file(GLOB module_files | ||
"${SWIFT_SYNTAX_LIBRARIES_SOURCE_DIR}/${module_dir}/*.swiftinterface") | ||
add_custom_command( | ||
TARGET ObservationMacros PRE_BUILD | ||
COMMAND ${CMAKE_COMMAND} -E make_directory ${SWIFT_SYNTAX_LIBRARIES_DEST_DIR}/${module_dir} | ||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${module_files} ${SWIFT_SYNTAX_LIBRARIES_DEST_DIR}/${module_dir}/ | ||
COMMENT "Copying ${module_dir}" | ||
) | ||
endforeach() | ||
|
||
target_include_directories(ObservationMacros PUBLIC | ||
${SWIFT_SYNTAX_LIBRARIES_DEST_DIR}) | ||
|
||
# Ensure the install directory exists before everything gets started | ||
add_custom_command( | ||
TARGET ObservationMacros PRE_BUILD | ||
COMMAND ${CMAKE_COMMAND} -E make_directory "lib${LLVM_LIBDIR_SUFFIX}/host/plugins" | ||
) | ||
|
||
swift_install_in_component(TARGETS ObservationMacros | ||
LIBRARY | ||
DESTINATION "lib${LLVM_LIBDIR_SUFFIX}/host/plugins" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This path seems like it's missing a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whoops, that is definitely missing - will follow up on that |
||
COMPONENT compiler | ||
ARCHIVE | ||
DESTINATION "lib${LLVM_LIBDIR_SUFFIX}/host/plugins" | ||
COMPONENT compiler) | ||
|
||
set_property(GLOBAL APPEND PROPERTY SWIFT_EXPORTS ObservationMacros) | ||
|
||
endif() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxMacros | ||
|
||
@_implementationOnly import SwiftDiagnostics | ||
@_implementationOnly import SwiftOperators | ||
@_implementationOnly import SwiftSyntaxBuilder | ||
|
||
private extension DeclSyntaxProtocol { | ||
var isObservableStoredProperty: Bool { | ||
guard let property = self.as(VariableDeclSyntax.self), | ||
let binding = property.bindings.first | ||
else { | ||
return false | ||
} | ||
|
||
return binding.accessor == nil | ||
} | ||
} | ||
|
||
public struct ObservableMacro: MemberMacro, MemberAttributeMacro, ConformanceMacro { | ||
// MARK: - ConformanceMacro | ||
public static func expansion< | ||
Declaration: DeclGroupSyntax, | ||
Context: MacroExpansionContext | ||
>( | ||
of node: AttributeSyntax, | ||
providingConformancesOf declaration: Declaration, | ||
in context: Context | ||
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { | ||
let protocolName: TypeSyntax = "Observable" | ||
return [(protocolName, nil)] | ||
} | ||
|
||
// MARK: - MemberMacro | ||
public static func expansion< | ||
Declaration: DeclGroupSyntax, | ||
Context: MacroExpansionContext | ||
>( | ||
of node: AttributeSyntax, | ||
providingMembersOf declaration: Declaration, | ||
in context: Context | ||
) throws -> [DeclSyntax] { | ||
guard let identified = declaration.asProtocol(IdentifiedDeclSyntax.self) else { | ||
return [] | ||
} | ||
|
||
let parentName = identified.identifier | ||
|
||
let registrar: DeclSyntax = | ||
""" | ||
let _registrar = ObservationRegistrar<\(parentName)>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably QoI work, but what's the developer experience going to be like if there's already a I assume they'll have to use this registrar if they're implementing their own tracking e.g. for computed properties — is there any other kind of control we should provide? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The registrar cannot be private or file private since they may have computed properties in extensions or extensions in other files. I guess it could be explicitly |
||
""" | ||
|
||
let transactions: DeclSyntax = | ||
""" | ||
public nonisolated func transactions<Delivery>(for keyPaths: KeyPaths<\(parentName)>, isolation: Delivery) -> ObservedTransactions<\(parentName), Delivery> where Delivery: Actor { | ||
_registrar.transactions(for: keyPaths, isolation: isolation) | ||
} | ||
""" | ||
|
||
let changes: DeclSyntax = | ||
""" | ||
public nonisolated func changes<Member>(for keyPath: KeyPath<\(parentName), Member>) -> ObservedChanges<\(parentName), Member> where Member: Sendable { | ||
_registrar.changes(for: keyPath) | ||
} | ||
""" | ||
|
||
let memberList = MemberDeclListSyntax( | ||
declaration.members.members.filter { | ||
$0.decl.isObservableStoredProperty | ||
} | ||
) | ||
|
||
let storageStruct: DeclSyntax = | ||
""" | ||
private struct _Storage { | ||
\(memberList) | ||
} | ||
""" | ||
|
||
let storage: DeclSyntax = | ||
""" | ||
private var _storage = _Storage() | ||
""" | ||
|
||
return [ | ||
registrar, | ||
transactions, | ||
changes, | ||
storageStruct, | ||
storage, | ||
] | ||
} | ||
|
||
// MARK: - MemberAttributeMacro | ||
|
||
public static func expansion< | ||
Declaration: DeclGroupSyntax, | ||
MemberDeclaration: DeclSyntaxProtocol, | ||
Context: MacroExpansionContext | ||
>( | ||
of node: AttributeSyntax, | ||
attachedTo declaration: Declaration, | ||
providingAttributesFor member: MemberDeclaration, | ||
in context: Context | ||
) throws -> [AttributeSyntax] { | ||
guard member.isObservableStoredProperty else { | ||
return [] | ||
} | ||
|
||
return [ | ||
AttributeSyntax( | ||
attributeName: SimpleTypeIdentifierSyntax( | ||
name: .identifier("ObservableProperty") | ||
) | ||
) | ||
] | ||
} | ||
} | ||
|
||
public struct ObservablePropertyMacro: AccessorMacro { | ||
public static func expansion< | ||
Context: MacroExpansionContext, | ||
Declaration: DeclSyntaxProtocol | ||
>( | ||
of node: AttributeSyntax, | ||
providingAccessorsOf declaration: Declaration, | ||
in context: Context | ||
) throws -> [AccessorDeclSyntax] { | ||
guard let property = declaration.as(VariableDeclSyntax.self), | ||
let binding = property.bindings.first, | ||
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier, | ||
binding.accessor == nil | ||
else { | ||
return [] | ||
} | ||
|
||
if identifier.text == "_registrar" || identifier.text == "_storage" { return [] } | ||
|
||
let getAccessor: AccessorDeclSyntax = | ||
""" | ||
get { | ||
_registrar.access(self, keyPath: \\.\(identifier)) | ||
return _storage.\(identifier) | ||
} | ||
""" | ||
|
||
let setAccessor: AccessorDeclSyntax = | ||
""" | ||
set { | ||
_registrar.withMutation(of: self, keyPath: \\.\(identifier)) { | ||
_storage.\(identifier) = newValue | ||
} | ||
} | ||
""" | ||
|
||
return [getAccessor, setAccessor] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#===--- CMakeLists.txt - Observation support library ---------------------===# | ||
# | ||
# This source file is part of the Swift.org open source project | ||
# | ||
# Copyright (c) 2023 Apple Inc. and the Swift project authors | ||
# Licensed under Apache License v2.0 with Runtime Library Exception | ||
# | ||
# See https://swift.org/LICENSE.txt for license information | ||
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
# | ||
#===----------------------------------------------------------------------===# | ||
|
||
add_subdirectory(Sources/Observation) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a big pile of CMake that it would be really nice to share with ASTGen and any other macro implementations we might land. That refactor doesn't have to happen with your PR necessarily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea I was tempted to generalize this into a macro library specific function - but my rush was to get things in first and then have follow-on tasks (where we have more time) to clean that up.