-
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
Conversation
@swift-ci please build toolchain |
@swift-ci please build toolchain |
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 going to require an associated change to apple/swift-installer-script to package up the new modules in the SDKs and Runtime .
stdlib/public/Observation/Sources/_ObservationRuntime/ThreadLocal.cpp
Outdated
Show resolved
Hide resolved
stdlib/public/Observation/Sources/_ObservationRuntime/ThreadLocal.cpp
Outdated
Show resolved
Hide resolved
@swift-ci please build toolchain |
1 similar comment
@swift-ci please build toolchain |
The
and do what you will with them. |
f82be48
to
4df6010
Compare
@swift-ci please smoke test |
@swift-ci please build toolchain |
@swift-ci please smoke test |
1 similar comment
@swift-ci please smoke test |
func _tlsSet(_ value: UnsafeMutableRawPointer?) | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
struct ThreadLocal { |
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.
These likely should instead use the existing infra in: stdlib/public/core/ThreadLocalStorage.swift
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.
I need TLS keys that are specific - that set of APIs from what I can tell does not do that.
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.
Then add API over there rather than a new parallel world here perhaps?
46bc547
to
f5fa395
Compare
@swift-ci please smoke test |
@swift-ci please smoke test |
1 similar comment
@swift-ci please smoke test |
f55c899
to
9cafbe5
Compare
@swift-ci please smoke test |
1 similar comment
@swift-ci please smoke test |
@swift-ci please smoke test |
@swift-ci please smoke test |
@swift-ci please build toolchain macosx |
1 similar comment
@swift-ci please build toolchain macosx |
@swift-ci please smoke test |
4 similar comments
@swift-ci please smoke test |
@swift-ci please smoke test |
@swift-ci please smoke test |
@swift-ci please smoke test |
@swift-ci please build toolchain macosx |
@swift-ci please smoke test |
|
||
private let buffer: ManagedBuffer<State, UInt8> | ||
|
||
init(_ buffer: ManagedBuffer<State, UInt8>) { |
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.
Nit: In the stdlib, we prefer to always spell out internal
, even if it's the implicit default. (This is to avoid having to figure it out from context.)
} | ||
|
||
let kind: Kind | ||
var collected: KeyPaths<Subject>? |
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.
Nit: This public struct does not appear to have an explicit initializer -- it's a good idea to add one explicitly, even if it matches whatever the compiler synthesizes on its own.)
(Quick, which struct do these belong to? See other nit about presentation of type definitions.)
let context = Context() | ||
let lifetime: Lifetime |
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.
Nit: Readers can't easily tell where these declarations will end up -- we need to carefully hunt down the start of this overlong declaration a couple hundred lines above. It's not polite to put a myriad lines in between a struct's name and its stored properties.
(See Presentation of type definitions in the manual.)
To ease reading/understanding type declarations, we prefer to define members in the following order:
- Crucial type aliases and nested types, not exceeding a handful of lines in length
- Stored properties
- Initializers
- Any other instance members (methods, computed properties, etc)
Please keep all stored properties together in a single uninterrupted list, followed immediately by the type's most crucial initializer(s). Put these as close to the top of the type declaration as possible -- we don't want to force readers to scroll around to find these core definitions.
...
In general, it is a good idea to keep the main
struct
/class
definition as short as possible: preferably it should consist of the type's stored properties and a handful of critical initializers, and nothing else.Everything else should go in standalone extensions, arranged by logical theme. For example, it's often nice to define protocol conformances in dedicated extensions. If it makes sense, feel free to add a comment to title these sectioning extensions.
...It's okay for the core type declaration to forward reference large nested types or static members that are defined in subsequent extensions. It's often a good idea to define these in an extension immediately following the type declaration, but this is not a strict rule.
func _lockUnlock(_: UnsafeRawPointer) | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
struct ManagedCriticalState<State> { |
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.
Crucial nit: In the stdlib, we strictly follow the leading underscore rule when naming internal types and functions.
All APIs that aren't part of the stdlib's official public API must include at least one underscored component in their fully qualified names. This includes symbols that are technically declared public but that aren't considered part of the public stdlib API, as well as @usableFromInline internal, plain internal and [file]private types and members.
...
This rule ensures we don't accidentally clutter the public namespace with @usableFromInline things (which could prevent us from choosing the best names for newly public API later), and it also makes it easy to see at a glance if a piece of stdlib code uses any non-public entities.
(Things declared purely internal have a remarkable tendency to become @usableFromInline
or @inlinable
before the release ships, usually as a side effect of performance work.)
} | ||
|
||
init(_ initial: State) { | ||
self.init(LockedBuffer.create(minimumCapacity: _lockSize()) { buffer in |
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 does not technically guarantee that the managed buffer's storage will have the right alignment to store a lock.
|
||
let registrar: DeclSyntax = | ||
""" | ||
let _registrar = ObservationRegistrar<\(parentName)>() |
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 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?
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.
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.
//===----------------------------------------------------------------------===// | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
public struct KeyPaths<Root>: SetAlgebra, Hashable, @unchecked Sendable { |
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.
I still think this will be better off without the SetAlgebra
conformance, especially since it will sometimes have members that aren't accessible by a developer who is tracking changes. Does the public API need to provide more than the sequence initializer and a contains(_:)
method?
The name also really implies collection conformance, but I don't think it's a dealbreaker. Does it need to have "key paths" in the name? TrackedProperties
would communicate the intent more, since it isn't really a general-purpose type.
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.
Equatable
conformance also seems suspect, since we can have KeyPaths
that are functionally equivalent as far as the user is concerned but that don't compare as equal due to inaccessible members. Is that a necessary conformance?
@available(SwiftStdlib 5.9, *) | ||
extension ObservedChanges: @unchecked Sendable where Subject: Sendable { } | ||
|
||
@available(*, unavailable) | ||
extension ObservedChanges.Iterator: Sendable { } |
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.
Why is the Sendable
conformance okay for the sequence, but not the iterator? It looks like the context is going to mutated out from under both.
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.
AsyncIterator
s must not be Sendable
. We learned this the hard way. The problem with them being Sendable
is that you cannot tie a specific consumer task to an iterator anymore. This makes it super hard to implement the AsyncSequence
itself because it has to suddenly handle multiple consumers on a single iterator even though it is a unicast AsyncSequence
. Our latest stance on this is that AsyncSequence
s should most of the time be Sendable
and can be passed to different tasks. Each task should create its own iterator to consume the sequence.
If an AsyncSequence
doesn't support multiple iterators we are fatalError
ing on this in most places right now.
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.
the AsyncSequence definition is just a description of what to observe which means that if the subject in question can be sent across tasks the the changes associated with said observation can also be sent across tasks. However the iteration infers stateful cancellation and mutation of the context of which next element is being iterated, that cannot be sent meaningfully because if one task cancels then all tasks would get nil, and more of an issue is that if one task consumes a next value that value is denied to other tasks.
So the sequence itself is sendable but the iterator is not.
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.
Interesting! This all sounds very similar to the headaches around copying a regular sequence iterator. (If we designed things from scratch today, perhaps iterators would not be required to be copyable.)
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.
Yeah, that's my reaction — it's surprising that you can make multiple copies of the iterator and then iterate them concurrently, drawing from the same underlying context
, but the Sendable
semantics don't explicitly ban such a system. The cancellation bit is what I was missing in the difference between the sequence(s) and iterator(s). Thanks!
} | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
extension ObservedChanges: @unchecked Sendable where Subject: Sendable { } |
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.
why @unchecked Sendable
here?
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.
due to KeyPath
not being Sendable
(but in this use case it is fine to be sent across tasks)
@available(SwiftStdlib 5.9, *) | ||
extension ObservedChanges: @unchecked Sendable where Subject: Sendable { } | ||
|
||
@available(*, unavailable) | ||
extension ObservedChanges.Iterator: Sendable { } |
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.
AsyncIterator
s must not be Sendable
. We learned this the hard way. The problem with them being Sendable
is that you cannot tie a specific consumer task to an iterator anymore. This makes it super hard to implement the AsyncSequence
itself because it has to suddenly handle multiple consumers on a single iterator even though it is a unicast AsyncSequence
. Our latest stance on this is that AsyncSequence
s should most of the time be Sendable
and can be passed to different tasks. Each task should create its own iterator to consume the sequence.
If an AsyncSequence
doesn't support multiple iterators we are fatalError
ing on this in most places right now.
@swift-ci please smoke test |
stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift
Outdated
Show resolved
Hide resolved
afe06f4
to
604e00f
Compare
@swift-ci please smoke test and merge |
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.
I've only looked at the macros and build-integration bits, and they all seem file.
${SWIFT_SYNTAX_TARGETS} | ||
) | ||
|
||
set(SWIFT_SYNTAX_LIBRARIES_SOURCE_DIR |
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.
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
This path seems like it's missing a swift
?
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.
whoops, that is definitely missing - will follow up on that
# 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() | ||
|
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
most of this was copy-pasta from ASTGen
//===----------------------------------------------------------------------===// | ||
|
||
#if SWIFT_OBSERVATION_MACROS | ||
#if compiler(>=5.8) && hasAttribute(attached) |
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.
You shouldn't actually need the hasAttribute
check here
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.
will remove
#if SWIFT_OBSERVATION_MACROS | ||
#if compiler(>=5.8) && hasAttribute(attached) | ||
@available(SwiftStdlib 5.9, *) | ||
@attached(member) |
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 going to need to document the names it produces, per SE-0389
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.
what specifically needs to happen there? The MemberMacro
will add in a field _registrar
, the functions transactions(for:isolation:)
and changes(for:)
, a sub-type _Storage
, and a field _storage
; are those what need to be listed?
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.
I think your attribute should read
@attached(member, names: named(_registrar), named(transactions), named(changes), named(_Storage), named(_storage))
This is an experimental implementation of the Observation and macros associated. It provides a compiler plugin for defining the
@Observable
macro and a module for defining the protocols and default observation registrar.