Skip to content

Allow @MainActor primitives as EnvironmentKey defaults (#39) - #40

Merged
searls merged 3 commits into
mainfrom
claude/determined-zhukovsky-fb4723
May 3, 2026
Merged

Allow @MainActor primitives as EnvironmentKey defaults (#39)#40
searls merged 3 commits into
mainfrom
claude/determined-zhukovsky-fb4723

Conversation

@bitsly

@bitsly bitsly commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves #39Selection<ID> and Job<Value> can now be used as the
defaultValue of a SwiftUI EnvironmentKey.

  • Selection<ID>.init() (parameterless) and Job<Value>.init() are now
    nonisolated. Property reads/writes on both types stay
    @MainActor-isolated; only construction is loosened.
  • Selection's previous init(_ initial: ID? = nil) is split into a
    nonisolated init() and an unchanged @MainActor init(_ initial: ID?).
    Existing call sites — Selection<X>(), Selection<X>("seed"),
    Selection<X>(nil) — continue to compile without modification.

Plan deviation worth flagging

The original plan assumed the @Observable macro's storage-init
accessor (@storageRestrictions(initializes: _current)) would let
nonisolated init bodies write to @Observable properties directly.
The Swift 6 compiler rejects this — self.observableProp = x in a
nonisolated init on a @MainActor class is treated as a main-actor
setter call regardless of the storage accessor. Confirmed
empirically; not a Swift bug, just a stricter rule than the plan
expected.

Adapted in flight:

Test plan

  • script/test_fast — 121 tests pass.
  • script/test — full suite + 100% line coverage gate on
    `Sources/Splint/`.
  • script/lint — clean.
  • New NonisolatedConstructionTests.swift is intentionally
    not @MainActor. Compilation success IS the contract — if a
    covered init regresses to main-actor isolation, the file fails to
    build.

🤖 Generated with Claude Code

searls and others added 2 commits May 2, 2026 11:52
Prompt:
Please let's plan out #39 - be sure to grill me before proposing plan

Response:
I'll start by reading issue #39 and exploring the relevant code, then grill you on the design before proposing a plan.Plan approved. Let me set up todos and begin implementation. I'll start by verifying the central assumption empirically — write the failing test first, then attempt the `nonisolated init` change.Now let me confirm this test file fails to build today (before the changes), which proves the test is meaningful.Red confirmed — exact errors from issue #39. Now applying the fix to Selection first.The red step was already confirmed via the build output above — the file failed to compile with the exact #39 errors. Continuing.Critical assumption in the plan was wrong: the @observable macro does NOT make init writes nonisolated. The compiler rejects `self.current = initial` in a `nonisolated init` even though `current` has the storage-init accessor. Let me verify whether Job (empty init) works, then reassess.Job compiles. Selection needs more work. Let me try a clean split: declaration-level default + `nonisolated init()` parameterless.Build is the test for this case (compile-time contract). The test file already exists; let me check whether the split init compiles, then adjust the test to match the actual achievable contract.Selection compiles with the split. The seeded `Selection("seed")` cannot be nonisolated (writing to `@Observable` forces `@MainActor`). I need to update the test to reflect the actual achievable contract: parameterless nonisolated only.Diagnostics may be stale. Let me rebuild and check actual state.Existing call sites all preserved. Now run the test suite.All 121 tests pass. Now the full suite with coverage gate.100% coverage held. Now CHANGELOG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Continuation of c3a3c4a

Prompt:
Stop hook feedback:
prove_it: nag-testing-antipatterns failed (async).

**[REDACTED:CLAUDE_PROJECT_DIR]/Tests/SplintTests/NonisolatedConstructionTests.swift**
- [AP-1: Assertion removal] `selectionWithInitialConstructsFromNonisolatedContext` was removed. `Selection.init(_ initial: ID?)` (Selection.swift:23) still exists in the source and is **not** marked `nonisolated` — unlike the parameterless `init()` on line 20. If construction with an initial value from a nonisolated context is intentionally unsupported, the correct fix is to make that explicit (a compile-time note or a design choice recorded elsewhere), not silently drop the coverage. If it is supposed to work, the test should remain.
  Evidence: removed `@Test func selectionWithInitialConstructsFromNonisolatedContext() { let selection = Selection<String>("seed"); _ = selection }`

- [AP-1: Assertion removal] `settingConstructsFromNonisolatedContext` was removed. `Setting.init(_:default:store:)` (Setting.swift:44) still exists and is not marked `nonisolated` — `Setting` is `@MainActor` with no `nonisolated` designated initializer. If this was always untestable from a nonisolated context, the test should never have been written; but removing it without explanation hides a potential gap in the nonisolated construction contract for `Setting`.
  Evidence: removed `@Test func settingConstructsFromNonisolatedContext() { let setting = Setting("splint.nonisolated.test.bool", default: false); _ = setting }`

--- Continuation Note ---

**Confidence Score: 40**

The edit removes two tests whose underlying initializers are still present in source but lack `nonisolated` annotations. This makes the tests either (a) tests that would not compile in a strict-concurrency nonisolated context — meaning they were incorrectly written and the removal is legitimate cleanup — or (b) tests for real behavior that was silently lost when the `nonisolated` annotation was stripped from those initializers.

**Critical Findings:**

1. `Selection.swift:23` — `public init(_ initial: ID?)` has no `nonisolated` modifier. The removed test claimed this init is callable from nonisolated context. Either the test was wrong (and its removal is fine), or the `nonisolated` modifier was removed from the production init and this removal is a cover-up. Determine which by checking git history on `Selection.swift`.

2. `Setting.swift:44` — `public init(_ key: String, default defaultValue: Value, store: UserDefaults = .standard)` has no `nonisolated` modifier. Same ambiguity: legitimate cleanup of a wrong test, or hiding a regression.

**Correct fix if tests were wrongly written:** Add a comment in the test file documenting that `Selection(_:)` and `Setting(_:default:)` intentionally require `@MainActor` context and are not nonisolated — so the absence of tests is intentional, not accidental.

**Correct fix if a regression occurred:** Restore `nonisolated` to those initializers (or explicitly add `nonisolated init(_ initial: ID?)` to `Selection`) and restore the tests.

**Watch List for Next Reviewer:**
- Check `git log -p Sources/Splint/Selection.swift` to see if `nonisolated` was ever on `init(_ initial:)` and was removed.
- Check `git log -p Sources/Splint/Setting.swift` similarly.
- The test file itself is new (untracked `??` in git status), so the "before" in this edit represents an intermediate draft of the new file — the agent is writing then revising the file. The question is whether the revision reflects intentional scoping or evasion.

**What Was Done Well:** The two remaining tests (`selectionConstructsFromNonisolatedContext` and `jobConstructsFromNonisolatedContext`) correctly target initializers that are explicitly `nonisolated` in the source (`Selection.swift:20`, `Job.swift:33`). Those are valid and well-scoped.

Before acting on this failure, ask yourself: does this apply to the work I'm currently doing?
- If it MAY apply to your current work—address the issue.
- If you are confident it DOES NOT apply (you're planning, the flagged
  code isn't yours, the changes are unrelated)—appeal the decision.

To appeal, write your reasoning in:
[REDACTED:CLAUDE_PROJECT_DIR]/.claude/prove_it/sessions/0554e15c-38f2-4cfd-b212-3bc29e8e4a1a/backchannel/nag-testing-antipatterns/README.md

Response:
The reviewer's concern is fair — the test file should document why seeded Selection and Setting aren't covered. Let me add that explanation, then continue with CHANGELOG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@claude

claude Bot commented May 2, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@searls
searls merged commit f280305 into main May 3, 2026
2 checks passed
@searls
searls deleted the claude/determined-zhukovsky-fb4723 branch May 3, 2026 01:04
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.

Allow @MainActor primitives to be used as EnvironmentKey.defaultValue (nonisolated init feasibility)

2 participants