WorksComputer Science
Handling JAX
Abstract
Languages that target hardware accelerators typically expose a restricted, first-order core without recursion or first-class functions, leaving programmers without a principled way to express computational effects. This paper presents a technique for compiling effect handlers to such a constrained target. We introduce a novel translation that statically eliminates effect handlers, and prove that it preserves both typing and semantics. Moreover, we address the challenges of integrating effect handlers with structured control flow, where continuations cannot be constructed statically. The calculus and all our results are fully mechanized in the Lean 4 proof assistant. Finally, we implement our design as a library for JAX, a Python domain-specific language for accelerator programming, and demonstrate its expressiveness through practical case studies.
Introduction
“When walking about the countryside of Italy, the people will not hesitate to tell you that JAX has ‘una anima di pura programmazione funzionale’.” – JAX documentation1
Languages that target hardware accelerators are deliberately restrictive. To generate efficient code for GPUs and TPUs, they typically expose a purely functional, first-order core: programs are built from array primitives and a fixed set of structured control-flow constructs such as conditionals and loops, without recursion or first-class functions. Examples include array languages such as Futhark [21], Dex [36], and Accelerate [7], and compiler intermediate representations such as XLA [17]. While these restrictions enable aggressive optimization, they leave programmers without a principled way to express computational effects such as mutable state and exceptions. Without language support, each such feature is typically implemented via a bespoke program transformation or an external library.
We study this problem in the setting of JAX [6], a domain-specific language embedded in Python for accelerator-oriented array computations, widely adopted in high-performance numerical computing and large-scale machine learning, including for Google’s large language models [8, 15, 18] and by Anthropic [2]. JAX traces Python programs into a first-order intermediate representation, jaxprs, with exactly the restrictions described above, and lacks direct support for effectful features. As a result, popular JAX libraries resort to different, often ad-hoc mechanisms to implement these features, causing inconsistencies and interoperability issues across the ecosystem (see § 2).
We address this challenge using algebraic effect handlers [41, 42], a flexible and composable mechanism for defining structured control-flow abstractions. While effect handlers have been incorporated into various languages [3, 29, 48], including recent work on low-level languages such as C [1, 28], C++ [16], and WebAssembly [38], all such implementations rely on features that first-order languages lack: higher-order functions, recursion, or a capturable runtime stack. The central question of this paper is therefore: how can effect handlers be compiled to a first-order target without higher-order functions or recursion? We resolve this by introducing a translation that statically eliminates handlers, carefully extending the technique across loop boundaries where continuations cannot be constructed statically.
Contributions. Following the high-level overview in §2, we present our contributions in the following sections:
Formalization (§3): We formalize , a calculus that extends a core subset of JAX’s intermediate representation, a first-order language with second-order control-flow primitives, with parameterized algebraic effect handlers. We establish preservation and progress with effects.
Translation (§4): We introduce a novel continuation-based translation that statically compiles effect handlers away without needing higher-order functions or recursion, and prove that it preserves both typing and semantics.
Iterative control flow (§5): We analyze the challenges of translating effect handlers across loop boundaries, where continuations cannot be constructed statically, and present a mechanism to support tail-resumptive and abortive handlers while preserving handler parameters and scoping.
Mechanization: We fully mechanize our calculus, its type system and operational semantics, translation, and all soundness proofs in the Lean 4 proof assistant.
Implementation (§6): We implement
effecax, an effect handler library for JAX, outlining its programming interface and our implementation.Evaluation (§7): We evaluate
effecaxacross practical case studies, demonstrating that it provides an expressive, flexible, and natural interface for JAX programs.
While this paper focuses on JAX, where effecax provides immediate utility to a broad user base, neither the problem nor our solution is specific to it: the compilation applies to any first-order target, such as XLA’s intermediate representation. More broadly, we hope this work offers a new perspective on compiling effect handlers to low-level target languages. Lastly, we discuss related work in §8.
Overview
Motivation
JAX adopts a functional programming paradigm: its compilation pipeline is designed for pure Python functions free of side effects. This model aligns naturally with modern machine learning workloads, which predominantly consist of static mathematical operations connected in computation graphs. Functional purity simplifies graph optimization and yields significant performance gains [14]. Moreover, purity enables JAX to reliably trace Python functions into its intermediate representation, jaxpr2: a first-order, statically typed language in A-normal form [13], with a fixed set of second-order control-flow primitives such as cond and while. JAX also provides powerful function transformations, such as jax.grad for automatic differentiation, jax.vmap for automatic vectorization, and jax.jit for just-in-time compilation of functions to hardware accelerators via XLA [17].
The effectiveness of this functional programming model is evidenced by JAX’s widespread adoption in the machine learning community [2, 8, 15, 18]. Its paradigm of composable transformations is so powerful that it inspired PyTorch’s functorch3, which has been fully integrated into core PyTorch4.
However, while the functional programming model is highly desirable for machine learning workloads, practical ML programming is inherently stateful and side-effect intensive. ML training pipelines must manage mutable state, handle exceptions, perform asynchronous computation, and occasionally use specialized effects such as coroutines in probabilistic programming. In short, computational effects abound. This creates a fundamental tension: JAX’s functional core provides no principled mechanism for needed side-effectful computations. To illustrate this, we examine two effects common in everyday ML programming: mutable state and exception handling.
Case 1: Mutable state. To use mutable references, developers can either leverage JAX’s experimental built-in Ref arrays5 or rely on external libraries. The Ref approach uses a custom program transformation that converts programs containing Refs into purely functional programs by automatically threading state parameters through the computation.
Alternatively, JAX-based libraries such as Flax NNX6, a popular neural network library, expose Modules that are mutable by default. To maintain compatibility with JAX’s functional requirements, NNX implements its own transformations, such as flax.nnx.jit. This transformation wraps jax.jit by “lifting” the mutable module: it splits the module into a static structure and a pure state, applies the transformation to those pure components, and then propagates any state updates back to the original mutable object.
Both approaches involve trade-offs. While Ref integrates tightly with JAX’s built-in transformations, it is constrained by JAX’s tracing rules: for instance, Refs cannot be returned from jit-compiled functions or from the bodies of higher-order primitives (e.g. while), and cannot be passed where arrays are expected. Conversely, flax.nnx.jit provides a more ergonomic mutable interface, but requires a duplicated ecosystem of custom transformations and incurs Python performance overhead in training loops from repeatedly lifting stateful objects7.
Case 2: Exceptions. A similar situation exists for exception handling. JAX provides an experimental built-in transformation called checkify8, which rewrites a program that may raise errors into a functionally pure version that returns error values. JAX libraries such as Equinox9, on the other hand, offer equinox.error_if to raise runtime errors imperatively during execution.
While checkify adheres to JAX’s functional paradigm and composes cleanly with other transformations, it introduces significant boilerplate by forcing developers to manually propagate and inspect error values throughout the computation. It also lacks support for early termination, which can be important for performance. Conversely, Equinox makes it easy to insert error checks into existing models without refactoring, but degrades performance by forcing synchronous execution barriers between the hardware accelerator and the host.
Current landscape and limitations. As a result, JAX programmers must constantly choose between specialized, ad-hoc JAX transformations or fragmented external libraries. Neither is ideal. Dedicated internal transformations encourage code duplication and lack modularity. For instance, the Ref transformation implements state mutation using a substantial amount of code, while the checkify transformation relies on an entirely separate implementation. Despite this, both transformations share similar structures and “parameter plumbing” patterns. This redundancy inflates JAX’s codebase and its surface for compiler bugs.
Alternatively, relying on custom libraries often results in performance degradation, poor tool integration, and ecosystem fragmentation. Interoperability remains a painful challenge. For example, disparate state management mechanisms across libraries require complex, fragile manual state conversions10. Moreover, combining multiple effects from different libraries may not work as expected.
Furthermore, if a developer requires a specific effect for which no custom JAX transformation or third-party library exists, they are forced to sacrifice expressive power or resort to brittle, ad-hoc workarounds. Given JAX’s central role in machine learning, the lack of a unified approach to computational effects remains a fundamental limitation.
Key Ideas
To resolve these limitations, we seek a side-effect framework that has:
Extensibility: Developers should be able to define custom side effects as well as specify their corresponding handling logic. Multiple effects should compose seamlessly; for example, a program should be able to manage mutable state while simultaneously throwing exceptions.
Integration: The mechanism should integrate seamlessly with the language’s existing facilities. Moreover, since inspecting the intermediate representation is standard practice for debugging and optimization, it should produce clean, idiomatic code in that representation.
Backend preservation: Accelerator-oriented languages rely on a downstream compiler, XLA in the case of JAX, to target hardware. The mechanism should remain fully compatible with existing backends and require no changes to that compiler.
In this work, we propose to extend JAX with algebraic effect handlers [40, 43], a flexible framework for extensible and composable computational effects in functional settings. Concretely, we extend the jaxpr representation with effect-handler constructs. As an example, consider the function safe_div, which performs a throw operation on division by zero (types are omitted; the syntax is detailed in § 3):
We handle it with a handler whose parameter (args) is unused and thus takes the unit value , runs the handled computation (body), which calls safe_div on 42 and 0, returns its result unchanged (ret), and handles throw by resuming the computation with the fallback value 0 (impl):
We then compile these constructs away by a translation that produces standard, vanilla jaxprs, ensuring that existing transformations and backends apply unchanged. Realizing this plan, however, raises two key challenges, which we discuss next.
Challenge 1: a first-order target. Effect handler implementations typically capture operation continuations either at runtime, by copying or manipulating the stack [1, 16, 38, 48], or at compile time, via continuation passing style (CPS) translation that represents continuations as closures [23, 46, 47]. Jaxprs offer neither facility, as they have no runtime stack to capture, and no higher-order functions or recursion to represent continuations.
We present a novel compilation strategy for this setting; to our knowledge, this is the first work to study effect handlers under such restrictions. Conceptually, our compilation combines a CPS transformation, which compiles handlers into continuations, with partial evaluation to statically evaluate all handler-related constructs. As a result, neither handlers nor continuations appear in the output, leaving only primitive computations for the hardware. Fusing CPS with static reduction is a classic idea underlying one-pass CPS transformations [9, 10] and CPS translations of effect handlers without administrative redexes [23, 47]; we apply it here directly to the handler constructs themselves. On the example above, the translation produces the following jaxpr:
Here, the do throw in the then branch has been statically evaluated to its handling code, here just the value 0, while the primitive computations are left untouched for execution on the hardware.
Challenge 2: loop boundaries. This strategy, however, requires the continuation of every operation to be known statically. Consider an operation performed inside a while loop but handled outside it, as in Figure 1. The continuation of the operation comprises not only the rest of the current iteration but also all remaining iterations, that is, the loop itself. Translating it would then unroll the loop. As a result, the translation does not terminate. Unfortunately, this is a common use case: the state and exception effects of § 2.1 are typically performed inside training and solver loops whose handlers live outside. We support these most common cases by identifying two classes of handlers that can nevertheless be compiled across loop boundaries.

Figure 1. Handling across the loop boundary
We focus on tail-resumptive handlers, which resume the computation exactly once at the end of the operation clause and are common in practice [52, 54]. For such common clauses, we avoid materializing continuations entirely by handling operations in-place and inlining the clause directly into the loop body. This approach introduces two subtleties that we systematically address: threading handler parameters through the loop via an extension we call the final continuation, and preventing naive inlining from altering the handler scope. Compared to prior work, our approach is novel in several aspects: it applies to jaxprs, supports parameterized handlers, and preserves semantics without restrictions [52, 54].
Additionally, we support abortive handlers, which never resume and are common for exceptions. We achieve this through a complementary source-level rewrite of the while expression, denoted , before translation. Importantly, our abortive handlers are more general than JAX’s checkify, which must complete all iterations before checking for errors; in contrast, ours catches the exception inside the loop and aborts at the next condition check.
These two classes do not cover every handler; for example, a clause that resumes more than once is not supported across a loop boundary. We discuss this limitation further in § 8.
The rest of the paper proceeds as follows. § 3 presents the calculus, § 4 the translation, and § 5 its extension across loop boundaries; all results are mechanized in Lean 4. § 6 describes the effecax library built on the translation, and § 7 evaluates it through case studies.
The Calculus
This section presents , a calculus formalizing a core subset of jaxprs extended with effect-handling constructs. At its heart, is simply a first-order calculus with second-order primitives: functions cannot be passed as values, but a fixed set of primitives, such as conditionals and loops, take jaxprs as arguments. This design is not specific to JAX; rather, it captures the essential shape of accelerator-oriented intermediate representations. At the same time, the calculus remains faithful to actual jaxprs, allowing our translation to apply directly to the implementation. To our knowledge, this is the first formalization of a core jaxpr language.
Syntax
Figure 2 presents the syntax of . The calculus is a statically-typed, first-order language in A-normal form (ANF) [13]. We focus on modeling a minimal set of primitives; additional primitives can be added in a straightforward manner. The syntax comprises three syntactic categories: types, terms (which include expressions and jaxprs), and primitives. We assume disjoint name spaces for variables (), constants (), basic primitive functions (), and effect operations ().

Figure 2. Syntax of
Types are product types, written as sequences of array types for typing tensors (i.e. multidimensional arrays). Array types have the form , where represents the underlying scalar data type (e.g. float, int), and is a shape tuple specifying the number and sizes of the tensor dimensions (e.g. ). For scalar values, we often omit the empty shape tuple for brevity, e.g. writing bool instead of . We also often write for the unit type (and its unit value).
Expressions consist of a sequence of let bindings that end with a return statement . Binders are typed variables. Atoms are either variables or literals specifying tensors. Literals include constants and tensors . A jaxpr is a lambda expression, written .
Primitive operations include both basic functions and higher-order primitives built from jaxprs. These higher-order primitives include function calls (call ), that call the jaxpr when applied within let bindings; conditionals (cond ), that take a boolean argument and execute the appropriate branch; while loops (while ) with condition jaxpr and body jaxpr ; and scans (scan ) that provide a functional loop-fold, where iterates over input arrays while updating a carry state initialized by .
To support algebraic effects, introduces primitives for performing and handling effectful computations. First, the primitive do performs an effectful operation . Second, following Leijen [29], Schuster et al. [47], we represent resumptions using a resume primitive. Finally, the syntax includes a parameterized handler primitive: handle . This consists of four components: the arguments provide the initial value for the handler parameters; the body expression provides the computation being handled; the return jaxpr converts the final handler parameters and the body’s result into the final output; and the implementation mapping gives the jaxpr clauses defining the operations.
Throughout this paper, we use an overline to denote sequences; for example, represents the sequence (), and indicates that each has the corresponding type .
Typing
Figure 3 presents the typing rules for . The typing context is a finite sequence of variable typings and at most one resumption typing resume: . We assume no variable appears twice within a context, and we work up to -equivalence as usual. We use in and out for the input and output types of an operation, and par for the type of a handler’s parameter. We write for the component-wise derivation for all (assuming ).

Figure 3. Typing rules for
There are four typing judgments; most rules are straightforward. The judgment types atoms. In rule T-Const, primitive constants have predefined base types (e.g. , and ). Rule T-Array forms a tensor of shape from tensors of shape . The judgment types expressions. Rule T-Let checks that the arguments of a primitive have type , and extends the context with to type the body . Rule T-Ret types the returned atoms. The judgment types jaxprs as standard lambda abstractions.
The judgment types primitives. Basic primitives (rule T-Basic) are assumed to have given signatures (for example, tensor concatenation ). Rule T-Call promotes a jaxpr to a primitive of the same type. Rule T-Cond requires both branches to have the same type and yields type . Rule T-While follows the standard typing of functional loops, with condition and state update . Rule T-Scan takes an initial carry of type and a body , and yields type .
For effects, each operation is assumed to have a given signature . Rule T-Do uses this signature, and requires an argument of type to produce a result of type . Rule T-Handler types handler expressions: the initial parameters have type , the handled computation has type , and the return clause maps the final parameters and the result to the overall output type . Each operation clause is typed under a context extended with resume : , so that resuming takes updated parameters and the operation result and yields the handler’s output. The handler has type . Rule T-Resume simply retrieves the type of resume from the context.
Operational Semantics
Figure 4 presents the operational semantics of , which serves as the reference behavior against which we prove that our translation preserves semantics. The top of Figure 4 defines auxiliary notations used in the operational semantics. The application expands to , and abbreviates . A context is either a hole or a sequence of let-bindings ending in a hole. Every well-formed expression can be uniquely written as . The operation

Figure 4. Operational semantics for .
then takes , substitutes for the binders in , and plugs the result into the hole of . Plugging is capture-avoiding (our mechanization uses a locally nameless representation, and our implementation performs -renaming when needed). This plugging is similar to re-normalization, which is a well-known technique used to keep ANF terms valid during reduction [26].
In rule S-FUNC, we assume each basic primitive comes with a total, deterministic evaluation relation (e.g. ), and the results are substituted for the binders .
Rule S-CALL evaluates a call. However, reducing directly to would violate ANF. So instead, rule S-CALL reduces to , which runs the jump application and binds its results in the rest of .
The control-flow rules are standard. Rule S-COND reduces a conditional to a call of the branch selected by the boolean . Rule S-WHILE evaluates the condition and, if true, runs one iteration of before calling the while primitive again; otherwise it returns the current state . Rules S-SCAN1 and S-SCAN2 evaluate a scan by induction on the input array, running the body on the first element and recursing on the rest.
The final four rules are for handlers. Rule S-HCONG evaluates the handled computation, and rule S-HRET applies the return clause to the current parameters and the result once the computation finishes. For operations, rule S-HDO1 forwards an operation to the handler does not handle, while rule S-HDO2 calls the corresponding clause with the handler parameters and operation arguments.
_footer: Vol. 1, No. 1, Article . Publication date: November 2026.
Within the clause, resume is replaced by a call whose body captures the continuation following the operation and reinstalls the handler. We inspect only to determine the parameter type needed to construct . Appendix § 10 illustrates these rules with two example programs that use handlers for exceptions and for state.
Metatheory
We prove that enjoys the following desirable properties:
Lemma 3.1 (Determinacy). If and , then .
Theorem 3.2. (Preservation) If and then .
(Progress with effects) If , then for some , or has the form , or has the form .
The third case of progress arises because does not track effects in types, so a well-typed program may perform an unhandled operation; the theorem nevertheless guarantees that this is the only way it can get stuck. An effect type system [22, 29] would rule this out, but is orthogonal to our focus, and JAX itself does not statically track effects.
Effect-Handling Translation
We compile effect handlers away via a novel translation from to its handler-free subset , while leaving primitive computations to the hardware. This section presents the core translation without control flow; control flow is addressed in § 5.
The Translation
Figure 5 presents the translation rules. The function translates an expression with respect to a handler context mapping operations to clauses, their parameters , and a list of continuations , which are jaxprs in our setting. The list contains both return clauses of handlers and normal continuations , which we assume are kept distinguishable. (The special form is discussed in § 5.1 and can be ignored for now.) The function maintains the invariant that the length of matches both the number of return clauses in and the length of . We assume that the output type of the elaboration is given, which will always be provided (e.g. in Theorem 4.3).

Figure 5. Effect-handling translation
The first three rules translate according to the continuation list. If the list is empty (E-Ret1), the translation is complete. Otherwise, if the innermost continuation is a normal continuation (E-Ret2), we apply it to and translate the result with removed. If it is a return clause (E-Ret3), we apply to the handler parameters and , and continue after popping the innermost handler , its parameters, and its return clause from their lists.
Primitive functions are preserved, since they are to be evaluated on the hardware; thus, rule E-Func keeps the function and translates the body. For call and cond, when the handler and continuation lists are empty (E-Call1 and E-Cond1), we simply translate the subexpressions recursively, using the jaxpr translation given at the bottom of the figure. Otherwise (E-Call2 and E-Cond2), the subexpressions may perform operations, so we capture the current continuation , prepend it to the continuation list, translate the subexpressions, apply the result to , and return the result bound to fresh variables .
Translating a do expression (rule E-Hop) is slightly more involved. First, we look up the operation clause in the handler , together with its corresponding parameters and return clause . This lookup relies on the structural property that the length of the handler list equals both the number of return clauses in and the length of the parameter list . We then translate applied to and under the lists preceding . The complication is that may use resume, which
where ,
where
replaced by its continuation. Yet we cannot translate that continuation immediately: resume may be invoked inside under various contexts, so the context for translating the resumed expression is unknown at this point. We therefore substitute resume with a call primitive whose body receives the updated parameters and the operation results , and returns the special expression , which captures the lists up to along with the updated parameters. Rule E-HResume then resolves a construct by concatenating the captured lists with the current ones and translating in the combined context. Therefore, if resume is used several times inside , is translated under each context as intended.
Finally, E-Handle1 and E-Handle2 translate handle expressions. When the handler list is empty (E-Handle1), we translate with the current handler and bind its result to the translation of under empty lists. Otherwise (E-Handle2), we push the handler, its parameters, the continuation , and the return clause onto the lists to translate the handled expression.
Elaboration Soundness and Semantics Preservation
First, it is worth noting that our translation is a selective CPS transformation [34]: CPS is performed only when the stack is non-empty.
Lemma 4.1 (Pure programs). If contains no handler-related constructs, then .
footer: , Vol. 1, No. 1, Article . Publication date: November 2026.
We then prove a group of mutually recursive structural properties of the elaboration:
Lemma 4.2 (K-family lemmas). (K-push) . (K-compose) . (K-delay) .
We now turn to elaboration soundness. First, we define well-formedness, denoted as a judgment indicating that the lists take an expression of type and produce one with type . Here, abbreviates the operation-clause premises of rule T-HANDLER for a handler that takes parameters and has result type . The last premise of WF-CONT ensures continuations always end with a return clause.
(WF-EMPTY)
(WF-CONT)
(WF-HANDLER)
We now establish that our translation preserves typing; a similar statement holds for jaxprs.
Theorem 4.3 (Elaboration Soundness). Given , if , then .
Next, we prove semantics preservation. Semantic preservation proofs usually proceed by constructing a simulation diagram [30]; we first prove that such a diagram exists and commutes:
Lemma 4.4 (Simulation step). If , then
Using the simulation step, we then prove semantics preservation:
Theorem 4.5 (Semantics preservation). If , then .
Handling Control-Flow Primitives
The translation of the previous section does not account for control-flow primitives such as while and scan. Consider translating . Translating the loop body requires extending the continuation list with the current continuation, which contains a copy of the while expression itself. Since continuations are unrolled during translation, we eventually return to translating the same while expression, and the translation fails to terminate! The root cause is that statically eliminating handlers requires statically determining the continuation of each operation. Bounded loops such as scan can be unrolled, but for unbounded loops no valid jaxpr continuation can be constructed. We are unaware of prior work addressing this challenge. Following §2.2, we handle tail-resumptive handlers in-place (§5.1) and abortive handlers by a source-level rewrite (§5.3), and prove the tail-resumptive translation sound (§5.4).
Handling In-Place
The left of Figure 6 defines tail-resumptive expressions (), jaxprs (), and primitives (): an expression is tail-resumptive if it ends in resume, or if it is a let binding whose primitive is tail-resumptive, or whose primitive contains no resume and whose body is tail-resumptive; a jaxpr is tail-resumptive when its body is, and call and cond are when their jaxprs are.

Figure 6. Selected rules for resumption inlining
From a tail-resumptive expression, the judgment extracts the pre-resume computation, which we inline instead of materializing a continuation. It tracks the handler context , since the expression may perform further tail-resumptive operations, and threads the parameters , which such operations may update. The type annotates generated let-expressions: is the type of , the parameter type of the handler evaluated in-place, and the operation’s
output type. We omit the straightforward rules for jaxprs and primitives. Rule R-Resume returns the current parameters alongside the arguments to resume, i.e. the updated parameters and the operation’s output. Rule R-Let1 recurses into the primitive. Rule R-Let2 handles let bindings where body is tail-resumptive, and primitive does not mention resume. Here, the body can be inlined recursively, and is translated under , allowing for tail-resumptive operations of . Therefore, and are mutually recursive. Notably, may install new handlers whose operations need not be tail-resumptive, as local handlers capture continuations without crossing the loop boundary.
The final continuation. A final subtlety of R-Let2 is that because operations of can update the parameters, the translation of must return the final parameters. To address this, we introduce a new continuation form , the final continuation, which marks the end of the computation and receives both the current parameters and the result (as in E-Ret4). Accordingly, R-Let2 translates the binding under the final continuation , so that the updated parameters are returned, bound to , alongside the result , and then translates using as the new parameters.
Handling in-place. Rule E-Hop2 in Figure 7 handles an operation in-place. It looks up the clause , with , and the parameters in the handler , without inspecting the continuations, and forms a call whose body is the inlined resumption of , computed under the handlers preceding , the parameters , and their types. The call threads the updated parameters and returns the output ; the body is then translated under the original context with the updated parameters.

Figure 7. Handling in-place
Notably, this extended translation relaxes the invariant that the number of return clauses in always matches the number of handlers in (e.g. in E-Ret4). This is desired: when translating do, if the corresponding handler has no return clause in , rule E-Hop cannot apply, leaving rule E-Hop2 to handle only tail-resumptive operations.
Translation of while. In rule E-While, the loop’s return type is extended with the parameters; the translated loop takes the current parameters and yields the updated ones . The condition $j_1 ignores the parameters and translates under empty contexts11; the body translates with the input parameters, using the final continuation to return the updated ones.
An Example
Resumption inlining must preserve handler scoping, which is subtle when a handler’s clause itself performs operations. Consider the following program, where the outermost and innermost handlers handle with clauses and , the middle one handles , and its clause performs :
When the innermost body performs , naively taking the pre-resume part of the clause, i.e. the computation before resume, would yield the translation
This changes the program’s semantics: is now handled by the innermost handler (), although it was originally performed within the scope of the outermost handler (). To prevent this, Xie et al. [52], Xie and Leijen [54] either restrict resumptions to be applied in the very scope of their original handler context, or add a runtime construct that dynamically adapts handler contexts.
Our system needs neither, as inlining naturally preserves handler scopes: because inlining recursively processes let-bindings, it is performed under the handler context that preceded the one being inlined, here (§ 5.1). Instead of the naive pre-resume part, the inlining emits a call to the following translation, so that is correctly handled by :
Abortive Handlers
Handling in-place covers tail-resumptive handlers. Abortive handlers never resume, so there is no resumption to inline; instead, we support them by a source-level rewrite of the loop that leaves the translation untouched. Concretely, we support operations (for ) with signatures whose abortive handlers reside outside the loop by the following rewrite rule; for space reasons, we give the full definitions of the rewritten components in Appendix B.§ 2.
The idea is that the loop records an abortive operation and exits, and the operation is re-performed after the loop. The loop state carries an integer flag (initially 0) identifying the recorded operation, if any, together with its arguments (initially zeros). The body wraps the original body in a handler for all that discards the resumption and returns the loop state, , and the operation arguments; the condition additionally checks ; and after the loop, a cond on (where branches abbreviates nested conditions on an integer) returns the loop results if and otherwise re-performs with the stored arguments. Both this rewrite and the one for effectful conditions (Appendix B.§ 1) are formally proven to preserve typing and semantics; the proofs proceed by a simulation between the source loop and the rewritten one.
Elaboration Soundness and Semantics Preservation
To show that the new translation rules preserve elaboration soundness, we first establish the soundness of resumption inlining:
Lemma 5.1 ( soundness). Given and , and , if , then .
We then extend our well-formedness relation with a fourth case for the final continuation : The final continuation may only occur as the leftmost element of : it is the last continuation to run, as it ends the computation:
That is, receives the current parameters alongside the result of type , and produces the final result of type . Using this extension and Lemma 5.1, elaboration soundness (Theorem 4.3) extends to the new translation rules.
Next, we turn to semantics preservation. The simulation diagram in § 4.2 says the following: if , then the translated program should take one or more steps, . For CPS transformations, however, this diagram notoriously often doesn’t commute. In particular, administrative redexes introduced by CPS change the syntactic shape of the target program, so target reductions may not correspond to source reductions. We avoided this issue in § 4.2 because there the translation generates no administrative redexes, and when it changes the shape of a program by pushing a continuation inward, the K-family lemmas (Lemma 4.2) restore syntactic equality. Resume inlining, however, is different: rather than following the source semantics of handlers, which resumes a captured continuation, it executes the handler’s body in place. We therefore relate the inlined program to the CPS translation, which is already proven to match the source semantics. Because the two differ by administrative redexes, the simulation diagram of § 4.2 no longer holds. Plotkin [39] handles this issue with a colon translation, which contracts administrative redexes at the head of a term as it translates. More recent work [35] instead uses a logical relation to show that, although the syntactic shape of the translated program changes, the programs remain semantically equivalent. Our simulation proof combines both: a logical relation captures semantic equivalence, and, in the spirit of Plotkin’s colon translation, our simulation relation accounts for the administrative redexes that separate the two programs.
First, we adapt this logical relation to our setting:
The expression relation says that two expressions are related when every final value produced by the first is the same value produced by the second. A similar relation holds for jaxprs.
Lemma 5.2. (Reflexivity) . (Transitivity) If and , then .
Lemma 5.3. (Compatibility of Bind) If , then . (Compatibility of Let) If for all , then .
Simulation between translations. Since the tail-resumptive translation differs syntactically from the core translation, we start by relating the two by a simulation (Figure 8) between a program on the core side and a program on the tail-resumptive side, modulo a pending continuation .
!Simulation relation ; Sim-Resume: ; Sim-Admin: ; Sim-Inline: , ; Sim-Prefix: , ; Sim-Compose: , , , bbox=0.09,0.11,0.91,0.26
Figure 8. Simulation between the core translation (left) and the tail-resumptive translation (right)
The pending continuation is what the core side still runs once the tail-resumptive side returns, so Sim-Resume relates to . The two sides may be unfolded to different degrees: Sim-Admin lets the core side wrap a continuation in an administrative call, and Sim-Inline lets the tail-resumptive side keep a call in place of its inlined body. Sim-Prefix is congruence under a common prefix, and Sim-Compose handles sequencing: when the tail-resumptive side continues with , the core side instead runs its continuation , so the prefixes are related with pending. Like Plotkin’s colon translation, this relation bridges the syntactic gap that administrative redexes create. Plotkin’s translation removes the redexes at the head of a term; our relation keeps them and describes how the terms differ.
The relation is preserved by reduction:
Lemma 5.4 (Step matching). If , and , then there exists such that and .
When the tail-resumptive side returns, the core side runs to the pending continuation applied to the same atoms: if , then . At the identity continuation this is itself. Therefore:
Lemma 5.5 (Adequacy of ). If , then .
Semantics Preservation. Because resumption inlining is an instance of the simulation at the identity continuation, by Lemma 5.5 we obtain:
Lemma 5.6 (Resumption inlining preserves semantics). If , with , and , and , and , then
Using Lemma 5.6, we update the simulation step lemma (Lemma 4.4) with the logical relation: each source step is matched up to .
Theorem 5.7 (Simulation step). If , then .
Since is transitive (Lemma 5.2), this extends from single steps to whole runs: a closed program and its translation return the same values.
Theorem 5.8 (Semantics preservation). If and , then .
Implementation
We implement effex as a JAX library, leveraging our effect-handling extension to jaxprs.
Programming with effecax. JAX offers a NumPy-like interface [19] together with composable transformations. effecax extends this interface with effects and handlers, as Figure 9a illustrates. Effects are declared with their type signatures (via abstract_eval) and invoked like ordinary functions, as get and set in double_mul. A handler is installed with handle, which takes the handled computation, an initial parameter, a default return clause, and the operation cases. Each case receives the handler parameter, the operation arguments, and the continuation k, which resumes the computation with the updated parameter and the operation’s result.

Figure 9. An effecax program (a) and the jaxpr it traces to (b).
Programs are traced into our extended jaxprs by JAX’s tracing infrastructure [14]. For example, tracing double_mul_handled yields the jaxpr in Figure 9b, which contains the effect primitives delimited_handler, resume, and effect. Like all jaxpr primitives, they are statically typed; each carries a PyTree field describing the structure of its shape-dependent arguments.
Compiling effect handlers. We implement the translations described in §4 and §5 that compile our extended jaxprs to standard JAX jaxprs. Our implementation implements -conversion and capture-avoiding substitution to systematically rename variables and ensure freshness. In-place handling also serves as an automated optimization for non-iterative code. For instance, because the handlers in Figure 9b are tail-resumptive, they are inlined during the translation to produce a more compact program shown in Figure 10.

Figure 10. Compiled jaxpr
Case Studies
We explore the usage of effecax across two real-world domains in the JAX ecosystem: numerical differential equation solving (§7.1) and probabilistic programming (§7.2). We select these domains because they represent common JAX use cases, both benefit from incorporating effects, and both feature mature, established JAX libraries that serve as baselines for comparison. A complete overview of these domains is beyond our scope; we refer interested readers to Kidger [27] and van de Meent et al. [50] for further background. Using these case studies, we evaluate effecax through the following research questions:
RQ1: Can effecax express effect patterns that are awkward to write in today’s JAX?
RQ2: Does effecax compose with JAX’s built-in primitives, such as control flow?
RQ3: Does expressing effect patterns with effecax come at a performance cost?
Throughout this section, all runtimes are measured on an NVIDIA A100 (SXM4, 40 GB) GPU; compilation (Figure 12 and Table 2) is host-side work and runs on the machine’s CPU, a 12-vCPU Intel Xeon (Cascade Lake, 2.20 GHz) host with 85 GB RAM.

Figure 12. Runtime of the effecax refactor vs. Diffrax (lower is better). Left: forward solve (top) and forward solve with gradient (bottom) on three workload benchmarks. Right: Lotka–Volterra work–precision (top) and cold-start compilation time on Diffrax’s neural-ODE compile-time benchmark (bottom).
| EX (s) | OX (s) | NP (s) | |
| Proportion | 1.37 (0.05 + 1.32) | 1.82 (0.12 + 1.69) | 1.34 (0.04 + 1.30) |
| Stoch. volatility | 0.48 (0.09 + 0.39) | 0.90 (0.19 + 0.70) | 0.59 (0.10 + 0.48) |
| Bayesian NN | 0.48 (0.13 + 0.36) | 0.78 (0.19 + 0.59) | 0.46 (0.09 + 0.37) |
| Semi-sup. HMM | 1.44 (0.20 + 1.25) | 1.45 (0.15 + 1.30) | 1.98 (0.72 + 1.27) |
| UCBadmit GLMM | 0.88 (0.15 + 0.73) | 0.87 (0.07 + 0.80) | 1.37 (0.25 + 1.12) |
| HSGP | 1.64 (0.31 + 1.33) | 1.80 (0.26 + 1.54) | 1.81 (0.31 + 1.49) |
Table 2. Compile-time overhead. Rows and abbreviations as in Table 1.
Optimizers and Solvers
Given a vector field and an initial value , a solver numerically approximates the trajectory where . The core is an integration loop: starting from , it repeatedly calls a one-step method to advance the solution over time. In real-world solvers, the loop is a complex imperative program. In JAX, Diffrax12 implements this loop using ad-hoc mechanisms described in § 2.1. In particular, Diffrax’s integration core13 carries a single record of 20 variables serving orthogonal purposes. The structure of this core loop is sketched in Figure 11a.

Figure 11. The core integration loop of Diffrax versus the same loop refactored with effecax.
We contribute a refactor of Diffrax’s core integration module using effecax.
Expressiveness. With effecax, each of these concerns becomes an independent handler and the loop body is written in direct style. The 20-field record is replaced by one state handler per concern; taking a step and adapting the step size are effects (try_step, adapt) interpreted by handlers; and early termination is an exception (fail) rather than a flag threaded through the state and re-checked in the loop condition. The resulting program is written modularly in direct-style and results in a shrinkage in code size (measured by lines of code). The same loop written in effecax is shown in Figure 11b. With effecax, parameterized handlers allow us to update the state in-place, without having to worry about threading it correctly. Moreover, the loop doesn’t take in parameters itself, and the loop’s conditional doesn’t directly check the results flag, rather a single fail() operation immediately aborts the loop. With effecax, the logic can be seen directly without any obfuscation behind state threading (RQ1). Note also that the effectful loop body sits inside the standard while_loop and cond (RQ2).
Performance. We compare the performance of our refactor against Diffrax. From Diffrax’s default benchmark suite, we select all programs that exercise the integration loop. Each sweeps one parameter, plotted on the x-axis:
(a) a reaction-diffusion PDE on an grid, sweeping the grid size (increasing grid size, hence per-step work);
(b) a small neural ODE, whose vector field is a small MLP (batch size 1024, i.e. each solver step runs the MLP on 1024 states), sweeping the integration length: a longer integration takes more solver steps, hence more work per step;
(c) a jump-step SDE (batch size 1024, i.e. each solver step advances 1024 independent SDE paths), likewise sweeping the integration length;
(d) the classic Lotka-Volterra system (a tiny two-variable ODE) as a work-precision study [44] (increasing the error tolerance and measuring how long the solve takes).
Figure 12 demonstrates the effecax refactor is competitive on the three substantial workload benchmarks (a)-(c) (RQ3). The right panel of Figure 12 shows the work-precision plot of the Lotka-Volterra benchmark (d). As the error grows (-axis), the runtime falls (-axis) since a looser tolerance does less work. The two implementations roughly match across the entire sweep. Lotka-Volterra is a two-variable system whose steps do minimal numerical work, so the runtime is dominated by the integration-loop machinery itself which is precisely where a per-step overhead of the effect-handler compilation would show up if there were one (RQ3).
Finally, we compare compilation time (Figure 12, bottom right) from Diffrax’s compile-time benchmark. The benchmark deliberately stresses the compiler: it is written in such a way that produces large jaxprs. For Diffrax, this is the cold-start cost of the first call: tracing its (larger) integrator program to a jaxpr and letting XLA compile it. For effecax, it additionally includes an ahead-of-time stage that compiles the handlers away (tracing occurs before this step, XLA compilation after). Despite this extra stage, the effecax refactor compiles faster on the forward case and matches Diffrax on the forward with gradient case: the effect handler compilation pass is cheap and Diffrax spends longer tracing since it has a larger program.
Probabilistic Programming
Probabilistic models describe a joint distribution over a space of latent variables and observations . Bayesian inference computes the posterior . The marginal likelihood quickly becomes intractable in higher dimensions of latent variables, so inference algorithms approximate the posterior instead. Probabilistic programming languages (PPLs) [50] express models with sample (drawing from a distribution) and observe (conditioning on data) operations, and run inference to learn model parameters from observed data.
We contribute a new PPL library in effecax via inference patterns [32, 33]: an inference pattern provides inference operations, whose semantics is provided by handlers, thus achieving modularity and extensibility.
Expressiveness over NumPyro and Oryx. We compare with two representative JAX-based PPL systems: NumPyro [37] and Oryx [51]. Both implement effects based on ad-hoc mechanisms similar to those described in § 2.1, which impose certain restrictions. NumPyro implements effects using a global state mechanism (“messengers”)14 outside of JAX, so effects break within JAX control flow. NumPyro therefore introduces and requires users to use its specialized versions of cond and scan, and does not support while. Oryx implements effects using a dedicated JAX transformation for each effect. In particular, its harvest transformation15 is limited to state-like effects and currently does not support control flow primitives.
Figure 13 shows a Hidden Markov Model (HMM), whose hidden states evolve by a Markov process and are naturally expressed as a loop, in NumPyro and in effecax (full definitions in the artifact). NumPyro must use its specialized scan; effecax uses the standard JAX scan (RQ2).


Figure 13. Hidden Markov models in NumPyro (left) and effecax (right)
These restrictions can also cause a loss of expressiveness in NumPyro and Oryx. Figure 14 shows two practical models. (1) A Hierarchical HMM (left) is naturally written with nested scans. However, NumPyro rejects nested scan with an error, and Oryx’s harvest is incompatible with JAX control flow entirely, rejecting any latent variable inside a lax.scan. (2) In a Regime-Switching SSM (right), NumPyro’s cond requires all sample sites to share one distribution class and rejects the model at trace time, while Oryx silently drops the latent variable inside lax.cond from the joint density, giving wrong results without warning. effecax expresses both models directly and correctly (RQ1, RQ2). Users can often restructure such models around these restrictions, but doing so sacrifices compositionality and hand-encodes the error-prone interaction between control flow and effects.


Figure 14. Two effecax models that sample inside control flow: a nested effectful scan (left) and an effectful cond (right), neither of which NumPyro nor Oryx can express as-is.
Performance. We compare effecax against NumPyro and Oryx across both runtime cost and compile-time cost using Hamiltonian Monte Carlo (HMC) inference. HMC is an ideal benchmark because its core loop repeatedly exercises the target log-density gradient, which is exactly the computation generated by each framework’s side-effect abstraction, and because it is the standard default sampler in NumPyro. We measure compile time for the log-density function, and report the total runtime spent running the log-density program over a full inference run, across 100 000 evaluations. We run these benchmarks over a wide range of real-world probabilistic models using varying large dataset sizes16: (a) proportion test logistic regression, (b) stochastic-volatility state space, (c) Bayesian neural network, (d) scan-marginalized semi-supervised HMM, (e) hierarchical GLMM based on UCBadmit dataset, (f) Hilbert-space GP approximation.
Table 1 reports each framework’s total time over that run, alongside a hand-written baseline; in both tables, the shaded cell marks the fastest entry per row. The result shows that recovering the log-density computation via effect handlers, harvest, or messengers adds minimal runtime overhead, matching the baseline within a few percent in most cases (RQ3). Table 2 shows the compilation time. It measures the cost of running each library’s side-effect elimination strategy to produce the log-density computation and then XLA-compiling it. Entries are formatted as () seconds, separating the framework’s effect-elimination transform time () and XLA compilation time () from their total (). On half of the models, effecax compiles fastest of the three frameworks, and is within a few percent of the leader on the remaining ones (RQ3).
| Model | Size | HW (s) | EX (s) | OX (s) | NP (s) |
| Proportion | 3M | 10.25 | 17.78 | 11.56 | 17.73 |
| Stoch. volatility | 2.5k | 1.75 | 1.36 | 2.72 | 1.99 |
| Bayesian NN | 50k | 8.97 | 11.52 | 9.11 | 8.99 |
| Semi-sup. HMM | 5k | 9365 | 9526 | 9423 | 9434 |
| UCBadmit GLMM | 49k | 6.66 | 6.67 | 6.65 | 7.29 |
| HSGP | 7.3k | 5.24 | 4.20 | 5.23 | 4.93 |
Table 1. Runtime overhead. HW = hand-written, EX = effecax, OX = Oryx, NP = NumPyro.
Discussion and Related Work
Compiling functional languages to accelerators. GPUs offer a restricted programming model and lack support for function pointers and dynamic recursion. As a result, compiling higher-order functions typically requires defunctionalization [45] to produce first-order functions, a transformation that replaces each application with a call to an apply function that branches across all possible function forms. However, as Hovgaard et al. [24] observe, naive defunctionalization introduces the issue of branch divergence on GPUs, where extensive branching leaves many threads idle while a subset executes instructions, leading to suboptimal performance. To address this, Hovgaard et al. [24] introduced a specialized defunctionalization transformation for the Futhark language [20, 21]. They restrict the use of functions, specifically disallowing loops and conditionals from returning functions and forbidding arrays from containing functions, a design that brings the language remarkably close to jaxprs. Under these constraints, Futhark’s defunctionalization transformation specializes each application to the particular form of function that may occur at runtime. As Hovgaard et al. [24] note, “the result is essentially equivalent to inlining completely the apply function in a program produced by Reynolds defunctionalisation.” Our translation plays the same role for effect handlers: we conjecture that it yields the program one would obtain by CPS-transforming the handlers, defunctionalizing the resulting higher-order functions, and fully inlining the apply function. It does so directly, in a single pass over the first-order program, without materializing the higher-order intermediate or inducing branch divergence.
The array language Dex [36] similarly uses a simplification pass that leaves only first-order, top-level functions that are fully applied at their call sites, provided that the overall program is of a non-functional type. Both Futhark and Dex normalize higher-order programs, whereas our transformation operates on first-order jaxprs and normalizes only effect handlers. Dex supports Accum, an effect for associative accumulation in parallel computations, and later generalizes this concept to parallel effect handlers [53], though this feature remains yet unimplemented. Each parallel handler must provide a traverse clause that defines the semantics of operations executed in parallel. Nevertheless, this strategy applies only to specific classes of effects, such as accumulation. Also, their parallel exception handler must finish parallel execution before checking for exceptions.
Effect handlers. Existing work on effect handler compilation [23, 47, 54] commonly employs a higher-order CPS transformation or targets pure lambda calculi. In contrast, our work targets a restricted, first-order language without first-class functions or recursion. While prior work has addressed the mechanization of CPS compilation (e.g. [35]), to our knowledge, no prior work has mechanized a semantics-preserving CPS transformation for effect handlers. Schuster et al. [46] explicitly note that they did not mechanize their CPS semantics-preserving result as their “mechanized translation introduces administrative reductions”. Prior work [5, 31, 32, 33, 37] has explored specialized effect handlers for PPL, but these implementations are largely specialized to this specific domain, while effecax provides a natural interface for integrating with JAX.
Limitations. We currently restrict handlers across loop boundaries to be tail-resumptive or abortive, so a clause that e.g. resumes multiple times is rejected. Capturing continuations in a loop requires materializing continuations as data and turning the loop body into a state machine, following Ikebuchi et al. [25] for coroutines, though extending this approach to multi-shot continuations remains non-trivial. However, dispatching over states in every iteration would reintroduce the branch divergence our translation avoids. A further obstacle is space. Non-tail resumptive handlers require space proportional to the number of times a loop runs. Unfortunately, this space requirement cannot be determined at compile time and XLA requires static bounds on memory usage for jaxprs. A similar restriction arises in reverse-mode automatic differentiation of while loops, which requires storing intermediate states of statically unknown memory bounds.18 We leave addressing this limitation to future work.
Beyond JAX. Our work is motivated by the practical constraints of JAX, in the same way that effect handler implementations for e.g. OCaml, WebAssembly, and C/C++ are shaped by the constraints of their targets. The core ideas, however, apply to any intermediate representation that operates under similar hardware-imposed restrictions. In addition to Futhark and Dex mentioned above, PyTorch has a similar story: its functorch component requires functional code, which PyTorch obtains through a custom functionalization pass19 that removes state mutation, much like JAX’s Ref transformation. This pass has had a number of bugs since its introduction,20 and we believe effect handlers offer a more principled treatment of side effects in this setting. Lastly, by establishing a framework for effect handlers in JAX, we hope to open new avenues for applying existing reasoning and verification techniques of effect handler programs [4, 11, 12, 49] to this domain.
References
Example Programs
This appendix walks through two small programs, one using a handler for exceptions and one for state, showing for each how it evaluates under the operational semantics of § 3 and how it is translated by the rules of § 4. Throughout, denotes the handler’s return clause and its operation clauses.
Exceptions
Given an exception operation , safe_div takes two integers and divides them if the divisor is non-zero, and throws otherwise:
safe_div = { λm : int, n : int. let b : bool = eq n 0 in
let r : int = cond [then: { λx : int, y : int. let z : int = do throw ⋆ in return z },
else: { λx : int, y : int. let z : int = div x y in return z }] b m n
in return r }
We apply it to 42 and 0 under a handler whose throw clause resumes with the fallback value 0; the handler takes no parameter, and its return clause drops the parameter:
let v : int = handle [ args: ⋆,
body: let w : int = call [body: safe_div] 42 0 in return w,
ret: j_r = { λx : int. x in return y },
impl: throw ↦ j = { λx : ⋆, y : ⋆. let u : int = resume ⋆ 0 in return u }] ⋆
in return v
Evaluation. Evaluation proceeds inside the handler body (S-HCONG). The call is inlined with and (S-CALL), eq evaluates to 1 (S-FUNC), and the then branch is selected and inlined (S-COND), so that, up to trivial returns, the body becomes let z : int = do throw ⋆ in return z. The operation is now performed directly under a handler that handles it, so S-HDO2 applies: it runs the clause with the handler parameter and the operation argument, replacing resume by a call to the captured continuation , which reinstalls the handler around the rest of the body:
Inlining (S-Call) yields let : int = call [ body: ] 0 in return , and inlining reinstalls the handler with . Its body has returned, so S-HRet applies the return clause, and S-Call finishes:
The program’s result is 0.
Translation. We translate the program under empty lists. Rule E-Handle1 translates the handler body under the handler, with , continuation list , and parameter , and binds the result to for the translation of return under empty lists, which is return $v itself (E-Ret1). The interesting part is therefore
By E-Call2, we capture the continuation and translate the jaxpr safe_div under , producing let : int = call [ body: ] 42 0 in return . Rule E-JaxpR recurses into the body of safe_div; E-Func keeps the primitive eq; and E-Cond2 captures and translates both branches under . The else branch performs no operation: E-Func keeps div, and return is translated by popping and (E-Ret2, twice) and then (E-Ret3), each application reducing to return , so the branch is unchanged. The then branch is where the handler is compiled away:
By E-Hop, we look up the clause for throw in , replace resume in it by a call whose body delays the translation of the rest of the program, and translate the clause applied to the handler parameter and the operation argument under the lists preceding , which are empty:
The steps are E-Call1 (the lists are empty, so the call is kept and its body translated), E-HResume (the delayed translation resumes under the captured lists), and finally the return rules (E-Ret2 twice and E-Ret3), which unroll , , and to return . The operation has been eliminated: the call to simply yields the fallback value. The complete translation is
which is a vanilla jaxpr evaluating to 0, as the source program does.
State
Consider state operations and . The handler below carries the state as its parameter, initially 1. Its get clause resumes with the current state both as the new parameter and as the operation result; its set clause resumes with the operation argument as the new parameter and as the result; and its return clause returns the final state alongside the body’s result. The body reads the state, doubles it, stores it back, and returns 42:
Evaluation. The body immediately performs get, so S-Hdo2 runs the clause with the parameter 1 and the argument , with replaced by a call to the continuation
where is the rest of the body. Inlining and then (S-Call, twice) reinstalls the handler with parameter 1 and . Inside it, add evaluates to 2 (S-Func, under S-Hcong), and the body performs set 2. Again S-Hdo2 runs with parameter 1 and argument 2; its reinstalls the handler with parameter 2 around the remaining body . That body has returned, so S-HRet applies to 2 and 42, and the program evaluates to 2, 42: the final state and the body’s result.
Translation. As before, E-Handle1 translates the body under , the continuation list , and the parameter 1, and binds the result to . The body starts with get, so E-Hop applies: in is replaced by , and applied to 1 and is translated under empty lists. By E-Call1 the call is kept and its body translated, and by E-HResume that body becomes : the rest of the body, now translated with the updated parameter in place of 1. Rule E-Func keeps add, and the set operation is handled by E-Hop in the same way, with and the clause applied to and . Its delayed body is resolved by E-Ret3: the return clause is applied to the current parameter and the result, giving . Assembling the pieces, the complete translation is
Both handlers and both operations are gone. The state is threaded explicitly: the outer call receives the initial state 1 twice, as the parameter and as the result of get; the inner call receives the doubled value 2 as the new parameter, which the inlined return clause pairs with 42. The jaxpr evaluates to 2, 42, as the source program does.
B Source-Level Rewrites of While Loops
Effectful Conditions
The translation of while in Figure 7 forbids operations of outer handlers in the loop condition. This restriction can be lifted by moving the condition into the loop body, with an additional boolean carry , through the source-level rewrite
with the rewritten condition and body defined as follows:
The new condition returns the boolean carry . The new body evaluates the original condition on the state and branches on the result: if true, it runs the original body and returns together with the new state; otherwise it returns with the current state, which makes terminate the loop at the next iteration.
Abortive Handlers
Abortive handlers never resume, so an operation performed inside a loop and handled abortively outside it needs no continuation: the loop can simply stop and the operation be re-performed after it. § 5.3 supports this by a source-level rewrite, applied before translation, for operations (for ) with signatures whose abortive handlers reside outside the loop:
The loop state is extended with an integer flag , initially 0, that records which operation aborted the loop, if any, together with slots for its arguments, initially zeros. After the loop, a cond on the flag either returns the loop results or re-performs the recorded operation with the stored arguments, where branches abbreviates nested conditions on an integer. The rewritten components are defined as follows:
The extended state type adds to the original loop state an integer flag and the argument types of every operation. The condition evaluates the original condition on the original state and conjoins it with the check . The body runs the original body under a handler with a unit parameter. Its return clause passes the updated state through with and zeroed argument slots. Its clause for discards the resumption and returns the current state , the identifier , and the operation arguments placed in the -th slot, with all other slots zeroed. Finally, the branch jaxprs are selected on the flag after the loop: returns the loop results, while (for ) re-performs with the stored arguments .
Elaboration Soundness and Semantics Preservation
We write for the effectful conditions rewrite of §B.1 and for the abortive handlers rewrite of §B.2. We prove that both rewrites preserve typing.
Theorem B.1 (Effectful Conditions Elaboration Soundness). If and , then .
Theorem B.2 (Abortive Handlers Elaboration Soundness). If and , then .
The rewrites also preserve semantics: whenever the original loop returns, the rewritten loop returns the same atoms. For the rewrite in §B.1, we first start developing a syntactic relation (on the source program) that captures how the rewrite changes the syntax of the program (). We then show that this relation is closed under step:
Lemma B.3 (Effectful Conditions Step Simulation). If , , and , then for some with .
Using Lemma B.3, we then show semantics preservation:
Theorem B.4 (Effectful Conditions Semantics Preservation). If , , and , then .
For the semantics preservation of §B.2, we also develop a syntactic relation on the source program that shows how the rewrite changes the program shape (). We then prove step simulation:
Lemma B.5 (Abortive Handlers Step Simulation). If and , then for some with .
Using Lemma B.5, we can prove semantics preservation. Since the abortive rewrite is a peephole rewrite, thus such a contextual program could end in an unhandled operation. This operation could be an abortive do or not. The theorem therefore says that if they the step results in a do then the continuation of the do should also be in the relation, if it is not abortive. If it is abortive, it doesn’t matter because the continuation is aborted.
Theorem B.6 (Abortive Handlers Semantics Preservation). If and , then
(Returns) if , then ;
(Operations) if , then ; if , then .
The abortive rewrite applies to a loop nested anywhere in a program, for instance inside the body of another loop or of a handler. We therefore state its semantics preservation for the abortive rewrite congruence of Figure 15, which applies at any number of positions in . The judgment is parameterized by the set of abortive operations, whose handlers never resume, and rule Rw-Loop only rewrites loops whose are in . Rule Rw-Loop states the rewrite of a loop without its continuation: the rewritten loop ends in the dispatch on the flag, which is then composed with the original continuation . The remaining rules are congruences. Rule Rw-Call rewrites a called jaxpr; the rules for the jaxprs of cond, while and scan are similar. rule Rw-Handle rewrites the body of an abortive handler , i.e. one whose clauses never resume. rule Rw-Handle also has other variant which can rewrite the return and operation clauses of such handlers.

Figure 15. Abortive rewrite congruence (selected rules).
Using the lemmas Lemma B.5 and theorem B.6, we then prove semantics preservation for the contextual equivalence:
Theorem B.7 (Abortive Rewrite Congruence Semantics Preservation). If , , and , then .
Mechanization
Table 3 lists the Lean counterpart of each result in the paper.
| Result | Paper Name | Lean Name | File | Line |
| Lemma 3.1 | Determinacy | determinacy | MiniJaxprH | 2976 |
| Theorem 3.2 | Preservation | preservation | MiniJaxprH | 8419 |
| Theorem 3.2 | Progress with effects | progress | MiniJaxprH | 8130 |
| Definition | translateExpr | Translation | 213 | |
| Definition | translateJaxpr | Translation | 456 | |
| Lemma 4.1 | Pure programs | pure_programs | Kfamily | 13 |
| Lemma 4.2 | K-push | K_push | Kfamily | 3418 |
| Lemma 4.2 | K-compose | K_compose | Kfamily | 3465 |
| Lemma 4.2 | K-delay | K_delay | Kfamily | 3518 |
| Theorem 4.3 | Elaboration soundness | elaboration_soundness | ElaborationSoundness | 1070 |
| Lemma 4.4 | Simulation step | simulation_step | SemanticsPreservation | 3200 |
| Theorem 4.5 | Semantics preservation | semantics_preservation | SemanticsPreservation | 4711 |
| Definition | RExpr | Translation | 463 | |
| Lemma 5.1 | soundness | resumption_inlining_typed | ElaborationSoundness | 1365 |
| Definition | Logical relation | Refines | Simulation | 121 |
| Lemma 5.2 | Reflexivity | Refines_refl | Simulation | 124 |
| Lemma 5.2 | Transitivity | Refines_trans | Simulation | 127 |
| Lemma 5.3 | Compatibility (Bind) | Refines_bind | Simulation | 305 |
| Lemma 5.3 | Compatibility (Let) | Refines_let | Simulation | 2455 |
| Definition | Simulation relation | Rsim1 | Simulation | 409 |
| Definition | Simulation relation (top level) | Rsim0 | Simulation | 456 |
| Lemma 5.4 | Step matching | sim_step | Simulation | 1595 |
| Lemma 5.5 | Adequacy of | simulation_preserves_semantics | Simulation | 2372 |
| Lemma 5.6 | Resumption inlining preserves semantics | resumption_inlining_preserves_semantics | SemanticsPreservation | 1951 |
| Theorem 5.7 | Simulation step | simulation_step | SemanticsPreservation | 3200 |
| Theorem 5.8 | Semantics preservation | semantics_preservation | SemanticsPreservation | 4711 |
| Theorem B.1 | Effectful conditions: typing | translateWhile_preserves_typing | EffectfulConditional | 436 |
| Definition | WSim | EffectfulConditional | 784 | |
| Lemma B.3 | Effectful conditions: step | WSim_step | EffectfulConditional | 1242 |
| Theorem B.4 | Effectful conditions: semantics | translateWhile_semantics_preservation | EffectfulConditional | 1756 |
| Theorem B.2 | Abortive handlers: typing | translateWhile_preserves_typing | Abortive | 1619 |
| Definition | ASimCong | Abortive | 5998 | |
| Lemma B.5 | Abortive handlers: step | ASimCong_step | Abortive | 6904 |
| Theorem B.6 | Abortive handlers: semantics | translateWhile_preserves_terminals | Abortive | 5858 |
| Definition | \❀^{*} | RewriteExprAnywhere | Abortive | 508 |
| Theorem B.7 | Rewrite congruence: semantics | RewriteExprAnywhere_star | Abortive | 7522 |
Table 3. Paper results and their Lean counterparts. Files are JaxEffects/File.lean.
References
- [1]Mario Alvarez-Picallo, Teodoro Freund, Dan R. Ghica, and Sam Lindley. 2022. Effect Handlers for C via Coroutines. Proc. ACM Program. Lang. 8, OOPSLA2, Article 358 (Oct. 2022), 31 pages. doi:10.1145/3689798
- [2]Anthropic. 2026. Anthropic’s Transparency Hub. https://www.anthropic.com/transparency.
- [3]Andrej Bauer and Matija Pretnar. 2015. Programming with algebraic effects and handlers. Journal of logical and algebraic methods in programming 84, 1 (2015), 108–123.DOI
- [4]Dariusz Biernacki, Maciej Piróg, Piotr Polesiuk, and Filip Sieczkowski. 2017. Handle with care: relational interpretation of algebraic effects and handlers. Proceedings of the ACM on Programming Languages 2, POPL (2017), 1–30.
- [5]Eli Bingham, Jonathan P. Chen, Martin Jankowiak, Fritz Obermeyer, Neeraj Pradhan, Theofanis Karaletsos, Rohit Singh, Paul A. Szerlip, Paul Horsfall, and Noah D. Goodman. 2019. Pyro: Deep Universal Probabilistic Programming. J. Mach. Learn. Res. 20 (2019), 28:1–28:6. http://jmlr.org/papers/v20/18-403.htmlarxiv.org/abs/1810.09538
- [6]James Bradbury, Roy Frostig, Peter Hawkins, Matthew James Johnson, Yash Katariya, Chris Leary, Dougal Maclaurin, George Necula, Adam Paszke, Jake VanderPlas, Skye Wanderman-Milne, and Qiao Zhang. 2018. JAX: composable transformations of Python+NumPy programs. http://github.com/jax-ml/jax
- [7]Manuel MT Chakravarty, Gabriele Keller, Sean Lee, Trevor L McDonell, and Vinod Grover. 2011. Accelerating Haskell array codes with multicore GPUs. In Proceedings of the sixth workshop on Declarative aspects of multicore programming. 3–14.DOI
- [8]Aakanksha Chowdhery, Sharan Narang, Jacob Devlin, Maarten Bosma, Gaurav Mishra, Adam Roberts, Paul Barham, Hyung Won Chung, Charles Sutton, Sebastian Gehrmann, Parker Schuh, Kensen Shi, Sasha Tsvyashchenko, Joshua Maynez, Abhishek Rao, Parker Barnes, Yi Tay, Noam Shazeer, Vinodkumar Prabhakaran, Emily Reif, Nan Du, Ben Hutchinson, Reiner Pope, James Bradbury, Jacob Austin, Michael Isard, Guy Gur-Ari, Pengcheng Yin, Toju Duke, Anselm Levskaya, Sanjay Ghemawat, Sunipa Dev, Henryk Michalewski, Xavier Garcia, Vedant Misra, Kevin Robin- son, Liam Fedus, Denny Zhou, Daphne Ippolito, David Luan, Hyeontaek Lim, Barret Zoph, Alexander Spiridonov, Ryan Sepassi, David Dohan, Shivani Agrawal, Mark Omernick, Andrew M. Dai, Thanumalayan Sankaranarayana Pil- lai, Marie Pellat, Aitor Lewkowycz, Erica Moreira, Rewon Child, Oleksandr Polozov, Katherine Lee, Zongwei Zhou, Xuezhi Wang, Brennan Saeta, Mark Diaz, Orhan Firat, Michele Catasta, Jason Wei, Kathy Meier-Hellstern, Douglas Eck, Jeff Dean, Slav Petrov, and Noah Fiedel. 2023. PaLM: Scaling Language Modeling with Pathways. Journal of Machine Learning Research 24, 240 (2023), 1–113. http://jmlr.org/papers/v24/22-1144.htmlarxiv.org/abs/2204.02311
- [9]Olivier Danvy and Andrzej Filinski. 1992. Representing Control: A Study of the CPS Transformation. Mathematical Structures in Computer Science 2, 4 (1992), 361–391. doi:10.1017/S0960129500001535
- [10]Olivier Danvy and Lasse R. Nielsen. 2003. A First-Order One-Pass CPS Transformation. Theoretical Computer Science 308, 1–3 (2003), 239–257. doi:10.1016/S0304-3975(02)00733-8
- [11]Paulo Emílio de Vilhena and François Pottier. 2021. A separation logic for effect handlers. Proc. ACM Program. Lang. 5, POPL, Article 33 (Jan. 2021), 28 pages. doi:10.1145/3434314DOI
- [12]Paulo Emílio de Vilhena, Simcha van Collem, Ines Wright, and Robbert Krebbers. 2026. A Relational Separation Logic for Effect Handlers. Proc. ACM Program. Lang. 10, POPL, Article 34 (Jan. 2026), 29 pages. doi:10.1145/3776676DOI
- [13]Cormac Flanagan, Amr Sabry, Bruce F. Duba, and Matthias Felleisen. 1993. The essence of compiling with continuations. In Proceedings of the ACM SIGPLAN 1993 Conference on Programming Language Design and Implementation (Albuquerque, New Mexico, USA) (PLDI ’93). Association for Computing Machinery, New York, NY, USA, 237–247. doi:10.1145/155090.155113
- [14]Roy Frostig, Matthew James Johnson, and Chris Leary. 2019. Compiling machine learning programs via high-level tracing. In SysML conference 2018.
- [15]Gemini Team. 2024. Gemini: A Family of Highly Capable Multimodal Models. arXiv:2312.11805 [cs.CL] https://arxiv.org/abs/2312.11805
- [16]Dan R. Ghica, Sam Lindley, Marcos Maroñas Bravo, and Maciej Piróg. 2022. High-level effect handlers in C++. Proc. ACM Program. Lang. 6, OOPSLA2, Article 183 (Oct. 2022), 29 pages. doi:10.1145/3563445DOI
- [17]Google. 2017. XLA - TensorFlow, compiled. https://developers.googleblog.com/2017/03/xla-tensorflow-compiled.html
- [18]Google Cloud. 2023. Google Cloud demonstrates the world’s largest distributed training job for large language models across 50000+ TPU v5e chips. https://cloud.google.com/blog/products/compute/the-worlds-largest-distributed-llm-training-job-on-tpu-v5e
- [19]Charles R Harris, K Jarrod Millman, Stéfan J Van Der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J Smith, et al. 2020. Array programming with NumPy. Nature 585, 7825 (2020), 357–362.
- [20]Troels Henriksen. 2017. Design and implementation of the Futhark programming language. Ph. D. Dissertation. University of Copenhagen, Faculty of Science [Department of Computer Science].
- [21]Troels Henriksen, Niels GW Serup, Martin Elsman, Fritz Henglein, and Cosmin E Oancea. 2017. Futhark: purely functional GPU-programming with nested parallelism and in-place array updates. In Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation. 556–571.
- [22]Daniel Hillerström and Sam Lindley. 2016. Liberating effects with rows and handlers. In Proceedings of the 1st International Workshop on Type-Driven Development. 15–27.DOI
- [23]Daniel Hillerström, Sam Lindley, Robert Atkey, and K. C. Sivaramakrishnan. 2017. Continuation Passing Style for Effect Handlers. In 2nd International Conference on Formal Structures for Computation and Deduction, FSCD 2017, September 3–9, 2017, Oxford, UK (LIPIcs, Vol. 84), Dale Miller (Ed.). Schloss Dagstuhl – Leibniz-Zentrum für Informatik, 18:1–18:19. doi:10.4230/LIPIcs.FSCD.2017.18DOI
- [24]Anders Kiel Højgaard, Troels Henriksen, and Martin Elsman. 2018. High-performance defunctionalisation in futhark. In International Symposium on Trends in Functional Programming. Springer, 136–156.
- [25]Mirai Ikebuchi, Andres Erbsen, and Adam Chlipala. 2022. Certifying derivation of state machines from coroutines. Proc. ACM Program. Lang. 6, POPL, Article 24 (Jan. 2022), 31 pages. doi:10.1145/3498685DOI
- [26]Andrew Kennedy. 2007. Compiling with continuations, continued. SIGPLAN Not. 42, 9 (Oct. 2007), 177–190. doi:10.1145/1291220.1291179DOI
- [27]Patrick Kidger. 2021. On Neural Differential Equations. Ph. D. Dissertation. University of Oxford.arxiv.org/abs/2202.02435
- [28]Daan Leijen. 2017. Implementing Algebraic Effects in C. Technical Report MSR-TR-2017-23. https://www.microsoft.com/en-us/research/publication/implementing-algebraic-effects-c/
- [29]Daan Leijen. 2017. Type directed compilation of row-typed algebraic effects. In Proceedings of the 44th ACM SIGPLAN Symposium on Principles of Programming Languages (Paris, France) (POPL ’17). Association for Computing Machinery, New York, NY, USA, 486–499. doi:10.1145/3009837.3009872DOI
- [30]Xavier Leroy. 2009. A Formally Verified Compiler Back-end. J. Autom. Reason. 43, 4 (Dec. 2009), 363–446. doi:10.1007/s10817-009-9155-4DOI
- [31]Dave Moore and Maria I. Gorinova. 2018. Effect Handling for Composable Program Transformations in Edward2. CoRR abs/1811.06150 (2018). arXiv:1811.06150 http://arxiv.org/abs/1811.06150
- [32]Minh Nguyen, Roly Perera, Meng Wang, and Steven Ramsay. 2023. Effect Handlers for Programmable Inference. In Proceedings of the 16th ACM SIGPLAN International Haskell Symposium (Seattle, WA, USA) (Haskell 2023). Association for Computing Machinery, New York, NY, USA, 44–58. doi:10.1145/3609026.3609729DOI
- [33]Minh Nguyen, Roly Perera, Meng Wang, and Nicolas Wu. 2022. Modular probabilistic models via algebraic effects. Proceedings of the ACM on Programming Languages 6, ICFP (2022), 381–410.DOI
- [34]Lasse R. Nielsen. 2001. A Selective CPS Transformation. In Seventeenth Conference on the Mathematical Foundations of Programming Semantics, MFPS 2001, Aarhus, Denmark, May 23-26, 2001 (Electronic Notes in Theoretical Computer Science, Vol. 45), Stephen D. Brookes and Michael W. Mislove (Eds.). Elsevier, 311–331. doi:10.1016/S1571-0661(04)80969-1DOI
- [35]Zoe Paraskovopoulou and Anvay Grover. 2021. Compiling with continuations, correctly. Proc. ACM Program. Lang. 5, OOPSLA, Article 114 (Oct. 2021), 29 pages. doi:10.1145/3485491
- [36]Adam Paszke, Daniel D Johnson, David Duvenaud, Dimitrios Vytiniotis, Alexey Radul, Matthew J Johnson, Jonathan Ragan-Kelley, and Dougal Maclaurin. 2021. Getting to the point: index sets and parallelism-preserving autodiff for pointful array programming. Proceedings of the ACM on Programming Languages 5, ICFP (2021), 1–29.arxiv.org/abs/2104.05372
- [37]Du Phan, Neeraj Pradhan, and Martin Jankowiak. 2019. Composable effects for flexible and accelerated probabilistic programming in NumPyro. arXiv preprint arXiv:1912.11554 (2019).
- [38]Luna Phipps-Costin, Andreas Rossberg, Arun Gupta, Daan Leijen, Daniel Hillerström, KC Sivaramakrishnan, Matija Pretnar, and Sam Lindley. 2023. Continuing WebAssembly with Effect Handlers. Proc. ACM Program. Lang. 7, OOPSLA2, Article 238 (Oct. 2023), 26 pages. doi:10.1145/3622814
- [39]G.D. Plotkin. 1975. Call-by-name, call-by-value and the λ-calculus. Theoretical Computer Science 1, 2 (1975), 125–159. doi:10.1016/0304-3975(75)90017-1DOI
- [40]Gordon Plotkin and John Power. 2001. Adequacy for Algebraic Effects. In Proceedings of the 4th International Conference on Foundations of Software Science and Computational Structures (FoSSaCS ’01). Springer-Verlag, Berlin, Heidelberg, 1–24.DOI
- [41]Gordon Plotkin and John Power. 2003. Algebraic Operations and Generic Effects. Appl. Categorical Struct. 11, 1 (2003), 69–94. doi:10.1023/A:1023064908962DOI
- [42]Gordon Plotkin and Matija Pretnar. 2009. Handlers for Algebraic Effects. In Proceedings of the 18th European Symposium on Programming Languages and Systems: Held as Part of the Joint European Conferences on Theory and Practice of Software, ETAPS 2009 (York, UK) (ESOP ’09). Springer-Verlag, Berlin, Heidelberg, 80–94. doi:10.1007/978-3-642-00590-9_7DOI
- [43]Gordon Plotkin and Matija Pretnar. 2013. Handling algebraic effects. Logical methods in computer science 9 (2013).DOI
- [44]C Rackauckas. 2022. ODE solver multi-language wrapper package work-precision benchmarks (MATLAB, SciPy, Julia, deSolve (R)). https://docs.sciml.ai/SciMLBenchmarksOutput/html/MultiLanguage/wrapper_packages.html
- [45]John C Reynolds. 1972. Definitional interpreters for higher-order programming languages. In Proceedings of the ACM annual conference-Volume 2. 717–740.
- [46]Philipp Schuster, Jonathan Immanuel Brachthäuser, Marius Müller, and Klaus Ostermann. 2022. A typed continuation-passing translation for lexical effect handlers. In PLDI ’22: 43rd ACM SIGPLAN International Conference on Programming Language Design and Implementation, San Diego, CA, USA, June 13 - 17, 2022, Ranjit Jhala and Isil Dillig (Eds.). ACM, 566–579. doi:10.1145/3519939.3523710DOI
- [47]Philipp Schuster, Jonathan Immanuel Brachthäuser, and Klaus Ostermann. 2020. Compiling effect handlers in capability-passing style. Proc. ACM Program. Lang. 4, ICFP (2020), 93:1–93:28. doi:10.1145/3408975DOI
- [48]KC Sivaramakrishnan, Stephen Dolan, Leo White, Tom Kelly, Sadiq Jaffer, and Anil Madhavapeddy. 2021. Retrofitting effect handlers onto OCaml. In Proceedings of the 42nd ACM SIGPLAN International Conference on Programming Language Design and Implementation (Virtual, Canada) (PLDI 2021). Association for Computing Machinery, New York, NY, USA, 206–221. doi:10.1145/3453483.3454039DOI
- [49]Yahui Song, Darius Foo, and Wei-Ngan Chin. 2022. Automated temporal verification for algebraic effects. In Asian Symposium on Programming Languages and Systems. Springer, 88–109.
- [50]Jan-Willem van de Meent, Brooks Paige, Hongseok Yang, and Frank Wood. 2018. An Introduction to Probabilistic Programming. arXiv:1809.10756 [stat.ML]. doi:10.48550/arXiv.1809.10756arxiv.org/abs/1809.10756
- [51]Sharad Vikram. 2022. Oryx: a library for probabilistic programming and deep learning built on top of JAX. https://github.com/jax-ml/oryx
- [52]Ningning Xie, Jonathan Immanuel Brachtäuser, Daniel Hillerström, Philipp Schuster, and Daan Leijen. 2020. Effect handlers, evidently. Proceedings of the ACM on Programming Languages 4, ICFP (2020), 1–29.
- [53]Ningning Xie, Daniel D Johnson, Dougal Maclaurin, and Adam Paszke. 2024. Parallel algebraic effect handlers. Proceedings of the ACM on Programming Languages 8, ICFP (2024), 756–788.arxiv.org/abs/2110.07493
- [54]Ningning Xie and Daan Leijen. 2021. Generalized evidence passing for effect handlers: efficient compilation of effect handlers to C. Proceedings of the ACM on Programming Languages 5, ICFP (2021), 1–30.