Skip to content

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

Merged
merged 2 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,10 @@ option(SWIFT_ENABLE_EXPERIMENTAL_REFLECTION
"Enable build of the Swift reflection module"
FALSE)

option(SWIFT_ENABLE_EXPERIMENTAL_OBSERVATION
"Enable build of the Swift observation module"
FALSE)

option(SWIFT_ENABLE_DISPATCH
"Enable use of libdispatch"
TRUE)
Expand Down Expand Up @@ -1117,6 +1121,7 @@ if(SWIFT_BUILD_STDLIB OR SWIFT_BUILD_SDK_OVERLAY)
message(STATUS "String Processing Support: ${SWIFT_ENABLE_EXPERIMENTAL_STRING_PROCESSING}")
message(STATUS "Unicode Support: ${SWIFT_STDLIB_ENABLE_UNICODE_DATA}")
message(STATUS "Reflection Support: ${SWIFT_ENABLE_EXPERIMENTAL_REFLECTION}")
message(STATUS "Observation Support: ${SWIFT_ENABLE_EXPERIMENTAL_OBSERVATION}")
message(STATUS "")
else()
message(STATUS "Not building Swift standard library, SDK overlays, and runtime")
Expand Down
2 changes: 2 additions & 0 deletions include/swift/Threading/Impl/Darwin.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ inline tls_key_t tls_get_key(tls_key k) {
return __PTK_FRAMEWORK_SWIFT_KEY4;
case tls_key::concurrency_fallback:
return __PTK_FRAMEWORK_SWIFT_KEY5;
case tls_key::observation_transaction:
return __PTK_FRAMEWORK_SWIFT_KEY6;
}
}

Expand Down
3 changes: 2 additions & 1 deletion include/swift/Threading/TLSKeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ enum class tls_key {
compatibility50,
concurrency_task,
concurrency_executor_tracking_info,
concurrency_fallback
concurrency_fallback,
observation_transaction
};

} // namespace swift
Expand Down
1 change: 1 addition & 0 deletions lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ add_subdirectory(Immediate)
add_subdirectory(IRGen)
add_subdirectory(LLVMPasses)
add_subdirectory(Localization)
add_subdirectory(Macros)
add_subdirectory(Markup)
add_subdirectory(Migrator)
add_subdirectory(Option)
Expand Down
13 changes: 13 additions & 0 deletions lib/Macros/CMakeLists.txt
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)
134 changes: 134 additions & 0 deletions lib/Macros/Sources/ObservationMacros/CMakeLists.txt
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
Copy link
Member

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.

Copy link
Contributor Author

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.

"${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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path seems like it's missing a swift?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
159 changes: 159 additions & 0 deletions lib/Macros/Sources/ObservationMacros/ObservableMacro.swift
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)>()
Copy link
Member

Choose a reason for hiding this comment

The 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 _registrar or _storage property implemented on the type? Can we detect that and provide a better error message?

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 internal if we wanted.

"""

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]
}
}
4 changes: 4 additions & 0 deletions stdlib/cmake/modules/SwiftSource.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ function(_add_target_variant_swift_compile_flags
list(APPEND result "-D" "SWIFT_ENABLE_EXPERIMENTAL_REFLECTION")
endif()

if(SWIFT_ENABLE_EXPERIMENTAL_OBSERVATION)
list(APPEND result "-D" "SWIFT_ENABLE_EXPERIMENTAL_OBSERVATION")
endif()

if(SWIFT_STDLIB_OS_VERSIONING)
list(APPEND result "-D" "SWIFT_RUNTIME_OS_VERSIONING")
endif()
Expand Down
4 changes: 4 additions & 0 deletions stdlib/public/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ if(SWIFT_BUILD_STDLIB)
if(SWIFT_ENABLE_EXPERIMENTAL_REFLECTION)
add_subdirectory(Reflection)
endif()

if(SWIFT_ENABLE_EXPERIMENTAL_OBSERVATION)
add_subdirectory(Observation)
endif()
endif()

if(SWIFT_BUILD_STDLIB OR SWIFT_BUILD_REMOTE_MIRROR)
Expand Down
13 changes: 13 additions & 0 deletions stdlib/public/Observation/CMakeLists.txt
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)
Loading