Skip to content

Feature: [Swift6] Sendable / Strict Concurrency - Swift emitter never emits Sendable, so generated types cannot cross an isolation boundary #120

Description

@fbartho

I generate Swift types with facet_generate 0.19.0, via crux_core's typegen.swift(), into a SwiftPM package that also receives bindings from a second generator (BoltFFI). Under Swift 6's complete concurrency checking, none of the facet_generate-emitted types can cross an actor isolation boundary.

Reproduction

Generate any struct or enum with the Swift target and use it inside a Task or across an async boundary in a Swift 6 project:

error: sending 'myValue' risks causing data races

Measured over my generated output — 1,929 lines, 30 generated type declarations:

Conformance Occurrences
Hashable 30
Equatable 30
Sendable 0

So Hashable and Equatable are emitted, correctly and on every type. Sendable is the only one missing. The string Sendable does not appear anywhere in facet_generate 0.19.0's source outside test fixtures — the emitter has no code path that produces it.

The types are structurally sendable: the emitter produces only structs and enums over String, Bool, the fixed-width integers, UUID, Optional, and Array of the same, with no class declarations in the output.

Why this is awkward in practice

The same SwiftPM package also holds BoltFFI-generated types, which do carry Sendable. One package directory ends up with two classes of generated Swift type, and there is no way to tell which is which except by trying to use one across an isolation boundary. The compiler error names the call site that crossed the boundary rather than the type missing the conformance, so there is no path from the diagnostic to the cause.

Where the gap is

Hashable and Equatable are computed deliberately: the emitter documents the rule at src/generation/swift/emitter/mod.rs:45-51 — types whose fields are all Hashable declare the conformance so they can serve as Set elements or Dictionary keys — and it raises a generation-time error when a non-Hashable type is used as a Set element or Map key. That machinery works. It simply has no Sendable equivalent.

There is an extension point that could supply one. EmitterPlugin::type_conformances (src/generation/plugin.rs:261) appends arbitrary conformances to a type declaration and defaults to empty. crux_core passes only BincodePlugin and does not implement the hook, and its Config does not expose the Installer to callers — so a downstream consumer cannot reach it from the typegen.swift() path.

A default in this emitter looks like the root fix: every type it can produce is structurally sendable, so emitting the conformance mirrors how Hashable and Equatable are already computed here. If you would rather keep default output unchanged and treat Sendable as opt-in — via the decorator or the plugin hook — then crux_core additionally needs to expose plugin configuration before any consumer on that path could opt in, which would be a second, separate issue.

The decorator fixtures

src/tests/test_default_decorators/mod.rs carries a commented-out // TODO: #[facet(swift = "Equatable")], and that fixture's output.swift shows struct EmptyType: Codable, Equatable, Identifiable, Sendable. Two sibling fixtures (struct_decorator, adjacently_tagged_enum_decorator) carry the same shape of TODO.

These read as the expected output of a per-type decorator feature that is not implemented yet — the current emitter produces none of Codable, Identifiable, or Sendable for real input.

Suggestion

Either default to emitting Sendable wherever it is derivable — which appears to be everywhere for the field types the emitter already supports, and would match how Hashable and Equatable are already handled — or finish the per-type decorator so consumers can opt in.

Workaround: hand-write retroactive conformances in a separate file, which is exactly the drift generated types are supposed to eliminate: every type added to the source arrives in Swift without Sendable, and nothing regenerates the workaround.

LLM Disclosure

I was using claude-code to explore making a Crux app. I noticed code I didn't like, and pushed back against the agent and investigated more deeply. The above ticket description was authored with the agent across several rounds, but I'm happy to trim it down entirely to content that I write by hand.

Please let me know if you need more input from me! I'd be happy to make a stab at fixing this issue, but I'd like a maintainer's input for which suggested approach (if any) is appropriate.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions