Modular Denotational Semantics for Effects with Guarded Interaction Trees

We present guarded interaction trees -- a structure and a fully formalized framework for representing higher-order computations with higher-order effects in Coq, inspired by domain theory and the recently proposed interaction trees. We also present an accompanying separation logic for reasoning about guarded interaction trees. To demonstrate that guarded interaction trees provide a convenient domain for interpreting higher-order languages with effects, we define an interpretation of a PCF-like language with effects and show that this interpretation is sound and computationally adequate; we prove the latter using a logical relation defined using the separation logic. Guarded interaction trees also allow us to combine different effects and reason about them modularly. To illustrate this point, we give a modular proof of type soundness of cross-language interactions for safe interoperability of different higher-order languages with different effects. All results in the paper are formalized in Coq using the Iris logic over guarded type theory.


INTRODUCTION
Interaction trees [Xia et al. 2019] are a recently proposed formalism for representing and reasoning about (possibly) non-terminating programs with side effects in Coq (a terminating type theory without effects).Since its inception, interaction trees have been applied, including but not limited, to specifying and verifying network servers [Koh et al. 2019;Zhang et al. 2021], semantics of LLVM [Zakowski et al. 2021], semantics of a language for robotics [Ye et al. 2022], non-interference [Silver et al. 2023], and verification of concurrent objects with transactional memory [Lesani et al. 2022].
The introduction of interaction trees was motivated by a desire to simplify mechanized formalizations of interactive, effectful, non-terminating computations and the developers of the ITrees library argued that ITrees can represent computations in a way which is more modular than representations based on operational semantics and executable (in contrast to earlier representations based on traces represented as predicates on events).In particular, the idea is that interaction trees can be used to give denotational semantics to programming languages and thus allow one to abstract away from syntactic details and reuse meta-language features such as function composition so as to obtain more robust mechanizations.And, indeed, the applications mentioned above demonstrate that interaction trees work well for giving semantics to first-order programming languages with first-order effects.
The challenge we address in this paper is that interaction trees cannot easily be used as a model of higher-order programming languages with higher-order effects, which, of course, limits the applicability of interaction trees.Indeed, the ease of use of interaction trees is enabled, in part, by two restrictions imposed on the computations represented by the interaction trees: the computations must be first-order, and the effects that the computation performs must be first-order as well.With those restrictions, the type of interaction trees forms a monad, which allows one to compose the represented computations and reason about them modularly.(In principle, one could represent higher-order computations by means of closures in interaction trees, but that would

IRIS LOGIC OVER GUARDED TYPE THEORY
In this section we describe the Iris logic, in which we shall define and work with guarded interaction trees.Our treatment is brief since Iris has been described in many other papers and we are just using a small extension of the usual presentation; we refer the reader to the literature on Iris [Jung et al. 2018] and guarded type theory [Birkedal et al. 2012] for more details.
Iris is usually presented as a separation logic over a simple type theory.The model of Iris, however, models a richer type theory and in this paper we are going make use of that and consider Iris over a guarded type theory with (1) a modicum of dependent type theory, and (2) the ability to define guarded recursive types.Both of these features are supported by the existing Coq implementation of Iris and the associated Iris proof mode [Krebbers et al. 2017b].
Note that since we are working formally in Iris in Coq, there are two logical levels at play: the statements and proofs at the Coq level (which we refer to as the meta-logic level, or as the meta level), and the statements and proofs at the level of Iris (which we refer to as the logic level).
We recall the grammar of Iris in Figure 1; the syntax consists of types, terms, and propositions.Most of the grammar is standard for higher-order intuitionistic logic, with the parts related to guarded recursion highlighted in blue.As usual in higher-order logic, we have a well-typedness judgment  1 :  1 , . . .,   :   ⊢  :  stating that the term  has type , under the assumption that the variables   have types   .In the grammar for types, I ranges over so-called discrete types, which are meta-level types embedded into the types of Iris.Note that types include dependent types over discrete types.While we have not shown it in the grammar, we can also form types as solutions to guarded recursive domain equations, i.e., type equations where the recursive occurence of the type being defined is guarded under the ▶ type modality.Such types are defined up to isomorphism; we will see an example shortly: the type of guarded interaction trees will be such a recursive type and will be introduced in the following section.A useful semantic intuition for the types of Iris is that they denote (certain kinds of) time-indexed sets, i.e., families of sets indexed over natural numbers.At time step  > 0, the later type ▶ consists of the elements of  at a later time step, i.e., at  − 1.At time step  = 0, the type ▶ is a singleton set.Intuitively, guarded recursive types exist because to understand what a guarded recusive type is at time step , one only needs to understand what it is at  − 1, since the recursion is guarded.Elements of ▶ can be constructed from elements of , using the next constructor, and we can form fixed points for guarded endo-functions: The ▶ type former is functorial and we write ▶ : ▶ 1 → ▶ 2 for its action on terms  :  1 →  2 .
For propositions  : iProp we also have the provability judgment Γ |  ⊢  stating that  is derivable from  in the typing context Γ =  1 :  1 , . . .,   :   .The rules corresponding to the intuitionistic fragment are standard.Here we present the rules concerning the guarded part of the logic.
On the level of propositions, we have a(nother) later modality ⊲.This is the later modality most users of Iris are already familiar with.It is related to the later modality on types in that ⊲( =   ′ ) ⊣⊢ next() = ▶ next( ′ ).We recall that ⊲ can be used to define guarded recursive predicates and and that it supports reasoning via Löb induction: Γ ⊢  : iProp Iris also includes separation logic connectives; we recall those later, when we need them, in Section 6.
If  is a proposition that consists only of intuitionistic logic connectives without ⊲, then we can interpret it both as a meta-level proposition (i.e. a Coq proposition), and as an Iris proposition.For such propositions we have the following result, connecting Iris with the meta-level: Theorem 2.1 (Iris Adeqacy).Let  be a proposition containing only intuitionistic connectives.Then, if True ⊢  is derivable in Iris, then  also holds at the meta-level.

GUARDED INTERACTION TREES
The type of guarded interaction trees (or GITrees for short) IT  () is defined for a ground type  and an effect signature , as we explain below.In Figure 2 IT  () is written down as a guarded datatype. 1The first constructor Ret says that any element  of the ground type  can be associated with a "terminated" guarded interaction tree Ret().The second constructor Fun says that functions are also guarded interaction trees, and it is this constructor that allows us to model higher-order computations.Since the function constructor contains a negative occurrence of IT  () in its argument, we must put it under a ▶.The third constructor Err represents an error state, or a stuck computation, which we take from some predefined set Error of errors.We assume that it contains at least one element RunTime ∈ Error representing a generic run-time error.The fourth constructor Tau denotes a delayed computation, or a computation that is available "later".We also write Tick : IT  () → IT  () for the composition Tau • next.Then the term Tick() represents a guarded interaction tree that takes a silent step to .It satisfies the following rule for equality: Finally, the last constructor Vis allows us to model effects.The possible effects are given by the signature  = (I, Ins − , Outs − ), where I is an indexing set on the meta-level (i.e. a set of operation names), and Ins and Outs are functors determining the arities of the operations.That is Ins i , Outs i : Type → Type for i ∈ I.The Type argument in Ins i and Outs i is instantiated with IT  itself, and is used for giving signatures to higher-order effects.With this, the first argument to Vis i is then the input for the operation, and the second parameter is a continuation which, given an arbitrary output of the operations, produces the remainder of the computation.One way to visualize this is to think of Vis i as a node in the tree, with the annotation Ins i and having Outs i many branches.
We refer to guarded interaction trees Ret() and Fun( ) as GITree values, and write IT   () ⊆ IT  () for the set of values.When quantifying over an indexing set, we implicitly coerce  to I, i.e. we write i ∈  to mean i ∈ .I.Similarly we write Ins for .Ins and Outs for .Outs when the signature  is clear from the context.When the signature  is obvious or unimportant we simply write IT() for IT  () and IT  () for IT   ().Let us demonstrate the syntax of guarded interaction trees with some running examples of effects.
Example 3.1 (Input/output on a tape).Suppose we want to model two effectful operations, for reading a number from STDIN and for writing an output on STDOUT.We will model them as guarded interaction trees IT   (1 + Nat), where 1 = {()} is the unit type and We write Input and Output() for the GITrees Here we use inl(()) : 1 + Nat as a "dummy" value, since we do not care about the return value of Output.
The operations Input and Output above are represented as GITrees IT   (1 + Nat).However, the exact ground type is not important, as long as it contains the unit 1 and the natural numbers Nat.As such, we assume that we can write down operations like Input and Output as GITrees IT   () where  ≃ 1 + Nat +  for some type .We return again to this point in Section 8, but for now we assume that we always pick a ground type  that is "large enough" to represent all the ground values that we need.
Example 3.2 (Higher-order store).We can model higher-order store with the following signature.
where Loc is a countable type of locations/pointers.We write Alloc, Read, Write, and Dealloc for the following GITrees: Here we assume that the ground type  is isomorphic to 1 +  for some , with the injection inj : 1 → .

Recursion Principle for Guarded Interaction Trees
In order to write programs that eliminate GITrees, i.e. programs of type IT  () → , we need to come up with a suitable recursion principle.Recursion principles for inductive datatypes usually follow from the initiality principles of the defined datatypes.However, the type of GITrees is not purely inductive, as it has mixed-variance recursive occurrences, and the corresponding recursion principle should reflect that.To understand the necessary recursion principle we need to understand first how the GITrees are defined.The definition at the beginning of this section presents GITrees as a guarded datatype, but how should such a datatype be constructed?In the type theory, the type IT  () is given as the solution to the following guarded equation: The isomorphism is witnessed by the pair of functions (unfold, fold), and the constructors we presented at the beginning of the section are compositions of injections and fold.Since Equation (1) contains recursive occurrences with mixed variance, we cannot use the usual recursion principle for inductive data type.Instead, we employ a mixed initial-algebra/final-coalgebra principle, following [Freyd 1991;Pitts 1996].To understand it better, we first write out the bi-functor, where the fixed point corresponds to the type of GITrees: 2 Here the bi-functor  (−, −) is contravariant in the first argument and covariant in the second one.
The bi-algebra corresponding to the type of GITrees is given by the (fold, unfold) pair: 2 For a detailed category-theoretic treatment, see [Birkedal et al. 2010].
where we write IT as a shorthand for IT  ().The recursion principle that we are looking for then states that this bi-algebra is both initial and terminal.Recursion from iteration on inductive types.Let us then look at how to solve the issue of defining primitive recursive functions on inductive types using initiality.Suppose the function  : N →  that we want to construct is defined by equations  (0) =  1 and  ( + 1) =  2 (,  ()).Then we can obtain this function  using the following trick: instead of eliminating N into  using initiality, we eliminate it into N × , in such a way that the induced map N → N ×  is the identity on the first component.More concretely, suppose we have maps  1 : 1 →  and  2 : N ×  → , forming together the equations for primitive recursion.Then we construct an N-algebra over N ×  as where  : N → N is the successor function.The initiality of N will then induce the unique map  : N → N × , which, when composed with projection N ×  → , determines the recursive function given by the clauses  1 and  2 .
Recursion/corecursion from mixed-variance types.Dually, for coinductive datatypes we can obtain a form of primitive coinduction from coiteration by using coproducts.In our case we have a datatype with mixed variance, and we use coproducts for the negative occurrences and products for the positive ones.That is, in order to eliminate IT into a type  we will assume an unfolding  →  (, ), and a folding  (IT + , IT × ) → .More concretely: The resulting maps (ℎ, ) will then satisfy the following computational rules: • plus equations for ℎ.
(Recall that we write ▶ : ▶ → ▶ for a function  :  → .) This recursion/corecursion principle is constructed using guarded recursion, and can be used to define a large variety of combinators.For example, we can write a generalization of the aforementioned function  that returns its argument, if the argument is a function, and returns an error otherwise.
In the next section we will see how to use get_fun to define an application function  •  for applying a GITree function  to a GITree argument .
In the rest of the paper we will define other operations on GITrees using just the computational rules, with the understanding that we can write down the explicit recursor for any such set of equations.Interested readers are referred to the Coq formalization for the details.

Programming with GITrees
Using the recursion principle we can define operations on GITrees that correspond to common programming constructs.For example, with get_fun we can write a function App  (, ), which applies  to  if  is a function, and returns Err(RunTime) otherwise.
This operation gives us "call-by-name" application, in the sense that it satisfies App  (Fun(next()), ) = Tick(()) for  : IT  () → IT  () for any argument .In particular, it invokes the underlying function  even if the argument  is a Tick or an effect, without evaluating the argument first.In order to define a "call-by-value" application, we compose App  with the following operation.
The One can see that there are common properties for the computational rules between get_fun(−,  ), App  (−, ),  • −, and − •   (where   ∈ IT   ()): they all preserve ticks, effects, and errors.Functions that have these preservation properties are called homomorphisms of GITrees and will play an important role in later sections.
Definition 3.4.A function  : IT  () → IT  () is a homomorphism, written as  ∈ Hom, if it satisfies the following equations: •  (Err()) = Err(); As expected from the name, the identity function is a homomorphism and composition of two homomorphisms is a homomorphism.This notion of homomorphism is inspired by the one in [Hoshino 2012].It follows from the definition, that in order to define a homomorphism it suffices to define its action on GITree values.
Programming with GITrees and natural numbers.In the remainder of this paper we work with a lot of examples involving programming with natural numbers (as an illustrative ground type).It is then useful to assume in the remainder of this paper that the ground type  in any type IT  () of GITrees is "large enough" to contain natural numbers, and the unit type.That is, we assume that  ≃ 1 + Nat + . . ., and we simply write Ret() for Ret(inj()) and Ret(()) for Ret(inj ′ (())) (for appropriate injections inj : Nat →  and inj ′ : 1 → ).We will also abbreviate IT  () as IT or IT  when  is generic as above or is clear from the context.
In Figure 3 we summarize the operations on GITrees that we define using recursion and other functions.The computational rules described in Figure 3 are only for the base cases; the other computational rules follow from the fact that those operations are homomorphisms.Concretely, we have the following operations.The get_nat function extracts a natural number from a GITree and applies the function  : Nat → IT to it.It is a homomorphism in the first argument.If it encounters a function Fun() or a different ground value Ret(), then it returns an error.The If operations test whether the first argument is zero, and picks the appropriate branch.The function If(−,  1 ,  2 ) is a homomorphism.Similarly, if the first argument is not a natural number then If returns an error.The NatOp  operation applies the binary function  to its integer arguments, returning an error on all the other values.The maps NatOp  (, −) and NatOp  (−,   ) are homomorphisms for   ∈ IT  .The  ;  is a sequencing operation: it puts all the effects and ticks in  before the effects and ticks in .This is witnessed by the fact that (−) ;  is a homomorphism.The While  do  represents a while loop with the conditional  and the body ; it is defined using guarded recursion, and is equal to its one-step unfolding using the If construct.
Let us look at some example programs that we can write using the operations we have defined.
Example 3.5 (Factorial).In the first example, we have a factorial function that we implement using the store operations (Example 3. The program factBody computes the factorial of the number stored in the location ℓ using an intermediate location a for the accumulated result.The complete program fact then allocates the needed references and runs factBody before reading off the result from the location a. Example 3.6 (Encoding of pairs).Our definition of GITrees does not include arbitrary algebraic datatypes, like pairs or sums.We can, however, encode them using a Church-style encoding.We write (, ) : IT for the guarded interaction tree Note that (  ,   ) is a GITree value whenever   and   are.Furthermore, (, −) and (−,   ) are homomorphisms.We then define the projection functions as The projection functions then satisfy the following computational rules: We can use similar style encodings to represent other algebraic datatypes as guarded interaction trees.

REIFICATION OF EFFECTS AND REDUCTIONS OF GITREES
GITrees allow us to conveniently write down and combine various effects.But in order to reason about the effects we also need a way of giving them meaning.In this section we establish a way of reifying effects of GITrees and use reification to define reductions of GITrees, which explain how computations represented by GITrees reduce.
In order to interpret stateful effects we assume that we have a type State, and each effect is interpreted using the state monad with a function: We call a tuple (, State,  ) a reifier for the effects .Assuming we have such a reifier, we write a function reify : Example 4.1 (Reification for the input/output operations Example 3.1).We take the state State to be a pair of two lists of natural numbers, corresponding to input and output tapes.The reifier is defined as Example 4.2 (Reification for the higher-order store operations Example 3.2).For higher-order store we take State to be the type of finite partial maps Loc fin − ⇀ ▶IT.The reifier function is defined in the expected way: where ℓ is the smallest location not present in From reification to reductions.Using the reification function we can formulate the reduction relation on interaction trees.The internal reduction relation ⇝: Intuitively, a reduction of GITrees corresponds to either stripping away one computational step, or to reifying an effect.We consider an (annotated) transitive closure of the reduction relation: We write ⇝ * for the reflexive transitive closure of the reduction relation.
Continuation-independent reifiers.The reifiers that we consider here produce an output based on the input, but do not have direct access to the continuation.The reification function reify just calls the continuation with the produced output.This continuation-independence is crucial for proving Lemma 4.4 (and the associated rule wp-hom in separation logic in Section 6).Not all effects are continuation-independent, for example call/cc cannot be implemented this way.In this paper, just like in [Xia et al. 2019], we stick to working with continuation-independent effects, as it simplifies the separation logic and the reasoning principles, and we defer studying continuation-dependent effects to future work.

MODELING A HIGHER-ORDER EFFECTFUL PROGRAMMING LANGUAGE
In this section we show how guarded interaction trees provide a model for a programming language with recursion, higher-order functions, and effects.Specifically, we study a PCF-like higher-order programming language with input/output effects, give its interpretation into IT  (see Examples 3.1 and 4.1), and show its soundness, i.e., that the interpretation agrees with the operational semantics.The same approach applies to other classes of effects for which you can write operational semantics.
Syntax and operational semantics.The syntax for the programming language, which we dub  rec,io , consists of values and expressions: where  ranges over the set of natural numbers, and  ,  range over the set Var of variables.
The operational semantics for  rec,io is given in Figure 4 as a small-step reduction relation on the configurations Expr × State, where State is a pair of lists as in Example 4.1.The reductions are defined, following [Felleisen and Hieb 1992], using evaluation contexts  ∈ Ectx, given as: By  [] we denote the result of replacing the hole [ • ] in the context  with the expression .The evaluation contexts ensure the call-by-value right-to-left evaluation order of  rec,io , as having a predefined evaluation order is important in the presence of effects.
Interpretation in guarded interaction trees.We will interpret a closed program  as an interaction tree  : IT io ().The effects io are those of Examples 3.1 and 4.1, and we assume that the ground type  is "large enough" to have natural numbers.For convenience, we drop the ground type and write simply IT io for IT io ().
In order to provide a (compositional) denotational semantics we need to provide an interpretation not only for closed terms, but for open terms as well.Given a set fv() = { 1 , . . .,   } of free variables of , we define the interpretation   : IT io , where  maps the free variables of  to interaction trees.The interpretation function is defined in Figure 5.The definition follows the standard notion of semantics for (untyped) -calculus, adjusted for effects and explicit recursion.The interpretation of recursive functions rec  () =  is defined using the guarded fixed pointed operation fix IT : (▶IT → IT) → IT, and satisfies the following equality: rec We show that the interpretation is sound: We prove Theorem 5.1 by induction on the →-derivation.The most interesting cases are for the reductions red-beta and red-ectx, which we now sketch.For the former, we need a substitution lemma, and for the latter we need to extend the interpretation to evaluation contexts.Lemma 5.2 (Substitution lemma).For any expression  with a free variable  we have Proof.By induction on , using Löb induction in the case of recursive functions.□ In order to handle red-ectx we provide the following auxiliary interpretation for evaluation contexts.Each evaluation context  is interpreted as a homomorphism   : IT → IT, such that  []  =   (   ), which together with Lemma 4.3 implies the soundness of the red-ectx reduction.

SEPARATION LOGIC OVER GITREES
In this section we define a separation logic as a program logic for guarded interaction trees.We define a proposition wp  Φ to denote that an interaction tree  is safe to reduce, and if  reduces to an interaction tree value   , then   satisfies the postcondition Φ : IT  → iProp.
In this section we make use of the separation logic connectives of Iris, which we recall here: For brevity, we only briefly recall the intuitive reading of these propositions and refer to [Jung et al. 2018] for details.The proposition  *  says that the propositions  and  hold over disjoint resources; the proposition  − *  says that if we were to add any resources which satisfy , then  would be satisfied.The proposition | ⇛ says that the current resources can be updated to satisfy .
The proposition □ states that  holds persistently, i.e., without asserting any resources.Crucially, such propositions can be duplicated: □ ⊢ □ * □.An example of a persistent proposition is the invariant proposition  , which satisfies  ⊢ □ .
Selected rules for the weakest precondition proposition wp  Φ are given in Figure 6.The rule wp-val states that to verify a value it suffices to check that the value satisfies the postcondition.The rule wp-tick states that in order to verify Tick() it suffices to verify , under a later ⊲.The rule wp-hom states that in order to verify  () for a homomorphism  , it suffices to reduce  to a value   , and then verify  (  ).
The rule wp-reify tells us how to deal with effects.The rule uses the proposition has_state() which signifies the exclusive ownership of the current state .The use of separation logic is crucial in this case, as we do not want to allow duplicating that proposition.The rule then states that in order to verify an effect, one has to provide the current state  ′ and the proof that the interaction tree with the effect reifies into some Tick().Then, the user has to verify that the resulting  reduces to a value satisfying the postcondition, under the assumption that the state has been updated to  ′ .
The rule wp-upd states that one can update ghost resources before and after reducing .The rule wp-mono states that one can always weaken the postcondition in wp  Φ .Finally, wp-lam is an example of a derived rule.It combines the computational rule for function application of GITrees, and rules wp-hom and wp-tick.Let us look at an example derivation using these rules.
By calculation, NatOp + (Ret(), Ret(1)) = Ret( + 1), which is also a GITree value.We can then apply wp-val again to reduce the goal to Φ(Ret( + 1)), which follows from the assumption.□ We define the weakest precondition as a guarded recursive predicate, as is standard in Iris.The weakest precondition then satisfies the following adequacy and safety theorem, the proof of which relies on the adequacy of Iris (Theorem 2.1).Theorem 6.2.Let  be an interaction tree and  be a state such that has_state() ⊢ wp  Φ is derivable for some meta-level predicate Φ (containing only intuitionistic logic connectives).Then for any  and  ′ such that (, ) ⇝ * (,  ′ ), one of the following two things hold: • (adequacy) either  ∈ IT  , and Φ() holds in the meta-logic; • (safety) or there are  1 and  1 such that (,  ′ ) ⇝ ( 1 ,  1 ) In particular, safety implies that  ≠ Err() for any error  ∈ Error. 3inally, it is worth noting that separation logic/Iris is useful for reasoning about higher-order GITrees even in the absence of effects, as demonstrated by the following example.
Example 6.3.Using guarded recursion, we can write down a GITree Iter that satisfies the equation: That is, Iter •  • Ret() •  computes the iterated application   • .We can give Iter the following higher-order specification: The specification says that if  initially satisfies Ψ and  preserves Ψ , then Iter •  • Ret() •  will also satisfy Ψ.The second premise is the specification of  , and it can be used multiple times in the proof.For that reason the that premise is behind the persistently modality □.
It is also worth noting that while Iter itself does not use state, the function  that we supply to it might as well use all sorts of effects internally, and our implementation and specification of Iter is oblivious to that.

Domain-Specific Logic for Higher-Order Store
Now we show how we can use the standard mechanisms in Iris to recover a fairly standard-looking separation logic for a programming language with references from the weakest precondition calculus presented above.We use Iris's notion of higher-order ghost state [Jung et al. 2016[Jung et al. , 2018] ] to provide the following logical interface for the higher-order store operations: Here heap_ctx is a persistent proposition, which is part of the logical interface.
Thus our goal is to provide definitions of heap_ctx and ℓ ↦ →  that allow us to derive the rules above.The main challenge is that the resource has_state() provides a singular complete view of the state, without the ability to split it into local portions corresponding to individual locations.That has_state is not splittable by itself is not surprising -it is an abstract representation of an arbitrary state for arbitrary effects, and there is no a priori way of splitting it.However, for our specific state (a heap Loc fin − ⇀ ▶IT) we know how to do the splitting.What we need to do is to provide an alternative view of the state, amenable to splitting, and tie it together with the actual state of the has_state predicate.
Our first step is then to provide a resource algebra for this view of the state.Following the standard practice of Iris, we use an authoritative resource algebra of the heap.It contains two kinds of resources: the "full heap" •  and the "fragmental heap" •  ′ .The fragmental heap is guaranteed to be a subheap of the full one  ′ ⊆ .Then we make the following definitions: The heap_ctx predicate is an invariant that says that the full view of the heap coincides with the actual state that we have as part of has_state, and the points-to predicate ℓ ↦ →  states that [ℓ ↦ → next()] is in the fragmental view of the heap.Together those predicates imply that the actual state  maps ℓ to next(), which is precisely what allows us to deduce the rules wp-alloc, wp-read and wp-write from the rule wp-reify.
As a simple example, we can use the rules for the store operations to verify the factorial program from Example 3.5.We show the following specification: wp fact()   .  = Ret(!) .For this, we will use the intermediate lemma: Lemma 6.4.Under the assumptions heap_ctx, a ↦ → Ret() and ℓ ↦ → Ret(), we have wp factBody(a, ℓ) _. a ↦ → Ret(×!) .
Proof.We proceed by allocating the locations a and ℓ symbolically using wp-alloc, and then appeal to Lemma 6.4.□ As one can see, the logic that we recovered for the higher-order store effects is very close to a normal separation logic one would normally see for a programming language with a heap [Jung et al. 2018].Our logic, however, is amenable to extensions with other effects and programming language constructs.Indeed, we explain how to obtain a logic for reasoning about different combined effects in Section 8.In the next section we show how to apply the separation logic to show computational adequacy of the model of  rec,io .
Typing rules for  rec,io .

COMPUTATIONAL ADEQUACY FOR 𝜆 rec,io
In Section 5 we constructed a compositional model of  rec,io in guarded interaction trees and proved that it is sound: if a  rec,io program  terminates to a natural number , then  terminates to Ret().In this section we show the other direction, known as computational adequacy in domain theory [Plotkin 1977], for the well-typed fragment of  rec,io ; the typing relation (Γ ⊢  : ) is given in Figure 7. Computational adequacy is formally stated as the following theorem: Theorem 7.1 (Adeqacy).If ⊢  : Nat and (  , ) ⇝ * (Ret(),  ′ ) then (, ) → * (,  ′ ).
Computational adequacy is usually proved using logical relations between the syntax (terms of  rec,io in our case) and semantics (guarded interaction trees in our case).Here we follow the recent practice [Krebbers et al. 2017b] of using the separation logic (see Section 6) to define our logical relations model.
We define a logical relation Γ |=  ≾  : , relating a guarded interaction tree  and an expression .Here,  is an open expression for which we have Γ ⊢  :  while  is "an open interaction tree", i.e., a function of type (fv(Γ) → IT) → IT.As usual, we first define the relation over closed GITrees and expressions, and then generalize it to the open case.The logical relation, given in Figure 8, is, as usual for call-by-value languages, is decomposed into an expression relation E  and a value relation V  .The expression relation simply states that related expressions should produce related values.The value relation is defined by induction on the type in the standard way: values of base types should be equal while functions take related values to related expressions.As values, once computed, can be used multiple times (cf. the logical relation in Section 9.1) the value relation is required to be persistent; hence the persistently modality □ in the value relation for functions.In order to define the relation on open terms we define a relation for typing contexts V * Γ which relates, point-wise, two substitutions respectively of the types fv(Γ) → IT  and fv(Γ) → Val.

MODULAR REASONING ABOUT COMBINATIONS OF EFFECTS
Because (guarded) interaction trees define effects abstractly, one of the main advantages is the ability to combine programs with different effects modularly in the same setting.In this section we demonstrate how we achieve this for guarded interaction trees.Given two signatures  and  , with indexing sets  and  , we say that  is a subsignature of  , written as  ↣  , if there is a mapping  :  →  such that .Ins  ( ) ≃  .Ins  ( ) ( ) and .Outs  ( ) ≃  .Outs  ( ) ( ) for any  ∈  and for any type  .Here, ≃ stands for isomorphism of types.
In regular interaction trees, a subsignature  ↣  induces an embedding IT  → IT  of interaction trees.However, such an embedding is not possible for guarded interaction trees due to the mixed-variance definition: a function IT  → IT  cannot be converted to a function IT  → IT  which takes a guarded interaction tree with a larger set of effects.
To achieve modularity we will instead work with an open-ended collection of effects which is large enough to embed all the effects that we need.It is only at the "top-level", e.g., when applying the adequacy theorem, that we pick a concrete signature of effects.For example, the precise type of the Alloc function from Example 3.2 is the following type, for any  such that  store ↣  : Alloc : IT  × (Loc → IT  ) → IT  (In practice, the function Alloc is polymorphic in  at the meta-level, i.e., in Coq.)With this, we can easily combine two programs with different collections of effects, assuming both of the programs are written in such an open ended manner; we just need to pick  to be large enough to embed the effects of both programs.For example, we can combine the factorial implementation from Example 3.5 with input/output effects, to write a program that takes a natural number from the input, computes its factorial, and prints the result to the output: fact_io ≜ get_nat(get_nat(Input, fact), Output).
The resulting program fact_io has type IT  for any  such that  store ↣  and  io ↣  .
Reifiers for modular effects.Writing down programs with modular combinations of effects is not enough by itself: we also want to reason about the reification of effects modularly.Suppose we write a program with effects  as an GITree IT  with  ↣  , and suppose that we have a reifier for .Recall that we defined a reifier for the effects  to be a tuple (, State,  : i∈ Ins i (▶IT  ) ×State → option(Outs i (▶IT  ) ×State).However, if the state itself includes interaction trees, as in Example 4.2, we need also to make the state and the reifier parametric in the effects.Therefore, instead of a fixed type State we consider a family of states State( ), and instead a single reifier function  we consider a family of functions In practice, we assume that the global state is the product of states of reifiers for sub-effects, in which each sub-effect acts only on its own part of the state.Concretely, given a sequence Then to write down the abstractions for the domain-specific logic in Section 6.1 we change the heap_ctx definition to link together only the state corresponding to the specific effects: where the higher-order store reifier is the  th subreifier of ì .
Example 8.1.Recall the program fact_io from the beginning of this section.We use Proposition 6.5 to show the following specification: where has_state  tracks the state of the input/output effects.The specification tells us that if we run fact_io with the starting state ( ì , ì ) for the input/output effects, then we end up with the state ( ì , (!) ì ) for the input/output effects.

Modular reasoning with a generic ground type
As we have mentioned in Section 3.2, we often would like to work with the GITrees IT  () for some generic ground type  that is "large enough" to contain ground values that we need to represent (e.g. the unit type, natural numbers, the type of locations, etc).That is, we assume that the ground type  is isomorphic to a sum 1 + Nat + Loc + . . ., depending on ground values we need.We tackle this generic ground type in a similar way we deal with different effect signatures modularly.Specifically, we write  ⇁  if  ≃  +  for some type .We then have the generic return constructor Ret :  → IT  () for any  ⇁ .Similarly, we have a generic "destructor" get_ret : IT  () × ( → IT  ()) → IT  () which allows us to extract a ground value of type , as long as we have  ⇁ .such that get_ret is a homomorphism in the first argument, which satisfies: Then, the get_nat function from Section 3.2 is just the specialization of get_ret to the situation Nat ⇁ .
The predicate  ⇁  is formalized in Coq as a typeclass, making it easy to use the generic operations like Ret and get_ret.In the remainder of this paper we will stick to those generalized operations, and will assume that the ground type  contains all the ground values we need.

TYPE SAFETY FOR CROSS-LANGUAGE INTEROPERABILITY
One of the advantages of using GITrees for denotational semantics is that it provides a common setting for interpreting and reasoning about different languages with different effects, and then combining the results in a modular manner.In this section we demonstrate this point by verifying type safety of interoperability between two different languages.The interoperability is achieved by allowing embeddings from one language into another at a particular boundary [Matthews and Findler 2007].We take inspiration for this case study from the approach of Patterson et al. [2022], who consider interoperability of different languages at a level of a common third language, which both the source languages are compiled down to.The communication between the source languages is done through glue code at the level of the target language, which converts types from one language to another.The type safety result then states that well-typed programs in a combined language can only go wrong due to conversion errors at the boundaries.
Specifically, we first consider an affine programming language  ⊸,ref with linear references and strong updates, which we interpret in guarded interaction trees using the higher-order store effects (Example 3.2).We show the type safety of  ⊸,ref by building a logical relations model.
We then consider cross-language interoperability, by allowing embedding of (higher-order) programs from the non-affine language  rec,io (Section 5) into  ⊸,ref , thus combining two languages with different type systems and different effects.Following the approach outlined in Section 8 we reuse the standalone interpretations of  rec,io and  ⊸,ref to interpret the combined language in IT store,io .Finally, we show type safety of this combined language, by building the logical relations model out of the models for the individual languages.
We stress that our approach here allows us to prove type safety of  rec,io and  ⊸,ref separately, and then prove type safety of the combined language by reusing the logical relations for the individual languages, thus highlighting the modular nature of the GITrees framework.

An Affine Programming Language
First, we consider an affine programming language  ⊸,ref with references with strong updates, and show how to interpret it in GITrees in a way that enforces linearity.We then consider the combination  ⊸,ref +  rec,io , which allows us to embed  rec,io programs, including functions, into  ⊸,ref .The syntax for the affine language  ⊸,ref is as follows: To differentiate between the terms of  ⊸,ref and the terms of  rec,io , we use the orange color to refer to types and programs of  ⊸,ref .
The type system for  ⊸,ref is given in Figure 9.Let us explain some of the details.The language  ⊸,ref contains Booleans, natural numbers, and the unit type.It also features linear functions  1 ⊸ 2 .Note that in the typing rule for function application, the context is split between typing of the function and typing of the argument.This ensures that the function and its argument do not share any variables or resources in common.
The language also features linear pairs  1 ⊗ 2 .In the typing rule for pairs the typing environment has to be split between the two components.This ensures that we cannot have, e.g., pairs of the form (,). Finally, the language features references with strong updates, i.e., references that can store values of different types.The constructors alloc and dealloc are used to allocate and free the references, respectively.To ensure linearity, we have a single operation that combines reading from a reference and performing a strong update.The program replace(, ) reads the value that is stored in the reference  and updates it to the value .It then returns a linear pair consisting of the old value and the reference itself, allowing one to reuse the reference later on.
The meaning of  ⊸,ref is given by the interpretation function   : IT  (), where  is the environment mapping the free variables of  to GITrees, and where  is a signature which contains the higher-order store effects (Example 3.2).We assume that the ground type  contains, in addition to natural numbers and the unit type, the type Loc of locations.The semantic interpretation of  ⊸,ref is given in Figure 10.In the interpretation of compound expression we split the environment  into the environments  1 and  2 , for the free variables of  1 and  2 respectively.
In order to ensure that the variables from the context Ω are used at most once, we wrap every variable in a thunk which can be evaluated at most once: Thunk() ≜ Alloc (0), ℓ.Fun(next(_.If (Read(ℓ), Err(), Write(ℓ, Ret(1)) ; ))) When we called a thunked GITree for the second time, it will return the error Err(), signifying that we broke the linearity condition.Here we assume that we have a separate error state  ∈ Error, because we want to treat linearity condition errors separate from type errors or memory safety errors.As such, in the interpretation of a function application we put the argument in a Thunk, and whenever we use the argument (or any affine variable) we then have to Force it.We can show that if we have a well-typed program, then it does not have any run-time errors, and that all the thunks are evaluated at most once: Proposition 9.1.Suppose that ⊢  : , and suppose that (,  ) ⇝ * ( ′ , ).Then  ≠ Err( ).
To prove Proposition 9.1 we use a logical relation, given in Figure 11, defined similarly to the logical relation from Section 7. The interpretation V − of the base types cover the appropriate subsets of natural numbers.Reference types are interpreted using the "pointsto" ℓ ↦ →   proposition, and affine pairs are interpreted component-wise.The main differences to note here are: (1) variables in  ⊸,ref are interpreted as thunks, and thus we adjust the interpretation V * Ω to account for that; (2) values in  ⊸,ref can be used at most once; hence the value interpretation is not persistent, i.e., there is no persistently modality □ in the interpretation of function types.Lemma 9.2 (Fundamental property).For any expression , if Ω ⊢  : , then Ω |= .  : .
We prove the fundamental property by induction on the typing derivation.More specifically, for each typing rule we prove an associated compatibility lemma, by replacing expressions with interaction trees and the derivability ⊢ with validity |=.For example, the compatibility lemma for dealloc looks as follows: Proving all the compatibility lemmas is relatively straightforward using the separation logic rules.Having separate compatibility lemmas will be useful for us in the next section.
By combining the fundamental property with the safety theorem for the weakest precondition calculus (Theorem 6.2) we obtain a proof of Proposition 9.1.
Safety for  rec,io .Similar to the logical relation for safety for  ⊸,ref , we define a logical relation for  rec,io .It is simply an unary version of the logical relation from Section 7.For example, the expression relation is defined as where has_state  tracks the state for the input/output effects.We omit the other details here and direct an interested reader to the Coq formalization.We only note that just like for  ⊸,ref , we split the proof of the fundamental property into compatibility lemmas, which we will use in the next section.

Interoperability Between Languages
Following the approach of [Patterson et al. 2022], we allow the interaction between the languages  rec,io and  ⊸,ref , using the guarded interaction trees as the "common ground", combining the effects of the two languages.At the syntactic language level, this is done by allowing one to embed the expressions of  rec,io into the  ⊸,ref programs.The embedding is given by the following typing rule: 4 typed-conv The crucial ingredient for the interaction is a type conversion relation  ′ ∼  stating that the  rec,io type  ′ is convertible to the  ⊸,ref type .
We have the following type conversions: The first three type conversions say that we can freely convert between integers, Booleans, and the unit type (since all of them have similar internal representation).The last conversion is more interesting.It says that we can convert between affine functions and non-affine functions.The affine argument  1 is represented as a thunk (Nat →  ′ 1 ), which we will protect at runtime to ensure that it is not invoked more than once.The type Nat is used in lieu of the unit type which is absent from  rec,io .
Glue code for conversion functions.In order to (1) convert between different types, and (2) ensure the linearity of  ⊸,ref programs that might cross the boundary to  rec,io , we need to interpret embedded terms with additional glue code.For every type conversion  ′ ∼  we generate recursively a pair of conversion functions   ′ , and  , ′ which convert the representations from  ′ to  and vice versa.The glue code for converting between the base types ensures that the underlying natural number representation stays within the range.For example, for Nat ∼ B we have: The glue code for converting functions is a bit more involved: 5 4 For simplicity, we only consider one-way embeddings from  rec,io to  ⊸,ref , and we only consider embeddings of closed terms.This simplifies the type system, but does not lead to loss of expressivity, since we allow type conversions of functions. 5The glue code for functions is a bit more complicated than in [Patterson et al. 2022].Specifically, they do not protect the converted affine functions.This is fine in their setting, because their affine functions are pure, and invoking them multiple times does not lead to run-time errors.However, in the presence of references with strong updates this assumption is no longer true, and not protecting the converted functions will lead to unsound behavior!When we convert a function, we need to recursively convert the argument and the result; in addition the argument to the function needs to be protected with a Thunk.Furthermore, when affine functions are converted to non-affine ones, we need to protect the function itself with a Thunk, to ensure that the function is not invoked multiple times.Calling an affine function multiple times might be unsound, e.g., calling the following function twice will attempt to deallocate an already deallocated reference: (ℓ._.(_.7) dealloc(ℓ)) (alloc( 42)) : Unit⊸Nat.
Partial safety for the combined language.We interpret the combined language with embeddings using the glue code.The embedding from  rec,io to  ⊸,ref is interpreted as follows: where IO − is the interpretation function for  rec,io expressions from Section 5.The interpretation for all the operations, except for the embedding, remains unchanged.
Using the modular approach we described in Section 8, we interpret the extended language  rec,io +  ⊸,ref into the guarded interaction trees IT store,io with the signature that combines the input/output effects and the store effects.This ensures that the interpretation of the languages end up in the same domain, where they can interact.By combining the reifiers for the effects of the individual languages we also get the reduction relation (( 1 ,  ′ 1 ), ) ⇝ (( 2 ,  ′ 2 ), ), where  1 and  2 are stores, and  ′ 1 and  ′ 2 are input/output tapes.Of course, in the combined language with conversions, our programs can actually violate the linearity condition, since it is no longer enforced by the type system.However, we can prove that linearity violations at the boundary are the only errors that we will possibly get.Thus we will to prove the following safety theorem: Theorem 9.4.Suppose that ⊢  :  with the embedding rule, and suppose that (( 1 ,  ′ 1 ),  ) ⇝ * (( 2 ,  ′ 2 ), ).Then either  is not an error, or  = Err().

Logical Relations for the Combined Safety
We will prove Theorem 9.4 by constructing a logical relation similarly to what we did for the individual languages in Section 9.1.Our goal is to do so modularly, by reusing as much material from Section 9.1 as possible.In particular, we will reuse all the compatibility lemmas we used to prove Lemma 9.2, and only prove one (!) new compatibility lemma for typed-conv.Letting E  ′ and E  be the expression relations for the logical relation for  rec,io and  ⊸,ref resp., this compatibility lemma is: Lemma 9.5.Suppose that E  ′ () and  ′ ∼ .Then E  (  ′ , ()).Moreover, for the other direction, suppose that E  () and  ′ ∼ .Then E  ′ ( , ′ ()).
When we presented the separation logic and logical relations earlier, we presented a slightly simplified version which was sufficient for our purposes.However, in order to prove Lemma 9.5 we need to make use of features that we have not yet presented.We describe those features now.
Separation logic for weak safety.The first feature is that our notion of weakest precondition is actually parameterized by a stuckness predicate  : Error → iProp, and satisfies the following rule (in addition to the rules presented in Section 6): This means that if the stuckness predicate  holds for some error , then the weakest precondition predicate holds for that error, irrespectively of the postcondition.The earlier presented weakest precondition, which did not allow for errors, is obtained by using  () = False.The general weakest precondition with the stuckness predicate satisfies a version of the adequacy/safety property (Theorem 6.2), in which the (safety) condition is replaced with the following condition: • (weak safety) either there are  1 and  1 such that (,  ′ ) ⇝ ( 1 ,  1 ), or  = Err() with  satisfying the predicate .
All the logical relations presented earlier are actually parameterized by a predicate  and uses wp   Φ in the expression interpretation -to recover the earlier stated theorems for full safety we simply instantiate the logical relations with  = .False.
Freely adjoined Kripke structure.In addition to the stuckness bit, we actually formulate our logical relations a bit more generally than what we have shown so far.This is because in the logical relations for individual languages require particular resources that we need to combine when constructing a logical relation for the combined language.
Indeed, to ensure that our logical relations are sufficiently modular, we parameterize the expression relation by an arbitrary predicate  :  → iProp of an arbitrary type .We refer to this predicate  as freely adjoined Kripke structure (because, in the underlying model of Iris, it allows us to make arbitrary transitions between worlds, constrained by the predicate ).The general definition of the expression relation for all our logical relation is thus: The idea is that the  parameter describes additional resources (for other effects) and ensures that ITrees in the expression relation preserve any such additional resources.The idea is akin to the "baking-in" of the frame rule in models of separation logic for higher-order languages [Birkedal et al. 2008;Birkedal and Yang 2008].
The expression relations for individual languages  rec,io and  ⊸,ref are then both parameterized by predicates  :  → iProp and  : Error → iProp and defined as For  rec,io and for  ⊸,ref we prove the compatibility properties for arbitrary  and  (in the proofs of the compatibility lemmas, the resources described by  are just passed through).We obtain full safety for the individual languages by instantiating  with  .True and  with .False.
When we prove partial safety for the combined language, we reuse the same logical relations and the same compatibility lemmas for the individual languages, by instantiating  in E  ′ with  () = heap_ctx, and by instantiating  in E  with  () = has_state  (), and  with .( = ) in both cases.Then, to prove the fundamental property of the combined language we can reuse the compatibility lemmas for individual languages, and it only remains to prove the compatibility Lemma 9.5 for the type conversion.
The most interesting case of Lemma 9.5 is the conversion between functions, which involves showing soundness of the glue code.The interpretation of non-affine functions is persistent, as it begins with □, since it is expected that you can use non-affine functions multiple times.The interpretation of affine functions, however, is not persistent -functions can be invoked only once.Because of that, we cannot directly use the interpretation V  1 ⊸ 2 when proving V (Nat →  ′ 1 ) →  ′ 2 .Instead, we put the interpretation of the affine function in a persistent invariant, which states: This describes when the affine function   is protected with the Thunk via a reference ℓ: either the function has not been invoked yet (ℓ ↦ → Ret(0)) and it satisfies the value interpretation, or the function has already been invoked (ℓ ↦ → Ret(1)) and forcing its thunk will result in Err(), in which case we do not invoke the function.(As a side remark, we note that we here crucially rely on Iris's powerful notion of invariants -this is another example of why it is advantageous to use Iris as the basis for our separation logic.) Having established the compatibility lemma for type conversions, and the compatibility lemmas for the operations for each individual language, we prove the fundamental property for the logical relation for the combined language.In particular, for a closed term ⊢  :  we obtain E  .heap_ctx * has_state  ( ) .= (V  ) (  ).From the adequacy property of the weakest precondition we can then conclude Theorem 9.4.
In summary, this approach to logical relations, with freely adjoined Kripke structure as a parameter, allows us to scale proofs to interoperability between multiple different languages in a modular way.For each individual language we can prove safety separately (without knowing in advance with which other languages we are going to interface).Then we can combine logical relations for individual languages together, by instantiating the freely adjoined Kripke structure with the shared resources or with resources needed to verify the glue code, and reusing the compatibility lemmas.

DISCUSSION AND RELATED WORK
We have already discussed a lot of related work throughout the paper; in this section we include some further discussion of related work.
Differences with interaction trees.Whilst our work takes direct inspiration from the interaction trees approach, there are some crucial differences.One of the main difference comes from the treatment of effect reification.The original type of interaction trees is a monad, and the effects in an interaction tree can be reified "in one go", for example, with a state monad transformer over ITrees.In our case, we cannot reify all the effects in a guarded interaction tree, due to higher-order functions and higher-order effects.For example, a guarded interaction tree can be a function that contains latent effects, but these effects can only be reified at the point when the function is invoked.Because regular interaction trees contain only first-order structures, it is possible to traverse them completely, reifying all the effects.
Another difference worth mentioning, is that regular interaction trees can be extracted and executed from Coq.Our formalization cannot be directly extracted, as it is built upon a layer of guarded type theory.One potential approach would be to erase the guarded parts from the formulation of GITrees and obtain that way a representation of GITrees in a functional language like OCaml or Haskell, which already supports mixed-variance datatypes.Then, the extraction can be set up in such a way as to use this representation.We have not researched this direction and leave it to future work.
Finally, an important difference between our work and that on ITrees, is that the ITrees development relies heavily on the weak bisimilarity theory of interaction trees, while we opt for developing separation logic and refinements instead.There are several things that complicate the study of bisimilarity for guarded interaction trees.Firstly, the higher-order nature of GITrees suggests that we need to study a more involved notion of behavioral equivalence, like applicative bisimilarity.Secondly, we believe that developing a theory of weak bisimilarity in the context of Iris and guarded type theory is still an open question, complicated by issues with step-indices.For these reasons we believe that developing weak applicative bisimularity for guarded interaction trees will require new techniques and we leave it for future work.
Differences with the standard Iris approach to verification.The standard Iris-based approach to separation logic [Jung et al. 2016[Jung et al. , 2015;;Krebbers et al. 2017a] is based on operational semantics, and has been proven to scale well to complicated programming language features.The main difference with our work, is that we are the first to build an Iris-based separation logic over denotational semantics, in a way that is tightly integrated with the existing Iris ecosystem.In particular, we rely on the Iris ecosystem for various data structures, resource algebras, base logic (but not the program logic), and the Iris Proof Mode.As such, in terms of reasoning about specific concrete programs, the GITrees-based approach is not that different from what a normal Iris user expects, with the added advantage of using equational reasoning for many computation steps that usually requires some form of symbolic execution.
The main advantage of our approach comes into play when we want to consider new models of programming languages, or reasoning about programs with combinations of effects (as in Section 8), or reasoning about interoperability (as in Section 9).Domain theory and guarded type theory.Guarded type theory [Birkedal and Møgelberg 2013;Bizjak et al. 2016] has been studied as a setting for domain theory before [Birkedal et al. 2012;Møgelberg and Paviotti 2019;Møgelberg and Vezzosi 2021;Paviotti et al. 2015], but previous works mostly focused on specific typed models and was not formalized.In contrast, here we work with guarded interaction trees as a "universal domain", similar to domain theoretic models of untyped -calculus.
The previous work used (dependent) guarded type theory not just for modeling, but also for reasoning about programs.This required a more complicated type theory and precluded the work from being formalized in a traditional proof assistant like Coq.By contrast, our reasoning is done in the logic over a guarded type theory.This is arguably simpler, and allowed us to make use of Iris and formalize all of our results in Coq.
Logical relations and language interoperability.Our case study on language interoperability in Section 9 is inspired by the seminal work of Patterson et al. [2022].We believe that the approach we take in our work is more modular.Firstly, our approach here is less syntactic, as we use the domain theory of guarded interaction trees as the common setting for interpreting different languages with different effects, and we do not need to come up with a target language for each pair of source languages for which we wish to set up interoperability.Secondly, the models that we construct for individual languages are "local", and that is exactly what allows for true reuse of proofs and for constructing a common model for the combined language from the individual models.This opens up the possibility of a model for a single language to be reused for different cross-language interactions.In contrast, the type safety of individual source languages in [Patterson et al. 2022] requires to have a model for the combined language in advance.And finally, the treatment of effects and step-indices is more abstract and high-level in our work, since we construct our models using separation logic.
(Guarded) Interaction Trees and Algebraic Effects and Handlers.The treatment of effects in interaction trees is reminiscent of effects in programming languages based on algebraic effects and handlers [Bauer and Pretnar 2015;Plotkin and Pretnar 2013].Algebraic effects and handlers have been extensively studied in various contexts, including separation logic [de Vilhena and Pottier 2021], and both higher-order effects [Bach Poulsen and van der Rest 2023;van den Berg et al. 2021;Wu et al. 2014] and reasoning about combinations of effects [Johann et al. 2010;Yang and Wu 2021] have been investigated.Despite the aesthetic and moral similarities to (guarded) interaction trees, there are some substantial differences between the two approaches.Under algebraic effects and handlers, both the representation and reification of effects is done inside the programming language.As such, a particular theory and implementation of algebraic effects is always tied to a specific programming language.Whereas in the interaction trees-based approach, the effects are handled in the ambient type theory, outside the type of the (guarded) interaction trees itself.See also the discussion in [Xia et al. 2019, Section 8.2].
To our knowledge, the two approaches have not been formally compared.It would be interesting to consider a denotational model of a programming language with algebraic effects inside guarded interaction trees, and to see what kind of properties can be proved using such a model.
Additionally, such a comparison might help us understand the exact class of effects that can be represented with the GITrees-based approach.As it currently stands, our approach to representing effects is "open-ended", in the sense that we can consider different classes of effects by varying the reification procedure.Of course, different classes of effects allow for different reasoning principles.For example, as we mentioned in the end of Section 4, we consider context-independent effects, which preclude us from modeling call/cc, but allows us to use the bind rule for the weakest precondition calculus.We leave a formal comparison with algebraic effects and further investigations in that area to future work.
Fig. 1.Grammar for the base logic.
That is, for any other bi-algebra (,  , ) we have unique maps ℎ and  such that the following diagram commutes:That is, in order to construct a function  : IT → , one has to provide the "unfolding"  →  (, ), as well as functions  → , Error → , ▶ → , ▶( → ) → , and i∈ Ins i (▶) → (Outs i (▶) → ▶) → .This alone would allow us to iterate over GITrees.However, we would run intro trouble if we wish to write a primitive-recursive style function.For example, we might wish to write a destructor function  such that  () returns  if  itself is a function Fun( ), and Err(RunTime) otherwise.We cannot do so with the scheme outlined above, since in the recursive call we don't have access to the original argument, only to the result of applying recursion to the argument.This is similar to how the iteration scheme  → ( → ) → N →  for natural numbers does not allow us to (easily) write the predecessor function  : N → N satisfying  (0) = 0 and  ( + 1) =  if we pick  = N.