Explicit Effects and Effect Constraints in ReML

An important aspect of building robust systems that execute on dedicated hardware and perhaps in constrained environments is to control and manage the effects performed by program code. We present ReML, a higher-order statically-typed functional language, which allows programmers to be explicit about the effects performed by program code and in particular effects related to memory management. Allowing programmers to be explicit about effects, the regions in which values reside, and the constraints under which code execute, makes programs robust to changes in the program source code and to compiler updates, including compiler optimisations. ReML is integrated with a polymorphic inference system that builds on top of region-inference, as it is implemented in the MLKit, a Standard ML compiler that uses region-based memory management as its primary memory management scheme.


INTRODUCTION
Region inference is a memory discipline that aims at inserting instructions for allocating and deallocating memory in a program at compile time, without relying on collecting or maintaining liveness information dynamically as is usually the case for dynamic garbage collection techniques such as reference tracing garbage collection [Tofte and Birkedal 1998;Tofte and Talpin 1997].
To make region inference tractable, memory is organised into a stack of regions, each of which may grow dynamically (organised in pages).New regions are pushed on top of the region stack and the top-most region may be popped (thereby freeing the pages) when it is certain that no values residing in the top-most region will be needed for the remainder of the computation.Each allocating expression (e.g., pair construction or string concatenation) is annotated with a region variable and each expression may be translated into an expression of the form letregion in , which follows the semantics that first a region (specified by ) is pushed (on the region stack), then the expression is evaluated to a value , perhaps using the region bound to , and finally, the region is popped, with being the result of evaluating the letregion construct.Functions may be inferred to take regions as arguments, which allows for modular development, meaning that a function may allocate in and read from regions that are not allocated when the function is defined.
Region inference builds on a region-and effect-based program analysis [Jouvelot and Gifford 1991;Lucassen and Gifford 1988;Talpin and Jouvelot 1994], which at its core tracks the memory effects (reads and writes) of expressions through an extended notion of types, where, in particular, lambda-calculus augmented with support for recursive functions featuring type polymorphism and polymorphism in regions and effects and an assertion construct that halts evaluation unless an explicit effect constraint is satisfied.The language also features polymorphic recursion in regions and effects and a letregion construct for local bindings of regions and effects.
In Section 4, we present the syntactic constructs of ReML and show how the constructs interact with Standard ML syntax.We also give examples of how ReML complains if a program is not consistent with the region-inference typing rules.
In Section 5, we describe the notion of effect constraints using the notion of while-types.We also demonstrate the modular properties of working with effect constraints by showing how constraints from a calling context may be used for establishing the constraints of a called function.In particular, we continue the example concerning parallelism and demonstrate how the constraints of a low-level parallel primitive are satisfied by constraints specified by a higher-level construct (two functions may be evaluated in parallel without using allocation locks if the allocation effects of the two functions do not intersect).
In Section 6, we discuss aspects of ReML that allow the programmer to reason about other effects than memory effects, including exceptions, mutable updates, IO, non-termination, nondetermination, and perhaps even user-defined effects.In Section 7, we describe how region and effects as well as effect constraints and constraint resolution are integrated with region inference.The section discusses the integration both from a theoretical perspective and from the perspective of the implementation in the MLKit compiler.Section 8 contains a discussion of a series of larger examples and gives an overview of the status of ReML and the artifact associated with the paper [Elsman 2024].In Section 9, we describe related work and in Section 10, we conclude and discuss future work.

A MOTIVATING EXAMPLE FOR EFFECT CONSTRAINTS
As a first motivating example for effect constraints, consider the version of parallel Mergesort on lists shown in Figure 1(a). 1 The function pmsort takes as argument an integer P approximating the available parallel resources.When only one processor is available, pmsort reverts to a sequential version of Mergesort (provided by the function smsort).The function pmsort assumes a function split:int list → int list * int list, which takes a list and divides it into two approximately equally sized lists, and a function merge:int list*int list → int list, which, given two sorted lists, returns a sorted list containing the elements in the two argument lists.The strategy applied by pmsort is simple; if the argument is non-empty, split the argument into equally sized lists, sort the lists in parallel, and merge the sorted lists to produce the result.A function for executing code in parallel is the fork-join style operator par, which, in ReML, has the following type: The par function is specified to take a pair of two functions as argument and it returns a pair holding the result of evaluating the two functions. 2Notice that each of the argument functions is annotated with a so-called effect variable and that the declaration is annotated with a so-called effect constraint e1 ## e2, which specifies that the two supplied functions should not have overlapping put-effects, meaning that they should not allocate into the same regions.In fact, it is critical for performance that the effect constraint is satisfied.Whereas the underlying runtime system will protect the allocation pointer in case of a race, a drastic performance penalty will appear if each thread makes many allocations into the shared region.We seek a lightweight method to ensure statically that there are no overlapping effects.
To appreciate properly the inferred region-annotated version of the code, which is listed in Figure 1(b), we first give the region-and effect-annotated types for split and merge, simplified slightly to serve the example: We see that split stores the resulting lists in two potentially different regions, possibly distinct from the region holding the argument list.Similarly, merge stores the merged list in a potentially different region from the regions holding the argument lists.Notice that in the region-and effectannotated types for split and merge, put( ) indicates the effect of storing into the region and get( ) indicates a read from the region .
Looking now at the inferred region-annotated version of pmsort in Figure 1(b), we first see that split and merge are passed only the regions into which they allocate (for which there are put effects in their region-annotated types).We also see that support for region-polymorphic recursion has made it possible to use local regions (declared using an internal-language with-declaration) for storing the temporary sorted lists and that the regions into which the temporary lists are stored are disjoint as seen from the point-of-view of the par function.However, the inferred annotations provided in Figure 1(b) are fragile to changes in the source code.For instance, if the programmer provides a merge function that does not return its result in a region distinct from those holding the arguments, all threads will end up storing the merged lists in the same region.A traditional version of the function merge will have exactly this problem due to the typing rules for polymorphic typeand effect-inference: We shall see later, in Section 5.1, that par is not a primitive function in ReML but that the function is built on top of a more fundamental thread library and that the constraints specified by par are required to meet the specified constraints of the underlying functionality.
We emphasise here that effect constraints of the form discussed in this section is only part of what can be expressed with the technique described in this paper.Although the formalisation given in the next section focuses on providing disjointness guarantees about effect sets, the technique can be used also to reason about other effects, including exceptions, mutation, IO, and termination, as we shall discuss in Section 6.

FORMALISATION
We shall use to range over so-called region variables and to range over so-called effect variables.An effect ( ) is a set of atomic effects ( ), each of which can be either an effect variable or a region variable.Notice that, for the sake of the formalisation, we have simplified the notion of atomic effects and identified get and put effects.We shall later, in Section 6.1, return to the topic of refining the notion of atomic effects.Latent effects in the types of functions are of the form . .Such objects are called arrow effects and are central for identifying effects and for defining the notion of substitution, which is the foundation for unification of region-and effect-annotated types and the region inference algorithm that we build upon [Tofte andBirkedal 1998, 2000].
A basic constraint ( ) takes the form # and a constraint set ( ) is a set of basic constraints.When = # ′ is a basic constraint, we write swap( ) to denote the basic constraint ′ # .We also write # ′ to mean { } # { ′ } when and ′ are atomic effects.A basic constraint of the forms ∅ # or # ∅ is called a nil-constraint and we write nil( We shall use to range over so-called type variables.Here are the definitions of atomic effects, effects, types, and type schemes, which are types parameterised over type variables, effect variables, and region variables: When is some type, we shall sometimes implicitly treat it as the type scheme with no quantified variables and the empty constraint set.Similarly, we shall often implicitly drop empty constraint sets in type schemes.When = ∀ ì ì ì .⊲ , we consider ì , ì , and ì to be bound in and and, in general, we consider objects identical up to renaming of bound names.Also, when is some object, we write fv to denote the set of free type variables, free region variables, and free effect variables in .We also write frev to denote the set of free region variables and free effect variables of .Finally, we write fev to denote the free effect variables of .

Constraint Entailment
Constraint sets specify evidence that effects are disjoint.They are, in particular, used to reason about aliasing of region and effect parameters.Thus, when a function is defined, we may specify that two region parameters never alias.This property must then be established in each calling context.Moreover, region and effect variables introduced by letregion-constructs never alias other region and effect variables.
Whenever is some constraint set, we write # to specify the constraint # ′ , where ′ = frev( ).Constraints of this form are established when denotes an effect set bound by a letregion construct, which, by definition, introduces regions and effects that are distinct from all other regions and effects in the context.
At (function) instantiation sites, we need to check that all instantiated constraints are satisfied, perhaps using constraints established in the calling context.For this purpose, we introduce the notion of constraint entailment.We first introduce a notion of constraint normalisation.When is some basic constraint # ′ , we write || || to denote the normalised basic constraint set We say that a constraint set entails another constraint set ′ , written |= ′ , if the judgment can be derived by the following rules: Notice that if |= ′ for some constraint sets and ′ then ∪ ′′ |= ′ , for any other constraint set ′′ .We refer to this property as constraint entailment extensibility.
An important property of constraint entailment is that validity is ensured for entailed constraint sets.That is, if |= ′ then ⊢ ′ , which holds even if is not valid.It is really the property of validity that is important for proving soundness (in particular progress) of the dynamic semantics.The machinery that we define here makes it possible to maintain validity of constraints also when functions are composed and instantiated in particular contexts.

Substitutions
A substitution ( ) is a triple ( r , t , e ), where r is a region substitution, mapping region variables to region variables, t is a type substitution mapping type variables to types, and e is an effect substitution, mapping effect variables to arrow effects.The effect of applying a substitution on an object is to perform the three substitutions simultaneously on the three kinds of variables in the object (by renaming of bound variables within the object to avoid capture and by extending the maps to be the identity outside of their domains).For effects and arrow effects, substitution is defined as follows [Tofte and Birkedal 2000], assuming = ( r , t , e ): , where e ( ) = ′ .′ Substitutions form the basis for unification of region-and effect-annotated types in region inference and having substitutions map effect variables to arrow effects makes it possible to find unifiers (unifying substitutions) for two different region-and effect-annotated types with the same underlying ML types [Tofte and Birkedal 2000].Whereas it may be possible to specify the region typing rules using another notion of substitution, we maintain consistency with region inference by using the same definition of substitution here.Substitutions may be composed (obeying function composition properties) and the restriction of a substitution to some domain is written ↓ .Substitutions act as the identity outside of their domains.
In the following, we shall often work with substitutions that are particularly restricted.We therefore introduce the notation |= • ′ to mean |= ( ′ ) and Dom ∩ frev = ∅.
The following proposition holds: Proof.By induction over the derivation of ∪ ′ |= ′′ .
Case If

Instantiation
Given a constraint set , we say that a type instantiates a type scheme = ∀ ì ì ì .′ ⊲ ′ via a substitution , written ⊢ ≥ via , if the following conditions hold: When we are interested in only the region instance list, we write ⊢ ≥ via ì ′ to mean, there exists a substitution = ( t , r , e ) such that ⊢ ≥ via and Rng r = ì ′ .Notice that the relation requires that ( ′ ) can be proven based on the constraints in .It is this requirement that ensures that the calling context is compatible with the function requirements.

Expressions
Expressions ( ) and values ( ) take the following forms: Values include unboxed integers ( ), boxed ordinary closures, and boxed recursive function closures.Whereas a boxed value is annotated with information about in which region the value is located, the expression counterparts are annotated with information about in which region the value is stored when the allocating expression is evaluated.An expression can be a value, a variable, a lambda-expression, a let-expression, a function application, a recursive function expression, or a recursive function application.The language also includes an expression of the form assert in , which, when takes the form { } # { ′ }, has the effect of testing whether the two regions and ′ are indeed different before evaluation proceeds into .If the two regions are identical, evaluation is stuck.Thus, one purpose of the type system is to guarantee that a well-typed expression is not stuck due to an assertion.We shall sometimes write assert # ′ in to mean assert { } # { ′ } in .The purpose of having the assert construct in the formal language is exactly to model operations such as parallel constructs that require certain constraints to be satisfied.
Notice that recursive function definitions may take regions as arguments and are annotated with explicit type schemes, which allow for specifying constraint set assertions.By allowing functions to be annotated with constraint set assertions, function preconditions can be established without a calling context knowing the internals of a function.At the same time, each function body can be type-checked in isolation from contexts that apply the function.

Typing Rules for Values
⊢ : Typing Rules for Expressions , Γ ⊢ : , for values.In expressions letregion { ì ì } in , the variables ì and ì are bound in .We consider expressions identical up to renaming of bound names.

Typing Rules
Environments (Γ) map program variables to type schemes.When Γ and Γ ′ are environments, we write Γ + Γ ′ to denote the environment with domain Dom Γ ∪ Dom Γ ′ and values The typing rules for values and expressions are mutually dependent and are given in Figure 2. The typing rules for values allow inference of sentences of the form ⊢ : , stating that "the value has type scheme ".The typing rules for expressions allow inference of sentences of the form , Γ ⊢ : , , which states that "under the constraint set and in the type environment Γ, the expression has type scheme and effect .
There are several aspects to note about the typing rules.First, in the typing rules for functions, the latent effect of functions are annotated as arrow effects on the arrows.We see here the importance of arrow effects, which allows us to identify effects comprised of other effects. 3Second, both the value and expression rules for recursive functions allow for polymorphic recursion in regions and effects, whereas polymorphic recursion in types is not supported.Third, in the typing rule for letregion, we assert that created local regions (and effects) are distinct from all other region and effect variables.Fourth, the typing rule for assert expressions requires that the asserted basic constraint is entailed by the constraint set provided by the context.Recall here that the constraint entailment also provides a guarantee that the constraint is valid.Also, constraint entailment ensures that the region and effect variables occurring in the involved constrained sets in assert expressions are free in the constraint set provided by the context.In particular, the rules [TV-Fun] and [T-Fun] introduce nil-constraints in the typing judgments for the body of the recursive function, which make the abstracted region and effect variables available for assertions (the assert construct requires involved variables to occur free in the contextual constraint set).Another observation is that free region-and effect-variables occurring in an asserted basic constraint in the assert construct are added to the effect set of the assert expression, which ensures that the involved regions and effects are not being discharged prematurely by invocation of the letregion evaluation rule.Finally, the typing rule for recursive function calls requires that such function calls are fully applied, which models that recursive function calls need not lead to intermediate closures; instead, both region arguments and the value argument can be provided in registers (or in a stack frame).

Typing Properties
The typing rules possess some important properties that we shall emphasise.First, the typing rules are closed under environment and constraint extensibility: Proof.By induction on the derivation of ∪ ′ , Γ ⊢ : , .The proof makes essential use of Proposition 3.2 for the case of recursive function application and essential use of Proposition 3.1 for the case of assertions. □ 3 If we had instead annotated arrows with effects of the form { , }, given two such effects, { , } and { ′ , ′ }, we would not be able to come up with a unifier (i.e., a substitution) that would uniquely identify the two effects as other effect variables could appear in and

Allocation and Deallocation
Fig. 3. Dynamic semantics for region-annotated programs.

Dynamic Semantics
To give a dynamic semantics for the region-annotated language, we first define the grammar for redexes ( ) and evaluation contexts ( ): Evaluation contexts make explicit, through , the region and effect variables bound to regions and effects in encapsulating letregion constructs.When is an evaluation context and is an expression, we write [ ] to denote the expression formed by filling the hole [.] in the context with the expression .
The evaluation rules are given in Figure 3 and consist of allocation and deallocation rules, reduction rules, and a context rule.The rules are of the form ↩− − → ′ , which says that, given a set of allocated regions , the expression reduces to the expression ′ in one step.Notice the reduction rule for letregion constructs, which puts no constraints on which regions are deallocated.In contrast, the reduction rule for assert is instrumented with the requirements that the two involved constrained effect sets are disjoint.
We further define the evaluation relation ↩− → * as the least relation formed by the reflexive transitive closure of the relation ↩− − →.We further define ⇓ to mean ↩− → * , and ⇑ to mean that there exists an infinite sequence,

Type Safety
The proof of type safety is based on well-known techniques for proving type safety for statically typed languages [Morrisett 1995;Wright and Felleisen 1994].
A well-typed expression is either a value or it can be expressed as the composition of an evaluation context and a redex.Proposition 3.6 (Uniqe Decomposition).If ⊢ : , , then either (1) is a value, or (2) there exist a unique ′ , ′ , and ′ such that = ′ [ ′ ] and ⊢ ′ : ′ , ∪ ′ and ′ is a redex.
Proof.By induction on the structure of .□ A type preservation property (i.e., subject reduction) for the language, as well as progress and type soundness, can be stated as follows: Proposition 3.7 (Type Preservation).If ⊢ : , and ↩− − → ′ then ⊢ ′ : , .
Proof.By induction on the derivation ↩− − → ′ .The most involved cases include the case for contextual evaluation (i.e., [Ctx]), which proceeds by case analysis on the structure of contexts, and the case for recursive function application.
Proof.If is not a value, then by Proposition 3.6 there exist a unique ′ , , and ′ such that = ′ [ ] and ⊢ : ′ , ∪ ′ .The remainder of the proof argues that 3. We proceed by case analysis.
Case = letregion ′ in , for some ′ and some .We have immediately, from the region reduction rule, that 2 = .
Proof.By induction on the length of the evaluation sequence, applying Proposition 3.7 and Proposition 3.8.□

SYNTACTIC CONSTRUCTS
As mentioned earlier, a Standard ML program is also a ReML program.ReML introduces new name spaces for explicit region variables and explicit effect variables.Besides the top-level region variables r0top, r0pair, r0triple, r0ref, r0array, and r0string and the top-level effect variable e0, new explicit region and effect variables may be introduced using with declarations (in which variables prefixed with an e are deemed to be effect variables).Here is an example function that declares a local region r for storing a temporary pair, followed by a projection of the first element: Notice how the pair is annotated with an explicit region-annotation, specifying that the pair should be stored in the local region r.As an alternative, we may choose that the pair should be stored in the global region r0pair: Either way, ReML decides that the programs are well-typed according to the region-inference typing rules, albeit the latter function g will leak a pair whenever the function is called, a property, which (we shall see) is captured in the effect of the function.
An alternative to annotating allocating expressions with region information, a ReML programmer may use region-annotated type ascriptions to enforce values to be stored in particular regions: In general, ReML will happily optimise (i.e., simplify) the program [Elsman and Hallenberg 1995], but if optimisations are disabled, we can get ReML to print the internal representation of h: ) at r0pair with r15 :1 in ( fn at r15 v90 ⇒ let val v91 = #1 v90 in v91 end ) x end One aspect to notice is that an internal region analysis [Birkedal et al. 1996] has determined that the region r15 takes up only one word of memory (for storing the code pointer of the closure), which means that r15 will be allocated in the function frame of h.We shall later in Section 6 discuss extensions that make it possible to specify such properties directly as programmer annotations.
Instead of storing values in local or global regions, a function may store values in regions that are passed as parameters to the function.As a simple example, here is a function that takes an integer as argument and creates a list of integers: When calling the function, the programmer may choose to be explicit about region parameters or rely on region inference to do its best (as in the recursive call to down above).Assuming a function first:int list→int, returning ~1 (negative one) if the argument list is empty, here is an example call to down, followed by a call to first: val x = let with r in first ( down `r 5) end Here, ReML has inferred that it is safe to deallocate the local region r after extracting the first element of the list.

Exomorphisms and Endomorphisms
An exomorphic function is a function that stores its result in regions different from those containing the arguments.Here is an example exomorphic function: Here we have specified that copy receives its argument in r1 and returns a list in r2.If we had forgotten to copy xs in the last line, ReML complains with an error message: copylist .sml , line 4, column 9: fun copy `[ r1 r2 ] ( xs : list `r1 ) : list `r2 = ^^^^^^^Ĉ annot unify the explicit region variables `r1 and `r2 An endomorphic function, on the other hand, is a function that stores its result in the same regions as its arguments.A good example is the infix append-function @, which is defined as follows: Notice that the first argument list is allowed to reside in a different region than the second argument and that the result is stored in the same region as the second argument.If we want to make sure that the result is stored into a region different from the second argument, the programmer may first copy the second argument: fun copyappend `[ r1 r2 r3 ] ( xs : list `r1 , ys : list `r2 ) : list `r3 = xs @ ( copy ys ) It is an important design choice that explicit region variables are never unified, which gives us modular properties about functions that are explicitly annotated.Notice, however, that a calling context may pass the same region for different formal explicit region parameters.We shall see later, in Section 5, that ReML allows for expressing that two arguments reside in different regions.

Specifying Effects
ReML distinguishes between explicit region variables and explicit effect variables, both of which may be declared using with declarations and as parameters to functions (using the `notation).
Here is an example of a type ascription that refers to an explicit effect variable: Whereas explicit effect variables are often not useful in their own right, they are important for expressing constraints through the use of while-types, as we shall see in Section 5.

EFFECT CONSTRAINTS
Effect constraints, which is a novel concept in ReML, are expressed using so-called while-types, which are types annotated with the while keyword, followed by an effect constraint.ReML supports effect constraints on the forms 1 ## 2 and 1 # 2 , where 1 and 2 are effects.An effect in ReML is either an effect variable or a set of atomic effects.An atomic effect takes one of the forms get r, put r, or e, where r is an explicit region variable and e is an explicit effect variable (in the formalisation in Section 3, atomic effects of the forms put r and get r are conflated as the atomic effect ).Multiple constraints may be specified using nested while-types.
Constraints of the form 1 ## 2 express that 1 and 2 contain no intersecting put-effects.Constraints of the form 1 # 2 express that the set of explicit region and effect variables of 1 and 2 are disjoint.

Unlocking Parallelism
In recent work, a fork-join parallel construct has been added to the MLKit [Elsman and Henriksen 2023].In essence, the fork-join functionality is expressed through the following interface: Here, the spawn function takes two functions as arguments.In a call spawn f g, the function f is evaluated in parallel with g, which may use the get functionality to join the thread executing f.
A particular problem that the spawn interface exposes is that there are no guarantees that the allocation effect of evaluating f does not intersect with the allocation effect of evaluating g.In particular, the two threads may each attempt to modify a shared allocation pointer, causing a race-condition, unless a mutual exclusion lock (i.e., a mutex) is used for controlling access to the shared resource.
Whereas the overhead of potential race-conditions may be mitigated with a so-called protection inference [Elsman and Henriksen 2023], in ReML, we can express that the allocation effects of the two threads are disjoint, using while-types, as is also demonstrated in Section 2: Here the effect variables e1 and e2 are implicitly quantified in function specifications.While while -types allow for expressing effect constraints of the kind shown for spawn, it now becomes the obligation of the caller to establish that the effects of the two provided functions do not have intersecting allocation effects.In particular, instantiating spawn in some context must obey the while-type constraint, which may require evidence obtained from other while-type constraints.

EXTENDING THE NOTIONS OF EFFECTS
Although ReML provides general support for managing effects through its type-and effect-system, each type of effect must be added to ReML case-by-case.ReML currently supports effects that are related to memory allocation and memory use through the notion of put and get effects, but the foundation supports other kinds of effects and refinements of memory effects, as we shall discuss in detail below.

Refinement of Memory Effects
Allowing programmers to reason about mutable updates at the type level provides a good foundation for reasoning about local state.We refine atomic effects and effect constraints as follows: Notice that the letregion construct (with declaration in ReML) acts as a handler for mutation effects in a local region.Thus, local mutable state is supported and mutation cannot be observed if the region holding the mutable data is local to the function.
In practice, here are the types for reference construction, dereference, and assignment, without showing auxiliary region and effect variables: We see that the latent effect specified for mutable update (:=) includes a mut( ) effect.As for reference updates, the type for array update (Array.update)contains a mut effect on the region containing the array.Notice also the support for effect constraints on the form noput( ) and nomut( ), which allow for specifying that a function does not allocate in non-local regions and that a function makes no mutations in non-local data structures.Moreover, with the refinement of atomic effects, it is natural to include a basic constraint ## ′ , which specifies that put effects in and ′ do not intersect.For the formalisation, we extend the notion of what constitutes a valid basic constraint (i.e., the relation ⊢ ): We further refine the notion of normalisation to contain also definitions for the new cases.Thus, we have, for instance, It is important to notice here that validity of a basic constraint is not a sufficient property for the basic constraint to be satisfied.In order for a basic constraint to be satisfied, it must be entailed by the constraint set specified in the context of a particular call site, which may include precondition constraints for effect variables, for instance, that have not yet been instantiated.
A further possibility would be to allow the programmer to be explicit about whether a region should be allocated directly on the stack, which it can be if it can be determined statically that only one value will ever reside in the region and that the maximum size of that value can be determined statically [Birkedal et al. 1996 may be a desirable property of a function and it may be desirable for a programmer to specify and reason about such a property.

Exceptions
Extending the effect language to allow also for specifying possibly uncaught exceptions [Fahndrich et al. 1998;Pessaux and Leroy 1999] fit naturally in ReML.Special care must be taken to deal properly with the generative nature of exceptions in Standard ML (and ReML), however. 4A key benefit in ReML from integrating region inference and exception inference is that exceptions that can be inferred to live locally may be allocated in local regions.Currently, exceptions that carry payloads are allocated in global regions to ensure that exception-carried values are still allocated at potential handler-sites.
In the following, we use to range over exception names.For extending the ReML formalism with support for exceptions (that do not take payloads), we first extend the language with support for raising exceptions and for handling exceptions, as well as typing rules for specifying the typing and effect properties for the new constructs.For simplicity, we assume that the construct for raising an exception takes an exception name as argument and that the handling construct has only one branch for handling a particular exception: The dynamic semantics is extended to allow for an expression to evaluate to an exception and particular context rules are added for formally defining the propagation of raised exceptions (we shall not give the rules here).
We then refine the notion of atomic effects and basic constraints as follows: We further extend the notion of what constitutes a valid basic constraint (i.e., the relation ⊢ ), as we did for the refined memory effects: Basic Constraint Validity for Exceptions ⊢ .exn ∈ ⊢ noexn Finally, we further refine the notion of normalisation to contain also a definition for the new case.Thus, we have Notice that for normalisation, we eliminate only those atomic effects that may not result in invalid constraints.

Exotic Effects
Other relevant effects that may be modeled in ReML include non-termination, non-determinism, and IO effects.
For IO effects, we can simply add an atomic effect io and enrich each library function that perform IO by adding the io atomic effect to the latent effect of the function type.Such effects will have no natural handler but can be used to reason about whether a function is pure, for instance, in conjunction with a basic effect constraint on the form noio .
For reasoning about non-termination, we may add an atomic effect rec to the language along with a basic effect constraint on the form norec .In this way, we can reason about termination in a structural way, which may also be used to determine that a function as pure, which, again, can be used by a compiler optimiser pass.By further distinguishing between recursive and tail-recursive function calls (refine atomic effects of the form rec into the atomic effects trec and rec), we may add support in ReML for giving guarantees that a function runs in bounded space (no heap allocation and no stack allocation).Again, as for io effects, termination and boundedness effects will have no natural handler.
There are quite a few other possibilities.For instance, ATS [Chen and Xi 2005] makes it possible to distinguish between functions that are represented as closures (which have a non-empty environment) and functions that need no environment.We can model such properties with atomic effects specifying whether the function will read from a closure or not when evaluated.
It would be interesting to explore useful predicates over effects, including a purity predicate, a non-allocation predicate, and an idempotence predicate.Such predicates could potentially be used for implementing optimisations in the backend part of ReML.
One limitation of the ReML effect system is that it does not support reasoning about linear or affine use of resources, as effects are really modeled as sets of atomic effects and because a function can be annotated with any arbitrary effect (meaning that the function could potentially have this effect).While this restriction is well suited for inference and typing, a possibility for future work would be to support effects that have more substructural properties.

INTEGRATION OF REGION INFERENCE AND CONSTRAINT RESOLUTION
Formally, region inference can be presented as a substitution-based type-inference algorithm, based on Hindley-Milner's algorithm W and augmented with a mechanism for discharging effects and regions that are local to an expression (for insertion of letregion constructs).Due to the support for polymorphic recursion in regions and effects, the algorithm is split into a so-called spreading phase, which annotates allocation sites with fresh regions and function types with effects identified by fresh effect variables.A separate co-called contraction phase applies contracting substitutions (i.e., substitutions that unify effects and regions) repeatedly in order for a program to adhere to the region typing-rules.In the contraction phase, no new effect and region variables are created, thus, the algorithm is guaranteed to terminate with a region-annotated program that satisfies the region typing-rules.
ReML augments region-inference with syntax (explicit region-and effect-variables) for referring to the underlying region-and effect-variables that region inference works with.Substitution is implemented as graph-unification, with unifiable nodes being region-and effect-variables and edges representing effect membership (the implementation also represents other atomic effects, such as mut( ), put( ), and get( ) as nodes).Here is an overview of how the different ReML annotation mechanisms influence region inference, which, without annotations, is guaranteed to result in a well-typed program, provided the underlying program is a well-typed Standard ML program: (1) During the spreading phase, explicitly annotated regions (annotated using the back-tick syntax) are looked up in the environment when spreading expressions and types.The implementation complaints with a type error if unification attempts to unify two different explicit effect variables or two different explicit region variables.
(2) Explicit with declarations and support for explicit region-and effect-parameters may be used to pin region-and effect-variables to a particular scope.This pinning is implemented by introducing fresh region-and effect-variables and binding them to the explicit counterparts in the environment used for spreading subexpressions.If unification results in a violation of the pinning (that is, if region inference is forced to push the binding outwards), a type error is reported.For details about how effects are unified, we refer the reader to [Tofte and Birkedal 1998] and [Tofte and Birkedal 2000], in particular with respect to information about how to deal with so-called secondary region-and effect-variables, variables that occur in arrow effects but are not paired directly with a type constructor.
(3) Effect constraints, expressed using the while-type mechanism, have no influence on region inference, which, as we have seen, is in contrast to explicit with annotations, explicit parameter annotations, and explicit at-annotations.Instead, all effect constraints are checked after region-inference, where we can assume that all graph cycles have been collapsed.The implementation decomposes effect constraints into basic constraints that are added to the nodes of the effect-graph.When parts of the graph are copied, which happens when a type scheme is instantiated, also the constraints are copied (and instantiated).For discharging an effect, it is sufficient to check each basic constraint, under the assumption that other constraints are satisfied (cycles in the graph have been collapsed by region inference).From a formal perspective, region inference makes the assumption that the set of arrow effects occurring in a region-and effect-annotated program is consistent, meaning that the set is functional (i.e., each effect variable is associated with a unique effect), transitive (i.e., if .and ′ .′ are both in the set then ′ ∈ implies ′ ⊆ ), and closed (i.e., each effect variable occurring in the set is defined by the set) [Tofte and Birkedal 1998].Whereas this consistency property is not used for establishing soundness, it is essential for establishing termination and correctness of region inference.A central property of a contracting substitution is that when it is applied to a consistent set of arrow effects, the result is itself a consistent set of arrow effects.We see here the link to the implementation that uses unifiable graphs for representing arrow effects.Much like Hindley-Milner's algorithm W, where type substitution may be implemented by unification, region inference may be implemented using region-and effect-unification to model region-and effect-substitutions.

REML STATUS AND LARGER EXAMPLES
Whereas ReML is a fully working system featuring explicit region and effect annotations, mut, put, and get effects, and constraints such as noput, nomut, and ## constraints, current work aims at improving type error reporting with proper indication of the source of a constraint violation.The changes to the implementation have little influence on overall compilation times (if any) as overhead is introduced only in relation to managing annotations, effect constraint propagation, and constraint checking.
We have carried out experiments on larger benchmarks from [Elsman and Henriksen 2023], including the benchmarks mandelbrot, vpmsort, pmsort, and ray.Whereas the benchmark programs vpmsort and pmsort uses the par function shown in the paper, the benchmarks mandelbrot and ray uses a parfor function, which, as par, is based on the underlying spawn functionality, and which applies a function of type int → unit in parallel on an interval of integers.Using a noput constraint, the ## constraint on the used spawn function can be discharged and we get the property that the ray-tracer and Mandelbrot image creator can execute without allocation races.In the process of porting the ray benchmark, we were directed by the type system to modify the ray-tracer implementation (using an array-of-structs to struct-of-arrays transformation) to ensure that pixel values (triples of integers) are not allocated by the individual threads but rather saved in individual channel arrays.
ReML is open source and is available from the MLKit source code repository. 5Moreover, this paper comes with an artifact, which, in addition to a snapshot of the ReML source code, includes a tutorial demonstrating the features of ReML presented in this paper, and an in-depth description of the implementation aspects of ReML [Elsman 2024].ReML builds on the MLKit infrastructure and shares source code with the native MLKit backend, targeting x68-64 machine code, and SMLtoJs [Elsman 2011], which targets JavaScript engines.
The artifact is comprised by a docker image, which readily makes available the ReML compiler in terms of a reml executable.The reml executable accepts ReML programs as input and generates x86-64 machine code as a result.The artifact tutorial demonstrates how to apply reml to the examples presented in this paper and how to apply ReML to new examples.Concretely, the artifact describes the ReML parallel library and demonstrates how to compile and run the parallel Mergesort example, the parallel Mandelbrot example, and the parallel ray-tracer.

RELATED WORK
Much related to this work is the work on Cyclone [Gerakios et al. 2010;Grossman et al. 2002;Hicks et al. 2004;Swamy et al. 2006], a C-like programming language aimed at safe system-level programming (and even thread-based programming) based on regions, but with limited support for region inference and higher-order programming.Another region-based language is RC [Gay and Aiken 1998], which features support for explicit regions in C, combined with reference counting of regions (instead of relying on the stack discipline).Another related language for system-level programming is ATS [Chen and Xi 2005], which, in addition to supporting refinement types and dependent types, also allows a programmer to specify specific properties about a function, such as the property that a function has no free variables and therefore can be represented without a closure and only by its code pointer.
The region-and effect calculus by Tofte and Talpin [Tofte and Talpin 1997] can also be modeled using polymorphism and monads [Fluet and Morrisett 2004], which has inspired extensions to region-based memory management leading, for instance, to a discipline that does not follow a LIFO-lifetime of regions, and uses of region-based memory management for managing other types of resources, such as file descriptors [Kiselyov and Shan 2008].The notion of monadic regions has also contributed with techniques for reasoning about code generation in multi-stage languages [Kiselyov et al. 2016].In comparison to the techniques we present here, monadic regions are centralised around an "outlives" relationship between regions, suggesting that one region lives longer than another, and a subset relation between effects.Whereas these relations induce a notion of subtyping constraints, region inference in ReML is based on substitutions with arrow effects providing the mechanism that allows for finding region-and effect-unifiers (i.e., substitutions) that will unify two arbitrary region-and effect-annotated types, as long as their underlying ML types (i.e., after region-and effect-erasure) are identical.We shall not here argue that the one approach is better than the other, but rather emphasise that the design span here is large, which is also exemplified by the existence of two different inference algorithms for region-inference [Birkedal and Tofte 2001;Tofte and Birkedal 1998].
Besides the work on monadic regions, there has been a large body of work on providing simpler proofs of soundness for region-based systems, including [Calcagno 2001;Calcagno et al. 2002;Helsen and Thiemann 2001], and [Elsman 2023], which consider soundness in the context of augmenting region-based memory management with reference-tracing garbage collection.The present work builds on these techniques, which are all based on the strategy of syntactic soundness proofs [Morrisett 1995;Wright and Felleisen 1994].
Also related to the present work is the work on Embedded ML [Pareto 2000], which is inspired by the intermediate region-based program representation in the MLKit.Contrary to ReML, Embedded ML aims at more precise reasoning about the sizes of allocated regions, while requiring the programmer to be explicit about regions (no region inference fallback).Related to this work involves work on optimising the representation of regions, including distinguishing between regions that may be allocated directly on the stack and regions that are heap-allocated as a linked list of pages [Birkedal et al. 1996].Region inference, as it is implemented in the MLKit (and ReML), may be combined with garbage collection [Elsman 2023;Elsman and Hallenberg 2021;Hallenberg et al. 2002].This area of work is complementary to the present work, which may be used with and without reference-tracing garbage collection.
A dominant language that supports explicit programming with region-based memory is Rust [Jung et al. 2017;Klabnik and Nichols 2018], which owns much of its design to the earlier work on Cyclone, RC, previous work on region-based memory management, and capability-based memory management [Walker et al. 2000] and ownership types [Jung et al. 2017].
Another area of related work is the bulk of recent work on programming languages based on effect handlers, including Eff [Bauer and Pretnar 2015], Koka [Leijen 2014], Effekt [Brachthäuser et al. 2020b], and System C [Brachthäuser et al. 2022, 2020a].Here both Effect, which also features an embedding in Scala, and System C are based on the notion of capabilities.
Yet another body of related work is the work on refinement types for refining the set of values being specified by a type.Such work includes the seminal work on refinement types for ML [Freeman and Pfenning 1991] and newer work on refinement types in Liquid Haskell [Vazou et al. 2014].We consider combining the type-and effect-system of ReML with a more traditional refinement type system a good candidate for future work.
One of the motivating examples of the present work is efficient support for parallelism.The present work uses much of the infrastructure presented in [Elsman and Henriksen 2023] for parallelism support in ReML, with the difference that in ReML, protection against allocation races can be ensured statically with the use of while-types.In contrasts, the constraint-based protectioninference algorithm, presented in [Elsman and Henriksen 2023], aims at distinguishing regions that require protection (e.g., using mutexes) from those that do not.With the lack of explicit constraints provided by the programmer, protection inference is fragile to small program changes and gives the programmer only limited control of the desired runtime behavior.Technically, protection inference infers, for each allocated region, whether a mutex should be associated with the region at runtime.Effect constraints in ReML and protection inference are complementary concepts in the sense that one mechanism does not necessarily preclude the other.It is perfectly fine to have two different versions of the par function around, one that uses protection inference and dynamic features, such as mutexes, to preclude allocation races, and another, that uses ReML effect constraints to give static guarantees about the avoidance of allocation races.
Much related work has investigated the possibilities for adding support for shared-memory parallel OS threads (and light-weight threads) in ML-like languages, such as OCaml [Sivaramakrishnan et al. 2020], Standard ML [Cooper and Morrisett 1990;Westrick et al. 2019], and Manticore [Farvardin and Reppy 2020;Fluet et al. 2008].In all cases, the implementations require special attention to the garbage collection techniques used, in particular with respect to mutable effects.In the case of MPL [Westrick et al. 2019], which adds shared-memory fork-join parallelism to the MLton Standard ML compiler through a simple par-function, the memory discipline is centered around so-called disentangled heaps, for which each thread is associated with an individual heap and where pointers between heaps can only point upwards towards the root [Raghunathan et al. 2016].The disentangled-heap property, which is required by MPL and its parallel garbage collection of leaf heaps, can be enforced automatically by a combination of static and dynamic techniques [Westrick et al. 2022].If a thread assigns to an object allocated by a sibling, the object will be allocated in a memory region allocated by a common ancestor.
Finally, a large area of related work includes work on qualified types [Jones 1994], including qualifier inference for C [Foster et al. 2002], implementation of Haskell type classes [Hall et al. 1996;Peterson and Jones 1993], elimination of polymorphic equality [Elsman 1998], and type inference with constraints, in general [Odersky et al. 1999;Sulzmann and Hudak 2000].Whereas previous work shed important light on particular aspects of constraint inference and constraint abstraction, the present work differs from previous work by involving constraints on effects that are treated as separate objects in judgments.Much of the previous work on constraint solving, however, may show to carry over to the setting of effect constraints.
Another effect system based on boolean unification and constraints, is implemented in Flix.This system allows for tracking complementary effects, expressing, for instance, that an expression does not raise a particular exception [Lutze et al. 2023].In Flix, however, it is not possible to express that two effects are disjoint.

CONCLUSION AND FUTURE WORK
We have presented ReML, a higher-order statically-typed functional language that allows programmers to be explicit about the effects performed by program code, including effects related to memory management.While work on ReML is ongoing, the current implementation is freely available, as described in Section 8.
There are a number of directions for future work.First, by integrating region inference and exception inference, exceptions that can be inferred to live locally may be allocated in local regions.Currently, exceptions that carry payloads are allocated in global regions to ensure that carried values are still allocated at potential handler-sites.Second, enriched effect information may open up for compiler optimisations that are otherwise difficult to implement.Such optimisations may involve reasoning about whether a function is pure (code elimination) or whether an effect is idempotent (code duplication).
With explicit annotations in ReML, it may be possible to augment region-and effect-inference with the possibility of higher-order region-and effect-polymorphism, existential regions [Henglein et al. 2001], and even first-class regions.We consider such developments good possibilities for future work.
We emphasise here that ReML is work-in-progress and that the integration of exception effects in the ReML effect system is under active development.That said, even in its current state, ReML can be used to reason locally about memory aspects of library functionality and for establishing guarantees about the lack of allocation races in parallel programs.We also consider it future work to investigate generalisations in terms of more general effect constraints (e.g., supporting set operations) and user-defined effects.

DATA AVAILABILITY STATEMENT
This paper is accompanied by a software artifact [Elsman 2024] that demonstrates the main contributions of the paper.A more detailed description of the content of the software artifact is available in Section 8.
Fig. 1.Parallel Mergesort on Lists.Version (a) is without region annotations and version (b) is explicitly annotated with regions.
as required.The case for [E-NormR] follows similarly as the case for [E-NormL].The cases for [E-Emp], [E-Nil], and [E-Swap] are straightforward.□