Skip to content

feat: per-step setup/teardown with setup()#70

Open
schloerke wants to merge 32 commits into
r-lib:mainfrom
schloerke:issue-68
Open

feat: per-step setup/teardown with setup()#70
schloerke wants to merge 32 commits into
r-lib:mainfrom
schloerke:issue-68

Conversation

@schloerke

@schloerke schloerke commented Jun 2, 2026

Copy link
Copy Markdown

Summary

Adds coro::setup(expr) (closes #68) — registers an expression that runs at the start of every coroutine step (initial entry and every resume after yield/await), with any withr::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-level on.exit() cannot provide because it only fires once when the whole coroutine exits.

Works in generator(), async(), and async_generator().

Semantics

  • Isolated scope — the body runs in a child of the coroutine's environment; plain assignments stay local, while reads of args/lexical scope and external mutation (the$x <- ..., <<-) work.
  • Runtime registration, deduped per call-sitesetup() 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. Updated: setup() inside a for/while/repeat loop is now a compile-time error (it still registers when reached elsewhere, e.g. in an if branch). For per-iteration setup/teardown, delegate the loop body to a sub-generator (for (x in step(i)) yield(x)).
  • Stacking — multiple setup() calls run in registration order; their teardowns fire in reverse (LIFO) order.
  • Isolated teardowns — a failing teardown doesn't block the others; the first error is re-raised. cleanup() still runs (separate defer).
  • Compile-time guardsyield()/await() inside setup(), setup() inside a loop, and assigning setup()'s result are all errors.

Implementation

  • R/setup.R — exported setup() stub + docs (recognised by the compiler, like yield/await).
  • R/parser.R — recognise setup, compile it to do_setup(<call-site-id>, quote(<body>)); setup_state(), check_setup_body(), and check_setup_calls() (rejects setup() inside loops).
  • R/generator.R — runtime helpers (run_one_setup/do_setup/run_setups/run_step_teardowns) using a capture-and-clear of on.exit; a step-end defer() hook in instance().

Test plan

  • New tests/testthat/test-setup.R: per-step run + teardown timing, the feat: Add coro::setup(expr) - Handler support for promise domain setup/teardown parity #68 async await() 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 a withr::defer()/withr::local_*() contract test.
  • Compiled state-machine snapshot (_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.

Comment thread R/generator.R
schloerke added 2 commits June 2, 2026 14:12
…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).
schloerke

This comment was marked as resolved.

@schloerke schloerke marked this pull request as ready for review June 4, 2026 20:09

@lionel- lionel- left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look!

I have a few preliminary comments and questions

Comment thread tests/testthat/test-setup.R
Comment thread tests/testthat/test-setup.R Outdated
Comment thread tests/testthat/test-setup.R Outdated
Comment thread tests/testthat/test-setup.R Outdated
Comment thread tests/testthat/test-setup.R Outdated
Comment thread R/setup.R Outdated
Comment thread R/setup.R Outdated
Comment thread R/setup.R
Comment thread R/generator.R Outdated
Comment thread R/generator.R Outdated
schloerke added 6 commits July 1, 2026 17:23
…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.
@schloerke schloerke changed the title feat: per-step setup/teardown with setup() (#68) feat: per-step setup/teardown with setup() Jul 2, 2026
@schloerke schloerke requested a review from lionel- July 2, 2026 14:58
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.

feat: Add coro::setup(expr) - Handler support for promise domain setup/teardown parity

2 participants