Skip to content

Rule: prefer ownership transfer and regions to @Sendable requirements #76

Description

@coenttb

Problem

Institute APIs currently can impose @Sendable on closure values even when the actual law is one-shot ownership transfer, scoped borrowing, or region-based isolation. That unnecessarily requires concurrent reusability, blocks ~Copyable and ~Escapable designs, and encourages shallow constraint cascades instead of the deeper ownership refactor. The Institute rule is: prefer scoped borrowing and non-escape, then consuming/sending ownership transfer, then isolated/region transfer; reserve @Sendable for values genuinely reusable concurrently or stored across independently concurrent isolation, with an explicit local justification.

This policy is mechanically enforceable as an Institute Memory-family rule. The portable swift-linter engine already owns SwiftSyntax-based diagnostics, severities, fix refusal, and swift-linter:disable infrastructure; this repository is the exact owner of the Institute rule and its source-law fixtures.

Proposed outcome

Add an Institute Memory-family rule, tentatively identified as sendable sharing requirement, and enable it through Lint.Rule.Bundle.institute. Its structural contract is:

  • Diagnose public or package API closure parameters, stored/computed closure properties, closure-valued results, and public closure typealiases whose type carries @Sendable when the declaration expresses a one-shot boundary that can instead be represented by scoped non-escape/borrowing, consuming or sending, or isolated/region transfer. Report the declaration and the preferred next ownership spelling; do not claim semantic equivalence when syntax alone cannot prove it.

  • Emit a stronger diagnostic when the same declaration or enclosing generic surface carries ~Copyable, ~Escapable, consuming, or sending, yet @Sendable still constrains a value that crosses the boundary only once. These are direct signals that the API is single-use or region-transferred rather than concurrently reusable.

  • Accept justified sharing cases: closures stored and executable by multiple concurrent tasks; cancellation callbacks that may run concurrently; genuinely shared Sendable endpoints; and actor-independent reusable operations. Acceptance requires an adjacent local suppression in the existing engine format:

    // swift-linter:disable:next sendable sharing requirement
    // REASON: CATEGORY: <multi-task-storage|concurrent-cancellation|shared-endpoint|actor-independent-reuse>; SHARING: <concise proof naming the independently concurrent users or storage boundary>.

    The rule must mechanically reject a suppression whose REASON: lacks either a recognized CATEGORY: or a non-empty SHARING: proof. A generic statement such as “needed for concurrency” is not a proof.

  • Offer no automatic rewrite unless SwiftSyntax can prove the replacement is mechanically equivalent. A fix-it may be offered only for such proven cases; all other findings must explain the ownership preference and leave the deeper refactor to the author.

  • Encode the preference order in the diagnostic and rule documentation: scoped borrowing/non-escaping → consuming/sending ownership transfer → isolated/region transfer → @Sendable only when concurrent sharing is real.

Source-law fixtures must include both diagnostic and accepted controls for:

  • ~Copyable and ~Escapable closure/value boundaries;
  • sending parameters and results;
  • consuming closures and values;
  • actor and isolated-region transfer;
  • each accepted justified @Sendable category;
  • missing, unknown, and empty suppression category/proof fields;
  • the rule’s own positive control, proving the diagnostic fires.

The observable completion contract is: the structural fixtures pass in both directions; the rule is present in the Institute bundle; an audit run produces an adjudicable inventory; every false positive is either fixed in the predicate or locally suppressed with the required category and sharing proof; and the default advances from warning to error once that exact non-zero baseline is adjudicated. Warning is a staged rollout state, not the end-state.

After the rule lands and graduates, universal Institute CI consumes it through the existing generated leaf caller → universal reusable → verified ci-binaries swift-linter path. Do not add or edit repository workflow files for this issue; the existing one-hop integration is the contract.

Alternatives considered

  • Documentation only: rejected because it leaves a mechanically detectable ownership-law regression unenforced.
  • A blanket ban on @Sendable: rejected because concurrently reusable closures and independently concurrent storage genuinely require it.
  • Permanent advisory-only lint: rejected because the programme end-state is CI enforcement after false-positive adjudication.
  • Automatic conversion to sending or consuming: rejected except where SwiftSyntax can prove mechanical equivalence; ownership and escape semantics generally require design judgment.
  • Implementing the rule in the swift-linter engine or CI workflow: rejected because portable execution/fix mechanics and Institute policy have separate owners.

Compatibility and migration impact

The initial warning/audit stage will inventory affected public and package APIs across Institute packages without silently changing source. Findings will require either a deeper ownership/region refactor or the explicit category-plus-sharing proof above. Move-only and lifetime-dependent APIs should receive the strongest benefit because unnecessary @Sendable constraints currently exclude their intended forms.

Promotion to error follows this repository’s standing graduation discipline: a structural SwiftSyntax predicate, both-direction fixtures for every carve-out, and a non-zero fleet inventory whose findings have all been adjudicated. The final CI contract is enforcing, universal, and uses the existing one-hop swift-linter mechanism; this issue authorizes no workflow edits, CI reads, builds, tests, resolves, compiler probes, or source changes.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions