Prototype A: vanilla async launch functions over the typed engine#117
Open
kyleve wants to merge 2 commits into
Open
Prototype A: vanilla async launch functions over the typed engine#117kyleve wants to merge 2 commits into
kyleve wants to merge 2 commits into
Conversation
Plan steps: vanilla-core, vanilla-ui-seam, vanilla-where — landed as one commit because the change is compile-coupled across the three layers (deleting the plan API breaks every consumer in the same build), unlike the incremental combinator migration this branch is based on. Core (LifecycleKit): - Setup and teardown are now ordinary async functions run through a LifecycleContext: data flows through lets (ordering is plain Swift), conditionality through ifs, and the context wrappers add exactly what a bare function can't — phase publication, run-once memoization, and failure attribution. Only the Void step overload accepts modes: (a value-producing step can't be skipped — now compile-time, replacing the combinator engine's runtime precondition), and detached bodies return Void so nothing can depend on fire-and-forget work. - LaunchPlan, its combinators/builders/erased nodes, and the LifecycleStep protocol are deleted; LifecycleGate slims to id + modes + Value (conditionality is the caller's if; the type survives because the UI registry keys on it). Phase, gate handles, cancel-and-drain, superseded-drive rules, promotion, detached drain-before-relaunch, and teardown-input release all carry over. - Memo integrity replaces construction-time validation: per-site launch/teardown memo stores (teardown IDs may now legally reuse launch IDs — the disjointness precondition is gone), a type-checked memo hit that traps on ID reuse across call sites, and a per-walk seen-IDs precondition that traps duplicates on any complete run. - Retry and promotion collapse into one code path: re-run the function with the memo. Deliberate semantic change, pinned by a new test: plain ifs before a failed step re-evaluate on retry. A throw outside any step is attributed to LifecycleFunctionID.launch/.teardown. - @_spi(Testing) executedStepIDs recording replaces nodeIDs for order-style assertions (the function has no inspectable structure). - Known footgun, documented loudly and pinned by a test: bare glue between steps re-runs on every re-drive; effects belong inside steps. UI (LifecycleKitUI): only the LifecycleDriving seam and LifecycleProxy.teardown change shape — the typed input + body are captured into a closure at the call site to cross the non-generic environment seam. Container, gate registry, and views untouched. Where: the step types fold back into WhereLaunch.launch(for:)/ reset(for:) as one readable function each (doc comments moved inline); LaunchStepID and OnboardingGate survive; SettingsView passes the reset function + session through the proxy. RootView/AppDelegate/WhereModel unchanged. Tests: kit runner/reset/fuzz suites ported to the closure API (the 200+120 seeded fuzz suites now build their launch functions dynamically from the same element lists); UI and Where suites updated; the parity test asserts on executed-step recording. Full Stuff-iOS-Tests scheme green; ./swiftformat --lint clean.
… model Plan step: vanilla-docs-pr (docs half). - LifecycleKit README/AGENTS now describe the async-function engine: the context wrappers, the effects-inside-steps discipline (bare glue re-runs on every re-drive), the memo/one-ID-one-call-site rules and their deterministic runtime traps, retry/promotion as one re-run code path, and the executed-ID SPI that replaces static structure inspection. - LifecycleKitUI docs + Where AGENTS/README updated for the teardown(input:_:) proxy signature and the launch-function shape. ./sync-agents run.
This was referenced Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
This is a prototype for comparison, not a default-merge candidate. The question it answers: how much of the combinator engine's (#116) safety survives if setup and teardown become ordinary async functions — and what does the simplification actually buy?
The combinator
LaunchPlanbought compile-checked data flow at the cost of a whole concept layer: step protocol conformances, plan combinators/builders, erased node internals, and one extra hop between "what the launch does" and where you read it.Changes
Setup and teardown are now vanilla async functions driven through a
LifecycleContext:LaunchPlan+ combinators/builders/erased nodes and theLifecycleStepprotocol;LifecycleGateslims to id + modes +Value(conditionality is the caller'sif; the type survives because the UI registry keys on it). Where's step types fold back intoWhereLaunch.launch(for:)/reset(for:)with their doc comments.Phase(.ready(Launch), gate handles), the entire LifecycleKitUI layer (container, gate registry — only the proxy'steardown(input:_:)signature changed), cancel-and-drain, superseded-drive rules, promotion semantics, detached drain-before-relaunch, teardown-input release,DetachedFailureReporter.ifs before a failed step re-evaluate on retry.What's kept vs. lost (the actual trade)
thenconstraintslets — equivalentpreconditionmodes:on the producing overload;ifforces anelse)Output == VoidconstraintVoidbodies — equivalentnodeIDs, parity tests)@_spi(Testing)executed-step recordingNet: −412 lines of Swift (−443 overall including docs; 1,365(+) / 1,808(−)), one whole concept layer removed, and Where's launch reads top-to-bottom in one function.
Testing
Same coverage shape as the base branch, ported: runner drive/gate/detached/promotion/retry/teardown suites, both seeded fuzz suites (200 randomized functions vs. an independent model, 120 flaky-retry drains — the fuzz now builds its launch closures dynamically), plus new pins for the changed retry semantics, the bare-glue-re-runs rule, and executed-ID recording. LifecycleKitUI suites pass with only construction-site updates; Where suites port mechanically (parity via executed IDs). Full
Stuff-iOS-Testsscheme green;./swiftformat --lintclean.