skip to main content
research-article
Open Access

Omnisemantics: Smooth Handling of Nondeterminism

Published:08 March 2023Publication History

Skip Abstract Section

Abstract

This article gives an in-depth presentation of the omni-big-step and omni-small-step styles of semantic judgments. These styles describe operational semantics by relating starting states to sets of outcomes rather than to individual outcomes. A single derivation of these semantics for a particular starting state and program describes all possible nondeterministic executions (hence the name omni), whereas in traditional small-step and big-step semantics, each derivation only talks about one single execution. This restructuring allows for straightforward modeling of both nondeterminism and undefined behavior as commonly encountered in sequential functional and imperative programs. Specifically, omnisemantics inherently assert safety (i.e., they guarantee that none of the execution branches gets stuck), while traditional semantics need either a separate judgment or additional error markers to specify safety in the presence of nondeterminism.

Omnisemantics can be understood as an inductively defined weakest-precondition semantics (or more generally, predicate-transformer semantics) that does not involve invariants for loops and recursion but instead uses unrolling rules like in traditional small-step and big-step semantics. Omnisemantics were previously described in association with several projects, but we believe the technique has been underappreciated and deserves a well-motivated, extensive, and pedagogical presentation of its benefits. We also explore several novel aspects associated with these semantics, in particular, their use in type-safety proofs for lambda calculi, partial-correctness reasoning, and forward proofs of compiler correctness for terminating but potentially nondeterministic programs being compiled to nondeterministic target languages. All results in this article are formalized in Coq.

Skip 1INTRODUCTION Section

1 INTRODUCTION

Today, a typical project in rigorous reasoning about programming languages begins with an operational semantics (or maybe several), with proofs of key lemmas proceeding by induction on derivations of the semantics judgment. An extensive toolbox has been built up for formulating these relations, with common wisdom on the style to choose for each situation. With decades having passed since operational semantics became the standard technique in the 1980s, one might expect that the base of wisdom is sufficient. Yet, a style that we call omnisemantics has emerged in recent years as a new, powerful technique with numerous applications.

In short, omnisemantics relate starting states to their sets of possible outcomes, rather than to individual outcomes. The omni-big-step judgment takes the form \({t / s}\,\Downarrow \,{Q}\) and asserts that every possible evaluation starting from the configuration \(t/s\) reaches a final configuration that belongs to the set Q. This set Q is isomorphic to a postcondition from a Hoare triple. The omni-small-step judgment takes the form \(t / s \longrightarrow P\). It asserts both that the configuration \(t/s\) can take one reduction step and that, for any step it might take, the resulting configuration belongs to the set P. On top of this judgment, one may define the eventually judgment \(t / s \longrightarrow ^\lozenge P\), which asserts that every possible evaluation of \(t/s\) is safe and eventually reaches a configuration in the set P.

On the one hand, omnisemantics can be viewed as operational semantics, because they are not far from traditional operational semantics or executable interpreters. On the other hand, omnisemantics can be viewed as axiomatic semantics, because they are not far form reasoning rules; in particular, they directly give a practical, usable definition of a weakest-precondition judgment, which can be used for verifying concrete programs. The fact that they are both closely related to operational semantics and to axiomatic semantics is precisely the strength of omnisemantics.

To the best of our knowledge, the ideas of omnisemantics have been studied prior to the writing of this article by three different groups of researchers. First, Schäfer et al. [2016] present an omni-big-step judgment for a nondeterministic source language of guarded commands, as well as for a deterministic target language with named continuations, using the term axiomatic semantics to refer to this style of semantics. They establish the correctness of a function that compiles terminating programs from the source language into the target language. Their proof is by induction on the derivation of an omni-big-step judgment for the source program rather than on a derivation for the target program, a key insight that we will discuss in Sections 1.3 and 6. They also present characterizations of program equivalence and present a proof of equivalence with traditional small-step semantics, though only in the case of a deterministic semantics. Second, Erbsen et al. [2021] make use of both omni-big-step semantics, applied to a high-level, core imperative language with external calls, and omni-small-step semantics, applied to a low-level, RISC-V machine language. They call this style of semantics CPS semantics. They establish end-to-end compiler-correctness results for terminating programs. They also set up Separation Logic reasoning rules in weakest-precondition style. Third, Charguéraud’s [Charguéraud [2020]] course notes make use of omni-big-step semantics for the purpose of deriving Separation Logic triples, for both partial and total correctness. The language considered is a nondeterministic, imperative \(\lambda\)-calculus, with a substitution-based semantics. In particular, that work establishes the relationship between omni-big-step semantics and traditional big-step semantics, in the presence of nondeterminism.

Throughout the three pieces of work, the fundamental feature of omnisemantics being exploited is the ability to carry out proofs by induction on derivations that follow the flow of program execution, with smooth handling of nondeterminism. Indeed, nondeterministic choices result in universally quantified induction hypotheses at steps where nondeterministic choices are made. Before further presenting omnisemantics, we believe that it is useful to begin by presenting in more detail the several important problems that omnisemantics solve.

1.1 Feature #1: Stuck Terms and Nondeterminism

In an impure language, an execution may get stuck, for instance, due to a division by zero or an out-of-bounds array access. In a nondeterministic language, some executions may get stuck while others do not. Thus, for an impure, nondeterministic language, the existence of a traditional big-step derivation for a starting configuration is not a proof that getting stuck is impossible.

How to fix the problem? A popular but cumbersome approach is to add errors as explicit outcomes (written \(\textsf {err}\) in the rules below), so that we can state theorems ruling out stuck terms. For example, if the semantics of an impure functional language includes the rule big-let, it needs to be augmented with two additional rules for propagating errors: big-let-err-1 and big-let-err-2.

The set of inference rules grows significantly, and the very type signature of the relation is complicated. Omni-big-step semantics provide a way to reason, in big-step style, about the absence of stuck terms in nondeterministic languages without introducing error-propagation rules.

1.2 Feature #2: Termination and Nondeterminism

In a nondeterministic language, a total-correctness Hoare triple, written \({}^{\textsf {total}}\lbrace H\rbrace \;t\;\lbrace Q\rbrace\), asserts that in any state satisfying the precondition H, any execution of the term t terminates and reaches a final state satisfying the postcondition Q. In foundational approaches, Hoare triples must be defined in terms of or otherwise formally related to the operational semantics of languages.

When the (nondeterministic) semantics is expressed using the standard small-step relation, there are two classical approaches to defining total-correctness Hoare triples. The first one involves bounding the length of the execution. This approach not only involves tedious manipulation of integer bounds but also is restricted to finitely branching forms of nondeterminism. The second approach is to define total correctness as the conjunction of a partial-correctness property (if t terminates, then it satisfies the postcondition) and of a separate, inductively defined termination judgment. With both of these approaches, deriving reasoning rules for total-correctness Hoare triples becomes much more tedious than in the case of partial correctness.

One may hope for simpler proofs using a big-step judgment. Indeed, Hoare triples inherently have a big-step flavor. Moreover, for deterministic, sequential languages, the most direct way to derive reasoning rules for Hoare triples is from the big-step evaluation rules. Yet, when the semantics of a nondeterministic language is expressed using a traditional big-step judgment, we do not know of any direct way to capture the fact that all executions terminate. Omni-big-step semantics provide a direct definition of total-correctness Hoare triples with respect to a big-step-style, nondeterministic semantics, in a way that leads to simple proofs of the Hoare-logic rules.

1.3 Feature #3: Simulation Arguments with Nondeterminism and Undefined Behavior

Many compiler transformations map source programs to target programs that require more steps to accomplish the same work, because they must make do with lower-level primitives. Intuitively, we like to think of a compiler transformation being correct in terms of forward simulation: the transformation maps each step from the source program to a number of steps in the target program. Yet, in the context of a nondeterministic language, such a result is famously insufficient even in the special case of safely terminating programs. Concretely, compiler correctness requires showing that all possible behaviors of the target program correspond to possible behaviors of the source program. A tempting approach is to establish a backward simulation, by showing that any step in the target program can be matched by some number of steps in the source program. The trouble is that all intermediate target-level states during a single source-level step need to be related to a source-level state, severely complicating the simulation relation.

To avoid that hassle, most compilation phases from CompCert [Leroy 2009] are carried out on deterministic intermediate languages, for which forward simulation implies backward simulation. Yet, many realistic languages (C included) are not naturally seen as deterministic. CompCert involves special effort to maintain determinism, through its celebrated memory model [Blazy and Leroy 2009]. Rather than revealing pointers as integers, CompCert semantics allocate pointers deterministically, taking care to trigger undefined behavior for any coding pattern that would be sensitive to the literal values of pointers. As a result, any compiler transformations that modify allocation order require the complex machinery of memory injections, to connect executions that use different deterministic pointer values. Omnisemantics make it possible to retain the simplicity of forward simulation, while keeping nondeterminism explicit.

1.4 Feature #4: Linear-Size Type-Safety Proofs

Type safety asserts that if a closed term is well typed, then none of its possible evaluations get stuck. A type-safety proof in the syntactic style [Wright and Felleisen 1994] reduces to a pair of lemmas: preservation and progress. \(\begin{equation*} \begin{array}{l@{~}l} {\scriptsize{\text{PRESERVATION}}:} & E \, \vdash \,t \, : \,T\quad \wedge \quad t \longrightarrow t^{\prime } \quad \Rightarrow \quad E \, \vdash \,t^{\prime } \, : \,T \\ {\scriptsize{\text{PROGRESS}}:} & \emptyset \, \vdash \,t \, : \,T\quad \Rightarrow \quad (\textsf {isvalue}\,t) \;\, \vee \;\,(\exists t^{\prime } .\; t \longrightarrow t^{\prime }) \end{array} \end{equation*}\) The Wright and Felleisen approach, although widely used, suffers from two limitations that can be problematic at the scale of real-world languages with hundreds of syntactic constructs.

The first limitation is that this approach requires performing two inductions over the typing judgment. Nontrivial language constructs are associated with nontrivial statements of their induction hypotheses, for which the same manual work needs to be performed twice, once in the preservation proof and once in the progress proof. Factoring out the cases makes a huge difference in terms of proof effort and maintainability.

The second limitation is associated with the case inspection involved in the preservation proof. Concretely, for each possible rule that derives the typing judgment (\(E \, \vdash \,t \, : \,T\)), one needs to select the applicable rules that can derive the reduction rule (\(t \longrightarrow t^{\prime }\)) for that same term t. Typically, only a few reduction rules are applicable. The trouble is that fully rigorous checking of the proof must still inspect all of those cases to confirm their irrelevance. A direct Coq proof, of the form “induction H1; inversion H2”, results in a proof term of size quadratic in the size of the language.1 As we expect to handle each possible transition at most once, a proof that takes only linear work would be more satisfying. It would also avoid potential blow-up in the proof-checking time for languages involving hundreds of constructs.

Interestingly, in the particular case of a deterministic language, there exists a strategy [Rompf and Amin 2016] for deriving type safety through a single inductive proof, which moreover avoids the quadratic case inspection. The key idea is to carry out an induction over the following statement: a well-typed term either is a value or can step to a term that admits the same type. \(\begin{equation*} \emptyset \, \vdash \,t \, : \,T\quad \Rightarrow \quad \big (\textsf {isvalue}\,t\big) \;\, \vee \;\,\big (\exists t^{\prime } .\; (t \longrightarrow t^{\prime }) \, \wedge \,(\emptyset \, \vdash \,t^{\prime } \, : \,T)\big) \end{equation*}\) Omnisemantics allow to generalize this approach to the case of nondeterministic languages. As we show in one of this article’s original contributions, practical proofs of type safety can be carried out with respect to both omni-small-step and omni-big-step semantics.

1.5 Contributions and Contents of the Article

The contributions of this article are as follows:

We present big-step and small-step omnisemantics for a standard imperative \(\lambda\)-calculus as well as for a standard imperative while language, which we believe should make the presentation more accessible than in prior publications. Moreover, we accompany this presentation with a Coq formalization of all definitions and proofs.2

We explain four key beneficial features of omnisemantics: They provide a convenient way to reason about the absence of stuck terms (feature #1) and the absence of diverging terms (feature #2) in nondeterministic languages, they enable forward-simulation-based correctness proofs for compilers with nondeterministic target languages (feature #3), and they enable type-safety proofs that avoid quadratic case inspection even in the case of a nondeterministic language (feature #4).

We introduce the coinductive variant of omni-big-step semantics, which yields a partial-correctness judgment. This possibility was left as future work by Schäfer et al. [2016].

We present numerous properties of omnisemantics, as well as their relationship to traditional operational semantics. Some of these properties were described in Erbsen et al. [2021] but only briefly. For example, the connection between traditional and omnisemantics only covered traditional small-step semantics with no undefined behavior, and small-step omnisemantics themselves were given one paragraph of description.

We present in detail the proof techniques from two case studies on compiler-correctness results, adapted from Erbsen et al.’s [Erbsen et al. [2021]] prior work.

We present a new case study illustrating an example of a correctness proof for a compiler transformation that increases the amount of nondeterminism. In contrast, work by Schäfer et al. [2016] and Erbsen et al. [2021] only considered transformations that decrease the amount of nondeterminism.

The article is organized as follows:

In Section 2, we introduce the omni-big-step judgment, which can be defined either inductively, to capture termination of all executions, or coinductively, in partial-correctness fashion. We also state and prove properties about the judgment, including the notion of smallest and largest admissible sets of outcomes.

In Section 3, we introduce the omni-small-step judgment, as well as the eventually judgment defined on top of it and three practical reasoning rules associated with these judgments.

In Section 4, we present type-safety proofs carried out with respect to either omni-small-step or omni-big-step semantics. We explain the improvement over the prior state of the art, as suggested in the earlier discussion of features #1 and #4.

In Section 5, we explain how the omni-big-step judgment or the omni-small-step eventually judgment can be used to define Hoare triples and weakest-precondition predicates. We consider both partial and total correctness, and we show how the associated reasoning rules can be established via one-line proofs (recall feature #2). Moreover, we explain how one may derive the frame rule from Separation Logic.

In Section 6, we demonstrate how omnisemantics can be used to prove that a compiler correctly compiles terminating programs, via forward-simulation proofs (recall feature #3). We illustrate this possibility through two case studies carried out on a while-language. The first one, “heapification” of pairs, increases the amount of nondeterminism; it involves omni-big-step semantics for both the source and the target language. The second one, introduction of stack allocation, decreases the amount of nondeterminism; it involves an omni-big-step semantics for the source language and an omni-small-step semantics for the target language.

Note that we leave it to future work to investigate how omnisemantics may be exploited to establish full compiler correctness, that is, not just the correctness of compilation for terminating programs but also that of programs that may crash, diverge, or perform infinitely many I/O interactions.

Skip 2OMNI-BIG-STEP SEMANTICS Section

2 OMNI-BIG-STEP SEMANTICS

In the section, we introduce the omni-big-step judgment, written \({t / s}\,\Downarrow \,{Q}\). We use this judgment in particular for establishing type safety (Section 4.3), for setting up program logics (Section 5), and for establishing compiler verification results (Section 6). To present the definition of this judgment, we consider an imperative, nondeterministic lambda-calculus, for which we first present the semantics in standard big-step style (Section 2.1). We then discuss the properties and interpretation of the omni-big-step judgment (Section 2.2). In particular, we focus on why the set Q that appears in \({t / s}\,\Downarrow \,{Q}\) is interpreted as an overapproximation of the set of possible results, rather than as the exact set of possible results. We next present the corresponding coinductive judgment, written \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\), which captures partial correctness in the sense that it allows for diverging executions (Section 2.4). We conclude this section by presenting the bind rule for handling programs that are not in A-normal form (Section 2.5).

2.1 Definition of the Omni-Big-Step Judgment

Syntax. As a running example, we consider an imperative lambda-calculus, including a random-number generator \(\textsf {rand}\). Both this operator and allocation are nondeterministic.

The grammar of the language appears next. The metavariable \(\pi\) ranges over primitive operations, v ranges over values, t ranges over terms, and x and f range over program variables. A value can be the unit value \(\mathit {t\!t}\), a Boolean b, a natural number n, a pointer p, a primitive operator, or a closure.3 \(\begin{equation*} \begin{array}{l@{\,}l@{\quad }l} \pi & \quad := \quad & \textsf {add}\;\, | \;\, \textsf {rand}\;\, | \;\, \textsf {ref}\;\, | \;\, \textsf {free} \;\, | \;\, \textsf {get}\;\, | \;\, \textsf {set}{1}\\ v & \quad := \quad & \mathit {t\!t}\;\, | \;\, b \;\, | \;\, n \;\, | \;\, p \;\, | \;\, \pi \;\, | \;\, \mu f.\lambda x.t {1}\\ t & \quad := \quad & v \;\, | \;\, x \;\, | \;\, (t\,t) \;\, | \;\, \textsf {let}\, x = t \,\textsf {in}\, t \;\, | \;\, \textsf {if}\; t \;\textsf {then}\; t \;\textsf {else}\; t \end{array} \end{equation*}\)

For simplicity, we present evaluation rules by focusing first on programs in A-normal form: the let-binding construct is the only one that involves evaluation under a context. In an application \((t_1\,t_2)\), the two terms must be either variables or values. Similarly, the condition of an if-statement must be either a variable or a value, and likewise for arguments of primitive operations. In Section 2.5, we present the bind rule, which enables the evaluation of subterms under all valid evaluation contexts.

Evaluation judgments. The standard big-step-semantics judgment for this language appears in Figure 1. States s are finite partial maps from pointers p to values v. The evaluation judgment \(t / s \Downarrow v / s^{\prime }\) asserts that the configuration \(t/s\), made of a term t and an initial state s, may evaluate to the final configuration \(v/s^{\prime }\), made of a value v and a final state \(s^{\prime }\).

Fig. 1.

Fig. 1. Standard big-step semantics (for terms in A-normal form).

The corresponding omni-big-step semantics appears in Figure 2. Its evaluation judgment, written \({t / s}\,\Downarrow \,{Q}\), asserts that all possible evaluations starting from the configuration \(t/s\) reach final configurations that belong to the set Q. Observe how the standard big-step judgment \(t / s \Downarrow v / s^{\prime }\) describes the behavior of one possible execution of \(t/s\), whereas the omni-big-step judgment describes the behavior of all possible executions of \(t/s\). The set Q that appears in \({t / s}\,\Downarrow \,{Q}\) corresponds to an overapproximation of the set of final configurations: it may contain configurations that are not actually reachable by executing \(t/s\). We return to that aspect in Section 2.3.

Fig. 2.

Fig. 2. Omni-big-step semantics (for terms in A-normal form).

The set Q contains pairs made of values and states. Such a set can be described equivalently by a predicate of type “\(\textsf {val}\rightarrow \textsf {state}\rightarrow \textsf {Prop}\)” or by a predicate of type “\((\textsf {val}\times \textsf {state}) \rightarrow \textsf {Prop}\)”. In this article, in order to present definitions in the most idiomatic style, we use set-theoretic notation such as \((v,s)\in Q\) for stating semantics and typing rules, and we use the logic-oriented notation \(Q\,v\,s\) when discussing program logics. (The type of Q may be generalized for languages that include exceptions; see Appendix C.)

Description of the evaluation rules. The base case is the rule omni-big-val: a final configuration \(v/s\) satisfies the postcondition Q if this configuration belongs to the set Q.

The let-binding rule omni-big-let ensures that all possible evaluations of an expression \(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\) in state s terminate and satisfy the postcondition Q. First of all, we need all possible evaluations of \(t_1\) to terminate. Let \(Q_1\) denote (an overapproximation of) the set of results that \(t_1\) may reach, as captured by the first premise \({t_1 / s}\,\Downarrow \,{Q_1}\). One can think of \(Q_1\) as the type of \(t_1\), in a very precise type system where any set of values can be treated as a type. The second premise asserts that, for any configuration \(v^{\prime }/s^{\prime }\) in that set \(Q_1\), we need all possible evaluations of the term \([v^{\prime }/x]\,t_2\) in state \(s^{\prime }\) to satisfy the postcondition Q.

The evaluation rule omni-big-add for an addition operation is almost like that of a value: it asserts that the evaluation of \(\textsf {add}\,n_1\,n_2\) in state s satisfies the postcondition Q if the pair \(((n_1+n_2),s)\) belongs to the set Q. The nondeterministic rule omni-big-rand is more interesting. The term \(\textsf {rand}\,n\) evaluates safely only if \(n\gt 0\). Under this assumption, its result, named m in the rule, may be any integer in the range \([0,n)\). Thus, to guarantee that every possible evaluation of \(\textsf {rand}\,n\) in a state s produces a result satisfying the postcondition Q, it must be the case that every pair of the form \((m,s)\) with \(m\in [0,n)\) belongs to the set Q.

The evaluation rule omni-big-ref, which describes allocation at a nondeterministically chosen, fresh memory address, follows a similar pattern. For every possible new address p, the pair made of p and the extended state \({s}[p :=v]\) needs to belong to the set Q. The remaining rules, omni-big-free, omni-big-get, and omni-big-set, are deterministic and follow the same pattern as omni-big-add, only with a side condition \(p \in \textsf {dom}\,s\) to ensure that the address being manipulated does belong to the domain of the current state.

2.2 Properties of the Omni-Big-Step Judgment

In this section, we discuss some key properties of the omni-big-step judgment \({t / s}\,\Downarrow \,{Q}\). Recall that the metavariable Q denotes an overapproximation of the set of possible final configurations.

Total correctness. The predicate \({t / s}\,\Downarrow \,{Q}\) captures total correctness in the sense that it captures the conjunction of termination (all executions terminate) and partial correctness (if an execution terminates, then its final state satisfies the postcondition Q). Formally, let \(t / s \Downarrow v / s^{\prime }\) denote the standard big-step evaluation judgment, and let \(\textsf {terminates}(t,s)\) be a predicate that captures the fact that all executions of \(t/s\) terminate (a formal definition is given in Appendix D). We prove: \(\begin{equation*} \begin{array}{l} {\rm\small OMNI-BIG-STEP-IFF-TERMINATES-AND-CORRECT}:\\ \qquad {t / s}\,\Downarrow \,{Q} \quad \iff \quad \textsf {terminates}(t,s) \;\, \wedge \;\,\big (\forall v s^{\prime } .\; (t / s \Downarrow v / s^{\prime }) \, \Rightarrow \,(v,s^{\prime })\in Q\big) \end{array} \end{equation*}\) In particular, if we instantiate the postcondition Q with the always-true predicate, we obtain the predicate \({t / s}\,\Downarrow \,{ \lbrace (v,s^{\prime }) \,|\, {\textsf {True}} \rbrace }\), which captures only the termination property.

Consequence rule. The judgment \({t / s}\,\Downarrow \,{Q}\) still holds when the postcondition Q is replaced with a larger set. In other words, the postcondition can always be weakened, like in Hoare logic. \(\begin{equation*} {\rm\small OMNI-BIG-CONSEQUENCE}:\qquad {t / s}\,\Downarrow \,{Q} \quad \wedge \quad Q \subseteq Q^{\prime } \quad \Rightarrow \quad {t / s}\,\Downarrow \,{Q^{\prime }} \end{equation*}\)

Strongest postcondition. If the omni-big-step judgment holds for at least one set, then there exists a smallest possible set Q for which \({t / s}\,\Downarrow \,{Q}\) holds. This set corresponds to the strongest possible postcondition Q, in the terminology of Hoare logic. Formally, if \({t / s}\,\Downarrow \,{Q}\) holds for at least one Q, then \({t / s}\,\Downarrow \,{(\textsf {strongest-post}\,t\,s)}\) holds, where the strongest postcondition is equal to the intersection of all valid postconditions. \(\begin{equation*} \textsf {strongest-post}\,t\,s \quad = \quad \bigcap _{Q \;\, | \;\, ({t / s}\,\Downarrow \,{Q})}\,Q \quad = \quad \big \lbrace { (v,s^{\prime })\;} \,\big |\, {\;\forall Q,\; ({t / s}\,\Downarrow \,{Q}) \, \Longrightarrow \, (v,s^{\prime }) \in Q } \big \rbrace \end{equation*}\)

No derivations for terms that may get stuck. The fact that \(\textsf {rand}\,0\) is a stuck term is captured by the fact that \({(\textsf {rand}\,0) / s}\,\Downarrow \,{Q}\) does not hold for any Q. More generally, if one or more nondeterministic executions of t may get stuck, then we have \(\forall Q.\; \lnot \; ({t / s}\,\Downarrow \,{Q})\).

Relationship to standard big-step semantics. The standard big-step judgment \(t / s \Downarrow v / s^{\prime }\) relates one input configuration \(t/s\) to one single result configuration \(v/s^{\prime }\). The omni-big-step judgment, which relates inputs to sets of results, thus appears as an immediate generalization of the standard big-step judgment. The following two results formalize their relationship.

First, if \({t / s}\,\Downarrow \,{Q}\) holds, then any final configuration for which the standard big-step judgment holds necessarily belongs to the set Q. \(\begin{equation*} \scriptsize{\text{OMNI-BIG-AND-BIG-INV}:}\qquad {t / s}\,\Downarrow \,{Q} \quad \wedge \quad t / s \Downarrow v / s^{\prime } \quad \Rightarrow \quad { (v,s^{\prime }) \in Q } \end{equation*}\)

Second, if \({t / s}\,\Downarrow \,{Q}\) holds, then there exists at least one evaluation according to the standard big-step judgment whose final configuration belongs to the set Q. \(\begin{equation*} \scriptsize{\text{OMNI-BIG-TO-ONE-BIG}:} \qquad {t / s}\,\Downarrow \,{ Q } \quad \Rightarrow \quad \exists v s^{\prime } .\;\; t / s \Downarrow v / s^{\prime } \; \wedge \;(v,s^{\prime }) \in Q \end{equation*}\)

A corollary asserts that if \({t / s}\,\Downarrow \,{Q}\) holds with Q being a singleton set made of a unique final configuration \(v/s^{\prime }\), then the standard big-step judgment holds for that configuration. \(\begin{equation*} \scriptsize{\text{OMNI-BIG-SINGLETON}:}\qquad { {t / s}\,\Downarrow \,{ \lbrace (v,s^{\prime })\rbrace } } \quad \Rightarrow \quad { t / s \Downarrow v / s^{\prime } } \end{equation*}\)

Particular case of deterministic languages. In a deterministic language, an input configuration \(t/s\) may evaluate to at most one configuration \(v/s^{\prime }\). In such a case, the strongest postcondition is reduced to the singleton set \(\lbrace (v,s^{\prime })\rbrace\).

Nonempty outcome sets.

Observe that the judgment \({t / s}\,\Downarrow \,{Q}\), as defined in Figure 2, can only hold for a nonempty set Q. When designing omni-big-step rules for a new language, one has to be careful not to accidentally include rules that allow derivations of empty outcome sets for some programs. To illustrate the matter, consider the term “\(\textsf {rand}\,0\)”. According to the standard big-step semantics, this term is stuck because the rule big-rand requires a positive argument to \(\textsf {rand}\). In the omni-big-step semantics, if we were to omit the premise \(n\gt 0\) in the rule omni-big-rand, we would be able to derive \({(\textsf {rand}\,0) / s}\,\Downarrow \,{Q}\) for any s and Q. Indeed, the premise \(\forall m . \; 0 \le m \lt n \Rightarrow (m,s)\in Q\) becomes vacuously true when n is nonpositive.

A similar subtlety appears in the rule omni-big-ref, where the fresh location p must be picked fresh from the domain of s. This quantification could become vacuously true if the semantics allowed for infinite states or if the set of memory locations were finite. (We discuss in Section 6.5 the treatment of a language whose semantics account for a finite memory.)

The likelihood of inadequate formalization due to missing premises might be viewed as the main weakness of omnisemantics. Yet, if needed, additional confidence can easily be restored at the cost of minor additional work: one may consider a standard small-step semantics as reference (i.e., as part of the trusted code base), then relate it to the corresponding omni-big-step semantics and use the latter to carry out big-step-style, inductive proofs on nondeterministic executions.

2.3 About the Overapproximation of the Set of Results

The omni-big-step judgment \({t / s}\,\Downarrow \,{Q}\) associates an initial configuration \(t/s\) with a postcondition Q, which denotes an overapproximation of the set of possible final configurations. One may thus wonder: why not associate it with a precise set of results? In this section, we show that it is technically possible to define a precise judgment, but at the same time we argue why that judgment is much less practical to work with than the overapproximating omni-big-step judgment.

The precise judgment, written \({t / s}\,\Downarrow ^{\prime }\,{Q}\), is precise in the sense that it relates a configuration \(t/s\) to at most one set of results Q. This precise judgment, like the overapproximating omni-big-step judgment, guarantees safety: a judgment \({t / s}\,\Downarrow ^{\prime }\,{Q}\) can be derived for some Q if and only if none of the possible executions of \(t/s\) can get stuck. Thus, the precise judgment relates a safe configuration \(t/s\) to exactly one Q.

Figure 3 shows selected rules from the definition of the precise judgment, written \({t / s}\,\Downarrow ^{\prime }\,{Q}\). The rule precise-big-val relates a value v in a state s to the singleton set made of the pair \((v,s)\). The rule precise-big-ref relates the term \((\textsf {ref}\,v)\) in a state s to the set of pairs made of a location p fresh from s and of the state s updated at location p with the value v. Observe how this compares with the rule omni-big-ref, which only requires that set of pairs to be included in the result set Q. The rule precise-big-rand follows a similar pattern, only with the premise \(n\gt 0\) to ensure that the term is not stuck.

Fig. 3.

Fig. 3. Selected rules defining a precise variant of omni-big-step semantics, written \({t / s}\,\Downarrow ^{\prime }\,{Q}\) .

Most interesting is the rule precise-big-let. Its first premise involves an intermediate set \(Q_1\), which denotes exactly the set of results that \(t_1\) can produce when executed in the input state s. The second premise describes, for each result \((v^{\prime },s^{\prime })\) from the set \(Q_1\), the evaluation of \(([v^{\prime }/x]\,t_2)\) in state \(s^{\prime }\). The result of the execution is asserted to be exactly a set of configurations written \(Q^{\prime }_{(v^{\prime },s^{\prime })}\). Here \(Q^{\prime }\) denotes a (possibly infinite) family of postconditions, indexed by the possible results of \(t_1\). The final postcondition of the term \((\textsf {let}\, x = t_1 \,\textsf {in}\, t_2)\) is obtained by taking the union over that family of postconditions.4

In practice, working with indexed families of postconditions introduces significant overhead, compared with the overapproximating omni-big-step judgment. Moreover, for practical applications such as type-checking or program verification (using either weakest preconditions or Hoare triples), we are only interested in overapproximations of the semantics. For such applications, building the overapproximation on top of a precise judgment would only introduce a level of indirection. For other situations where a notion of an exact set of results might be desirable, typically for metatheoretical results (e.g., completeness results), we can always refer to the strongest postcondition, which, as explained earlier, can be formalized as the intersection of all valid postconditions.

In summary, we believe that it is interesting to know that a precise judgment can be defined, as it might be useful in other contexts, but for the applications that we have in mind the overapproximating omni-big-step judgment appears much better suited.

2.4 Coinductive Interpretation of the Omni-Big-Step Judgment

Let \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) denote the judgment defined by the coinductive interpretation of the same set of rules as for the inductively defined judgment \({t / s}\,\Downarrow \,{Q}\), i.e., rules from Figure 2. The coinductive interpretation allows for infinite derivation trees, and thus the coinductive omni-big-step judgment can be used to capture properties of nonterminating executions.

More precisely, the judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) asserts that every possible execution of configuration \(t/s\) either diverges or terminates in a final configuration satisfying Q. In particular, this judgment rules out the possibility for an execution of \(t/s\) to get stuck, and it can be used to express type safety, as detailed in Section 4. The judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) can also be used to define partial-correctness Hoare triples, as detailed in Section 5.

Formally, we can relate the meaning of \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) to the small-step characterization of partial correctness as follows: for every execution prefix, the configuration reached is either a value satisfying the postcondition or a term that can be reduced further. Below, \(t / s \longrightarrow t^{\prime } / s^{\prime }\) denotes the standard small-step evaluation judgment (defined in Appendix G), and \(\textsf {val}\) denotes the constructor that injects values into the grammar of terms. \(\begin{equation*} \begin{array}{l} {\rm\small CO-OMNI-BIG-IFF-SAFE-AND-CORRECT}\\ {t / s}\,\Downarrow ^{\textsf {co}}\,{Q} \qquad \iff \qquad \forall s^{\prime }t^{\prime } .\;\, (t / s \longrightarrow ^*t^{\prime } / s^{\prime }) \, \Rightarrow \,\begin{array}[!t]{@{}l@{\,}l} & \big (\exists v .\;\, t^{\prime } = \textsf {val}\,v \, \wedge \,(v,s^{\prime })\in Q \big) \\ \vee & \big (\exists t^{\prime \prime }s^{\prime \prime } .\;\; t^{\prime } / s^{\prime } \longrightarrow t^{\prime \prime } / s^{\prime \prime } \big) \end{array} \end{array} \end{equation*}\)

The judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) can also be used to characterize divergence, by instantiating Q as the empty set: the predicate \({t / s}\,\Downarrow ^{\textsf {co}}\,{\emptyset }\) asserts that every possible execution of \(t/s\) diverges. Because the judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) is covariant in Q, the predicate \({t / s}\,\Downarrow ^{\textsf {co}}\,{\emptyset }\) holds if and only if the predicate \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) holds for any Q. In summary, we formally characterize divergence as follows: \(\begin{equation*} \textsf {diverges}\,t\,s \;\, \equiv \;\,({t / s}\,\Downarrow ^{\textsf {co}}\,{\emptyset }) \qquad \qquad \textsf {diverges}\,t\,s \;\, \iff \;\,\forall Q .\;\;({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}) \end{equation*}\)

2.5 The Bind Rule for Reasoning about Evaluation Contexts

In this section, we explain how to reason about programs that are not in A-normal form. We follow the approach of the bind rule, popularized by Iris [Jung et al. 2018] in the context of program logics. The bind rule follows the pattern of the let-binding rule but allows for evaluation of a subterm t that appears in an evaluation context E. For the syntax introduced in Section 2.1, we can define evaluation contexts by the following grammar, where \(\square\) denotes the hole, i.e., the empty context: \(\begin{equation*} \begin{array}{l@{\,}l@{\quad }l} E & \quad := \quad & \square \quad | \quad \textsf {let}\, x = E \,\textsf {in}\, t \quad | \quad (E\,t) \quad | \quad (v\,E) \quad | \quad \textsf {if}\; E \;\textsf {then}\; t \;\textsf {else}\; t \end{array} \end{equation*}\)

We write \({E}[t]\) for the context E whose hole is filled with the term t. We write \(\textsf {value}\,t\) for the predicate that asserts that t is a value. The bind rule describes how to evaluate or reason about subterms that appear in evaluation contexts and that are not already values. The omni-big-step bind rule takes the following form:

The premise \(\lnot \; \textsf {value}\,t\) could be omitted for the inductive interpretation of the omni-big-step rules. It is required, however, for the coinductive interpretation, to prevent the construction of infinite derivations for terms that do not diverge.

Skip 3OMNI-SMALL-STEP SEMANTICS Section

3 OMNI-SMALL-STEP SEMANTICS

In this section, we present the omni-small-step judgment, written \(t / s \longrightarrow P\). Here, P denotes a set of pairs each made of a term and a state. We then present the eventually judgment, written \(t / s \longrightarrow ^\lozenge P\). We use these judgments in particular for establishing type-safety (Section 4.1) and compiler-verification (Section 6.6) results.

3.1 The Omni-Small-Step Judgment

The omni-small-step judgment, written \(t / s \longrightarrow P\), asserts that the configuration \(t/s\) can take one reduction step and that, for any step it might take, the resulting configuration belongs to the set P. It is defined by the rules from Figure 4. There is one per small-step transition. The interesting rules are those involving nondeterminism, namely omni-small-rand and omni-small-ref, which follow a pattern similar to the corresponding omni-big-step rules. Observe also how the rule omni-small-let-ctx handles the case of a reduction that takes place in the evaluation context of a let-binding, by quantifying over an intermediate set of results named \(P_1\).

Fig. 4.

Fig. 4. Omni-small-step semantics (for terms in A-normal form).

We prove that the judgment \(t / s \longrightarrow P\) captures the expected property w.r.t. the standard small-step judgment: the configuration \(t/s\) can make a step, and for every step it might take, it reaches a configuration in P. \(\begin{equation*} \begin{array}{l} {\rm\small OMNI-SMALL-STEP-IFF-PROGRESS-AND-CORRECT}\\ t / s \longrightarrow P \quad \iff \quad \big (\exists t^{\prime }s^{\prime } .\;\; t / s \longrightarrow t^{\prime } / s^{\prime } \big) \, \wedge \,\big (\forall t^{\prime }s^{\prime } .\;\; t / s \longrightarrow t^{\prime } / s^{\prime } \;\, \Rightarrow \;\,(t^{\prime },s^{\prime })\in P\big) \end{array} \end{equation*}\)

3.2 The “Eventually” Judgment

The judgment \(t / s \longrightarrow ^\lozenge P\) captures the property that every possible evaluation of \(t/s\) is safe and eventually reaches a configuration in the set P. Here, P denotes a set of configurations—it is not limited to being a set of final configurations like in the previous section. The judgment \(t / s \longrightarrow ^\lozenge P\) is defined inductively by the following two rules. The first one asserts that the judgment is satisfied if \(t/s\) belongs to P. The second one asserts that the judgment is satisfied if \(t/s\) is not stuck and that for any configuration \(t^{\prime }/s^{\prime }\) that it may reduce to, the predicate \(t^{\prime } / s^{\prime } \longrightarrow ^\lozenge P\) holds. The latter property is expressed using the omni-small-step judgment \(t / s \longrightarrow P^{\prime }\), where \(P^{\prime }\) denotes an overapproximation of the set of configurations \(t^{\prime }/s^{\prime }\) to which \(t/s\) may reduce.

If Q denotes a set of final configurations, then the judgment \(t / s \longrightarrow ^\lozenge Q\) can be viewed as a particular case of the judgment \(t / s \longrightarrow ^\lozenge P\), where P denotes a set of configurations. We prove that \(t / s \longrightarrow ^\lozenge Q\) matches our omni-big-step judgment \({t / s}\,\Downarrow \,{Q}\). \(\begin{equation*} \scriptsize{\text{EVENTUALLY-IFF-OMNI-BIG-STEP}:} \qquad t / s \longrightarrow ^\lozenge Q \quad \iff \quad {t / s}\,\Downarrow \,{Q} \end{equation*}\)

3.3 Chained Rule and Cut Rule for the “Eventually” Judgment

To apply the rule eventually-step, one needs to provide upfront an intermediate postcondition \(P^{\prime }\). Doing so is not always convenient. It turns out that we can leverage the omni-small-step judgment \(t / s \longrightarrow P^{\prime }\) to provide an introduction rule for \(t / s \longrightarrow ^\lozenge P\) that does not require providing \(P^{\prime }\) upfront. This rule, which we call the chained version of eventually-step, admits the statement shown below. It reads as follows: if every possible step of \(t/s\) reduces in one step to a configuration that eventually reaches a configuration from the set P, then every possible evaluation of \(t/s\) eventually reaches a configuration from the set P. \(\begin{equation*} {\rm\small EVENTUALLY-STEP-CHAINED}:\qquad {t / s \longrightarrow \big \lbrace { (t^{\prime },s^{\prime }) \;} \,\big |\, {\; t^{\prime } / s^{\prime } \longrightarrow ^\lozenge P } \big \rbrace } \quad \Rightarrow \quad {t / s \longrightarrow ^\lozenge P} \end{equation*}\)

One may wonder why we did not use this rule directly in the inductively defined judgment, and the reason is Coq’s strict positivity requirement. The considerations for encoding sequencing here are similar to those discussed in Appendix A in the context of the omni-big-step let-binding rule.

Another interesting property of the judgment \(t / s \longrightarrow ^\lozenge P\) is its cut rule, which is derivable. It asserts the following: if every possible evaluation of \(t/s\) eventually reaches a configuration in the set \(P^{\prime }\), and if every configuration from the set \(P^{\prime }\) eventually reaches a configuration from the set P, then every possible evaluation of \(t/s\) eventually reaches a configuration from the set P. \(\begin{equation*} {\rm\small EVENTUALLY-CUT}:\qquad t / s \longrightarrow ^\lozenge P^{\prime } \quad \wedge \quad \big (\forall (t^{\prime },s^{\prime }) \in P^{\prime } .\;\; t^{\prime } / s^{\prime } \longrightarrow ^\lozenge P \big) \quad \Rightarrow \quad t / s \longrightarrow ^\lozenge P \end{equation*}\)

This cut rule also admits a chained version, which reads as follows: if every possible evaluation of \(t/s\) eventually reaches a configuration that itself eventually reaches a configuration from the set P, then every possible evaluation of \(t/s\) eventually reaches a configuration from the set P. \(\begin{equation*} {\rm\small EVENTUALLY-CUT-CHAINED}:\qquad {t / s \longrightarrow ^\lozenge \big \lbrace { (t^{\prime },s^{\prime }) \;} \,\big |\, {\; t^{\prime } / s^{\prime } \longrightarrow ^\lozenge P } \big \rbrace } \quad \Rightarrow \quad {t / s \longrightarrow ^\lozenge P} \end{equation*}\) The cut rule and the chained rules are particularly handy to work with, as we illustrate in Section 6.6.

3.4 Coinductive Interpretation of the Omni-Small-Step Judgment

Let \({ t / s }\longrightarrow ^\lozenge _{\textsf {co}}{ P }\) denote the coinductive interpretation of the two rules that define \(t / s \longrightarrow ^\lozenge P\). Divergence can be captured by instantiating P as the empty set. We prove that the judgment \({t / s}\longrightarrow ^\lozenge _{\textsf {co}}{\emptyset }\) is equivalent to the standard small-step characterization of divergence, which asserts that any execution prefix may be extended with at least one additional step. \(\begin{equation*} \begin{array}[!t]{l} {\rm\small CO-EVENTUALLY-EMPTY-IFF-SMALL-STEP-DIVERGES}\\ {t / s}\longrightarrow ^\lozenge _{\textsf {co}}{\emptyset } \qquad \iff \qquad \forall s^{\prime }t^{\prime } .\;\, (t / s \longrightarrow ^*t^{\prime } / s^{\prime }) \, \Rightarrow \,\big (\exists t^{\prime \prime }s^{\prime \prime } .\;\; t^{\prime } / s^{\prime } \longrightarrow t^{\prime \prime } / s^{\prime \prime } \big) \end{array} \end{equation*}\)

Besides, we can relate the coinductive omni-small-step judgment \({ t / s }\longrightarrow ^\lozenge _{\textsf {co}}{ P }\) to the coinductive omni-big-step judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) defined in Section 2.4. Here again, we let Q denote a set of final configurations. We prove the following equivalence: \(\begin{equation*} \begin{array}[!t]{l} \scriptsize{\text{CO-EVENTUALLY-IFF-CO-OMNI-BIG-STEP}:} \qquad {t / s}\longrightarrow ^\lozenge _{\textsf {co}}{Q} \quad \iff \quad {t / s}\,\Downarrow ^{\textsf {co}}\,{Q} \end{array} \end{equation*}\)

The proofs of these two equivalences co-eventually-iff-co-omni-big-step and co-eventually-empty-iff-small-step-diverges, as well as the proof of co-omni-big-iff-safe-and-correct from Section 3.4, are interesting in that they involve yet another judgment. This judgment, written \({t / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\), is defined in terms of the standard small-step semantics, by taking the coinductive interpretation of the following two rules:

The desired equivalences are established in three steps. First, we prove that the standard small-step characterization of partial correctness that appears in the statement of co-omni-big-iff-safe-and-correct (Section 3.4) is equivalent to this new coinductive judgment \({t / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\). The proof is relatively straightforward because both of these characterizations are expressed using small-step transitions.

Second, we prove that the co-eventually judgment \({t / s}\longrightarrow ^\lozenge _{\textsf {co}}{Q}\) is equivalent to \({t / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\). The proof is relatively straightforward because the coinductive definitions for these two judgments share a similar structure. As a corollary, by instantiating Q as the empty set, we establish co-eventually-empty-iff-small-step-diverges.

Third, we prove that the co-omni-big-step judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) is equivalent to \({t / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\). This third proof is the most challenging, especially for establishing the implication from the small-step-style judgment to the big-step-style judgment. The proof involves a key intermediate lemma, which consists of an inversion rule for let-bindings: if \({(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\) holds, then there exists a set \(Q_1\) such that \({t_1 / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q_1}\) and \(\forall (v_1,s^{\prime })\in Q_1 .\;{([v_1/x]\,t_2) / s^{\prime }}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\) hold. The proof of this key lemma itself relies on two auxiliary results, whose purpose is to justify that we can take as witness for \(Q_1\) the strongest postcondition of \(t_1/s\). The first one asserts that \({(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\) implies \({t_1 / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{ \lbrace (v_1,s^{\prime }) \;|\; t_1 / s \longrightarrow ^*v_1 / s^{\prime } \rbrace }\). The second one asserts that \({(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) / s}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\) and \(t_1 / s \longrightarrow ^*v_1 / s^{\prime }\) imply \({([v_1/x]\,t_2) / s^{\prime }}\longrightarrow \!\!\!\!\!\!\!\!\!\!\longrightarrow ^{*}_{\textsf {co}}{Q}\). We refer to our Coq development for details.

A key observation about all the proofs involved in Sections 2 and 3 is that they are constructive.5 In particular, we are able to establish equivalences betweeen coinductive omni-big-step semantics and small-step style semantics without recourse to classical logic. This contrasts with coinductive big-step semantics [Leroy and Grall 2009], whose connection to small-step semantics requires classical logic. We discuss this aspect further in the related work section (Section 8).

Skip 4TYPE-SAFETY PROOFS USING OMNISEMANTICS Section

4 TYPE-SAFETY PROOFS USING OMNISEMANTICS

In this section, we show how the omni-small-step and omni-big-step judgments may be used to carry out type-safety proofs. We illustrate the proof structures using simple types (STLC). As a warm-up, we begin with a presentation of type safety on the restriction to the state-free fragment of our running-example language.

For this section, we need to consider a different semantics for the random-number generator. Indeed, the current rule omni-big-rand asserts that the program is stuck if \(\textsf {rand}\,n\) is invoked with an argument \(n\le 0\). Since here we are interested in proving that well-typed programs do not get stuck, let us consider a modified semantics, where \(\textsf {rand}\,n\) is turned into a total function that returns 0 when \(n\le 0\).

Additionally, for this section, we also exclude the primitive operation \(\textsf {free}\), which is not type-safe.

The grammar of types, written T, appears below: \(\begin{equation*} \begin{array}{l@{\,}l@{\quad }l} T & \quad := \quad & {\textsf {unit}}\;\, | \;\, \textsf {bool}\;\, | \;\, \textsf {int}\;\, | \;\, {T}\rightarrow {T} \;\, | \;\, \textsf {ref}\,T \\ \end{array} \end{equation*}\) A typing environment, written E, maps variable names to types. The judgment \(\, \vdash \,v \, : \,T\) asserts that the closed value v admits the type T. The judgment \(E \, \vdash \,t \, : \,T\) asserts that the term t admits type T in the environment E. We let \(\mathbb {V}\) denote the set of terms that are either values or variables—recall that we consider A-normal forms to simplify the presentation. The typing rules are essentially standard, apart from the fact that they involve side conditions of the form \(t\in \mathbb {V}\) to constrain terms to be in A-normal form. We include here two example rules; the other rules are given in Appendix E.

4.1 Omni-Small-Step Type-Safety Proof for a State-Free Language

A stuck term is a term that is not a value and that cannot take a step. Type safety asserts that if a closed term t is well-typed, then none of its possible evaluations get stuck. In other words, if t reduces in a number of steps to \(t^{\prime }\), then \(t^{\prime }\) either is a value or can further reduce. \(\begin{equation*} \begin{array}{l} \scriptsize{\text{TYPE-SAFETY (STATE-FREE LANGUAGE)}:}\\ \qquad (\emptyset \, \vdash \,t \, : \,T) \;\, \wedge \;\,(t \longrightarrow ^*t^{\prime }) \quad \Rightarrow \quad (\textsf {isvalue}\,t^{\prime }) \;\, \vee \;\,(\exists t^{\prime \prime } .\; t^{\prime } \longrightarrow t^{\prime \prime }) \end{array} \end{equation*}\)

The traditional approach to establishing type safety is by proving the preservation and progress properties [Wright and Felleisen 1994; Pierce 2002]. \(\begin{equation*} \begin{array}{l@{\quad \quad }l} \scriptsize{\text{PRESERVATION (STATE-FREE LANGUAGE)}:} & E \, \vdash \,t \, : \,T\quad \wedge \quad t \longrightarrow t^{\prime } \quad \Rightarrow \quad E \, \vdash \,t^{\prime } \, : \,T \\ \scriptsize{\text{PROGRESS (STATE-FREE LANGUAGE)}:} & \emptyset \, \vdash \,t \, : \,T\quad \Rightarrow \quad (\textsf {isvalue}\,t) \;\, \vee \;\,(\exists t^{\prime } .\; t \longrightarrow t^{\prime }) \end{array} \end{equation*}\) Each of these proofs is most typically carried out by induction on the typing judgment. One difficulty that might arise in the type-preservation proof for a large language with dozens (if not hundreds) of typing rules is the fact that one needs, for each case of the typing judgment \(E \, \vdash \,t \, : \,T\), to inspect all the potential cases of the reduction judgment \(t \longrightarrow t^{\prime }\). This inspection is not really quadratic in practice, because one can filter out applicable rules based on the shape of the term t. Nevertheless, a typical Coq proof performing “intros HT HR; induction HT; inversion HR” does produce a proof term whose size is quadratic in the number of term constructs. Coq users have experienced performance challenges with quadratic-complexity proof terms when formalizing PL metatheory [Monin and Shi 2013].

Interestingly, in the particular case of a deterministic language, there exists a known strategy (e.g., of Rompf and Amin [2016]) to reformulate the preservation and progress statements in a way that not only factors out the two into a single statement but also can be proved with a linear-size proof term. This combined statement, shown below, asserts that a well-typed term t either is a value or can make a step toward a term \(t^{\prime }\) that admits the same type. \(\begin{equation*} \begin{array}{l@{\qquad \qquad }l} \scriptsize{\text{INDUCTION-FOR-TYPE-SAFETY, STATE-FREE, STANDARD SMALL-STEP, DETERMINISTIC}} \\ \qquad \emptyset \, \vdash \,t \, : \,T\quad \Rightarrow \quad \big (\textsf {isvalue}\,t\big) \;\, \vee \;\,\big (\exists t^{\prime } .\; (t \longrightarrow t^{\prime }) \, \wedge \,(\emptyset \, \vdash \,t^{\prime } \, : \,T)\big) \\ \end{array} \end{equation*}\)

As we explain next, this approach can be generalized to the case of nondeterministic languages using the omni-small-step judgment. Let us write \(t \longrightarrow P\) for the judgment that corresponds to \(t / s \longrightarrow P\) without the state argument. We can state type safety by considering for the postcondition P the set of terms \(t^{\prime }\) that admit the same type as t.

Lemma 4.1 (Induction-for-type-safety, State-free, Omni-small-step, Nondeterministic).

\(\begin{equation*} \begin{array}{l@{\qquad \qquad }l} \qquad \emptyset \, \vdash \,t \, : \,T\quad \Rightarrow \quad \big ({\textsf {isvalue}}\,t\big) \;\, \vee \;\,\big (t \longrightarrow \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace \big) \end{array} \end{equation*}\)

Proof.

The proof is carried out by induction on the typing judgment. For the case where t is a value, the left part of the disjunction applies. For all other cases, the right part needs to be established. We next detail two representative proof cases.

Case 1: The term t has been typed using rule typ-rand. In this case, the term t has the form “\(\textsf {rand}\,{t_1}\)”. The rule concludes \(\emptyset \, \vdash \,(\textsf {rand}\,t_1) \, : \,\textsf {int}\), from the premise \(\emptyset \, \vdash \,t_1 \, : \,\textsf {int}\) and the premise \(t_1 \in \mathbb {V}\). The latter means that \(t_1\) is either a value or a variable (recall that we assume A-normal form to simplify the presentation). Because \(t_1\) typechecks in the empty environment, it cannot be a variable. Thus, it must be a value, and because this value has type \(\textsf {int}\), it must be an integer value. (In other words, \(\emptyset \, \vdash \,t_1 \, : \,\textsf {int}\) must have been derived using the rules typ-val and vtyp-int stated in Appendix E.) Let us call n this integer. We need to establish \((\textsf {rand}\,{n}) \longrightarrow \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,\textsf {int}) \big \rbrace\). Recall the rule omni-small-rand-complete introduced at the start of Section 4. We apply this rule (ignoring the state component) and need to establish its premise: \(\forall m . \; 0 \le m \lt \max (n,1)\, \Rightarrow \,m\in \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \, \textsf {int}) \big \rbrace\). Consider an integer m such that \(0 \le m \lt \max (n,1)\). We are left to prove \(\emptyset \, \vdash \,m \, : \, \textsf {int}\), which is derivable from the rules typ-val and vtyp-int.

Case 2: The term t has been typed using rule typ-let. In this case, the term t has the form “\(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\)”. The rule concludes \(\emptyset \, \vdash \,(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) \, : \,T\), from the two premises \(\emptyset \, \vdash \,t_1 \, : \,T_1\) and \(x :T_1 \, \vdash \,t_2 \, : \,T\). We need to prove \((\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) \longrightarrow \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace\). By the induction hypothesis applied to the first assumption, either \(t_1\) is a value or \(t_1 \longrightarrow \big \lbrace t_1^{\prime } \;\big |\; (\emptyset \, \vdash \,t_1^{\prime } \, : \,T_1) \big \rbrace\).

In the first subcase, \(t_1\) is a value; let us call it \(v_1\). We exploit omni-small-let and are left to justify \(([v_1/x]\,t_2) \in \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace\), that is, \(\emptyset \, \vdash \,([v_1/x]\,t_2) \, : \,T\). This result follows from the standard substitution lemma applied to \(x :T_1 \, \vdash \,t_2 \, : \,T\) and to \(\emptyset \, \vdash \,v_1 \, : \,T_1\).

In the second subcase, we have \(t_1 \longrightarrow \big \lbrace t_1^{\prime } \;\big |\; (\emptyset \, \vdash \,t_1^{\prime } \, : \,T_1) \big \rbrace\). To prove \((\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) \longrightarrow \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace\), we exploit omni-small-let-ctx with \(P_1 = \big \lbrace t_1^{\prime } \;\big |\; (\emptyset \, \vdash \,t_1^{\prime } \, : \,T_1) \big \rbrace\). We need to justify the second premise of that rule: \(\forall t_1^{\prime }\in P_1 .\; (\textsf {let}\, x = t_1^{\prime } \,\textsf {in}\, t_2) \in \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace\). Consider a particular \(t_1^{\prime }\). The assumption \(t_1^{\prime }\in P_1\) is equivalent to \(\emptyset \, \vdash \,t_1^{\prime } \, : \,T_1\). The proof obligation \((\textsf {let}\, x = t_1^{\prime } \,\textsf {in}\, t_2) \in \big \lbrace t^{\prime } \;\big |\; (\emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace\) is equivalent to \(\emptyset \, \vdash \,(\textsf {let}\, x = t_1^{\prime } \,\textsf {in}\, t_2) \, : \,T\). This result follows from the rule typ-let applied to the facts \(\emptyset \, \vdash \,t_1^{\prime } \, : \,T_1\) and \(x :T_1 \, \vdash \,t_2 \, : \,T\).□

The statement induction-for-type-safety above entails the preservation property (for empty environments) and the progress property. We prove once and for all that the statement of induction-for-type-safety entails the type-safety property.6

4.2 Omni-Small-Step Type-Safety Proof for an Imperative Language

Let us now generalize the results from the previous section to account for memory operations.

A store-typing environment, written S, is a map from locations to types. The typing judgment for values is extended with a store-typing environment, taking the form \(S \, \vdash \,v \, : \,T\). Likewise, the typing judgment for terms is extended to the form \(S; E \, \vdash \,t \, : \,T\). The store-typing entity S only plays a role in the typing rule for memory locations. The rules for typing memory locations and memory operations are standard; they appear in Appendix F.

The type-safety property asserts that the execution of any well-typed term, starting from the empty state, does not get stuck. In the statement below, \(\varnothing\) denotes an empty state or an empty store typing, whereas \(\emptyset\) denotes, as before, the empty typing context. \(\begin{equation*} \begin{array}{l} \scriptsize{\text{TYPE-SAFETY}:}\\ \qquad (\varnothing ; \emptyset \, \vdash \,t \, : \,T) \;\, \wedge \;\,(t / \varnothing \longrightarrow ^*t^{\prime } / s^{\prime }) \quad \Rightarrow \quad (\textsf {isvalue}\,t^{\prime }) \;\, \vee \;\,(\exists t^{\prime \prime } s^{\prime \prime } .\; t^{\prime } / s^{\prime } \longrightarrow t^{\prime \prime } / s^{\prime \prime }) \end{array} \end{equation*}\)

To establish a type-safety result by induction on a reduction sequence, one needs to introduce a typing judgment for stores. A store s admits type S, written \(\, \vdash \,s \, : \,S\), if and only if s and S have the same domain and, for any location p in the domain, \(s[p]\) admits the type \(S[p]\). Formally: \(\begin{equation*} \, \vdash \,s \, : \,S \qquad \equiv \qquad \big (\textsf {dom}\,s = \textsf {dom}\,S\big) \;\, \wedge \;\,\big (\forall p\in \textsf {dom}\,s .\;\; S; \emptyset \, \vdash \,s[p] \, : \,S[p]\big) \end{equation*}\)

The preservation and progress lemmas associated with the traditional approach to proving type safety are updated as shown below. In particular, the preservation lemma requires the output state to admit a type that extends the store typing associated with the input state \(({S^{\prime }\supseteq S})\). \(\begin{equation*} \begin{array}{l@{\qquad }l} \scriptsize{\text{PRESERVATION}:} & \phantom{\Rightarrow \quad \; } t / s \longrightarrow t^{\prime } / s^{\prime } \quad \wedge \quad \, \vdash \,s \, : \,S \quad \wedge \quad S; \emptyset \, \vdash \,t \, : \,T \\ & \Rightarrow \quad \exists S^{\prime }\supseteq S .\;\, \, \vdash \,s^{\prime } \, : \,S^{\prime } \quad \wedge \quad S^{\prime }; \emptyset \, \vdash \,t^{\prime } \, : \,T \\ \scriptsize{\text{PROGRESS}:} & S; \emptyset \, \vdash \,t \, : \,T\quad \wedge \quad \, \vdash \,s \, : \,S\quad \Rightarrow \quad (\textsf {isvalue}\,t) \;\, \vee \;\,(\exists t^{\prime }s^{\prime } .\; t / s \longrightarrow t^{\prime } / s^{\prime }) \end{array} \end{equation*}\)

In contrast, using the omni-small-step judgment, we can establish type safety through a single induction on the typing judgment. To that end, we formulate a lemma in terms of the predicate \(t / s \longrightarrow P\), instantiating the set P as the set of configurations \(t^{\prime }/s^{\prime }\) such that \(t^{\prime }\) admits the same type as t and such that \(s^{\prime }\) admits a type that extends the type of s. \(\begin{equation*} \begin{array}{l@{\qquad \qquad }l} \scriptsize{\text{INDUCTION-FOR-TYPE-SAFETY (OMNI-SMALL-STEP, WITH STATE)}} \\ \quad \;\qquad \big (S; \emptyset \, \vdash \,t \, : \,T) \quad \wedge \quad \big (\, \vdash \,s \, : \,S\big) \\ \quad \Rightarrow \quad \big (\textsf {isvalue}\,t\big) \;\, \vee \;\,\big (t / s \longrightarrow \big \lbrace (t^{\prime },s^{\prime }) \;\big |\;\exists S^{\prime }\supseteq S .\;\, (\, \vdash \,s^{\prime } \, : \,S^{\prime }) \, \wedge \,(S^{\prime }; \emptyset \, \vdash \,t^{\prime } \, : \,T) \big \rbrace \big) \end{array} \end{equation*}\)

4.3 Omni-Big-Step Type-Safety Proof for an Imperative Language

Traditionally, a big-step safety proof can only be carried out if the semantics is completed using error-propagation rules. Here, we demonstrate how to establish type safety with respect to an omni-big-step judgment, without any need for error-propagation rules. First, we introduce the construct \([\![ T/S ]\!]\) to denote the set of possible outputs produced by a term of type T, well-typed in a store of type S. Second, we describe the statement and proof for type safety.

Consider a type T and a store typing S. We define \([\![ T/S ]\!]\) as the set of final configurations of the form \(v/s\) such that the state s admits a type \(S^{\prime }\) that extends S, and the value v admits type T, under the store typing \(S^{\prime }\). The extension \(S^{\prime }\) involved here accounts for the fact that the evaluation of a term t of type T may perform allocation operations that extend the store in which t is well-typed. \(\begin{equation*} [\![ T/S ]\!] \quad \equiv \quad \big \lbrace (v,s) \;\,|\;\, \exists S^{\prime }\supseteq S .\;\, (\, \vdash \,s \, : \,S^{\prime }) \, \wedge \,(S^{\prime } \, \vdash \,v \, : \,T) \big \rbrace \end{equation*}\) A key lemma involved in the type soundness proof asserts that, if \(S^{\prime }\) is a store typing that enforces more constraints than another store typing S, then \([\![ T/S^{\prime } ]\!]\) is a smaller set than \([\![ T/S ]\!]\).

Lemma 4.2 (Configuration-typing-subset).

\(\begin{equation*} S^{\prime }\supseteq S \quad \Rightarrow \quad [\![ T/S^{\prime } ]\!] \subseteq [\![ T/S ]\!] \end{equation*}\)

Proof.

Assume \(S^{\prime }\supseteq S\). Consider a pair \((v,s) \in [\![ T/S^{\prime } ]\!]\). By definition, there exists \(S^{\prime \prime }\) such that \(S^{\prime \prime }\supseteq S^{\prime }\) and \(\, \vdash \,s \, : \,S^{\prime \prime }\) and \(S^{\prime \prime } \, \vdash \,v \, : \,T\). By transitivity, \(S^{\prime \prime }\supseteq S\). We conclude that \((v,s) \in [\![ T/S ]\!]\) holds, by taking \(S^{\prime \prime }\) as witness for the existential quantifier in the definition of \([\![ T/S ]\!]\).□

We are now ready to state type safety. The coinductive omni-big-step judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{[\![ T/S ]\!] }\) asserts that any evaluation of \(t/s\) executes safely, without ever getting stuck, and that if an evaluation reaches a final configuration \(v/s^{\prime }\), then this configuration satisfies the postcondition \([\![ T/S ]\!]\). Given our definition of \([\![ T/S ]\!]\), the judgment \({t / \varnothing }\,\Downarrow ^{\textsf {co}}\,{[\![ T/\varnothing ]\!] }\) thus captures exactly the type-safety property associated with the typing judgment \(\varnothing ; \emptyset \, \vdash \,t \, : \,T\). Type safety may be established by proving the following statement by coinduction.

Lemma 4.3 (Coinduction-for-type-safety, Omni-big-step, Nondeterministic).

\(\begin{equation*} \begin{array}{l} \qquad S; \emptyset \, \vdash \,t \, : \,T \quad \wedge \quad \, \vdash \,s \, : \,S \quad \Rightarrow \quad {t / s}\,\Downarrow ^{\textsf {co}}\,{[\![ T/S ]\!] } \end{array} \end{equation*}\)

Proof.

For technical reasons, the Coq coinduction tactic needs to be applied to the following statement, which introduces an intermediate set Q: \(\begin{equation*} S; \emptyset \, \vdash \,t \, : \,T \quad \wedge \quad \, \vdash \,s \, : \,S \quad \wedge \quad [\![ T/S ]\!] \subseteq Q \quad \Rightarrow \quad {t / s}\,\Downarrow ^{\textsf {co}}\,{Q} \end{equation*}\) Observe that this alternative statement is logically equivalent to the previous one: on the one hand, we can instantiate Q as \([\![ T/S ]\!]\); on the other hand, we can exploit omni-big-conseqence to prove \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) from \({t / s}\,\Downarrow ^{\textsf {co}}\,{[\![ T/S ]\!] }\) and \([\![ T/S ]\!] \subseteq Q\).

We carry out a proof by coinduction on that alternative statement. The coinduction hypothesis asserts that we can assume the alternative statement to hold, provided that we have already applied at least one evaluation rule (i.e., a coinductive constructor) to the conclusion at hand (\({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\)).

The first step of the proof is to perform a case analysis on the typing hypothesis \(S; \emptyset \, \vdash \,t \, : \,T\). We then consider each of the possible typing rules one by one. Let us consider two representative proof cases: the case of \(\textsf {rand}\) and the case of a let-binding. In each case, the assumptions are \(S; \emptyset \, \vdash \,t \, : \,T\) and \(\, \vdash \,s \, : \,S\) and \([\![ T/S ]\!] \subseteq Q\); the goal is to prove \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\).

Case 1: The term t has been typed using rule typ-rand. In this case, the term t has the form “\(\textsf {rand}\,{t_1}\)”, and T is \(\textsf {int}\). The rule concludes \(S; \emptyset \, \vdash \,(\textsf {rand}\,t_1) \, : \,\textsf {int}\), from the premise \(S; \emptyset \, \vdash \,t_1 \, : \,\textsf {int}\) and the premise \(t_1 \in \mathbb {V}\). Because \(t_1\) typechecks in the empty environment, it must be a value. Because this value has type \(\textsf {int}\), it must be an integer value; let us call it n. We need to establish \({(\textsf {rand}\,{n}) / s}\,\Downarrow ^{\textsf {co}}\,{Q}\). We apply the rule co-omni-big-rand-complete, which is like omni-big-rand-complete but part of the coinductive interpretation of the set of evaluation rules. We need to prove its premise: \(\forall m . \; 0 \le m \lt \max (n,1)\, \Rightarrow \,(m,s) \in Q\). Consider a particular m in that range. We have \([\![ \textsf {int}/S ]\!] \subseteq Q\). Thus, to show \((m,s) \in Q,\) it suffices to show \((m,s) \in [\![ \textsf {int}/S ]\!]\). By definition of the operator \([\![ T/S ]\!]\), this amounts to proving \(\exists S^{\prime }\supseteq S .\;\, (\, \vdash \,s \, : \,S^{\prime }) \, \wedge \,(S^{\prime } \, \vdash \,m \, : \,\textsf {int})\). We conclude by taking \(S^{\prime }=S\) and checking that \(\, \vdash \,s \, : \,S\) and \(S^{\prime } \, \vdash \,m \, : \,\textsf {int}\) indeed hold.

Case 2: The term t has been typed using rule typ-let. In this case, the term t has the form “\(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\)”. The rule concludes \(S; \emptyset \, \vdash \,(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) \, : \,T\), from the two premises \(S; \emptyset \, \vdash \,t_1 \, : \,T_1\) and \(S; (x :T_1) \, \vdash \,t_2 \, : \,T\). We need to establish \({(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) / s}\,\Downarrow ^{\textsf {co}}\,{Q}\). We apply the rule co-omni-big-let (which is like omni-big-let but part of the coinductive interpretation of the set of evaluation rules) with \(Q_1\) instantiated as \([\![ T_1/S ]\!]\). We have to establish the two premises: \({t_1 / s}\,\Downarrow \,{[\![ T_1/S ]\!] }\) and \(\forall (v^{\prime },s^{\prime })\in [\![ T_1/S ]\!] . \; {([v^{\prime }/x]\,t_2) / s^{\prime }}\,\Downarrow \,{Q}\). The first premise follows directly from the coinduction hypothesis applied to \(S; \emptyset \, \vdash \,t_1 \, : \,T_1\) and to \([\![ T_1/S ]\!] \subseteq [\![ T_1/S ]\!]\). For the second premise, consider a pair \((v^{\prime },s^{\prime })\in [\![ T_1/S ]\!]\). This amounts to assuming the existence of some \(S^{\prime }\) such that \(S^{\prime }\supseteq S\) and \(\, \vdash \,s^{\prime } \, : \,S^{\prime }\) and \(S^{\prime } \, \vdash \,v \, : \,T_1\). There remains to show \({([v^{\prime }/x]\,t_2) / s^{\prime }}\,\Downarrow \,{Q}\). A standard “type preservation upon store typing extension” lemma shows that, because \(S^{\prime }\supseteq S\), we can refine \(S\,; (x :T_1) \, \vdash \,t_2 \, : \,T\) into \(S^{\prime }\,; (x :T_1) \, \vdash \,t_2 \, : \,T\). Then, by the standard substitution lemma applied to \(S^{\prime }\,; (x :T_1) \, \vdash \,t_2 \, : \,T\) and to \(S^{\prime } \, \vdash \,v \, : \,T_1\), we derive \(S^{\prime }; \emptyset \, \vdash \, ([v^{\prime }/x]\,t_2) \, : \,T\). Besides, the lemma configuration-typing-subset applied to \(S^{\prime }\supseteq S\) gives \([\![ T/S^{\prime } ]\!] \subseteq [\![ T/S ]\!]\). Composing this subset relation by transitivity with \([\![ T/S ]\!] \subseteq Q\) yields \([\![ T/S^{\prime } ]\!] \subseteq Q\). The conclusion \({([v^{\prime }/x]\,t_2) / s^{\prime }}\,\Downarrow \,{Q}\) then follows from the coinduction hypothesis applied to \(S^{\prime }; \emptyset \, \vdash \, ([v^{\prime }/x]\,t_2) \, : \,T\) and \(\, \vdash \,s^{\prime } \, : \,S^{\prime }\) and \([\![ T/S^{\prime } ]\!] \subseteq Q\).

Note that most of these arguments are easily handled by automated proof search in Coq.□

Like for the small-step settings, we proved once and for all that the statement coinduction-for-type-safety entails type-safety.

Our coinductive omni-big-step approach offers, to those who have good reasons to work with a big-step-style semantics, a means to establish type safety without introducing error rules.

Regarding the comparison with the standard preservation-and-progress approach, at this stage we cannot draw general conclusions on whether omni-big-step and omni-small-step type-safety proofs are more effective, because we considered a relatively simple language. Nevertheless, it appears that each of the two approaches that we propose results in proof scripts that (1) require only one induction or one coinduction instead of two separate inductions, (2) are no longer than with preservation and progress separated, and (3) avoid the issue of nested inversions requiring a number of cases quadratic in the size of the language.

Skip 5DEFINITION OF PROGRAM PROOF RULES Section

5 DEFINITION OF PROGRAM PROOF RULES

This section discusses the construction of a foundational program logic, that is, a program logic whose reasoning rules are derived through mechanized proofs from the formal semantics of the targeted programming language. We specifically focus on Separation Logic [O’Hearn et al. 2001; Reynolds 2002], which has proved in the past two decades to be an invaluable tool for carrying out practical, modular program verification, both for low-level and high-level languages—see the broad survey by O’Hearn [2019] and the survey by Charguéraud [2020] that focuses on sequential programs.

We first review the properties that a program logic might capture, and we describe the key challenges in deriving a foundational Separation Logic that captures total correctness with respect to a standard big-step semantics (Section 5.1). We then explain how omnisemantics overcome these challenges, allowing one to derive a foundational, total-correctness Separation Logic judgment in a straightforward, direct manner (Section 5.2). Moreover, by referring to the coinductive omni-big-step judgment instead of the inductive one, one can similarly define partial-correctness triples. We explain how to derive the reasoning rules (Section 5.3) and in particular the frame rule of Separation Logic (Section 5.4). Finally, we present reasoning rules in weakest-precondition style (Section 5.5), which have proved very useful to set up practical tools, and which turn out to be even easier to derive.

5.1 Challenges in Defining Foundational Separation Logic Triples

A triple, written \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\), describes the behavior of the evaluation of the configurations \(t/s\) for any s satisfying the precondition H, in terms of the postcondition Q. The exact interpretation of a triple depends on whether it accounts for total correctness or partial correctness, which differ on how they account for termination. For nondeterministic languages, the key notions of interest for definining a triple \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\) are as follows:

Safety: For any s satisfying H, none of the possible evaluations of \(t/s\) can get stuck.

Correctness: For any s satisfying H, if \(t/s\) can evaluate to \(v/s^{\prime }\), then \(Q\,v\,s^{\prime }\) holds.

Termination: For any s satisfying H, all possible evaluations of \(t/s\) are finite.

Partial correctness: Safety and correctness hold.

Total correctness: Safety, correctness, and termination hold.

When targeting total correctness, one key challenge in defining triples with respect to a standard big-step semantics is that the direct definition of Hoare triples yields a judgment that does not satisfy the frame rule of Separation Logic. The frame rule asserts that if a triple \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\) holds, then the pre- and the postcondition may be extended with an arbitrary predicate \(H^{\prime }\), yielding the valid triple \(\lbrace H \star H^{\prime } \rbrace \;t\;\lbrace Q {⋆̣} H^{\prime } \rbrace\). Here, \(Q {⋆̣} H\) denotes the postcondition \(\lambda v.\,{ (Q\,v\star H)}\).

Concretely, consider the following definition of a Hoare triple with respect to a standard big-step, deterministic semantics. It asserts that, for any input state s satisfying the precondition H, there exists a result value v and a final state \(s^{\prime }\) such that the configuration \(t/s\) evaluates to a final configuration \(v/s^{\prime }\) that satisfies the postcondition Q. \(\begin{equation*} {}^{\textsf {Hoare}}\,\lbrace H\rbrace \,t\;\lbrace Q\rbrace \quad \equiv \quad \forall s .\; H\,s \, \Rightarrow \,\exists v .\,\exists s^{\prime } .\; (t / s \Downarrow v / s^{\prime }) \, \wedge \,(Q\,v\,s^{\prime }). \end{equation*}\)

For such a judgment, one can prove that, starting from an empty heap, the allocation of a reference returns a specific memory location, say 0. For example, if the reference contains 3 and the location l denotes its address, one can prove \({}^{\textsf {Hoare}}\,\lbrace [\,]\rbrace \,(\textsf {ref}\,3)\;\lbrace \lambda l.\,\;[l = 0]\star (0 \hookrightarrow 3)\rbrace\). To see why the judgment does not satisfy the frame rule, let us attempt to extend the pre- and the postcondition of this triple with the heap predicate \((0 \hookrightarrow 1)\), which denotes a reference at location 0 storing the value 1. We obtain \({}^{\textsf {Hoare}}\,\lbrace 0 \hookrightarrow 1\rbrace \,(\textsf {ref}\,3)\;\lbrace \lambda l.\,\;[l = 0]\star (0 \hookrightarrow 3) \star (0 \hookrightarrow 1)\rbrace\). This triple does not hold, because the separating conjunction \((0 \hookrightarrow 3) \star (0 \hookrightarrow 1)\) is equivalent to \(\textsf {False}\).

To derive a Separation Logic judgment that does satisfy the frame rule, one can exploit the classic technique of the baked-in frame rule [Birkedal et al. 2005]—for technical and historical details, we refer to Charguéraud [2020, Sections 5.1 and 10.2]. Separation Logic triples are defined as follows: \(\begin{equation*} {}^{\textsf {Sep. Logic via baked-in frame rule}}\,\lbrace H\rbrace \,t\;\lbrace Q\rbrace \quad \equiv \quad \forall H^{\prime } .\;\; {}^{\textsf {Hoare}}\,\lbrace H \star H^{\prime }\rbrace \,t\;\lbrace Q {⋆̣} H^{\prime }\rbrace \end{equation*}\) This definition quantifies over a heap predicate \(H^{\prime }\) that describes the “rest of the world.” The resulting triples inherently satisfy the frame rule, as a direct consequence of the associativity of the separating-conjunction operator. Indirectly, the introduction of \(H^{\prime }\) rules out the judgments whose postconditions refer to specific locations, such as in the aforementioned counterexample.

The two-stage construction presented above, for building Separation Logic triples on top of the standard big-step judgment via the baked-in frame rule technique, can be applied to deterministic languages or to languages that are deterministic up to the choice of memory addresses. In what follows, we show that, by grounding Separation Triples not on top of standard big-step semantics but instead on top of omnisemantics, we can avoid the need to go through the two-stage construction associated with the baked-in frame rule technique. Moreover, the omnisemantics construction applies to the general case of nondeterministic semantics, and it unfolds similarly for both total- and partial-correctness triples.

5.2 Definition of Hoare Triples w.r.t. Omni-Big-Step Semantics

Consider a possibly nondeterministic semantics. A total-correctness Hoare triple \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\) asserts that, for any input state s satisfying the precondition H, every possible execution of \(t/s\) terminates and satisfies the postcondition Q. This property can be captured using the inductive omni-big-step judgment as follows: \(\begin{equation*} {}^{\textsf {Hoare}}\,\lbrace H\rbrace \,t\;\lbrace Q\rbrace \quad \equiv \quad \forall s .\;\; H\,s \;\, \Rightarrow \;\,({t / s}\,\Downarrow \,{Q}) \end{equation*}\) Note that an omni-big-step judgment may be interpreted as a particular Hoare triple, featuring a singleton precondition to constrain the input state: \(\begin{equation*} \big ({t / s}\,\Downarrow \,{Q}\big) \quad \iff \quad {}^{\textsf {Hoare}}\,\lbrace (\lambda s^{\prime }.\,\;s^{\prime }=s)\rbrace \,t\;\lbrace Q\rbrace \end{equation*}\)

A partial-correctness Hoare triple asserts that, for any input state s satisfying the precondition H, every possible execution of \(t/s\) either diverges or terminates and satisfies the postcondition. This property can be captured using the coinductive omni-big-step judgment as follows: \(\begin{equation*} {}^{\textsf {Hoare, partial correctness}}\,\lbrace H\rbrace \,t\;\lbrace Q\rbrace \quad \equiv \quad \forall s .\;\; H\,s \;\, \Rightarrow \;\,({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}) \end{equation*}\) Note that instantiating Q with the always-false predicate in the partial-correctness triple yields a characterization of programs whose execution always diverges—and never gets stuck.

Throughout the rest of this section, we present results for total correctness. We write \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\) for the definition of \({}^{\textsf {Hoare}}\,\lbrace H\rbrace \,t\;\lbrace Q\rbrace\) given above. As we show, these triples inherently satisfy the frame rule and hence yield a Separation Logic. All the corresponding results for partial correctness hold and may be found in our Coq formalization.

5.3 Deriving Reasoning Rules for Hoare Triples

In a foundational program logic, reasoning rules take the form of lemmas proved correct with respect to the definition of triples and with respect to the semantics of the language. Consider, for example, the case of a let-binding. Let us compare the semantics rule omni-big-let with the Hoare-logic rule hoare-let, which are shown below. Throughout this section, we formulate rules by viewing postconditions as predicates of type \(\textsf {val}\rightarrow \textsf {state}\rightarrow \textsf {Prop}\), as this presentation style is more idiomatic in program logics. We also present reasoning rules using the horizontal bar, but keep in mind that the statements are not inductive definitions but lemmas.

The only difference between omni-big-let and hoare-let is that the first rule considers one specific state s, whereas the second rule considers a set of possible states satisfying the precondition H. By exploiting the fact that \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\) is defined as \(\forall s .\; H\,s \, \Rightarrow \,({t / s}\,\Downarrow \,{Q})\), it is straightforward to establish that hoare-let is a consequence of omni-big-let. The corresponding Coq proof script witnesses the simplicity of the proof: “intros. eapply mbig_let; eauto.

Likewise, we derive a version of the bind rule, which generalizes the let-binding rule (recall Section 2.5). For the reasoning rule, shown below, we purposely do not include the premise \(\lnot \; \textsf {value}\,t\).

As another example, consider the consequence rule. The Hoare-logic rule is, again, an immediate consequence of the omni-big-step rule.

5.4 Deriving the Frame Rule of Separation Logic

We next explain how to derive the frame rule for total-correctness triples. To that end, we first need to state and prove a key lemma capturing the preservation of the omni-big-step judgment \({t / s_1}\,\Downarrow \,{Q}\) when the input state \(s_1\) is augmented with a disjoint piece of state \(s_2\). We write \(s_1 \perp s_2\) to assert that \(s_1\) and \(s_2\) have disjoint domains.

Lemma 5.1 (Frame Property for Big-step Omnisemantics).

Proof.

The proof is carried out by induction on the omnisemantics judgment. There are two interesting cases in the proof: the treatment of an allocation (four lines of Coq script) and that of a let-binding (three lines of Coq script). In each case, we assume \(s_1 \perp s_2\).

Case 1: t is \(\textsf {ref}\,v\). The assumption is \({(\textsf {ref}\,v) / s_1}\,\Downarrow \,{Q}\). It is derived by the rule omni-big-ref, whose premise is \(\forall p\notin \textsf {dom}\,s_1 .\; Q\,p\,({s_1}[p :=v])\). We need to prove \({(\textsf {ref}\,v) / (s_1 \uplus s_2)}\,\Downarrow \,{(Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))}\). By omni-big-ref, we need to justify \(\forall p\notin \textsf {dom}\,(s_1 \uplus s_2) .\; (Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))\,p\,({(s_1 \uplus s_2)}[p :=v])\). Consider a location p not in \(\textsf {dom}\,s_1\) nor in \(\textsf {dom}\,s_2\). The predicate \((Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))\,p\) is equivalent to \((Q\,p) \star (\lambda s^{\prime }.\,\,s^{\prime }=s_2)\). The state update \({(s_1 \uplus s_2)}[p :=v]\) is equivalent to \(({s_1}[p :=v]) \uplus s_2\). Thus, there remains to prove \(((Q\,p) \star (\lambda s^{\prime }.\,\,s^{\prime }=s_2))\;(({s_1}[p :=v]) \uplus s_2)\). By definition of separating conjunction and exploiting \(({s_1}[p :=v]) \perp s_2\), it suffices to show \(Q\,p\,({s_1}[p :=v])\). This fact follows from \(\forall p\notin \textsf {dom}\,s_1 .\; Q\,p\,({s_1}[p :=v])\).

Case 2: t is “\(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\)”. The assumption is \({t / s_1}\,\Downarrow \,{Q}\). It is derived by the rule omni-big-let, whose premises are \({t_1 / s_1}\,\Downarrow \,{Q_1}\) and \(\forall v^{\prime } s^{\prime } . \; Q_1\,v^{\prime }\,s^{\prime } \Rightarrow {([v^{\prime }/x]\,t_2) / s^{\prime }}\,\Downarrow \,{Q}\). We need to prove \({(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) / (s_1 \uplus s_2)}\,\Downarrow \,{(Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))}\). To that end, we invoke omni-big-let. For its first premise, we prove \({t_1 / (s_1 \uplus s_2)}\,\Downarrow \,{(Q_1 {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))}\) by exploiting the induction hypothesis applied to \({t_1 / s_1}\,\Downarrow \,{Q_1}\). For the second premise, we have to prove \(\forall v^{\prime } s^{\prime \prime } . \; (Q_1 {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))\,v^{\prime }\,s^{\prime \prime } \Rightarrow {([v^{\prime }/x]\,t_2) / s^{\prime \prime }}\,\Downarrow \,{(Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))}\). Consider a particular \(v^{\prime }\) and \(s^{\prime \prime }\). The assumption \((Q_1 {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))\,v^{\prime }\,s^{\prime \prime }\) is equivalent to \(((Q_1\,v^{\prime }) \star (\lambda s^{\prime }.\,\,s^{\prime }=s_2))\,s^{\prime \prime }\). By definition of separating conjunction, we deduce that \(s^{\prime \prime }\) decomposes as \(s_1^{\prime } \uplus s_2\), with \(s_1^{\prime } \perp s_2\) and \(Q_1\,v^{\prime }\,s_1^{\prime }\), for some \(s_1^{\prime }\). There remains to prove \({([v^{\prime }/x]\,t_2) / (s_1^{\prime } \uplus s_2)}\,\Downarrow \,{(Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))}\). We first exploit \(\forall v^{\prime } s^{\prime } . \; Q_1\,v^{\prime }\,s^{\prime } \Rightarrow {([v^{\prime }/x]\,t_2) / s^{\prime }}\,\Downarrow \,{Q}\), on \(Q_1\,v^{\prime }\,s_1^{\prime }\) to obtain \({([v^{\prime }/x]\,t_2) / s_1^{\prime }}\,\Downarrow \,{Q}\). We then conclude by applying the induction hypothesis to the latter judgment.□

Lemma 5.2 (Frame Rule).

Proof.

Assume \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\). Recall from Section 5.2 that this judgment is defined as \(\forall s .\; H\,s \Rightarrow ({t / s}\,\Downarrow \,{Q})\). We have to prove \(\lbrace H \star H^{\prime } \rbrace \;t\;\lbrace Q {⋆̣} H^{\prime } \rbrace\), that is, \(\forall s .\; (H \star H^{\prime })\,s \, \Rightarrow \,({t / s}\,\Downarrow \,{(Q {⋆̣} H^{\prime })})\). Consider a particular s such that \((H \star H^{\prime })\,s\). By definition of separating conjunction, we can deduce that the input state s decomposes as \(s_1 \uplus s_2\), with \(s_1 \perp s_2\) and \(H\,s_1\) and \(H^{\prime }\,s_2\). The goal is to prove \({t / (s_1 \uplus s_2)}\,\Downarrow \,{(Q {⋆̣} H^{\prime })}\). By exploiting \(\forall s .\; H\,s \Rightarrow ({t / s}\,\Downarrow \,{Q})\) on \(H\,s_1\), we derive \({t / s_1}\,\Downarrow \,{Q}\). By invoking the lemma omni-big-frame on this judgment and on \(s_1 \perp s_2\), we derive \({t / (s_1 \uplus s_2)}\,\Downarrow \,{(Q {⋆̣} (\lambda s^{\prime }.\,\,s^{\prime }=s_2))}\). From there, to obtain the conclusion \({t / (s_1 \uplus s_2)}\,\Downarrow \,{(Q {⋆̣} H^{\prime })}\), it suffices to exploit the consequence rule omni-big-consequence and justify that \((\lambda s^{\prime }.\,\,s^{\prime }=s_2)\) entails \(H^{\prime }\). In other words, we need to show that for any state \(s^{\prime }\) being equal to \(s_2\), this state \(s^{\prime }\) does satisfy \(H^{\prime }\). Indeed, \(H^{\prime }\,s_2\) holds. (The Coq proof script for this lemma is four lines long.)□

In summary, the above proofs establish the frame property for the omni-big-step semantics and for the triples that we build on top of that semantics. Those results hold for the imperative \(\lambda\)-calculus that we have considered in this article. We believe that these results could be similarly established for other programming languages for which a Separation Logic can be set up. For example, we proved that the frame property holds for the omni-big-step semantics involved in the case study presented in Section 6.3.

5.5 Deriving Weakest-Precondition-Style Reasoning Rules

The weakest-precondition operator, written \(\textsf {wp}\,t\,Q\), computes the weakest predicate H for which the triple \(\lbrace H\rbrace \;t\;\lbrace Q\rbrace\) holds. Here, “weakest” is interpreted w.r.t. the entailment relation, written \(H \vdash H^{\prime }\) and defined as pointwise predicate implication \((\forall s .\, H\,s \Rightarrow H\,s^{\prime })\). Weakest reasoning rules are expressed as entailments. See, e.g., the rule for let-bindings and the bind rule shown below.

Many proof tools simply axiomatize the weakest-precondition rules. In a foundational approach, however, one needs to prove the reasoning rules correct with respect to the formal semantics of the source language.

What is very appealing about describing the semantics of the language using an omni-big-step semantics is that it delivers the weakest-precondition-style reasoning rules almost for free. Indeed, the interpretation of the inductive judgment \({t / s}\,\Downarrow \,{Q}\) matches, up to the order of arguments, the standard interpretation of the weakest-precondition operator. \(\begin{equation*} \textsf {wp}\,t\,Q\,s \quad \iff \quad {t / s}\,\Downarrow \,{Q} \end{equation*}\) Thus, in a foundational approach, we can formally define \(\textsf {wp}\) as \(\lambda tQs.\,({t / s}\,\Downarrow \,{Q})\).

There remains to describe how the weakest-precondition-style reasoning rules can be derived from the omni-big-step evaluation rules. Doing so is even easier than for deriving triples. Consider, for example, the semantics rule and the \(\textsf {wp}\)-reasoning rule associated with a let-binding.

To derive the rule wp-let from omni-big-let, it suffices to instantiate \(Q_1\) as \(\lambda v_1.\, \textsf {wp}\,([v_1/x]\,t_2)\,Q\).

The frame rule in weakest-precondition style follows directly from the omni-big-frame lemma established in the previous section. The rule appears below, together with a very handy corollary named the ramified frame rule [Krishnaswami et al. 2010; Hobor and Villard 2013]. In that corollary, the magic wand between postconditions, written \(Q_1 {–̣} Q_2\), is defined as \(\forall \hspace{-5.0pt}{\forall }v.\, Q_1\,v -\!\!\!\star Q_2\,v\), where \(\forall \hspace{-5.0pt}{\forall }\) and \(-\!\!\!\star\) are the standard Separation Logic operators (see, e.g., [Charguéraud 2020, Sections 3.2 and 7]).

For most other term constructs, the \(\textsf {wp}\) rule is nothing but a copy of the omni-big-step rule with arguments reordered. One interesting exception is that of loops. “While” loops have not been discussed so far, but they appear in the language used for the case studies in Section 6. Typically, standard weakest-precondition rules for while loops are stated using loop invariants. In contrast, an omni-big-step rule essentially unfolds the first iteration of the loop, just like in a standard big-step semantics. From that unfolding rule, one can derive the loop-invariant-based rule by induction, in just a few lines of proof.

In summary, by considering a semantics expressed in omni-big-step style, one can derive practical reasoning rules, both in Hoare-triple style and in weakest-precondition style, in most cases via one-line proofs. The construction of a program logic on top of an omni-big-step semantics is thus a significant improvement, both over the use of a standard big-step semantics, which falls short in the presence of nondeterminism, and over the use of a small-step semantics, which requires much more work for deriving the reasoning rules, especially if one aims for total correctness. Besides, a major benefit of considering an omni-big-step semantics is that, unlike a set of weakest-precondition reasoning rules, it delivers an induction principle for reasoning about program executions. Such induction principles are exploited in the case studies (Section 6).

Skip 6COMPILER-CORRECTNESS PROOFS FOR TERMINATING PROGRAMS Section

6 COMPILER-CORRECTNESS PROOFS FOR TERMINATING PROGRAMS

Omnisemantics also simplify some of the characteristic complexities of behavior-preservation proofs for compilers.

6.1 Motivation: Avoiding Both Backward Simulations and Artificial Determinism

Following CompCert’s terminology [Leroy 2009], one particular evaluation of a program can admit one out of four possible behaviors: terminate (with a value, an exception, a fatal error, etc.), trigger undefined behavior, diverge silently after performing a finite number of I/O operations, or be reactive by performing an infinite sequence of I/O operations. Whether an error such as a division by zero is considered as a terminating behavior or as an undefined behavior is a design decision associated with each programming language. A general-purpose compiler ought to preserve behaviors, except that undefined behaviors can be replaced with anything.

In this article, we focus on proofs of compiler correctness for programs that always terminate safely. Such a result is sufficient for many practical applications in software verification where source programs are proven to be safe, and often, the only use case for nontermination is a top-level infinite event-handling loop, which can be implemented in assembly language [Erbsen et al. 2021]. We leave to future work the application of omnisemantics to the correct compilation of programs that diverge, react, or trigger undefined behavior on some inputs but not others.

In the particular case of a deterministic programming language, compiler correctness for terminating programs can be established via a forward-simulation proof.7 Such a proof consists of showing that each step from the source program corresponds to a number of steps in the compiled program. The correspondence involved is captured by a relation between source states and target states. Such forward-simulation proofs work well in practice. The main problem is that they do not generalize to nondeterministic languages.

Indeed, in the presence of nondeterminism, a source program may have several possible executions. As we restrict ourselves to the case of terminating programs, let us assume that all executions of the source program terminate, only possibly with different results. In that setting, a compiler is correct if (1) the compiled program always terminates and (2) for any result that the compiled program may produce, the source program could have produced that result. It may not be intuitive at first, but the inclusion is indeed backwards: the set of behaviors of the target program must be included in the set of behaviors of the source program.

To establish the backward behavior inclusion, one may set up a backward-simulation proof. Such a proof consists of showing that each step from the target program corresponds to one or more steps in the source program.8 Yet, backward simulations are much more unwieldy to set up than forward simulations. Indeed, in most cases one source-program step is implemented by multiple steps in the compiled program, and thus a backward-simulation relation typically needs to relate many more pairs than a forward-simulation relation.

This observation motivated the CompCert project [Leroy 2009] to exploit forward simulations as much as possible, at the cost of modeling features of the intermediate language as deterministic even when it is not natural to do so, and even when doing so requires introducing artificial functions for, e.g., computing fresh memory locations in a deterministic manner. Even so, runtime input does not fit the fully deterministic model, leading to the technical definitions of receptiveness and determinacy (roughly, capturing the idea of determinism modulo input) so that lemmas for flipping forward simulations into backwards simulations can be stated and proven. Omnisemantics remove the need for this machinery.

In this section:

We explain how omnisemantics sidestep the need for backward simulations, by carrying out forward-simulation proofs of compiler correctness, for nondeterministic terminating programs.

We show how the idea generalizes to languages including I/O operations and to the case where the source language and target language are different.

We present two case studies: one transformation that increases the amount of nondeterminism and one that decreases the amount of nondeterminism.

We comment on the fact that our second case study features an omni-big-step semantics for the source language, whereas it features an omni-small-step semantics for the target language.

6.2 Establishing Correctness via Forward Simulations using Omnisemantics

Consider a compilation function written \(\mathcal {C}(t)\). For simplicity, we assume that the source and target language are identical, we assume that compilation does not alter the result values, and we assume the language to be state-free—we will generalize the results in the next subsection. In this subsection, \(t \Downarrow v\) denotes the standard big-step judgment, \({t}\,\Downarrow \,{Q}\) denotes the omni-big-step judgment, and \(\textsf {terminates}(t)\) asserts that all executions of t terminate safely, without undefined behavior. The compiler-correctness result for terminating programs captures preservation of termination and backward inclusion for results—points (1) and (2) stated earlier. \(\begin{equation*} \begin{array}{l} \scriptsize{\text{CORRECTNESS-FOR-TERMINATING-PROGRAMS}:}\\ \qquad \textsf {terminates}(t)\qquad \Rightarrow \qquad \textsf {terminates}(\mathcal {C}(t)) \quad \wedge \quad \big (\forall v .\quad \mathcal {C}(t) \Downarrow v \quad \Rightarrow \quad t \Downarrow v\big) \end{array} \end{equation*}\)

We claim that this correctness result can be derived from the following statement, which describes forward preservation of specifications: \(\begin{equation*} \begin{array}{l} \scriptsize{\text{OMNI-FORWARD-PRESERVATION}:} \qquad \forall Q . \qquad {t}\,\Downarrow \,{Q} \quad \Rightarrow \quad {\mathcal {C}(t)}\,\Downarrow \,{Q} \end{array} \end{equation*}\)

Let us demonstrate the claim. Let us assume that \(\textsf {terminates}(t)\) hold. First of all, recall from Section 2.2 the equivalence named omni-big-step-iff-terminates-and-correct that relates the omni-big-step judgment and the termination judgment. \(\begin{equation*} \begin{array}{l} {t}\,\Downarrow \,{Q} \quad \iff \quad \textsf {terminates}(t) \;\, \wedge \;\,\big (\forall v .\; (t \Downarrow v) \, \Rightarrow \,v\in Q\big) \end{array} \end{equation*}\) Exploiting this equivalence, the omni-forward-preservation assumption reformulates as follows: \(\begin{equation*} \begin{array}{ll} \forall Q . & \big (\textsf {terminates}(t) \;\, \wedge \;\,\big (\forall v .\; (t \Downarrow v) \, \Rightarrow \,v\in Q\big)\big) \\ \qquad \quad \Rightarrow \quad & \big (\textsf {terminates}(\mathcal {C}(t)) \;\, \wedge \;\,\big (\forall v .\; (\mathcal {C}(t) \Downarrow v) \, \Rightarrow \,v\in Q\big)\big) \end{array} \end{equation*}\)

The hypothesis \(\textsf {terminates}(t)\) holds by assumption. Let us instantiate Q as the strongest postcondition for t, that is, as the set \(\lbrace {v} \,|\, {(t \Downarrow v)} \rbrace\). We obtain \(\begin{equation*} \begin{array}{ll} \big (\forall v .\; (t \Downarrow v) \, \Rightarrow \,(t \Downarrow v)\big) \quad \Rightarrow \quad \textsf {terminates}(\mathcal {C}(t)) \;\, \wedge \;\,\big (\forall v .\; (\mathcal {C}(t) \Downarrow v) \, \Rightarrow \,(t \Downarrow v)\big) \end{array} \end{equation*}\) The premise is a tautology, and the conclusion proves correctness-for-terminating-programs.

6.3 Omnisemantics Simulations for I/O and Cross-Language Compilation

More generally, the behavior of a terminating program consists of the final result and its interactions with the outside world (input and output). These interactions include, e.g., interaction with the standard input and output streams, system calls, and so forth. Each interaction is called an event, and the semantics judgment is extended to collect such events into a trace \(\tau\). Figure 5 shows three illustrative cases of how the rules from Figure 2 are augmented with traces, making the choice to treat \(\textsf {rand}\) calls as observable events while reference-allocation nondeterminism remains internal.

Fig. 5.

Fig. 5. Omni-big-step semantics with traces, selected rules.

Requiring a compiler to preserve only the nondeterministic choices recorded in the trace enables us to pick and choose which (external) interactions must be preserved by compilations and which (internal) nondeterministic choices the compiler may resolve as it sees fit. As a particularly fine-grained example, the trace might record that malloc was called and succeeded but omit the pointer it returned, to allow for optimizations that reduce the amount of allocation. To our knowledge, this level of flexibility is unique to omnisemantics. For a forward-simulation-based compiler-correctness proof, constructing a deterministic model of all internal nondeterminism can be arbitrarily complicated (the CompCert memory model is an example).

We restrict our attention to semantics that only accept terminating commands c that do not go wrong and do not return values, for the rest of this section. For languages of terms (that return values) rather than commands (that do not return values), we would need a representation relation between source-level and target-level values—we omit one here for brevity, but Section 6.4 tackles a similar challenge. In the current setting, behavior inclusion holds between a source-language program and a target-language program if all traces that the target-language program can produce (according to traditional small-step or big-step semantics) can also be produced by the source-language program. More formally, we define the traces that can be produced from a starting configuration \(c/s/\tau\) as \(\begin{equation*} \textsf {traces}(c,s,\tau) := \lbrace \tau ^{\prime } ~|~ \exists s^{\prime }.~ c/s/\tau \Downarrow s^{\prime }/\tau ^{\prime } \rbrace \end{equation*}\) and say that a compiler \(\mathcal {C}{}\) satisfies behavior inclusion for a command starting from the initial source-level state \(s_{{\it src}}\) related to the initial target-level state \(s_{{\it tgt}}\) and initial trace \(\tau\) if TraceInclusion as defined below holds. \(\begin{equation*} \begin{array}{c} {\rm T}{\rm\small{RACE}}{\rm I}{\rm\small{NCLUSION}}(c, s_{{\it src}}, s_{{\it tgt}}, \tau) ~:=~ \textsf {traces}(\mathcal {C}(c), s_{{\it tgt}}, \tau) \subseteq \textsf {traces}(c, s_{{\it src}}, \tau) \end{array} \end{equation*}\)

Assuming omni-big-step semantics \(\Downarrow _{{\it src}}\) and \(\Downarrow _{{\it tgt}}\) for the source and target languages, plus a relation R between source- and target-language states, we define omnisemantics simulation, a compiler-correctness criterion designed to be provable by induction on the \(\Downarrow _{{\it src}}\) judgment, as follows: \(\begin{equation*} \begin{array}{rl} {\rm O}{\rm\small{MNISEMANTICS}}{\rm S}{\rm\small{IMULATION}}(c) := & \forall s_{{\it src}} ~ s_{{\it tgt}} ~ \tau ~ Q . ~ R(s_{{\it src}},s_{{\it tgt}}) \,\wedge \, c/s_{{\it src}}/\tau \Downarrow _{{\it src}} Q \\ & \Longrightarrow \mathcal {C}(c)/s_{{\it tgt}}/\tau \Downarrow _{{\it tgt}}Q_R \\ \text{where } Q_R(s_{{\it tgt}}^{\prime },\,\tau ^{\prime }) := & \exists s_{{\it src}}^{\prime }\,.~R(s_{{\it src}}^{\prime },s_{{\it tgt}}^{\prime }) \wedge Q(s_{{\it src}}^{\prime }, \tau ^{\prime }) \end{array} \end{equation*}\)

Our goal in this section is to prove that an omnisemantics simulation implies trace inclusion if the source program terminates, i.e., to show \(\begin{equation*} \begin{array}{rl} \forall c . & {\rm O}{\rm\small{MNISEMANTICS}}{\rm S}{\rm\small{IMULATION}}(c) ~~ \Longrightarrow \\ & \forall ~ s_{{\it src}} ~ s_{{\it tgt}} ~ \tau . ~~ \textsf {terminates}(c,s_{{\it src}},\tau) ~\wedge ~ R(s_{{\it src}},s_{{\it tgt}}) \quad \Longrightarrow \quad {\rm T}{\rm\small{RACE}}{\rm I}{\rm\small{NCLUSION}}(c, s_{{\it src}}, s_{{\it tgt}}, \tau) \end{array} \end{equation*}\) We rely on two properties: First, soundness of omni-big-step semantics with respect to traditional big-step semantics: (1) \(\begin{equation} \forall c ~ s ~ s^{\prime } ~ \tau ~ \tau ^{\prime } ~ Q.\;\;\; ~ c/s/\tau \Downarrow s^{\prime }/\tau ^{\prime } \;\wedge \; c/s \Downarrow Q \quad \Longrightarrow \quad Q(s^{\prime }, \tau ^{\prime }) \end{equation}\)

And conversely, that a program that terminates safely and whose traditional big-step executions all satisfy a postcondition also has an omnisemantics derivation: (2) \(\begin{equation} \forall c ~ s ~ \tau ~ Q.~ \textsf {terminates}(c,s,\tau) \wedge (\forall s^{\prime }~\tau ^{\prime }.~ c/s/\tau \Downarrow s^{\prime }/\tau ^{\prime } \, \Longrightarrow \,Q(s^{\prime }, \tau ^{\prime })) \quad \Longrightarrow \quad c/s/\tau \Downarrow Q \end{equation}\)

To show trace inclusion, i.e., \(\textsf {traces}(\mathcal {C}(c), s_{{\it tgt}}, \tau) \subseteq \textsf {traces}(c, s_{{\it src}}, \tau)\), we can assume a target-language execution \(\mathcal {C}(c)/s_{{\it tgt}}/\tau \Downarrow s_{{\it tgt}}^{\prime }/\tau ^{\prime }\) producing trace \(\tau ^{\prime }\), and we need to show \(\tau ^{\prime } \in \textsf {traces}(c, s_{{\it src}}, \tau)\). By applying Equation (2) to the source program (whose termination we assume) and setting \(Q(s_{{\it src}}^{\prime },\tau ^{\prime })\,:=\,c/s_{{\it src}}/\tau \Downarrow s_{{\it src}}^{\prime }/\tau ^{\prime }\) so that the second premise becomes a tautology, we obtain the source-level omnisemantics derivation \(c/s_{{\it src}}/\tau \Downarrow (\lambda s_{{\it src}}^{\prime }~\tau ^{\prime }.~c/s_{{\it src}}/\tau \Downarrow s_{{\it src}}^{\prime }/\tau ^{\prime })\). Passing this fact into the omnisemantics simulation yields \(\mathcal {C}(c)/s_{{\it tgt}}/\tau \Downarrow (\lambda s_{{\it tgt}}^{\prime }~\tau ^{\prime }.\exists s_{{\it src}}^{\prime }.R(s_{{\it src}}^{\prime },s_{{\it tgt}}^{\prime }) \wedge c/s_{{\it src}}/\tau \Downarrow s_{{\it src}}^{\prime }/\tau ^{\prime })\). Now we can apply Equation (1) to this fact and the originally assumed target-level execution and obtain an \(s_{{\it src}}^{\prime }\) such that \(R(s_{{\it src}}^{\prime },s_{{\it tgt}}^{\prime })\) and \(c/s_{{\it src}}/\tau \Downarrow s_{{\it src}}^{\prime }/\tau ^{\prime }\), which by definition is exactly what needs to hold to have \(\tau ^{\prime } \in \textsf {traces}(c, s_{{\it src}}, \tau)\).

6.4 Case Study: Compiling Immutable Pairs to Heap-Allocated Records

This section describes a simple compiler pass that increases the amount of nondeterminism. The source language assumes a primitive notion of tuples, whereas the target language encodes such tuples by means of heap allocation. This case study is formalized with respect to a language based on commands whose arguments all must be variables. Such a language could be an intermediate language in a compiler pipeline, reached after an expression-flattening phase.

Language syntax. We let c denote a command; x, y, and z denote identifiers; and n denote unbounded natural-number constants. The grammar of the language is as follows: \(\begin{equation*} \begin{array}{lll} c & := & x=unop(y) \mid x=binop(y,z) \mid x=\textsf {input}() \mid \textsf {output}(x) \mid x=y[n] \mid x[n]=y \mid \\ & & x=\textsf {alloc}(n) \mid x=n \mid x=y \mid c_1;c_2 \mid \textsf {if }x\textsf { then }c_1\textsf { else }c_2 \mid \textsf {while }x\textsf { do }c \mid \textsf {skip} \end{array} \end{equation*}\) We actually consider two variants of this language, differing only in the types of values and in the available unary operators unop and binary operators binop. The source language features an inductively defined type of values that can be natural numbers n or immutable pairs of values (i.e., the grammar of values is \(v := n \mid (v, v)\)). It includes as unary operators the projection functions fst and snd (defined only on pairs) and the Boolean negation not (defined only on \(\lbrace 0,1\rbrace\)). Its binary operators are addition \((+)\) and pair creation mkpair. The target language admits only natural numbers as values. It includes only the negation and addition operators.

Omni-big-step semantics. For both languages, the omni-big-step evaluation judgment takes the form \(c/m/\ell /\tau \Downarrow Q\), where c is a command, m is a memory state (a partial map from natural numbers to natural numbers), \(\ell\) is an environment of local variables (a partial map from identifiers to values, whose type differs between the source and target languages as described above), \(\tau\) denotes the I/O trace made of the events already performed before executing c, and the postcondition Q is a predicate over triples of the form \((m^{\prime },\ell ^{\prime },\tau ^{\prime })\). A trace consists of a list of I/O events e whose grammar is \(e := \textsf {IN}~n \mid \textsf {OUT}~n\).

The rules defining the judgment appear in Figure 6. They are common to both languages—only the set of supported unary and binary operators differs. The semantics of operators are defined by two straightforward auxiliary relations (spelled out in Appendix H), \(\textsf {evalunop}(unop, v_1, v_2)\) asserting that applying unop to value \(v_1\) results in \(v_2\), and \(\textsf {evalbinop}(binop, v_1, v_2, v_3)\) asserting that applying binop to \(v_1\) and \(v_2\) results in \(v_3\). The load command \(x=y[n]\) requires that the local variable y contains a natural number a and stores the value of the memory at address \(a+n\) into variable x (and is undefined if \(a+n\) is not mapped by the memory). The store command \(x[n]=y\) stores the natural number contained in the local variable y at memory location \(a+n\), where a is the address contained in local variable x, but only if memory at address \(a+n\) has already been allocated.

Fig. 6.

Fig. 6. Nondeterministic omni-big-step semantics for an imperative language (selected rules).

The command \(x=\textsf {input}()\) reads a natural number n, stores it into local variable x, and adds the event \((\textsf {IN }n)\) to the event trace. The number n is chosen nondeterministically but recorded in the trace, resulting in external nondeterminism. The language has a built-in memory allocator, but for simplicity, we do not deal with deallocation. The command \(x=\textsf {alloc}(n)\) nondeterministically picks an address (natural number) a such that a, as well as the \(n-1\) addresses following a, are not yet part of the memory; initializes these addresses with nondeterministically chosen values; and returns a. This rule encodes internal nondeterminism, because this action is not recorded in the event trace. Semantics of while loops are given by sequencing the first iteration with the loop itself as long as the loop test succeeds.

In practice, we found it convenient also to derive a chained version eval-seq-chained of the omni-big-step rule eval-seq, just like we did for omni-small-step rules in Section 3.2. \(\begin{equation*} {\rm\small EVAL-SEQ-CHAINED}:\qquad c_1/m/\ell /\tau \Downarrow \big (\lambda m^{\prime }\ell ^{\prime }\tau ^{\prime }.~ (c_2/m^{\prime }/\ell ^{\prime }/\tau ^{\prime } \Downarrow Q)\big) \quad \Rightarrow \quad (c_1;c_2)/m/\ell /\tau \Downarrow Q \end{equation*}\)

Note that the chained variant cannot be used to define the judgment inductively in Coq due to the strict positivity requirement; more details on encoding choices can be found in Appendix A.

Compilation function. The compilation function \(\mathcal {C}\) lays out the pairs of the source language on the heap memory of the target language. This function is defined recursively on the source program. It maps the source-language operators that are not supported by the target language as follows: \(\begin{equation*} \begin{array}{lll} \mathcal {C}(x=\textsf {fst}(y)) & := & x=y[0] \\ \mathcal {C}(x=\textsf {snd}(y)) & := & x=y[1] \\ \mathcal {C}(x=\textsf {mkpair}(y,z)) & := & \text{tmp}=\textsf {alloc}(2); \;\text{tmp}[0]=y;\; \text{tmp}[1]=z;\; x=\text{tmp} \end{array} \end{equation*}\)

Note that to compile mkpair, we cannot simply store the address returned by alloc directly into x, because if x is the same variable name as y or z, then we would be overwriting the argument. For this reason, we use a temporary variable \(\textsf {tmp}\) that we declare to be reserved for compiler usage.

Simulation relation.

To carry out the proof of correctness of the function \(\mathcal {C}(c)\), we introduce a simulation relation R for relating a source-language state \((m_1, \ell _1)\) with a target-language state \((m_2, \ell _2)\). To that end, we first define the relation \(\textsf {valuerepr}(v,w,m)\), to relate a source-language value v with the corresponding target-language value w, in a target-language memory m. This relation is implemented as the recursive function shown below—it could equally well consist of an inductive definition. A pair \((v_1,v_2)\) is represented by address w if recursively \(v_1\) is represented by the value at address w, and \(v_2\) is represented by the value at address \(w+1\). A natural number n has the same representation on the target-language level; i.e., we just assert \(w=n\). \(\begin{equation*} \begin{array}{lll} \textsf {valuerepr}((v_1, v_2), w, m) & := & (\exists w_1. \; (w,w_1) \in m \, \wedge \,\textsf {valuerepr}(v_1, w_1, m)) \, \wedge \,\\ & & (\exists w_2. \; (w+1,w_2) \in m \wedge \textsf {valuerepr}(v_2, w_2, m)) \\ \textsf {valuerepr}(n, w, m) & := & w = n \end{array} \end{equation*}\)

The relationship R between source and target states can then be defined using valuerepr. In the definition shown below, we write \(m_2 \supseteq m_1\) to mean that memory \(m_2\) extends \(m_1\), and we write \(m_2 \setminus m_1\) to denote the map-subtraction operator that restricts \(m_2\) to contain only addresses not bound in \(m_1\). The locations bound by \(m_2\) but not by \(m_1\) correspond to the memory addresses of the pairs allocated on the heap in the target language. \(\begin{equation*} \begin{array}{lll} R((m_1, \ell _1), (m_2, \ell _2)) & := & \text{tmp} \notin \textsf {dom}~\ell _1 \wedge m_2 \supseteq m_1 \wedge \\ & & \forall (x,v) \in \ell _1. \;\exists w.\; (x, w) \in \ell _2 \wedge \textsf {valuerepr}(v, w, m_2 \setminus m_1) \end{array} \end{equation*}\)

Correctness proof.

We are now ready to tackle the omni-forward-simulation proof.

Theorem 6.1 (Omnisemantics Simulation for the Pair-heapification Compiler).

\(\begin{equation*} \begin{array}{c} \forall c ~ m_{{\it src}} ~ \ell _{{\it src}} ~ m_{{\it tgt}} ~ \ell _{{\it tgt}} ~ \tau ~ Q. \, \,~ \mbox{tmp} \notin {\mbox{vars}}(c) \,\wedge \; R((m_{\it src}, \ell _{\it src}), (m_{\it tgt}, \ell _{\it tgt})) \;\wedge \; \\ c/m_{\it src}/\ell _{\it src}/\tau \Downarrow _{{\it src}} Q \, \Longrightarrow \,\\ \mathcal {C}(c)/m_{\it tgt}/\ell _{\it tgt}/\tau \Downarrow _{{\it tgt}}Q_R \quad \quad \, \,\;\;{}\\ \mbox{where }\quad Q_R(m_{\it tgt}^{\prime },\,\ell _{\it tgt}^{\prime },\,\tau ^{\prime }) := \exists m_{\it src}^{\prime } ~ \ell _{\it src}^{\prime }\,.~R((m_{\it src}^{\prime }, \ell _{\it src}^{\prime }), (m_{\it tgt}^{\prime }, \ell _{\it tgt}^{\prime })) \wedge Q(m_{{\it src}}^{\prime },\,\ell _{{\it src}}^{\prime },\, \tau ^{\prime }) \end{array} \end{equation*}\)

Proof.

By induction on the derivation of \(c/m_{{\it src}}/\ell _{\it src}/\tau \Downarrow Q\). In each case, the goal to prove is initially of the form \(\mathcal {C}(c)/m_{{\it tgt}}/\ell _{\it tgt}/\tau \Downarrow Q_R\), where c has some structure that allows us to simplify \(\mathcal {C}(c)\) into a more concrete program snippet. We consider the resulting simplified goal as an invocation of a weakest-precondition generator on that program snippet, and we view the rules of Figure 6 as weakest-precondition rules that we can apply in order to step through the program snippet, using the hypotheses obtained from inverting the source-level derivation \(c/m_{{\it src}}/\ell _{\it tgt}/\tau \Downarrow Q\) to discharge the side conditions that arise. Whenever we encounter a sequence of commands, we use eval-seq-chained instead of eval-seq, so that we do not have to provide an intermediate postcondition. In the cases where commands have subcommands, we use the inductive hypotheses about their execution as if they were previously proven lemmas about these “functions.”

We only present the case where \(c=(x=\textsf {mkpair}(y,z))\) in more detail: we have to prove a goal of the form \(\mathcal {C}(x=\textsf {mkpair}(y,z))/m_{{\it tgt}}/\ell _{\it tgt}/\tau \Downarrow Q_R\), which simplifies to \(\begin{equation*} (\text{tmp}=\textsf {alloc}(2); \text{tmp}[0]=y; \text{tmp}[1]=z; x=\text{tmp})/m_{{\it tgt}}/\ell _{{\it tgt}}/\tau \Downarrow Q_R \end{equation*}\) Applying eval-seq-chained turns it into \(\begin{equation*} (\text{tmp}=\textsf {alloc}(2))/m_{{\it tgt}}/\ell _{{\it tgt}}/\tau \Downarrow \left(\lambda m_{{\it tgt}}^{\prime }~\ell _{\it tgt}^{\prime }~\tau ^{\prime }.\;(\text{tmp}[0]=y; \text{tmp}[1]=z; x=\text{tmp})/m_{{\it tgt}}^{\prime }/\ell _{\it tgt}^{\prime }/\tau ^{\prime } \Downarrow Q_R\right) \end{equation*}\) Applying eval-alloc turns it into \(\begin{equation*} \begin{array}{c}\forall ~a~\overline{v}.\;\textsf {{len}}(\overline{v})=2 \Longrightarrow a, a+1 \notin \textsf {{dom}}~m_{{\it tgt}} \Longrightarrow \\ (\text{tmp}[0]=y; \text{tmp}[1]=z; x=\text{tmp})/m_{{\it tgt}}[a..(a+1):=\overline{v}]/\ell _{{\it tgt}}[\text{tmp}:=a]/\tau \Downarrow Q_R\end{array} \end{equation*}\)

Note how the fact that the address a and the list of initial values \(\overline{v}\) are chosen nondeterministically naturally shows up as a universal quantification, and note how the memory and locals appearing in the state to the left of the \(\Downarrow\) have been updated by the \(\textsf {{alloc}}\) function. After introducing these universally quantified variables and the hypotheses, we again have a goal of the form “\(\dots \Downarrow \dots\)” and continue by applying eval-seq-chained, eval-store, eval-seq-chained, eval-store, eval-set. Finally, we prove \(Q_R\) for the locals and memory updated according to the various eval-... rules that we applied by using map laws and the initial hypothesis \(R((m_{\it src}, \ell _{\it src}), (m_{\it tgt}, \ell _{\it tgt}))\).□

6.5 Case Study: Introduction of Stack Allocation

This second case study illustrates the case of a transformation that reduces the amount of nondeterminism. The transformation consists of adding a stack-allocation feature to the compiler developed by Erbsen et al. [2021]. Proving this transformation correct using an omni-big-step forward simulation was straightforward and took us only a few days of work—most of the work was not concerned with dealing with nondeterminism. This smooth outcome is in stark contrast to the outlook of using traditional evaluation judgments: verifying the same transformation would have required either more complex invariants, to set up a backward simulation, or completely rewriting the memory model so that pointers are represented by deterministically generated unobservable identifiers, to allow for a compiler-correctness proof by forward simulation. In fact, addressable stack allocation was initially omitted from the language exactly to avoid these intricacies (based on the experience from CompCert), but switching to omnisemantics made its addition local and uncomplicated.

The input language is an imperative command language similar to the one described in Section 6.4. The memory is modeled as a partial map from machine words (32-bit or 64-bit integers) to bytes. The stack-allocation feature here consists of a command \(\textsf {let }x = \textsf {stackalloc}(n) \textsf { in } c\) made available in the source language. This command assigns an address to variable x at which n bytes of memory will be available during the execution of command c. Our compiler extension implements this command by allocating the requested n bytes on the stack, computing the address at runtime based on the stack pointer.

The key challenge is that the source-language semantics does not feature a stack. The stack gets introduced further down the compilation chain. Thus, the simplest way to assign semantics to the stackalloc function in the source language is to pretend that it allocates memory at a nondeterministically chosen memory location. This nondeterministic choice is described using a universal quantification in the omni-big-step rule shown below, like in rule omni-big-ref from Section 2.

In the source language, the address returned by \(\textsf {stackalloc}\) is picked nondeterministically, whereas in the target language the address used for the allocation is deterministically computed, as the current stack pointer augmented with some offset. Thus, the compiler phase that compiles away the stackalloc command reduces the amount of nondeterminism.

Compiler-correctness proof. The compiler-correctness proof proceeds by induction on the omnisemantics derivation for the source language, producing a target-language execution with a related postcondition. The simulation relation R describes the target-language memory as a disjoint union of unallocated stack memory and the source-language memory state. Critically, the case for stackalloc has access to a universally quantified induction hypothesis (derived from the rule shown above) about target-level executions of \(\mathcal {C}(c)\) for any address a.

As the address of the stack-allocated memory is not recorded in the trace, we are free to instantiate it with the specific stack-space address, expressed in terms of compile-time stack-layout parameters and the runtime stack pointer. Re-establishing the simulation relation to satisfy the precondition of that induction hypothesis then involves carving out the freshly allocated memory from unused stack space and considering it a part of the source-level memory instead, matching the compiler-chosen memory layout and the preconditions of the stackalloc omnisemantics rule. It is this last part that made up the vast majority of the verification work in this case study; handling the nondeterminism itself is as straightforward as it gets.

Note that it would not be possible to complete the proof by instantiating a with a compiler-chosen offset from the stack pointer if the semantics recorded the value of a in the trace. The (unremarkable) proof for the input command in the previous section also has access to a universally quantified execution hypothesis, but it must directly instantiate its universally quantified induction hypothesis with the variable introduced when applying the target-level omnisemantics input rule to the goal, to match the target-language trace to the source-language trace. Either way, reasoning about the reduction of nondeterministim in an omni-forward-preservation proof boils down to instantiating a universal quantifier.

Design decisions around proving absence of out-of-memory. In the verified software-hardware stack described in Erbsen et al. [2021], the main bottleneck in terms of complexity that prevents us from developing bigger applications is the level of proof automation available for verification of mundane aspects of source programs such as address arithmetic. Therefore, we made an effort to avoid adding more proof obligations in the program logic whenever possible. At the same time, for the targeted application it was fine to limit the expressivity of the source language. In particular, we decided that disallowing recursive calls is acceptable. Given that setting, we want to avoid reasoning about out-of-memory conditions in the source language, while still proving that the compiled program will not run out of memory, which we can achieve as follows.

In the omni-big-stackalloc rule of our source language, we deliberately use a vacuous universal quantification if we run out of memory, because we prefer to handle out-of-memory conditions outside of the omnisemantics judgment, in an additional external judgment. In particular, this means that if omni-big-stackalloc is applied with a memory m whose domain already contains all (or almost all) addresses (which are 32-bit or 64-bit words), there might be no \(m_{{\it new}}\) and a such that the left-hand side of the implication above the line in omni-big-stackalloc holds, so we can derive any postcondition P, something that we cautioned against in Section 2.2.

Effectively, this means that our source-language evaluation rules do not guarantee that the program never runs out of memory. This choice simplifies the program-logic proofs for concrete input programs but requires additional work in the compiler: the compiler performs a simple static-analysis pass over the call graph of the program to determine the maximum amount of stack space that the program needs. Since this analysis rejects recursive calls, the space upper bound is not hard to compute. The compiler-correctness proof contains an additional hypothesis requiring that at least that computed amount of memory is available in the state on which the target-language program begins its execution.

An alternative approach would be to introduce a notion of “amount of used stack space” in the source-language semantics and include an additional precondition in the omni-big-stackalloc rule that requires this amount to be bounded. This approach would put more complexity into the verification of source programs, while simplifying the compiler-correctness proof. In order to allow recursive calls and dynamically chosen stack-allocation sizes, reasoning about the amount of stack space in the program logic seems to become unavoidable, in which case this alternative approach would be preferable.

6.6 Compilation from a Language in Omni-Big-Step to One in Omni-Small-Step

If the semantics of the source language of a compiler phase are most naturally expressed in omni-big-step but the target language’s semantics are best expressed in omni-small-step semantics, it is convenient to prove an omni-forward simulation directly from a big-step source execution to a small-step target execution. For instance, the compiler in the project by Erbsen et al. [2021] includes such a translation, relating a big-step intermediate language to a small-step assembly language. In fact, this translation happens in the same case study described in the previous subsection. In what follows, we attempt to give a flavor of the proof obligations that arise from switching from omni-big-step to omni-small-step during the correctness proof.

Consider one sample omni-small-step rule, for the load-word instruction lw that loads the value at the address stored in register \(r_2\) and assigns it to register \(r_1\):

Here, we model a machine state \(s_{{\it tgt}}\) as a quadruple of a memory m (that contains both instructions and data), a register file \({\it rf}\) mapping register names to machine words, a program counter pc, and a trace \(\tau\). One can prove an omni-forward simulation from big-step source semantics directly to small-step target semantics: \(\begin{equation*} \forall s_{{\it src}}~s_{{\it tgt}}~P.\; R(s_{{\it src}}, s_{{\it tgt}}) \,\wedge \, s_{{\it src}} \Downarrow P \quad \Longrightarrow \quad s_{{\it tgt}} \longrightarrow ^\lozenge (\lambda s_{{\it tgt}}^{\prime }. \exists s_{{\it src}}^{\prime }.\; R(s_{{\it src}}^{\prime }, s_{{\it tgt}}^{\prime }) \wedge P(s_{{\it src}}^{\prime })) \end{equation*}\) where R asserts, among other conditions, that the memory of the target state \(s_{{\it tgt}}\) contains the compiled program.

Like the proof described in Section 6.4, this proof also works by stepping through the target-language program by applying target-language rules and discharging their side conditions using the hypotheses obtained by inverting the source-language execution, with the only difference that instead of using the derived big-step rule eval-seq-chained for chaining, one now uses the following two rules: eventually-step-chained and eventually-cut.

Applying eventually-step-chained turns the goal into an omni-single-small-step goal with a given postcondition, which is suitable to discharge using rules with universally quantified postconditions like asm-lw. Applying eventually-cut, on the other hand, creates two subgoals containing an uninstantiated unification variable for the intermediate postcondition. The unification variable appears as the postcondition in the first subgoal, so an induction hypothesis with the concrete postcondition from the theorem statement can be applied. In the second subgoal, this postcondition becomes the precondition for the remainder of the execution.

Skip 7RELATED WORK Section

7 RELATED WORK

This works builds on that of Schäfer et al. [2016], Charguéraud [2020], and Erbsen et al. [2021], all of which are discussed in the introduction (Section 1). We now will review some additional connections.

Relationship to coinductive big-step semantics. Leroy and Grall [2009] argue that fairly complex, optimizing compilation passes can be proved correct more easily using big-step semantics than using small-step semantics. These authors propose to reason about diverging executions using coinductive big-step semantics, following up on an earlier idea by Cousot and Cousot [1992]. Leroy and Grall’s judgment, written \(t / s \Uparrow ^{\textsf {co}}\), asserts that there exists a diverging execution of \(t/s\). This judgment is defined coinductively, and a number of its rules refer to the standard big-step judgment. For example, consider the two rules associated with divergence of a let-binding. An expression \(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\) diverges either because \(t_1\) diverges (rule div-let-1) or because \(t_1\) terminates on a value \(v_1\) and the term \([v_1/x]\,t_2\) diverges (rule div-let-2).

In contrast, the coinductive omni-big-step judgment involves a single rule, namely co-omni-big-let, defined as part of the coinductive interpretation of the rules from Figure 2.

In that rule, if \(Q_1\) is instantiated as the empty set, the second premise becomes vacuous, and we recover the rule div-let-1. Otherwise, if \(Q_1\) is nonempty, then it describes the values \(v_1\) that \(t_1\) may evaluate to. For each possible value \(v_1\), the second premise of the rule requires the term \([v_1/x]\,t_2\) to diverge, just like in the rule div-let-2. In summary, co-omni-big-let captures at once the logic of both div-let-1 and div-let-2.

The article by Leroy and Grall [2009], which focuses on a deterministic semantics, points out that the principle of excluded middle (classical logic) is required for establishing the equivalence between a coinductive big-step semantics for divergence and the standard small-step semantics for divergence. Interestingly, classical logic is not required for establishing the equivalence between a coinductive omni-big-step semantics of divergence and the standard small-step semantics for divergence. In the explanations that follow, we omit the state for simplicity, and we write \(t \longrightarrow ^\infty _{\textsf {co}}\) for the standard small-step divergence judgment, defined as \(\forall t^{\prime } .\; (t \longrightarrow ^*t^{\prime }) \, \Rightarrow \,\exists t^{\prime \prime } .\;(t^{\prime } \longrightarrow t^{\prime \prime })\).

The implication that requires classical logic to be established is \((t \longrightarrow ^\infty _{\textsf {co}}) \, \Rightarrow \,(t \Uparrow ^{\textsf {co}})\). To see why, consider a term t of the form \(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\), where \(t_1\) corresponds to a program whose termination is an open mathematical problem, and where \(t_2\) is an infinite loop. Thus, no matter whether \(t_1\) diverges or not, the program \(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\) diverges. Yet, to establish the judgment \((\textsf {let}\, x = t_1 \,\textsf {in}\, t_2) \Uparrow ^{\textsf {co}}\), one needs to know whether \(t_1\) diverges, in which case the rule div-let-1 applies, or whether \(t_1\) terminates, in which case the rule div-let-2 applies. In the general case, one has to invoke the excluded middle to decide on the termination of an abstract term \(t_1\).

In contrast, the implication \((t \longrightarrow ^\infty _{\textsf {co}}) \, \Rightarrow \,({t}\,\Downarrow ^{\textsf {co}}\,{\emptyset })\), which targets a coinductive omni-big-step semantics, can be proved without classical logic, as pointed out earlier in Section 3.4. Intuitively, to prove that the same example program \(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\) diverges, one can apply the rule co-omni-big-let, regardless of whether \(t_1\) diverges or not. It suffices to instantiate \(Q_1\), which denotes the set of possible results of \(t_1\), as the strongest postcondition of \(t_1\). The strongest postcondition may be expressed in terms of the omni-big-step judgment (recall Section 2.2) or equivalently in terms of the small-step judgment by instantiating \(Q_1\) as \(\lbrace v_1 \;\, | \;\, t_1 \longrightarrow ^*v_1 \rbrace\). In particular, if \(t_1\) diverges, then the set \(Q_1\) is empty and the second premise of co-omni-big-let becomes vacuous. What matters for the proof of equivalence between the small-step semantics and the coinductive omni-big-step semantics is that we do not need to decide whether \(Q_1\) is empty, i.e., whether \(t_1\) diverges or not. We thereby avoid the need for the excluded middle.

Coinductive characterization of safety. Wang et al. [2014] define a safety judgment, written \(\textsf {safe}(t,s)\), to assert that all possible executions of the configuration \(t/s\) execute safely, i.e., do not get stuck. To reason in big-step style, and to avoid the cumbersome introduction of error-propagation rules, they consider a coinductive definition. We reproduce below the rule for let-bindings, which reads as follows: to establish that \(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2\) executes safely, prove that \(t_1\) executes safely and that, for any possible result \(v_1\) produced by \(t_1\), the result of the substitution \([v_1/x]\,t_2\) executes safely.

Our judgment \({t / s}\,\Downarrow ^{\textsf {co}}\,{Q}\) generalizes the notion of safety, by baking the postcondition directly into the judgment (Section 2.4). It asserts not only that \(t/s\) cannot get stuck but also that any potential final configuration belongs to Q. We formalized in Coq the following equivalence. \(\begin{equation*} \begin{array}{l} {\rm\small OMNI-CO-BIG-STEP-IFF-SAFE-AND-CORRECT}:\\ \qquad {t / s}\,\Downarrow ^{\textsf {co}}\,{Q} \quad \iff \quad \textsf {safe}(t,s) \;\, \wedge \;\,\big (\forall v s^{\prime } .\; (t / s \Downarrow v / s^{\prime }) \, \Rightarrow \,(v,s^{\prime })\in Q\big) \end{array} \end{equation*}\)

Our rule omni-big-let extends safe-let not just by adding the postcondition Q to the judgment but also by changing the quantification over \(v_1/s^{\prime }\). In the rule safe-let, the quantification is constrained by \(t_1 / s \Downarrow v_1 / s^{\prime }\), whereas in the rule omni-big-let, it is constrained by \((v_1,s^{\prime }) \in Q_1\), where \(Q_1\) denotes the postcondition of \(t_1/s\). The key innovation here is that, thanks to the introduction of postconditions, we no longer need to refer to the standard big-step judgment—the judgment \({t / s}\,\Downarrow \,{Q}\) gives a stand-alone definition of the semantics of the language.

Semantics of nondeterministic programs. An important aspect of the present article is the setup of semantics for nondeterministic language constructs. Let us review the key historical papers that have focused on that task. Nondeterminism appears in the early work on semantics, including the language of guarded commands of Dijkstra [1976] that admits nondeterministic choice where guards overlap, and the par construct of Milner [1975]. Plotkin [1976] develops a powerdomain construction to give a fully abstract model in which equivalences such as \((p \, \textsf {par} \, q)=(q\, \textsf {par} \,p)\) hold. Francez et al. [1979] also present semantics that map each program to a representation of the set of its possible results. In all these presentations, nondeterminism is bounded: only a finite number of choices are allowed.

Subsequent work generalizes the powerdomain interpretation to unbounded nondeterminism. For example, Back [1983] considers a language construct \(x := \epsilon P\) that assigns x to an arbitrary value satisfying the predicate P—the program has undefined behavior if no such value exists. Apt and Plotkin [1986] address the lack of continuity in the models presented in earlier work, still leveraging the notion of powerdomains. Their presentation includes a (countable) nondeterministic assignment operator, written \(x :=\textsf {?}\), that sets x to an arbitrary integer in \(\mathbb {Z}\). More recent work by Tassarotti et al. [2017] heavily relies on the bounded nondeterminism assumption in an extension of Iris [Jung et al. 2018] for developing a logic to justify program refinement. These authors speculate that transfinite step-indexing [Schwinghammer et al. 2013; Svendsen et al. 2016] may allow handling unbounded nondeterminism.

Semantics of reactive programs. One key question is how much of a program’s internal nondeterminism should be reflected in its execution trace. At one extreme, one could include a delay event, a.k.a. a tick, to reflect in the trace each transition performed by the program, following the approaches of Danielsson [2012]. More recent work on interaction trees [Koh et al. 2019; Xia et al. 2019] maps each program to a coinductive structure featuring ticks in addition to I/O steps. Yet, these approaches come at the cost of reasoning “up to removal of a finite number of ticks.”

A promising route to avoiding ticks is the mixed inductive-coinductive approach of Nakata and Uustalu [2010], for distinguishing between reactive programs that always eventually perform I/O operations and silently diverging programs that eventually continue executing forever without performing any I/O. Despite apparent benefits, this approach seems not to have gained popularity or evaluation in the form of sizable case studies.

Compiler correctness as trace property preservation. Abate et al. [2021] define the notion of source trace property preservation (denoted \(\textrm {TP}^{\tilde{\tau }}\)) to mean that all properties that hold on traces produced by the source program also hold on traces produced by the target program. They allow different trace formats in the source and target language, relating the source trace s to a target trace t by a relation \(s \sim t\) and quantifying over them in the same way as we quantify over the source and target states in the definition of omnisemantics simulation (Section 6.3). If we instantiate the definition of Abate et al. [2021] by traces whose single events stand for emitting final states, we obtain our definition of omnisemantics simulation, and vice versa, if we generalize our definition to also allow different trace formats but omit the state component, we obtain their definition. However, including the state component in our definition makes it directly usable for a forward-style proof by induction on the source-language derivation, even in the presence of target-language nondeterminacy. We speculate that several proofs of example compilers in that article could be revisited using omnisemantics. Doing so would not only simplify the proofs but also make the results stronger by removing the target-language determinacy assumption, which they need to derive backward simulations from forward simulations.

Semantics of concurrent programs. Concurrency increases the amount of nondeterminism, due to interleavings, and generally increases the sources of undefined behaviors, due in particular to data races. The work on CompCertTSO [Ševčík et al. 2013] shows how to deal with this additional complexity in a compiler-correctness proof. A direction for future work is to investigate the extent to which omni-small-step semantics would help simplify proofs from CompCertTSO.

The Iris framework [Jung et al. 2015; , 2018] supports reasoning about concurrent programs in Separation Logic. In Iris, the source language is specified by means of a traditional small-step semantics. The weakest preconditions predicate is then defined using step-indexing: one first defines the notion of “a program is well-behaved for n steps” by induction over n, then defines the notion of “a program is well-behaved” as “it is well-behaved for any number of steps.” Proofs are then typically carried out by induction over the indices. Yet, the indices involved get in the way of compiler proofs where the number of computation steps may increase or decrease throughout a transformation. This observation motivated the introduction of more advanced techniques to tame the issue, such as transfinite step-indexing [Svendsen et al. 2016]. When reasoning about sequential programs, the use of step-indexing appears overkill for most applications: by leveraging an inductive definition of the weakest precondition predicate, an omni-big-step semantics provides a direct induction principle that avoids the technicalities and limitations of step-indexing altogether.

Reasoning about termination. Many foundational program logics provide reasoning rules for partial correctness, e.g., [Ni and Shao 2006; Chlipala 2013; Cao et al. 2018; Jung et al. 2018]. More recent frameworks have aimed to support reasoning about total correctness. For example, in the context of the CakeML verified compiler [Kumar et al. 2014], the work by Guéneau et al. [2017], subsequently simplified by Charguéraud [2022], provides a foundational approach to CFML’s characteristic formulae [Charguéraud 2011]. In the context of the Iris framework [Jung et al. 2018], Mével et al. [2019] encode in the notion of time credits [Charguéraud and Pottier 2019] for establishing upper and lower bounds on the execution cost. The existence of an upper bound on the number of time credits required by a program guarantees the termination of that program. Yet, that bound must be provided upfront, and thus this approach is not complete. To see why, consider as counterexample a program that picks an unbounded random number in \(\mathbb {Z}\), then executes a loop that number of times.9 This program is terminating according to the operational semantics, yet it does not admit any bound on its execution time. To handle such programs, Spies et al. [2021] introduce transfinite time credits, which allow for termination arguments based on dynamic information learned during program execution. In comparison, omnisemantics can handle such programs without requiring sophisticated models of Separation Logic. Indeed, an inductive omnisemantics derivation inherently corresponds to a transfinite derivation tree.

Semantics of probabilistic programs. Probabilistic semantics aim not just to describe which executions are possible but also to describe with what probability each execution may happen. A probabilistic small-step execution relation assigns a probability to every transition. One caveat is that probabilities do not suffice to describe all nondeterminism: in particular, memory is allocated at nondeterministically chosen addresses. We refer to Batz et al. [2019] for a solution to this challenge. In the context of program logics, McIver and Morgan [2005] introduce a weakest preexpectation calculus. Batz et al. [2019] generalize this notion to set up a Quantitative Separation Logic.

Additionally, there is a long line of work aiming at providing denotational models for probabilistic programs—e.g., Staton et al. [2016]; Wang et al. [2019]. Denotational and operational semantics serve different purposes; one important practical benefit of omnisemantics is that it is grounded in inductive definitions, with respect to which proofs by induction can be carried out easily in a proof assistant. An interesting question is whether omnisemantics could be adapted to provide an inductively defined operational semantics that accounts for probabilities, by relating configurations not to sets of outcomes but instead to probability distributions of outcomes.

The problem of termination of probabilistic programs is particularly subtle. On the one hand, one may be interested in capturing that any execution terminates. For example, Staton et al. [2016] define termination as “there exists n, such that termination occurs in n steps.” However, this approach does not apply to infinitely branching nondeterminism. On the other hand, one may design rules to establish almost-sure termination or positive-almost-sure termination [Chakarov and Sankaranarayanan 2013; Ferrer Fioriti and Hermanns 2015; Kaminski et al. 2016; McIver et al. 2017].

Dijkstra monads. Dijkstra monads [Maillard et al. 2019; Ahman et al. 2017] target code written in monadic form and specified using dependent types. The type-checking process essentially applies weakest-precondition rules and results in the production of proof obligations. In practice, specifications are expressed in first-order logic, so that proof obligations can be discharged using SMT solvers. Dijkstra monads encourage metareasoning using object-language dependent types only; they do not appear to have been designed for, or demonstrated capable of, carrying out inductions over program executions. Dijkstra monads can be instantiated in particular with the nondeterminism monad (NDet). In the current presentation [Ahman et al. 2017], the monad models sets of possible outcomes using finite sets, which rules out infinitely branching nondeterminism and does not allow for abstraction in intermediate postconditions (e.g., asserting that a subterm \(t_1\) returns an even integer).

Skip 8CONCLUSION AND FUTURE WORK Section

8 CONCLUSION AND FUTURE WORK

This article provides an in-depth introduction to the definitions, properties, and applications of the omni-big-step and omni-small-step semantics. These applications include mechanized proofs of type soundness, foundational constructions of Separation Logic, and compiler correctness proofs.

It would be interesting future work to investigate whether a mixed inductive-coinductive version of omni-big-step semantics could be defined and provide smooth reasoning for the combined challenge of potentially infinite executions, nondeterminism, and undefined behavior. The key challenge is to find a way to carry out compiler-correctness proofs through a single pass that handles reasoning about both terminating and nonterminating executions.

APPENDICES

A ON THE CHALLENGE OF DEFINING WP INDUCTIVELY

The weakest-precondition-style reasoning rule for let-bindings is traditionally stated as follows: \(\begin{equation*} \scriptsize{\text{wp-let}:}\qquad { \textsf {wp}\,t_1\,(\lambda v^{\prime }.\, \textsf {wp}\,([v^{\prime }/x]\,t_2)\,Q) } \;\, \vdash \;\,{ \textsf {wp}\,(\textsf {let}\, x = t_1 \,\textsf {in}\, t_2)\,Q }. \end{equation*}\) Translating it to a big-step omnisemantics rule results in the following rule:

The rule omni-big-let-chained can be useful for reasoning when one does not want to specify an explicit postcondition that needs to hold between \(t_1\) and \(t_2\). This chained rule can be straightforwardly derived from the omni-big-let rule part of the definition of the omni-big-step semantics, by instantiating \(Q_1\) as \({\lbrace (v^{\prime },s^{\prime }) \mid {([v^{\prime }/x]\,t_2) / s^{\prime }}\,\Downarrow \,{Q}\rbrace }\) in the first premise, then checking the tautology associated with the second premise.
One might wonder why we do not use omni-big-let-chained directly in the inductively defined rules. The reason is that Coq’s strict positivity requirement on the well-formedness of inductive definitions does not allow it.

To elaborate on this point, consider the four candidate Coq rules stated below.

The first rule directly translates wp-let. It is rejected by Coq because it includes a non-strictly positive occurrence of the predicate wp.

The second rule attempts a reformulation by expanding the definition of entailment and by introducing a variable name Q1 for the intermediate postcondition, together with an equality constraint on Q1. Yet, Coq rejects this rule just like the previous.

The third rule modifies the first rule by introducing an existentially quantified intermediate postcondition Q1, quantifying over the items that belong to it. This rule is accepted by Coq. Yet, in that form, Coq (v8.14) generates a useless induction principle, which provides no induction hypothesis for the nested occurence of wp. (This weakness can be corrected by stating and proving an induction principle manually, but we prefer to avoid the extra hassle.)

The fourth rule corresponds to omni-big-let. It adapts the previous rule by quantifying Q1 universally at the level of the constructor. This presentation is properly recognized by the induction-principle generator of Coq.

B UNSPECIFIED EVALUATION ORDER

For a language that uses an unspecified but consistent order of evaluation for arguments of, e.g., pairs or applications, we can consider a generalized version of the rule omni-big-pair from the previous section. Essentially, we duplicate the premises to account for the two possible evaluation orders.

To avoid the duplication in the premises, one can follow the approach described in Section 5.5 of the paper on the pretty-big-step semantics [Charguéraud 2013], which presents a general rule for evaluating a list of subterms in arbitrary order.

Note that we do not attempt to model languages that allow arbitrary interleavings in the evaluation of arguments, as, e.g., arithmetic expressions in the C language [Krebbers 2015]. More generally, concurrent evaluation is out of the scope of the present article.

C OMNISEMANTICS RULES IN THE PRESENCE OF EXCEPTIONS

For a programming language that features exceptions, the reasoning rule for let-bindings needs to be adapted in two ways. Indeed, if the body of the let-binding raises an exception, then the continuation should not be evaluated. Moreover, the exception raised should be included in the set of results that the let-binding can produce.

There are two ways to extend the grammar of results with exceptions. The first possibility is to add a constructor to the grammar of values. In this case, the postcondition Q remains a predicate over pairs of values and states. The second possibility is to introduce a type, to capture the sum of the type of values and of the type of exceptions. In that case, the postcondition Q becomes a predicate over pairs of results and states.

For simplicity, let us assume in what follows that the grammar of values includes a constant exception construct, written \(\textsf {exn}\). In that setting, the omni-big-step evaluation rule for a let-binding of the form \((\textsf {let}\, x = t_1 \,\textsf {in}\, t_2)\) can be stated as follows. The first premise describes the evaluation of \(t_1\). The second premise handles the case where \(t_1\) produces a normal value. The third premise handles the case where \(t_1\) produces an exception.

We proved in Coq the equivalence of this treatment of exceptions with the formalization of exceptions expressed both in standard small-step and in standard big-step semantics.

D DEFINITION OF THE TERMINATION JUDGMENT

We introduced the termination judgment to formalize the interpretation of the omni-big-step judgment (Section 2.2, omni-big-step-iff-terminates-and-correct). The predicate \(\textsf {terminates}(t,s)\) asserts that all executions of configuration \(t/s\) terminate. In this section, we present two formal definitions of this predicate, one in small-step style and one in big-step style.

The small-step version is inductively defined by the two rules shown below.

The big-step version is inductively defined using one rule per language construct. We show below the rules for values and for let-bindings. This definition corresponds to an inductive version of the coinductive judgment safe from Wang et al. [2014], described in Section 8.

E DEFINITION OF THE TYPING JUDGMENT

This section states the typing rules for the state-free language considered in Section 4.1. The typing rules are given for terms in A-normal form. The judgment \(\, \vdash \,v \, : \,T\) asserts that the closed value v admits the type T. The judgment \(E \, \vdash \,t \, : \,T\) asserts that the term t admits type T in the environment E. Finally, \(\mathbb {V}\) denotes the set of terms that are either values or variables.

F EXTENSION OF THE TYPING JUDGMENT FOR STATE

This section states the typing rules for the imperative language considered in Section 4.2. There, the typing judgment for terms takes the form \(S; E \, \vdash \,t \, : \,T\), and the typing judgment for closed values takes the form \(S \, \vdash \,v \, : \,T\), where the store typing S maps locations to types. The rules from the previous appendix are extended simply to thread S throughout the judgment. The new rules include the rule for typing locations and the rules for memory operations. They are shown next.

G DEFINITION OF THE STANDARD SMALL-STEP JUDGMENT

In Section 2.4, we gave a characterization of coinductive omni-big-step semantics in terms of the standard small-step semantics, written \(t / s \longrightarrow t^{\prime } / s^{\prime }\). For reference, we give below the rules that define the standard small-step judgment.

H EVALUATION OF UNARY AND BINARY OPERATORS

The following definitions complete the semantics described in the case study “compiling immutable pairs to heap-allocated records” (Section 6.4).

Footnotes

  1. 1 Lean matches Coq, and a proof based on Agda’s flexible dependent pattern matching still takes superlinear time to check.

    Footnote
  2. 2 The present article would, in particular, provide a formal publication of the results covered by the chapter on nondeterminism and the chapter on partial correctness from Charguéraud’s Separation Logic Foundations course, Volume 6 of the Software Foundations series. These results originally covered only omni-big-step semantics but have been extended in 2021 to cover omni-small-step semantics as well.

    Footnote
  3. 3 In our Coq formalization, the grammar of values is restricted to closed values (i.e., values without free variables). This design choice significantly simplifies the reasoning about substitutions. One minor consequence is that the function construct needs to appear twice: once in the grammar of closed values and once in the grammar of terms.

    Footnote
  4. 4 In Coq, we model sets with elements of type A as functions from A to propositions, and thus \(Q_1\) is represented as a function that takes a value and a state and returns a proposition; \(Q^{\prime }\) is a function that takes a value, a state, another value, and another state and returns a proposition; and the union over the family of results is written \(\lambda ~v^{\prime \prime }~s^{\prime \prime }.~\exists ~v^{\prime }~s^{\prime }.~Q_1~v^{\prime }~s^{\prime } ~\wedge ~ Q^{\prime }~v^{\prime }~s^{\prime }~v^{\prime \prime }~s^{\prime \prime }\).

    Footnote
  5. 5 The proofs that we present do not exploit classical logic axioms. However, we do not provide a machine-checked proof that our proofs are constructive. Indeed, our Coq development is building on top of general-purpose libraries that exploit classical logic in various places.

    Footnote
  6. 6 The generic entailment from induction-for-type-safety to type-safety holds for any typing judgment of the form \(\emptyset \, \vdash \,t \, : \,T\) and for any judgment \(t \longrightarrow P\) related to the small-step judgment \(t \longrightarrow t^{\prime }\) in the expected way, that is, satisfying the property omni-small-step-iff-progress-and-correct from Section 3.2.

    Footnote
  7. 7 We follow CompCert’s terminology, using “forward” and “backward” to refer to the direction of compilation, “forward” meaning from source language to target language. We note the conflict with other literature [Lynch and Vaandrager 1995] that uses “forward” and “backward” to refer to the direction of the state transitions.

    Footnote
  8. 8 The number of corresponding steps in the source program should not be zero; otherwise the target program could diverge, whereas the source program terminates. In practice, however, it is not always easy to find one source-program step that corresponds to a target-program step. In such situations, one may consider a generalized version of backward simulations that allow for zero source-program steps, provided that some well-founded measure decreases [Leroy 2009].

    Footnote
  9. 9 Operational semantics need not provide an actual implementation for the operation of picking a random number in \(\mathbb {Z}\).

    Footnote

REFERENCES

  1. Abate Carmine, Blanco Roberto, Ciobâcă Ştefan, Durier Adrien, Garg Deepak, Hriţcu Cătălin, Patrignani Marco, Tanter Éric, and Thibault Jérémy. 2021. An extended account of trace-relating compiler correctness and secure compilation. ACM Transactions on Programming Languages and Systems 43, 4 (Nov.2021), 14:1–14:48. Google ScholarGoogle ScholarDigital LibraryDigital Library
  2. Ahman Danel, Hriţcu Cătălin, Maillard Kenji, Martínez Guido, Plotkin Gordon, Protzenko Jonathan, Rastogi Aseem, and Swamy Nikhil. 2017. Dijkstra monads for free. ACM SIGPLAN Notices 52, 1 (Jan.2017), 515529. Google ScholarGoogle ScholarDigital LibraryDigital Library
  3. Apt K. R. and Plotkin G. D.. 1986. Countable nondeterminism and random assignment. J. ACM 33, 4 (Aug.1986), 724767. Google ScholarGoogle ScholarDigital LibraryDigital Library
  4. Back R. J. R.. 1983. A continuous semantics for unbounded nondeterminism. Theoretical Computer Science 23, 2 (1983), 187210. Google ScholarGoogle ScholarCross RefCross Ref
  5. Batz Kevin, Kaminski Benjamin Lucien, Katoen Joost-Pieter, Matheja Christoph, and Noll Thomas. 2019. Quantitative separation logic: A logic for reasoning about probabilistic pointer programs. Proc. ACM Program. Lang. 3, POPL, Article 34 (Jan.2019), 29 pages. Google ScholarGoogle ScholarDigital LibraryDigital Library
  6. Birkedal Lars, Torp-Smith Noah, and Yang Hongseok. 2005. Semantics of separation-logic typing and higher-order frame rules. In 20th Annual IEEE Symposium on Logic in Computer Science (LICS’05). IEEE, 260269. Google ScholarGoogle ScholarDigital LibraryDigital Library
  7. Blazy Sandrine and Leroy Xavier. 2009. Mechanized semantics for the Clight subset of the C language. Journal of Automated Reasoning 43, 3 (2009), 263288. Google ScholarGoogle ScholarCross RefCross Ref
  8. Cao Qinxiang, Wang Shengyi, Hobor Aquinas, and Appel Andrew W.. 2018. Proof pearl: Magic wand as frame. Unpublished.Google ScholarGoogle Scholar
  9. Chakarov Aleksandar and Sankaranarayanan Sriram. 2013. Probabilistic program analysis with Martingales. In Computer Aided Verification, Sharygina Natasha and Veith Helmut (Eds.). Springer, Berlin, 511526.Google ScholarGoogle ScholarCross RefCross Ref
  10. Charguéraud Arthur. 2011. Characteristic formulae for the verification of imperative programs. In International Conference on Functional Programming (ICFP’11). Association for Computing Machinery, 418430. Google ScholarGoogle ScholarDigital LibraryDigital Library
  11. Charguéraud Arthur. 2013. Pretty-big-step semantics. In Proceedings of the 22nd European Conference on Programming Languages and Systems (ESOP ’13). Springer-Verlag, 4160. Google ScholarGoogle ScholarDigital LibraryDigital Library
  12. Charguéraud Arthur. 2020. Separation logic for sequential programs (functional Pearl). Proc. ACM Program. Lang. 4, ICFP, Article 116 (Aug.2020), 34 pages. Google ScholarGoogle ScholarDigital LibraryDigital Library
  13. Charguéraud Arthur. 2022. A Modern Eye on Separation Logic for Sequential Programs. Technical Report. 142 pages. https://hal.inria.fr/hal-03864664v1.Google ScholarGoogle Scholar
  14. Charguéraud Arthur and Pottier François. 2019. Verifying the correctness and amortized complexity of a union-find implementation in separation logic with time credits. Journal of Automated Reasoning 62, 3 (March2019), 331365. Google ScholarGoogle ScholarDigital LibraryDigital Library
  15. Chlipala Adam. 2013. The bedrock structured programming system: Combining generative metaprogramming and Hoare logic in an extensible program verifier. In Proceedings of the 18th ACM SIGPLAN International Conference on Functional Programming (ICFP’13). Association for Computing Machinery, 391402. Google ScholarGoogle ScholarDigital LibraryDigital Library
  16. Cousot Patrick and Cousot Radhia. 1992. Inductive definitions, semantics and abstract interpretations. In Proceedings of the 19th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’92). Association for Computing Machinery, 8394. Google ScholarGoogle ScholarDigital LibraryDigital Library
  17. Danielsson Nils Anders. 2012. Operational semantics using the partiality Monad. SIGPLAN Not. 47, 9 (Sept.2012), 127138. Google ScholarGoogle ScholarDigital LibraryDigital Library
  18. Dijkstra Edsger W.. 1976. A Discipline of Programming.Prentice-Hall. I–XVII, 1–217 pages. Google ScholarGoogle Scholar
  19. Erbsen Andres, Gruetter Samuel, Choi Joonwon, Wood Clark, and Chlipala Adam. 2021. Integration verification across software and hardware for a simple embedded system. In Proceedings of the 42nd ACM SIGPLAN International Conference on Programming Language Design and Implementation (PLDI’21). Association for Computing Machinery, 604619. Google ScholarGoogle ScholarDigital LibraryDigital Library
  20. Fioriti Luis María Ferrer and Hermanns Holger. 2015. Probabilistic termination: Soundness, completeness, and compositionality. SIGPLAN Not. 50, 1 (Jan.2015), 489501. Google ScholarGoogle ScholarDigital LibraryDigital Library
  21. Francez Nissim, Hoare C. A. R., Lehmann Daniel J., and Roever Willem P. De. 1979. Semantics of nondeterminism, concurrency, and communication. J. Comput. System Sci. 19, 3 (Dec.1979), 290308. . http://www.sciencedirect.com/science/article/pii/00220000799000 60.Google ScholarGoogle ScholarCross RefCross Ref
  22. Guéneau Armaël, Myreen Magnus O., Kumar Ramana, and Norrish Michael. 2017. Verified characteristic formulae for CakeML. In European Symposium on Programming (ESOP’17), Yang Hongseok (Ed.). Springer, Berlin, 584610. Google ScholarGoogle ScholarDigital LibraryDigital Library
  23. Hobor Aquinas and Villard Jules. 2013. The ramifications of sharing in data structures. In Proceedings of the 40th Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’13). Association for Computing Machinery, 523536. Google ScholarGoogle ScholarDigital LibraryDigital Library
  24. Jung Ralf, Krebbers Robbert, Jourdan Jacques-Henri, Bizjak Aleš, Birkedal Lars, and Dreyer Derek. 2018. Iris from the ground up: A modular foundation for higher-order concurrent separation logic. Journal of Functional Programming 28 (2018). Google ScholarGoogle ScholarCross RefCross Ref
  25. Jung Ralf, Swasey David, Sieczkowski Filip, Svendsen Kasper, Turon Aaron, Birkedal Lars, and Dreyer Derek. 2015. Iris: Monoids and invariants as an orthogonal basis for concurrent reasoning. In Proceedings of the 42nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’15). Association for Computing Machinery, 637650. Google ScholarGoogle ScholarDigital LibraryDigital Library
  26. Kaminski Benjamin Lucien, Katoen Joost-Pieter, Matheja Christoph, and Olmedo Federico. 2016. Weakest precondition reasoning for expected run–Times of probabilistic programs. In Programming Languages and Systems, Thiemann Peter (Ed.). Springer, Berlin, 364389. Google ScholarGoogle ScholarDigital LibraryDigital Library
  27. Koh Nicolas, Li Yao, Li Yishuai, Xia Li-yao, Beringer Lennart, Honoré Wolf, Mansky William, Pierce Benjamin C., and Zdancewic Steve. 2019. From C to interaction trees: Specifying, verifying, and testing a networked server. In Proceedings of the 8th ACM SIGPLAN International Conference on Certified Programs and Proofs (CPP’19). Association for Computing Machinery, 234248. Google ScholarGoogle ScholarDigital LibraryDigital Library
  28. Krebbers Robbert. 2015. The C Standard Formalized in Coq. Ph.D. Dissertation. Radboud University Nijmegen. https://robbertkrebbers.nl/research/thesis.pdf.Google ScholarGoogle Scholar
  29. Krishnaswami Neel R., Birkedal Lars, and Aldrich Jonathan. 2010. Verifying event-driven programs using ramified frame properties. In Proceedings of the 5th ACM SIGPLAN Workshop on Types in Language Design and Implementation (TLDI’10). Association for Computing Machinery, 6376. Google ScholarGoogle ScholarDigital LibraryDigital Library
  30. Kumar Ramana, Myreen Magnus O., Norrish Michael, and Owens Scott. 2014. CakeML: A verified implementation of ML. In Principles of Programming Languages (POPL’14). ACM Press, 179191. Google ScholarGoogle ScholarDigital LibraryDigital Library
  31. Leroy Xavier. 2009. A formally verified compiler back-end. Journal of Automated Reasoning 43, 4 (Dec.2009), 363446. Google ScholarGoogle ScholarDigital LibraryDigital Library
  32. Leroy Xavier and Grall Hervé. 2009. Coinductive big-step operational semantics. Information and Computation 207, 2 (2009), 284304. Special issue on Structural Operational Semantics (SOS).Google ScholarGoogle ScholarDigital LibraryDigital Library
  33. Lynch Nancy and Vaandrager Frits. 1995. Forward and backward simulations part I: Untimed Systems. Information and Computation 121 (1995), 214233.Google ScholarGoogle ScholarDigital LibraryDigital Library
  34. Maillard Kenji, Ahman Danel, Atkey Robert, Martínez Guido, Hriţcu Cătălin, Rivas Exequiel, and Tanter Éric. 2019. Dijkstra monads for all. Proceedings of the ACM on Programming Languages 3, ICFP (July2019), 104:1–104:29. Google ScholarGoogle ScholarDigital LibraryDigital Library
  35. McIver Annabelle and Morgan Carroll. 2005. Abstraction, Refinement And Proof For Probabilistic Systems. Springer. Google ScholarGoogle Scholar
  36. McIver Annabelle, Morgan Carroll, Kaminski Benjamin Lucien, and Katoen Joost-Pieter. 2017. A new proof rule for almost-sure termination. Proc. ACM Program. Lang. 2, POPL, Article 33 (Dec.2017), 28 pages. Google ScholarGoogle ScholarDigital LibraryDigital Library
  37. Mével Glen, Jourdan Jacques-Henri, and Pottier François. 2019. Time credits and time receipts in Iris. In Programming Languages and Systems, Caires Luís (Ed.). Springer International Publishing, Cham, 329. Google ScholarGoogle ScholarCross RefCross Ref
  38. Milner Robin. 1975. Processes: A mathematical model of computing agents. In Studies in Logic and the Foundations of Mathematics. Vol. 80. Elsevier, 157173.Google ScholarGoogle Scholar
  39. Monin Jean-François and Shi Xiaomu. 2013. Handcrafted inversions made operational on operational semantics. In Interactive Theorem Proving, Hutchison David, Kanade Takeo, Kittler Josef, Kleinberg Jon M., Mattern Friedemann, Mitchell John C., Naor Moni, Nierstrasz Oscar, Rangan C. Pandu, Steffen Bernhard, Sudan Madhu, Terzopoulos Demetri, Tygar Doug, Vardi Moshe Y., Weikum Gerhard, Blazy Sandrine, Paulin-Mohring Christine, and Pichardie David (Eds.). Vol. 7998. Springer, Berlin, 338353. Google ScholarGoogle ScholarDigital LibraryDigital Library
  40. Nakata Keiko and Uustalu Tarmo. 2010. Mixed induction-coinduction at work for Coq. In 2nd Workshop of Coq Users, Developers, and Contributors (2010). http://www.cs.ioc.ee/ keiko/papers/Coq2.pdf.Google ScholarGoogle Scholar
  41. Ni Zhaozhong and Shao Zhong. 2006. Certified assembly programming with embedded code pointers. In Conference Record of the 33rd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’06). Association for Computing Machinery, 320333. Google ScholarGoogle ScholarDigital LibraryDigital Library
  42. O’Hearn, Reynolds, and Yang. 2001. Local reasoning about programs that alter data structures. In 15th Workshop on Computer Science Logic (CSL’01). LNCS, Springer-Verlag. Google ScholarGoogle ScholarCross RefCross Ref
  43. O’Hearn Peter W.. 2019. Separation logic. Commun. ACM 62, 2 (2019), 8695. The appendix is linked as supplementary material from the ACM Digital Library.Google ScholarGoogle ScholarDigital LibraryDigital Library
  44. Pierce Benjamin C.. 2002. Types and Programming Languages. MIT Press. Google ScholarGoogle ScholarDigital LibraryDigital Library
  45. Plotkin G. D.. 1976. A powerdomain construction. Siam J. of Computing (1976).Google ScholarGoogle ScholarDigital LibraryDigital Library
  46. Reynolds John C.. 2002. Separation logic: A logic for shared mutable data structures. In Annual IEEE Symposium on Logic in Computer Science (LICS’02). 5574. Google ScholarGoogle ScholarCross RefCross Ref
  47. Rompf Tiark and Amin Nada. 2016. Type soundness for dependent object types (DOT). In Proceedings of the 2016 ACM SIGPLAN International Conference on Object-Oriented Programming, Systems, Languages, and Applications. ACM, 624641. Google ScholarGoogle ScholarDigital LibraryDigital Library
  48. Schäfer Steven, Schneider Sigurd, and Smolka Gert. 2016. Axiomatic semantics for compiler verification. In Proceedings of the 5th ACM SIGPLAN Conference on Certified Programs and Proofs. ACM, 188196. Google ScholarGoogle ScholarDigital LibraryDigital Library
  49. Schwinghammer Jan, Bizjak Aleš, and Birkedal Lars. 2013. Step-indexed relational reasoning for countable nondeterminism. Logical Methods in Computer Science 9 (2013). Google ScholarGoogle ScholarCross RefCross Ref
  50. Ševčík Jaroslav, Vafeiadis Viktor, Nardelli Francesco Zappa, Jagannathan Suresh, and Sewell Peter. 2013. CompCertTSO: A verified compiler for relaxed-memory concurrency. J. ACM 60, 3 (June2013), 150. Google ScholarGoogle ScholarDigital LibraryDigital Library
  51. Spies Simon, Gäher Lennard, Gratzer Daniel, Tassarotti Joseph, Krebbers Robbert, Dreyer Derek, and Birkedal Lars. 2021. Transfinite Iris: Resolving an existential dilemma of step-indexed separation logic. In Proceedings of the 42nd ACM SIGPLAN International Conference on Programming Language Design and Implementation (PLDI’21). Association for Computing Machinery, 8095. Google ScholarGoogle ScholarDigital LibraryDigital Library
  52. Staton Sam, Yang Hongseok, Heunen Chris, Kammar Ohad, and Wood Frank. 2016. Semantics for probabilistic programming: Higher-order functions, continuous distributions, and soft constraints. Proceedings of the 31st Annual ACM/IEEE Symposium on Logic in Computer Science, 525534. arxiv:1601.04943.Google ScholarGoogle ScholarDigital LibraryDigital Library
  53. Svendsen Kasper, Sieczkowski Filip, and Birkedal Lars. 2016. Transfinite step-indexing: Decoupling concrete and logical steps. In Programming Languages and Systems, Thiemann Peter (Ed.). Springer, Berlin, 727751. Google ScholarGoogle ScholarCross RefCross Ref
  54. Tassarotti Joseph, Jung Ralf, and Harper Robert. 2017. A higher-order logic for concurrent termination-preserving refinement. In Programming Languages and Systems, Yang Hongseok (Ed.). Springer, Berlin, 909936. Google ScholarGoogle ScholarDigital LibraryDigital Library
  55. Wang Di, Hoffmann Jan, and Reps Thomas. 2019. A denotational semantics for low-level probabilistic programs with nondeterminism. Electronic Notes in Theoretical Computer Science 347 (2019), 303324. Proceedings of the 35th Conference on the Mathematical Foundations of Programming Semantics.Google ScholarGoogle ScholarDigital LibraryDigital Library
  56. Wang Peng, Cuellar Santiago, and Chlipala Adam. 2014. Compiler verification meets cross-language linking via data abstraction. In OOPSLA. ACM Press, 675690. Google ScholarGoogle ScholarDigital LibraryDigital Library
  57. Wright A. K. and Felleisen M.. 1994. A syntactic approach to type soundness. Information and Computation 115, 1 (Nov.1994), 3894. Google ScholarGoogle ScholarDigital LibraryDigital Library
  58. Xia Li-yao, Zakowski Yannick, He Paul, Hur Chung-Kil, Malecha Gregory, Pierce Benjamin C., and Zdancewic Steve. 2019. Interaction trees: Representing recursive and impure programs in Coq. Proceedings of the ACM on Programming Languages 4, POPL (Dec.2019), 51:1–51:32. Google ScholarGoogle ScholarDigital LibraryDigital Library

Index Terms

  1. Omnisemantics: Smooth Handling of Nondeterminism

              Recommendations

              Comments

              Login options

              Check if you have access through your login credentials or your institution to get full access on this article.

              Sign in

              Full Access

              • Published in

                cover image ACM Transactions on Programming Languages and Systems
                ACM Transactions on Programming Languages and Systems  Volume 45, Issue 1
                March 2023
                274 pages
                ISSN:0164-0925
                EISSN:1558-4593
                DOI:10.1145/3572862
                • Editor:
                • Jan Vitek
                Issue’s Table of Contents

                Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than the author(s) must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected].

                Publisher

                Association for Computing Machinery

                New York, NY, United States

                Publication History

                • Published: 8 March 2023
                • Online AM: 24 January 2023
                • Accepted: 16 November 2022
                • Revised: 26 September 2022
                • Received: 17 March 2022
                Published in toplas Volume 45, Issue 1

                Permissions

                Request permissions about this article.

                Request Permissions

                Check for updates

                Qualifiers

                • research-article
              • Article Metrics

                • Downloads (Last 12 months)370
                • Downloads (Last 6 weeks)76

                Other Metrics

              PDF Format

              View or Download as a PDF file.

              PDF

              eReader

              View online with eReader.

              eReader

              HTML Format

              View this article in HTML Format .

              View HTML Format
              About Cookies On This Site

              We use cookies to ensure that we give you the best experience on our website.

              Learn more

              Got it!