feat: per-step setup/teardown with setup()#70
Open
schloerke wants to merge 32 commits into
Open
Conversation
Add @examples to setup(), add withr to Suggests (was used in tests but undeclared), and add NEWS bullet for setup().
schloerke
commented
Jun 2, 2026
…frame (r-lib#68) Use base on.exit() instead of withr::defer() in tests and the example so coro gains no new package dependency (withr removed from Suggests). This exposed that teardowns must be evaluated in the setup body's own execution frame so a bare on.exit(x <- old) can resolve the body's locals; run_one_setup now captures and replays in that frame (withr::defer closures still work).
lionel-
reviewed
Jun 15, 2026
lionel-
left a comment
Member
There was a problem hiding this comment.
Thanks for taking a look!
I have a few preliminary comments and questions
# Conflicts: # NEWS.md # R/generator.R
Mixing per-step setup registration with iteration has opaque semantics, so `setup()` inside for/while/repeat is now a compile-time error (detected by a recursive AST check that ignores nested coroutine bodies). Document and test the sub-generator delegation workaround for per-iteration setup/teardown.
…lib#68) - Symmetric @description (entered/resumed vs suspends/finishes) with the yield/await + exit enumeration. - New "When setup and teardown run" section with a step-by-step timeline contrasting a regular defer, a setup defer, and a setup assignment. - "Assignments and scope" explains why plain assignments are local (setup re-runs each step) with a worked example. - "Using setup() in a loop" shows the naive error and the sub-generator workaround; rename the sub-generator to generate_x to avoid clashing with the "step" concept.
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.
Summary
Adds
coro::setup(expr)(closes #68) — registers an expression that runs at the start of every coroutine step (initial entry and every resume afteryield/await), with anywithr::defer()/on.exit()it registers torn down at the end of that step (suspend, return, error, or close). This gives per-step setup/teardown parity, which a top-levelon.exit()cannot provide because it only fires once when the whole coroutine exits.Works in
generator(),async(), andasync_generator().Semantics
the$x <- ...,<<-) work.Runtime registration, deduped per call-site —Updated:setup()registers when reached; re-encountering the same call (e.g. in a loop) is a no-op, so it runs once per step, not per iteration.setup()inside afor/while/repeatloop is now a compile-time error (it still registers when reached elsewhere, e.g. in anifbranch). For per-iteration setup/teardown, delegate the loop body to a sub-generator (for (x in step(i)) yield(x)).setup()calls run in registration order; their teardowns fire in reverse (LIFO) order.cleanup()still runs (separatedefer).yield()/await()insidesetup(),setup()inside a loop, and assigningsetup()'s result are all errors.Implementation
R/setup.R— exportedsetup()stub + docs (recognised by the compiler, likeyield/await).R/parser.R— recognisesetup, compile it todo_setup(<call-site-id>, quote(<body>));setup_state(),check_setup_body(), andcheck_setup_calls()(rejectssetup()inside loops).R/generator.R— runtime helpers (run_one_setup/do_setup/run_setups/run_step_teardowns) using a capture-and-clear ofon.exit; a step-enddefer()hook ininstance().Test plan
tests/testthat/test-setup.R: per-step run + teardown timing, the feat: Addcoro::setup(expr)- Handler support for promise domain setup/teardown parity #68 asyncawait()reprex, stacked setups, teardown isolation, compile-time guards,and a labelled "Known shortcomings" section pinning deliberate limitations.Updated: expected-behavior assertions (child-environment scope, non-retroactive registration, teardown-error handling, abandoned promise), the loop-is-an-error case with the sub-generator workaround, and awithr::defer()/withr::local_*()contract test._snaps/setup.md).devtools::check()clean: 0 errors, 0 warnings, only the pre-existing hidden-files NOTE. Full suite green except pre-existing reticulate/Python-not-installed failures.