Support WebAssembly + fix Linux collection-cast crash#30
Conversation
- Guard @published / ObservableObject with #if canImport(Combine) instead of #if !os(Linux) && !os(Windows). The latter includes os(WASI), so wasm took the Combine path and failed to build (unknown attribute 'Published'). canImport(Combine) correctly excludes Linux, Windows, AND wasm. (AppState#149) - Dictionary.get(_:as:): box through Any before the conditional cast ((self[key] as Any) as? Item). Casting Optional<Any> -> [ConcreteType] takes the runtime _arrayForceCast path, which aborts (swift_dynamicCastFailure) on Linux/WASI for collection-typed values. (AppState#151) - Add array round-trip regression tests through Cache<String, Any>. 178 tests pass on macOS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request improves platform compatibility (particularly for Linux and WebAssembly/WASI) by replacing OS-specific checks with #if canImport(Combine) and addressing a Swift runtime casting bug when retrieving collection-typed values from an Any cache. Review feedback suggests a more idiomatic way to resolve the casting bug by unwrapping the optional value first rather than boxing it to Any, and recommends explicitly importing Combine when extending Cache with ObservableObject to ensure reliable compilation.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Route through an explicit `Any` boxing step before the conditional cast. This avoids | ||
| // `swift_dynamicCastFailure` / `_arrayForceCast` aborts on Linux and WASI when casting an | ||
| // `Optional<Any>` that wraps a collection type (e.g. `[SomeStruct]`): without the | ||
| // intermediate cast, the runtime takes the `Optional<Any>` → `[T]` forced-array-cast path, | ||
| // which is not implemented the same way off Darwin. Boxing to `Any` first normalises the | ||
| // dynamic type to the concrete `Item` path. | ||
| guard let value = (self[key] as Any) as? Item else { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Instead of boxing the optional value into Any via (self[key] as Any) as? Item, we can simply unwrap the optional value first using guard let rawValue = self[key]. This completely avoids the buggy Optional<Any> to [T] cast path on Linux/WASI, is much more idiomatic, and avoids potential edge cases where nil is boxed into Any (which could behave unexpectedly if Item is Any or an optional type).
| // Route through an explicit `Any` boxing step before the conditional cast. This avoids | |
| // `swift_dynamicCastFailure` / `_arrayForceCast` aborts on Linux and WASI when casting an | |
| // `Optional<Any>` that wraps a collection type (e.g. `[SomeStruct]`): without the | |
| // intermediate cast, the runtime takes the `Optional<Any>` → `[T]` forced-array-cast path, | |
| // which is not implemented the same way off Darwin. Boxing to `Any` first normalises the | |
| // dynamic type to the concrete `Item` path. | |
| guard let value = (self[key] as Any) as? Item else { | |
| return nil | |
| } | |
| // Unwrapping the optional value first avoids a Swift runtime bug on Linux and WASI | |
| // where casting `Optional<Any>` directly to a collection type (e.g. `[T]`) aborts | |
| // with `swift_dynamicCastFailure`. | |
| guard let rawValue = self[key], let value = rawValue as? Item else { | |
| return nil | |
| } |
| #if canImport(Combine) | ||
| extension Cache: ObservableObject { } | ||
| #endif |
There was a problem hiding this comment.
To ensure that @Published and ObservableObject compile correctly across all platforms and build configurations (especially when this file is compiled incrementally or in isolation), we should explicitly import Combine when it is available.
| #if canImport(Combine) | |
| extension Cache: ObservableObject { } | |
| #endif | |
| #if canImport(Combine) | |
| import Combine | |
| extension Cache: ObservableObject { } | |
| #endif |
#149 (WASM): switch Combine/OSLog/SwiftUI-gated #if !os(Linux) && !os(Windows) guards to their canImport(...) checks. os(WASI) is neither Linux nor Windows, so the old guards pulled Combine/SwiftUI into the wasm build. Keychain/iCloud stay Apple-only. #150 (observation off Apple): delivery was bridged through Combine's consume(object: cache) (Apple-only), so withObservationTracking never fired on Linux/Windows/wasm. Now every mutation path (State/StoredState/FileState/SyncState/ SecureState/DependencySlice) calls notifyChange() directly on all platforms, and the Combine cache bridge is removed so Apple still fires exactly once. 163 tests pass on macOS. Observation tests stay gated off Linux/Windows: swift-corelibs -xctest cannot discover synchronous @mainactor test methods there (it force-casts test thunks to () -> () and aborts) — a test-harness limitation, not a runtime one. The #150 delivery code itself is cross-platform. Full wasm build + Linux collection-typed state also need the Cache fix (0xLeif/Cache#30). Addresses #149, #150. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two cross-platform fixes (baselined on 2.1.x, the line AppState 3.0 depends on).
WebAssembly build (AppState#149)
@Published/ObservableObjectwere gated with#if os(Linux) || os(Windows)/#if !os(Linux) && !os(Windows). WebAssembly isos(WASI)— neither Linux nor Windows — so wasm took the Combine path and failed to build (unknown attribute 'Published',cannot find type 'ObservableObject'). Switched both to#if canImport(Combine), which correctly excludes Linux, Windows, and wasm.Linux collection-cast crash (AppState#151)
Dictionary.get(_:as:)didself[key] as? Item. When the store isCache<String, Any>andItemis a collection (e.g.[SomeStruct]), castingOptional<Any> → [T]takes the runtime_arrayForceCastpath, which aborts (swift_dynamicCastFailure) on Linux/WASI. Boxing throughAnyfirst —(self[key] as Any) as? Item— normalises the cast and avoids the abort.Tests
Added array round-trip regression tests through
Cache<String, Any>. 178 tests pass on macOS.Closes the Cache side of AppState#149 and AppState#151.