Skip to content

feat: deep handler semantics for effect rotation#2

Merged
vic merged 2 commits into
denful:vicfrom
sini:vic
Apr 19, 2026
Merged

feat: deep handler semantics for effect rotation#2
vic merged 2 commits into
denful:vicfrom
sini:vic

Conversation

@sini
Copy link
Copy Markdown
Collaborator

@sini sini commented Apr 18, 2026

Problem: when scope.run installs scoped handlers (e.g. a value
provider), effects not matching the scope rotate outward to the
enclosing handler. If the outer handler's resume is an effectful
computation, those resume effects were processed by the outer
interpret loop — never re-entering the inner scope. This meant
scoped handlers were invisible to effects originating from outer
handler resumes.

Example: inner scope handles effect A. Computation sends effect B
(not in inner scope). B rotates to outer handler. Outer handler
for B returns resume = send "A" null. Previously, A was processed
by the outer interpret (unhandled → error). Now, A passes through
the inner scope's handlers first.

Solution: three changes in the trampoline.

  1. effectRotate/effectRotateSlow tag rotation continuations with
    __rawResume = true on the queue.

  2. interpret checks __rawResume: if true, uses resumeWithQueue
    (passes raw resume to rotation continuation) instead of
    resumeCompOrValue (which would eagerly splice resume effects
    into the interpret loop).

  3. The rotation continuation uses resumeCompOrValue to route the
    raw resume through effectRotate, where inner handlers get
    first opportunity to handle the effects. Non-matching effects
    re-rotate outward as before.

This implements deep handler semantics as described in the algebraic
effects literature (Plotkin & Pretnar 2013, Koka, Eff, OCaml 5).
Continuations now capture the full handler stack — when a handler
resumes, the continuation runs with ALL handler layers still active.

Backwards compatible: all 1636 existing tests pass unchanged.
Plain value resumes (the common case) are unaffected.
3 new scope tests validate deep handler behavior.

@sini sini requested a review from vic April 18, 2026 23:06
@vic
Copy link
Copy Markdown
Member

vic commented Apr 19, 2026

Awesome, thanks for detecting this and fixing.

Do you mind to include a test that demonstrates this feature?

@sini sini force-pushed the vic branch 2 times, most recently from 0e86a1a to f945464 Compare April 19, 2026 19:09
@sini sini changed the title fix: properly route outer handler resumptions in effect rotation feat: deep handler semantics for effect rotation Apr 19, 2026
Problem: when scope.run installs scoped handlers, effects not
matching the scope rotate outward to the enclosing handler. If the
outer handler returns an effectful resume (a computation), those
resume effects were processed by the outer interpret loop — never
re-entering the inner scope. Scoped handlers were invisible to
effects originating from outer handler resumes.

Example: inner scope handles effect A. Computation sends effect B
(not in inner scope). B rotates to outer handler. Outer handler
for B returns resume = send "A" null. Previously, A was processed
by the outer interpret (unhandled error). Now, A passes through
the inner scope's handlers first.

Solution: three changes in the trampoline.

1. effectRotate/effectRotateSlow tag rotation continuations with
   __rawResume = true on the queue.

2. interpret checks __rawResume: if true, uses resumeWithQueue
   (passes raw resume to rotation continuation) instead of
   resumeCompOrValue (which would eagerly splice resume effects
   into the interpret loop).

3. The rotation continuation uses resumeCompOrValue to route the
   raw resume through effectRotate, where inner handlers get
   first opportunity to handle the effects. Non-matching effects
   re-rotate outward as before.

This implements deep handler semantics as described in the algebraic
effects literature (Plotkin & Pretnar 2013, Koka, Eff, OCaml 5).
Continuations now capture the full handler stack — when a handler
resumes, the continuation runs with all handler layers still active.

Backwards compatible: all existing tests pass unchanged.
3 new scope tests validate deep handler behavior.
@vic vic merged commit 5fcb6be into denful:vic Apr 19, 2026
1 check passed
@sini
Copy link
Copy Markdown
Collaborator Author

sini commented Apr 19, 2026

sniped

@sini
Copy link
Copy Markdown
Collaborator Author

sini commented Apr 19, 2026

actually it looks like you merged the pre-fix in

@vic
Copy link
Copy Markdown
Member

vic commented Apr 19, 2026

Ok will merge from latest commit 23965f1 you pushed, is that the right commit ?

@sini
Copy link
Copy Markdown
Collaborator Author

sini commented Apr 19, 2026

yes

@vic
Copy link
Copy Markdown
Member

vic commented Apr 19, 2026

done.

@sini
Copy link
Copy Markdown
Collaborator Author

sini commented Apr 19, 2026

FYI: It was only styling/rebasing to try and remove your 'den version' from my tree.

@vic
Copy link
Copy Markdown
Member

vic commented Apr 19, 2026

oh, hahaha. I was keeping that den version on my vic branch just for me to know which PRs are still pending upstream, but if you prefer a linear history, I can remove that empty commit on vic branch.

@sini
Copy link
Copy Markdown
Collaborator Author

sini commented Apr 19, 2026

I assume you'll just drop/re-order it when you need to. :)

@vic
Copy link
Copy Markdown
Member

vic commented Apr 19, 2026

yes, I do when our PRs get upstreamed.

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.

2 participants