From ef29bc336a16cee2091eb30b0a3aef606d2163d9 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Mon, 28 Sep 2026 14:19:21 +0200 Subject: [PATCH 01/18] Update bundle version to 0.6.0 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index 3298dbe..44898ec 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.5.2 +MARKETING_VERSION = 0.6.0 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ From 0fff4ec975b3875c0126ff05b57e99b06133b692 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Thu, 8 Oct 2026 13:27:07 +0200 Subject: [PATCH 02/18] Preserve move semantics in Vector.h --- Sources/ComputeCxx/Data/Vector.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComputeCxx/Data/Vector.h b/Sources/ComputeCxx/Data/Vector.h index 7c19fa3..67ef5b0 100644 --- a/Sources/ComputeCxx/Data/Vector.h +++ b/Sources/ComputeCxx/Data/Vector.h @@ -162,7 +162,7 @@ vector::iterator vector::insert(zone *zone, const_iterator pos, const T &v template vector::iterator vector::insert(zone *zone, const_iterator pos, T &&value) { if (pos == end()) { - push_back(zone, value); + push_back(zone, std::move(value)); return end() - 1; } auto offset = pos - begin(); From cc35059700fc0f59b52ae371fc544f1562945f21 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 9 Oct 2026 09:39:55 +0200 Subject: [PATCH 03/18] Add @escaping to all closure parameters passed to C++ --- Sources/Compute/Attribute/AnyAttribute.swift | 4 +-- Sources/Compute/Attribute/AttributeType.swift | 2 +- .../RuleContext/AnyRuleContext.swift | 6 +++-- Sources/Compute/Graph/Graph.swift | 26 +++++++++++-------- Sources/Compute/Graph/Subgraph.swift | 8 +++--- Sources/Compute/Runtime/Metadata.swift | 9 ++++--- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/Sources/Compute/Attribute/AnyAttribute.swift b/Sources/Compute/Attribute/AnyAttribute.swift index b7d997c..3327494 100644 --- a/Sources/Compute/Attribute/AnyAttribute.swift +++ b/Sources/Compute/Attribute/AnyAttribute.swift @@ -5,14 +5,14 @@ func IAGGraphMutateAttribute( _ attribute: AnyAttribute, type: Metadata, invalidating: Bool, - modify: (UnsafeMutableRawPointer) -> Void + modify: @escaping (UnsafeMutableRawPointer) -> Void ) @_silgen_name("IAGGraphSearch") func IAGGraphSearch( attribute: AnyAttribute, options: SearchOptions, - predicate: (AnyAttribute) -> Bool + predicate: @escaping (AnyAttribute) -> Bool ) -> Bool extension AnyAttribute { diff --git a/Sources/Compute/Attribute/AttributeType.swift b/Sources/Compute/Attribute/AttributeType.swift index ca88289..36ce869 100644 --- a/Sources/Compute/Attribute/AttributeType.swift +++ b/Sources/Compute/Attribute/AttributeType.swift @@ -16,7 +16,7 @@ extension String { @_silgen_name("IAGRetainClosure") func IAGRetainClosure( - _ closure: (UnsafeMutableRawPointer, AnyAttribute) -> Void + _ closure: @escaping (UnsafeMutableRawPointer, AnyAttribute) -> Void ) -> _IAGClosureStorage extension _AttributeType { diff --git a/Sources/Compute/Attribute/RuleContext/AnyRuleContext.swift b/Sources/Compute/Attribute/RuleContext/AnyRuleContext.swift index dfd055a..1f20c03 100644 --- a/Sources/Compute/Attribute/RuleContext/AnyRuleContext.swift +++ b/Sources/Compute/Attribute/RuleContext/AnyRuleContext.swift @@ -1,7 +1,7 @@ import ComputeCxx @_silgen_name("IAGGraphWithUpdate") -func IAGGraphWithUpdate(_ attribute: AnyAttribute, body: () -> Void) +func IAGGraphWithUpdate(_ attribute: AnyAttribute, body: @escaping () -> Void) @frozen public struct AnyRuleContext { @@ -21,7 +21,9 @@ public struct AnyRuleContext { } public func update(body: () -> Void) { - IAGGraphWithUpdate(attribute, body: body) + withoutActuallyEscaping(body) { escapingBody in + IAGGraphWithUpdate(attribute, body: escapingBody) + } } public func changedValue( diff --git a/Sources/Compute/Graph/Graph.swift b/Sources/Compute/Graph/Graph.swift index 6584196..8a1797f 100644 --- a/Sources/Compute/Graph/Graph.swift +++ b/Sources/Compute/Graph/Graph.swift @@ -34,11 +34,13 @@ extension Graph { pointer.initialize(to: attributeType) return UnsafePointer(pointer) } - return internAttributeType( - ctx: ctx, - body: Metadata(body), - makeAttributeType: makeAttributeType - ) + return withoutActuallyEscaping(makeAttributeType) { escapingMakeAttributeType in + return internAttributeType( + ctx: ctx, + body: Metadata(body), + makeAttributeType: escapingMakeAttributeType + ) + } } } @@ -81,7 +83,7 @@ func IAGGraphSetInvalidationCallback( @_silgen_name("IAGGraphWithMainThreadHandler") func IAGGraphWithMainThreadHandler( _ graph: UnsafeRawPointer, - body: () -> Void, + body: @escaping () -> Void, mainThreadHandler: (() -> Void) -> Void ) @@ -118,11 +120,13 @@ extension Graph { } public func withMainThreadHandler(_ mainThreadHandler: (() -> Void) -> Void, do body: () -> Void) { - IAGGraphWithMainThreadHandler( - unsafeBitCast(self, to: UnsafeRawPointer.self), - body: body, - mainThreadHandler: mainThreadHandler - ) + withoutActuallyEscaping(body) { escapingBody in + IAGGraphWithMainThreadHandler( + unsafeBitCast(self, to: UnsafeRawPointer.self), + body: escapingBody, + mainThreadHandler: mainThreadHandler + ) + } } } diff --git a/Sources/Compute/Graph/Subgraph.swift b/Sources/Compute/Graph/Subgraph.swift index 546a965..f3e2897 100644 --- a/Sources/Compute/Graph/Subgraph.swift +++ b/Sources/Compute/Graph/Subgraph.swift @@ -3,7 +3,7 @@ import ComputeCxx @_silgen_name("IAGSubgraphAddObserver") func IAGSubgraphAddObserver( _ subgraph: UnsafeRawPointer, - observer: () -> Void + observer: @escaping () -> Void ) -> Int extension Subgraph { @@ -16,7 +16,7 @@ extension Subgraph { func IAGSubgraphApply( _ subgraph: UnsafeRawPointer, _ flags: Subgraph.Flags, - _ body: (AnyAttribute) -> Void + _ body: @escaping (AnyAttribute) -> Void ) extension Subgraph { @@ -36,7 +36,9 @@ extension Subgraph { _ flags: Subgraph.Flags, _ body: (AnyAttribute) -> Void ) { - IAGSubgraphApply(unsafeBitCast(self, to: UnsafeRawPointer.self), flags, body) + withoutActuallyEscaping(body) { escapingBody in + IAGSubgraphApply(unsafeBitCast(self, to: UnsafeRawPointer.self), flags, escapingBody) + } } } diff --git a/Sources/Compute/Runtime/Metadata.swift b/Sources/Compute/Runtime/Metadata.swift index b6494fb..76ff0af 100644 --- a/Sources/Compute/Runtime/Metadata.swift +++ b/Sources/Compute/Runtime/Metadata.swift @@ -13,7 +13,7 @@ public func forEachField(of type: Any.Type, do body: (UnsafePointer, Int, func IAGTypeApplyFields2( _ type: Metadata, options: Metadata.ApplyOptions, - body: (UnsafePointer, Int, Metadata) -> Bool + body: @escaping (UnsafePointer, Int, Metadata) -> Bool ) -> Bool extension Metadata { @@ -32,8 +32,11 @@ extension Metadata { ) -> Bool { - return IAGTypeApplyFields2(self, options: options) { fieldName, fieldOffset, fieldType in - return body(fieldName, fieldOffset, fieldType.type) + return withoutActuallyEscaping(body) { escapingBody in + let apply: (UnsafePointer, Int, Metadata) -> Bool = { fieldName, fieldOffset, fieldType in + return escapingBody(fieldName, fieldOffset, fieldType.type) + } + return IAGTypeApplyFields2(self, options: options, body: apply) } } From 1505ff5990179947882cee5c611fcce63cf00882 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 9 Oct 2026 10:27:22 +0200 Subject: [PATCH 04/18] Update bundle version to 0.6.1 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index 44898ec..abd4e8f 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.6.0 +MARKETING_VERSION = 0.6.1 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ From 516a0fdd7850a96a4c783602972127745b4af127 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 11 Oct 2026 16:51:29 +0800 Subject: [PATCH 05/18] Fix offsets and bounds in partial attribute comparisons --- .../Comparison/LayoutDescriptor.cpp | 4 +- .../Attribute/PartialComparisonTests.swift | 193 ++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 Tests/ComputeTests/Shared/Attribute/PartialComparisonTests.swift diff --git a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp index 5710b86..8666b66 100644 --- a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp +++ b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp @@ -766,7 +766,9 @@ Partial find_partial(ValueLayout layout, size_t range_location, size_t range_siz } }; - return {reader.layout, accumulated_size}; + // The compared values already start at the requested range. + size_t location = accumulated_size - range_location; + return {reader.layout, location < range_size ? location : range_size}; } void print(std::string &output, ValueLayout layout) { diff --git a/Tests/ComputeTests/Shared/Attribute/PartialComparisonTests.swift b/Tests/ComputeTests/Shared/Attribute/PartialComparisonTests.swift new file mode 100644 index 0000000..a3dc9f0 --- /dev/null +++ b/Tests/ComputeTests/Shared/Attribute/PartialComparisonTests.swift @@ -0,0 +1,193 @@ +import Foundation +import Testing + +@Suite(.serialized(for: \Subgraph.Type.current)) +struct PartialComparisonTests { + struct EnvironmentValues { + var value: Value + } + + enum Content { + case keyPath(KeyPath, Value>) + case value(Value) + } + + struct Container { + var prefix: Int + var content: Content + var suffix: Int + } + + struct ReadContent: StatefulRule { + @Attribute var content: Content + var environment: EnvironmentValues + var updates = 0 + + mutating func updateValue() { + updates += 1 + let input: Input + switch content { + case let .keyPath(keyPath): + input = environment[keyPath: keyPath] + case let .value(value): + input = value + } + value = (input, updates) + } + + typealias Value = (input: Input, updates: Int) + } + + @Test + func enumMemberAfterPrefix() async throws { + try await #require(processExitsWith: .success) { + setenv(asyncLayoutsEnvironmentVariable, "0", 1) + _ = prefetchCompareValues( + type: Metadata(Container.self), + options: [.comparisonModeEquatableAlways, .fetchLayoutsSynchronously], + priority: 0 + ) + + withGraph { + let source = Attribute(value: Container( + prefix: 0, content: .keyPath(\EnvironmentValues.value), suffix: 0 + )) + let result = Attribute(ReadContent( + content: source[offset: { .of(&$0.content) }], environment: .init(value: true) + )) + #expect(result.value.input) + #expect(result.value.updates == 1) + + source.value.prefix = 1 + #expect(result.value.input) + #expect(result.value.updates == 1) + + source.value.content = .value(false) + #expect(!result.value.input) + #expect(result.value.updates == 2) + } + } + } + + struct Payload: Equatable { + var first: Int + var second: Int + } + + struct ReadValue: StatefulRule { + @Attribute var input: Input + var updates = 0 + + mutating func updateValue() { + updates += 1 + value = (input, updates) + } + + typealias Value = (input: Input, updates: Int) + } + + struct Parity: Equatable { + var rawValue: Int + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue % 2 == rhs.rawValue % 2 + } + } + + @Test + func memberUsesItsEqualityImplementation() async throws { + try await #require(processExitsWith: .success) { + setenv(asyncLayoutsEnvironmentVariable, "0", 1) + _ = prefetchCompareValues( + type: Metadata(Triple.self), + options: [.comparisonModeEquatableAlways, .fetchLayoutsSynchronously], + priority: 0 + ) + + withGraph { + let source = Attribute(value: Triple(first: 0, second: Parity(rawValue: 0), third: 0)) + let result = Attribute(ReadValue(input: source[offset: { .of(&$0.second) }])) + #expect(result.value.input.rawValue == 0) + #expect(result.value.updates == 1) + + source.value = Triple(first: 1, second: Parity(rawValue: 2), third: 0) + #expect(result.value.updates == 1) + + source.value = Triple(first: 1, second: Parity(rawValue: 3), third: 0) + #expect(result.value.input.rawValue == 3) + #expect(result.value.updates == 2) + } + } + } + + struct InlineValues: Equatable { + var first: Int + var second: Int + var third: Int + } + + struct ValueContainer { + var values: InlineValues + var content: Content + } + + @Test + func byteComparisonStaysWithinTheMember() async throws { + try await #require(processExitsWith: .success) { + setenv(asyncLayoutsEnvironmentVariable, "0", 1) + _ = prefetchCompareValues( + type: Metadata(ValueContainer.self), + options: [.comparisonModeEquatableAlways, .fetchLayoutsSynchronously], + priority: 0 + ) + + withGraph { + let source = Attribute(value: ValueContainer( + values: InlineValues(first: 0, second: 1, third: 2), content: .value(true) + )) + let result = Attribute(ReadValue(input: source[offset: { .of(&$0.values.second) }])) + #expect(result.value.input == 1) + #expect(result.value.updates == 1) + + source.value.values.third = 3 + #expect(result.value.input == 1) + #expect(result.value.updates == 1) + + source.value.values.second = 4 + #expect(result.value.input == 4) + #expect(result.value.updates == 2) + } + } + } + + @Test + func enumPayloadIsNotCopiedFromAnAdjacentField() async throws { + try await #require(processExitsWith: .success) { + setenv(asyncLayoutsEnvironmentVariable, "0", 1) + _ = prefetchCompareValues( + type: Metadata(Container.self), + options: [.comparisonModeEquatableAlways, .fetchLayoutsSynchronously], + priority: 0 + ) + + withGraph { + let payload = Payload(first: 0, second: 0x1_0000_0000) + let source = Attribute(value: Container(prefix: 0, content: .value(payload), suffix: 0)) + let result = Attribute(ReadContent( + content: source[offset: { .of(&$0.content) }], environment: .init(value: payload) + )) + #expect(result.value.input == payload) + #expect(result.value.updates == 1) + + source.value.prefix = 1 + #expect(result.value.input == payload) + #expect(result.value.updates == 1) + + let changed = Payload(first: 1, second: payload.second) + source.value.content = .value(changed) + #expect(result.value.input == changed) + #expect(result.value.updates == 2) + } + } + } +} From a0b19ef94fc1dd2aa74c5eec165be64dc8f6d6bd Mon Sep 17 00:00:00 2001 From: James Moschou Date: Tue, 13 Oct 2026 09:28:51 +0200 Subject: [PATCH 06/18] Update bundle version to 0.6.2 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index abd4e8f..f2c9b0c 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.6.1 +MARKETING_VERSION = 0.6.2 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ From 5eb5234d084fcbe63f0a54b355c41247672e92be Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 14 Oct 2026 00:20:14 +0200 Subject: [PATCH 07/18] Use bitwise & instead of logical && in AttributeID::resolve_slow --- Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp b/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp index 5e7c8c8..e669514 100644 --- a/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp +++ b/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp @@ -73,7 +73,7 @@ OffsetAttributeID AttributeID::resolve_slow(TraversalOptions options) const { } } - if (options && TraversalOptions::EvaluateWeakReferences) { + if (options & TraversalOptions::EvaluateWeakReferences) { if (indirect_node->source().expired()) { if (options & TraversalOptions::AssertNotNil) { precondition_failure("invalid indirect ref: %u", _value); From 8d41777653d3f26e761d3a3a86432d72ded795ed Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 14 Oct 2026 01:48:05 +0200 Subject: [PATCH 08/18] Ensure all node edges get properly removed when a subgraph is being invalidated # Conflicts: # Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.cpp --- .../Attribute/AttributeID/AttributeID.cpp | 2 +- .../Attribute/AttributeID/WeakAttributeID.cpp | 15 +++++++++++++-- .../Attribute/AttributeID/WeakAttributeID.h | 11 +++++++++-- Sources/ComputeCxx/Graph/Graph.cpp | 10 +++++----- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp b/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp index e669514..7ebdddc 100644 --- a/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp +++ b/Sources/ComputeCxx/Attribute/AttributeID/AttributeID.cpp @@ -74,7 +74,7 @@ OffsetAttributeID AttributeID::resolve_slow(TraversalOptions options) const { } if (options & TraversalOptions::EvaluateWeakReferences) { - if (indirect_node->source().expired()) { + if (indirect_node->source().is_freed()) { if (options & TraversalOptions::AssertNotNil) { precondition_failure("invalid indirect ref: %u", _value); } diff --git a/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.cpp b/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.cpp index f22a517..e874113 100644 --- a/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.cpp +++ b/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.cpp @@ -6,7 +6,7 @@ namespace IAG { -bool WeakAttributeID::expired() const { +bool WeakAttributeID::is_expired() const { uint64_t raw_page_seed = data::table::shared().raw_page_seed(_identifier.page_ptr()); if (raw_page_seed & 0xff00000000) { auto zone_info = data::zone::info::from_raw_value(uint32_t(raw_page_seed)); @@ -17,8 +17,19 @@ bool WeakAttributeID::expired() const { return true; } +bool WeakAttributeID::is_freed() const { + uint64_t raw_page_seed = data::table::shared().raw_page_seed(_identifier.page_ptr()); + if (raw_page_seed & 0xff00000000) { + auto zone_info = data::zone::info::from_raw_value(uint32_t(raw_page_seed)); + if (zone_info.zone_id() == _seed) { + return false; + } + } + return true; +} + const AttributeID WeakAttributeID::evaluate() const { - return _identifier && !expired() ? _identifier : AttributeID(IAGAttributeNil); + return _identifier && !is_expired() ? _identifier : AttributeID(IAGAttributeNil); }; } // namespace IAG diff --git a/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.h b/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.h index 3f41e44..50f5f38 100644 --- a/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.h +++ b/Sources/ComputeCxx/Attribute/AttributeID/WeakAttributeID.h @@ -25,9 +25,16 @@ class WeakAttributeID { const AttributeID identifier() const { return _identifier; }; uint32_t seed() const { return _seed; } - bool expired() const; + /// Returns true if the attribute can no longer be evaluated. + bool is_expired() const; - /// Returns the attribute it is has not expired, otherwise returns the nil + /// Returns true when the attribute's subgraph has been freed (or reused). + /// + /// This method may return false if the attribute's subgraph has been + /// invalidated but not yet destroyed. + bool is_freed() const; + + /// Returns the attribute if it has not expired, otherwise returns the nil /// attribute. const AttributeID evaluate() const; }; diff --git a/Sources/ComputeCxx/Graph/Graph.cpp b/Sources/ComputeCxx/Graph/Graph.cpp index 8084e12..dc96cc8 100644 --- a/Sources/ComputeCxx/Graph/Graph.cpp +++ b/Sources/ComputeCxx/Graph/Graph.cpp @@ -522,7 +522,7 @@ void Graph::remove_indirect_node(data::ptr indirect_node) { return; } - if (indirect_node->source().expired()) { + if (indirect_node->source().is_freed()) { return; } @@ -556,7 +556,7 @@ void Graph::remove_indirect_node(data::ptr indirect_node) { } break; } else { - if (source_indirect_node->source().expired()) { + if (source_indirect_node->source().is_freed()) { break; } @@ -624,7 +624,7 @@ bool Graph::remove_removed_output(AttributeID attribute, AttributeID output, boo if (output_indirect_node->is_mutable()) { auto &mutable_output_indirect_node = output_indirect_node->to_mutable(); auto initial_source = mutable_output_indirect_node.initial_source(); - if (initial_source.identifier() && !initial_source.identifier().is_nil() && !initial_source.expired()) { + if (initial_source.identifier() && !initial_source.identifier().is_nil() && !initial_source.is_expired()) { new_source = initial_source; new_offset = mutable_output_indirect_node.initial_offset(); } @@ -636,7 +636,7 @@ bool Graph::remove_removed_output(AttributeID attribute, AttributeID output, boo output_indirect_node->modify(new_source, new_offset); - if (new_source.identifier() && !new_source.identifier().is_nil() && !new_source.expired()) { + if (new_source.identifier() && !new_source.identifier().is_nil() && !new_source.is_expired()) { add_input_dependencies(output, new_source.identifier()); } @@ -917,7 +917,7 @@ bool Graph::indirect_attribute_reset(data::ptr indirect_node, bool auto initial_source = indirect_node->to_mutable().initial_source(); auto initial_offset = indirect_node->to_mutable().initial_offset(); - if (!initial_source.expired()) { + if (!initial_source.is_expired()) { new_source = initial_source; new_offset = initial_offset; } else { From eb71ec54eb5c04caad01520bb93ce6fcd506d028 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 14 Oct 2026 09:31:21 +0200 Subject: [PATCH 09/18] Update bundle version to 0.6.3 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index f2c9b0c..0110e5c 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.6.2 +MARKETING_VERSION = 0.6.3 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ From 88af343d12f297856b81ac709028cb310bfef1f4 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Tue, 13 Oct 2026 10:12:57 +0200 Subject: [PATCH 10/18] Change recursion to iteration in UpdateStack::update() # Conflicts: # Sources/ComputeCxx/Graph/UpdateStack.cpp --- Sources/ComputeCxx/Graph/UpdateStack.cpp | 88 +++++++++++++----------- Sources/ComputeCxx/Graph/UpdateStack.h | 1 + 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/Sources/ComputeCxx/Graph/UpdateStack.cpp b/Sources/ComputeCxx/Graph/UpdateStack.cpp index 0290f80..078e47a 100644 --- a/Sources/ComputeCxx/Graph/UpdateStack.cpp +++ b/Sources/ComputeCxx/Graph/UpdateStack.cpp @@ -152,6 +152,51 @@ bool Graph::UpdateStack::push_slow(data::ptr node_ptr, Node &node, bool ig return true; } +bool Graph::UpdateStack::push_inputs(Frame &frame, data::ptr node) { + for (auto input_index = frame.num_pushed_inputs, num_inputs = node->input_edges().size(); + input_index != num_inputs; ++input_index) { + const InputEdge &input_edge = node->input_edges()[input_index]; + + AttributeID input_attribute = input_edge.attribute; + while (input_attribute.is_indirect_node()) { + // TODO: warning: variable 'input_attribute' is uninitialized + // when used (revisit this) within its own initialization + auto input_attribute_source = input_attribute.get_indirect_node()->source().identifier(); + if (input_attribute.get_indirect_node()->is_mutable()) { + if (AttributeID dependency = input_attribute.get_indirect_node()->to_mutable().dependency()) { + if (!dependency.get_node()->is_value_initialized() || dependency.get_node()->is_dirty()) { + frame.num_pushed_inputs = input_index; + push(dependency.get_node(), *dependency.get_node(), false, false); + // pushed a frame regardless + return true; + } + } + } + input_attribute = input_attribute_source; + } + + if (auto input_node = input_attribute.get_node()) { + if (input_edge.options & IAGInputOptionsChanged) { + frame.pending = true; + } + + if (!input_node->is_value_initialized() || input_node->is_dirty()) { + + if (!(input_edge.options & IAGInputOptionsUnprefetched) && input_attribute.subgraph()->is_valid()) { + frame.num_pushed_inputs = input_index + 1; + if (push(input_node, *input_node, true, true)) { + return true; + } + } + + frame.pending = true; + } + } + } + + return false; +} + Graph::UpdateStatus Graph::UpdateStack::update() { while (true) { Frame &frame = _frames.back(); @@ -200,46 +245,9 @@ Graph::UpdateStatus Graph::UpdateStack::update() { // Push prefetched inputs - for (auto input_index = frame.num_pushed_inputs, num_inputs = node->input_edges().size(); - input_index != num_inputs; ++input_index) { - const InputEdge &input_edge = node->input_edges()[input_index]; - - AttributeID input_attribute = input_edge.attribute; - while (input_attribute.is_indirect_node()) { - // TODO: warning: variable 'input_attribute' is uninitialized - // when used (revisit this) within its own initialization - auto input_attribute_source = input_attribute.get_indirect_node()->source().identifier(); - if (input_attribute.get_indirect_node()->is_mutable()) { - if (AttributeID dependency = input_attribute.get_indirect_node()->to_mutable().dependency()) { - if (!dependency.get_node()->is_value_initialized() || dependency.get_node()->is_dirty()) { - frame.num_pushed_inputs = input_index; - push(dependency.get_node(), *dependency.get_node().get(), false, false); - // go to top regardless - return update(); - } - } - } - input_attribute = input_attribute_source; - } - - if (auto input_node = input_attribute.get_node()) { - if (input_edge.options & IAGInputOptionsChanged) { - frame.pending = true; - } - - if (!input_node->is_value_initialized() || input_node->is_dirty()) { - - if (!(input_edge.options & IAGInputOptionsUnprefetched) && input_attribute.subgraph()->is_valid()) { - frame.num_pushed_inputs = input_index + 1; - if (push(input_node, *input_node.get(), true, true)) { - // go to top - return update(); - } - } - - frame.pending = true; - } - } + if (push_inputs(frame, node)) { + // go to top + continue; } // Update value diff --git a/Sources/ComputeCxx/Graph/UpdateStack.h b/Sources/ComputeCxx/Graph/UpdateStack.h index 6805634..832891b 100644 --- a/Sources/ComputeCxx/Graph/UpdateStack.h +++ b/Sources/ComputeCxx/Graph/UpdateStack.h @@ -30,6 +30,7 @@ class Graph::UpdateStack { IAGGraphUpdateOptions _options; bool push_slow(data::ptr node_ptr, Node &node, bool ignore_cycles, bool initialize_value); + bool push_inputs(Frame &frame, data::ptr node); public: UpdateStack(Graph *graph, IAGGraphUpdateOptions options); From b129f659ac9f7e3791737314604d73764311644d Mon Sep 17 00:00:00 2001 From: James Moschou Date: Thu, 15 Oct 2026 12:50:45 +0200 Subject: [PATCH 11/18] Update bundle version to 0.6.4 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index 0110e5c..c64bfdc 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.6.3 +MARKETING_VERSION = 0.6.4 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ From 4c9b18c557d68dd8c07ae2ac50dc3f3cf3caab11 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 22 Oct 2026 01:23:02 +0800 Subject: [PATCH 12/18] Fix inline vector storage lifetime during subgraph updates (#86) --- Sources/ComputeCxx/Vector/Vector.h | 6 ++- .../Shared/Subgraph/SubgraphUpdateTests.swift | 43 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Sources/ComputeCxx/Vector/Vector.h b/Sources/ComputeCxx/Vector/Vector.h index fb1c0a9..4e8bf27 100644 --- a/Sources/ComputeCxx/Vector/Vector.h +++ b/Sources/ComputeCxx/Vector/Vector.h @@ -31,7 +31,11 @@ class vector { using size_type = _size_type; private: - T _inline_buffer[_inline_capacity]; + // Elements are constructed and destroyed explicitly. After relocation to + // the heap, the inline storage must not destroy the old element copies. + union { + T _inline_buffer[_inline_capacity]; + }; T *_Nullable _buffer = nullptr; size_type _size = 0; size_type _capacity = _inline_capacity; diff --git a/Tests/ComputeTests/Shared/Subgraph/SubgraphUpdateTests.swift b/Tests/ComputeTests/Shared/Subgraph/SubgraphUpdateTests.swift index d2883ce..2319bda 100644 --- a/Tests/ComputeTests/Shared/Subgraph/SubgraphUpdateTests.swift +++ b/Tests/ComputeTests/Shared/Subgraph/SubgraphUpdateTests.swift @@ -2,6 +2,49 @@ import Testing @Suite struct SubgraphUpdateTests { + @Test + func updatePreservesChildrenBeyondInlineCapacity() async { + await #expect(processExitsWith: .success) { + struct CountingRule: StatefulRule { + typealias Value = Int + var count = 0 + + mutating func updateValue() { + count += 1 + value = count + } + } + + withGraph { + let parent = Subgraph.current! + let flags = Subgraph.Flags(rawValue: 1) + let children = (0..<64).map { _ in Subgraph(graph: parent.graph) } + let attributes = children.map { child in + parent.addChild(child) + return child.apply { + let attribute = Attribute(CountingRule()) + attribute.flags = flags + return attribute + } + } + + for expectedValue in 1...3 { + for attribute in attributes { + attribute.invalidateValue() + } + parent.update(flags: flags) + for child in children { + precondition(child.isValid) + } + for attribute in attributes { + precondition(attribute.value == expectedValue) + } + } + withExtendedLifetime(children) {} + } + } + } + @Test func updateIterativelyEvaluatesDirtiedAttributesDuringTheSamePass() { class Counter { From 75533153c96aa983ee8824b50a045979a39d86de Mon Sep 17 00:00:00 2001 From: James Moschou Date: Wed, 21 Oct 2026 19:26:32 +0200 Subject: [PATCH 13/18] Update bundle version to 0.6.5 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index c64bfdc..6008ad8 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.6.4 +MARKETING_VERSION = 0.6.5 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++ From e861b54ff223b6f506c31b47717b41b187ef1ed8 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Thu, 22 Oct 2026 18:19:09 +0200 Subject: [PATCH 14/18] Fix use-after-free in Builder::visit_case for unresolvable payload types When the payload type of an enum case could not be resolved, visit_case popped the EnumItem from the items vector, destroying the case that _current_enum_case pointed into, and then fell through to code that read the case's children and popped it from the destroyed EnumItem. Return immediately after popping the EnumItem instead, restoring the enum case depth and current enum case, as the original implementation does. Callers already revert their items when visit_case returns false. Co-Authored-By: Claude Opus 5.5 --- .../ComputeCxx/Comparison/LayoutDescriptor.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp index 8666b66..1aa285a 100644 --- a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp +++ b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp @@ -1145,23 +1145,25 @@ bool Builder::visit_case(const swift::metadata &type, const swift::field_record }; enum_item.cases.push_back(std::move(enum_case)); - bool result; - _enum_case_depth += 1; EnumItem::Case *prev_enum_case = _current_enum_case; _current_enum_case = &enum_item.cases.back(); if (field.isIndirectCase() && _current_comparison_mode == 0) { add_field(sizeof(void *)); - result = true; } else { auto mangled_type_name = field.MangledTypeName.get(); auto field_type = mangled_type_name != nullptr ? type.mangled_type_name_ref(mangled_type_name, false, nullptr) : nullptr; if (field_type == nullptr) { - // bail out if we can't get a type for the enum case payload + // bail out if we can't get a type for the enum case payload. The enum item and its cases are destroyed, so + // return without touching them again. items.pop_back(); - result = false; + + _enum_case_depth -= 1; + _current_enum_case = prev_enum_case; + + return false; } else if (field.isIndirectCase()) { if (auto field_size = field_type->vw_size()) { IndirectItem item = { @@ -1171,7 +1173,6 @@ bool Builder::visit_case(const swift::metadata &type, const swift::field_record }; _current_enum_case->children.push_back(item); } - result = true; } else { IAGComparisonMode prev_comparison_mode = _current_comparison_mode; _current_comparison_mode = mode_for_type(&type, _current_comparison_mode); @@ -1209,8 +1210,6 @@ bool Builder::visit_case(const swift::metadata &type, const swift::field_record } _current_comparison_mode = prev_comparison_mode; - - result = true; } } @@ -1221,7 +1220,7 @@ bool Builder::visit_case(const swift::metadata &type, const swift::field_record _enum_case_depth -= 1; _current_enum_case = prev_enum_case; - return result; + return true; } bool Builder::visit_existential(const swift::existential_type_metadata &type) { From fbddb7ecebdf41a134d69108f970dcaf518591f7 Mon Sep 17 00:00:00 2001 From: James Moschou Date: Thu, 22 Oct 2026 18:19:10 +0200 Subject: [PATCH 15/18] Capture item count before visiting enum case payload in Builder::visit_case The item count passed to revert() was read after visiting the payload type, so the revert never removed anything. Read it before the visit, as the original implementation and visit_element do. This does not change the resulting layout, since the case's children are cleared immediately after reverting. Co-Authored-By: Claude Opus 5.5 --- Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp index 1aa285a..2cd0c17 100644 --- a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp +++ b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp @@ -1181,6 +1181,7 @@ bool Builder::visit_case(const swift::metadata &type, const swift::field_record // same as visit_element auto &items = get_items(); + auto num_items = items.size(); size_t prev_offset = -1; size_t prev_size = 0; if (auto data_item = items.size() > 0 ? std::get_if(&items.back()) : nullptr) { @@ -1190,7 +1191,7 @@ bool Builder::visit_case(const swift::metadata &type, const swift::field_record if (!field_type->visit(*this)) { RevertItemsInfo info = { - items.size(), + num_items, { prev_offset, prev_size, From 843674f45087d0b749dc64287313710b7371cf5e Mon Sep 17 00:00:00 2001 From: James Moschou Date: Thu, 22 Oct 2026 18:43:04 +0200 Subject: [PATCH 16/18] Start enum case emitters at the enum's offset Case items share the enum item's offset space, and the comparison starts each case at the enum's offset. Starting the case emitter at 0 encoded an extra skip equal to the enum's offset, so enums laid out inline at a nonzero offset compared the wrong bytes. Co-Authored-By: Claude Opus 5.5 --- Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp index 2cd0c17..4bcd62c 100644 --- a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp +++ b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp @@ -1385,7 +1385,10 @@ void Builder::Emitter>::operator()(const En emit_value((void *)item.type); } + // Case items have offsets in the same space as the enum item, and the + // comparison starts each case at the enum's offset. auto children_emitter = Emitter>(_data); + children_emitter._emitted_size = item.offset; for (auto &&child : enum_case.children) { std::visit(children_emitter, child); } From 1ec5e81be71f643f42b60a07de0877c1c37e640d Mon Sep 17 00:00:00 2001 From: James Moschou Date: Thu, 22 Oct 2026 18:56:33 +0200 Subject: [PATCH 17/18] Propagate invalid enum case layouts to the enclosing enum Match AttributeGraph: an enum's layout is invalid if any case's children are invalid, not only when a case extends past the enum's size. Co-Authored-By: Claude Opus 5.5 --- Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp index 4bcd62c..bd95aa2 100644 --- a/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp +++ b/Sources/ComputeCxx/Comparison/LayoutDescriptor.cpp @@ -1395,9 +1395,8 @@ void Builder::Emitter>::operator()(const En is_first = false; - if (!_invalid && !children_emitter.is_invalid()) { - _invalid = _emitted_size + item.type->vw_size() < children_emitter.emitted_size(); - } + _invalid = _invalid || children_emitter.is_invalid() || + _emitted_size + item.type->vw_size() < children_emitter.emitted_size(); } } From 5e0900007c3d582fe673731d8ac131989918d87f Mon Sep 17 00:00:00 2001 From: James Moschou Date: Fri, 23 Oct 2026 16:09:22 +0200 Subject: [PATCH 18/18] Update bundle version to 0.6.6 --- Xcode/Configs/Compute.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/Configs/Compute.xcconfig b/Xcode/Configs/Compute.xcconfig index 6008ad8..5224d30 100644 --- a/Xcode/Configs/Compute.xcconfig +++ b/Xcode/Configs/Compute.xcconfig @@ -34,7 +34,7 @@ USE_HEADERMAP = NO CODE_SIGN_STYLE = Automatic CURRENT_PROJECT_VERSION = 1 -MARKETING_VERSION = 0.6.5 +MARKETING_VERSION = 0.6.6 ENABLE_MODULE_VERIFIER = YES MODULE_VERIFIER_SUPPORTED_LANGUAGES = objective-c objective-c++