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

Conversation

phausler
Copy link
Contributor

@phausler phausler commented Feb 16, 2023

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.

@Observable final class MyModel {
  var someField: String = "test"
}

let m = MyModel()
...
for await value in m.changes(for: \.someField) {
}

@phausler phausler requested a review from compnerd as a code owner February 16, 2023 19:48
@phausler
Copy link
Contributor Author

@swift-ci please build toolchain

@phausler phausler requested a review from Azoy February 16, 2023 19:58
@phausler
Copy link
Contributor Author

@swift-ci please build toolchain

Copy link
Member

@compnerd compnerd left a 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 .

@phausler
Copy link
Contributor Author

@swift-ci please build toolchain

1 similar comment
@phausler
Copy link
Contributor Author

@swift-ci please build toolchain

@al45tair
Copy link
Contributor

The ThreadLocal.cpp file seems to duplicate a lot of what's already done for you in swift/Threading/ThreadLocalStorage.h. I wonder if it could be written using the support that's in that header instead? You'd want to add observation_tracking and observation_transaction to swift/Threading/TLSKeys.h and also to the tls_get_key() function in swift/Threading/Impl/Darwin.h, and then you could just declare two variables, ala:

static SWIFT_THREAD_LOCAL_TYPE(void *, tls_key::observation_tracking) observationTracking;
static SWIFT_THREAD_LOCAL_TYPE(void *, tls_key::observation_transaction) observationTransaction;

and do what you will with them.

@phausler
Copy link
Contributor Author

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

@swift-ci please build toolchain

@phausler
Copy link
Contributor Author

@swift-ci please smoke test

1 similar comment
@phausler
Copy link
Contributor Author

@swift-ci please smoke test

func _tlsSet(_ value: UnsafeMutableRawPointer?)

@available(SwiftStdlib 5.9, *)
struct ThreadLocal {
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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?

@phausler
Copy link
Contributor Author

@swift-ci please smoke test

@phausler phausler requested a review from FranzBusch February 27, 2023 17:30
@phausler
Copy link
Contributor Author

@swift-ci please smoke test

1 similar comment
@phausler
Copy link
Contributor Author

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

@swift-ci please smoke test

1 similar comment
@phausler
Copy link
Contributor Author

@swift-ci please smoke test

@phausler phausler requested a review from etcwilde February 28, 2023 18:08
@phausler
Copy link
Contributor Author

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

@swift-ci please build toolchain macosx

1 similar comment
@phausler
Copy link
Contributor Author

@swift-ci please build toolchain macosx

@phausler
Copy link
Contributor Author

phausler commented Mar 1, 2023

@swift-ci please smoke test

4 similar comments
@phausler
Copy link
Contributor Author

phausler commented Mar 1, 2023

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

phausler commented Mar 1, 2023

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

phausler commented Mar 1, 2023

@swift-ci please smoke test

@phausler
Copy link
Contributor Author

phausler commented Mar 1, 2023

@swift-ci please smoke test

@phausler phausler changed the title [WIP] Initial draft of observation Observation and associated macros Mar 1, 2023
@phausler
Copy link
Contributor Author

phausler commented Mar 1, 2023

@swift-ci please build toolchain macosx

@phausler
Copy link
Contributor Author

phausler commented Mar 2, 2023

@swift-ci please smoke test


private let buffer: ManagedBuffer<State, UInt8>

init(_ buffer: ManagedBuffer<State, UInt8>) {
Copy link
Member

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>?
Copy link
Member

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.)

Comment on lines 365 to 366
let context = Context()
let lifetime: Lifetime
Copy link
Member

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:

  1. Crucial type aliases and nested types, not exceeding a handful of lines in length
  2. Stored properties
  3. Initializers
  4. 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> {
Copy link
Member

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

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)>()
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.

//===----------------------------------------------------------------------===//

@available(SwiftStdlib 5.9, *)
public struct KeyPaths<Root>: SetAlgebra, Hashable, @unchecked Sendable {
Copy link
Member

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.

Copy link
Member

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?

Comment on lines +54 to +58
@available(SwiftStdlib 5.9, *)
extension ObservedChanges: @unchecked Sendable where Subject: Sendable { }

@available(*, unavailable)
extension ObservedChanges.Iterator: Sendable { }
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

AsyncIterators 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 AsyncSequences 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 fatalErroring on this in most places right now.

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 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.

Copy link
Member

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.)

Copy link
Member

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

Choose a reason for hiding this comment

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

why @unchecked Sendable here?

Copy link
Contributor Author

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)

Comment on lines +54 to +58
@available(SwiftStdlib 5.9, *)
extension ObservedChanges: @unchecked Sendable where Subject: Sendable { }

@available(*, unavailable)
extension ObservedChanges.Iterator: Sendable { }
Copy link
Member

Choose a reason for hiding this comment

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

AsyncIterators 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 AsyncSequences 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 fatalErroring on this in most places right now.

@phausler
Copy link
Contributor Author

phausler commented Mar 2, 2023

@swift-ci please smoke test

@phausler phausler force-pushed the pr/observation branch 4 times, most recently from afe06f4 to 604e00f Compare March 2, 2023 23:02
@phausler
Copy link
Contributor Author

phausler commented Mar 2, 2023

@swift-ci please smoke test and merge

@swift-ci swift-ci merged commit 8dac45e into swiftlang:main Mar 3, 2023
Copy link
Member

@DougGregor DougGregor left a 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
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_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

Comment on lines +89 to +98
# 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()

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

//===----------------------------------------------------------------------===//

#if SWIFT_OBSERVATION_MACROS
#if compiler(>=5.8) && hasAttribute(attached)
Copy link
Member

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

Copy link
Contributor Author

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)
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 going to need to document the names it produces, per SE-0389

Copy link
Contributor Author

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?

Copy link
Member

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))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.