alphaFeedback

WorksComputer Science

Handling JAX

AI contributions
Not recorded for this paper
View details

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 λJAXPR\lambda_{\mathrm{JAXPR}}, 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 effecax across 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):

safe_div⁡≜{λm,n. let b=eq⁡ n 0 in let r=cond⁡ [then:{λx,y. let z=do throw ⋆ in return z}, else:{λx,y. let z=div⁡ x y in return z}] b m n in return r}\operatorname{safe\_div} \triangleq\{ \lambda m,n.\ \text{let } b = \operatorname{eq}\ n\ 0\ \text{in}\ \text{let } r = \operatorname{cond}\ [\text{then}: \{ \lambda x,y.\ \text{let } z = \text{do throw}\ \star\ \text{in return } z \},\ \text{else}: \{ \lambda x,y.\ \text{let } z = \operatorname{div}\ x\ y\ \text{in return } z \}] \ b\ m\ n\ \text{in return } r \}

We handle it with a handler whose parameter (args) is unused and thus takes the unit value ⋆\star, 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):

handle⁡ [args:⋆, body:let w=call⁡ [body:safe_div⁡] 42 0 in return w, ret:{λx,y. return y}, impl:throw↦{λx,y. let u=resume⁡ ⋆ 0 in return u}] ⋆\operatorname{handle}\ [\text{args}: \star,\ \text{body}: \text{let } w = \operatorname{call}\ [\text{body}: \operatorname{safe\_div}]\ 42\ 0\ \text{in return } w,\ \text{ret}: \{ \lambda x,y.\ \text{return } y \},\ \text{impl}: \text{throw} \mapsto\{ \lambda x,y.\ \text{let } u = \operatorname{resume}\ \star\ 0\ \text{in return } u \}]\ \star

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:

let z1=call [body: {λm,n. let b=eq n 0  inlet r=cond [then: {λx,y. let u=call [body: {λp,z. return z}] ⋆ 0  in return u},else: {λx,y. let z=div x y  in return z}] b m n in return r}] 42 0  in return z1\begin{aligned} \text{let } z_1 &= \text{call }[\text{body: }\{\lambda m,n.\ \text{let } b = \text{eq } n\ 0\ \text{ in}\\ &\qquad\text{let } r = \text{cond }[\text{then: }\{\lambda x,y.\ \text{let } u = \text{call }[\text{body: }\{\lambda p,z.\ \text{return } z\}]\ \star\ 0\ \text{ in return } u\},\\ &\qquad\qquad\text{else: }\{\lambda x,y.\ \text{let } z = \text{div } x\ y\ \text{ in return } z\}]\ b\ m\ n\\ &\qquad\text{ in return } r\}]\ 42\ 0\ \text{ in return } z_1 \end{aligned}

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.

Handling across the loop boundary

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 e⇝e′e \rightsquigarrow e', 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 λJAXPRε\lambda_{\mathrm{JAXPR}}^{\varepsilon}, a calculus formalizing a core subset of jaxprs extended with effect-handling constructs. At its heart, λJAXPRε\lambda_{\mathrm{JAXPR}}^{\varepsilon} 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 λJAXPR\lambda_{\mathrm{JAXPR}}. 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 (xx), constants (cc), basic primitive functions (ff), and effect operations (op\mathit{op}).

Syntax of lambda JAXPR

Figure 2. Syntax of λJAXPR\lambda_{\mathrm{JAXPR}}

Types σ\sigma are product types, written as sequences of array types for typing tensors (i.e. multidimensional arrays). Array types α\alpha have the form d[s]d[s], where dd represents the underlying scalar data type (e.g. float, int), and ss is a shape tuple specifying the number and sizes of the tensor dimensions (e.g. 3×43 \times4). For scalar values, we often omit the empty shape tuple for brevity, e.g. writing bool instead of bool[]\mathrm{bool}[]. We also often write ⋆\star for the unit type (and its unit value).

Expressions ee consist of a sequence of let bindings letb∗=p a∗ in e\mathop{\mathrm{let}} b^{*} = p\ a^{*}\ \mathop{\mathrm{in}}\ e that end with a return statement return a∗\mathop{\mathrm{return}}\ a^{*}. Binders bb are typed variables. Atoms aa are either variables xx or literals ll specifying tensors. Literals ll include constants cc and tensors [l∗][l^{*}]. A jaxpr jj is a lambda expression, written {λb∗.e}\{\lambda b^{*}.e\}.

Primitive operations pp include both basic functions ff and higher-order primitives p^\hat{p} built from jaxprs. These higher-order primitives include function calls (call [ body:j ][\ \mathrm{body}:j\ ]), that call the jaxpr jj when applied within let bindings; conditionals (cond [ then:j1, else:j2 ][\ \mathrm{then}:j_{1},\ \mathrm{else}:j_{2}\ ]), that take a boolean argument and execute the appropriate branch; while loops (while [ cond:j1, body:j2 ][\ \mathrm{cond}:j_{1},\ \mathrm{body}:j_{2}\ ]) with condition jaxpr j1j_{1} and body jaxpr j2j_{2}; and scans (scan [ body:j, carry:a1,…,an ][\ \mathrm{body}:j,\ \mathrm{carry}:a_{1},\ldots,a_{n}\ ]) that provide a functional loop-fold, where jj iterates over input arrays while updating a carry state initialized by a1,…,ana_{1},\ldots,a_{n}.

To support algebraic effects, λJAXPR\lambda_{\mathrm{JAXPR}} introduces primitives for performing and handling effectful computations. First, the primitive do op\mathit{op} performs an effectful operation op\mathit{op}. Second, following Leijen [29], Schuster et al. [47], we represent resumptions using a resume primitive. Finally, the syntax includes a parameterized handler primitive: handle [ args:a∗, body:e, ret:j, impl:op∗↦j∗ ][\ \mathrm{args}:a^{*},\ \mathrm{body}:e,\ \mathrm{ret}:j,\ \mathrm{impl}:\mathit{op}^{*}\mapsto j^{*}\ ]. This consists of four components: the arguments a∗a^{*} provide the initial value for the handler parameters; the body expression ee provides the computation being handled; the return jaxpr jj converts the final handler parameters and the body’s result into the final output; and the implementation mapping op∗↦j∗\mathit{op}^{*}\mapsto j^{*} gives the jaxpr clauses defining the operations.

Throughout this paper, we use an overline to denote sequences; for example, aˉ\bar{a} represents the sequence a1,…,ana_{1},\ldots,a_{n} (n≥0n \ge0), and aˉ:σ\bar{a}:\sigma indicates that each ai:σia_{i}:\sigma_{i} has the corresponding type σi\sigma_{i}.

Typing

Figure 3 presents the typing rules for λJAXPR\lambda_{\mathrm{JAXPR}}. The typing context Γ\Gamma is a finite sequence of variable typings x:αx:\alpha and at most one resumption typing resume: σ→τ\sigma\to\tau. We assume no variable appears twice within a context, and we work up to α\alpha-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 Γ⊢aˉ:σ\Gamma\vdash\bar{a}:\sigma for the component-wise derivation Γ⊢ai:σi\Gamma\vdash a_{i}:\sigma_{i} for all ii (assuming ∣aˉ∣=∣σ∣|\bar{a}|=|\sigma|).

Typing rules for $\lambda^\varepsilon_{\mathrm{JAXPR}}$

Figure 3. Typing rules for λJAXPRε\lambda^\varepsilon_{\mathrm{JAXPR}}

There are four typing judgments; most rules are straightforward. The judgment Γ⊢aˉ:α\Gamma\vdash\bar{a} : \alpha types atoms. In rule T-Const, primitive constants cc have predefined base types dd (e.g. tt, f:boolf : \mathsf{bool} and ⋆:⋆\star: \star). Rule T-Array forms a tensor of shape (n,s)(n,s) from nn tensors of shape ss. The judgment Γ⊢e:σ\Gamma\vdash e : \sigma types expressions. Rule T-Let checks that the arguments aˉ\bar{a} of a primitive p:σ→τp : \sigma\to\tau have type σ\sigma, and extends the context with xˉ:τ\bar{x} : \tau to type the body ee. Rule T-Ret types the returned atoms. The judgment Γ⊢j:σ→τ\Gamma\vdash j : \sigma\to\tau types jaxprs as standard lambda abstractions.

The judgment Γ⊢p:σ→τ\Gamma\vdash p : \sigma\to\tau types primitives. Basic primitives ff (rule T-Basic) are assumed to have given signatures (for example, tensor concatenation (::):d[s],d[ns]→d[(n+1)s](::) : d[s],d[ns] \to d[(n+1)s]). Rule T-Call promotes a jaxpr to a primitive of the same type. Rule T-Cond requires both branches to have the same type σ→τ\sigma\to\tau and yields type (bool,σ)→τ(\mathsf{bool},\sigma) \to\tau. Rule T-While follows the standard typing of functional loops, with condition j1:σ→boolj_1 : \sigma\to\mathsf{bool} and state update j2:σ→σj_2 : \sigma\to\sigma. Rule T-Scan takes an initial carry of type σ\sigma and a body j:(σ,d1[s1])→(σ,d2[s2])j : (\sigma,d_1[s_1]) \to(\sigma,d_2[s_2]), and yields type (d1[n,s1])→(σ,d2[n,s2])(d_1[n,s_1]) \to(\sigma,d_2[n,s_2]).

For effects, each operation is assumed to have a given signature op:in→outop : in \to out. Rule T-Do uses this signature, and requires an argument of type inin to produce a result of type outout. Rule T-Handler types handler expressions: the initial parameters aˉ\bar{a} have type parpar, the handled computation ee has type τ\tau, and the return clause jj maps the final parameters and the result to the overall output type τ′\tau'. Each operation clause jij_i is typed under a context extended with resume : par,outi→τ′par,out_i \to\tau', so that resuming takes updated parameters and the operation result and yields the handler’s output. The handler has type ⋆→τ′\star\to\tau'. Rule T-Resume simply retrieves the type of resume from the context.

Operational Semantics

Figure 4 presents the operational semantics of λJAXPRε\lambda^\varepsilon_{\mathrm{JAXPR}}, 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 {λxˉ:σ.e}aˉ\{\lambda\bar{x} : \sigma.e\}\bar{a} expands to e[aˉ/xˉ]e[\bar{a}/\bar{x}], and j@aˉ1@aˉ2j@\bar{a}_1@\bar{a}_2 abbreviates j@⟨aˉ1,aˉ2⟩j@\langle\bar{a}_1,\bar{a}_2\rangle. A context CC is either a hole □\square or a sequence of let-bindings ending in a hole. Every well-formed expression can be uniquely written as e=C[return aˉ]e=C[\textbf{return }\bar{a}]. The operation e1▹e2e_1 \triangleright e_2

Operational semantics for lambda JaxpR

Figure 4. Operational semantics for λJAXPR\lambda_{\mathrm{JAXPR}}.

reduction{λxˉ:σ.e}aˉ≜e[aˉ/xˉ]contextC::=□∣let b∗=p a∗ in Cbindinge1▹bˉ.e2≜C[e2[aˉ/bˉ]]where e1=C[return aˉ]\begin{aligned} \text{reduction}\qquad&\{\lambda\bar{x}:\sigma.e\}_{\bar{a}} \triangleq e[\bar{a}/\bar{x}]\\ \text{context}\qquad&C \mathrel{::=} \square\mid\text{let } b^* = p\ a^* \text{ in } C\\ \text{binding}\qquad&e_1 \triangleright\bar{b}.e_2 \triangleq C[e_2[\bar{a}/\bar{b}]]\quad\text{where }e_1=C[\text{return }\bar{a}] \end{aligned}

then takes e1=C[return aˉ]e_1 = C[\textbf{return }\bar{a}], substitutes aˉ\bar{a} for the binders bˉ\bar{b} in e2e_2, and plugs the result into the hole of CC. Plugging is capture-avoiding (our mechanization uses a locally nameless representation, and our implementation performs α\alpha-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 f:σ→τf:\sigma\to\tau comes with a total, deterministic evaluation relation f(aˉ)→aˉ′f(\bar{a})\to\bar{a}' (e.g. (l::[l])→[ll](l::[l])\to[ll]), and the results are substituted for the binders bˉ\bar{b}.

Rule S-CALL evaluates a call. However, reducing let bˉ=call[body:j] aˉ in e\text{let }\bar{b}=\textbf{call}[\textbf{body}:j]\ \bar{a}\ \textbf{in}\ e directly to let bˉ=j@aˉ in e\text{let }\bar{b}=j@\bar{a}\ \textbf{in}\ e would violate ANF. So instead, rule S-CALL reduces to j@aˉ▹bˉ.ej@\bar{a}\triangleright\bar{b}.e, which runs the jump application and binds its results in the rest of ee.

The control-flow rules are standard. Rule S-COND reduces a conditional to a call of the branch jcj_c selected by the boolean cc. Rule S-WHILE evaluates the condition j1j_1 and, if true, runs one iteration of j2j_2 before calling the while primitive again; otherwise it returns the current state aˉ\bar{a}. 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 jj 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 jkj_k captures the continuation following the operation and reinstalls the handler. We inspect jj only to determine the parameter type τ\tau needed to construct jkj_k. Appendix § 10 illustrates these rules with two example programs that use handlers for exceptions and for state.

Metatheory

We prove that λJAXPRε\lambda^{\varepsilon}_{\mathrm{JAXPR}} enjoys the following desirable properties:

Lemma 3.1 (Determinacy). If e→e1e \to e_1 and e→e2e \to e_2, then e1=e2e_1 = e_2.

Theorem 3.2. (Preservation) If Γ⊢e:σ\Gamma\vdash e : \sigma and e→e′e \to e' then Γ⊢e′:σ\Gamma\vdash e' : \sigma.

(Progress with effects) If ∙⊢e:σ\bullet\vdash e : \sigma, then e→e′e \to e' for some e′e', or ee has the form return aˉ\mathbf{return}\ \bar{a}, or ee has the form let bˉ=do op aˉ in e′\mathbf{let}\ \bar{b} = \mathbf{do}\ op\ \bar{a}\ \mathbf{in}\ e'.

The third case of progress arises because λJAXPRε\lambda^{\varepsilon}_{\mathrm{JAXPR}} 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 λJAXPRε\lambda^{\varepsilon}_{\mathrm{JAXPR}} to its handler-free subset λJAXPR\lambda_{\mathrm{JAXPR}}, 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 E⟦H∣P∣K⊢e⟧\mathcal{E}\llbracket\mathbb{H} \mid\mathbb{P} \mid\mathbb{K} \vdash e \rrbracket translates an expression ee with respect to a handler context H\mathbb{H} mapping operations to clauses, their parameters P\mathbb{P}, and a list of continuations K\mathbb{K}, which are jaxprs in our setting. The list K\mathbb{K} contains both return clauses jrj_r of handlers and normal continuations kk, which we assume are kept distinguishable. (The special form ⟨k⟩\langle k\rangle is discussed in § 5.1 and can be ignored for now.) The function maintains the invariant that the length of H\mathbb{H} matches both the number of return clauses in K\mathbb{K} and the length of P\mathbb{P}. We assume that the output type τo\tau_o of the elaboration is given, which will always be provided (e.g. in Theorem 4.3).

Effect-handling translation

Figure 5. Effect-handling translation

The first three rules translate return aˉ\mathbf{return}\ \bar{a} 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 kk (E-Ret2), we apply it to aˉ\bar{a} and translate the result with kk removed. If it is a return clause jrj_r (E-Ret3), we apply jrj_r to the handler parameters aˉh\bar{a}_h and aˉ\bar{a}, and continue after popping the innermost handler hh, 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 call\mathop{\mathbf{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 {λbˉ.e}\{\lambda\bar{b}.e\}, prepend it to the continuation list, translate the subexpressions, apply the result to aˉ\bar{a}, and return the result bound to fresh variables zˉ\bar{z}.

Translating a do expression (rule E-Hop) is slightly more involved. First, we look up the operation clause op↦jop \mapsto j in the handler hh, together with its corresponding parameters aˉh\bar{a}_h and return clause jrj_r. This lookup relies on the structural property that the length of the handler list H′\mathbb{H}' equals both the number of return clauses in K′\mathbb{K}' and the length of the parameter list P′\mathbb{P}'. We then translate jj applied to aˉh\bar{a}_h and aˉ\bar{a} under the lists preceding hh. The complication is that jj may use resume, which

H::=∙∣H⋅hh::=op↦j\mathcal{H} ::= \bullet\mid\mathcal{H} \cdot h \qquad h ::= op \mapsto j
P::=∙∣P⋅aˉ\mathcal{P} ::= \bullet\mid\mathcal{P} \cdot\bar{a}
K::=∙∣K⋅k∣K⋅jr∣⟨k⟩\mathcal{K} ::= \bullet\mid\mathcal{K} \cdot k \mid\mathcal{K} \cdot j_r \mid\langle k\rangle
E⟦H∣P∣K⊢e⟧:τo\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash e\rrbracket: \tau_o
E⟦∙∣∙∣∙⊢returnaˉ⟧=returnaˉ\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash\mathop{\mathrm{return}}\bar{a}\rrbracket=\mathop{\mathrm{return}}\bar{a}
E⟦H∣P∣K⋅k⊢returnaˉ⟧=E⟦H∣P∣K⊢k@aˉ⟧\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\cdot k\vdash\mathop{\mathrm{return}}\bar{a}\rrbracket =\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash k@\bar{a}\rrbracket
E⟦H⋅h∣P⋅aˉh∣K⋅jr⊢returnaˉ⟧=E⟦H∣P∣K⊢jr@aˉh@aˉ⟧\mathcal{E}\llbracket\mathcal{H}\cdot h\mid\mathcal{P}\cdot\bar{a}_h\mid\mathcal{K}\cdot j_r\vdash\mathop{\mathrm{return}}\bar{a}\rrbracket =\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash j_r@\bar{a}_h@\bar{a}\rrbracket
E⟦H∣P∣K⊢letbˉ=f aˉ in e⟧=letbˉ=f aˉ inE⟦H∣P∣K⊢e⟧\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash\mathop{\mathrm{let}}\bar{b}=f\ \bar{a}\ \mathop{\mathrm{in}}\ e\rrbracket =\mathop{\mathrm{let}}\bar{b}=f\ \bar{a}\ \mathop{\mathrm{in}}\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash e\rrbracket
E⟦∙∣∙∣∙⊢letbˉ=call[body:j]aˉ in e⟧=letbˉ=call[body:J⟦∙∣∙∣∙⊢j⟧]aˉ inE⟦∙∣∙∣∙⊢e⟧\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{call}}[\mathop{\mathrm{body}}:j]\bar{a}\ \mathop{\mathrm{in}}\ e\rrbracket =\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{call}}[\mathop{\mathrm{body}}:\mathcal{J}\llbracket\bullet\mid\bullet\mid\bullet\vdash j\rrbracket]\bar{a}\ \mathop{\mathrm{in}}\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e\rrbracket
E⟦H∣P∣K⊢letbˉ=call[body:j]aˉ in e⟧=letzˉ:τo=call[body:J⟦H∣P∣K⋅{λbˉ.e}⊢j⟧]aˉ inreturnzˉ\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{call}}[\mathop{\mathrm{body}}:j]\bar{a}\ \mathop{\mathrm{in}}\ e\rrbracket =\mathop{\mathrm{let}}\bar{z}:\tau_o=\mathop{\mathrm{call}}[\mathop{\mathrm{body}}:\mathcal{J}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\cdot\{\lambda\bar{b}.e\}\vdash j\rrbracket]\bar{a}\ \mathop{\mathrm{in}}\mathop{\mathrm{return}}\bar{z}
E⟦∙∣∙∣∙⊢letbˉ=cond[then:j1,else:j2]aˉ in e⟧=letbˉ=cond[then:J⟦∙∣∙∣∙⊢j1⟧,else:J⟦∙∣∙∣∙⊢j2⟧]aˉ inE⟦∙∣∙∣∙⊢e⟧\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{cond}}[\mathop{\mathrm{then}}:j_1,\mathop{\mathrm{else}}:j_2]\bar{a}\ \mathop{\mathrm{in}}\ e\rrbracket =\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{cond}}[\mathop{\mathrm{then}}:\mathcal{J}\llbracket\bullet\mid\bullet\mid\bullet\vdash j_1\rrbracket,\mathop{\mathrm{else}}:\mathcal{J}\llbracket\bullet\mid\bullet\mid\bullet\vdash j_2\rrbracket]\bar{a}\ \mathop{\mathrm{in}}\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e\rrbracket
E⟦H∣P∣K⊢letbˉ=cond[then:j1,else:j2]aˉ in e⟧=letzˉ:τo=cond[then:j1′,else:j2′]aˉ inreturnzˉ\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{cond}}[\mathop{\mathrm{then}}:j_1,\mathop{\mathrm{else}}:j_2]\bar{a}\ \mathop{\mathrm{in}}\ e\rrbracket =\mathop{\mathrm{let}}\bar{z}:\tau_o=\mathop{\mathrm{cond}}[\mathop{\mathrm{then}}:j'_1,\mathop{\mathrm{else}}:j'_2]\bar{a}\ \mathop{\mathrm{in}}\mathop{\mathrm{return}}\bar{z}

where j1′=J⟦H∣P∣K⋅{λbˉ.e}⊢j1⟧j'_1=\mathcal{J}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\cdot\{\lambda\bar{b}.e\}\vdash j_1\rrbracket, j2′=J⟦H∣P∣K⋅{λbˉ.e}⊢j2⟧j'_2=\mathcal{J}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\cdot\{\lambda\bar{b}.e\}\vdash j_2\rrbracket

E⟦H⋅h⋅H′∣P⋅aˉh∣K⋅jr′⊢letx:out=do op aˉ in e⟧=E⟦H∣P∣K⊢j[p/resume]@aˉh@aˉ⟧\mathcal{E}\llbracket\mathcal{H}\cdot h\cdot\mathcal{H}'\mid\mathcal{P}\cdot\bar{a}_h\mid\mathcal{K}\cdot j'_r\vdash\mathop{\mathrm{let}}x:\mathop{\mathrm{out}}=\mathop{\mathrm{do}}\ op\ \bar{a}\ \mathop{\mathrm{in}}\ e\rrbracket =\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash j[p/\mathop{\mathrm{resume}}]@\bar{a}_h@\bar{a}\rrbracket

where op↦j∈h∧op∉H∧∣H∣=∣K∣=∣P∣∧h:parop\mapsto j\in h\land op\notin\mathcal{H}\land|\mathcal{H}|=|\mathcal{K}|=|\mathcal{P}|\land h:p\mathbin{\mathrm{ar}}

p=call[body:{λp,x:par,out.D⟦h⋅H′∣p⋅P′⊢e⟧}]p=\mathop{\mathrm{call}}[\mathop{\mathrm{body}}:\{\lambda p,x:p\mathbin{\mathrm{ar}},out.\mathcal{D}\llbracket h\cdot\mathcal{H}'\mid p\cdot\mathcal{P}'\vdash e\rrbracket\}]
E⟦H∣P∣K⊢D⟦H′∣P′∣K′⊢e⟧=E⟦H⋅H′∣P⋅P′∣K⋅K′⊢e⟧\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash\mathcal{D}\llbracket\mathcal{H}'\mid\mathcal{P}'\mid\mathcal{K}'\vdash e\rrbracket =\mathcal{E}\llbracket\mathcal{H}\cdot\mathcal{H}'\mid\mathcal{P}\cdot\mathcal{P}'\mid\mathcal{K}\cdot\mathcal{K}'\vdash e\rrbracket
E⟦∙∣∙∣∙⊢letbˉ=handle[args:aˉh,body:eb,ret:jr,impl:op↦j]⋆ in e⟧=E⟦op↦j∣aˉh∣jr⊢eb⟧▹bˉ.E⟦∙∣∙∣∙⊢e⟧\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{handle}}[\mathop{\mathrm{args}}:\bar{a}_h,\mathop{\mathrm{body}}:e_b,\mathop{\mathrm{ret}}:j_r,\mathop{\mathrm{impl}}:op\mapsto j]\star\ \mathop{\mathrm{in}}\ e\rrbracket =\mathcal{E}\llbracket op\mapsto j\mid\bar{a}_h\mid j_r\vdash e_b\rrbracket\mathbin{\triangleright}\bar{b}.\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e\rrbracket
E⟦H∣P∣K⊢letbˉ=handle[args:aˉh,body:eb,ret:jr,impl:op↦j]⋆ in e⟧=E⟦H⋅op↦j∣P⋅aˉh∣K⋅{λbˉ.e}⋅jr⊢eb⟧\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash\mathop{\mathrm{let}}\bar{b}=\mathop{\mathrm{handle}}[\mathop{\mathrm{args}}:\bar{a}_h,\mathop{\mathrm{body}}:e_b,\mathop{\mathrm{ret}}:j_r,\mathop{\mathrm{impl}}:op\mapsto j]\star\ \mathop{\mathrm{in}}\ e\rrbracket =\mathcal{E}\llbracket\mathcal{H}\cdot op\mapsto j\mid\mathcal{P}\cdot\bar{a}_h\mid\mathcal{K}\cdot\{\lambda\bar{b}.e\}\cdot j_r\vdash e_b\rrbracket
J⟦H∣P∣K⊢j⟧\mathcal{J}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash j\rrbracket
J⟦H∣P∣K⊢{λx:ρ.e}⟧={λx:ρ.E⟦H∣P∣K⊢e⟧}\mathcal{J}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash\{\lambda x:\rho.e\}\rrbracket =\{\lambda x:\rho.\mathcal{E}\llbracket\mathcal{H}\mid\mathcal{P}\mid\mathcal{K}\vdash e\rrbracket\}

replaced by its continuation. Yet we cannot translate that continuation immediately: resume may be invoked inside jj under various contexts, so the context for translating the resumed expression ee is unknown at this point. We therefore substitute resume with a call primitive whose body receives the updated parameters pˉ\bar{p} and the operation results xˉ\bar{x}, and returns the special expression D⟦h⋅H′∣pˉ⋅P′∣jr⋅K′⊢e⟧\mathcal{D}\llbracket h\cdot\mathcal{H}'\mid\bar{p}\cdot\mathcal{P}'\mid j_r\cdot\mathcal{K}'\vdash e\rrbracket, which captures the lists up to hh along with the updated parameters. Rule E-HResume then resolves a D⟦H′∣P′∣K′⊢e⟧\mathcal{D}\llbracket\mathcal{H}'\mid\mathcal{P}'\mid\mathcal{K}'\vdash e\rrbracket construct by concatenating the captured lists with the current ones and translating ee in the combined context. Therefore, if resume is used several times inside jj, ee 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 ebe_b with the current handler and bind its result to the translation of ee under empty lists. Otherwise (E-Handle2), we push the handler, its parameters, the continuation {λbˉ.e}\{\lambda\bar{b}.e\}, 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 ee contains no handler-related constructs, then E⟦∙∣∙∣∙⊢e⟧=e\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e\rrbracket=e.

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) E⟦H∣P∣K⊢e1▹b.e2⟧=E⟦H∣P∣K⋅{λb.e2}⊢e1⟧\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash e_1\mathbin{\triangleright} b.e_2\rrbracket=\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\cdot\{\lambda b.e_2\}\vdash e_1\rrbracket. (K-compose) E⟦H∣P∣K⋅{λx.e1▹b.e2}⋅K′⊢e⟧=E⟦H∣P∣K⋅{λb.e2}⋅{λx.e1}⋅K′⊢e⟧\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\cdot\{\lambda x.e_1\mathbin{\triangleright}b.e_2\}\cdot\mathbb{K}'\vdash e\rrbracket=\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\cdot\{\lambda b.e_2\}\cdot\{\lambda x.e_1\}\cdot\mathbb{K}'\vdash e\rrbracket. (K-delay) E⟦H∣P∣K⊢D⟦H⋅{λb.e2}⊢e1⟧⟧=E⟦H∣P∣K⊢D⟦H′⊢e1▹b.e2⟧⟧\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash\mathcal{D}\llbracket\mathbb{H}\cdot\{\lambda b.e_2\}\vdash e_1\rrbracket\rrbracket=\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash\mathcal{D}\llbracket\mathbb{H}'\vdash e_1\mathbin{\triangleright}b.e_2\rrbracket\rrbracket.

We now turn to elaboration soundness. First, we define well-formedness, denoted Γ⊢H⋈P⋈K:τ1→τ2\Gamma\vdash\mathbb{H}\bowtie\mathbb{P}\bowtie\mathbb{K}:\tau_1\to\tau_2 as a judgment indicating that the lists take an expression of type τ1\tau_1 and produce one with type τ2\tau_2. Here, Γ⊢h:par⇒τ\Gamma\vdash h:par\Rightarrow\tau abbreviates the operation-clause premises of rule T-HANDLER for a handler that takes parameters parpar and has result type τ\tau. The last premise of WF-CONT ensures continuations always end with a return clause.

Γ⊢∙⋈∙⋈∙:τ→τ\begin{aligned} \Gamma&\vdash\bullet\bowtie\bullet\bowtie\bullet:\tau\to\tau \end{aligned}

(WF-EMPTY)

Γ⊢H⋈P⋈K:τ2→τ3Γ⊢k:τ1→τ2H≠∙Γ⊢H⋈P⋈K⋅k:τ1→τ3\begin{aligned} \Gamma&\vdash\mathbb{H}\bowtie\mathbb{P}\bowtie\mathbb{K}:\tau_2\to\tau_3 \qquad \Gamma\vdash k:\tau_1\to\tau_2 \qquad \mathbb{H}\ne\bullet\\ \Gamma&\vdash\mathbb{H}\bowtie\mathbb{P}\bowtie\mathbb{K}\cdot k:\tau_1\to\tau_3 \end{aligned}

(WF-CONT)

Γ⊢H⋈P⋈K:τ2→τ3Γ⊢h:par⇒τ2Γ⊢a:parΓ⊢jr:par,τ1→τ2Γ⊢H⋅h⋈P⋅a⋈K⋅jr:τ1→τ3\begin{aligned} \Gamma&\vdash\mathbb{H}\bowtie\mathbb{P}\bowtie\mathbb{K}:\tau_2\to\tau_3 \qquad \Gamma\vdash h:par\Rightarrow\tau_2 \qquad \Gamma\vdash a:par \qquad \Gamma\vdash j_r:par,\tau_1\to\tau_2\\ \Gamma&\vdash\mathbb{H}\cdot h\bowtie\mathbb{P}\cdot a\bowtie\mathbb{K}\cdot j_r:\tau_1\to\tau_3 \end{aligned}

(WF-HANDLER)

We now establish that our translation preserves typing; a similar statement holds for jaxprs.

Theorem 4.3 (Elaboration Soundness). Given Γ⊢H⋈P⋈K:τ1→τn+1\Gamma\vdash\mathbb{H}\bowtie\mathbb{P}\bowtie\mathbb{K}:\tau_1\to\tau_{n+1}, if Γ⊢e:τ1\Gamma\vdash e:\tau_1, then Γ⊢E⟦H∣P∣K⊢e⟧:τn+1\Gamma\vdash\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash e\rrbracket:\tau_{n+1}.

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 e⟶e′e\longrightarrow e', then E⟦H∣P∣K⊢e⟧⟶∗E⟦H∣P∣K⊢e′⟧\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash e\rrbracket\longrightarrow^{*}\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash e'\rrbracket

Using the simulation step, we then prove semantics preservation:

Theorem 4.5 (Semantics preservation). If e⟶return∗ve\longrightarrow^{*}_{\mathrm{return}}v, then E⟦∙∣∙∣∙⊢e⟧⟶return∗v\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e\rrbracket\longrightarrow^{*}_{\mathrm{return}}v.

Handling Control-Flow Primitives

The translation of the previous section does not account for control-flow primitives such as while and scan. Consider translating E⟦H∣P∣K⊢let b=while [cond:j1,body:j2] a in e⟧\mathcal{E}\llbracket\mathbb{H}\mid\mathbb{P}\mid\mathbb{K}\vdash\mathbf{let}\ b=\mathbf{while}\ [\mathit{cond}:j_1,\mathit{body}:j_2]\ a\ \mathbf{in}\ e\rrbracket. Translating the loop body j2j_2 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 (∣e∣\lvert e\lvert), jaxprs (∣j∣\lvert j\lvert), and primitives (∣p∣\lvert p\lvert): 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.

Selected rules for resumption inlining

Figure 6. Selected rules for resumption inlining

From a tail-resumptive expression, the judgment R⟦H∣P∣ω⊢e⟧\mathcal{R}\llbracket\mathbb{H}\mid\mathbb{P}\mid\omega\vdash e\rrbracket extracts the pre-resume computation, which we inline instead of materializing a continuation. It tracks the handler context H\mathbb{H}, since the expression may perform further tail-resumptive operations, and threads the parameters P\mathbb{P}, which such operations may update. The type ω=τ,par,out\omega=\tau,par,out annotates generated let-expressions: τ\tau is the type of P\mathbb{P}, parpar the parameter type of the handler evaluated in-place, and outout the operation’s

⟦e⟧let xˉ:σ=resume⁡aˉ in return xˉlet xˉ:σ=paˉ in return xˉif plet xˉ:σ=paˉ in eif e∧resume⁡∉p⟦j⟧{λb∗.e}if e⟦p⟧call⁡[body⁡:j]if j⟦p⟧cond⁡[then⁡:j1,else⁡:j2]if j1∧j2\begin{aligned} &\begin{array}{|l|} \hline \llbracket e \rrbracket\\ \hline \end{array} \quad \begin{aligned} &\text{let }\bar{x}:\sigma=\operatorname{resume}\bar{a}\text{ in return }\bar{x}\\ &\text{let }\bar{x}:\sigma=p\bar{a}\text{ in return }\bar{x}\quad\text{if }p\\ &\text{let }\bar{x}:\sigma=p\bar{a}\text{ in }e\quad\text{if }e\land\operatorname{resume}\notin p \end{aligned}\\ &\begin{array}{|l|} \hline \llbracket j\rrbracket\\ \hline \end{array} \quad\{\lambda b^{*}.e\}\quad\text{if }e\\ &\begin{array}{|l|} \hline \llbracket p\rrbracket\\ \hline \end{array} \quad\operatorname{call}[\operatorname{body}:j]\quad\text{if }j\\ &\begin{array}{|l|} \hline \llbracket p\rrbracket\\ \hline \end{array} \quad\operatorname{cond}[\operatorname{then}:j_1,\operatorname{else}:j_2]\quad\text{if }j_1\land j_2 \end{aligned}
R⟦H∣P∣ω⊢e⟧\mathcal{R}\llbracket\mathsf{H}\mid\mathsf{P}\mid\omega\vdash e\rrbracket
(R-Resume)R⟦H∣P∣ω⊢let xˉ:σ=resume⁡aˉ in return xˉ⟧=return⁡ P,aˉ(R-Let1)R⟦H∣P∣ω⊢let xˉ:σ=paˉ in return xˉ⟧=let yˉ:ω=R⟦H∣P∣ω⊢p⟧aˉ in return yˉ(R-Let2)R⟦H∣P∣τ,par,out⊢let xˉ:σ=paˉ in e⟧=E⟦H∣P∣⟨{λyˉ:τ,σ.return⁡yˉ}⟩⊢let wˉ:σ=paˉ in return wˉ⟧with zˉ,xˉ. R⟦H∣zˉ∣τ,par,out⊢e⟧\begin{aligned} \text{(R-Resume)}\quad \mathcal{R}\llbracket\mathsf{H}\mid\mathsf{P}\mid\omega\vdash \text{let }\bar{x}:\sigma=\operatorname{resume}\bar{a}\text{ in return }\bar{x}\rrbracket &=\operatorname{return}\ \mathsf{P},\bar{a}\\ \text{(R-Let1)}\quad \mathcal{R}\llbracket\mathsf{H}\mid\mathsf{P}\mid\omega\vdash \text{let }\bar{x}:\sigma=p\bar{a}\text{ in return }\bar{x}\rrbracket &=\text{let }\bar{y}:\omega=\mathcal{R}\llbracket\mathsf{H}\mid\mathsf{P}\mid\omega\vdash p\rrbracket\bar{a}\text{ in return }\bar{y}\\ \text{(R-Let2)}\quad \mathcal{R}\llbracket\mathsf{H}\mid\mathsf{P}\mid\tau,\mathit{par},\mathit{out}\vdash \text{let }\bar{x}:\sigma=p\bar{a}\text{ in }e\rrbracket &=\mathcal{E}\llbracket\mathsf{H}\mid\mathsf{P}\mid \langle\{\lambda\bar{y}:\tau,\sigma.\operatorname{return}\bar{y}\}\rangle\vdash\\ &\qquad\text{let }\bar{w}:\sigma=p\bar{a}\text{ in return }\bar{w}\rrbracket\\ &\qquad\text{with }\bar{z},\bar{x}.\ \mathcal{R}\llbracket\mathsf{H}\mid\bar{z}\mid\tau,\mathit{par},\mathit{out}\vdash e\rrbracket \end{aligned}
E⟦H∣P∣K⊢e⟧\mathcal{E}\llbracket\mathsf{H}\mid\mathsf{P}\mid\mathbb{K}\vdash e\rrbracket
(E-Ret4)E⟦H∣P∣⟨k⟩⊢return⁡aˉ⟧=k@P@aˉ\text{(E-Ret4)}\quad \mathcal{E}\llbracket\mathsf{H}\mid\mathsf{P}\mid\langle k\rangle\vdash\operatorname{return}\bar{a}\rrbracket =k@\mathsf{P}@\bar{a}
(E-Hop2)E⟦H⋅h⋅H′∣P⋅aˉh⋅P′∣K⊢let xˉ:out=do⁡ op aˉ in e⟧=let yˉ:τ,par,xˉ:out=call⁡[body⁡:R⟦H∣P∣τ,par,out⊢j⟧]aˉh aˉin E⟦H⋅h⋅H′∣yˉ⋅P′∣K⊢e⟧where op↦j∈h∧op∉H′∧j∧∣H∣=∣P∣∧H:τ∧h:par\text{(E-Hop2)}\quad \begin{aligned} &\mathcal{E}\llbracket\mathsf{H}\cdot h\cdot\mathsf{H}'\mid\mathsf{P}\cdot\bar{a}_h\cdot\mathsf{P}'\mid\mathbb{K}\vdash \text{let }\bar{x}:\mathit{out}=\operatorname{do}\ op\ \bar{a}\text{ in }e\rrbracket\\ &=\text{let }\bar{y}:\tau,\mathit{par},\bar{x}:\mathit{out} =\operatorname{call}[\operatorname{body}:\mathcal{R}\llbracket\mathsf{H}\mid\mathsf{P}\mid\tau,\mathit{par},\mathit{out}\vdash j\rrbracket]\bar{a}_h\ \bar{a}\\ &\qquad\text{in }\mathcal{E}\llbracket\mathsf{H}\cdot h\cdot\mathsf{H}'\mid\bar{y}\cdot\mathsf{P}'\mid\mathbb{K}\vdash e\rrbracket\\ &\text{where }op\mapsto j\in h\land op\notin\mathsf{H}'\land j\land|\mathsf{H}|=|\mathsf{P}|\land\mathsf{H}:\tau\land h:\mathit{par} \end{aligned}
(E-While)E⟦H∣P∣K⊢let xˉ:σ=while⁡[cond⁡:{λb1.e1},body⁡:{λb2.e2}]aˉ in e⟧=let zˉ:τ,xˉ:σ=while⁡[cond⁡:j1,body⁡:j2] P aˉ in E⟦H∣zˉ∣K⊢e⟧where j1={λzˉ1:τ,bˉ1.E⟦∙∣∙∣∙⊢e1⟧}j2={λzˉ2:τ,bˉ2.E⟦H∣zˉ2∣⟨{λzˉ3:τ,yˉ:σ.return⁡zˉ3,yˉ}⟩⊢e2⟧}\text{(E-While)}\quad \begin{aligned} &\mathcal{E}\llbracket\mathsf{H}\mid\mathsf{P}\mid\mathbb{K}\vdash \text{let }\bar{x}:\sigma=\operatorname{while}[\operatorname{cond}:\{\lambda b_1.e_1\},\operatorname{body}:\{\lambda b_2.e_2\}]\bar{a}\text{ in }e\rrbracket\\ &=\text{let }\bar{z}:\tau,\bar{x}:\sigma=\operatorname{while}[\operatorname{cond}:j_1,\operatorname{body}:j_2]\ \mathsf{P}\ \bar{a} \text{ in }\mathcal{E}\llbracket\mathsf{H}\mid\bar{z}\mid\mathbb{K}\vdash e\rrbracket\\ &\text{where }j_1=\{\lambda\bar{z}_1:\tau,\bar{b}_1.\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e_1\rrbracket\}\\ &\qquad j_2=\{\lambda\bar{z}_2:\tau,\bar{b}_2.\mathcal{E}\llbracket\mathsf{H}\mid\bar{z}_2\mid \langle\{\lambda\bar{z}_3:\tau,\bar{y}:\sigma.\operatorname{return}\bar{z}_3,\bar{y}\}\rangle\vdash e_2\rrbracket\} \end{aligned}

output type. We omit the straightforward rules for jaxprs and primitives. Rule R-Resume returns the current parameters P\mathsf{P} 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 ee is tail-resumptive, and primitive pp does not mention resume. Here, the body can be inlined recursively, and paˉp\bar{a} is translated under H\mathsf{H}, allowing for tail-resumptive operations of H\mathsf{H}. Therefore, R⟦⋅⟧\mathcal{R}\llbracket\cdot\rrbracket and E⟦⋅⟧\mathcal{E}\llbracket\cdot\rrbracket are mutually recursive. Notably, pp 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 H\mathsf{H} can update the parameters, the translation of paˉp\bar{a} must return the final parameters. To address this, we introduce a new continuation form ⟨k⟩\langle k\rangle, 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 ⟨{λyˉ:τ,σ.return⁡yˉ}⟩\langle\{\lambda\bar{y}:\tau,\sigma.\operatorname{return}\bar{y}\}\rangle, so that the updated parameters are returned, bound to zˉ\bar{z}, alongside the result xˉ\bar{x}, and then translates ee using zˉ\bar{z} as the new parameters.

Handling in-place. Rule E-Hop2 in Figure 7 handles an operation in-place. It looks up the clause op↦jop\mapsto j, with j∣lj\mid l, and the parameters aˉh\bar{a}_h in the handler hh, without inspecting the continuations, and forms a call whose body is the inlined resumption of jj, computed under the handlers H\mathsf{H} preceding hh, the parameters P\mathsf{P}, and their types. The call threads the updated parameters yˉ\bar{y} and returns the output xˉ\bar{x}; the body ee is then translated under the original context with the updated parameters.

Handling in-place

Figure 7. Handling in-place

Notably, this extended translation relaxes the invariant that the number of return clauses in K\mathbb{K} always matches the number of handlers in H\mathsf{H} (e.g. in E-Ret4). This is desired: when translating do, if the corresponding handler has no return clause in K\mathbb{K}, 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 P\mathsf{P} and yields the updated ones zˉ\bar{z}. The condition $j_1 ignores the parameters and translates e1e_1 under empty contexts11; the body j2j_2 translates e2e_2 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 op1op_1 with clauses j1j_1 and j3j_3, the middle one handles op2op_2, and its op2op_2 clause performs op1op_1:

handle [args:⋆, ret:j1, impl:op1↦j1,body:let b1=handle [args:⋆, ret:j2,impl:op2↦{λb. let x:σ=do op1 a1 in let y=resume aˉ in return y},body:let b2=handle [args:⋆, ret:j3, impl:op1↦j3,body:let b3=do op2 a2 in …] ⋆ in …] ⋆ in …] ⋆\begin{aligned} &\mathop{\mathtt{handle}}\ [\mathtt{args}: \star,\ \mathtt{ret}: j_1,\ \mathtt{impl}: op_1 \mapsto j_1,\\ &\qquad\mathtt{body}: \mathtt{let}\ b_1 = \mathop{\mathtt{handle}}\ [\mathtt{args}: \star,\ \mathtt{ret}: j_2,\\ &\qquad\qquad\mathtt{impl}: op_2 \mapsto\{ \lambda b.\ \mathtt{let}\ x : \sigma= \mathtt{do}\ op_1\ a_1\ \mathtt{in}\ \mathtt{let}\ y = \mathtt{resume}\ \bar{a}\ \mathtt{in}\ \mathtt{return}\ y \},\\ &\qquad\qquad\mathtt{body}: \mathtt{let}\ b_2 = \mathop{\mathtt{handle}}\ [\mathtt{args}: \star,\ \mathtt{ret}: j_3,\ \mathtt{impl}: op_1 \mapsto j_3,\\ &\qquad\qquad\qquad\mathtt{body}: \mathtt{let}\ b_3 = \mathtt{do}\ op_2\ a_2\ \mathtt{in}\ \ldots]\ \star\ \mathtt{in}\ \ldots]\ \star\ \mathtt{in}\ \ldots]\ \star \end{aligned}

When the innermost body performs op2op_2, naively taking the pre-resume part of the op2op_2 clause, i.e. the computation before resume, would yield the translation

let b3=call [body:{λb. let x=do op1 a1 in return aˉ}] aˉ\mathtt{let}\ b_3 = \mathtt{call}\ [\mathtt{body}: \{ \lambda b.\ \mathtt{let}\ x = \mathtt{do}\ op_1\ a_1\ \mathtt{in}\ \mathtt{return}\ \bar{a} \}]\ \bar{a}

This changes the program’s semantics: op1op_1 is now handled by the innermost handler (j3j_3), although it was originally performed within the scope of the outermost handler (j1j_1). 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 j1j_1 (§ 5.1). Instead of the naive pre-resume part, the inlining emits a call to the following translation, so that op1op_1 is correctly handled by j1j_1:

R⟦op1↦j1∣⋆∣⋆,⋆,σ⊢let x:σ=do op1 a1 in let y=resume aˉ in return y⟧=E⟦op1↦j1∣⋆∣⟨{λy:⋆,σ. return y}⟩⊢let w:σ=do op1 a1 in return w⟧▹z,x. R⟦…⟧\begin{aligned} \mathcal{R}\llbracket op_1 \mapsto j_1 \mid\star\mid\star,\star,\sigma\vdash\mathtt{let}\ x : \sigma= \mathtt{do}\ op_1\ a_1\ \mathtt{in}\ \mathtt{let}\ y = \mathtt{resume}\ \bar{a}\ \mathtt{in}\ \mathtt{return}\ y \rrbracket\\ = \mathcal{E}\llbracket op_1 \mapsto j_1 \mid\star\mid\langle\{ \lambda y : \star,\sigma.\ \mathtt{return}\ y \} \rangle\vdash\mathtt{let}\ w : \sigma= \mathtt{do}\ op_1\ a_1\ \mathtt{in}\ \mathtt{return}\ w \rrbracket\triangleright z,x.\ \mathcal{R}\llbracket\ldots\rrbracket \end{aligned}

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 opiop_i (for i∈1…ni \in1 \ldots n) with signatures ini→outiin_i \to out_i 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.

let xˉ:σ=while [cond:j1, body:j2] aˉ in e⇝let zˉ, iˉ, vˉi:σ′=while [cond:j1′, body:j2′] aˉ 0 zeros(ini) in let xˉ:σ=cond [branches:ji′] zˉ vˉi in e\mathtt{let}\ \bar{x} : \sigma= \mathtt{while}\ [\mathtt{cond}: j_1,\ \mathtt{body}: j_2]\ \bar{a}\ \mathtt{in}\ e \quad\leadsto\quad \mathtt{let}\ \bar{z},\ \bar{i},\ \bar{v}_i : \sigma' = \mathtt{while}\ [\mathtt{cond}: j'_1,\ \mathtt{body}: j'_2]\ \bar{a}\ 0\ \mathtt{zeros}(in_i)\ \mathtt{in}\ \mathtt{let}\ \bar{x} : \sigma= \mathtt{cond}\ [\mathtt{branches}: j'_i]\ \bar{z}\ \bar{v}_i\ \mathtt{in}\ e

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 ii (initially 0) identifying the recorded operation, if any, together with its arguments vˉi\bar{v}_i (initially zeros). The body j2′j'_2 wraps the original body in a handler for all opiop_i that discards the resumption and returns the loop state, ii, and the operation arguments; the condition j1′j'_1 additionally checks i=0i = 0; and after the loop, a cond on ii (where branches abbreviates nested conditions on an integer) returns the loop results if i=0i = 0 and otherwise re-performs opiop_i 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 (R⟦⋅⟧\mathcal{R}\llbracket\cdot\rrbracket soundness). Given Γ⊢P:par‾\Gamma\vdash\mathsf{P} : \overline{\mathit{par}} and Γ⊢H:par‾⇒τi\Gamma\vdash\mathsf{H} : \overline{\mathit{par}} \Rightarrow\tau_i, and ⌞e⌟\llcorner e \lrcorner, if Γ,resume:par,out→σ⊢e:σ\Gamma, \mathsf{resume} : \mathit{par}, \mathit{out} \to\sigma\vdash e : \sigma, then Γ⊢R⟦H∣P∣par‾i,par,out⊢e⟧:par‾i,par,out\Gamma\vdash\mathcal{R}\llbracket\mathsf{H} \mid\mathsf{P} \mid\overline{\mathit{par}}_i, \mathit{par}, \mathit{out} \vdash e \rrbracket: \overline{\mathit{par}}_i, \mathit{par}, \mathit{out}.

We then extend our well-formedness relation Γ⊢H⋈P⋈K:τ1→τ2\Gamma\vdash\mathsf{H} \bowtie\mathsf{P} \bowtie\mathbb{K} : \tau_1 \to\tau_2 with a fourth case for the final continuation ⟨k⟩\langle k\rangle: The final continuation may only occur as the leftmost element of K\mathbb{K}: it is the last continuation to run, as it ends the computation:

Γ⊢H:par‾i⇒τiΓ⊢P:par‾iΓ⊢k:par‾i,τ1→τ2Γ⊢H⋈P⋈⟨k⟩:τ1→τ2(1)\frac{\Gamma\vdash\mathsf{H} : \overline{\mathit{par}}_i \Rightarrow\tau_i \qquad\Gamma\vdash\mathsf{P} : \overline{\mathit{par}}_i \qquad\Gamma\vdash k : \overline{\mathit{par}}_i, \tau_1 \to\tau_2}{\Gamma\vdash\mathsf{H} \bowtie\mathsf{P} \bowtie\langle k\rangle: \tau_1 \to\tau_2} \tag*{(1)}

That is, kk receives the current parameters par‾i\overline{\mathit{par}}_i alongside the result of type τ1\tau_1, and produces the final result of type τ2\tau_2. 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 e⟶e′e \longrightarrow e', then the translated program should take one or more steps, ⟦e⟧⟶∗⟦e′⟧\llbracket e \rrbracket\longrightarrow^* \llbracket e' \rrbracket. 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:

e1⪯e2⟺def∀a‾, e1⟶∗return a‾⟹e2⟶∗return a‾j1⪯j2⟺def∀a‾, j1@a‾⪯j2@a‾e_1 \preceq e_2 \quad\overset{\mathrm{def}}{\Longleftrightarrow} \quad\forall\overline{a},\ e_1 \longrightarrow^* \mathsf{return}\,\overline{a} \Longrightarrow e_2 \longrightarrow^* \mathsf{return}\,\overline{a} \qquad j_1 \preceq j_2 \quad\overset{\mathrm{def}}{\Longleftrightarrow} \quad\forall\overline{a},\ j_1@\overline{a} \preceq j_2@\overline{a}

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) e⪯ee \preceq e. (Transitivity) If e1⪯e2e_1 \preceq e_2 and e2⪯e3e_2 \preceq e_3, then e1⪯e3e_1 \preceq e_3.

Lemma 5.3. (Compatibility of Bind) If e1⪯e2e_1 \preceq e_2, then b‾. e⪯b‾. e\overline{b}. \ e \preceq\overline{b}. \ e. (Compatibility of Let) If e1[a‾/b]⪯e2[a‾/b]e_1[\overline{a}/b] \preceq e_2[\overline{a}/b] for all a‾\overline{a}, then let b‾=p‾ in e1⪯let b‾=p‾ in e2\mathsf{let}\ \overline{b} = \overline{p}\ \mathsf{in}\ e_1 \preceq\mathsf{let}\ \overline{b} = \overline{p}\ \mathsf{in}\ e_2.

Simulation between translations. Since the tail-resumptive translation differs syntactically from the core translation, we start by relating the two by a simulation e1⪯je2e_1 \preceq_j e_2 (Figure 8) between a program e1e_1 on the core side and a program e2e_2 on the tail-resumptive side, modulo a pending continuation jj.

Figure 8. Simulation between the core translation (left) and the tail-resumptive translation (right)

The pending continuation jj is what the core side still runs once the tail-resumptive side returns, so Sim-Resume relates j1@aˉj_1@\bar{a} to return⁡aˉ\operatorname{return}\bar{a}. 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 e4e_4, the core side instead runs its continuation e2e_2, so the prefixes are related with e2e_2 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 e1≃je2e_1 \simeq^{j} e_2, and e2⟶e2′e_2 \longrightarrow e_2', then there exists e1′e_1' such that e1⟶∗e1′e_1 \longrightarrow^{*} e_1' and e1′≃je2′e_1' \simeq^{j} e_2'.

When the tail-resumptive side returns, the core side runs to the pending continuation applied to the same atoms: if e1≃jreturn⁡aˉe_1 \simeq^{j} \operatorname{return}\bar{a}, then e1⟶∗j@aˉe_1 \longrightarrow^{*} j@\bar{a}. At the identity continuation id={λxˉ.return⁡xˉ}id=\{\lambda\bar{x}.\operatorname{return}\bar{x}\} this is return⁡aˉ\operatorname{return}\bar{a} itself. Therefore:

Lemma 5.5 (Adequacy of ≃j\simeq^{j}). If e1≃ide2e_1 \simeq^{id} e_2, then e1⪯e2e_1 \preceq e_2.

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 op↦j∈hop \mapsto j \in h, with ⌊j⌋\lfloor j \rfloor, and Γ,resume⁡:par⁡,out⁡→σ⊢j:σ\Gamma,\operatorname{resume}:\operatorname{par},\operatorname{out}\to\sigma\vdash j:\sigma, and op∉Hop\notin H, and Γ⊢H′⋈P′⋈K′:τ1→τ2\Gamma\vdash H'\bowtie P'\bowtie K':\tau_1\to\tau_2, then

E⟦H∣P∣K⊢j[call⁡[body⁡:{λp,x:par⁡,out⁡.D⟦h⋅H′∣p⋅P′⊢e2⟧}]/resume⁡]@hˉ@aˉ⟧⪯R⟦H∣P∣par⁡,out⁡⊢j⟧▹p,x.E⟦H⋅h⋅H′∣P⋅p⋅P′∣K⋅jr′⊢e2⟧\mathcal{E}\llbracket H\mid P\mid K\vdash j[\operatorname{call}[\operatorname{body}:\{\lambda p,x:\operatorname{par},\operatorname{out}.\mathcal{D}\llbracket h\cdot H'\mid p\cdot P'\vdash e_2\rrbracket\}]/\operatorname{resume}]@\bar{h}@\bar{a}\rrbracket \preceq \mathcal{R}\llbracket H\mid P\mid\operatorname{par},\operatorname{out}\vdash j\rrbracket\triangleright p,x.\mathcal{E}\llbracket H\cdot h\cdot H'\mid P\cdot p\cdot P'\mid K\cdot j_r'\vdash e_2\rrbracket

Using Lemma 5.6, we update the simulation step lemma (Lemma 4.4) with the logical relation: each source step is matched up to ⪯\preceq.

Theorem 5.7 (Simulation step). If e⟶e′e\longrightarrow e', then E⟦H∣P∣K⊢e′⟧⪯E⟦H∣P∣K⊢e⟧\mathcal{E}\llbracket H\mid P\mid K\vdash e'\rrbracket\preceq\mathcal{E}\llbracket H\mid P\mid K\vdash e\rrbracket.

Since ⪯\preceq 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 ⊢e:τ\vdash e:\tau and e⟶∗return⁡aˉe\longrightarrow^{*}\operatorname{return}\bar{a}, then E⟦∙∣∙∣∙⊢e⟧⟶∗return⁡aˉ\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash e\rrbracket\longrightarrow^{*}\operatorname{return}\bar{a}.

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.

An effecax program and the jaxpr it traces to

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 α\alpha-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.

Compiled jaxpr

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.

Runtime comparison plots for Diffrax and Diffrax with effecax

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)
Proportion1.37 (0.05 + 1.32)1.82 (0.12 + 1.69)1.34 (0.04 + 1.30)
Stoch. volatility0.48 (0.09 + 0.39)0.90 (0.19 + 0.70)0.59 (0.10 + 0.48)
Bayesian NN0.48 (0.13 + 0.36)0.78 (0.19 + 0.59)0.46 (0.09 + 0.37)
Semi-sup. HMM1.44 (0.20 + 1.25)1.45 (0.15 + 1.30)1.98 (0.72 + 1.27)
UCBadmit GLMM0.88 (0.15 + 0.73)0.87 (0.07 + 0.80)1.37 (0.25 + 1.12)
HSGP1.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 ff and an initial value y0y_0, a solver numerically approximates the trajectory y(t)y(t) where dy/dt=f(t,y)\mathrm{d}y/\mathrm{d}t = f(t,y). The core is an integration loop: starting from y0y_0, 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.

Comparison of the core integration loop of Diffrax and the refactored loop with effecax

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 ≈22%\approx22\% 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 N×NN \times N grid, sweeping the grid size NN (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 (xx-axis), the runtime falls (yy-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 p(x,z)p(x,z) over a space of latent variables zz and observations xx. Bayesian inference computes the posterior p(z∣x)=p(x,z)/p(x)p(z \mid x) = p(x,z)/p(x). The marginal likelihood p(x)=∫p(x,z) dzp(x) = \int p(x,z)\,\mathrm{d}z 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).

Hidden Markov model in NumPyroHidden Markov model in effecax

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.

Nested effectful scan in effecaxEffectful cond in effecax

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 zz (x+yx + y) seconds, separating the framework’s effect-elimination transform time (xx) and XLA compilation time (yy) from their total (zz). 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).

ModelSizeHW (s)EX (s)OX (s)NP (s)
Proportion3M10.2517.7811.5617.73
Stoch. volatility2.5k1.751.362.721.99
Bayesian NN50k8.9711.529.118.99
Semi-sup. HMM5k9365952694239434
UCBadmit GLMM49k6.666.676.657.29
HSGP7.3k5.244.205.234.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, jrj_r denotes the handler’s return clause and hh its operation clauses.

Exceptions

Given an exception operation throw⁡:⋆→int⁡\operatorname{throw} : \star\to\operatorname{int}, 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 jj resumes with the fallback value 0; the handler takes no parameter, and its return clause jrj_r 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 m=42m = 42 and n=0n = 0 (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 jj with the handler parameter and the operation argument, replacing resume by a call to the captured continuation jkj_k, which reinstalls the handler around the rest of the body:

let v: int=call [ body: j[call [ body: jk ]/resume] ⋆ ⋆ ] in return vwhere jk={λz′:⋆,z: int. let y: int= handle [ args: z′, body: return z, ret: jr, impl: throw ↦j ] ⋆ inreturn y}\begin{aligned} \text{let }v:\text{ int} &= \text{call [ body: }j[\text{call [ body: }j_k\text{ ]/resume] }\star\ \star\text{ ] in return }v\\ \text{where }j_k &= \{\lambda z' : \star, z : \text{ int}. \text{ let }y : \text{ int} = \text{ handle [ args: }z',\text{ body: return }z,\text{ ret: }j_r,\text{ impl: throw }\mapsto j\text{ ] }\star\text{ in}\\ &\qquad\text{return }y\} \end{aligned}

Inlining jj (S-Call) yields let uu : int = call [ body: jkj_k ] ⋆\star 0 in return uu, and inlining jkj_k reinstalls the handler with z=0z = 0. Its body has returned, so S-HRet applies the return clause, and S-Call finishes:

let y: int=handle [ args: ⋆, body: return 0, ret: jr, impl: … ] ⋆ in return y⟶let y: int=call [ body: jr ] ⋆ 0 in return y⟶∗return 0\begin{aligned} \text{let }y:\text{ int} &= \text{handle [ args: }\star,\text{ body: return }0,\text{ ret: }j_r,\text{ impl: }\ldots\text{ ] }\star\text{ in return }y\\ &\longrightarrow\text{let }y:\text{ int} = \text{call [ body: }j_r\text{ ] }\star\ 0\text{ in return }y\\ &\longrightarrow^{*}\text{return }0 \end{aligned}

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 h=throw↦jh = \text{throw} \mapsto j, continuation list jrj_r, and parameter ⋆\star, and binds the result to vv for the translation of return vv under empty lists, which is return $v itself (E-Ret1). The interesting part is therefore

E⟦h∣⋆∣jr⊢ let w: int=call [ body: safe_div ] 42 0 in return w⟧.\mathcal{E}\llbracket h\mid\star\mid j_r\vdash\text{ let }w:\text{ int}=\text{call [ body: safe\_div ] }42\ 0\text{ in return }w\rrbracket.

By E-Call2, we capture the continuation k1={λw: int. return w}k_1=\{\lambda w:\text{ int}.\text{ return }w\} and translate the jaxpr safe_div under jr⋅k1j_r\cdot k_1, producing let z1z_1 : int = call [ body: J⟦h∣⋆∣jr⋅k1⊢ safe_div⟧\mathcal{J}\llbracket h\mid\star\mid j_r\cdot k_1\vdash\text{ safe\_div}\rrbracket ] 42 0 in return z1z_1. Rule E-JaxpR recurses into the body of safe_div; E-Func keeps the primitive eq; and E-Cond2 captures k2={λr: int. return r}k_2=\{\lambda r:\text{ int}.\text{ return }r\} and translates both branches under jr⋅k1⋅k2j_r\cdot k_1\cdot k_2. The else branch performs no operation: E-Func keeps div, and return zz is translated by popping k2k_2 and k1k_1 (E-Ret2, twice) and then jrj_r (E-Ret3), each application reducing to return zz, so the branch is unchanged. The then branch is where the handler is compiled away:

E⟦h∣⋆∣jr⋅k1⋅k2⊢ let z: int=do throw ⋆ in return z⟧.\mathcal{E}\llbracket h\mid\star\mid j_r\cdot k_1\cdot k_2\vdash\text{ let }z:\text{ int}=\text{do throw }\star\text{ in return }z\rrbracket.

By E-Hop, we look up the clause jj for throw in hh, replace resume in it by a call p=call [ body: {λp:⋆,z: int.D⟦h∣p∣jr⋅k1⋅k2⊢ return z⟧} ]p=\text{call [ body: }\{\lambda p:\star,z:\text{ int}.\mathcal{D}\llbracket h\mid p\mid j_r\cdot k_1\cdot k_2\vdash\text{ return }z\rrbracket\}\text{ ]} 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 hh, which are empty:

\begin{aligned} \mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash j[p/\text{resume}]\rrbracket_{\star@\star} &=\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash\text{ let }u:\text{ int}=p\ \star\ 0\text{ in return }u\rrbracket\\ &=\text{let }u:\text{ int}=\text{call [ body: }\{\lambda p:\star,z:\text{ int}.\mathcal{E}\llbracket\bullet\mid\bullet\mid\bullet\vdash\mathcal{D}\llbracket h\mid p\mid j_r\cdot k_1\cdot k_2\vdash\text{ return }z\rrbracket\rrbracket\}\text{ ] }\star\ 0\text{ in return }u\\ &=\text{let }u:\text{ int}=\text{call [ body: }\{\lambda p:\star,z:\text{ int}.\mathcal{E}\llbracket h\mid p\mid j_r\cdot k_1\cdot k_2\vdash\text{ return }z\rrbracket\}\text{ ] }\star\ 0\text{ in return }u\\ &=\text{let }u:\text{ int}=\text{call [ body: }\{\lambda p:\star,z:\text{ int}.\text{ return }z\}\text{ ] }\star\ 0\text{ in return }u \end{aligned}

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 k2k_2, k1k_1, and jrj_r to return zz. The operation has been eliminated: the call to pp simply yields the fallback value. The complete translation is

let z1: int=call [ body: {λm: int,n: int. let b: bool= eq n 0 inlet r: int= cond [ then: {λx: int,y: int.let u: int=call [ body: {λp:⋆,z: int. return z} ] ⋆ 0 in return u},else: {λx: int,y: int. let z: int= div x y in return z}] b min return r} ] 42 0 in return z1\begin{aligned} \text{let }z_1:\text{ int}={}&\text{call [ body: }\{\lambda m:\text{ int},n:\text{ int}.\text{ let }b:\text{ bool}=\text{ eq }n\ 0\text{ in}\\ &\text{let }r:\text{ int}=\text{ cond [ then: }\{\lambda x:\text{ int},y:\text{ int}.\\ &\qquad\text{let }u:\text{ int}=\text{call [ body: }\{\lambda p:\star,z:\text{ int}.\text{ return }z\}\text{ ] }\star\ 0\text{ in return }u\},\\ &\text{else: }\{\lambda x:\text{ int},y:\text{ int}.\text{ let }z:\text{ int}=\text{ div }x\ y\text{ in return }z\}]\ b\ m\\ &\text{in return }r\}\text{ ] }42\ 0\text{ in return }z_1 \end{aligned}

which is a vanilla jaxpr evaluating to 0, as the source program does.

State

Consider state operations get⁡:⋆→int⁡\operatorname{get} : \star\to\operatorname{int} and set⁡:int⁡→⋆\operatorname{set} : \operatorname{int} \to\star. 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 ⋆\star 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:

let v:int⁡,int⁡=handle [ args:1,body:let x:int⁡=do get⁡ ⋆ in let y:int⁡=add⁡ x x in let z:⋆=do set⁡ y in return 42,ret:jr={λx:int⁡,y:int⁡. return x y},impl: get⁡↦jg={λx:int⁡,y:⋆. let z:int⁡,int⁡=resume x x in return z},set⁡↦js={λx:int⁡,y:int⁡. let z:int⁡,int⁡=resume y ⋆ in return z} ] ⋆in return v\begin{aligned} \mathsf{let}\ v &: \operatorname{int},\operatorname{int} = \mathsf{handle}\ [\ \mathsf{args}:1,\\ &\quad\mathsf{body}: \mathsf{let}\ x:\operatorname{int}=\mathsf{do}\ \operatorname{get}\ \star\ \mathsf{in}\ \mathsf{let}\ y:\operatorname{int}=\operatorname{add}\ x\ x\ \mathsf{in}\ \mathsf{let}\ z:\star=\mathsf{do}\ \operatorname{set}\ y\ \mathsf{in}\ \mathsf{return}\ 42,\\ &\quad\mathsf{ret}:j_r=\{\lambda x:\operatorname{int},y:\operatorname{int}.\ \mathsf{return}\ x\ y\},\\ &\quad\mathsf{impl}:\ \operatorname{get}\mapsto j_g=\{\lambda x:\operatorname{int},y:\star.\ \mathsf{let}\ z:\operatorname{int},\operatorname{int}=\mathsf{resume}\ x\ x\ \mathsf{in}\ \mathsf{return}\ z\},\\ &\qquad\qquad\operatorname{set}\mapsto j_s=\{\lambda x:\operatorname{int},y:\operatorname{int}.\ \mathsf{let}\ z:\operatorname{int},\operatorname{int}=\mathsf{resume}\ y\ \star\ \mathsf{in}\ \mathsf{return}\ z\}\ ]\ \star\\ &\mathsf{in}\ \mathsf{return}\ v \end{aligned}

Evaluation. The body immediately performs get, so S-Hdo2 runs the clause jgj_g with the parameter 1 and the argument ⋆\star, with resume\mathsf{resume} replaced by a call to the continuation

jk1={λz′:int⁡,x:int⁡. let y′:int⁡,int⁡=handle [args:z′, body:e1, ret:jr, impl:…] ⋆ in return y′}.j_{k_1}=\{\lambda z':\operatorname{int},x:\operatorname{int}.\ \mathsf{let}\ y':\operatorname{int},\operatorname{int}=\mathsf{handle}\ [\mathsf{args}:z',\ \mathsf{body}:e_1,\ \mathsf{ret}:j_r,\ \mathsf{impl}:\ldots]\ \star\ \mathsf{in}\ \mathsf{return}\ y'\}.
e1=let y:int⁡=add⁡ x x in let z:⋆=do set⁡ y in return 42,e_1=\mathsf{let}\ y:\operatorname{int}=\operatorname{add}\ x\ x\ \mathsf{in}\ \mathsf{let}\ z:\star=\mathsf{do}\ \operatorname{set}\ y\ \mathsf{in}\ \mathsf{return}\ 42,

where e1e_1 is the rest of the body. Inlining jgj_g and then jk1j_{k_1} (S-Call, twice) reinstalls the handler with parameter 1 and x=1x=1. Inside it, add evaluates to 2 (S-Func, under S-Hcong), and the body performs set 2. Again S-Hdo2 runs jsj_s with parameter 1 and argument 2; its resume 2 ⋆\mathsf{resume}\ 2\ \star reinstalls the handler with parameter 2 around the remaining body return 42\mathsf{return}\ 42. That body has returned, so S-HRet applies jrj_r 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 h=get⁡↦jg,set⁡↦jsh=\operatorname{get}\mapsto j_g,\operatorname{set}\mapsto j_s, the continuation list jrj_r, and the parameter 1, and binds the result to vv. The body starts with get, so E-Hop applies: resume\mathsf{resume} in jgj_g is replaced by p1=call [body:{λp:int⁡,x:int⁡. D⟦h∣p∣jr⊢e1⟧}]p_1=\mathsf{call}\ [\mathsf{body}:\{\lambda p:\operatorname{int},x:\operatorname{int}.\ \mathcal{D}\llbracket h\mid p\mid j_r\vdash e_1\rrbracket\}], and jg[p1/resume]j_g[p_1/\mathsf{resume}] applied to 1 and ⋆\star is translated under empty lists. By E-Call1 the call is kept and its body translated, and by E-HResume that body becomes E⟦h∣p∣jr⊢e1⟧\mathcal{E}\llbracket h\mid p\mid j_r\vdash e_1\rrbracket: the rest of the body, now translated with the updated parameter pp in place of 1. Rule E-Func keeps add, and the set operation is handled by E-Hop in the same way, with p2=call [body:{λp′:int⁡,z:⋆. D⟦h∣p′∣jr⊢return 42⟧}]p_2=\mathsf{call}\ [\mathsf{body}:\{\lambda p':\operatorname{int},z:\star.\ \mathcal{D}\llbracket h\mid p'\mid j_r\vdash\mathsf{return}\ 42\rrbracket\}] and the clause js[p2/resume]j_s[p_2/\mathsf{resume}] applied to pp and yy. Its delayed body E⟦h∣p′∣jr⊢return 42⟧\mathcal{E}\llbracket h\mid p'\mid j_r\vdash\mathsf{return}\ 42\rrbracket is resolved by E-Ret3: the return clause is applied to the current parameter p′p' and the result, giving return p′ 42\mathsf{return}\ p'\ 42. Assembling the pieces, the complete translation is

let z:int⁡,int⁡=call [body:{λp:int⁡,x:int⁡. let y:int⁡=add⁡ x x inlet z′:int⁡,int⁡=call [body:{λp′:int⁡,z′:⋆.return p′ 42}] y inreturn z′}] 1 1 in return z\begin{aligned} \mathsf{let}\ z:\operatorname{int},\operatorname{int}={}&\mathsf{call}\ [\mathsf{body}:\{\lambda p:\operatorname{int},x:\operatorname{int}.\ \mathsf{let}\ y:\operatorname{int}=\operatorname{add}\ x\ x\ \mathsf{in}\\ &\quad\mathsf{let}\ z':\operatorname{int},\operatorname{int}=\mathsf{call}\ [\mathsf{body}:\{\lambda p':\operatorname{int},z':\star.\mathsf{return}\ p'\ 42\}]\ y\ \mathsf{in}\\ &\quad\mathsf{return}\ z'\}]\ 1\ 1\ \mathsf{in}\ \mathsf{return}\ z \end{aligned}

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 rr, through the source-level rewrite

let x‾:σ=while[cond:j1,body:j2] a‾ in e ⇝1 let r:bool,x‾:σ=while[cond:j1′,body:j2′] a‾ in(2)\mathsf{let}\ \overline{x}:\sigma=\mathsf{while}[\mathsf{cond}:j_1,\mathsf{body}:j_2]\ \overline{a}\ \mathsf{in}\ e\ \leadsto_1\ \mathsf{let}\ r:\mathsf{bool},\overline{x}:\sigma=\mathsf{while}[\mathsf{cond}:j'_1,\mathsf{body}:j'_2]\ \overline{a}\ \mathsf{in} \tag*{(2)}

with the rewritten condition j1′j'_1 and body j2′j'_2 defined as follows:

j1′={λr:bool, y:σ. return r}j'_1 = \{ \lambda r : \mathrm{bool},\ y : \sigma.\ \mathrm{return}\ r \}
j2′={λr:bool, y:σ. let b:bool=call [body:j1] yˉ in let b′:bool, zˉ:σ=cond [then:{λzˉ:σ. let wˉ:σ=call [body:j2] zˉ in return t wˉ} else:{λzˉ:σ. return f yˉ}] b yˉ in return b′ zˉ}j'_2 = \{ \lambda r : \mathrm{bool},\ y : \sigma.\ \mathrm{let}\ b : \mathrm{bool} = \mathrm{call}\ [\mathrm{body}: j_1]\ \bar{y}\ \mathrm{in}\ \mathrm{let}\ b' : \mathrm{bool},\ \bar{z} : \sigma= \mathrm{cond}\ [\mathrm{then}: \{ \lambda\bar{z} : \sigma.\ \mathrm{let}\ \bar{w} : \sigma= \mathrm{call}\ [\mathrm{body}: j_2]\ \bar{z}\ \mathrm{in}\ \mathrm{return}\ t\ \bar{w} \}\ \mathrm{else}: \{ \lambda\bar{z} : \sigma.\ \mathrm{return}\ f\ \bar{y} \}]\ b\ \bar{y}\ \mathrm{in}\ \mathrm{return}\ b'\ \bar{z} \}

The new condition j1′j'_1 returns the boolean carry rr. The new body j2′j'_2 evaluates the original condition j1j_1 on the state yˉ\bar{y} and branches on the result: if true, it runs the original body j2j_2 and returns tt together with the new state; otherwise it returns ff with the current state, which makes j1′j'_1 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 opiop_i (for i∈1…ni \in1 \ldots n) with signatures ini→outiin_i \to out_i whose abortive handlers reside outside the loop:

let xˉ:σ=while [cond:j1, body:j2] aˉ in e ⇝2 let zˉ, i, vˉi:σ′=while [cond:j1′, body:j2′] aˉ 0 zeros(ini) in let xˉ:σ=cond [branches:ji] i zˉ vˉi in e\mathrm{let}\ \bar{x} : \sigma= \mathrm{while}\ [\mathrm{cond}: j_1,\ \mathrm{body}: j_2]\ \bar{a}\ \mathrm{in}\ e\ \rightsquigarrow_2\ \mathrm{let}\ \bar{z},\ i,\ \bar{v}_i : \sigma' = \mathrm{while}\ [\mathrm{cond}: j'_1,\ \mathrm{body}: j'_2]\ \bar{a}\ 0\ \mathrm{zeros}(in_i)\ \mathrm{in}\ \mathrm{let}\ \bar{x} : \sigma= \mathrm{cond}\ [\mathrm{branches}: j_i]\ i\ \bar{z}\ \bar{v}_i\ \mathrm{in}\ e

The loop state is extended with an integer flag ii, 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:

σ′=σ, int, ini‾\sigma' = \sigma,\ \mathrm{int},\ \overline{in_i}
j1′={λzˉ, i, vˉi:σ′. let b1:bool=call [body:j1] zˉ in let b2:bool=eq i 0 in let b:bool=and b1 b2 in return b}j'_1 = \{ \lambda\bar{z},\ i,\ \bar{v}_i : \sigma'.\ \mathrm{let}\ b_1 : \mathrm{bool} = \mathrm{call}\ [\mathrm{body}: j_1]\ \bar{z}\ \mathrm{in}\ \mathrm{let}\ b_2 : \mathrm{bool} = \mathrm{eq}\ i\ 0\ \mathrm{in}\ \mathrm{let}\ b : \mathrm{bool} = \mathrm{and}\ b_1\ b_2\ \mathrm{in}\ \mathrm{return}\ b \}
j2′={λzˉ, i, vˉi:σ′. let wˉ:σ′=handle [args:⋆, body:j2@zˉ, ret:{λyˉ:σ. return yˉ 0 zeros(ini)}, impl:opi↦{λvˉ:ini. return zˉ i zeros(ini−1) vˉ zeros(ini+1…)}] ⋆ in return wˉ}j'_2 = \{ \lambda\bar{z},\ i,\ \bar{v}_i : \sigma'.\ \mathrm{let}\ \bar{w} : \sigma' = \mathrm{handle}\ [\mathrm{args}: \star,\ \mathrm{body}: j_2@\bar{z},\ \mathrm{ret}: \{ \lambda\bar{y} : \sigma.\ \mathrm{return}\ \bar{y}\ 0\ \mathrm{zeros}(in_i) \},\ \mathrm{impl}: op_i \mapsto\{ \lambda\bar{v} : in_i.\ \mathrm{return}\ \bar{z}\ i\ \mathrm{zeros}(in_{i-1})\ \bar{v}\ \mathrm{zeros}(in_{i+1\ldots}) \}]\ \star\ \mathrm{in}\ \mathrm{return}\ \bar{w} \}
j0={λyˉ, i, vˉi:σ′. return yˉ}j_0 = \{ \lambda\bar{y},\ i,\ \bar{v}_i : \sigma'.\ \mathrm{return}\ \bar{y} \}
ji={λyˉ, i, vˉi:σ′. let zˉ:outi=do opi vˉi in return zˉ}j_i = \{ \lambda\bar{y},\ i,\ \bar{v}_i : \sigma'.\ \mathrm{let}\ \bar{z} : out_i = \mathrm{do}\ op_i\ \bar{v}_i\ \mathrm{in}\ \mathrm{return}\ \bar{z} \}

The extended state type σ′\sigma' adds to the original loop state σ\sigma an integer flag ii and the argument types ini‾\overline{in_i} of every operation. The condition j1′j'_1 evaluates the original condition j1j_1 on the original state zˉ\bar{z} and conjoins it with the check i=0i = 0. The body j2′j'_2 runs the original body j2j_2 under a handler with a unit parameter. Its return clause passes the updated state through with i=0i = 0 and zeroed argument slots. Its clause for opiop_i discards the resumption and returns the current state zˉ\bar{z}, the identifier ii, and the operation arguments vˉ\bar{v} placed in the ii-th slot, with all other slots zeroed. Finally, the branch jaxprs jˉi\bar{j}_i are selected on the flag after the loop: j0j_0 returns the loop results, while jij_i (for i≥1i \ge1) re-performs opiop_i with the stored arguments vˉi\bar{v}_i.

Elaboration Soundness and Semantics Preservation

We write ⇝1\rightsquigarrow_1 for the effectful conditions rewrite of §B.1 and ⇝2\rightsquigarrow_2 for the abortive handlers rewrite of §B.2. We prove that both rewrites preserve typing.

Theorem B.1 (Effectful Conditions Elaboration Soundness). If Γ⊢e1:τ\Gamma\vdash e_1 : \tau and e1⇝1e2e_1 \rightsquigarrow_1 e_2, then Γ⊢e2:τ\Gamma\vdash e_2 : \tau.

Theorem B.2 (Abortive Handlers Elaboration Soundness). If Γ⊢e1:τ\Gamma\vdash e_1 : \tau and e1⇝2e2e_1 \rightsquigarrow_2 e_2, then Γ⊢e2:τ\Gamma\vdash e_2 : \tau.

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 (≃cond\simeq_{\mathit{cond}}). We then show that this relation is closed under step:

Lemma B.3 (Effectful Conditions Step Simulation). If Γ⊢e1:τ\Gamma\vdash e_1 : \tau, e1≃conde2e_1 \simeq_{\mathit{cond}} e_2, and e1⟶e1′e_1 \longrightarrow e'_1, then e2⟶∗e2′e_2 \longrightarrow^{*} e'_2 for some e2′e'_2 with e1′≃conde2′e'_1 \simeq_{\mathit{cond}} e'_2.

Using Lemma B.3, we then show semantics preservation:

Theorem B.4 (Effectful Conditions Semantics Preservation). If Γ⊢e1:τ\Gamma\vdash e_1 : \tau, e1⇝1e2e_1 \rightsquigarrow_1 e_2, and e1⟶∗return aˉe_1 \longrightarrow^{*} \mathit{return}\ \bar{a}, then e2⟶∗return aˉe_2 \longrightarrow^{*} \mathit{return}\ \bar{a}.

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 (≃abort\simeq_{\mathit{abort}}). We then prove step simulation:

Lemma B.5 (Abortive Handlers Step Simulation). If e1≃aborte2e_1 \simeq_{\mathit{abort}} e_2 and e1⟶e1′e_1 \longrightarrow e'_1, then e2⟶∗e2′e_2 \longrightarrow^{*} e'_2 for some e2′e'_2 with e1′≃aborte2′e'_1 \simeq_{\mathit{abort}} e'_2.

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 Γ⊢e1:τ\Gamma\vdash e_1 : \tau and e1⇝2e2e_1 \rightsquigarrow_2 e_2, then

(Returns) if e1⟶∗return aˉe_1 \longrightarrow^{*} \mathit{return}\ \bar{a}, then e2⟶∗return aˉe_2 \longrightarrow^{*} \mathit{return}\ \bar{a};

(Operations) if e1⟶∗let bˉ=do op aˉ in ee_1 \longrightarrow^{*} \mathop{\mathit{let}}\ \bar{b} = \mathit{do}\ \mathit{op}\ \bar{a}\ \mathit{in}\ e, then e2⟶∗let bˉ=do op aˉ in e′e_2 \longrightarrow^{*} \mathop{\mathit{let}}\ \bar{b} = \mathit{do}\ \mathit{op}\ \bar{a}\ \mathit{in}\ e'; if op∉A\mathit{op} \notin A, then e≃aborte′e \simeq_{\mathit{abort}} e'.

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 e1⇝2∗e2e_1 \rightsquigarrow^{*}_2 e_2 of Figure 15, which applies ⇝2\rightsquigarrow_2 at any number of positions in e1e_1. The judgment is parameterized by the set AA of abortive operations, whose handlers never resume, and rule Rw-Loop only rewrites loops whose opi\mathit{op}_i are in AA. Rule Rw-Loop states the rewrite of a loop let xˉ:σ=while [cond:j1,body:j2] aˉ in return xˉ\mathop{\mathit{let}}\ \bar{x} : \sigma= \mathit{while}\ [\mathit{cond}: j_1,\mathit{body}:j_2]\ \bar{a}\ \mathit{in}\ \mathit{return}\ \bar{x} without its continuation: the rewritten loop ends in the dispatch ere_r on the flag, which is then composed with the original continuation ee. 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 hh, 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.

Abortive rewrite congruence selected rules

Figure 15. Abortive rewrite congruence (selected rules).

e⇝∗2e′e \mathrel{\overset{*}{\rightsquigarrow}_{2}} e'

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 ⊢e1:τ\vdash e_1 : \tau, e1⇝∗2e2e_1 \mathrel{\overset{*}{\rightsquigarrow}_{2}} e_2, and e1⟶∗return⁡a‾e_1 \longrightarrow^{*} \operatorname{return}\overline{a}, then e2⟶∗return⁡a‾e_2 \longrightarrow^{*} \operatorname{return}\overline{a}.

Mechanization

Table 3 lists the Lean counterpart of each result in the paper.

ResultPaper NameLean NameFileLine
Lemma 3.1DeterminacydeterminacyMiniJaxprH2976
Theorem 3.2PreservationpreservationMiniJaxprH8419
Theorem 3.2Progress with effectsprogressMiniJaxprH8130
DefinitionE⟦⋅⟧\mathcal{E}\llbracket\cdot\rrbrackettranslateExprTranslation213
DefinitionJ⟦⋅⟧\mathcal{J}\llbracket\cdot\rrbrackettranslateJaxprTranslation456
Lemma 4.1Pure programspure_programsKfamily13
Lemma 4.2K-pushK_pushKfamily3418
Lemma 4.2K-composeK_composeKfamily3465
Lemma 4.2K-delayK_delayKfamily3518
Theorem 4.3Elaboration soundnesselaboration_soundnessElaborationSoundness1070
Lemma 4.4Simulation stepsimulation_stepSemanticsPreservation3200
Theorem 4.5Semantics preservationsemantics_preservationSemanticsPreservation4711
DefinitionR⟦⋅⟧\mathcal{R}\llbracket\cdot\rrbracketRExprTranslation463
Lemma 5.1R⟦⋅⟧\mathcal{R}\llbracket\cdot\rrbracket soundnessresumption_inlining_typedElaborationSoundness1365
DefinitionLogical relationRefinesSimulation121
Lemma 5.2ReflexivityRefines_reflSimulation124
Lemma 5.2TransitivityRefines_transSimulation127
Lemma 5.3Compatibility (Bind)Refines_bindSimulation305
Lemma 5.3Compatibility (Let)Refines_letSimulation2455
DefinitionSimulation relationRsim1Simulation409
DefinitionSimulation relation (top level)Rsim0Simulation456
Lemma 5.4Step matchingsim_stepSimulation1595
Lemma 5.5Adequacy of ≃\simeqsimulation_preserves_semanticsSimulation2372
Lemma 5.6Resumption inlining preserves semanticsresumption_inlining_preserves_semanticsSemanticsPreservation1951
Theorem 5.7Simulation stepsimulation_stepSemanticsPreservation3200
Theorem 5.8Semantics preservationsemantics_preservationSemanticsPreservation4711
Theorem B.1Effectful conditions: typingtranslateWhile_preserves_typingEffectfulConditional436
Definition≃cond\simeq_{\mathrm{cond}}WSimEffectfulConditional784
Lemma B.3Effectful conditions: stepWSim_stepEffectfulConditional1242
Theorem B.4Effectful conditions: semanticstranslateWhile_semantics_preservationEffectfulConditional1756
Theorem B.2Abortive handlers: typingtranslateWhile_preserves_typingAbortive1619
Definition≃abort\simeq_{\mathrm{abort}}ASimCongAbortive5998
Lemma B.5Abortive handlers: stepASimCong_stepAbortive6904
Theorem B.6Abortive handlers: semanticstranslateWhile_preserves_terminalsAbortive5858
Definition\❀^{*}RewriteExprAnywhereAbortive508
Theorem B.7Rewrite congruence: semanticsRewriteExprAnywhere_starAbortive7522

Table 3. Paper results and their Lean counterparts. Files are JaxEffects/File.lean.

References

  1. [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. [2]Anthropic. 2026. Anthropic’s Transparency Hub. https://www.anthropic.com/transparency.
  3. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [14]Roy Frostig, Matthew James Johnson, and Chris Leary. 2019. Compiling machine learning programs via high-level tracing. In SysML conference 2018.
  15. [15]Gemini Team. 2024. Gemini: A Family of Highly Capable Multimodal Models. arXiv:2312.11805 [cs.CL] https://arxiv.org/abs/2312.11805
  16. [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. [17]Google. 2017. XLA - TensorFlow, compiled. https://developers.googleblog.com/2017/03/xla-tensorflow-compiled.html
  18. [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. [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. [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. [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. [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. [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. [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. [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. [26]Andrew Kennedy. 2007. Compiling with continuations, continued. SIGPLAN Not. 42, 9 (Oct. 2007), 177–190. doi:10.1145/1291220.1291179DOI
  27. [27]Patrick Kidger. 2021. On Neural Differential Equations. Ph. D. Dissertation. University of Oxford.arxiv.org/abs/2202.02435
  28. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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. [43]Gordon Plotkin and Matija Pretnar. 2013. Handling algebraic effects. Logical methods in computer science 9 (2013).DOI
  44. [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. [45]John C Reynolds. 1972. Definitional interpreters for higher-order programming languages. In Proceedings of the ACM annual conference-Volume 2. 717–740.
  46. [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. [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. [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. [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. [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. [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. [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. [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. [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.

Paper details

vlnc:W_hdbjd97q
v1 · latestHistory (1)

published · Fresh — added to the Hub within the last 30 days

added ·

Usage

views · 0

downloads · 0

A view counts once per browser per day, a download each time the PDF is requested.

CC BY 4.0

Original source

  • Computer Science

Not recorded for this paper · View statement →

Looking for related papers…
Contents