REMERCIEMENTS

Merci à mes directeur et co-directeur co-encadrant de thèse de m'avoir guidé jusque là.

Merci aux rapporteurs d'avoir pris le temps de relire ce pavé.

Merci à toute la liste Platypus pour les discussions et les soirées.

Merci à ma famille pour son soutien et les kiwis.

Merci à ma soeur pour ses élans chaotiques arbitraires.

Muxu. de précision. Par exemple, la sémantique de JavaScript est très détaillée et peut être vue comme une traduction en anglais de règles d'inférence, alors que la documentation de Python est une description de plus haut niveau du comportement attendu.

Ces définitions informelles limitent les utilisations possibles des sémantiques des langages de programmation. Il est difficile de les utiliser pour de la vérification ou de la certification. À l'inverse, une sémantique mécanisée sur ordinateur peut servir de référence pour prouver des propriétés du langage, ou pour certifier certains outils associés tels qu'un interpréteur ou un débogueur.

. Dans ce document, nous utilisons les sémantiques squelettiques qui permettent une manipulation très simple des sémantiques opérationnelles.

Sémantiques Squelettiques

Les sémantiques squelettiques sont un cadre logique pour formaliser les sémantiques opérationnelles de langages de programmation à l'aide d'un petit métalangage nommé Skel. La sémantique d'un langage est représentée par un objet écrit dans la syntaxe de Skel. Cette définition peut être vue comme un morceau de code (du métalangage) ou un interpréteur. Cela permet aux sémantiques d'être plus facile à écrire, lire et manipuler. Une telle définition est rendue formelle et non ambiguë indirectement, via la sémantique du métalangage Skel définie par des règles d'inférence. Le métalangage Skel est pensé pour permettre une grande modularité des sémantiques squelettiques. Le principe fondamental est que l'on peut définir précisément la structure d'une fonction d'évaluation (séquences d'opérations, appels récursifs, branchements non déterministes, ...) tout en gardant abstrait le comportement des opérations de base (comparaison de deux entiers, mise à jour d'un environnement, ...). La structure peut ainsi être analysée, transformée, ou certifiée indépendamment des choix d'implémentation des opérations de base. Par exemple, on peut formaliser le petit langage précédent de la manière suivante : type ident type expr = | Var ident | Plus of expr * expr | ... type stmt = | Skip | While of expr * stmt | ... val add : ... hook hexpr (s: state, e: expr) matching e = | Var (x) -> ... | Plus (e1, e2) -> let (s1, n1) = hexpr (s, e1) in let (s2, n2) = hexpr (s1, e2) in let n = add (n1, n2) in (s2, n) | ...

RÉSUMÉ EN FRANÇAIS

Langages et Sémantiques

On garde ainsi l'idée qu'évaluer Plus(e 1 , e 2 ) consiste à évaluer successivement e 1 et e 2 (par appels récursifs), puis à additionner les résultats. Ceci est indépendant du comportement de l'addition (add).

Les sémantiques squelettiques, et leur implémentation en OCaml [START_REF] Leroy | The OCaml system: Documentation and user's manual, version 4.12[END_REF] appelée Necro [START_REF] Courant | Necro: Animating Skeletons[END_REF],

peuvent ensuite traiter automatiquement ces définitions. Différents outils permettent notamment d'exporter le langage en une définition Coq, ou de générer automatiquement un interpréteur et un débogueur à partir de la sémantique.

Le cadre des sémantiques squelettiques a été progressivement enrichi, et ce document couvre deux versions différentes du métalangage Skel. La différence principale étant que la version utilisée en Partie I de ce document n'autorise pas les fonctions d'ordre supérieur.

Contributions

Il existe différents formats de sémantiques opérationnelles. Par exemple, on trouve des sémantiques à grand pas, reliant un programme à son résultat final, des sémantiques à petit pas, explicitant les calculs intermédiaires, et des machines abstraites, détaillant très précisément la gestion des différents éléments. Ces formats ont chacun leurs avantages et inconvénients. Les sémantiques à grand pas sont les plus intuitives et les plus répandues, mais ne peuvent pas rendre compte de tous les comportements observables avec d'autres types de sémantique. Selon les propriétés que l'on souhaite vérifier, il est parfois nécessaire de manipuler plusieurs version de la sémantique d'un même langage.

Cependant, formaliser plusieurs fois le même langage à la main n'est pas idéal. En plus de nécessiter davantage de travail, il faut s'assurer que les différentes définitions sont toutes équivalentes. Il est donc préférable de développer des méthodes systématiques pour générer toutes ces définitions à partir d'une seule, avec le plus de garanties possibles. Ce document approfondit ainsi les interdérivations de sémantiques opérationnelles, à la fois au niveau des langages utilisateurs et du métalangage Skel.

Transformation Automatique de Grand Pas en Petit Pas

La première partie de cette thèse présente une nouvelle méthode pour transformer automatiquement une sémantique squelettique à grand pas en une sémantique squelettique à petit pas équivalente.

La difficulté principale de cette transformation en petit pas est la création de nouveaux constructeurs. En effet, la syntaxe initiale n'est pas toujours suffisante pour exprimer les calculs intermédiaires qui peuvent apparaître durant une évaluation. Cette situation apparaît en général lorsqu'un argument a besoin d'être évalué plusieurs fois. Par exemple, pour le constructeur

Plus(e 1 , e 2 ), la règle d'inférence commence par une prémisse s, e 1 ⇓ s 1 , n 1 . En petit pas, si l'on commence le calcul de e 1 avec une prémisse s, e 1 → s ′ , e ′ 1 , on peut sans problèmes écraser la valeur initiale de e 1 . Notre méthode génère donc la règle suivante.

s, e 1 → s ′ , e ′ 1 s, Plus(e 1 , e 2 ) → s ′ , Plus(e ′ 1 , e 2 )

Pour le constructeur while e do c en revanche, on ne peut pas avoir de règle similaire. Si on commence à évaluer e avec une prémisse s, e → s ′ , e ′ , on ne peut pas écraser la valeur initiale de e avec e ′ , puisque l'on doit potentiellement réévaluer while e do c plus tard. La transformation étend alors la syntaxe, et introduit un nouveau constructeur (nommé While1) capable de stocker le résultat intermédiaire nécessaire.

Cette transformation se découpe en plusieurs étapes. Notamment, une phase d'analyse parcourt la sémantique pour déterminer quels points de programme nécessitent l'ajout de nouveaux constructeurs, et essaie de réutiliser les constructeurs existants autant que possible. Une dernière phase produit la sémantique à petit pas en utilisant ces nouveaux constructeurs.

Il est également important de vérifier que cet algorithme est correct, c'est à dire que la sémantique à petit pas en sortie est effectivement équivalente à la sémantique à grand pas fournie en entrée. Comme la phase d'analyse est complexe et sujette à des optimisations, la version complète de l'algorithme se prête mal à une certification directe. À la place, nous développons deux approches différentes et complémentaires pour s'assurer que la transformation produit le résultat escompté.

Tout d'abord, nous présentons une preuve papier que la transformation sans analyse est correcte. En pratique, cette version simplifiée produit une sémantique à petit pas moins optimisée avec plus de constructeurs que nécessaire. La preuve mathématique garantie que la stratégie fondamentale est valide sur tous les langages.

Ensuite, pour attester de l'implémentation de la version complète, nous générons également, pour chaque langage, un certificat Coq de l'équivalence entre les sémantiques d'entrée et de sortie. Necro permet d'obtenir les définitions Coq des deux sémantiques, et la stratégie connue de preuve d'équivalence ne dépend pas de ces sémantiques. Il nous est donc possible de générer un script de preuve Coq spécialisé pour chaque langage. Cette certification à posteriori permet à la fois de garantir l'équivalence directement sur le langage qui nous intéresse, mais aussi de ne pas craindre la présence de bugs dans l'implémentation puisque l'on ne vérifie que le résultat final.

Combiner cette transformation à l'environnement Necro nous permet également de générer automatiquement un interpréteur petit pas pour n'importe quel langage.

Nouvelles Sémantiques pour le Métalangage

Comme la sémantique squelettique d'un langage est écrite dans le métalangage Skel, elle n'a de sens qu'à travers la sémantique du métalangage lui-même. Skel est défini avec une sémantique à grand pas non déterministe, appelée interprétation concrète. Bien qu'utile pour de nombreuses preuves sur les langages et leurs programmes, cette interprétation concrète ne permet ni de raisonner sur des programmes qui ne terminent pas, ni d'exécuter ces programmes. Pour pallier cela, nous proposons deux nouvelles interprétations de Skel sous forme de machines abstraites déterministe et non déterministe.

Nous ne créons pas ces nouvelles sémantiques à la main, mais nous les générons en utilisant la technique de correspondence fonctionnelle [START_REF] Mads | A functional correspondence between evaluators and abstract machines[END_REF] à partir de l'interprétation concrète.

Plan du Manuscrit

La Section 2 de ce document présente les principales formes de sémantiques opérationnelles : sémantique à grand pas, sémantique à petit pas, sémantique à réduction, et machine abstraites.

Introduction

INTRODUCTION

Software development uses programming languages as a communication tool between human programmers and our computers. Formal definitions of these languages are thus crucial to prevent translation issues between humans and electronic devices.

The syntax of a language expresses which objects are valid programs of the language. Programs can be considered mathematical objects, and are easily described with a grammar. The semantics of a language gives meaning to these programs and are essential to reason about them. Depending on the use case, this "meaning" could be a logical relation, or even another mathematical object. In this document, we limit ourselves to operational semantics where a program is interpreted as a sequence of computations. I.e., an operational semantics explains how we can run a program.

Formalizing operational semantics can be problematic. When able, as with small languages, it is preferable to use unambiguous inference rules. However, for more substantial languages, inference rules can be difficult to read, write, and maintain. As such, semantic behaviors are often stated in plain text, with varied levels of detail. For instance, the semantics of JavaScript is very precise and can be seen as an English translation of inference rules, while the Python documentation offers higher-level explanations of the expected behaviors.

Informal paper definitions are not adapted for the certification of language properties or the verification of algorithms or tools. To this end, it is preferable to mechanize the semantics on a computer, either in a generic proof assistant or in a specialized framework. Theorem provers are comprehensive tools that do not limit the range of expressible semantics, but they can require an effort of calibration for each language. Notable examples include JSCert [START_REF] Bodin | A trusted mechanised JavaScript specification[END_REF], a Coq [START_REF] Morazán | The Coq Proof Assistant[END_REF] formalization of the semantics of JavaScript, and CakeML [START_REF] Kumar | CakeML: a verified implementation of ML[END_REF], a language fully formalized in the Isabelle [START_REF] Naraschewski | [END_REF] proof assistant.

Specialized frameworks can ease the formalization process and automatically provide relevant tools, such as generating an executable interpreter from a semantics. For instance, Ott [START_REF] Sewell | Ott: Effective tool support for the working semanticist[END_REF] and

Lem [START_REF] Owens | Lem: A Lightweight Tool for Heavyweight Semantics[END_REF] are lightweight tools providing a definition format-akin to inference rules-that can then be exported to multiple different formats, including LaTeX typesetting and several proof assistants. The predominant tool for language formalization is the K framework [START_REF] Rosu | An overview of the K semantic framework[END_REF]. Semantics of languages are defined using small-step rewriting systems that can be applied under any context. The authors also automatically provides an executable interpreter and access to their own verification tool. However, users are limited by the implementation of the framework and the rewriting semantics. Besides, there is no library to access the internal representation of the language, and no practical way to perform semantics manipulation.

In this document, we use skeletal semantics [START_REF] Bodin | Skeletal semantics and their interpretations[END_REF] where semantics of languages are expressed as objects of a meta-language called Skel. It is a convenient choice because it offers a straightforward access to the manipulation of user-defined semantics, and because definitions can be easily exported to Coq for further certifications. The use of a meta-language means semantics can easily be handled as data structures, and different semantics of the meta-language provide different interpretations of these objects. Since the skeletal semantics framework has been expanded

during the PhD, this document actually covers two slightly different versions, the main difference being that the skeletal semantics used in the first half does not allow higher-order functions.

The framework is not the only choice to make to formally describe a semantics: the format of the semantics itself is also of importance. Within the domain of operational semantics, one could use for instance a big-step semantics to relate a program to its result, a small-step semantics to express intermediate computations, or an abstract machine to precisely describe the manipulation of the different arguments. Each format has benefits and drawbacks, e.g., big-step semantics are concise and intuitive, but cannot be used to express partial computations. Depending on the kind of properties we are interested in, it is sometimes necessary to manipulate several versions of the semantics of a language. For instance, the semantics of the high-level language of CompCert [START_REF] Leroy | Formal verification of a realistic compiler[END_REF] was initially defined in big-step, and was later rewritten as small-step.

However, defining the same language multiple times by hand is far from optimal. Besides the additional formalization work, one has to make sure the different semantics are all equivalent. A better approach is to build generic transformations able to generate one format from another, and use them to automatically derive everything from a single main semantics.

Our work focuses on this idea of interderiving operational semantics formats. We apply known techniques to the skeletal semantics meta-language to expand its use cases. We also present a new interderivation strategy from big-step to small-step skeletal semantics.

Contributions

The first part of this document introduces a novel generic method to automatically transform a big-step skeletal semantics into an equivalent small-step skeletal semantics. It has also been implemented in OCaml, within the Necro [START_REF] Courant | Necro: Animating Skeletons[END_REF] toolbox for the manipulation of skeletal semantics.

Combined with other tools, we can also automatically generate an OCaml small-step interpreter for any language.

As the initial syntax is, in general, not sufficient to express partial computations, the trans-Chapter 1 -Introduction formation needs to extend this syntax with new constructors. The main difficulty comes from determining which new constructors are actually necessary, as to not clutter the output smallstep semantics, and is settled through a complex analysis phase.

Ensuring the correctness of this transformation is essential. I.e., we want to make sure that the output small-step semantics is equivalent to the input big-step semantics. Since the analysis phase is involved and contains optimizations, certifying the complete transformation would be challenging. Instead, we present two complementary results to ensure the correctness of the approach.

First, we produce a paper proof that the transformation without the analysis phase is correct.

This version would produce a less optimized small-step semantics, with more constructors than necessary. This proof establishes that the fundamental strategy is valid and applicable to any language.

Second, to confirm the correctness of the implementation of the full transformation, we generate for each language an a posteriori Coq certificate of equivalence between input and output semantics. The Necro toolbox can export a semantics into a Coq definition file, and we make use of it to check the result of the transformation. This ensures the equivalence at the level of the user-defined language, and bypasses certifying the analysis optimizations and the implementation.

The second part of this document focuses on the semantics of the Skel meta-language. Its reference definition is a non-deterministic big-step semantics formalized with inference rules, called concrete interpretation. While useful to prove some properties of a language or of programs, this interpretation cannot reason about non-terminating programs and cannot run them.

We thus derive two new alternative semantics, in the form of non-deterministic and deterministic abstract machines. We use the known strategy of functional correspondence [START_REF] Mads | A functional correspondence between evaluators and abstract machines[END_REF] to generate the new semantics. It is a generic interderivation technique, combining several known transformations, to progressively rewrite a big-step semantics into an equivalent abstract machine.

Applying it on the concrete interpretation produces a non-deterministic abstract machine. The deterministic variant is obtained similarly, but we introduce checkpoints and backtracking by hand during the transformation to force a deterministic evaluation.

Both abstract machines are formalized in the Coq proof assistant, and we certify they behave as expected: the non-deterministic version is equivalent to the initial concrete interpretation; the determining version is sound (but not complete) with respect to the other two semantics.

We use the Coq extraction mechanism on the deterministic abstract machine to obtain a OCaml interpreter running the meta-language Skel. By instantiating it, we automatically generate a certified OCaml interpreter for any language defined in skeletal semantics.

As a summary, the main contributions are the following.

Organization of the Document

• A novel generic and automatic transformation from big-step to small-step skeletal semantics, implemented in OCaml;

• A paper proof of the correctness of the transformation, in a simplified case;

• An automatic a posteriori Coq proof script checking the equivalence between the input and output semantics;

• A non-deterministic abstract machine semantics for the meta-language Skel, proved equivalent to the base big-step semantics;

• A deterministic abstract machine semantics for Skel, proved sound with respect to the non-deterministic one;

• A generic certified OCaml interpreter for the meta-language Skel, that can be automatically instantiated with any skeletal semantics.

The transformation from big-step to small-step has been presented to the Principles and Practice of Declarative Programming (PPDP) 2022 conference under the title Certified Derivation of Small-Step From Big-Step Skeletal Semantics [START_REF] Ambal | Certified Derivation of Small-Step From Big-Step Skeletal Semantics[END_REF]. The abstract machine derivation and the certified interpreter have been presented to the Certified Programs and Proofs (CPP) 2022 conference under the title Certified Abstract Machines for Skeletal Semantics [START_REF] Ambal | Certified abstract machines for skeletal semantics[END_REF]. At the start of my PhD position, in the continuation of a previous internship, I also briefly worked on the formalization of HOπ in Coq. This work lead to a journal publication [START_REF] Ambal | HOπ in Coq[END_REF], but is not presented in this document.

Organization of the Document

Chapter 2 presents the different formats of operational semantics (big-step semantics, smallstep semantics, reduction semantics, and abstract machines) and some known interderivation methods.

Part I introduces a novel strategy to automatically transform a big-step skeletal semantics into an equivalent small-step skeletal semantics. Chapter 3 presents the legacy skeletal semantics used in this first part. Chapter 4 displays and formalizes the transformation phases. Chapter 5 presents two complementary methods ensuring the correctness of the transformation. Chapter 6

discusses the implementation and the practical results, while Chapter 7 details the limits of the algorithm and its relation to the previous scientific literature.

Part II of this document focuses on the semantics of the higher-order skeletal semantics metalanguage, defined in Chapter 8. We describe the known strategy of functional correspondence in Chapter 9 before applying it to the Skel meta-language. Chapter 10 presents the derivation of the non-deterministic abstract machine and the proof of equivalence, while Chapter 11 introduces the deterministic one and the proof of soundness. Finally, Chapter 12 describes how this deterministic abstract machine is used to automatically provide a certified interpreter for any language.

Chapter 2

OPERATIONAL SEMANTICS

Denotational semantics [START_REF] Scott | Toward a mathematical semantics for computer languages[END_REF] is an approach where expressions are linked to mathematical objects, which represent their meaning. Programs might for instance be associated with game states, or mathematical functions. This approach is notably used for model-checking, to certify that a system respects a specification. Denotational semantics is defined by induction on the syntax of the language, which may simplify proofs at the cost of a more complex semantics to capture recursive constructs.

Axiomatic semantics is a different approach based on Hoare logic [START_REF] Hoare | An Axiomatic Basis for Computer Programming[END_REF]. In it, we consider assertions on program states in first-order logic. The meaning of a computation is then the effect it has on these assertions as the program state is modified.

In this document, we exclusively focus on operational semantics, where a computation is associated to its result. The semantics simply describes how an evaluation is performed. Different flavors of operational semantics allow for different levels of detail.

In this chapter, we present the main four styles of operational semantics: big-step semantics (Section 2.2); small-step semantics (Section 2.3); reduction semantics (Section 2.4); and abstract machine (Section 2.5). As a running example, we use a very simple imperative programming language (IMP) with while loops, described in Section 2.1. Finally, Section 2.6 presents different known strategies for transforming a semantics from one style to another.

Syntax of IMP

We use b to denote booleans, i.e., elements of B ≜ {⊤; ⊥}. We also use the standard notion of boolean conjunction (∧) and negation (¬), e.g., ⊥ ∧ ⊤ ≜ ⊥ and ¬⊥ ≜ ⊤. We use n to denote integers in Z, with addition (+) and equality test (

(σ[x → n])(y) ≜ n if x = y σ(y) otherwise
We use the word term to refer to either a command or an arithmetic or boolean expression.

The different operational semantics presented below evaluate configurations ⟨σ, c⟩ containing a state and a term.

Big-Step Semantics

Big-step operational semantics, also known as natural semantics, is the most common and intuitive form of operational semantics. It directly relates a computation to its final result. Most programming languages, such as JavaScript [START_REF]ECMAScript language specification[END_REF] or Standard ML [START_REF] Milner | The Definition of Standard ML[END_REF], are defined in big-step.

For our IMP example, we create three evaluation predicates. They are defined in Figure 2.1 using inference rules.

• Arithmetic expressions evaluation (⇓ a ) takes as input a state and an expression and outputs an integer, so that ⇓ a ⊆ (State × AExp) × Z.

• Boolean expressions evaluate to booleans:

⇓ b ⊆ (State × BExp) × B.
• Commands return a final evaluation state:

⇓ ⊆ (State × CExp) × State.
Integers and booleans evaluate to themselves. Variables are read from the state used for the evaluation. For arithmetic and boolean operations, we evaluate each subexpression before applying the appropriate mathematical operation. A skip command does nothing. An assignment leads to an extension of the state. A sequence computes both commands in order, using the output state of the first command to compute the second command. For conditional branchings and loops, we start by evaluating the boolean expression; depending on the resulting boolean, we can have two different behaviors. x := 1 ; if Equal(x, 2) then (x := 3) else (x := 4) Evaluating it results in a state where x is mapped to 4, as demonstrated with the following tree,

⟨σ, x⟩ ⇓ a σ(x) ⟨σ, n⟩ ⇓ a n ⟨σ, a 1 ⟩ ⇓ a n 1 ⟨σ, a 2 ⟩ ⇓ a n 2 ⟨σ, Plus(a 1 , a 2 )⟩ ⇓ a n 1 + n 2 ⟨σ, b⟩ ⇓ b b ⟨σ, e⟩ ⇓ b b ⟨σ, Neg(e)⟩ ⇓ b ¬b ⟨σ, a 1 ⟩ ⇓ a n 1 ⟨σ, a 2 ⟩ ⇓ a n 2 ⟨σ, Equal(a 1 , a 2 )⟩ ⇓ b (n 1 ? = n 2 ) ⟨σ, e 1 ⟩ ⇓ b b 1 ⟨σ, e 2 ⟩ ⇓ b b 2 ⟨σ, And(e 1 , e 2 )⟩ ⇓ b (b 1 ∧ b 2 ) ⟨σ, skip⟩ ⇓ σ ⟨σ, a⟩ ⇓ a n ⟨σ, x := a⟩ ⇓ σ[x → n] ⟨σ, c 1 ⟩ ⇓ σ ′ ⟨σ ′ , c 2 ⟩ ⇓ σ ′′ ⟨σ, c 1 ; c 2 ⟩ ⇓ σ ′′ ⟨σ, e⟩ ⇓ b ⊤ ⟨σ, c 1 ⟩ ⇓ σ ′ ⟨σ, if e then c 1 else c 2 ⟩ ⇓ σ ′ ⟨σ, e⟩ ⇓ b ⊥ ⟨σ, c 2 ⟩ ⇓ σ ′ ⟨σ, if e then c 1 else c 2 ⟩ ⇓ σ ′ ⟨σ, e⟩ ⇓ b ⊤ ⟨σ, c⟩ ⇓ σ ′ ⟨σ ′ , while e do c⟩ ⇓ σ ′′ ⟨σ, while e do c⟩ ⇓ σ ′′ ⟨σ, e⟩ ⇓ b ⊥ ⟨σ, while e do c⟩ ⇓ σ
where σ 1 is {x → 1}. ⟨ϵ, 1⟩ ⇓ a 1 ⟨ϵ, x := 1⟩ ⇓ σ 1 ⟨σ 1 , x⟩ ⇓ a 1 ⟨σ 1 , 2⟩ ⇓ a 2 ⟨σ 1 , Equal(x, 2)⟩ ⇓ b ⊥ ⟨σ 1 , 4⟩ ⇓ a 4 ⟨σ 1 , x := 4⟩ ⇓ {x → 4} ⟨σ 1 , if Equal(x, 2) then (x := 3) else (x := 4)⟩ ⇓ {x → 4} ⟨ϵ, x := 1 ; if Equal(x, 2) then (x := 3) else (x := 4)⟩ ⇓ {x → 4}
Unlike the other flavors of operational semantics presented later, a big-step derivation contains the whole evaluation of a program in a single object. This makes proofs by induction on this structure very simple and quite intuitive.

Big-step operational semantics also has important limitations. Notably, a wide variety of behaviors are not captured by this definition. For instance, we cannot assign a meaning to stuck computations (like ⟨ϵ, x := Plus(x, 1)⟩) or diverging computations (like ⟨σ, while ⊤ do skip⟩), which means we cannot tell them apart. Some extensions of big-step semantics recover this distinction, usually at the cost of an explicit error constructor added to the language. Big-step is not suitable for concurrent semantics (see next section), as it does not provide a way to interrupt a computation. As such, it is not adapted to low-level programs such as in operating systems.

Small-Step Semantics

The next format we present is small-step operational semantics, also known as structural operational semantics. syntax-oriented, which means that the behavior of a computation is expressed using the behavior if its subcomputations.

For our IMP example, we create three relations, defined in Figure 2.2 using inference rules.

• For arithmetic expressions:

→ a ⊆ (State × AExp) × (State × AExp).
• For boolean expressions:

→ b ⊆ (State × BExp) × (State × BExp). • For commands: → ⊆ (State × CExp) × (State × CExp).
It is important to note that, for each relation, the input type is the same as the output type.

Evaluating a configuration produces a new configuration. It allows us to create sequences of reductions, as we are mostly interested in the reflexive transitive closure (→ * ) of these relations.

In the case of arithmetic and booleans expressions, the output state is not modified, so inference rules have premises of the form ⟨σ, a⟩ → a ⟨σ, a ′ ⟩, reusing σ-we do not need to introduce a different state variable.

For variables, we simply look up the associated value in the given state, as previously. For the Plus constructor, we need to write three inference rules. If the first subexpression can do a step, then the whole expression can do the same step. The symmetric situation is when the first subexpression is an integer (and thus cannot reduce further), and the second subexpression can reduce. Finally, an expression Plus(n 1 , n 2 ) where both subexpressions are integers can perform the addition and reduce to n 1 + n 2 . We have similar cases for the other boolean operators, where we need two to three rules for each.

⟨σ, x⟩ → a ⟨σ, σ(x)⟩ ⟨σ, a 1 ⟩ → a ⟨σ, a ′ 1 ⟩ ⟨σ, Plus(a 1 , a 2 )⟩ → a ⟨σ, Plus(a ′ 1 , a 2 )⟩ ⟨σ, a 2 ⟩ → a ⟨σ, a ′ 2 ⟩ ⟨σ, Plus(n 1 , a 2 )⟩ → a ⟨σ, Plus(n 1 , a ′ 2 )⟩ ⟨σ, Plus(n 1 , n 2 )⟩ → a ⟨σ, n 1 + n 2 ⟩ ⟨σ, a 1 ⟩ → a ⟨σ, a ′ 1 ⟩ ⟨σ, Equal(a 1 , a 2 )⟩ → b ⟨σ, Equal(a ′ 1 , a 2 )⟩ ⟨σ, a 2 ⟩ → a ⟨σ, a ′ 2 ⟩ ⟨σ, Equal(n 1 , a 2 )⟩ → b ⟨σ, Equal(n 1 , a ′ 2 )⟩ ⟨σ, Equal(n 1 , n 2 )⟩ → b ⟨σ, (n 1 ? = n 2 )⟩ ⟨σ, e 1 ⟩ → b ⟨σ, e ′ 1 ⟩ ⟨σ, And(e 1 , e 2 )⟩ → b ⟨σ, And(e ′ 1 , e 2 )⟩ ⟨σ, e 2 ⟩ → b ⟨σ, e ′ 2 ⟩ ⟨σ, And(b 1 , e 2 )⟩ → b ⟨σ, And(b 1 , e ′ 2 )⟩ ⟨σ, And(b 1 , b 2 )⟩ → b ⟨σ, (b 1 ∧ b 2 )⟩ ⟨σ, e⟩ → b ⟨σ, e ′ ⟩ ⟨σ, Neg(e)⟩ → b ⟨σ, Neg(e ′ )⟩ ⟨σ, Neg(b)⟩ → b ⟨σ, ¬b⟩ ⟨σ, a⟩ → a ⟨σ, a ′ ⟩ ⟨σ, x := a⟩ → ⟨σ, x := a ′ ⟩ ⟨σ, x := n⟩ → ⟨σ[x → n], skip⟩ ⟨σ, c 1 ⟩ → ⟨σ ′ , c ′ 1 ⟩ ⟨σ, c 1 ; c 2 ⟩ → ⟨σ ′ , c ′ 1 ; c 2 ⟩ ⟨σ, skip ; c 2 ⟩ → ⟨σ, c 2 ⟩ ⟨σ, e⟩ → b ⟨σ, e ′ ⟩ ⟨σ, if e then c 1 else c 2 ⟩ → ⟨σ, if e ′ then c 1 else c 2 ⟩ ⟨σ, if ⊤ then c 1 else c 2 ⟩ → ⟨σ, c 1 ⟩ ⟨σ, if ⊥ then c 1 else c 2 ⟩ → ⟨σ, c 2 ⟩
⟨σ, while e do c⟩ → ⟨σ, if e then (c ; while e do c) else skip⟩ This shows the structure of small-step inference rules, as each rule has either zero or exactly one premise, where the reduction of a subterm takes place. In this document, we call the latter congruence rules: they state that if a subterm is able to reduce on its own, we can simply propagate the result to the whole term, keeping the other parts unchanged. We call inference rules without premises contraction rules [START_REF] Danvy | Defunctionalized interpreters for programming languages[END_REF]. They correspond to basic reduction steps, usually either the application of a basic operator or a simplification of the term.

For assignment commands, we have two rules: a congruence rule stating we can reduce the arithmetic expression, and a contraction rule to extend the state with an integer. In the second case, we return a skip command to represent a finished computation. For sequences, we reduce the first command if able, or drop a leading skip otherwise. For conditional branchings, a congruence rule allows us to reduce the boolean expression; when we have a boolean, two contraction rules then let us drop the part of the command we no longer need.

For loops, we bypass an important issue of small-step semantics with a peculiar rule. Normally, we would expect a congruence rule of the following form.

⟨σ, e⟩ → b ⟨σ, e ′ ⟩ ⟨σ, while e do c⟩ → ⟨σ, while e ′ do c⟩ However, such a rule would completely break the semantics. We need to evaluate e to check whether to enter the loop; if we do, i.e., e evaluates to ⊤, we need to loop back to (while e do c) later on. Since the infinite loop (while ⊤ do c) clearly does not have the intended semantics, we cannot permanently overwrite e with e ′ .

One of the main difficulties of small-step semantics is that the syntax of the language is sometimes not expressive enough to represent all partial computations. In this overview, we use a common trick to sidestep the problem: notice that while e do c has the same behavior as if e then (c ; while e do c) else skip, so we simply add a contraction rule going from the first to the second. This bigger program carries two versions of e and c, and is able to express partial computations.

In the general case, we can systematically extend the language with new constructors in such problematic situations. For IMP, it would require two additional constructors While1 and While2, the first of which remembers both the initial boolean expression e and the computation in progress e ′ : see Figure 2.3. Similarly, if skip did not exist, we would need to create it to represent finished computations.

Note that there are no rules for the base elements. We do not want contraction rules of the form ⟨σ, b⟩ → b ⟨σ, b⟩, ⟨σ, n⟩ → a ⟨σ, n⟩, or ⟨σ, skip⟩ → ⟨σ, skip⟩, as they would yield infinite reduction sequences. A reduction sequence represents a computation, and we do not want a computation to stay locked onto a base element without progressing further.

Small-step semantics allows for a more precise control of the behavior of programs, and make

⟨s, c 0 ⟩ → ⟨s ′ , c ′ 0 ⟩ ⟨s, While2(c 0 , e, c)⟩ → ⟨s ′ , While2(c ′ 0 , e, c
c ::= skip | . . . | c 1 ∥ c 2
Then, we simply need to extend the semantics with two congruence rules and a contraction rule.

⟨σ, c 1 ⟩ → ⟨σ ′ , c ′ 1 ⟩ ⟨σ, c 1 ∥ c 2 ⟩ → ⟨σ ′ , c ′ 1 ∥ c 2 ⟩ ⟨σ, c 2 ⟩ → ⟨σ ′ , c ′ 2 ⟩ ⟨σ, c 1 ∥ c 2 ⟩ → ⟨σ ′ , c 1 ∥ c ′ 2 ⟩ ⟨σ, skip ∥ skip⟩ → ⟨σ
⟨σ 1 , x⟩ → ⟨σ 1 , 1⟩ ⟨σ 1 , Equal(x, 2)⟩ → ⟨σ 1 , Equal(1, 2)⟩ ⟨σ 1 , if Equal(x, 2) then (x := 3) else (x := 4)⟩ → ⟨σ 1 , if Equal(1, 2) then (x := 3) else (x := 4)⟩ 4. ⟨σ 1 , Equal(1, 2)⟩ → ⟨σ 1 , ⊥⟩ ⟨σ 1 , if Equal(1, 2) then (x := 3) else (x := 4)⟩ → ⟨σ 1 , if ⊥ then (x := 3) else (x := 4)⟩ 5. ⟨σ 1 , if ⊥ then (x := 3) else (x := 4)⟩ → ⟨σ 1 , x := 4⟩ 6. ⟨σ 1 , x := 4⟩ → ⟨{x → 4}, skip⟩
By joining the different small steps, we construct the following sequence of reductions.

⟨ϵ, x := 1 ; if Equal(x, 2) then (x := 3) else (x := 4)⟩ → * ⟨{x → 4}, skip⟩

In general, big-step and small-step semantics define the same behavior. A big-step evaluation should correspond to a sequence of small-step reductions. This is formalized in the following theorem.

Theorem 2.3.3. For all programs c and states σ and σ ′ ,

⟨σ, c⟩ ⇓ σ ′ ⇐⇒ ⟨σ, c⟩ → * ⟨σ ′ , skip⟩
Proof. The proof is quite standard, and can be done by induction on the big-step derivation and on the length of the small-step reduction sequence. See for instance [START_REF] Hanne | Semantics with Applications: An Appetizer[END_REF].

The additional control offered by the small-step semantics rules can be useful for certifying some properties. Notably, it is a common representation for proving type safety of a language, by checking the properties of progress and preservation. However, small-step semantics is more verbose and less intuitive than big-step, as manipulating a sequence of reduction is harder than a self-contained derivation tree. It also often requires an extended syntax, as would be the case for while loops without the above sidestep.

Reduction Semantics

Reduction semantics is a rephrasing of small-step semantics where we separate the basic reduction rules (i.e., contraction rules) from the location in the term where they can be applied. For IMP, we have the following contraction rules (⇝).

⟨σ, x⟩ ⇝ ⟨σ, σ(x)⟩ ⟨σ, Plus(n 1 , n 2 )⟩ ⇝ ⟨σ, n 1 + n 2 ⟩ ⟨σ, Equal(n 1 , n 2 )⟩ ⇝ ⟨σ, n 1 ? = n 2 ⟩ ⟨σ, And(b 1 , b 2 )⟩ ⇝ ⟨σ, b 1 ∧ b 2 ⟩ ⟨σ, Neg(b)⟩ ⇝ ⟨σ, ¬b⟩ ⟨σ, x := n⟩ ⇝ ⟨σ[x → n], skip⟩ ⟨σ, skip ; c 2 ⟩ ⇝ ⟨σ, c 2 ⟩ ⟨σ, if ⊤ then c 1 else c 2 ⟩ ⇝ ⟨σ, c 1 ⟩ ⟨σ, if ⊥ then c 1 else c 2 ⟩ ⇝ ⟨σ, c 2 ⟩
⟨σ, while e do c⟩ ⇝ ⟨σ, if e then (c ; while e do c) else skip⟩

As seen in Figure 2.2, a large proportion of small-step inference rules are simply congruence rules, with very similar structure. In reduction semantics, we replace congruence rules with evaluation contexts, specifying where subcomputations are allowed. In the case of IMP, we have the following contexts.

E ::= [•] | Plus(E, a) | Plus(n, E) | Equal(E, a) | Equal(n, E) | And(E, e) | And(b, E) | Neg(E) | if E then c 1 else c 2 | E ; c 2 | x := E
Contexts are terms with a single hole ([•]) that can be filled with another term. For instance,

if E ≜ Equal(n, Plus([•], a)), then E[a ′ ] ≜ Equal(n, Plus(a ′ , a)).
A more formal definition of reduction semantics would distinguish different kinds of contexts depending on the type (AExp/BExp/CExp) of the hole and the type of the produced term. In this overview, we simplify the notations by regrouping them in a single definition.

Then, we can apply the contraction rules (⇝) above at any depth in our language programs with the following congruence rule 1 , making use of evaluation contexts. For all terms t:

⟨σ, t⟩ ⇝ ⟨σ ′ , t ′ ⟩ ⟨σ, E[t]⟩ → RS ⟨σ ′ , E[t ′ ]⟩
Evaluation contexts are defined such that each case corresponds exactly to a congruence rule of the small-step semantics (Figure 2.2). This approach leads to objects (evaluation contexts)

representing where computations can happen, separated from the reduction rules of the different base cases.

The fifth step directly uses the contraction rule

⟨{x → 1}, if ⊥ then (x := 3) else (x := 4)⟩ ⇝ ⟨{x → 1}, x := 4⟩. 6. Finally, the last step directly uses the rule ⟨{x → 1}, x := 4⟩ ⇝ ⟨{x → 4}, skip⟩.

As expected, we can easily show that the small-step semantics and reduction semantics define the exact same behavior. Theorem 2.4.2. For all programs c and c ′ and states σ and σ ′ ,

⟨σ, c⟩ → ⟨σ ′ , c ′ ⟩ ⇐⇒ ⟨σ, c⟩ → RS ⟨σ ′ , c ′ ⟩ Proof.
By simply specifying the one-to-one correspondence between context constructors and congruence rules.

Reduction semantics offers several quality-of-life improvements over small-step semantics.

It separates the computational part, i.e., reducing a subterm, from the navigation necessary 1. If we take the more formal approach, we need one congruence rule per kind of context.

to reach the subterm. This eliminates the need for a tree structure, and produces a cleaner representation. Notably, reconstruction is simply defined as plugging the result into a context, instead of painfully traversing the derivation tree in reverse. Having access to the evaluation context as an independent object also allows for more refined manipulations. As such, reduction semantics proves to be a useful format for languages with control operators, such as the callcc operator in Scheme.

However, reduction semantics is still mostly a rephrasing of small-step semantics, and does not provide, in practice, a clear method for decomposing a term into an evaluation context and a reducible term.

Abstract Machine

The last format we present is abstract machines. It is similar to reduction semantics, in the sense that we have a reification of evaluation contexts, but it also goes further by providing an explicit strategy for manipulating them. An abstract machine is a reduction system between objects, called machine states, which represent the internal state of a virtual computer executing a program of the language. Machines states are usually configurations (e.g., ⟨σ, c⟩) extended with the additional information needed by the virtual computer (memory, evaluation modes, flags, . . . ).

In the case of IMP, the extra information we need are continuations (κ), objects representing what is left to compute after we finish executing the term under focus. For IMP, they are lists of elementary evaluation contexts and represent what is around the term we are evaluating.

κ ::= • | Plus([•], a) :: κ | Plus(n, [•]) :: κ | . . . | x := [•] :: κ
The ellipsis hides the same cases as in the definition of evaluation contexts in reduction semantics, and • is the empty continuation signaling there is no more computation to perform.

Machine states are then separated into two different evaluation modes, depending on the kind of computations we are performing2 . ⟨σ, c | κ⟩ e is a state in computation mode, and can be seen as a computer currently evaluating ⟨σ, c⟩ with pending computations κ. Meanwhile, ⟨κ | σ, c⟩ k is in continuation mode, and corresponds to applying the continuation κ to a result c (here, c is either an integer, a boolean, or skip). We swap the order of arguments to visually contrast the two evaluation modes.

We cannot directly use evaluation contexts as continuations, as the abstract machine would need to modify the inside of a context when changing focus (see below). Instead, continuations can be seen as evaluation contexts in reverse. For instance, the context Equal(Plus(n, The steps of the abstract machine for IMP are given in Figure 2.4. For the sake of readability, we reuse the notation (→), previously used for small-step semantics, for the reduction of abstract machines. There should be no confusion, as the objects being reduced are different.

For machine states in computation mode (e.g., ⟨σ, Plus(a 1 , a 2 ) | κ⟩ e ), we do not need to look up the current continuation. If we need to evaluate a subterm, we focus on it and push the rest of the term on the continuation (e.g., ⟨σ, a 1 | Plus([•], a 2 ) :: κ⟩ e ). If we are focused on a value (e.g., skip) we switch to continuation mode. In some cases (variable and while loop), we can also directly perform a contraction rule.

For machine states in continuation mode, we need to look at the beginning of the continuation, which corresponds to the area immediately around the result under focus. If we have a first elementary context, we apply the appropriate action, usually either focusing on another subterm or applying a contraction rule (e.g. ⟨Neg([•]) :: κ | σ, b⟩ k → ⟨κ | σ, ¬b⟩ k ). There is no rule for the empty continuation, as in that case, we are done computing and the abstract machine stops.

In practice, to evaluate a configuration ⟨σ, c⟩, we start the machine with the empty continuation ⟨σ, c | •⟩ e and repeatedly reduce. The machine stops when no rule is applicable, in a machine state of the form

⟨• | σ ′ , skip⟩ k . ⟨σ, n | κ⟩ e → ⟨κ | σ, n⟩ k ⟨σ, b | κ⟩ e → ⟨κ | σ, b⟩ k ⟨σ, skip | κ⟩ e → ⟨κ | σ, skip⟩ k ⟨σ, x | κ⟩ e → ⟨κ | σ, σ(x)⟩ k ⟨σ, Plus(a 1 , a 2 ) | κ⟩ e → ⟨σ, a 1 | Plus([•], a 2 ) :: κ⟩ e ⟨σ, Equal(a 1 , a 2 ) | κ⟩ e → ⟨σ, a 1 | Equal([•],
κ | σ, n 1 ⟩ k → ⟨σ, a 2 | Plus(n 1 , [•]) :: κ⟩ e ⟨Plus(n 1 , [•]) :: κ | σ, n 2 ⟩ k → ⟨κ | σ, n 1 + n 2 ⟩ k ⟨Equal([•], a 2 ) :: κ | σ, n 1 ⟩ k → ⟨σ, a 2 | Equal(n 1 , [•]) :: κ⟩ e ⟨Equal(n 1 , [•]) :: κ | σ, n 2 ⟩ k → ⟨κ | σ, n 1 ? = n 2 ⟩ k ⟨And([•], e 2 ) :: κ | σ, b 1 ⟩ k → ⟨σ, e 2 | And(b 1 , [•]) :: κ⟩ e ⟨And(b 1 , [•]) :: κ | σ, b 2 ⟩ k → ⟨κ | σ, b 1 ∧ b 2 ⟩ k ⟨Neg([•]) :: κ | σ, b⟩ k → ⟨κ | σ, ¬b⟩ k ⟨x := [•] :: κ | σ, n⟩ k → ⟨κ | σ[x → n], skip⟩ k ⟨[•] ; c 2 :: κ | σ, skip⟩ k → ⟨σ, c 2 | κ⟩ e ⟨if [•] then c 1 else c 2 :: κ | σ, ⊤⟩ k → ⟨σ, c 1 | κ⟩ e ⟨if [•] then c 1 else c 2 :: κ | σ, ⊥⟩ k → ⟨σ, c 2 | κ⟩ e
• | ϵ, 1⟩ k → ⟨[•] ; if Equal(x, 2) then (x := 3) else (x := 4) :: • | {x → 1}, skip⟩ k → ⟨{x → 1}, if Equal(x, 2) then (x := 3) else (x := 4) | •⟩ e → ⟨{x → 1}, Equal(x, 2) | if [•] then (x := 3) else (x := 4) :: •⟩ e → ⟨{x → 1}, x | Equal([•], 2) :: if [•] then (x := 3) else (x := 4) :: •⟩ e → ⟨Equal([•], 2) :: if [•] then (x := 3) else (x := 4) :: • | {x → 1}, 1⟩ k → ⟨{x → 1}, 2 | Equal(1, [•]) :: if [•] then (x := 3) else (x := 4) :: •⟩ e → ⟨Equal(1, [•]) :: if [•] then (x := 3) else (x := 4) :: • | {x → 1}, 2⟩ k → ⟨if [•] then (x := 3) else (x := 4) :: • | {x → 1}, ⊥⟩ k → ⟨{x → 1}, x := 4 | •⟩ e → ⟨{x → 1}, 4 | x := [•] :: •⟩ e → ⟨x := [•] :: • | {x → 1}, 4⟩ k → ⟨• | {x → 4}, skip⟩ k
This abstract machine produces the same behavior as the previous forms of operational semantics.

Theorem 2.5.2. For all programs c and states σ and σ ′ , Ciobâcă IFM13 [START_REF] Ciobâcă | From Small-Step Semantics to Big-Step Semantics, Automatically[END_REF] Poulsen Mosses ESOP14 [START_REF] Bach Poulsen | Deriving Pretty-Big-Step Semantics from Small-Step Semantics[END_REF] Ager LOPSTR04 [2] Functional Correspondence

⟨σ, c | •⟩ e → * ⟨• | σ ′ , skip⟩ k ⇐⇒ ⟨σ, c⟩ → * RS ⟨σ ′ ,
Danvy ICFP08 [START_REF] Danvy | Defunctionalized interpreters for programming languages[END_REF] Refocusing Danvy 04 [START_REF] Danvy | Refocusing in Reduction Semantics[END_REF], IFL10 [START_REF] Sieczkowski | Automating Derivations of Abstract Machines from Reduction Semantics: -A Generic Formalization of Refocusing in Coq[END_REF], FSCD17 [START_REF] Biernacka | Generalized Refocusing: From Hybrid Strategies to Abstract Machines[END_REF] Part I Part II The main advantage of abstract machines is that every decision, every change of focus, is made entirely explicit. After a computation step, we do not fully rebuilt a term, but navigate to the next subterm to be evaluated. This produces a semantics very close to an actual implementation, and it can easily be simulated and executed in practice. This fine-grain description of steps of executions is also sometimes used as reference for defining complexity classes. Notably, abstract machines can be used for modeling and measuring the complexity of algorithms [START_REF] Accattoli | The Complexity of Abstract Machines[END_REF].

Interderivation

Each of the operational semantics flavor presented above has its own strengths and drawbacks.

Depending on the use case, it is sometimes necessary to manipulate several versions of a seman-tics. To limit human errors and efforts when duplicating definitions, systematic interderivation strategies have been developed. Figure 2.5 displays some of them. However, the correctness of most strategies is not properly certified; they should be seen as time-tested guidelines on how to transform a semantics.

The most studied relation is the connection between big-step and abstract machines. The most widespread method is functional correspondence [START_REF] Mads | A functional correspondence between evaluators and abstract machines[END_REF], a systematic approach for converting big-step semantics, written as interpreters 3 , to abstract machines and back. It combines several known techniques, such as CPS translation [START_REF] Plotkin | Call-by-Name, Call-by-Value and the lambda-Calculus[END_REF] and defunctionalization [START_REF] Reynolds | Definitional interpreters for higher-order programming languages[END_REF], to progressively restructure the semantics of a language. It has been applied to languages with various features [START_REF] Biernacka | An Abstract Machine for Strong Call by Value[END_REF][START_REF] Mads | A functional correspondence between call-by-need evaluators and lazy abstract machines[END_REF][START_REF] Piróg | A systematic derivation of the STG machine verified in Coq[END_REF][START_REF] Biernacki | From Interpreter to Logic Engine by Defunctionalization[END_REF][START_REF] Biernacka | An Operational Foundation for Delimited Continuations in the CPS Hierarchy[END_REF][START_REF] Mads | A functional correspondence between monadic evaluators and abstract machines for languages with computational effects[END_REF][START_REF] Danvy | Inter-deriving semantic artifacts for object-oriented programming[END_REF][START_REF] Jedynak | An operational foundation for the tactic language of Coq[END_REF], and has recently been implemented into a tool for the automatic translations of Racket interpreters [START_REF] Buszka | Automating the Functional Correspondence between Higher-Order Evaluators and Abstract Machines[END_REF]. The reverse direction is also possible. An abstract machine can be transformed into a direct-style big-step semantics by following the sequence of reverse transformations [START_REF] Mads | A functional correspondence between evaluators and abstract machines[END_REF]. This requires the abstract machine to be fully defunctionalized (i.e., the continuations are only dispatch by a single mechanism), but a few ad-hoc techniques can often be used to tweak the abstract machine to meet this requirement [START_REF] Danvy | A Rational Deconstruction of Landin's SECD Machine with the J Operator[END_REF][START_REF] Biernacki | A Dynamic Continuation-Passing Style for Dynamic Delimited Continuations[END_REF]. We use functional correspondence to transform a semantics in Part II of this document, and we describe this strategy in more details in Chapter 9. Ager [START_REF] Mads | From Natural Semantics to Abstract Machines[END_REF] also proposes a translation from big-step to abstract machine by directly manipulating inference rules, creating continuations keeping track of the big-step premise being evaluated.

From small-step to big-step semantics, the main issue is that the small-step format is more expressive, and some behaviors cannot be defined in big-step, such as interleaving (Example 2.3.1).

The different known approaches are partial and assume some structural properties to be applied.

Notable transformations include the works of Ciobâcă [START_REF] Ciobâcă | From Small-Step Semantics to Big-Step Semantics, Automatically[END_REF] and Poulsen and Mosses [START_REF] Bach Poulsen | Deriving Pretty-Big-Step Semantics from Small-Step Semantics[END_REF], which manipulate small-step inference rules and assemble them. In the second paper, the output is in a format called pretty-big-step (PBS) semantics [START_REF] Charguéraud | Pretty-Big-Step Semantics[END_REF]. It is a subset of big-step where rules can only evaluate a single subterm, and then might need to perform a tail call for further computations.

Terms are still related to their final result, but tree derivations are more vertical and there is no need to duplicate rules when extending the language to account for exceptions and divergence.

Transformation from big-step to small-step semantics has been less investigated. Huizing et al. [START_REF] Huizing | A Small Step for Mankind[END_REF] describe a very generic certified translation manipulating big-step inference rules, but the output small-step semantics is unusual and not intuitive: configurations are extended with a stack, and we guess starting states each time we focus on a new big-step premise. More recently, Vesely and Fisher [START_REF] Vesely | One Step at a Time -A Functional Derivation of Small-Step Evaluators from Big-Step Counterparts[END_REF] present a new approach for translating big-step interpreters. They combine numerous small known transformations-including CPS and direct-style translations and defunctionalization-into a complete pipeline for deriving a small-step semantics. In Part I of this document, we present a novel systematic big-step to small-step translation, for languages written in a specific framework called skeletal semantics. We compare our translation to previous techniques in Chapter 7.

Small-step and reduction semantics are quite similar, since the difference is only the representation of evaluation contexts. These contexts can be made explicit by performing a CPS transformation and giving names to the continuations [START_REF] Danvy | Defunctionalized interpreters for programming languages[END_REF].

From there, the strategy for constructing a proper abstract machine is called refocusing [START_REF] Danvy | Refocusing in Reduction Semantics[END_REF].

In reduction semantics, after every step we plug the result in the evaluation context, forgetting about the location where the contraction rule was applied. Using this strategy for an abstract machine would be extremely inefficient. Refocusing corresponds to searching for a new evaluation context from the focus point of the last step. This removes the need to fully unzoom and rezoom at every application of a contraction rule. This refocusing method has been formalized, generalized, and certified [START_REF] Sieczkowski | Automating Derivations of Abstract Machines from Reduction Semantics: -A Generic Formalization of Refocusing in Coq[END_REF][START_REF] Biernacka | Generalized Refocusing: From Hybrid Strategies to Abstract Machines[END_REF] in the Coq proof assistant [START_REF] Morazán | The Coq Proof Assistant[END_REF]. The authors proved, independently from the target language, that the resulting abstract machine perfectly simulates the initial reduction semantics. To the best of our knowledge, this interderivation is the only related work of Figure 2.5 to be fully certified.

Part I

Object Language Transformation

Chapter 3

SKELETAL SEMANTICS

Skeletal semantics is a framework to formalize the operational semantics of programming languages. Users can describe the semantics they are interested in using using a small meta-language named Skel. The fundamental idea of the approach is to only specify the structure of evaluation functions (e.g., sequences of operations, non-deterministic choices, recursive calls) while keeping abstract basic operations (e.g., updating an environment or comparing two values). The motivation for this semantics is that the structure can be analyzed, transformed, or certified independently from the implementation choices of the basic operations.

We present the formal definition of skeletal semantics we use to describe the transformation in Section 4.2 and state the equivalence results in Chapter 5. The framework has evolved since its first definition [START_REF] Bodin | Skeletal semantics and their interpretations[END_REF], and Part I uses an updated description [START_REF] Courant | Necro: Animating Skeletons[END_REF]. The version presented in this part also slightly differs from the high-order skeletal semantics of Part II that allows function types and anonymous functions.

Example

A skeletal semantics is composed of types, filters, hooks, and rules. Types represent categories for the elements of the language. We distinguish between base and program types. Base types are left unspecified and correspond to the base elements of the language, like environments or identifiers for variables. Program types are defined with a list of constructors, each having a precise typing.

Example 3.1.1 (Types). We write examples using the legacy Necro syntax [START_REF] Courant | Necro: Animating Skeletons[END_REF], and use as a running example an imperative language called IMP. It is slightly different from the language presented in Chapter 2, as we merge arithmetic and boolean expressions into a single category. A distinctive feature of skeletal semantics is that we do not need to further specify the implementation of base types. The only way we can manipulate them is through typed unspecified functions called filters, which represent the basic operations of the language. We can reason on the semantics as a whole by assuming some properties on these filters. For instance, the final representation of state does not matter as long as we define read and write operations as filters.

Example 3.1.2 (Filters). Filters are declared with the keyword val. They are explicitly typed, using the keyword unit in case of a missing input or output. We consider the following filters for IMP. The rules for If and While illustrate branchings. In both cases, we evaluate the first term to get a value v. In most programming languages, we would then branch depending on v. We encode this behavior with the non-deterministic branchings of skeletal semantics by starting each branch with a filter either isTrue or isFalse, which causes one of the branches to fail.

= | Iconst (i) -> let v = intToVal (i) in (s, v) | Bconst (b) -> let v = boolToVal (b) in (s, v) | Var (x) -> let v = read (x, s) in (s, v) | Plus (e1, e2) -> let (s1, v1) = hexpr (s, e1) in let (s2, v2) = hexpr (s1, e2) in let v = add (v1, v2) in (s2, v) | Equal (e1, e2) -> let (s1, v1) = hexpr (s, e1) in let (s2, v2) = hexpr (s1, e2) in let v = eq (v1, v2) in (s2, v) | Not (e1) -> let (s1, v) = hexpr (s, e1) in let v' = neg (v) in (s1, v
Evaluating expressions with hexpr returns a state although it is never modified. This choice prepares for extensions of the language, such as function calls, where the state could be mutated. This version of skeletal semantics also supports basic parametric polymorphism. We do not present this feature, as it is not supported by neither the Coq extraction (Section 3.5) nor the small-step transformation (Chapter 4).

Syntax of Skel

Formally, we write a for a (possibly empty) tuple (a 1 , . . . , a n ), and ∥ a∥ for its size, here n.

Unless explicitly stated, tuples are not expected to have the same arity. We write a, b and a, b for the extension of a tuple on the right, and we have ∥ a, b∥ = ∥ a∥ + ∥ b∥. We write a i ∈ a to state that a i is an element of the tuple a. Given a function or relation f , we write f (a) for (f (a 1 ), . . . , f (a n )) assuming ∥ a∥ = n. Similarly, assuming ∥ a∥ = ∥ b∥ = n, we write g(a, b) for

(g(a 1 , b 1 ), . . . , g(a n , b n )).
We let c, f , and h range over respectively constructor, filter, and hook names. Assuming a countable set V of variables ranged over by v (and also w, x, y, and z), the grammar of terms (t), skeletons (S), and skelements (K) is defined as follows.

t ::= v | c( t) S ::= let v = K in S | K K ::= Filter f ( t) | Hook h ( t, t) | Return ( t) | Branching ( S)
The skeletal semantics of a language consists in:

• a set (T = T b ∪ T p ) of types ranged over by s, combining base types T b , and program types T p ranged over by s p ;

• a set C of constructors, with a typing function ctype : C → T × T p ;

• a set F of filters, with a typing function ftype : F → T × T ;

• a set H of hooks, with a typing function htype : H → ( T × T p ) × T ;

• a set R of rules of the form h( y, c( x)) := S, assuming we have htype(h) = (( s, s p ), s ′ ), ctype(c) = ( s ′′ , s p ), ∥ y∥ = ∥ s∥, and ∥ x∥ = ∥ s ′′ ∥.

The typing of constructors restricts their output to a term of program type, while filters may produce terms of any type. The input of a hook T × T p is composed of auxiliary terms of type T and of the term being evaluated of program type T p . We define three projections htype in , htype p , and htype out so that if htype(h) = (( s, s p ), s ′ ), then htype in (h) = s, htype p (h) = s p , and htype out (h) = s ′ . The Skel meta-language is strongly statically typed. As such, the variables v in terms and skeletons are associated with a base or program type. As hinted at in Section 3.1, the typing system is polymorphic but we ignore the details for the sake of simplicity.

A rule h( y, c( x)) := S defines the behavior of h on the constructor c by the skeleton S, which describes the sequence of reductions to perform using the input variables x and y. We assume the variables x, y to be pairwise distinct, and to contain the free variables of S. We suppose that at most one rule handling c for h exists in R. The matching does not have to be exhaustive: a hook h without a rule for c simply cannot reduce terms with c as head constructor.

Concrete Interpretation

The dynamic of a skeletal semantics is given by the concrete interpretation of the rules defining its hooks [START_REF] Bodin | Skeletal semantics and their interpretations[END_REF], which corresponds to a big-step semantics. This interpretation computes the result of applying a hook to a term by inductively interpreting the skeleton of the rule of the corresponding constructor and hook, under some environment mapping skeleton variables to values.

The interpretation supposes an instantiation of the base types and filters. For every base type • intToVal and boolToVal inject their arguments in (Z ∪ B);

• read(x,s) returns the result of applying s to x;

• write(x,s,v) returns the partial function mapping x to v and every y ̸ = x to s(y);

• eq(x,y) returns ⊤ if both values are equal, ⊥ otherwise;

• add(x,y) is only defined on integers so that R add (i 1 , i 2 ) ⇓ (i 1 + i 2 ) for any i 1 and i 2 ;

• neg is only defined on booleans so that R neg (⊤) ⇓ (⊥) and R neg (⊥) ⇓ (⊤);

• isTrue only accepts ⊤ so that R isTrue (⊤) ⇓ ();

• isFalse only accepts ⊥ so that R isFalse (⊥) ⇓ ().

The relations above make the distinction between integers and booleans. We can define an interpretation where conditional branching on an integer is allowed, by extending the interpretation of the filters isTrue and isFalse as follows:

R isFalse (0) ⇓ (); ∀i ̸ = 0, R isTrue (i) ⇓ ()
The strength of skeletal semantics is that this choice is local to the interpretation of filters: we do not have to change anything in the definitions or interpretations of the hooks. We write Σ for an environment mapping a finite set of variables in V (its domain) to values in s∈T C(s). We write Σ(v) to access the mapping associated to v in Σ, and we extend the notation to terms Σ(t) as follows: Σ(c( t)) = c( Σ(t)). The environment mapping a single variable v to a value b is written {v → b}, and we write Σ + Σ ′ for the update of Σ with Σ ′ , so that

Σ( t) ⇓ h b Σ ⊢ Hook h t ⇓ b R f ( Σ(t)) ⇓ b Σ ⊢ Filter f t ⇓ b Σ(t) = b Σ ⊢ Return t ⇓ b S i ∈ S Σ ⊢ S i ⇓ b Σ ⊢ Branching S ⇓ b Σ ⊢ K ⇓ a Σ + {v → a} ⊢ S ⇓ b Σ ⊢ let v = K in S ⇓ b h( y, c( x)) := S ∈ R {y → a} + {x → a ′ } ⊢ S ⇓ b ( a, c( a ′ )) ⇓ h b
(Σ + Σ ′ )(v) = Σ ′ (v) if v is in the domain of Σ ′ , and (Σ + Σ ′ )(v) = Σ(v) otherwise.
The interpretation Σ ⊢ S ⇓ b, defined in Figure 3.2, is a relation stating that S outputs the values b under the environment Σ; it assumes that the free variables of S are in the domain of Σ. A LetIn structure is evaluated sequentially, starting with K under Σ and then continuing with S under the environment updated with the outputs of K. The environment Σ is used when interpreting skelements to turn the input terms t into values. These values are simply returned in the case of a Return skelement. A filter call looks for a possible result related to these values in R f . A branching returns the result of one of its branches; it does not matter if some branches are stuck or non-terminating as long as one branch succeeds. To evaluate a hook call, we first compute the arguments of the hook, and find the rule corresponding to its constructor. We then interpret the skeleton of the rule under a new environment mapping the free variables of the skeleton to the appropriate values taken from Σ. Note that our approach differs from the one of [START_REF] Bodin | Skeletal semantics and their interpretations[END_REF]: they take the smallest fixpoint of a functional describing one step of the relation, whereas we directly define the semantics as an inductive definition. This interpretation is inherently big-step, as a judgment Σ ⊢ S ⇓ b computes the final value returned by S. It is also non-deterministic, as apparent in the rules for branching and filter call, since R f may relate several results to the same input.

Example 3.3.2. We interpret the following statement st with the hook hstmt:

st := (If Equal(Var("length"),Plus(Var("width"),2))

Assign("width",Iconst(4))

Assign("flag",Bconst(⊥)) )

The program is a simple conditional: if length ? = width + 2, then width := 4, otherwise flag := ⊥. We evaluate this statement in the state a 0 = {length → 4 ; width → 2}, so we expect the result to be a 1 = {length → 4 ; width → 4}. We remind that the rule is hstmt(s, If(e1,t2,t3)) := S If , where S If is the following skeleton (cf. Figure 3.1):

let (s1, v) = hexpr (s, e1) in branch let () = isTrue (v) in (* | S1 *) hstmt (s1, t2) (* | *) or let () = isFalse (v) in (* || S2 *) hstmt (s1, t3) (* || *) end
We interpret this skeleton in the initial environment: Σ 0 = {s → a 0 ; e1 → Equal(Var("length"),Plus(Var("width"),2)); t2 → Assign("width",Iconst(4)) ; t3 → Assign("flag",Bconst(⊥))}

We recursively evaluate the first hook call on the values Σ 0 (s, e1) by finding the corresponding rule hexpr(s, Equal(e1,e2)) := S Equal in R. We interpret S Equal under the new environment Σ 2 = {s → a 0 ; e1 → Var("length") ; e2 → Plus(Var("width"),2)}, which results in (a 0 , ⊤).

We then evaluate the branching in the extended environment Σ 1 = Σ 0 + {s1 → a 0 ; v → ⊤}.

Only the branch guarded by isTrue can succeed; given our definition of R isTrue , we have R isTrue (Σ 1 (v)) ⇓ (), meaning that we pass this filter call.

Finally, we compute the recursive call on the values (a 0 , Assign("width",Iconst(4))). Once again we look for the corresponding rule in R and create a new environment for this evaluation.

Σ( t) ⇑ h Σ ⊢ Hook h t ⇑ = = = = = = = = = = = = = = h( y, c( x)) := S ∈ R {y → a} + {x → a ′ } ⊢ S ⇑ ( a, c( a ′ )) ⇑ h = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = S i ∈ S Σ ⊢ S i ⇑ Σ ⊢ Branching S ⇑ = = = = = = = = = = = = = = = = = = Σ ⊢ K ⇑ Σ ⊢ let v = K in S ⇑ = = = = = = = = = = = = = = = = = = = = = = Σ ⊢ K ⇓ a Σ + {v → a} ⊢ S ⇑ Σ ⊢ let v = K
⊢ S If ⇓ a 1 . . . . Σ 2 ⊢ S Equal ⇓ (a 0 , ⊤) Σ 0 ⊢ hexpr (s,e1) ⇓ (a 0 , ⊤) R isTrue (⊤) ⇓ () Σ 1 ⊢ isTrue (v) ⇓ () . . . Σ 3 ⊢ S Assign ⇓ a 1 Σ 1 ⊢ hstmt (s1,t2) ⇓ a 1 Σ 1 ⊢ S 1 ⇓ a 1 Σ 1 ⊢ Branching(S 1 , S 2 ) ⇓ a 1 Σ 0 ⊢ S If ⇓ a 1

Coinductive Interpretation

The concrete interpretation defined in Section 3.3 is a standard big-step semantics, and cannot be used to express infinite computations. As an additional tool, we define a coinductive semantics for the meta-language Skel. The rules are given in Figure 3.3, where the double lines mean the rules must be interpreted coinductively.

There is no rule for returns and filters, as they cannot diverge. A hook call diverges if the evaluation of the corresponding skeleton diverges. A branching diverges if one of the branches diverges. For LetIns, we have two different rules, as there is two opportunities for infinite behaviors. A sequence diverges if either the first skelement diverges, or the skelement succeeds (using the concrete interpretation) but the rest of the computation then diverges. This coinductive interpretation is used in Chapter 5 to prove that the transformation of Chapter 4 preserves infinite behaviors.

Necro

Necro [START_REF] Courant | Necro: Animating Skeletons[END_REF] is an OCaml implementation for the manipulation of skeletal semantics. Users can easily define programming languages via the readable syntax we use for our examples (e.g., The main feature of Necro is its ability to create an OCaml interpreter from a skeletal semantics. The interpreter is parameterized by the types and functions representing the base types and filters. Once these have been instantiated, the interpreter provides a module containing an evaluation function for each hook of the skeletal semantics. The evaluation follows the approach of the concrete interpretation presented in Section 3.3, recursively calling the evaluation function each time a hook is encountered. We extend the tool to also generate a small-step interpreter (see Section 6.1).

More importantly, Necro is able to export a skeletal semantics into a formal Coq definition of the language. This tool outputs a deep embedding-i.e., a data structure containing the user definitions but without any semantic meaning-parameteric in the base types and filters. The concrete interpretation of Section 3.3 is independently formalized in Coq, and can be applied to give a semantics to user languages. We extensively use this tool to automatically provide equivalence certificates for the results of our transformation (see Section 5.2).

Chapter 4

SMALL-STEP TRANSFORMATION

Given a big-step skeletal semantics, we transform it to produce a skeletal semantics whose concrete interpretation behaves like a small-step interpretation of the initial semantics. We first present the main steps of the transformation on the IMP language (defined in Section 3.1), before defining the transformation formally in Section 4.2.

Overview on an Example

Coercions

The first phase of our transformation is to coerce return values into terms. Since we want smallstep reductions to transform a term into another term of the same program type, it means that values returned by hooks need to be considered as terms of the corresponding input type.

In our example, we need to add constructors corresponding to the return types of the two hooks, one of type (state, value) → expr for hexpr, and one of type state → stmt for hstmt.

The program types become:

type expr = | Iconst of int | ... | Ret_hexpr of state * value type stmt = | Skip | ... | Ret_hstmt of state
In the final small-step semantics, we need to be able to extract these coerced values. To this end, we define hooks to unpack the values for each constructor we introduce. In our example, we get the two following functions: hook getRet_hexpr (e : expr) matching e : state * value = | Ret_hexpr (v1, v2) -> (v1, v2) hook getRet_hstmt (t : stmt) matching t : state = | Ret_hstmt v1 -> v1

These hooks are only defined for the corresponding newly created constructors, as trying to unpack the value of a term not fully reduced should fail.

The transitory semantics at this point of the transformation is available in Appendix B.2.

New Constructors

The second phase is to determine which new constructors are required in order to produce a small-step semantics. Most reduction rules use the state and the arguments of the constructor only once. For instance, the evaluation of the term Plus(e 1 , e 2 ) consists of first evaluating e 1 , then evaluating e 2 , then combining the results. If we make progress on one subterm, let us say

e 1 → e ′
1 , then we reconstruct the term as Plus(e ′ 1 , e 2 ). We can discard the initial value of e 1 because the variables standing for e 1 and e 2 appear only once in the skeleton for Plus(e 1 , e 2 ). This allows us to reuse the constructor to rebuild a term after a step of computation.

In some cases, however, we cannot reconstruct using the same constructor after a step. The different problematic situations are detailed in Section 4.2.2; here we only describe the main problem, namely that we cannot remember two versions of the same term.

Unlike Plus, some constructors make use of their arguments several times in their reduction rules, such as While. The reduction of While(e 1 , t 2 ) might evaluate both e 1 and t 2 before cycling back to the original term While(e 1 , t 2 ). In a small-step setting, to reduce e 1 , we need to remember both a working copy e ′ 1 of the expression and its initial value to cycle back. We cannot store both e 1 and e ′ 1 in the While constructor, so we create a new one While1 to do so. In practice, the second phase of our transformation analyzes each hook call to determine in which of the following categories if falls in.

• It is a tail-call, i.e., a final hook call forwarding its return values. Then there is nothing to do.

• The terms being evaluated are only used once. In this case, we can reuse the same constructor to reconstruct a term.

• Some of the evaluated terms are used elsewhere. The naive reconstruction does not work, and we need to create an additional constructor.

In the third case, the additional constructor we create mirrors the situation at the program point and carries two copies of the terms evaluated several times. We also extend the corresponding hook with a new reduction rule for this new constructor, which is roughly the remainder of the initial skeleton rooted at the analyzed hook call.

We illustrate our analysis on several IMP constructors. For Plus, we can reuse the constructor after each hook call:

| Plus (e1, e2) -> let (s1, v1) = hexpr (s, e1) in (* reuse *) let (s2, v2) = hexpr (s1, e2) in (* reuse *) let v = add (v1, v2) in (s2, v)
In both cases, the arguments of the calls-respectively s, e1 and s1, e2-are not needed in the rest of the skeleton, so we can reconstruct at each program point reusing Plus.

For Seq, we have:

| Seq (t1, t2) -> let s1 = hstmt (s, t1) in (* reuse *) hstmt (s1, t2) (* tail-call *)
As before, we can reuse Seq after the first hook call as s and t1 are utilized only once. The second hook call is simply a tail-call, so there is no need to worry about reconstruction.

The analysis gets more interesting for While:

| While (e1, t2) -> let (s1, v) = hexpr (s, e1) in (* new constr: While1 *) branch let () = isTrue (v) in let s2 = hstmt (s1, t2) in (* new constr: While2 *) hstmt (s2, While (e1, t2)) (* tail-call *) or let () = isFalse (v) in s1 end
The third hook call is a tail-call, as it is the final instruction of one of the reduction paths.

When analyzing the first one, we see that e1 is needed later, thus we cannot reuse While here.

Similarly, we cannot reuse the constructor for the second hook call since t2 is needed in the tail-call.

For each of these two calls, we need to create a new constructor corresponding to their respective program point. The new constructors are built with two different kinds of arguments. Firstly, we create an argument for every term being evaluated at the analyzed hook call, namely s, e1 for the first one, and s1, t2 for the other. Secondly, we create arguments for the variables needed in the rest of the skeleton; in our example, it means keeping e1 and t2 in both cases. However, we do not need to duplicate s1 nor add an argument for v as they are no longer necessary.

As a result, while most variables appear only once in the arguments of a new constructor, the contentious ones-used in and after the corresponding hook call-are duplicated. In the end, we extend the definition of IMP with the following constructors:

type stmt = | ... | While of expr * stmt | While1 of state * expr * expr * stmt | While2 of state * stmt * expr * stmt | Ret_hstmt of state
We also add a new rule for each constructor introduced. The new skeleton consists in resuming the computation from the corresponding analyzed hook call, updating the input of the call to make use of the new arguments of the constructor. The resulting rule for While1 is almost the same as the one for While, while the skeleton of While2 covers only its last two skelements. We do not modify the rules for While or the other constructors at this stage. hook hstmt (s : state, t : stmt) matching t : state

= | ... | While1 (s0, e0, e1, t2) -> let (s1, v) = hexpr (s0, e0) in branch let () = isTrue (v) in let s2 = hstmt (s1, t2) in hstmt (s2, While (e1, t2)) or let () = isFalse (v) in s1 end | While2 (s0, t0, e1, t2) -> let s2 = hstmt (s0, t0) in hstmt (s2, While (e1, t2))
The added rules become useful when we change the rules to introduce calls to While1 and While2, as part of the last step of the transformation. Note that these added rules already include the information from the analysis, see Section 4.2.2 for details.

The transitory semantics at this point of the transformation is provided in Appendix B.3.

Make the Skeletons Small-Step

Previous phases set the stage for the main transformation, but our semantics is still big-step at this point, since the hooks fully compute their arguments. The last phase of the transformation makes the hooks behave in a small-step way.

Firstly, we need to change the output types of every hook to make them match the input ones. The headers of the hooks become: hook hexpr (s : state, e : expr) matching e : state * expr = ... hook hstmt (s : state, t : stmt) matching t : state * stmt = ... Doing so makes the types coherent with a small-step reduction process meant to be iterated.

More importantly, we need to update the skeletons themselves. We recall that skeletons are sequences of operations (skelements), which are mostly composed of filter calls and hook calls.

For our transformation, we consider that only hook calls correspond to reduction steps. The reason is that filters represent simple atomic operations that are not meant to be interrupted, while hook calls often correspond to the evaluation of a subterm. Thus, this last phase essentially focuses on transforming hook calls. We also need to take care of the result returned at the end of a skeleton, so that its type matches the updated output type of its hook.

We distinguish four cases, that we illustrate with rules from IMP. In each example, for readability, we provide small-step inference rules equivalent to the concrete interpretation of the output skeletal semantics rules.

1 If the last skelement of a skeleton is not a tail-call, then we need to wrap the results differently to match the new typing of the hook. The final result needs to be coerced to the input program type using one of the new constructors defined in the first phase (Section 4.1.1). For type checking, we also have to return the other arguments of the hook, even if they are not of any use.

For instance, the output of the rule for the Iconst constructor in the initial big-step skeleton is (s, v) (cf. Figure 3.1). Using a coercion, we turn this pair into an expression Ret_hexpr(s, v);

if we could we would return this term only, but the output type of the hexpr hook is (state * expr), so we also return a useless copy of s. The rule for Iconst is thus as follows.

| Iconst i -> let v = intToVal (i) in (s, Ret_hexpr (s, v)) s, i → s, (s, intToVal(i)) 2
The most interesting case is when we reach a hook call where we know we can reuse the constructor. In this situation, we are at a program point corresponding to the evaluation of a subterm, and we have two possibilities: either the subterm needs to be evaluated further, in which case we need to take a reduction step and reconstruct, or the subterm has been fully evaluated, and we need to extract its value and continue the reduction according to the rest of the skeleton. We distinguish the two behaviors in skeletal semantics using branches. For instance, transforming the first hook call of the Plus constructor produces a rule structured as follows:

| Plus (e1, e2) -> branch let (w1, w2) = hexpr (s, e1) in (w1, Plus (w2, e2)) or let (s1, v1) = getRet_hexpr (e1) in ... end s, e 1 → w 1 , w 2 s, e 1 + e 2 → w 1 , w 2 + e 2 e 1 = (s 1 , v 1 ) . . . s, e 1 + e 2 → . . .
where the dots correspond to the transformation of the second hook call. In the first branch, we reconstruct as (w1, Plus (w2, e2)), overwriting the variables s and e1 with the new terms resulting from the reduction step. In the second branch, we extract the coerced value using the hook defined alongside the constructor in Section 4.1.1. Even though we use a branching, the reduction is deterministic, as the definition of getRet_hexpr is restricted to coerced values, while hexpr operates only on terms that are not coerced values.

3 If we reach a hook call where we are not able to reuse the constructor, i.e., one of the calls for which we created a new constructor during the analysis, then the small-step function has to change the constructor after a reduction step. To simplify the semantics, we can equivalently decide to duplicate the necessary terms and change the constructor before reducing. To simplify even further, we consider the change of constructor to be a reduction step by itself; the next small step can then reduce the hook call. For instance, the reduction rule for the While constructor becomes:

| While (e1, t2) -> (s, While1 (s, e1, e1, t2)) s, While(e 1 , t 2 ) → s, While1(s, e 1 , e 1 , t 2 )
We simply duplicate s and e1 using the new constructor While1. We immediately return this new configuration. Calling the hook hstmt after that would then executes the skeleton for While1

where the next reduction step and reconstruction actually take place. Similarly, we call the new constructor While2 in the rule created for While1:

| While1 (s0, e0, e1, t2) -> branch (* | *) let (w1, w2) = hexpr (s0, e0) in (* | first hook call *) (s, While1 (w1, w2, e1, t2)) (* | transformed as *) or (* | previously *) let (s1, v) = getRet_hexpr (e0) in (* | *) branch (* # initial structure *) let () = isTrue (v) in (* # *) (s, While2 (s1, t2, e1, t2)) (* || second hook call *) or (* # *) let () = isFalse (v) in (* # *) (s, Ret_hstmt s1) (* ## coerced return *) end (* # *) end (* | *) s 0 , e 0 → w 1 , w 2 s, While1(s 0 , e 0 , e 1 , t 2 ) → s, While1(w 1 , w 2 , e 1 , t 2 ) e 0 = (s 1 , v) isTrue(v) s, While1(s 0 , e 0 , e 1 , t 2 ) → s, While2(s 1 , t 2 , e 1 , t 2 ) e 0 = (s 1 , v) isFalse(v) s, While1(s 0 , e 0 , e 1 , t 2 ) → s, s 1
This shows that the final transformation phase operates not only on the rules of the initial semantics, but also on the ones created during the analysis.

behaviors closer to usual pen-and-paper definitions. We can see this with the Seq constructor:

the first hook call is transformed as previously, but the second is turned into a return.

| Seq (t1, t2) -> branch (* | *) let (w1, w2) = hstmt (s, t1) in (* | first hook call *) (w1, Seq (w2, t2)) (* | transformed as previously *) or (* | *) let s1 = getRet_hstmt (t1) in (* | *) (s1, t2) (* || 2nd call becomes return *) end (* | *) s, t 1 → w 1 , w 2 s, t 1 ; t 2 → w 1 , w 2 ; t 2 t 1 = s ′ s, t 1 ; t 2 → s ′ , t 2
This final phase produces a small-step skeletal semantics where each hook call reduces its arguments only once. It is equivalent to the initial big-step one, in the sense that evaluating a term with either semantics produces the same value. The result of the complete transformation on IMP, as well as the different intermediate semantics, can be found in Appendix B. We state the equivalence between the two semantics in Section 5.2.

Formal Transformation Phases

We define formally the different phases of our transformation using the notations introduced in Section 3.2. We start with a given skeletal semantics (T b , T p , C 0 , F, H 0 , R BS , ctype, ftype, htype).

The elements describing types and filters (i.e., T b , T p , F , and ftype) are not modified by the transformation, and are implicitly carried over to all intermediate semantics. The sets of constructors (C 0 ) and hooks (H 0 ), as well as their typing functions ctype and htype, are expanded during the transformation. To simplify the notations, we do not change the names of the typing functions throughout the transformation. The main focus of the transformation is the set of rules defining the semantics of the user language. We start with a set R BS , and each phase of the translation expands or modifies it. Intermediate semantics are thus described by their sets of constructors and hooks, but most importantly by their set of rules.

Coercions

The first step is to add coercions for return values like in Section 4.1.1. For every hook we add a new constructor to pack its result as well as the corresponding hook to unpack it.

C 1 = C 0 ∪ {Ret_h | h ∈ H 0 } with ctype(Ret_h) = (htype out (h), htype p (h)) H 1 = H 0 ∪ {getRet_h | h ∈ H 0 } with htype(getRet_h) = (((), htype p (h)), htype out (h))
We remind that htype in , htype p , and htype out are the projections of the typing function htype : H 0 → ( T × T p ) × T , representing respectively the types of input states, the program type being reduced, and the output type of the hook. Each coercion turns the return values htype out (h) of the corresponding hook into an executable program of type htype p (h).

Each extracting hook takes this program type as its single input, as it does not depend on any environment type to reduce. It is defined only for the constructor it destructs. We know how many variables it returns by looking at the output type of the hook.

R getRet = {getRet_h(Ret_h v) := Return v | h ∈ H 0 ∧ ∥htype out (h)∥ = ∥ v∥}
Note that the rules R getRet do not need to be transformed further. The following phases of the transformation focus on changing the initial big-step rules. We finish this phase with the constructors C 1 , hooks H 1 , and rules R BS ∪ R getRet .

New Constructors

As presented in the overview, the second phase consists in an analysis of the hook calls to split them into three different categories. We exploit the results of the analysis during the final stage of the transformation. For the formal presentation, we introduce an extended skeletal semantics as an intermediate representation to carry over the information we need-the implementation uses ad-hoc data structures instead. We annotate hook calls with either Tail for a tail-call,

Reuse for calls from which we can reconstruct while reusing the same constructor, or New c when we create a new constructor c.

a ::= New c | Tail | Reuse S ::= let v = K in S | K K ::= Filter f t | Hook a h ( t, t) | Return t | Branching S
We proceed in two steps: we first analyze the rules to annotate the hook calls, then we create the needed new constructors and their rules.

Analysis

A simple way to make the semantics small-step would be to introduce a new constructor for each hook call. While it is safe to do so, the resulting semantics would be unnecessarily bloated, as we can reuse constructors and reconstruct in many cases. Our goal is to reuse them as much as possible to obtain a semantics close to the usual small-step semantics. It turns out that constructor reuse is not possible in the following cases:

• after a filter call;

• in the continuation of a branching;

• if an argument of the hook call is not a variable, or if it is used several times in the skeleton.

Firstly, even if we do not consider computing a filter as a step, we do not want to recompute the same filter several times. A reuse implies that the whole skeleton is evaluated up to the hook call at each reduction step, meaning that a filter placed before the hook call would be called at every reduction step. This could have unintended consequences if the filters are defined with side effects. We therefore give up on reuse if the analyzed hook call is after a filter call.

Secondly, we need to take into account the non-determinism induced by a branching. In a

skeleton like let v = Branching(Hook h 1 t 1 , Hook h 2 t 2 ) in Hook h t, two different reduc-
tion paths lead to the hook call after the branching. The premise of constructor reuse is that reevaluating the skeleton from the start should lead to the same evaluation context. However, reevaluating the skeleton in such a situation may take a different path and reach the last hook call with different values bound to the variables in v. As such, we give up on reuse if the analyzed hook call is in the continuation of a branching.

Lastly, as illustrated in the overview, reusing constructors means that we should be able to store the partially reduced terms in the constructor being evaluated. It is not possible if some of the arguments of the constructor are not variables, or if these variables are reused in the skeleton.

Formally, the annotation process of a given skeleton S is noted [S] hr,V L,b , where:

L is a boolean indicating if we are at the toplevel of the LetIn structure of the main skeleton, used to detect tail-calls;

b is a boolean indicating if we are at a position allowing for reuse, i.e., indicating whether we are after a filter call or in the continuation of a branching;

V is the list of the variables that are only used once throughout the whole initial skeleton;

hr is the name of the hook corresponding to the rule being analyzed, also used to detect tail-calls, as detailed below.

The analysis is defined in The analysis goes through the skeleton, leaving filters calls and returned values unchanged.

As expected, the boolean b is set to ⊥ after going through a filter call or a branching. Similarly, L is switched to ⊥ in the first part of a LetIn structure.

A final hook call is considered a tail-call if and only if it is situated at the toplevel of the main skeleton (L = ⊤) and if the hook being called is the one being analyzed (h = hr).

The [Branching(S 1 , . . . , S n )] hr,V L,b ≜ Branching([S 1 ] hr,V L,b , . . . , [S n ] hr,V L,b ) [Filter f t] hr,V L,b ≜ Filter f t [Return t] hr,V L,b ≜ Return t [Hook h t] hr,V L,b ≜ Hook Tail h t if L = ⊤, h = hr [Hook h w] hr,V L,b ≜ Hook Reuse h w if b = ⊤, w ∈ V [Hook h t] hr,V L,b ≜ Hook (New c) h t otherwise, c fresh [let v = K in S] hr,V L,b ≜ let v = [K] hr,V ⊥,b in [S] hr,V L,⊥ if K ̸ = Hook h t [let v = Hook h w in S] hr,V L,b ≜ let v = Hook Reuse h w in [S] hr,V L,b if b = ⊤, w ∈ V [let v = Hook h t in S] hr,V L,b ≜ let v = Hook (New c) h t in [S] hr,V L,b
otherwise, c fresh The call to h 2 has to be modified to make the types match, hence it cannot be a tail-call.

A hook call can only reuse its constructor if it is not after a filter call or a branching (b = ⊤)

and every term is a variable not used elsewhere ( w ∈ V ). In the case a hook call can be annotated with either Tail or Reuse, we choose to give precedence to the former, because tail-calls are more specific and lead to simpler skeletal semantics at the end of the transformation. Hook calls that cannot be annotated Tail or Reuse are instead associated with a fresh constructor name created on the fly.

We apply the analysis to every skeleton in the semantics, updating the set of rules as follows.

R lbl = {h( y, c( x)) := [S] h,SglUse(S) ⊤,⊤ | (h( y, c( x)) := S) ∈ R BS }
We now have the extended set of rules R lbl ∪ R getRet , on constructors C 1 and hooks H 1 .

Generation

After the analysis, we process every hook call annotated with a fresh constructor name c to compute its type and generate a fresh rule for c. When traversing a skeleton to find such a call, we construct its continuation-the rest of the computation, represented as a context-as it is used to create the fresh skeleton of the new rule. To do so, we define a monadic bind on skeletons, noted < S 1 | x | S 2 >, which executes S 1 , binds the results to x then executes S 2 .

< let y = K in S 1 | x | S 2 > ≜ let y = K in < S 1 | x | S 2 > < K | x | S 2 > ≜ let x = K in S 2
We define contexts representing the continuation of a specific skelement or skeleton as follows.

E ::= [•] | < [•] | x | S >
The generation process for a hook call annotated with c in a given rule r = (hr( y, c r ( x)) := S r ) is done in two steps: we go through S r to find the call and build its continuation, then we update the semantics by adding c, its typing, and its rule. We write S r E for the first step, where r is the rule under consideration, E the continuation built so far, and S the skeleton we traverse (initially S r ). We write generate(c, h, r, E) for the second step, i.e., extending the hook h with a rule for c built from r and E.

The operation S r E inductively goes through S, building the continuation in its parameter E and returning the set of new rules.

Branching (S 1 , . . . , S n ) r E ≜ S 1 r E ∪ . . . ∪ S n r E Filter f t r E = Return t r E ≜ ∅ Hook Reuse h t r E = Hook Tail h t r E ≜ ∅ Hook (New c) h t r E ≜ {generate(c, h, r, E)} let v = K in S r E ≜ K r <[•]| v|E[S]> ∪ S r E
Once a hook call h needing c is found, we generate the corresponding rule using generate(c, h, r, E).

Assuming r = (hr( y, c r ( x)) := S r ), we proceed as follows.

We first compute the variables needed as arguments of c and their respective type. We start with the variables z (with type s) needed to evaluate E, which are the ones occurring in E, but not defined in E and not in y. We do not need to include the variables y as arguments of c since they are already accessible at that program point. We then introduce fresh variables w to evaluate the current hook call h, as it is the first skelement of the new rule for c. The types u of the variables w are given by the input types of h, i.e., u = (htype in (h), htype p (h)). Finally, the typing of c is ctype(c) = (( u, s), htype p (hr)), since the new constructor should build the program type evaluated by the analyzed rule.

We then create the new rule for c as hr( y, c( w, z)) := E[Hook Reuse h w]: it evaluates h with the variables w and then E with y and z. The call h can reuse the new constructor c as the variables have been defined so that there is no overlap in their uses.

Example 4.2.1. We apply the generation process to the second hook call in the rule for While presented in Section 4.1.2. We reach this hook call K in a context E:

K := Hook (New While2) hstmt (s1, t2) E := < [•] | s2 | Hook Tail hstmt (s2, While (e1, t2)) >
The new constructor needs the variables (e1 : expr) and (t2 : stmt) to evaluate E, but not s2, as it is defined by E. We get the input types u = (state, stmt) of the hook call, for which we create the fresh variables w = (s0, t0). From there we type the new constructor and create its rule:

ctype(While2) = (
s 0 , e 0 ⇓ s 1 , v 1 s 1 , e 2 ⇓ s 2 , v 2 s, (s 0 , e 0 ) + 1 e 2 ⇓ s 2 , (v 1 + v 2 ) s 0 , e 0 ⇓ s 2 , v 2 s, (s 0 , e 0 ) + 2 v 1 ⇓ s 2 , (v 1 + v 2 )
To sum up, the generation phase consists of running the scanning S r r

[•] for every rule r = (hr( y, c r ( x)) := S r ) ∈ R lbl , resulting in a new set C 2 ⊇ C 1 of constructors and a new set R gen ⊇ R lbl of rules.
We now have a set of rules R gen ∪ R getRet , defined on constructors C 2 and hooks H 1 .

Optimization

With the first analysis, we determine which hook calls can be reconstructed reusing the initial constructor. The resulting annotations are still present in the new rules we create, but may not be as accurate as they could be in presence of the new constructors. With a constructor restarting the computation at a closer program point, some hook calls can now reuse their constructors. A common example, presented below, is a rule evaluating two values with the same state.

As an optional optimization, we can repeat the analysis on the newly created skeletons to increase the number of constructor reuse. However, such an optimization also requires to garbage collect constructors and rules that have been introduced but are no longer needed. For this, we go through the final skeletal semantics and remove the constructors and rules that are no longer reachable from an initial term. Both hook calls make use of the variable s, so s is not part of the set SglUse(S) used for the analysis. As a result, none of the hook calls can reuse the constructor C, and the analysis creates two constructors C1 and C2. Then the generation phase builds the corresponding rules and we reach the following situation:

hook h (s : state, t : term) matching t : value := | C(t1, t2) -> let v1 = h (s, t1) in (* new constr: C1 *) let v2 = h (s, t2) in (* new constr: C2 *) merge (v1, v2) | C1(s0, t0, t2) -> let v1 = h (s0, t0) in (* reuse *) let v2 = h (s, t2) in (* new constr: C2 *) merge (v1, v2) | C2(s0, t0, v1) -> let v2 = h (s0, t0) in (* reuse *) merge (v1, v2)
Inside the new evaluation rule of C1, the second hook call can be reconstructed reusing C since s is now only used once. Intuitively, there is no need for a second new constructor C2. By repeating the analysis on the new rules we find a new possible constructor reuse:

| C1(s0, t0, t2) -> let v1 = h (s0, t0) in (* reuse *) let v2 = h (s, t2) in (* reuse *) merge (v1, v2)
At this point, C2 still appears in the rule for C so we cannot get rid of it. However, at the end of the full transformation it will be apparent that C immediately uses the constructor C1 and that C2 cannot be reached. We garbage collect it at this point.

Distribute Branchings

This phase is not present in the extended example as the issue it solves does not occur in IMP.

Reconstructing terms is problematic for hooks in nested computations. In the structure let x = (let y = Hook eval t in S1) in S2, a small-step transformation of eval may return a partially evaluated term which ends up stored in x, while S2 may expect x to contain a value; for example S2 may start by filtering x with isTrue. We avoid the issue by sequencing such nested computations as let y = Hook eval t in let x = S1 in S2, and the hook transformation of Section 4.2.4 ensures that x may only contain a value.

The grammar of skeletal semantics of Section 3.2 does not allow for nested LetIn, but the same issue is present for branchings inside LetIn. We therefore recursively transform a skeleton of the form (let v = Branching(S 1 , S 2 ) in S) into Branching(let v = S 1 in S, let v = S 2 in S), so that hook calls in S 1 and S 2 can be transformed in the final phase.

The distribution of LetIn over Branching, noted ⌈S⌋ is recursive and makes use of the binding on skeletons

< S 1 | v | S 2 > defined previously. ⌈Branching(S 1 , . . . , S n )⌋ ≜ Branching(⌈S 1 ⌋, . . . , ⌈S n ⌋) ⌈K⌋ ≜ K if K ̸ = Branching(..) ⌈let v = Branching(S 1 , . . . , S n ) in S⌋ ≜ Branching(⌈< S 1 | v | S >⌋, . . . , ⌈< S n | v | S >⌋) ⌈let v = K in S⌋ ≜ let v = K in ⌈S⌋ if K ̸ = Branching(..)
We apply this operation to every skeleton of our semantics:

R dist = {h( y, c( x)) := ⌈S⌋ | (h( y, c( x)) := S) ∈ R gen }
Note that duplicating the continuations recursively will not exponentially grow the size of the final small-step semantics. We generate new constructors before this phase, hence the generated constructors are shared between branches, avoiding any unwanted bloat. As an optimization, this distribution and duplication could also be skipped for branchings only containing filter calls and no hook calls.

The output of this phase is the set of rules R dist ∪ R getRet , still defined on C 2 and H 1 .

Make the Skeletons Small-Step

With the results of the analysis and generation phase done so far, we are ready to make the initial hooks small-step. As explained in the overview, we first update their output types.

Forall h ∈ H 0 with htype(h) = (( s, s p ), u), we redefine: htype(h) = (( s, s p ), ( s, s p ))

We only change the initial hooks-the ones in H 0 -as the hooks getRet_h added in H 1 have been created with the desired, and different, output types: they actually extract the value from a term.

We then treat the skeletons defining these hooks, including the rules added in Section 4.2.2.

At this stage of the transformation, we argue these skeletons respect the following simplified grammar, either directly or with a simple modification.

S ::= let v = K in S | Branching S | Hook Tail h t | Return t K ::= Filter f t | Hook Reuse h w | Hook (New c) h t | Return t
A skeleton let v = Branching S in S is impossible because of the distribution step of Section 4.2.3. A tail-call is necessarily a final hook, so a skeleton let v = Hook Tail h t in S is also not possible. Whenever a hook call is annotated Reuse, the analysis of Section 4.2.2 implies that its input terms are all variables w. If a skeleton ends with K that is not a tail-call (i.e., a skelement Filter f t or Hook a h t with a ̸ = Tail), we can transform it into an equivalent skeleton following the grammar above by delaying the return. For this, we replace K with the skeleton let z = K in Return z, where z are freshly created variables, in number corresponding to the output type of K. As such, it is sufficient to define our transformation process on skeletons respecting the simplified grammar.

The transformation relies on a substitution to remember how the initial arguments of the rule are changed through the different hook calls, as we show in Example 4.2.5. A substitution σ is a total mapping from variables to terms equal to the identity except on a finite set of variables called its domain. We write xσ for the application of σ to x, ϵ for the identity substitution, and [t/x] for the substitution whose domain is {x} and such that xσ = t. We extend the notion to terms tσ and tuples tσ as expected. Given two substitutions σ and σ ′ , we define their sequence

σ ; σ ′ so that x(σ ; σ ′ ) = (xσ)σ ′ for all x. ∥ Branching (S 1 , . . . , S n ) ∥ r σ ≜ Branching (∥ S 1 ∥ r σ , . . . , ∥ S n ∥ r σ ) ∥ let v = Return t in S ∥ r σ ≜ let v = Return t in ∥ S ∥ r σ ∥ let v = Filter f t in S ∥ r σ ≜ let v = Filter f t in ∥ S ∥ r σ ∥ Return t ∥ r σ ≜ Return ( y, Ret_hr( t)) ∥ Hook Tail hr t ∥ r σ ≜ Return t ∥ let v = Hook (New c) h t in S ∥ r σ ≜ Return ( y, c( t, z c ))
where (hr( y, c( w

c , z c )) := S c ) ∈ R dist ∥ let v = Hook Reuse h ( w ′ , w) in S ∥ r σ ≜ Branching(S 1 , S 2 )
where We coerce the results of a final Return skelement with the constructor Ret_hr defined in Section 4.2.1. We remind that we also return the environment variables y of the rule to respect the updated typing of the hook.

S 1 = let z = Hook h w in Return ( y, c r ( x))(σ ; [z/w]) z fresh, w = ( w ′ , w) S 2 = let v = Hook getRet_h (w) in ∥ S ∥ r σ ; [Ret_h( v)/w]
A tail-call is simply turned into a return, as the hook being called is identical to the one where the current rule is defined (see Figure 4.1).

As explained in Section 4.1.3, a hook call annotated (New c) is turned into a return with a term built with c, so that the rule created for c in Section 4. We change a hook call that can reuse its constructor into a branching representing its possible behaviors. The first branch begins by reducing the hook one step further let z = Hook h w in ..., storing the results in some fresh variables z. We then reconstruct a configuration using the constructor c r of the rule being processed. Starting from the initial input ( y, c r ( x)), we apply σ before changing w by their new values z. The substitution σ is necessary if one of the variables w is not part of the initial arguments but defined from a previous hook call, as we can see in the Plus example below. The second branch covers the case where the term represented by w is a coerced set of values. We extract the content of w into the variables v of the initial skeleton and continue transforming S, remembering that w is equal to Ret_h( v).

Example 4.2.5. Consider the rule for Plus:

| Plus (e1, e2) -> let (s1, v1) = hexpr (s, e1) in (* reuse *) let (s2, v2) = hexpr (s1, e2) in (* reuse *) let v = add (v1, v2) in (s2, v)
The first call is turned into two branches, the first one stepping once (s, e1) into some fresh variables (z1, z2). The reconstructed configuration is simply the input (s, Plus (e1, e2))

where s and e1 are replaced by z1 and z2. In the second branch, we extract the content of e1, and then transform the rest of the skeleton, remembering that e1 = Ret_hexpr(s1, v1) in σ.

Transforming the second hook call illustrates why we need σ. As for the first call, the first branch steps once (s1, e2) into some fresh variables (z3, z4) and then reconstructs a configuration. We see that s1 does not occur in the initial configuration (s, Plus (e1, e2)); we therefore apply the substitution to create (s, Plus (Ret_hexpr(s1, v1), e2)), and now we can turn (s1, e2) into (z3, z4), resulting in the configuration (s, Plus (Ret_hexpr(z3, v1), z4)). The second branch continues transforming the rest of the skeleton, where we no longer need the substitution. In the end, we obtain:

s, e 1 → z 1 , z 2 s, e 1 + e 2 → z 1 , z 2 + e 2 e 1 = (s 1 , v 1 ) s 1 , e 2 → z 3 , z 4 s, e 1 + e 2 → s, (z 3 , v 1 ) + z 4 e 1 = (s 1 , v 1 ) e 2 = (s 2 , v 2 ) s, e 1 + e 2 → s, (s 2 , v 1 + v 2 )
If we do not reuse the initial constructors, we do not need the substitution σ as we cannot have nested hook calls where we reuse constructors. Instead, we change constructor, e.g., going from Plus1 to Plus2. As a result, the small-step transformation without reuse is written ∥ S ∥ r and the skeletons S 1 and S 2 of the last case of Figure 4.2 become:

S 1 = let z = Hook h w in Return ( y, c r ( x)) [z/w] z fresh, w = ( w ′ , w) S 2 = let v = Hook getRet_h (w) in ∥ S ∥
s 0 , e 0 → z 1 , z 2 s, (s 0 , e 0 ) + 1 e 2 → s, (z 1 , z 2 ) + 1 e 2 s, (s 0 , v 1 ) + 1 e 2 → s, v 1 + 2 (s 0 , e 2 ) s 0 , e 0 → z 1 , z 2 s, v 1 + 2 (s 0 , e 0 ) → s, v 1 + 2 (z 1 , z 2 ) s, v 1 + 2 (s 0 , v 2 ) → s, (s 0 , v 1 + v 2 )
Once again, the last phase of the transformation is applied only to the rules R dist defining Chapter 5

CERTIFICATION OF THE TRANSFORMATION

We prove that the transformation is correct, i.e., that the initial and transformed semantics are equivalent. To deal with the complexity of the approach, we provide two complementary correctness results. The first one is a pen-and-paper proof that the transformation without constructor reuse is correct. The proof is available in Appendix C, and we present the results in Section 5.1. To further our trust in the transformation and to deal with constructor reuse, we provide a mechanized approach to correctness, presented in Section 5.2. This second approach does not attempt to formalize the whole transformation nor the reuse analysis, but it instead generates a fully automatic Coq proof of the equivalence of the initial and transformed semantics instantiated on a given language.

The pen-and-paper proof gives us confidence that the generation will not fail in the absence of constructor reuse, and the automatic Coq proof shows the correctness of the analysis for constructor reuse for a given language. The generated Coq proof can thus be considered as a certificate for an instance of the transformation.

To state the equivalence theorems between the two semantics, we use the interpretation judgments of Section 3.3 (Figure 3.2). In summary, we write a ⇓ h b to state that h takes the values a as input and output the values b. As the transformation provides several intermediate semantics (i.e., set of rules), we extend the notation to include the rule set used, writing for instance a ⇓ BS h b for the initial big-step semantics, and a ⇓ SS h b for the generated small-step semantics of Section 4.2.4, without reuse in Section 5.1, and with reuse in Section 5.2.

We recall that the inductive interpretation is inherently big-step, as a judgment computes the whole skeleton, so we keep the formal notation ⇓ SS h even for the output semantics. However, the resulting set of rules is created such that its interpretation corresponds to a standard small-step reduction.

Pen-and-Paper Proof

As said above, we do not certify the analysis of Section 4.2.2, as it would make the proof substantially more complex. We would need to justify the correctness of the analysis and that the information is used properly, including for instance the substitution in Section 4.2.4. We only certify the base transformation without reuse, but extend the proof to both finite and diverging computations. The full proof is available in Appendix C.

We prove that a big-step evaluation is possible if and only if a sequence of small-step reductions can lead to the same result up to a Ret_h coercion. In the finite case, assuming the terms a and b are written using the initial big-step semantics, we show that for all hook

h a ⇓ BS h b ⇐⇒ ∃ a ′ , a (⇓ SS h ) * ( a ′ , Ret_h( b))
where R * is the reflexive transitive closure of a relation R.

For diverging derivations, we define infinite small-step reductions coinductively with this single rule:

a ⇓ SS h b b ∞ → h a ∞ → h = = = = = = = = = = = = = = = = Big-step divergence (noted ⇑ h
) is defined in Section 3.4 (Figure 3.3). We prove that the following equivalence holds for all initial terms a and hook h.

a ⇑ BS h ⇐⇒ a ∞ → h

Proof Sketch

The interesting direction is to show that a sequence of small-step reductions implies a big-step evaluation, which can be done in two ways. A first technique [START_REF] Hanne | Semantics with applications -a formal introduction[END_REF] is to recognize subcomputations in the sequence of small steps which correspond to subtrees of the big-step derivation. For example, if Plus(e 1 , e 2 ) evaluates into v with small steps, it means that e 1 evaluates to some v 1 , e 2 evaluates to some v 2 , and v = v 1 + v 2 . By induction, the two subcomputations for e 1 and e 2 can be turned into big-step derivations which are then combined to create the derivation for

Plus(e 1 , e 2 ).

Another strategy [START_REF] Bach Poulsen | Flag-based big-step semantics[END_REF][START_REF] Leroy | Coinductive big-step operational semantics[END_REF] relies on a concatenation lemma, stating that we can merge a small step into a big step: if e makes a small step to e ′ and e ′ evaluates in a big step to v, then e evaluates in a big step to v. We use this technique as it is easier to automatize for the Coq certification (see Section 5.2). It only works for finite sequences, however; we use a different strategy when dealing with divergence.

The downside of the approach based on the concatenation lemma is that the big-step and small-step semantics need to be defined on the same constructors. However, the initial big-step semantics is not defined on the newly created constructors, such as While1, While2, Plus1, or

Plus2-remember that we do not reuse the initial constructors in the proof of this section. To bridge the gap between the initial big-step (BS) and the small-step (SS) semantics, we consider an extended big-step semantics (EBS) defined on all constructors.

The rule set for EBS is noted R EBS . It corresponds to the situation before going small-step (see Section 4.2.3), i.e., R dist (after delaying returns), but with additional custom rules so that the coercion constructors Ret_h can be evaluated.

R EBS ≜ R dist ∪ {h( y, Ret_h( v)) := Return v | h ∈ H 0 }
The additional rules allow derivations of the following form (see Lemma C.4.4).

(

a, Ret_h( b)) ⇓ EBS h b
As an example, the EBS semantics for IMP-with constructor reuse however-can be found in Appendix B.5.

We thus manipulate three semantics in this proof: the initial BS semantics written by the user, the EBS semantics generated in the middle of the transformation, and the SS semantics, result of the transformation. The proof strategy is to show that BS and EBS are equivalent on initial terms, and then prove that EBS is equivalent to SS on all terms (including the extended ones). In the end, we get that BS is equivalent to SS on initial terms. The first equivalence is straightforward since EBS has almost the same rules as BS for the initial constructors. Proving that SS implies EBS relies on the concatenation lemma in the finite case, and on the splitting strategy in the infinite one. The opposite direction is done by induction on the EBS derivation.

After some preliminary results, we detail each step in the following subsections.

Transformation Properties

Before stating the equivalence theorems, the proof introduces a few properties and certifies the behavior of the different phases of the transformation.

We define free (fv) and bound (bv) variables of terms and skeletons as expected (see Definitions 1 and 2). We also note NoRedef(S) the statement that a skeleton S does not bind multiple times the same variable name, defined as follows.

NoRedef(K) ≜ ⊤ if (K ̸ = Branching(. . .)) NoRedef(Branching (S 1 , . . . , S n )) ≜ ∀i, NoRedef(S i ) NoRedef(let v = K in S) ≜       
{ v}, bv(K), and bv(S) are disjoint NoRedef(K)

NoRedef(S)
From this, we specify a Static Single Assignment form for skeletons, noted SSA(S) and defined as follows 1 .

SSA(S) ≜ NoRedef(S) ∧ fv(S) ∩ bv(S) = ∅ It states that a skeleton S does not reuse variables names. The transformation is supposed to be applied to such skeletons. A consequent but straightforward part of the proof (Section C.3) ensures the different phases of the transformation preserve the SSA conditions, as to not inadvertently capture free variables.

Then, we prove a few results showing that the different transformation phases work as intended. For example, we verify that the distribution of branchings does not modify the semantics (Lemma C.4.1).

Lemma 5.1.1. For all skeleton S, state Σ, and terms a such that SSA(S),

Σ ⊢ S ⇓ a ⇐⇒ Σ ⊢ ⌈S⌋ ⇓ a
This equivalence relies on a plain but important lemma (C.2.7), stating that, under some freshness conditions, the monadic bind on skeletons operates exactly like a LetIn structure: The most important result at this point synthesizes the effects of both the generation of new constructors and the distribution of branchings to relate constructor names in labels and rules in the extended big-step semantics (Lemma C.4.13).

Σ ⊢< S 1 | x | S 2 >⇓ a ⇐⇒ ∃ b,    Σ ⊢ S 1 ⇓ b Σ + {x → b} ⊢ S 2
Lemma 5.1.2. For all rule (h( y, c( x)

) := S) in R EBS , if (let v = Hook (New c 0 ) h 1 t in S 0
) is a subskeleton of S, then R EBS contains a rule of the following form.

h( y, c 0 ( w, z)) := let v = Hook Reuse h 1 w in S 0
It states that if we encounter a hook call annotated with a fresh constructor c 0 in the extended big-step skeletal semantics, then we indeed previously generated the corresponding constructor c 0 and its skeleton. Remember that the last phase of the transformation (see Section 4.2.4) changes

(let v = Hook (New c 0 ) h 1 t in S 0 ) into Return( y, c 0 ( t, z)
). This lemma ensures indirectly that the small-step behavior is not modified, as running h again still computes h 1 t followed by (the transformed version of) S 0 . The proof mainly checks that the contexts E of Section 4.2.2 used to define new skeletons interacts well with the distribution of branchings of Section 4.2.3.

With the effects of of the different transformation phases made explicit, we state and prove the equivalences between the different rule sets.

Initial and Extended Big-Step Semantics

The The only significant transformation between the two semantics is the distribution of branchings (Section 4.2.3), but we prove it does not modify the semantics (Lemma 5.1.1). Thus, the equivalence holds on initial terms (Theorems C.5.1 and C.5.2). To state the theorems, we write |t| for the the canonical injection of an initial term t into the extended semantics.

|a| ⇓ EBS h b ′ =⇒ ∃ b, b ′ = |b| ∧ a ⇓ BS h b
We follow the same strategy for diverging computations. Similarly, the corresponding lemma (C.4.2) assures us there is no problem with the distribution of branchings for coinduction, and we can deduce the equivalence between BS and EBS on initial terms (Theorem C.5.3).

Theorem 5.1.5 (Div: BS⇔EBS). For all hook h and initial terms a,

a ⇑ BS h ⇐⇒ | a| ⇑ EBS h
The equivalence between big-step and extended big-step is the only part manipulating both initial and extended terms. In the following subsections, all results manipulate extended terms only.

Small-Step Implies Extended Big-Step

We prove a sequence of small-step reductions can be turned into a big-step evaluation in the following sense (Theorem C.7.2).

Theorem 5.1.6 (SS⇒EBS). For all hook h and terms a, a ′ , and b,

a (⇓ SS h ) * ( a ′ , Ret_h( b)) =⇒ a ⇓ EBS h b
As explained previously, in the finite setting, we rely on a concatenation lemma (C.7.1), stating that we can merge a small step into a big step.

Lemma 5.1.7 (Concatenation). For all hook h and terms a, a ′ , and b,

a ⇓ SS h a ′ ∧ a ′ ⇓ EBS h b =⇒ a ⇓ EBS h b
The main theorem then follows from iterating this result, starting from ( a ′ , Ret_h( b)) ⇓ EBS h b as a base case. We also use this strategy for the Coq certification, as generating automatically splitting lemmas would be more difficult (see Section 5.2).

For this concatenation result, because the evaluation of tuples (⇓ h ) is defined mutually with the evaluation of skeletons (• ⊢ • ⇓ •), we need to prove in parallel a second property2 :

Σ ⊢ ∥ S ∥ r ⇓ SS a ′ ∧ a ′ ⇓ EBS h b =⇒ Σ ⊢ S ⇓ EBS b
We proceed by mutual induction on the derivations of a ⇓ SS h a ′ and Σ ⊢ ∥ S ∥ r ⇓ SS a ′ . The main difficulty is to show that changing constructor in small-step corresponds to an EBS derivation where such a change does not occur. For this, we use Lemma 5.1.2 presented in Section 5.1.2. We discuss the issue in more details in the next section for the reverse implication, where we encounter the same difficulty.

For infinite behaviors, we prove the following theorem (C.7.4).

Theorem 5.1.8 (Div: SS⇒EBS). For all hook h and terms a,

a ∞ → h =⇒ a ⇑ EBS h
The concatenation lemma cannot be used here as we no longer have a base case to start its iteration. Instead, we show that a ∞ → h satisfies the big-step coinductive rules of Figure 3.3 by splitting the infinite sequence of small steps into subderivations. Such a subderivation may occur when evaluating a constructor c( x, y) with a rule (let v = Hook Reuse h 1 x in S) in R EBS (such rules are present for new constructors, see Example 4.2.2). We distinguish two cases: either the small-step divergence occurs when evaluating x, which correspond to the hook call h 1 , or when evaluating S. In the first case, by coinduction, we have a diverging EBS derivation for that skelement, and therefore the complete LetIn structure diverges. Otherwise, the evaluation of x terminates, and S diverges. Using Theorem 5.1.6 above for the finite case, we get a finite EBS derivation for x, and by coinduction, a diverging EBS derivation for S, and the complete LetIn structures diverges as well.

Extended Big-Step implies Small-Step

Lastly, we prove we can transform a big-step evaluation into a sequence of small-step reductions (Theorem C.6.1).

Theorem 5.1.9 (EBS⇒SS). For all hook h and terms a and b,

a ⇓ EBS h b =⇒ ∃ a ′ , a (⇓ SS h ) * ( a ′ , Ret_h( b))
As before, we need to prove in parallel a similar result for skeletons 3

Σ ⊢ S ⇓ EBS b =⇒ ∃ a, a ′ . Σ ⊢ ∥ S ∥ r ⇓ SS a ∧ a (⇓ SS h ) * ( a ′ , Ret_h( b))
and proceed by mutual induction on the derivations of a ⇓ EBS h b and Σ ⊢ S ⇓ EBS b. The proof is quite long because of the large number of cases.

The main difficulty is in changing constructors when needed. Indeed the EBS semantics does not make use of the newly defined constructors in the evaluation of the original constructors, unlike the small-step semantics, which for instance switches from While to While1. When we have Σ ⊢ S While ⇓ EBS b, we need to argue that the skeleton for While1 is almost the same as the one for While to be able to use the hypotheses we have at this point. This is where Lemma 5.1.2 becomes crucial: the first hook call in the rule for While is annotated with (New While1), and thus, by construction, we know the rule for While1 uses (almost) the same skeleton and behaves similarly.

For infinite behaviors, we state the result as follows (Theorem C.6.4).

a ⇑ EBS h =⇒ ∃ b, a ⇓ SS h b ∧ b ⇑ EBS h 3.
Once again, there is a couple additional hypothesis omitted here stating that S is a legitimate skeleton of the semantics, so we can leverage its properties (see Theorem C.6.1).

This result can be seen as the reverse of a concatenation lemma, i.e., a big-step divergence can be split into a small-step reduction and another big-step divergence. We proceed by induction on a, and by case analysis on the definition of a ⇑ EBS h . We conclude in each case with a proof very similar to the corresponding case in the proof of Theorem 5.1.9 for finite evaluations.

Coq Proof Script Generation

The paper proof of Section 5.1 comforts us in the correctness of (most of) the transformation.

However, a mechanized proof is still valuable, as it protects us from human error in both the proof and the implementation.

A complete formalization of the transformation in a proof assistant seems out of reach because of its many intermediary steps. Instead, we follow the easier route of providing an a-posteriori certification alongside the resulting semantics. To this end, we rely on the possibility offered by Necro to export a skeletal semantics into a Coq representation. We automatically generate proof scripts showing that the produced small-step semantics corresponds to the initial big-step one.

We explain how to generate the scripts using IMP as an example; the repository [START_REF] Ambal | Implementation of "Automatic Transformation of a Big-Step Skeletal Semantics into Small-Step[END_REF] contains examples of Coq scripts for other languages as well as the script generator.

Proof Sketch

Just like the paper proof of Section 5.1, we rely on the concrete interpretation judgment of Section 3.3 (see Figure 3.2).

In the case of IMP, the equivalence theorem between the semantics is as follows.

Theorem 5.2.1. For all (s, s 0 : state), (e : expr), (t : stmt), and (v : value),

(s, e) ⇓ BS hexpr (s 0 , v) ⇐⇒ ∃s ′ , (s, e) (⇓ SS hexpr ) * (s ′ , Ret_hexpr(s 0 , v)) (s, t) ⇓ BS hstmt s 0 ⇐⇒ ∃s ′ , (s, t) (⇓ SS hstmt ) * (s ′ , Ret_hstmt(s 0 ))
A big-step evaluation is possible if and only if a sequence of small-step reductions can lead to the same result up to coercions.

For the sake of readability, we limit the examples of this section to expressions and deliberately ignore the states. We also write Ret for Ret_hexpr, and use a more intuitive notations for the concrete interpretation of the small-step semantics (→

SS

).

The challenging direction is to transform a sequence of small-step reductions into a big-step evaluation. The textbook proof method [START_REF] Hanne | Semantics with applications -a formal introduction[END_REF] relies on auxiliary lemmas to split the sequences of reductions in order to recreate big-step derivation trees, such as the following result for Plus.

Plus(e 1 , e 2 )

→ SS k v =⇒ ∃k 1 , k 2 , v 1 , v 2 . (k = k 1 + k 2 + 1) ∧ e 1 → SS k 1 v 1 ∧ e 2 → SS k 2 v 2 ∧ (v = v 1 + v 2 )
This technique depends a lot on the semantics of the language, as each lemma must be derived from the constructor skeleton. We instead reuse the strategy of Section 5.1.4, with a concatenation lemma stating that we can merge a small step into a big step.

e → SS e ′ ⇓ BS v =⇒ e ⇓ BS v
Such a local result can be verified with language-independent tactics that simply decompose the small-step reduction. We then iterate the lemma to get the desired result.

Since the concatenation lemma requires a common syntax, we generate the extended big-step semantics (EBS) we used in the paper proof (Section 5.1.1), defined on all constructors. The three semantics we need-the user-defined BS semantics, the EBS semantics generated in the middle of the transformation, and the resulting SS semantics-are transformed into Coq definitions using the export function of Necro. Necro also provides a Coq definition of the concrete interpretation, which itself depends on interpretations for the base types and filters. Our proof script takes as global parameters such interpretations for the initial BS, which are carried to the other two semantics through coercions. This way, the certification is independent from the behavior and implementation of these basic elements.

The proof strategy is therefore similar to the paper proof: to show that BS and EBS are equivalent on initial terms, and then prove that EBS is equivalent to SS on all terms (including the extended ones). In the end, we get that BS is equivalent to SS on initial terms, as stated in Theorem 5.2.1.

Initial and Extended Big-Step

The

(|s|, |e|) ⇓ EBS hexpr (s ′ 0 , v ′ ) =⇒ ∃s 0 , v, (s ′ 0 , v ′ ) = (|s 0 |, |v|) ∧ (s, e) ⇓ BS hexpr (s 0 , v) (|s|, |t|) ⇓ EBS hstmt s ′ 0 =⇒ ∃s 0 , s ′ 0 = |s 0 | ∧ (s, t) ⇓ BS hstmt s 0
This proof step is the only one at the interface between initial and extended terms and as such, the only one using coercions. Henceforth, the results are stated on extended terms (eexpr and estmt).

Small-Step Implies Extended Big-Step

As explained before, the strategy for this direction relies on a concatenation lemma merging a small step and an extended big step together. The proof is done by induction on the small step;

in each case, we also need a case analysis on the big-step hypothesis which generates numerous subcases. Fortunately, the proof principle is simple enough that elementary tactics are sufficient for Coq to automatically reconstruct the extended big step in all cases. For instance, if the small step comes from a congruence, i.e.,

Plus(e 1 , e 2 ) → SS Plus(e ′ 1 , e 2 ) ⇓ EBS v from e 1 → SS e ′ 1 , e ′ 1 ⇓ EBS v 1 , e 2 ⇓ EBS v 2 , and v = v 1 + v 2 ,
we apply the induction hypothesis to get e 1 ⇓ EBS v 1 and reconstruct a big step from our pieces.

To automate this reasoning, we need tactics to automatically apply the induction hypothesis, which are straightforward and language-independent, and tactics to recreate an extended big step from the right hypotheses. Concluding each case can be automated without prior knowledge of the language because we only have to check the result and there is nothing to guess. For Plus, we want Plus(e 1 , e 2 ) ⇓ EBS v: we know the term to evaluate, we know the resulting value, and we have the necessary subevaluations and filter hypotheses. The checking tactic simply opens the skeleton and verifies that there is indeed a path from the initial term to the resulting one.

Lemma 5.2.4 (Concatenation)

. For all (s, s ′ , s 0 : estate), (e, e ′ : eexpr), (t, t ′ : estmt), (v : evalue),

(s, e) ⇓ SS hexpr (s ′ , e ′ ) ⇓ EBS hexpr (s 0 , v) =⇒ (s, e) ⇓ EBS hexpr (s 0 , v) (s, t) ⇓ SS hstmt (s ′ , t ′ ) ⇓ EBS hstmt s 0 =⇒ (s, t) ⇓ EBS hstmt s 0
Then, using this concatenation lemma, a simple induction on the reflexive and transitive closure gives us the desired results.

Theorem 5.2.5 (SS⇒EBS). For all (s, s ′ , s 0 : estate), (e : eexpr), (t : estmt), and (v : evalue),

(s, e) (⇓ SS hexpr ) * (s ′ , Ret_hexpr(s 0 , v)) =⇒ (s, e) ⇓ EBS hexpr (s 0 , v) (s, t) (⇓ SS hstmt ) * (s ′ , Ret_hstmt(s 0 )) =⇒ (s, t) ⇓ EBS hstmt s 0

Extended Big-Step Implies Small-Step

Proving that a big-step evaluation of an extended term corresponds to a small-step sequence of reduction is done by induction on the derivation of the big-step evaluation. In each case, we need to build a complete sequence of small steps from the partial sequences we get from using the induction hypothesis. For instance, decomposing the evaluation Plus(e 1 , e 2 ) ⇓ EBS v produces the hypotheses e 1 → SS * Ret(v 1 ), e 2 → SS * Ret(v 2 ), and v = v 1 + v 2 , from which we want to show

Plus(e 1 , e 2 ) → SS * Ret(v).
As in the previous section, we could let Coq conclude in a bruteforce way, by trying to derive the desired conclusion from the hypotheses, without any knowledge of the semantics of the language. It requires Coq to guess the intermediary configurations; e.g., if

e 1 → SS e ′ 1 → SS * Ret(v 1 ),
then the final sequence should go through Plus(e ′ 1 , e 2 ). On complex cases, we may also need to change constructors (e.g. While1(..) → SS While2(..)). A lot of trial-and-error may be necessary to find these intermediary configurations, notably for cases involving non-determinism where several small-step reductions are possible. Overall, such bruteforce tactics are not efficient enough: it works on small languages such as IMP, but not for more complex languages such as our miniML example.

Instead, we help Coq by generating congruence results about the small-step reduction. With them, we still might need backtracking to find the right combination of lemmas to apply, but all possible small-step reduction cases have been defined so we are not repeatedly losing exploration time to regenerate them in each search. In the case of Plus, we need lemmas of the form:

e 1 → SS * e ′ 1 =⇒ Plus(e 1 , e 2 ) → SS * Plus(e ′ 1 , e 2 ) e 2 → SS * e ′ 2 =⇒ Plus(Ret(v 1 ), e 2 ) → SS * Plus(Ret(v 1 ), e ′ 2 ) v = v 1 + v 2 =⇒ Plus(Ret(v 1 ), Ret(v 2 )) → SS * Ret(v)
Combining them lets us prove the desired result. Such congruence results come for free if the reduction is written using small-step inference rules. In our case, we have skeletons and the concrete interpretation, so we need to derive them. It is the only part of the proof script that really depends on the semantics of the language, as we read the skeletons to generate these lemmas.

Each lemma corresponds to a path in a small-step rule. In the case of Plus (whose skeleton is detailed in Example 4.2.5), following the different branches gives us three different paths:

-let (z1, z2) = hexpr (s, e1) in (z1, Plus (z2, e2)) -let (s1, v1) = getRet_hexpr (e1) in let (z3, z4) = hexpr (s1, e2) in (s, Plus (Ret_hexpr (z3, v1), z4)) -let (s1, v1) = getRet_hexpr (e1) in let (s2, v2) = getRet_hexpr (e2) in let v = add (v1, v2) in (s, Ret_hexpr (s2, v))
For each path, the LetIn definition contains the hypothesis of the lemma, while the final result is the configuration to step towards. If we forget about the state, we see that we obtain exactly the three previous lemmas. They are proved either by unfolding the definitions or doing a straightforward induction; each proof is simple since the structure of the lemma matches a path of the skeleton.

Once this is done, the proof of the main theorem is simply done by induction on the extended big step. In each case, we apply the induction hypothesis on every big-step premise, which gives us several small-step sequences on subcomputations. Then, the congruence lemmas are automatically applied and the results merged together by Coq to create the wanted small-step sequence.

Theorem 5.2.6 (EBS⇒SS). For all (s, s 0 : estate), (e : eexpr), (t : estmt), and (v : evalue),

(s, e) ⇓ EBS hexpr (s 0 , v) =⇒ ∃s ′ , (s, e) (⇓ SS hexpr ) * (s ′ , Ret_hexpr(s 0 , v)) (s, t) ⇓ EBS hstmt s 0 =⇒ ∃s ′ , (s, t) (⇓ SS hstmt ) * (s ′ , Ret_hstmt(s 0 ))

IMPLEMENTATION AND EVALUATION

The transformation and the self-certification have been implemented in Necro [START_REF] Courant | Necro: Animating Skeletons[END_REF], with a number of options (Section 6.1.1) to tailor the transformation to specific needs. Using the existing Necro tools, we can generate a Coq version of the small-step semantics to prove properties on it-on top of the equivalence with the big-step one already automatically proved as presented in Section 5.2.

We also generate an OCaml interpreter for the language allowing for small-step and big-step reductions (Section 6.1.2). In practice, we tested our transformation and automatic certification on a variety of user languages, with and without reuse. Section 6.2 presents our empirical results.

Implementation

Options and Optimization

The small-step transformation is part of the expanding Necro toolbox for the manipulation of skeletal semantics. We make the transformation more flexible by providing a few options for it.

Limiting the Transformation. Skeletal semantics can be defined with different levels of precision. For instance, booleans and their basic operations can either be left abstract by considering them a basic type with filters, or explicitly defined as a program type with True/False constructors and hooks. In the second case, these hooks can be converted to small-step reduction processes.

However, sometimes we are only interested in the big-picture of the reduction of main expressions and do not wish to stop and reconstruct when computing boolean operations. For these situations, we propose an option to only transform a specific subset of hooks. High-level evaluation processes can thus be turned into a small-step reduction while keeping low-level operations in their big-step form. On the IMP language example, we could wish to only transform the evaluation of statements. This is akin to considering a small-step operational semantics for statements and a denotational semantics for expressions.

No Reuse. Our transformation tries to reuse the user defined constructors as much as possible in order to reduce the number of additional constructors. If need be, an option forces the creation of new constructors for each hook call. This would lead to more terms, but also to significantly simpler skeletons as every constructor would focus on a specific hook call. Depending on the purpose of the small-step semantics, it might be an interesting trade-off.

No Additional

Steps. The transformation presented above makes additional administrative steps when changing constructor or refocusing, as it is common on paper to reduce the number of inference rules of the language. However, this is not necessary, and an option forces the transformation to keep tail-calls and perform a recursive call after changing constructor. This leads for instance to skeletons mimicking the rules:

s, t 2 → s ′ , t ′ 2 s, If(True, t 2 , t 3 ) → s ′ , t ′ 2 s, e 1 → s ′ , e ′ 1 s, While(e 1 , t 2 ) → s, While1(s ′ , e ′ 1 , e 1 , t 2 )
The Coq certification generator is compatible with the two previous options but not this one, as the implemented proof script expects the administrative steps.

Optimization. The difficulty of the transformation is to reconstruct and reuse as much of the initial semantics as possible, as well as simplify the necessary new constructors. To help and clean up the results, we implemented a few optimizations, and further extensions are still possible.

For instance, we implemented a small optimization to reduce the number of unnecessary arguments. When a hook call is the only one to use a state, and we need a new constructor for it, duplicating the state is not necessary. This happens for instance for While1, where our implementation outputs:

| While (e1, t2) -> (s, While1 (e1, e1, t2)) | While1 (e0, e1, t2) -> branch let (w1, w2) = hexpr (s, e0) in (w1, While1 (w2, e1, t2)) or ... end However, this check is not sufficient for all cases, and While2 still has 4 arguments as it evaluates s1 which is not the main state. To recover the usual hand-written semantics, we would need to overwrite s with the content of s1. We are not certain overwriting states is always preferable, as it might conflict with other potential optimizations such as detecting read-only states (see Section 6.2).

Ocaml Interpreter

As presented in Section 3.5, Necro provides a tool to automatically generate an OCaml interpreter from a skeletal semantics. Using the transformation of this paper as an intermediate step, we are able to create an interpreter providing both big-step and small-step implementations of hooks. As terms now include values, which may be present as subterms of constructors, the small-step interpreter operates on a syntax extending the one used in big-step. We therefore automatically create separate program types for extended terms (e.g., eexpr and estmt for the IMP example), and generate new hooks to bridge the gap between the initial and extended program types (e.g., ext_expr : expr → eexpr and unext_eexpr : eexpr → expr). Using coercions, the small-step interpreter requires the same instantiated types and functions as the big-step one, so it provides an executable small-step semantics at no additional cost.

Evaluation

The transformation can be applied to any semantics written in skeletal semantics. The theorems of Chapter 5 hold even if the input semantics has an unusual shape. However, the result only seems useful if the input follows the big-step paradigm. For instance, if the input is already smallstep, then the output would be an odd semantics akin to a malformed abstract machine [START_REF] Ambal | Implementation of "Automatic Transformation of a Big-Step Skeletal Semantics into Small-Step[END_REF]. In this section, we limit the evaluation to the transformation of big-step semantics.

We compare the sizes of the generated small-step semantics and equivalence proof scripts for various languages in Table 6.1. The examples include variants of the call-by-value (CBV) and callby-name (CBN) λ-calculus implemented with closures, and evaluated in an environment mapping variables to closures; CBV has also been extended with non-determinism and exceptions. The examples written in an imperative style include a small language roughly corresponding to IMP expressions (Arithmetic), and extensions of IMP with local (IMP, LetIn) or global (IMP, write) state modification, and with exceptions and handlers (IMP, try/catch). Lastly, miniML is an ML-like language, extending the λ-calculus with arithmetic and boolean operations as well as constructs to define algebraic datatypes and perform pattern-matching on them. The generated small-step semantics and proofs for all the examples can be found in the source repository [START_REF] Ambal | Implementation of "Automatic Transformation of a Big-Step Skeletal Semantics into Small-Step[END_REF].

In the certification, about 500 lines of code are completely independent of the input language and contain definitions of skeletons or concrete interpretation. About 450 lines of code are templates which are filled with basic information about the syntax of the input semantics (the names of the hooks, constructors, and filters): these are general definitions, results, and tactics to manipulate concrete interpretation or coercions. They are part of the generated proof script, but are not counted in Table 6.1, where we lists the sizes of the language-dependent parts of the proof.

We see that the resulting small-step semantics are significantly longer than the initial bigstep ones. This is because recursive computations are replaced with case disjunctions using branchings, quickly increasing the number of lines of the rules but not their complexity, as we can see with IMP. We also observe that the language-dependent part of the Coq proof scripts remains linear in the size of the small-step semantics. Indeed, it is composed mostly of the The transformation would compute that the state s is used twice and thus create a new constructor with a copy of it to evaluate e1. A small-step operational semantics written by hand would rely on the fact that hexpr does not modify the state to avoid introducing a new constructor in that case. Such an analysis requires a global understanding of the semantics which goes beyond the local study of hook calls our transformation is based on. This discrepancy occurs in the miniML language, for which we create 10 new constructors (with reuse, cf. Table 6.1), when only 9 are strictly necessary.

Chapter 7

CONCLUSION OF PART I

We present a fully automatic transformation from a big-step to a small-step skeletal semantics with or without reuse of the initial constructors. With reuse, we generate new constructors only for problematic program points. This allows users to benefit from the convenience of big-step definitions, while having access to the fine control of small-step semantics when necessary.

The resulting small-step skeletal semantics can then be given to Necro to generate OCaml interpreters or Coq formalizations. We exploit the latter feature to automatically and a-posteriori certify the correctness of the result of the transformation with reuse on any language for terminating evaluations. Equivalence proofs between big-step and small-step semantics are wellknown [START_REF] Hanne | Semantics with applications -a formal introduction[END_REF][START_REF] Bach Poulsen | Flag-based big-step semantics[END_REF][START_REF] Leroy | Coinductive big-step operational semantics[END_REF] but repetitive for large languages, so they benefit greatly from the automation and trust of proof assistants. The transformation without reuse is also proved to be correct independently from the input language in the terminating and diverging cases.

The transformations and the proofs make no assumptions about filters, and the filters defined for the big-step input are used by the small-step outputs up to coercions. Our work remains parametric in the implementation of filters, in the spirit of skeletal semantics.

In practice, this automated transformation and certification have been successfully tested on a wide variety of languages. The transformation with reuse produces small-step skeletal semantics close to the SOS rules that one would write by hand. The current implementation still has some limitations discussed below (see Section 7.2), but we believe the core idea of the transformation can be expanded to handle these cases.

Related Work

While several approaches go from a small-step to a big-step setting by manipulating either inference rules [START_REF] Bach Poulsen | Deriving Pretty-Big-Step Semantics from Small-Step Semantics[END_REF][START_REF] Ciobâcă | From Small-Step Semantics to Big-Step Semantics, Automatically[END_REF] or interpreters [START_REF] Danvy | A walk in the semantic park[END_REF][START_REF] Danvy | From Reduction-based to Reduction-free Normalization[END_REF], the other direction has been less pursued.

Vesely and Fisher [START_REF] Vesely | One Step at a Time -A Functional Derivation of Small-Step Evaluators from Big-Step Counterparts[END_REF] propose an automatic transformation from a big-step to a small-step interpreter. The input interpreter contains functions for small operations (e.g., updating a state) and a single evaluation function eval. Roughly, the transformation starts with a partial CPStransform of eval to turn recursive calls into continuations, considered as newly created terms.

After making eval a stepping function, it ends with an inverse CPS-transform recreating a small-step interpreter. As in our work, creating a new constructor for every continuation would be correct but the authors aim for an output closer to a semantics written by hand. For this, they substitute continuations that can be expressed as initial terms in order to simplify the resulting interpreter.

Vesely and Fisher's approach only considers subcomputations as reduction steps, as they transform only eval calls-similar to hook calls in our case-and ignore other simple functions calls-filter calls-or focus changes. The input interpreter, defined in an ad-hoc language, may not be as expressive as skeletal semantics, in particular because only one evaluation function is possible. It is not clear whether the approach scales to several mutually recursive functions.

An important difference is that their resulting small-step function only recreates a term and not a configuration. This systematically leads to a new constructor C packing a state and a term.

It is not necessarily less efficient than our approach, as they do not need new constructors when only state variables are reused. For instance, to reduce a λ-calculus term App(t 1 , t 2 ) with a subreduction s, t 1 → s ′ , t ′ 1 , we would reconstruct a configuration as s, App(t 1 , t 2 ) → s, App1(s ′ , t ′ 1 , t 2 ) while they would reconstruct a term as s ⊢ App(t 1 , t 2 ) → App(C(s ′ , t ′ 1 ), t 2 ). As a result, it is difficult to compare the outputs of the two approaches based on the number of additional constructors or rules, but the output semantics are very close to usual small-step definitions, with a minimal number of created constructors in both cases.

The strategy for simplifying the output semantics is also quite different. Our transformation performs an analysis first to avoid generating and using useless constructors, while they transform every eval call into a new constructors and rely on an a posteriori analysis to factorize constructors that behave the same. The analysis they present is lightweight and cannot regroup terms using other variables (e.g., states) differently. This does not seem as general as our approach, but it could probably be solved by embedding a stronger analysis if needed.

Finally, Vesely and Fisher [START_REF] Vesely | One Step at a Time -A Functional Derivation of Small-Step Evaluators from Big-Step Counterparts[END_REF] claim to have informal proofs of several parts of their transformation. We have a language-independent proof of the transformation without initial constructors reuse for terminating and diverging evaluations. With reuse, we generate an equivalence proof script for terminating evaluations for any input semantics.

Huizing et al. [START_REF] Huizing | A Small Step for Mankind[END_REF] present a transformation from a big-step to a small-step semantics, by directly manipulating inference rules. Small-step configurations are extended with a stack to keep track of the big-step premises that have already been computed. For each non-axiom big-step rule, they create several terms to indicate which premise is under evaluation, and a multitude of small-step rules to either initiate/conclude the big-step rule, change the premise under consideration, or reduce it. Rules for focusing on a new premise also guess an input state for the subcomputation; coherence is only checked when concluding the big-step rule. Guessing intermediate states, and delaying the unification until the end of the corresponding big-step rule, make the transformation very generic and interesting for languages with complicated control flow. However, the large number of small-step rules and new terms as well as the stack make the resulting semantics very different from usual SOS definitions.

Limitations and Perspectives

Our current small-step transformation has a few limitations, and cannot be applied to every language expressible in the recent higher-order skeletal semantics. In this section, we informally present the two problematic cases, and discuss potential solutions.

Polymorphism

The version of skeletal semantics used in this first part supports polymorphism, even if we did not detail it in Chapter 3. However, our transformation do not apply to polymorphic hooks, as they present additional challenges. As an example, consider the following function. However, if the matched program type is not "polymorphic enough", then the procedure might fail. In the example above, the type nat is not polymorphic, so we cannot create a coercion from list<a> to nat 2 . A relatively simple solution would be to create a new (polymorphic) configuration program type with a single constructor, and modify the hook to take a single argument. In our example above, it would generate something along the lines of: Then, the transformation of Chapter 4 could apply. However, this approach has two limitations. First, it requires a new type and the modification of every call to the polymorphic function.

Second, since hook calls to the polymorphic hook now explicitly use the new constructor (e.g., firstn Conf(l2, m)), partial results cannot be stored and the transformation systematically requires new constructors (see analysis of Section 4.2.2). User-provided constructors cannot be reused, which complicates the resulting small-step semantics.

Anonymous Functions

The legacy skeletal semantics presented in this first part does not allow the definition of anonymous functions. However, the recent version used in the second part of this document does (see Chapter 8). This creates new problematic cases, as it is unclear how to transform anonymous functions that might call upon evaluation functions.

A first solution would be to precede our transformation with a phase of defunctionalization (see Chapter 9), i.e., transform every anonymous function into object types before applying the strategy of Chapter 4. While it is a correct approach, semantics making heavy use of higherorder functions and anonymous functions would be heavily modified, and the resulting small-step semantics might be hard to follow.

2. Of course, in this toy example, the code could be re-written to pattern-match on the list instead of the integer. This would solve the issue. But in general, this problem cannot always be side-stepped.

Notably, the recent skeletal semantics supports the use of monads, allowing users to factorize common data manipulations. Since monads make use of anonymous functions, reshaping them properly becomes crucial. For instance, consider the following example using the syntax of the recent skeletal semantics (see Chapter 8). Here, let p =@ eval t in s is syntactic sugar for let temp = eval t in bind temp (λp.s).

Evaluating a term Plus(t 1 , t 2 ) still consists into evaluating t 1 and t 2 , recovering two integers, and adding them. The monad type m can hide a number of data structures and additional operations. It can for instance contain a state to evaluate variables, or be able to raise an exception if something goes wrong (e.g., division by zero). Eliminating anonymous functions before applying the small-step transformation would completely reshape the semantics and make the structure of the evaluation of Plus(t 1 , t 2 ) difficult to follow.

A general strategy to transform arbitrary anonymous functions to small-step seems out of reach, however monads use them in a very controlled way, so a strategy with some restrictions might be feasible.

If we try to create, by hand, an equivalent small-step reduction function of type term -> term, we quickly notice that extracting the result from the monad type is not always possible, depending on the type of monad we want. However, an interesting opportunity appears if we preserve the monad around the small-step output type. For the example above, we could write We recover a similar structure to the small-step evaluation of IMP in Example 4.2.5. If t 1 can be computed further, we do so and reconstruct Plus(t ′ 1 , t 2 ), and inject it into the monad type. Otherwise t 1 is an integer, and we can proceed similarly with t 2 . Note that the monadic bind on t 1 can potentially shortcut the evaluation and return something else, just like it is possible in the big-step version of this function.

The main advantages are that we preserve the structure of the semantics while introducing a stepping function that should be equivalent to the initial evaluation function. However, the resulting small-step semantics is very unconventional, as the output type is not the same as the input type. We cannot chain the reduction by repeatedly applying evalss, but instead we should integrate the initial term in the monad and repeatedly apply (λw. bind w evalss).

t ret --→ w 0 bind _ evalss ---------→ w 1 bind _ evalss ---------→ w 2 bind _ evalss ---------→ . . .
Assessing the correctness and feasibility of this approach is left as a future work.

Part II

Meta-Language Transformation

89 Chapter 8

HIGHER-ORDER SKELETAL SEMANTICS

The legacy skeletal semantics, used in Part I of this document, has two shortcomings that make expressing complex semantics tedious. They are resolved with the recent Skel meta-language.

First, associating a behavior to a constructor can only be done when defining a top-level evaluation function. For behaviors depending on the presence of several constructors, the user has to define auxiliary functions, resulting in tangled and unclear formalizations. For instance, the transformation of Section 4 needs to create auxiliary functions named getRet because there is no direct way to associate a behavior to specific terms such as Plus(Ret(v 1 ), e 2 ). This issue also applies to functions that need to pattern-match on several arguments. The problem is solved by strengthening the LetIn structure. We allow pattern recognition on the result of the first computation. For instance, the extraction of a value previously noted let v1 = getRet e1 in ... can now be performed with a skeleton let Ret(v1) = e1 in ..., without the need for an auxiliary function. If the term e1 uses the constructor Ret, then the variable v1 is bound as expected and the computation continues; else, there is no behavior and the evaluation of the skeleton fails.

Combining this pattern recognition with branchings, it is possible to simulate a usual patternmatching. As such, we no longer enforce a mandatory pattern-matching when defining an evaluation function. For instance, Figure 8.1 contains a semantics for Call-by-Value λ-calculus using the recent Skel meta-language and Necro syntax. The skeleton for the eval function starts with a branching, and each branch enforces a specific constructor, allowing us to associate a behavior to each constructor.

Second, the legacy skeletal semantics does not support anonymous functions. There is no function type, and it is not possible to bind a variable to a function. While anonymous functions are not necessary to transcribe usual inference rules for small programming languages, they are frequently used in interpreters and complex languages. The legacy skeletal semantics is notably unpractical for languages making use of monads, such as JavaScript.

The recent skeletal semantics allows for function types and anonymous functions of the form (λp → s), where the input p is a pattern that may enforce a specific constructor, and s is a skeleton. This also enables the formalization of higher-order evaluation functions. For instance, we can define the standard head and map operations on lists as follows:

Syntax

We now introduce the new syntax of skeletal semantics. We write [] for an empty list and (a :: l)

for adding an element a to a list l. We note [a 1 ; . . . ; a n ] for the list (a 1 :: . . . :: a n :: []). We write

l 1 + + l 2 the concatenation of two lists, [a 1 ; . . . ; a n ] + + [b 1 ; . . . ; b m ] ≜ [a 1 ; . . . ; a n ; b 1 ; . . . ; b m ].
The skeletal semantics of a language is composed of:

• a set of unspecified types;

• a set of specified types, with either typed constructors (for abstract datatypes) or typed field names (for record types);

• a set of typed unspecified skelterms;

• a set of typed specified skelterms.

We use the word "skelterm" for terms of the Skel meta-language to avoid confusion with terms of the embedded language (e.g., λ-terms). In the example of Figure 8.1, ident is an unspecified type; lterm a specified type with three constructors, getEnv is an unspecified skelterm, and eval a specified skelterm.

An unspecified skelterm is an identifier representing an abstract object/function not given when defining the language. To execute the language in practice, we would need to determine how to interpret this identifier (see Section 8.2). In the Necro syntax (see Figure 8.1), they are introduced with the keyword val.

We call specified skelterms identifiers associated to an explicit skelterm. We note SpecDecl the mapping from identifiers (i.e., strings) to their corresponding skelterm. They are also introduced with the keyword val, but require a definition.

We let c range over constructors; n and m over natural number; and x and d over identifiers (i.e., strings). For any entity e, we note l e for lists of elements of this entity: for instance, l c represents a list of constructors. The grammar of types (T ), variables (v), patterns (p), skelterms (t), and skeletons (S) is defined as follows.

T ::= TyVar(x) | TyBase(x, l T ) | TyProd(l T ) | TyArrow(T 1 , T 2 ) v ::= VLet(x, T ) | VUnspec(x, T, l T ) | VSpec(x, T, l T ) p ::= PWild(T ) | PVar(x, T ) | PConstr(c, l T , p) | PTuple(l p ) | PRec([(d 1 , l T 1 , p 1 ); . . . ; (d m , l Tm , p m )]) t ::= TVar(v) | TConstr(c, l T , t) | TTuple(l t ) | TNth(t, l T , n) | TFunc(p, S) | TRec(t opt , [(d 1 , l T 1 , t 1 ); . . . ; (d m , l Tm , t m )]) | TField(t, l T , d) t opt ::= Some(t) | None S ::= Branching(T, l S ) | LetIn(p, S 1 , S 2 ) | Apply(t, l t ) | Return(t) | Exists(p, S, T )
Just like the previous version, this new skeletal semantics is strongly and statically typed.

Every piece of syntax is associated with a precise type. TyVar(x) is a variable used to create polymorphic types. TyBase(x, l T ) represents the basic types defined by the user, either specified or unspecified. The type of λ-terms in Figure 8.1 is TyBase("lterm", []). The second argument is used for polymorphism; e.g., a polymorphic list would be typed as TyBase("list", [TyVar("a")]), whereas a list of λ-terms would be typed TyBase("list", [TyBase("lterm", [])]). Then, TyProd(l T ) is a product type corresponding to tuples. Notably, the unit type is TyProd([]), with the unit element being TTuple([]). Finally, TyArrow(T 1 , T 2 ) represents a function type taking an argument of type T 1 and returning a result of type T 2 .

Constructors are associated with a triple indicating the polymorphic variables, the input types, and the name of the output base type. For instance in the example of Figure 8.1, the constructor Lam is associated with ([], TyProd([TyBase("ident", []); TyBase("lterm", [])]), "lterm"), while Cons is mapped to (["a"], TyProd([TyVar("a"); TyBase("list", [TyVar("a")])]), "list").

Skelterms are typed objects, usually representing completed computations. They are recursively composed of constructors, tuples, and records. Records are maps from datafields (d) to skelterms, and are implemented using lists. Base elements are either variables or functions. TNth can be used to extract part of a tuple based on positioning; TField to extract a field from a record (see Section 8.2). Lists of types are needed in some cases to instantiate polymorphism.

Patterns are very similar to terms and used for pattern-matching, with a special constructor PWild to ignore part of a term.

A skeleton represents a computation to perform. It can either be a LetIn structure with pattern-matching (let p = S 1 in S 2 ) chaining two computations, a non-deterministic choice among several computations (Branching(T, l S )), an application of a function to a list of arguments (Apply(t, l t )), or simply returning a skelterm. Branchings require an output type in case the list is empty. Skeletal semantics also allows for the existential quantification of a result we do not know how to compute (Exists(p, S, T )). It can be interpreted as let p = (? : T ) in S.

It is used to transcribe programming languages where inference rules are not syntax-directed, such as transitivity rules for subtyping.

Skeletal typed variables v correspond to the different strings which may appear in a skelterm, pattern, or skeleton. A variable of the language is defined using PVar(x, T ) in a pattern and used with VLet(x, T ). A specified skelterm is referenced using VSpec(x, T, l T ), an unspecified one using VUnspec(x, T, l T ). Once again, lists of types are used to instantiate polymorphism.

As an example, the skelterm of the eval function of Figure 8.1 is of the following form-we only detail the second branch let Var x = l in getEnv (x, s).

SpecDecl("eval") = t t ≜ TFunc(PVar("s", T s ), Return(TFunc(PVar("l", T l ), S)))

S ≜ Branching(T c , [. . . ; LetIn(p, S 1 , S 2 ); . . .]) p ≜ PConstr("Var", [], PVar("x", T x ))

S 1 ≜ Return(TVar(VLet("l", T l ))) S 2 ≜ Apply(TVar(VUnspec("getEnv", T getEnv , [])), [t xs ])
t xs ≜ TTuple([TVar(VLet("x", T x )); TVar(VLet("s", T s ))])

T s ≜ TyBase("env", []) T l ≜ TyBase("lterm", []) T c ≜ TyBase("clos", []) T x ≜ TyBase("ident", [])
T getEnv ≜ TyArrow(TyProd([T x ; T s ]), T c )

Concrete Interpretation

We keep the name concrete interpretation for the standard big-step semantics of the metalanguage Skel. In part I, it simply related input terms to (closed) output terms. Now that terms contain functions, the concrete interpretation can also output closures, i.e., a function paired with the environment it should be evaluated with. We create a new type for the results of this evaluation, called concrete values, or cvalues for short. The grammar of concrete values (r), and environments (Σ) is defined as follows. CVUnspec(x, n, l T , l r ) represents a partially applied unspecified function x, with partial arguments l r (see below for more details). CVRec(l dr ) is a record mapping fields to cvalues.

Once again, the argument l T is used for polymorphism. From now on, we stop specifying the internal types of skeletal semantics, as they use a standard system and are not relevant to the work presented here. We also present the semantics of Skel ignoring types (see below Finally, CVBase(a) is an injection from base values of unspecified types to cvalues. Indeed, the concrete interpretation assumes given an instantiation of unspecified types and unspecified skelterms. For each unspecified type, we expect a set representing its values (e.g., N for "nat", or {⊤; ⊥} for "bool"). In the grammar above, a represents an object in the interpretation of an unspecified type.

For each unspecified skelterm, we require an arity and a function producing a list of possible results. For notation purposes, we group these elements in two main auxiliary functions: Arity of type (string → nat); and UnspecDecl of type (string → cvalue list → cvalue list).

For an unspecified skelterm x, UnspecDecl(x) takes as argument a list of size Arity(x) and outputs a list of possible results.

Example 8.2.1.

To simulate an If/Then/Else construction, we can provide unspecified skelterms "isTrue" and "isFalse" of type ("bool" → ()) with the following definitions:

Arity("isTrue") = 1

UnspecDecl("isTrue")[CVBase(⊤)] = [CVTuple([])]

UnspecDecl("isTrue")[CVBase(⊥)] = [] and similarly for "isFalse". This shows that an empty list result can be interpreted as a failure.

A conditional branching "If v then . . . else . . . " can be simulated with branchings, as with the legacy skeletal semantics (Section 3. Most of the rules are self-explanatory. For instance, evaluating a skeleton LetIn(p, S 1 , S 2 ) under Σ corresponds to evaluating S 1 under Σ to get r ′ , performing a pattern-matching between p and r ′ to expand the environment to Σ ′ , and finally evaluating S 2 under Σ ′ . Some premises might not be doable, for instance the pattern-matching between p and r ′ could fail. In this case, the rule does not apply and the skeleton cannot be evaluated.

For records, we usually simply evaluate each subterm. An optional record can be given to serve as a template, and it would then be extended with new fields, similarly to the OCaml notation {rc with d1 = t1 ; ... ; dn = tn}. Note that the new datafields and values are added to the beginning of the list, and thus overwrite previous definitions on the initial record.

Environment lookup (Σ(x) = r) corresponds to finding the first pair of the form (x, r) in Σ.

Pattern-matching (Σ+{p → r} ⇓ p Σ ′ ) corresponds to extending Σ into Σ ′ with pairs (x i , r i ), and is only valid if p and r have (recursively) tuples of the same arity and use the same constructors.

Patterns can also read fields from records.

The evaluation of unspecified skelterms is probably the least intuitive, as the concrete interpretation allows for partially applied unspecified functions. If we encounter an unspecified skelterm of arity 0, we immediately apply its interpretation. Otherwise, (Arity(x) = n + 1), we create a concrete value CVUnspec(x, n, []), keeping track of the missing number of arguments (n + 1) and the already given arguments (starting with an empty list).

The behavior of application for unspecified skelterms depends on the number of arguments.

If there is none, we simplify as expected. Else we have (m + 1) arguments. If we have enough new arguments (m + 1 ≥ n + 1, simplified to m ≥ n in Figure 8.3) we apply the interpretation of the skelterm. Otherwise (m < n), we expand the list of partial arguments and diminish the counter of missing arguments.

A few rules of Figure 8.2 are non-deterministic, as we sometimes pick an element in a list.

This happens when choosing a branch in a branching, which can be seen as an internal source of non-determinism; and when selecting a result from the interpretation of unspecified skelterms, which can be seen as external non-determinism. The use of the Exists(p, S) constructor is also fundamentally non-deterministic, but a derivation for it can only be provided by hand.

Note that there is no rule for generating or consuming base values CVBase(a) of unspecified types, as they can only be handled by unspecified skelterms.

Interpretation of Skelterms:

Σ(x) = r Σ ⊢ TVar(VLet(x)) ⇓ t r Arity(x) = 0 r ∈ UnspecDecl(x)[] Σ ⊢ TVar(VUnspec(x)) ⇓ t r Arity(x) = n + 1 Σ ⊢ TVar(VUnspec(x)) ⇓ t CVUnspec(x, n, []) SpecDecl(x) = t [] ⊢ t ⇓ t r Σ ⊢ TVar(VSpec(x)) ⇓ t r Σ ⊢ t ⇓ t r Σ ⊢ TConstr(c, t) ⇓ t CVConstr(c, r) ∀i, Σ ⊢ t i ⇓ t r i Σ ⊢ TTuple([t 1 ; . . . ; t n ]) ⇓ t CVTuple([r 1 ; . . . ; r n ]) Σ ⊢ t ⇓ t TTuple([r 0 ; . . . ; r m ]) n ≤ m Σ ⊢ TNth(t, n) ⇓ t r n Σ ⊢ TFunc(p, S) ⇓ t CVClos(Σ, p, S) Σ ⊢ t ⇓ t CVRec(l dr ) l dr (d) = r Σ ⊢ TField(t, d) ⇓ t r ∀i, Σ ⊢ t i ⇓ t r i Σ ⊢ TRec(None, [(d 1 , t 1 ); . . . ; (d m , t m )]) ⇓ t CVRec([(d 1 , r 1 ); . . . ; (d m , r m )]) Σ ⊢ t ⇓ t CVRec(l dr ) Σ ⊢ TRec(None, l ′ dt ) ⇓ t CVRec(l ′ dr ) Σ ⊢ TRec(Some(t), l ′ dt ) ⇓ t CVRec(l ′ dr + + l dr )
Interpretation of Skeletons:

S ∈ l Σ ⊢ S ⇓ s r Σ ⊢ Branching(l) ⇓ s r Σ ⊢ t ⇓ t r Σ ⊢ Return(t) ⇓ s r Σ ⊢ t ⇓ t r 0 ∀i, Σ ⊢ t i ⇓ t r i r 0 $ [r 1 ; . . . ; r n ] ⇓ a r Σ ⊢ Apply(t, [t 1 ; . . . ; t n ]) ⇓ s r Σ ⊢ S 1 ⇓ s r ′ Σ + {p → r ′ } ⇓ p Σ ′ Σ ′ ⊢ S 2 ⇓ s r Σ ⊢ LetIn(p, S 1 , S 2 ) ⇓ s r Σ + {p → r ′ } ⇓ p Σ ′ Σ ′ ⊢ S ⇓ s r Σ ⊢ Exists(p, S) ⇓ s r
for any r ′ Environment Lookup:

((x, r) :: Σ)(x) = r x ̸ = y Σ(x) = r ((y, r) :: Σ)(x) = r Interpretation of Application: r $ [] ⇓ a r Σ + {p → r 0 } ⇓ p Σ ′ Σ ′ ⊢ S ⇓ s r 1 r 1 $ l r ⇓ a r CVClos(Σ, p, S) $ (r 0 :: l r ) ⇓ a r m ≥ n r ′ ∈ UnspecDecl(x)(l + + [r 0 ; . . . ; r n ]) r ′ $ [r n+1 ; . . . ; r m ] ⇓ a r CVUnspec(x, n, l) $ [r 0 ; . . . ; r m ] ⇓ a r m < n n ′ = n -(m + 1) l ′ = l + + [r 0 ; . . . ; r m ] CVUnspec(x, n, l) $ [r 0 ; . . . ; r m ] ⇓ a CVUnspec(x, n ′ , l ′ )
Pattern-Matching:

Σ + {PWild → r} ⇓ p Σ Σ + {PVar(x) → r} ⇓ p (x, r) :: Σ Σ + {p → r} ⇓ p Σ ′ Σ + {PConstr(c, p) → CVConstr(c, r)} ⇓ p Σ ′ Σ + {PTuple([]) → CVTuple([])} ⇓ p Σ Σ + {p → r} ⇓ p Σ ′ Σ ′ + {PTuple(l p ) → CVTuple(l r )} ⇓ p Σ ′′ Σ + {PTuple(p :: l p ) → CVTuple(r :: l r )} ⇓ p Σ ′′ Σ + {PRec([]) → CVRec(l dr )} ⇓ p Σ l dr (d) = r Σ + {p → r} ⇓ p Σ ′ Σ ′ + {PRec(l dp ) → CVRec(l dr )} ⇓ p Σ ′′ Σ + {PRec((d, p) :: l dp ) → CVRec(l dr )} ⇓ p Σ ′′

PRIMER ON FUNCTIONAL CORRESPONDENCE

Functional correspondence [START_REF] Mads | A functional correspondence between evaluators and abstract machines[END_REF] is a systematic approach for converting a big-step semantics into an abstract machine. It has been manually applied to many languages with different features [START_REF] Biernacka | An Abstract Machine for Strong Call by Value[END_REF][START_REF] Mads | A functional correspondence between call-by-need evaluators and lazy abstract machines[END_REF][START_REF] Piróg | A systematic derivation of the STG machine verified in Coq[END_REF][START_REF] Biernacki | From Interpreter to Logic Engine by Defunctionalization[END_REF][START_REF] Biernacka | An Operational Foundation for Delimited Continuations in the CPS Hierarchy[END_REF][START_REF] Mads | A functional correspondence between monadic evaluators and abstract machines for languages with computational effects[END_REF][START_REF] Danvy | Inter-deriving semantic artifacts for object-oriented programming[END_REF][START_REF] Jedynak | An operational foundation for the tactic language of Coq[END_REF], showing its robustness and usefulness. In Chapters 10 and 11, we use it to rephrase the semantics of Skel-the meta-language of skeletal semantics-into non-deterministic and deterministic abstract machines.

Starting from an interpreter, the transformation combines several known techniques, notably CPS translation [START_REF] Plotkin | Call-by-Name, Call-by-Value and the lambda-Calculus[END_REF] (Section 9.2) and defunctionalization [START_REF] Reynolds | Definitional interpreters for higher-order programming languages[END_REF] (Section 9.3) to progressively generate an abstract machine (Section 9.4).

The strategy can also be reversed to transform a large range of abstract machines into big-step semantics, under some conditions [START_REF] Mads | A functional correspondence between evaluators and abstract machines[END_REF][START_REF] Danvy | A Rational Deconstruction of Landin's SECD Machine with the J Operator[END_REF][START_REF] Biernacki | A Dynamic Continuation-Passing Style for Dynamic Delimited Continuations[END_REF]. In this overview, we focus only on the transformation from big-step semantics to abstract machines, as it is the only direction we use in this document.

Throughout this chapter, we use the arithmetic expressions from Chapter 2 as a small running example. For readability, the pseudo-code is written in an OCaml-like syntax. The full transformation on the complete IMP example is also available in Appendix A.

Rewrite the Semantics in Pseudo-Code

Functional correspondence was designed to bridge the gap between abstract machines and interpreters, i.e., semantics written either in pseudo-code or in a functional meta-language. If the input interpreter uses higher-order functions, the first step of functional correspondence would be to get rid of anonymous functions with free variables. This can be done by defunctionalization (as in Section 9.3 below).

In this document, the starting point is instead a big-step semantics defined with inference rules. We therefore rewrite the input semantics into an interpreter using pseudo-code. As an example, we transform the following toy language.

a ::= x | n | Plus(a 1 , a 2 ) ⟨σ, x⟩ ⇓ a σ(x) ⟨σ, n⟩ ⇓ a n ⟨σ, a 1 ⟩ ⇓ a n 1 ⟨σ, a 2 ⟩ ⇓ a n 2 ⟨σ, Plus(a 1 , a 2 )⟩ ⇓ a n 1 + n 2
For each type defined in the syntax, we similarly define the same type in pseudo-code. Since we do not explicitly define variables (x), integers (n), and states (σ), we simply assume the corresponding types exists in the pseudo-code. Since we create temporary pseudo-code, only used as a transition between big-step and abstract machine, we can invent names for the different basic operations. For instance, for a state s, we note lookup s x for looking up the associated integer.

We obtain a pseudo-interpreter corresponding to our big-step semantics. Since the initial inference rules do not manipulate functions, the pseudo-code does not use higher-order functions, and we do not need to perform any additional manipulation.

CPS Transform

The next phase of functional correspondence is to transform all evaluation functions into Continuation Passing Style [START_REF] Plotkin | Call-by-Name, Call-by-Value and the lambda-Calculus[END_REF].

This first change makes the order of operations more explicit by creating tail-recursive function calls. At every program point, the rest of the computation is expressed as a function called a continuation. The CPS transformation modifies every evaluation function to take such a continuation as an additional argument. In practice, we apply two modifications throughout the pseudo-code.

First, at every return point of every evaluation function, instead of directly returning the obtained result, we call the current continuation with the result as argument. For instance, instead of returning an integer n, we make the function call k n.

Second, we make use of continuations to prevent nested computations. Every function call of the form let n = eval .. in .. is changed into a final call eval .. (fun n -> ..). The chaining in the computation is replaced by a continuation, containing what is computed after the in keyword.

For our running example, we obtain the following pseudo-code.

let eval s a (k : nat -> nat) : nat = match a with | Var(x) -> k (lookup s x) | Nat(n) -> k n | Plus(a1, a2) -> eval s a1 (fun n1 -> eval s a2 (fun n2 -> k (n1+n2)))
In the general case, the typing should be let eval s a (k : nat -> 't) : 't = ..., i.e., continuations can be made polymorphic. Since we do not need it, we output a nat for simplicity.

This creates a very directed semantics where every behavior-ignoring basic operations like lookup and addition-is simply a final call to another function, either a continuation or an evaluation function.

Defunctionalization

The next main phase of functional correspondence is to defunctionalize the continuations generated previously. Abstract machines manipulate explicit objects, and not functions nor closures.

As such, we need to reify-i.e., transform into objects-the anonymous functions used as continuations.

For each function type among the anonymous functions in the pseudo-code, we create a new associated object type. For our example, we only have a single function type used by all continuations: nat -> nat. We create a corresponding object type named kt.

Then, we create a new constructor (of the associated type) for each anonymous function. The arguments of the constructor are the free variables of the anonymous function. In our example, we have two anonymous functions, the second being (fun n2 -> k (n1+n2)). Since its free variables are n1 and k, we create an associated constructor KPlus2 of nat * kt. Similarly, the first anonymous function has three free variables (s, a2, and k), and the corresponding constructor has three arguments.

The goal is to replace each anonymous function with a corresponding object. However, continuations can be applied (e.g., k n) while objects cannot. We obtain an equivalent semantics without anonymous functions. Continuations are now objects, even if they still indirectly contain the rest of the computation to perform. Applying a continuation now requires an explicit call to the corresponding dispatch function.

Additionally, we create a continuation constructor KID without arguments. It can be seen as an empty continuation, or the identity function (λn.n), and is used to start and stop a computation (see next section). In disp, there is no dispatch rule for KID, as there is no computation left to perform and we expect the abstract machine to stop.

Abstract Machine

The pseudo-code at the end of the previous section is almost an abstract machine. The only difference is that we want to stage a computation into several tiny steps, so we need to introduce a pause at every function call. We also stop using pseudo-code, and introduce more standard notations.

Types can easily be transformed into usual syntactic definitions.

a ::= x | n | Plus(a 1 , a 2 ) k ::= K id | KPlus1(σ, a, k) | KPlus2(n 1 , k)
For each function of the pseudo-code, we create an evaluation mode for our new abstract machine. For each evaluation mode, the arguments of the machine states are simply the arguments of the corresponding function. For our example, we have machine states of the form ⟨σ, a, k⟩ e for the function eval, and of the form ⟨k, n⟩ k for the function disp. I.e., a machine state corresponds to the computation of one of the functions of the pseudo-code.

Then, the steps of the abstract machine follow the different cases of the pseudo-code.

For each behavior of the pseudo-code, we create a step: the left-hand side state corresponds to the evaluation of the input arguments; the right-hand side state corresponds to the final function call of the behavior. For instance, running the eval function with arguments s Nat(n) k makes a function call to disp k n; this is translated into a step ⟨σ, n, k⟩ e → ⟨k, n⟩ k .

For arithmetic expressions, we end up with five steps.

⟨σ, x, k⟩ e → ⟨k, σ(x)⟩ k ⟨σ, n, k⟩ e → ⟨k, n⟩ k ⟨σ, Plus(a 1 , a 2 ), k⟩ e → ⟨σ, a 1 , KPlus1(σ, a 2 , k)⟩ e ⟨KPlus1(σ, a, k), n⟩ k → ⟨σ, a, KPlus2(n, k)⟩ e ⟨KPlus2(n 1 , k), n⟩ k → ⟨k, (n 1 + n)⟩ k
Note that there is no step for the evaluation of the basic continuation K id , as there is no corresponding behavior in the pseudo-code. This continuation is used as a base for starting and stopping a computation, like the continuation • in the example of Section 2.5. To evaluate a configuration ⟨s, a⟩ with this abstract machine, we would create an initial machine state ⟨s, a, K id ⟩ e and repeatedly reduce. The machine would stop at a state of the form ⟨K id , n⟩ k , indicating that the result is n.

The derivation and the final abstract machine for the complete IMP example are available in Appendix A. The non-determinism of the evaluation of a branching is reflected by the pick function which behaves like an oracle. It is able to choose, within a list, an appropriate element that will make the whole evaluation succeed and output a result.

Similarly, we use the same function for unspecified skelterms. As presented before, the evaluation of such a skelterm depends on its arity. If it does not require arguments, we immediately call its interpretation, and then arbitrarily select a result from the obtained list, using pick.

Otherwise, we create a partially applied skelterm initialized with zero arguments.

let eval_trm (t : skelterm) (e : env) : cvalue = match t with We also define evaluation functions for lists of skelterms and lists of pattern-matchings. The former corresponds to mapping the evaluation function for skelterms over the list, while the latter behaves like a fold over the list, in accordance with the rules of Figure 8.2.

| TVar (v) -> match v with | VLet (x) -> lookup e x | VSpec (h) -> eval_trm (specdecl h) [] | VUnspec (f) -> match arity f with | Z -> let rl = unspecdecl f [] in (*
The full initial pseudo-code is available in Appendix D.1.1.

CPS-Transform

We apply a CPS transformation to the main evaluation functions: eval_trm, eval_sk, eval_pat, lookup, etc. We do not modify the parametric functions (pick / specdecl / arity / unspecdecl) nor the basic functions such as the concatenation of two lists. The arity zero case is an example where the result is passed to the continuation. Calls to unmodified functions, such as pick, are left unchanged. A call to an evaluation function f is changed so that we create a continuation for the rest of the code and make a single tail-call to f with it. When several of such calls are chained, we build nested continuations, like for the constructor LetIn: the continuation after evaluating sk1 consists of performing the patternmatching with a continuation evaluating sk2.

let eval_trm t e (k : cvalue -> cvalue) : cvalue = match t with | TVar (v) -> match v with | VLet (x) -> lookup e x k | VSpec (h) -> eval_trm (specdecl h) [] k | VUnspec (f) -> match arity f with | Z -> let r = pick (unspecdecl f []) in k r | S m -> k (CVUnspec (f, m, [])) | TConstr (c, t') -> eval_trm t' e (fun r -> k (CVConstr (c, r)) ) | ...
The full pseudo-code at this point is available in Appendix D.1.2.

Defunctionalization

For each anonymous function generated in Section 10.2, we create a fresh constructor, whose arguments are the free variables of the function. In the code, we replace each anonymous continuation by its new corresponding constructor. Therefore, we also create new types klt, ket, and kdt corresponding to continuations expecting respectively lists of cvalues, environments, and record contents. These three types are also given their own identity continuation: kl id , ke id , and kd id .

type ket = | KEID | KELet of skeleton * krt | ... let disp_ke (k : ket) (e : env) : cvalue = match k with | KELet (sk, k') -> eval_sk sk e k' | ... ⟨Branching(l), Σ, k⟩ sk → ⟨S, Σ, k⟩ sk for (S ∈ l) ⟨LetIn(p, S 1 , S 2 ), Σ, k⟩ sk → ⟨S 1 , Σ, KRLet(p, S 2 , Σ, k)⟩ sk • • • → • • • ⟨kr id , r⟩ kr ̸ → (* end of computation *) ⟨KRLet(p, S, Σ, k), r⟩ kr → ⟨p, r, Σ, KELet(S, k)⟩ pat • • • → • • • ⟨KELet(S, k), Σ⟩ ke → ⟨S, Σ, k⟩ sk • • • → • • •

Abstract Machine

We can now generate the non-deterministic abstract machine. Each evaluation and dispatch function of the pseudo-interpreter becomes a mode of the abstract machine, with the same arguments as the ones of the function. For instance, the function eval_sk S Σ k is turned into the state ⟨S, Σ, k⟩ sk , while disp_kr k r corresponds to ⟨k, r⟩ kr .

Each path in the code of Section 10.3 produces a step of the abstract machine, where the resulting state corresponds to the tail-call. The parametric functions (such as specdecl and pick), can be translated as guarding conditions of the abstract machine. For instance, the LetIn case of the pseudo-code generates the following step.

⟨LetIn(p, S 1 , S 2 ), Σ, k⟩ sk → ⟨S 1 , Σ, KRLet(p, S 2 , Σ, k)⟩ sk
This correctly produces a stepping relation. Some of the steps of the NDAM are given in The initial states of the NDAM correspond to injections ⟨t, [], kr id ⟩ trm and ⟨S, [], kr id ⟩ sk for evaluating respectively a skelterm t and a skeleton S. Running this abstract machine in practice would require an oracle, as the rule ⟨Branching([S 1 , . . . , S n ]), Σ, k⟩ sk → ⟨S i , Σ, k⟩ sk for branchings is non-deterministic. If a wrong branch is selected, the abstract machine might fail to produce a result. If the NDAM reaches a final state of the form ⟨kr id , r⟩ kr , then r is a valid result of the evaluation. For all states a and b, we write a → * b for the reflexive and transitive closure of the stepping relation of the abstract machine, and a → n b for a sequence of n steps with n ∈ N.

Certification

The NDAM is formalized in Coq in the file Concrete_ndam.v (see the implementation [START_REF] Ambal | Implementation of "Certified Abstract Machines for Skeletal Semantics[END_REF]), and we prove this NDAM to be sound and complete with respect to the concrete interpretation. The Coq proof is done at the meta level (parametric in the skeletal semantics), as such it is valid independently from the language we are interested in (e.g., λ-calculus).

Intuitively, each big-step relation of the concrete interpretation (see Figure 8.2) corresponds to an evaluation mode of the NDAM. For instance, we have:

Σ ⊢ S ⇓ s r iff ⟨S, Σ, kr id ⟩ sk → * ⟨kr id , r⟩ kr
We call kr id , ke id , kl id , and kd id the basic continuations of the abstract machine. States stuck evaluating a basic continuation, e.g., ⟨kr id , r⟩ kr , are called final states.

Since the NDAM also manipulates reified continuations, we need a few results to reshape them when appropriate. We first define continuation composition k[k ′ ]. Given two continuations k and k ′ , we write k[k ′ ] for the substitution of the basic continuation inside k by k ′ , defined as follows:

kr id [k ′ ] ≜ k ′ KRLet(p, S, Σ, k)[k ′ ] ≜ KRLet(p, S, Σ, k[k ′ ]) . . . ≜ . . .
This continuation composition can be naturally extended to machine states, e.g., ⟨S, Σ,

k⟩ sk [k ′ ] ≜ ⟨S, Σ, k[k ′ ]⟩ sk .
Because the NDAM never pattern-matches on basic continuations, machine steps are preserved by continuation composition.

Lemma 10.5.1. For all a, b, and k, a

→ b implies a[k] → b[k].
This lemma is sufficient to prove the completeness part. Henceforth, we state lemmas and theorems for the skeleton mode of the machine, but they can be stated similarly for the other modes.

Theorem 10.5.2. For all S, Σ, and r, if Σ ⊢ S ⇓ s r, then ⟨S, Σ, kr id ⟩ sk → * ⟨kr id , r⟩ kr .

From the concrete interpretation to the NDAM, we proceed by structural induction on the inductive properties of the concrete interpretation (Figure 8.2), and simply apply the induction hypothesis and merge sequences, using Lemma 10.5.1.

We proceed by strong induction on the length of the sequence of steps, and make use of Lemma 10.5.3.

Sketch.

For instance, for the constructor LetIn, we start with:

⟨LetIn(p, S 1 , S 2 ), Σ, kr id ⟩ sk → * ⟨kr id , r⟩ kr .

Using the definition of the NDAM (Figure 10.1) and Lemma 10.5.3 several times, we can cut the sequence into the following pieces:

• ⟨S 1 , Σ, kr id ⟩ sk → * ⟨kr id , r ′ ⟩ kr • ⟨p, r ′ , Σ, ke id ⟩ pat → * ⟨ke id , Σ ′ ⟩ ke • ⟨S 2 , Σ ′ , kr id ⟩ sk → * ⟨kr id , r⟩ kr
Each piece is strictly shorter than the initial sequence, so we can apply the induction hypothesis on each of them. We obtain the premises of the LetIn concrete interpretation (cf. The other source of non-determinism is the evaluation of unspecified skelterms, which returns a list of possible results (see Figure 8.2). In this case, we create a new evaluation function select_list which follows the same principle as for branchings: it sequentially tries the elements of the list, and otherwise calls the failure continuation.

let select_list rl (k : cvalue -> (() -> cvalue) -> cvalue) (fk : () -> cvalue) : cvalue = match rl with | [] -> fk () | r::l -> k r (fun _ -> select_list l k fk)
Also, we complete the pattern-matchings of the pseudo-code to make them total, and backtrack in the problematic cases. Initially, the pseudo-code follows the rules of Figure 8.2 and does not cover cases that cannot evaluate. For instance, the rules for looking up a variable in an environment assumes the environment to have at least one entry:

def lookup e x : cvalue = match e with | (y,r)::e2 -> if x=y then r else lookup e2 x

Here, to cover all possible behaviors, we add a case for when the environment is empty. We add similar backtracking cases at several points throughout the pseudo-code.

def lookup e x k fk : cvalue = match e with | [] -> fk () | (y,r)::e2 -> if x=y then k r fk else lookup e2 x k fk Lastly, we need to pass the current failure continuation as an argument of the success continuation k. The reason is that a computation can seemingly succeed at first and fail later on;

we would then need to backtrack to checkpoints unknown to k. This is obvious for functions such as select_list: it succeeds in selecting an element of the list, but the continuation might fail later.

The extra failure continuation does not fundamentally change how deterministic constructors are CPS-transformed, as we can see with the resulting code for LetIn in the above eval_sk function: we just need to pass along the failure continuation, and be mindful of the new type of success continuations.

The full pseudo-code after CPS transformation is available in Appendix D.2.1.

Defunctionalization and Abstract Machine

On top of the types krt, klt, ket, and kdt of Section 10. As previously, the new constructors replace the anonymous functions, and we add a call to the dispatch function at every backtrack point. Transforming the defunctionalized evaluator produces a deterministic abstract machine, as each machine state is either stuck or reduces to exactly one machine state. Compared to the non-deterministic machine of Section 10.4, the states carry an extra argument corresponding to the failure continuation.

Defunctionalization and Abstract

Machine ⟨Branching([]), Σ, k, f ⟩ sk → ⟨f ⟩ fk ⟨Branching(S :: l), Σ, k, f ⟩ sk → ⟨S, Σ, k, FSK(Branching(l), Σ, k, f )⟩ sk ⟨LetIn(p, S 1 , S 2 ), Σ, k, f ⟩ sk → ⟨S 1 , Σ, KRLet(p, S 2 , Σ, k), f ⟩ sk • • • → • • • ⟨[], k, f ⟩ lst → ⟨f ⟩ fk ⟨r :: l, k, f ⟩ lst → ⟨k, r, FList(l, k, f )⟩ kr • • • → • • • ⟨kr id , r, f ⟩ kr ̸ → (* end of computation *) ⟨KRLet(p, S, Σ, k), r, f ⟩ kr → ⟨p, r, Σ, KELet(S, k), f ⟩ pat • • • → • • • ⟨KELet(S, k), Σ, f ⟩ ke → ⟨S, Σ, k, f ⟩ sk • • • → • • • ⟨FEmpty⟩ fk ̸ → (* failure *) ⟨FSK(S, Σ, k, f )⟩ fk → ⟨S, Σ, k, f ⟩ sk ⟨FList(l, k, f )⟩ fk → ⟨l, k, f ⟩ lst
The deterministic machine has two additional modes lst and fk, which correspond to the functions (select_list and disp_fk): we present their steps in Figure 11.1. As expected, the lst mode tries the continuation on each element of the list, and triggers the backtracking mode fk on the empty list; it is also invoked on an empty branching. The fk mode then restores the backtracking checkpoint, unless the failure continuation is empty, in which case the machine stops.

We initialize the machine with either ⟨t, [], kr id , FEmpty⟩ trm to evaluate a skelterm t or ⟨S, [], kr id , FEmpty⟩ sk for a skeleton S. There are three possible outcomes:

• the machine gets stuck at ⟨kr id , r, f ⟩ kr , which means r is a result (there might be other correct results, but we stop at the first);

• it gets stuck at ⟨FEmpty⟩ fk , which means all possible branches have been tried and there is no result;

• the evaluation does not terminate, as the abstract machine can loop on an infinite branch (there might be correct results on other branches).

The complete deterministic abstract machine is available in Appendix D.2.3.

Certification

The deterministic Abstract Machine (AM) is formalized in Coq in the file Concrete_am.v (see the implementation [START_REF] Ambal | Implementation of "Certified Abstract Machines for Skeletal Semantics[END_REF]), and once again the certification is independent from the skeletal semantics (language) we are interested in.

We prove in Coq that the AM is sound with respect to the NDAM: if the AM finds a result, then the NDAM can also find the same result. Because the NDAM is sound with respect to the concrete interpretation, so is the AM. However it is not complete: the AM can find at most one of the results, and may loop in an infinite branch even if there is a valid result elsewhere.

To avoid confusion, the stepping relations of the abstract machines are written (→ AM ) and (→ ND ) in this section. We note x for a sequence (x 1 , . . . , x n ), ⟨ x⟩ m for a NDAM state of mode m, and ⟨ x, f ⟩ m for an AM state of mode m-failure continuations are always the last argument in the deterministic case.

Unlike in Section 10.5, both semantics use success continuations so there is no need for lemmas to manipulate them. However, only the AM uses failure continuations, so we do need a few results to handle them.

Firstly, we define failure continuation composition f [f ′ ] which replaces the empty failure continuation in f by f ′ .

FEmpty[f ′ ] ≜ f ′ FSK(S, Σ, k, f )[f ′ ] ≜ FSK(S, Σ, k, f [f ′ ]) FList(l, k, f )[f ′ ] ≜ FList(l, k, f [f ′ ]) We extend it to AM states so that ⟨ x, f ⟩ m [f ′ ] ≜ ⟨ x, f [f ′ ]⟩ m .
As previously, steps hold after composition:

Lemma 11.3.1. For all a, b, and f , a → AM b implies a[f ] → AM b[f ].
Secondly, we prove another lemma to discard failure continuations. If a sequence never uses it, we can remove it from both the head and tail states. Otherwise, we can split this sequence at the point where it is called, and the first part can be written without using the failure continuation.

, if ⟨ x, f ⟩ m → n AM b then either: • there exists b ′ such that ⟨ x, FEmpty⟩ m → n AM b ′ and b = b ′ [f ], or • there exist n 1 and n 2 such that ⟨ x, FEmpty⟩ m → n 1 AM ⟨FEmpty⟩ fk , ⟨f ⟩ fk → n 2 AM b, and n = n 1 + n 2 .
The proof is done by strong induction on the length of the sequence of steps, and uses a few basic results about composition, such as associativity.

Lastly, we can pose our main theorem. It states that the AM is sound with respect to the NDAM.

Theorem 11.3.3. For all l, k, r, and f , if ⟨l, k, FEmpty⟩ lst → * AM ⟨kr id , r, f ⟩ kr then there exists r ′ ∈ l such that ⟨k, r ′ ⟩ kr → * ND ⟨kr id , r⟩ kr . For all NDAM mode m (i.e., other than lst and fk), for all ⟨ x, FEmpty⟩ m , r, and f , if

⟨ x, FEmpty⟩ m → * AM ⟨kr id , r, f ⟩ kr then ⟨ x⟩ m → *
ND ⟨kr id , r⟩ kr . In particular, for the mode evaluating skeletons sk, the following holds.

Corollary 11.3.4. For all S, Σ, r, and f , if ⟨S, Σ, kr id , FEmpty⟩ sk → * AM ⟨kr id , r, f ⟩ kr , then ⟨S, Σ, kr id ⟩ sk → * ND ⟨kr id , r⟩ kr . The mode lst is treated differently in the theorem because there is no corresponding mode in the NDAM. It amounts to choosing the first cvalue r ′ ∈ l that would produce a result. In the NDAM, we would have an oracle picking the right element of l to evaluate.

The proof is done by strong induction on the sequence of the AM. We follow the AM sequence, and most steps correspond to similar steps in the NDAM. We restrict ourselves to sequences with an empty failure continuation to make use of the induction hypothesis. When the AM would create a checkpoint, we use Lemma 11.3.2; a case disjunction on the result allows us to pick the correct branch for the NDAM and keep an empty failure continuation.

Remark. In the proof of Theorem 11.3.3, an AM state with an empty failure continuation and the corresponding NDAM state have exactly the same success continuation. This is why we no longer need the continuation composition of Section 10.5 (and the associated lemmas) to handle them.

Finally, since the NDAM is sound w.r.t. the concrete interpretation, so is the AM. Theorem 11.3.5. For all S, Σ, r, and f , if

⟨S, Σ, kr id , FEmpty⟩ sk → * AM ⟨kr id , r, f ⟩ kr , then Σ ⊢ S ⇓ s r.
Proof. From Theorems 11.3.3 and 10.5.4.

Chapter 12

CERTIFIED INTERPRETER

We show how to use the deterministic abstract machine of Section 11 to automatically generate a certified OCaml interpreter from a skeletal description of a language.

As stated before, the two abstract machines have been implemented and certified in Coq.

Since the NDAM is not computable, it is coded as a relation:

Inductive step : state -> state -> Prop
However, the AM is computable and coded as a partial function. The only states not being mapped are the final states (i.e., blocked on kr id /kl id /ke id or FEmpty): From these definitions, using Coq extraction to OCaml [START_REF] Letouzey | A New Extraction for Coq[END_REF], we generate an executable version of the deterministic abstract machine. This extraction only has do be done once and it is independent of the user-defined skeletal semantics. The output has been slightly reorganized to make better use of OCaml modules.

Definition
At this point, the generated OCaml functions can simulate the evaluation of skeletons. This generic interpreter still needs to be instantiated with an actual OCaml representation of a skeletal semantics. To this end, we use the Necro tool to deeply embed the semantics into Coq, which we then extract to OCaml. I.e., the user definition is translated successively into a Coq datatype and an OCaml datatype. A small script is provided to perform this translation and automatically generate an OCaml module to instantiate the generic abstract machine.

As explained before, the Coq deep embedding-and thus the OCaml module-is parametric in the representation of unspecified types and skelterms (e.g., ident, env, extEnv, and getEnv for the λ-calculus of Chapter 8 and Figure 12.1 below). To exploit this module, a user then needs to provide OCaml types and functions for them before having access to the AM functions, notably evalfuel to launch an evaluation.

The soundness proofs of Sections 10.5 and 11.3 are parameterized by the used skeletal semantics. As such, they are valid for any language. So this OCaml interpreter, extracted from the AM and specialized with a language, is sound with respect to the initial concrete interpretation of Section 8.2. If an execution of evalfuel produces a result, then this result is a correct behavior of the skeletal semantics. Once again, if the interpreter does not produce a result, we have no guarantees: the interpreter might need more fuel, it might be stuck in an infinite loop, or there might be no correct result at all.

The advantage of working at the meta-level, i.e., proving correction once and for all languages, has a drawback: the execution happens in the meta-language, namely Skel, while a user may prefer working at the level of the language, e.g., λ-terms. This deep embedding requires the user to understand the Skel meta-language. For instance, the user-defined OCaml functions representing unspecified skelterms (filters) manipulate concrete values. Using OCaml macros can alleviate notations, but cannot resolve fully this gap. Furthermore, an abstract machine for the language itself would be more efficient than the abstract machine for the meta-language.

As examples, we instantiated this meta-interpreter with different languages. The main ones are an imperative language with mutable state, and an extended lambda-calculus with features (pairs, fix-point recursion, etc.). The specialized interpreters, as well as the rest of this work, are available online [START_REF] Ambal | Implementation of "Certified Abstract Machines for Skeletal Semantics[END_REF].

As an example, we detail the process to create an interpreter for the λ-calculus language defined in Chapter 8, copied below. The first step for the user is to write its own language, e.g., the λ-calculus semantics above Part II, Chapter 12 -Certified Interpreter (Figure 12.1). Once the language is formalized in skeletal semantics, our tool can automatically generate an OCaml module. There is no need to open it as the content is equivalent to the Skel definition, and contains for instance the following lines. let base_type = "clos" :: ("env" :: ("ident" :: ("lterm" :: []))) let constructor = "App" :: ("Lam" :: ("Var" :: ("Clos" :: []))) let unspec_term = "extEnv" :: ("getEnv" :: []) let spec_term = "eval" :: [] ... As can be seen, this file is a crude translation from Skel to Coq to OCaml, and as such is not meant to be easily readable by humans. Every piece of the language is transformed into an OCaml value, including types, rules, skeletons, etc. This module is almost enough to instantiate the generic deterministic abstract machine presented in Section 11.2. Now, we need to create a new OCaml file to provide the missing pieces. For unspecified types, we create an OCaml type representing basic concrete values (see Section 8.2). For the λ-calculus, we need to cover the two unspecified types ident and env. Here, for extEnv, we check that the function is given a single argument, which is a triple composed of an environment, a variable name, and a cvalue to write. If something is off, the implementation fails by returning an empty list, which would cause the abstract machine to backtrack. Else, we return an extended environment, by adding the pair (x,v) to the beginning of the list. We also write a similar function for getEnv. Now we have all the definitions required to instantiate the abstract machine. It can then be used to execute programs. We can for instance write the following OCaml code. In this example, sk is the skeleton Apply("eval", [s; t]), with some required type annotations.

The functions inject_skel and evalfuel are already defined in the abstract machine module: the former creates an AM state from a skeleton and an environment, while the latter launches the computation on a machine state. In the rest of the program, r contains the expected result, i.e., an option, containing the closure λy.y in an empty environment.

In this example, 614 is the smallest amount of fuel needed to perform two beta-reductions and return a result. The computation is noticeably inefficient as we simulate the evaluation of the meta-language Skel. For instance, even evaluating a λ-abstraction into a λ-closure, by following the skeleton let Lam (x, t) = l in Clos (x, t, s), takes dozens of steps. The substitutions to perform (l at first, and (x,t,s) later) trigger the abstract machine to go through the environment several times. Also, the pattern-matching needs to recursively scan the term and bind the variables x and t one by one, which leads to the creation of multiple continuations and several changes of focus.

Chapter 13

CONCLUSION OF PART II

We present two new semantics for the meta-language Skel in the form of a non-deterministic and a deterministic abstract machine. They are derived from the initial big-step semantics using a known transformation strategy called functional correspondence. A novelty of our approach is to use these classic tools (CPS transformation and defunctionalization) at the meta-level. This yields a generic abstract machine than can be proved sound once and for all, independently of the input language.

We implement the machines in the Coq proof assistant to certify their soundness. Using previous tools and the Coq extraction mechanism, we can automatically generate a certified OCaml interpreter for the deterministic abstract machine specialized for any skeletal semantics. This can be used as a certified interpreter for any language written using skeletal semantics. We summarize the transformations needed to reach our goal in Figure 13.1.

As a future work, we would like to create a deterministic abstract machine in the form of a breadth-first search of all possible behaviors. Unlike the one presented in this document, it would not risk being stuck in a loop, ensuring it to eventually find a result if there exists one. Also, the certified interpreter could be transformed into a debugger, to make use of the certification of the evaluation in a setting where the drawback of inefficiency is not decisive.

CONCLUSION

As presented before, different operational semantics styles have different benefits. For instance, the simplicity and readability of big-step semantics make it the preferred style for defining programming languages, but small-step semantics are better suited to reason about non-terminating or stuck computations. Formalizing the same language multiple times by hand is not a reasonable solution, as it requires a great deal of work and cross-reviewing. We address this issue with the development of interderivation techniques, allowing the use of a central definition and the systematic derivation of other formats when needed. Skeletal semantics is a suitable framework for defining and implementing such interderivations, as it offers an easy manipulation of language semantics as data structures. Regrouping different transformation techniques under the same setting also allows us to compare them and, hopefully, compose them into more complex translations.

Our first contribution is a novel generic and automatic translation of a big-step skeletal semantics into an equivalent small-step skeletal semantics. The complexity of the transformation lies in the creation of new constructors to allow the reconstruction of partially evaluated terms. This method is implemented in the Necro toolbox for the manipulation of skeletal semantics.

We prove on paper that the core of the transformation (without constructor reuse) is correct.

We also ensure, in practice, that the resulting small-step semantics corresponds to the bigstep one by producing for any input language an equivalence certificate verifiable in Coq. This generic automatic transformation from big-step to small-step, with the corresponding proofs, is submitted for publication.

Our second contribution is the application of known interderivation techniques to the metalanguage Skel itself. We use functional correspondence to obtain an equivalent non-deterministic abstract machine, and a slightly modified approach to create a deterministic version. We certify in Coq that this last semantics is indeed correct with respect to the initial (big-step) concrete interpretation of Skel. While the translation techniques used are already established, applying them to the meta-level indirectly benefits any language written in skeletal semantics. Similarly, the Coq correctness proof only has to be done once. We extract an executable OCaml interpreter for Skel from the abstract machine, which can be instantiated with any language. These abstract machines, their derivations, as well as the certified interpreter, have been presented to the CPP 2022 conference [START_REF] Ambal | Certified abstract machines for skeletal semantics[END_REF].

Big Step

Abstract Machine

Small

Step Reduction Semantics PBS Huizing 10 [31], Vesely ESOP19 [START_REF] Vesely | One Step at a Time -A Functional Derivation of Small-Step Evaluators from Big-Step Counterparts[END_REF] Ciobâcă IFM13 [START_REF] Ciobâcă | From Small-Step Semantics to Big-Step Semantics, Automatically[END_REF] Poulsen Mosses ESOP14 [START_REF] Bach Poulsen | Deriving Pretty-Big-Step Semantics from Small-Step Semantics[END_REF] Ager LOPSTR04 [START_REF] Mads | From Natural Semantics to Abstract Machines[END_REF] Functional Correspondence

Danvy ICFP08 [START_REF] Danvy | Defunctionalized interpreters for programming languages[END_REF] Refocusing Danvy 04 [START_REF] Danvy | Refocusing in Reduction Semantics[END_REF], IFL10 [START_REF] Sieczkowski | Automating Derivations of Abstract Machines from Reduction Semantics: -A Generic Formalization of Refocusing in Coq[END_REF], FSCD17 [START_REF] Biernacka | Generalized Refocusing: From Hybrid Strategies to Abstract Machines[END_REF] Part I Part II

Future Work

As discussed in Chapter 7 and 13, the contributions presented in this document can also be expanded. For the big-step to small-step transformation, we would like to generalize the translation to polymorphic functions, and support the use of monads by generating, when able, a monadic small-step skeletal semantics. For the deterministic abstract machine, we could generate a breadth-first version that would not fail on infinite loops. Also, it would be interesting to combine the certified interpreter with other tools to generate trustworthy (albeit slow) debuggers.

Our main objective is to expand and develop the interderivation of different semantic formats, both for the meta-language Skel and to provide as tools to user-defined languages. Notably, we plan to cover the transformations missing from Figure 13.2, but also the own already known, which are defined so far at various degrees of precision. One is presented on an example only [START_REF] Danvy | Defunctionalized interpreters for programming languages[END_REF],

others are defined on a meta-language-either previously known [START_REF] Bach Poulsen | Deriving Pretty-Big-Step Semantics from Small-Step Semantics[END_REF] or somewhat ad-hoc [START_REF] Mads | From Natural Semantics to Abstract Machines[END_REF][START_REF] Ciobâcă | From Small-Step Semantics to Big-Step Semantics, Automatically[END_REF][START_REF] Biernacka | Generalized Refocusing: From Hybrid Strategies to Abstract Machines[END_REF][START_REF] Buszka | Automating the Functional Correspondence between Higher-Order Evaluators and Abstract Machines[END_REF]-, and only one has been formalized in a proof assistant [START_REF] Biernacka | Generalized Refocusing: From Hybrid Strategies to Abstract Machines[END_REF].

Using the same framework to express them all would allow to combine them, and see if different translation paths generate similar results. The restrictions on the input semantics would also be expressed in the same framework. For example, the functional correspondence transformation from abstract machines to big-step assumes the input machine to be in defunctionalized form, a criterion we could try to characterize at the level of the input skeletal semantics.

It would also allow for comparisons between transformations. For instance, the current transformations from small-step to big-step [START_REF] Ciobâcă | From Small-Step Semantics to Big-Step Semantics, Automatically[END_REF][START_REF] Bach Poulsen | Deriving Pretty-Big-Step Semantics from Small-Step Semantics[END_REF] have restrictions expressed w.r.t. their respective meta-language. It is not clear if one is more expressive than the other, or how they rule out the small-step semantics they cannot cover, such as the ones with interleaving. A unified framework would make such expressiveness comparisons possible.

= neg (v) in (s1, v1) | Bconst b -> let v = boolToVal (b) in (s, v) | Equal (e1, e2) -> let (s1, v1) = hexpr (s, e1) in let (s2, v2) = hexpr (s1, e2) in let v = eq (v1, v2) in (s2, v) | Iconst i -> let v = intToVal (i) in (s, v) | Plus (e1, e2) -> let (s1, v1) = hexpr (s, e1) in let (s2, v2) = hexpr (s1, e2) in let v = add (v1, v2) in (s2, v) | Var x -> let v = read (x,
s 0 , e 0 → z 1 , z 2 s, Not1(s 0 , e 0 ) → s, Not1(z 1 , z 2 ) e 0 = (s 1 , v) s, Not1(s 0 , e 0 ) → s, (s 1 , ¬v) s, b → s, (s, boolToVal(b)) s, (e 1 ? = e 2 ) → s, ((s, e 1 ) ? = 1 e 2 ) s 0 , e 0 → z 1 , z 2 s, ((s 0 , e 0 ) ? = 1 e 2 ) → s, ((z 1 , z 2 ) ? = 1 e 2 ) e 0 = (s 1 , v 1 ) s, ((s 0 , e 0 ) ? = 1 e 2 ) → s, (v 1 ? = 2 (s 1 , e 2 )) s 0 , e 0 → z 1 , z 2 s, (v 1 ? = 2 (s 0 , e 0 )) → s, (v 1 ? = 2 (z 1 , z 2 )) e 0 = (s 2 , v 2 ) s, (v 1 ? = 2 (s 0 , e 0 )) → s, (s 2 , (v 1 ? = v 2 )) s, i → s, (s, intToVal(i)) s, (e 1 + e 2 ) → s, ((s, e 1 ) + 1 e 2 ) s 0 , e 0 → z 1 , z 2 s, ((s 0 , e 0 ) + 1 e 2 ) → s, ((z 1 , z 2 ) + 1 e 2 ) e 0 = (s 1 , v 1 ) s, ((s 0 , e 0 ) + 1 e 2 ) → s, (v 1 + 2 (s 1 , e 2 )) | Plus2 (s0, e0, v1) -> branch let (z1, z2) = hexpr (s0, e0) in (s, Plus2 (z1, z2, v1)) or let (s2, v2) = getRet_hexpr (e0) in let v = add (v1, v2) in (s, Ret_hexpr (s2, v)) end | Var x -> let v = read (x, s) in (s, Ret_hexpr (s, v)) s 0 , e 0 → z 1 , z s, (v 1 + 2 (s 0 , e 0 )) → s, (v + 2 (z 1 , z 2 )) e 0 = (s 2 , v 2 ) s, (v 1 + 2 (s 0 , e 0 )) → s, (s 2 , (v 1 + v 2 )) s, x
| If (e1, t2, t3) -> (s, If1 (s, e1, t2, t3)) | If1 (s0, e0, t2, t3) -> branch let (z1, z2) = hexpr (s, e0) in (s, If1 (z1, z2, t2, t3)) or let (s1, v) = getRet_hexpr (e0) in branch let () = isTrue (v) in (s1, t2) or let () = isFalse (v) in (s1, t3) end end s, x := e → s, x := (s, e) s 0 , e 0 → z 1 , z s, x := 1 (s 0 , e 0 ) → s, x := 1 (z 1 , z 2 ) e 0 = (s 1 , v) s, x := 1 (s 0 , e 0 ) → s, (s [x → v]) s, If(e 1 , t 2 , t 3 ) → s, If1(s, e 1 , t 2 , t 3 ) s 0 , e 0 → z 1 , z s, If1(s 0 , e 0 , t 2 , t 3 ) → s, If1(z 1 , z 2 , t 2 , t 3 ) e 0 = (s 1 , v) isTrue(v) s, If1(s 0 , e 0 , t 2 , t 3 ) → s 1 , t 2 e 0 = (s 1 , v) isFalse(v) s, If1(s 0 , e 0 , t 2 , t 3 ) → s 1 , t 3 | Seq (t1, t2) -> (s, Seq1 (s, t1, t2)) | Seq1 (s0, t0, t2) -> branch let (z1, z2) = hstmt (s0, t0) in (s, Seq1 (z1, z2, t2)) or let s1 = getRet_hstmt (t0) in (s1, t2) end | Skip -> (s, Ret_hstmt s) | While (e1, t2) -> (s, While1 (s, e1, e1, t2)) | While1 (s0, e0, e1, t2) -> branch let (z1, z2) = hexpr (s0, e0) in (s, While1 (z1, z2, e1, t2)) or let (s1, v) = getRet_hexpr (e0) in branch let () = isTrue (v) in (s, While2 (s1, t2, e1, t2)) or let () = isFalse (v) in (s, Ret_hstmt s1) end end | While2 (s0, t0, e1, t2) -> branch let (z1, z2) = hstmt (s0, t0) in (s, While2 (z1, z2, e1, t2)) or let s2 = getRet_hstmt (t0) in (s2, While (e1, t2)) end s, t 1 ; t 2 → s, (s, t 1 ) ; 1 t 2 s 0 , t 0 → z 1 , z 2 s, (s 0 , t 0 ) ; 1 t 2 → s, (z 1 , z 2 ) ; 1 t 2 t 0 = (s 1 ) s, (s 0 , t 0 ) ; 1 t 2 → s 1 , t 2 s, Skip → s, (s) s, While(e 1 , t 2 ) → s, While1(s, e 1 , e 1 , t 2 ) s 0 , e 0 → z 1 , z 2 s, While1(s 0 , e 0 , e 1 , t 2 ) → s, While1(z 1 , z 2 , e 1 , t 2 ) e 0 = (s 1 , v) isTrue(v)
s, While1(s 0 , e 0 , e 1 , t 2 ) → s, While2(s 1 , t 2 , e 1 , t 2 )

e 0 = (s 1 , v) isFalse(v)
s, While1(s 0 , e 0 , e 1 , t 2 ) → s, (s 1 )

s 0 , t 0 → z 1 , z 2 s, While2(s 0 , t 0 , e 1 , t 2 ) → s, While2(z 1 , z 2 , e 1 , t 2 ) t 0 = (s 2 ) s, While2(s 0 , t 0 , e 1 , t 2 ) → s 2 , While(e 1 , t 2 ) Σ( t) ⇑ h Σ ⊢ Hook h t ⇑ = = = = = = = = = = = = = = Div-Pc S i ∈ S Σ ⊢ S i ⇑ Σ ⊢ Branching S ⇑ = = = = = = = = = = = = = = = = = = Div-Br Σ ⊢ K ⇑ Σ ⊢ let v = K in S ⇑ = = = = = = = = = = = = = = = = = = = = = = Div-LetL h( y, c( x)) := S ∈ R {y → a} + {x → a ′ } ⊢ S ⇑
. , a n )) ≜ fv(a 1 ) ∪ . . . ∪ fv(a n ) fv(v) ≜ {v} fv(c( t)) ≜ fv( t) fv(Filter f ( t)) ≜ fv( t) fv(Hook h ( t, t)) ≜ fv( t) ∪ fv(t) fv(Return ( t)) ≜ fv( t) fv(Branching ( S)) ≜ fv( S) fv(let v = K in S) ≜ (fv(S) \ { v}) ∪ fv(K) fv([•]) ≜ ∅ fv(< [•] | v | S >) ≜ fv(S) \ { v}
Definition 2. We note bv(S) the variables defined in a skeleton.

bv(Filter

f ( t)) ≜ ∅ bv(Hook h ( t, t)) ≜ ∅ bv(Return ( t)) ≜ ∅ bv(Branching (S 1 , . . . , S n )) ≜ bv(S 1 ) ∪ . . . ∪ bv(S n ) bv(let v = K in S) ≜ { v} ∪ bv(S)
Definition 3. We note SSA(S) the statement that a skeleton S does not reuse variables names. This can be seen as a Static Single Assignment form.

∥ Branching (S 1 , . . . , S n ) ∥ r ≜ Branching (∥ S 1 ∥ r , . . . , ∥ S n ∥ r ) ∥ let v = Return t in S ∥ r ≜ let v = Return t in ∥ S ∥ r ∥ let v = Filter f t in S ∥ r ≜ let v = Filter f t in ∥ S ∥ r ∥ Return t ∥ r ≜ Return ( y, Ret_hr( t)) ∥ let v = Hook (New c) h t in S ∥ r ≜ Return ( y, c( t, z c )) where (hr( y, c( w c , z c )) := S c ) ∈ R dist ∥ let v = Hook Reuse h ( w ′ , w) in S ∥ r ≜ Branching(S 1 , S 2 )
where

S 1 = let z = Hook h w in Return ( y, c r ( x)) [z/w] z fresh; w = ( w ′ , w) S 2 = let v = Hook getRet_h (w) in ∥ S ∥ r Figure C.3: Small-Step Transformation of a Skeleton without Analysis SSA(S) ≜ NoRedef(S) ∧ fv(S) ∩ bv(S) = ∅ NoRedef(Filter f ( t)) ≜ ⊤ NoRedef(Hook h ( t, t)) ≜ ⊤ NoRedef(Return ( t)) ≜ ⊤ NoRedef(Branching (S 1 , . . . , S n )) ≜ ∀i, NoRedef(S i ) NoRedef(let v = K in S) ≜                  bv(K) ∩ { v} = ∅ bv(S) ∩ { v} = ∅ bv(K) ∩ bv(S) = ∅ NoRedef(K) NoRedef(S)
Our transformation is meant to apply to such skeletons. If need be, a first transformation putting skeletons in SSA form can be necessary. Note that the naming "SSA" is an abuse, as parallel branchings are allowed to define the same variables. However, every execution of the skeleton follows a single branch and never overwrites the content of a variable. Definition 4. We note S 1 ∈ S 2 when S 1 is a subskeleton of S 2 . This property is the reflexive transitive closure of the following rules.

K ∈ let v = K in S S ∈ let v = K in S S i ∈ Branching (S 1 , . . . , S n )
Definition 5. We note S 1 ◁ S 2 when S 1 is a tail subskeleton of S 2 . This property is the reflexive transitive closure of the following two rules.

S ◁ let v = K in S S i ◁ Branching (S 1 , . . . , S n )
This definition does not consider subskeletons inside subevaluations.

For instance,

S i ⋪ let v = Branching (S 1 , . . . , S n ) in S ′ .
The proof uses the sets of rules at the different phases of the transformation to state and certify results. As explained in Chapter 5, we additionally introduce a set R EBS , corresponding to an extended big-step semantics, to simplify the proof strategy. We recall the notations for the different sets and the semantics they represent.

• R BS : initial rules, input of the transformation;

• R gen : rules after generation of new constructors and rules;

• R dist : rules after distributing branchings;

• R EBS : similar to R dist (after delaying returns), with rules for Ret_h constructors;

R EBS ≜ R dist ∪ {h( y, Ret_h( v)) := Return v | h ∈ H 0 }
• R SS : result of the transformation after going small-step, with getRet_h hooks.

We use these sets to specify the notations for derivation judgments. We write for instance

Σ ⊢ S ⇓ R BS b and a ⇓ R BS
Σ 2 ) \ dom(Σ 3 )) ∩ fv(S) = ∅, then Σ 1 + Σ 3 ⊢ S ⇓ a ⇐⇒ Σ 1 + Σ 2 + Σ 3 ⊢ S ⇓ a
Proof. By structural induction on S.

• If S = Return ( t)

Σ 1 + Σ 3 ⊢ Return ( t) ⇓ a ⇐⇒ (Σ 1 + Σ 3 )(t) = a ⇐⇒ (Σ 1 + Σ 2 + Σ 3 )(t) = a using Lemma C.2.3 (because fv( t) = fv(S)) ⇐⇒ Σ 1 + Σ 2 + Σ 3 ⊢ Return ( t) ⇓ a • If S is of the form Filter f ( t) or Hook h ( t, t), similarly • If S = Branching ( S) Σ 1 + Σ 3 ⊢ Branching ( S) ⇓ a ⇐⇒ ∃S i ∈ S, Σ 1 + Σ 3 ⊢ S i ⇓ a ⇐⇒ ∃S i ∈ S, Σ 1 + Σ 2 + Σ 3 ⊢ S i ⇓ a using induction hypothesis ⇐⇒ Σ 1 + Σ 2 + Σ 3 ⊢ Branching ( S) ⇓ a • If S = (let v = K in S ′ ) Σ 1 + Σ 3 ⊢ let v = K in S ′ ⇓ a ⇐⇒ ∃ b,    Σ 1 + Σ 3 ⊢ K ⇓ b Σ 1 + Σ 3 + {v → b} ⊢ S ′ ⇓ a ⇐⇒ ∃ b,    Σ 1 + Σ 2 + Σ 3 ⊢ K ⇓ b Σ 1 + Σ 2 + (Σ 3 + {v → b}) ⊢ S ′ ⇓ a using IH twice and Lemma C.2.2. fv(S ′ ) ⊂ fv(S) ∪ { v}, so (dom(Σ 2 ) \ (dom(Σ 3 ) ∪ { v})) ∩ fv(S ′ ) = ∅ ⇐⇒ Σ 1 + Σ 2 + Σ 3 ⊢ let v = K in S ′ ⇓ a Lemma C.2.5. For all S, Σ 1 , Σ 2 , and Σ 3 , if (dom(Σ 2 ) \ dom(Σ 3 )) ∩ fv(S) = ∅, then Σ 1 + Σ 3 ⊢ S ⇑ ⇐⇒ Σ 1 + Σ 2 + Σ 3 ⊢ S ⇑ Proof.
{ x}) ∩ fv(S 2 ) = ∅, then Σ ⊢< S 1 | x | S 2 >⇑ ⇐⇒ Σ ⊢ S 1 ⇑ OR ∃ b,    Σ ⊢ S 1 ⇓ b Σ + {x → b} ⊢ S 2 ⇑ I.e., < S 1 | x | S 2 > diverges if either S 1 or S 2 diverges.
Proof. This lemma is similar to Lemma C.2.7, but for divergence. It is also done by structural induction on S 1 . If

If S 1 is a skelement K, then < K | x | S 2 > is defined as let x = K in S 2
S 1 = (let v = K in S ′ ), then < S 1 | x | S 2 > is defined as let v = K in < S ′ | x | S 2 >.
Then:

Σ ⊢< S 1 | x | S 2 >⇑ ⇐⇒ Σ ⊢ K ⇑ OR ∃ c,    Σ ⊢ K ⇓ c Σ + {v → c} ⊢< S ′ | x | S 2 >⇑
by either rule Div-LetL or Div-LetR. If

⇐⇒

Σ ⊢ K ⇑ OR ∃ c,        Σ ⊢ K ⇓ c Σ + {v → c} ⊢ S ′ ⇑ OR ∃ b,    Σ + {v → c} ⊢ S ′ ⇓ b Σ + {v → c} + {x → b} ⊢ S 2 ⇑ by IH ⇐⇒ Σ ⊢ S 1 ⇑ OR ∃ b,    Σ ⊢ S 1 ⇓ b Σ + {v → c} + {x → b} ⊢ S 2 ⇑ by definitions ⇐⇒ Σ ⊢ S 1 ⇑ OR ∃ b,    Σ ⊢ S 1 ⇓ b Σ + {x → b} ⊢ S 2 ⇑
{ x}) ∩ fv(S 2 ) = ∅, then Σ ⊢< S 1 | x | S 2 >⇓ a ⇐⇒ ∃ b,    Σ ⊢ S 1 ⇓ b Σ + {x → b} ⊢ S 2 ⇓ a Proof. By structural induction on S 1 . If S 1 is a skelement K, then < K | x | S 2 > is defined as let x = K in S 2
S 1 is of the form let v = K in S ′ , then < S 1 | x | S 2 > is defined as let v = K in < S ′ | x | S 2
>, and we have the following.

Σ ⊢< S 1 | x | S 2 >⇓ a ⇐⇒ ∃ c,    Σ ⊢ K ⇓ c Σ + {v → c} ⊢< S ′ | x | S 2 >⇓ a by definition ⇐⇒ ∃ c,        Σ ⊢ K ⇓ c ∃ b,    Σ + {v → c} ⊢ S ′ ⇓ b Σ + {v → c} + {x → b} ⊢ S 2 ⇓ a by induction hypothesis ⇐⇒ ∃ c, b,        Σ ⊢ K ⇓ c Σ + {v → c} ⊢ S ′ ⇓ b Σ + {x → b} ⊢ S 2 ⇓ a by Lemma C.2.4 { v} ⊂ bv(S 1 ), so our hypothesis implies ({ v} \ { x}) ∩ fv(S 2 ) = ∅ ⇐⇒ ∃ b,    Σ ⊢ let v = K in S ′ ⇓ b Σ + {x → b} ⊢ S 2 ⇓ a by definition Lemma C.2.8. For all S 1 , v, and S 2 , ⌈S 2 ⌋ ◁ ⌈< S 1 | v | S 2 >⌋
Proof. By induction on S 1 . Note that this lemma holds only because we assume branchings to always contain at least one skeleton.

• If S 1 = Branching (S ′ 1 , . . . , S ′ n ), then ⌈< S 1 | v | S 2 >⌋ = Branching (⌈< S ′ 1 | v | S 2 > ⌋, . . .

.) and we have our result by applying our induction hypothesis on S ′

1 .

•

If S 1 = K is not a branching, then ⌈< S 1 | v | S 2 >⌋ = (let v = K in ⌈S 2 ⌋
) and we immediately have our result.

•

SSA(let w = K in < S | v | S 2 >) • bv(let w = K in < S | v | S 2 >) = v ∪ w ∪ bv(K) ∪ bv(S) ∪ bv(S 2 ) • fv(let w = K in < S | v | S 2 >) = fv(K) ∪ fv(S) ∪ fv(S 2 )
Lemma C.3.3. SSA form is preserved during the generation phase creating new skeletons. I.e.:

• If SSA(E[Branching (S 1 , . . . , S n )]) then SSA(E[S i ]). • If SSA(E[let v = K in S]) then SSA(E[S]). • If SSA(E[let v = K in S]) then SSA(< [•] | v | E[S] > [K]).
Proof. If E = [•], the three points simplify to:

• If SSA(Branching (S 1 , . . . , S n )) then SSA(S i ).

•

If SSA(let v = K in S) then SSA(S). • If SSA(let v = K in S) then SSA(let v = K in S).
The third case is trivial, the other two are solved using Lemma C.3.1.

Else E =< [•] | w | S E >
for some w and S E . The three points simplify to:

• If SSA(let w = Branching (S 1 , . . . , S n ) in S E ) then SSA(< S i | w | S E >). • If SSA(let v = K in < S | w | S E >) then SSA(< S | w | S E >). • If SSA(let v = K in < S | w | S E >) then SSA(let v = K in < S | w | S E >).
Once again, the third case is trivial, and the second can be solved using Lemma C. Proof. For this, we prove the following by structural induction on S. for each i. Then, using Lemma C.3.2, we get:

∀S, SSA(S) =⇒

• SSA(< S i | v | S ′ >) • v ∪ bv(S i ) ∪ bv(S ′ ) = bv(< S i | v | S ′ >) • fv(S i ) ∪ fv(S ′ ) = fv(< S i | v | S ′ >)
And we can conclude:

• bv(⌈S⌋) = i bv(< S i | v | S ′ >) = i ( v ∪ bv(S i ) ∪ bv(S ′ )) = ( i bv(S i )) ∪ ( v ∪ bv(S ′ )) = bv(S) • fv(⌈S⌋) = i fv(< S i | v | S ′ >) = i (fv(S i ) ∪ fv(S ′ )) = ( i fv(S i )) ∪ fv(S ′ ) = fv(S)
• SSA(⌈S⌋) by definition, using bv(S) ∩ fv(S) = ∅ and SSA(< For this, we check we have SSA(E[S]) at every step S r E . Initially, we start with a given skeleton S (from a rule of R BS , for which we now SSA(S)) and an environment E = [•], so it holds. Then, the preservation of the property comes from Lemma C.3.3.

S i | v | S ′ >) Lemma C.

C.4 Properties of the Transformation Phases

Lemma C.4.1. For all environment Σ, terms a, and skeleton S such that SSA(S), Σ ⊢ S ⇓ a ⇐⇒ Σ ⊢ ⌈S⌋ ⇓ a I.e., the distribution of branchings preserves the concrete interpretation.

Proof. The proof is done by induction on the size of S, and dealing with the different forms S can take. Three of the four cases are trivial (see Section 4.2.3 for the definition). The only interesting case is when S is of the form let v = Branching(S 1 , . . . , S n ) in S ′ . Then, by definition, ⌈S⌋ ≜ Branching(⌈<

S 1 | v | S ′ >⌋, . . . , ⌈< S n | v | S ′ >⌋). Σ ⊢ S ⇓ a ⇐⇒ ∃ b, S i ,    Σ ⊢ S i ⇓ b Σ + {v → b} ⊢ S ′ ⇓ a
by definition (using two rules)

⇐⇒ ∃S i , Σ ⊢< S i | v | S ′ >⇓ a using Lemma C.2.7
We have (bv

(S i ) \ { v}) ∩ fv(S ′ ) = ∅ from        bv(S) ∩ fv(S) = ∅ from SSA(S) fv(S ′ ) ⊂ fv(S) ∪ { v} bv(S i ) ⊂ bv(S) ⇐⇒ ∃S i , Σ ⊢ ⌈< S i | v | S ′ >⌋ ⇓ a using IH, since < S i | v | S ′ > is smaller than S ⇐⇒ Σ ⊢ ⌈S⌋ ⇓ a Lemma C.4.2.
For all environment Σ and skeleton S such that SSA(S), Σ ⊢ S ⇑ ⇐⇒ Σ ⊢ ⌈S⌋ ⇑ I.e., the distribution of branchings also preserves the coinductive semantics.

Proof. This lemma is similar to Lemma C.4.1, and so is the proof. This is done by a straightforward induction on S. Instead of using Lemma C.2.7, we use the similar Lemma C.2.6 for divergence. Also, we directly use Lemma C.4.1 instead of the induction hypothesis for finite subevaluations.

Lemma C.4.3. Delaying returns does not impact the inductive interpretation. Thus, the rules common to R dist and R EBS behave the same way.

Proof. This is done by induction on skeletons. The induction case is trivial, and the base case is to check that Σ ⊢ Filter f t ⇓ a ⇐⇒ Σ ⊢ let w = Filter f t in Return w ⇓ a (and similarly for hooks, with the same reasoning). This comes from the two following derivation trees having the same leaf R f ( Σ(t)) ⇓ a.

R f ( Σ(t)) ⇓ a Σ ⊢ Filter f t ⇓ a R f ( Σ(t)) ⇓ a Σ ⊢ Filter f t ⇓ a (Σ + {w → a})( w) = a Σ + {w → a} ⊢ Return w ⇓ R EBS a Σ ⊢ let w = Filter f t in Return w ⇓ R EBS a
The following two lemmas show that the rules added to create R EBS (from R dist ) behave as intended.

where Σ = {y → a} + { w → ( e, e 0 )} + {z → a ′ } and Σ ′ = Σ + { u → ( e, e ′ 0 )}.

′ )) ⇓ R SS h d =⇒ ∃e ′ 0 , d = ( a, c(( e, e ′ 0 ), a ′ )) e, e 0 ⇓ R SS h 1 ( e, e ′ 0 )
This Lemma is the converse of Lemma C.4.6 above.

Proof. The property holds because the only possible evaluation tree is the one presented in the proof of Lemma C.4.6 above. Indeed

[Σ ⊢ let v = Hook getRet_h1 (w) in ∥ S ∥ r ⇓ R SS . . .],
with Σ(w) = e 0 , is not possible since getRet_h1 only has a rule for the constructor Ret_h1.

The following two lemmas show the small-step semantics can also extract coerced values and continue a computation, for hook calls labeled Reuse when the first computation is finished.

Lemma C.4.8. If (h( y, c(( w, w), z)) := let v = Hook Reuse h 1 ( w, w) in S) ∈ R EBS , then {y → a} + {z → a ′ } + {v → b} ⊢ ∥ S ∥ r ⇓ R SS d =⇒ ( a, c(( e, Ret_h1( b)), a ′ )) ⇓ R SS h d Proof.
Let r be the rule in the formulation of the lemma.

(. . . := ∥ . . . ∥ r ) ∈ R SS Ret_h1( b) ⇓ R SS getRet_h1 b Σ ′ ⊢ ∥ S ∥ r ⇓ R SS d Σ ⊢ let v = Hook getRet_h1 (w) in ∥ S ∥ r ⇓ R SS d Σ ⊢ ∥ let v = Hook Reuse h 1 ( w, w) in S ∥ r ⇓ R SS d ( a, c(( e, Ret_h1( b)), a ′ )) ⇓ R SS
= let v = Hook Reuse h 1 ( w, w) in S) ∈ R EBS , then ( a, c(( e, Ret_h1( b)), a ′ )) ⇓ R SS h d =⇒ {y → a} + {z → a ′ } + {v → b} ⊢ ∥ S ∥ r ⇓ R SS d
This Lemma is the converse of Lemma C.4.8 above.

Proof. The property holds because the only possible evaluation tree is the one presented in the proof of Lemma C. • If S 0 is not a branching, there is no recursive call to the generation operation and we are done.

• If S 0 = Branching (S 1 , . . . , S n ), for each i we generate S i r E , and so we want to show

⌈E[S i ]⌋ ◁ ⌈S⌋. Note that we necessarily have ⌈E[S 0 ]⌋ = Branching (⌈E[S 1 ]⌋, . . . , ⌈E[S n ]⌋). If E = [•], this is trivial. If E is of the form < [•] | v | S ′ >, we have: ⌈E[S 0 ]⌋ = ⌈< Branching (S 1 , . . . , S n ) | v | S ′ >⌋ = ⌈let v = Branching (S 1 , . . . , S n ) in S ′ ⌋ = Branching (⌈< S 1 | v | S ′ >⌋, . . . , ⌈< S n | v | S ′ >⌋) = Branching (⌈E[S 1 ]⌋, . . . , ⌈E[S n ]⌋) So by transitivity we have ⌈E[S i ]⌋ ◁ ⌈E[S 0 ]⌋ ◁ ⌈S⌋.
▶ If S 0 is of the form let v = K in S 1 . Now we need to check to the property for the two recursive calls K

r <[•]| v|E[S 1 ]> and S 1 r E . We notice that E[S 0 ] = (let v = K in E[S 1 ]) = (< [•] | v | E[S 1 ] >)[K]
, so the property holds for the first one from our assumption ⌈E[S 0 ]⌋ ◁ ⌈S⌋.

We are left with checking the property for S 1 r E , i.e., we want to show that ⌈E[S 1 ]⌋ ◁ ⌈S⌋.

• If we have

K = Branching (S ′ 1 , . . . , S ′ n ), then ⌈E[S 0 ]⌋ = Branching (. . . , ⌈< S ′ i | v | E[S 1 ] >⌋, . . .). Using Lemma C.2.8, we conclude ⌈E[S 1 ]⌋ ◁ ⌈< S ′ i | v | E[S 1 ] >⌋ ◁ ⌈E[S 0 ]⌋ ◁ ⌈S⌋. • Lastly, if K is not a branching, we simply have ⌈E[S 0 ]⌋ = let v = K in ⌈E[S 1 ]⌋, so we also have ⌈E[S 1 ]⌋ ◁ ⌈E[S 0 ]⌋ ◁ ⌈S⌋
S 0 , E, E 0 , r, c 0 , h 1 , t, let K H ≜ Hook (New c 0 ) h 1 t. If we generate S 0 r E 0 , and know that K H ∈ S 0 , K H ̸ ∈ E 0 , and that E[K H ] ◁ ⌈E 0 [S 0 ]⌋, then there exists E ′ such that we also generate K H r E ′ and have ⌈E ′ [K H ]⌋ = E[K H ].
Proof. Note that K H appears exactly once in S 0 , since K H is annotated with a fresh constructor name c 0 . We prove this lemma by induction on S 0 .

• If S 0 = Branching (S ′ 1 , . . . , S ′ n ), then there is exactly one S ′ i that contains K H and we generate S

′ i r E 0 . Also ⌈E 0 [S 0 ]⌋ = Branching (⌈E 0 [S ′ 1 ]⌋, . . . , ⌈E 0 [S ′ n ]⌋), so we can deduce that E[K H ] ◁ ⌈E 0 [S ′
i ]⌋. By using our induction hypothesis with S ′ i , we immediately get our desired result.

• If S 0 = K is not a branching, then K H ∈ S 0 implies that S 0 = K H . We take E ′ = E 0 and need to check ⌈E 0 [K H ]⌋ = E[K H ]. We know that E[K H ] ◁ ⌈E 0 [K H ]⌋. If E 0 = [•], then E = [•]
and we have the equality. Else ⌈E 0 [K H ]⌋ is of the form let . . . = K H in ⌈. . .⌋, and since K H ̸ ∈ E 0 , it does not appear in the ". . ." parts. Necessarily (from Definition 5),

we have E[K H ] = ⌈E 0 [K H ]⌋. • If S 0 = (let v = Branching (S ′ 1 , . . . , S ′ n ) in S ′
) and there is exactly one

S ′ i that contains K H . Let E 1 =< [•] | v | E 0 [S ′ ] >, we generate S ′ i r E 1 . Also ⌈E 0 [S 0 ]⌋ = Branching (⌈< S ′ 1 | v | E 0 [S ′ ] >⌋, . . . , ⌈< S ′ n | v | E 0 [S ′ ] >⌋), so we can deduce that E[K H ] ◁ ⌈< S ′ i | v | E 0 [S ′ ] >⌋ = ⌈E 1 [S ′ i ]⌋.
We can use our induction hypothesis with E 1 and S ′ i to immediately get our desired result.

• If S 0 = (let v = Branching (S ′ 1 , . . . , S ′ n ) in S ′ ) and K H ∈ S ′ .
Then K H does not appear in any S ′ i and we generate S ′ r E 0 . Also ⌈E 0 [S 0 ]⌋ = Branching (⌈<

S ′ 1 | v | E 0 [S ′ ] > ⌋, . . . , ⌈< S ′ n | v | E 0 [S ′ ] >⌋). Since E[K H ] ◁ ⌈E 0 [S 0 ]⌋, there is an S ′ i such that E[K H ] ◁ ⌈< S ′ 1 | v | E 0 [S ′ ] >⌋.
We use Lemma C.2.9 to get more precisely that E[K H ] ◁ ⌈E 0 [S ′ ]⌋. Then we can use our induction hypothesis on E 0 and S ′ to get our desired result.

• If S 0 = (let v = K H in S ′ ). let E 1 =< [•] | v | E 0 [S ′ ] >, we generate K H r E 1 .
We take where w does not appear in fv(E).

E ′ = E
Proof. Firstly, if c is not an initial constructor, we can recover the same kind of hypotheses with an initial constructor. In this case, our rule is of the form Proof. Directly from Lemma C.4.12, as we go from R dist to R EBS by only delaying returns at the end of skeletons. This operation does the same modifications on both rules (we assume it selects the same fresh variables for simplicity), and does not create new free variables.

C.5 Big-Step and Extended Big-Step

The following three theorems state the equivalence between the initial and extended big-step semantics. For this, we note |t| the canonical injection of a term t into the extended semantics.

We show that the behavior is preserved when we limit ourselves to initial terms, i.e., to terms using only constructors in C 0 (see Section 4.2), and not newly created constructors (e.g., While1

or Ret_h).

C.6 Extended Big-Step Implies Small-Step

We first certify the finite case with the following theorem. Proof. Since ( a, c( a ′ )) ⇓ R EBS h b is defined mutually with the evaluation of skeletons (Σ ⊢ S ⇓ R EBS b), we will prove two results at the same time: Forall a, c, a ′ , b, h, y, x, S 0 , Σ, S, we have:

• ( a, c( a ′ )) ⇓ R EBS h b =⇒ ( a, c( a ′ )) (⇓ R SS h ) * ( a, Ret_h( b)) •                  r = (h( y, c( x)) := S 0 ) ∈ R EBS S ◁ S 0 Σ( y) = a Σ ⊢ S ⇓ R EBS b S has no Reuse label =⇒ ∃d, Σ ⊢ ∥ S ∥ r ⇓ R SS ( a, d) ( a, d) (⇓ R SS h ) * ( a, Ret_h( b))
The second result says that if S evaluates to b as part of a big-step evaluation, then ∥ S ∥ r can be (small-step) reduced to an intermediate configuration ( a, d) that can reduce further to ( a, Ret_h( b)).

We reason by mutual induction on the derivations of ( a, c( a ′ )) ⇓ R EBS h b and Σ ⊢ S ⇓ R EBS b.

▶ For the first result, our hypothesis is ( a, c( a ′ )) ⇓ R EBS h b.

• If c = Ret_h, using Lemma C.4.5 we have a ′ = b and this is trivial as we take zero small-steps.

• If c is a new constructor, the bottom of the derivation is of the form h( y, c( w, z)) := . . . We prove this separately in Lemma C.6.3 above.

Σ 0 ( w) ⇓ R EBS h 1 b ′ Σ 0 + {v → b ′ } ⊢ S ⇓ R EBS b Σ 0 ⊢ let v = Hook Reuse h 1 w in S ⇓ R EBS b ( a, c(( a ′ 0 , a ′ 1 ), a ′ 2 )) ⇓ R EBS

C.7 Small-Step implies Extended Big-Step

We first certify the finite case. This is done by iterating the following concatenation lemma. Forall a, c, a ′ , d, b, h, y, x, S 0 , Σ, S, we have:

                     Σ ⊢ K ⇓ b ′ Σ + {v → b ′ } ⊢ ∥ S 0 ∥ r ⇓ R SS ( a, d) Σ ⊢ let v = K in ∥ S 0 ∥ r ⇓ R SS ( a, d) ( a, d) ⇓ R EBS h b =⇒ Σ ⊢ K ⇓ b ′ Σ + {v → b ′ } ⊢ S 0 ⇓ R EBS b Σ ⊢ let v = K in S 0 ⇓ R EBS b
• If S is of the form let v = Hook (New c 0 ) h 1 t in S 0 , then from Lemma C.4.13, R EBS contains a rule of the form h( y, c 0 ( w, z)) := let v = Hook Reuse h 1 w in S 0 . We have: We now certify the infinite case. This time, we cannot use a concatenation result. Instead, we check the infinite sequence satisfies the properties of the coinductive interpretation. Before this, we need the following small lemma to split the infinite sequence when needed. Proof. We use the excluded middle axiom, and perform a case disjunction on whether there is a tuple of the form ( b, c(( a, Ret_h1( a ′ )), b ′ )) in the infinite sequence of reduction.

             Σ(
• If there is, we take the first such tuple and we have: )) that we have ( a, a 0 ) (⇓ R SS h 1 ) * ( a, Ret_h1( a ′ )). The base case (zero steps) is trivial. For the induction case, note that a 0 cannot be of the form Ret_h1(. . .) as we chose ( b, c(( a, Ret_h1( a ′ )), b ′ )) to be the first of this form. We then use Lemma C.4.7 to transform the first step, and the induction hypothesis to transform the rest.

• If there is no such tuple, we show ( a, a 0 ) ∞ → h 1 by coinduction. We cut our hypothesis and there is t such that ( b, c(( a, a 0 ), b ′ )) ⇓ R SS We first present an automatic translation of a skeletal semantics written in big-step style into an equivalent structural operational semantics. This translation is implemented on top of the Necro tool, which lets us automatically generate an OCaml interpreter for the small-step semantics and a Coq mechanization of both semantics. We prove the transformation correct in two ways: we provide a paper proof of the core of the translation, and we generate Coq certification scripts alongside the transformation.

We also propose an automatic generation of a certified OCaml interpreter for any language written in skeletal semantics. To this end, we introduce two new interpretations, i.e., formal meanings, of skeletal semantics, in the form of non-deterministic and deterministic abstract machines. These machines are derived from the usual big-step interpretation of skeletal semantics using functional correspondence, a standard transformation from bigstep evaluators to abstract machines. All these interpretations are formalized in the Coq proof assistant and we certify their soundness. We finally use the extraction from Coq to OCaml to obtain the certified interpreter.

1 , c ⇓ s 2 s 2 , while e do c ⇓ s 3 s, while e do c ⇓ s 3 s, e ⇓ s 1 , false s, while e do c ⇓ s 1 Évaluer

 12331 En informatique, le développement d'algorithmes et de logiciels se fait au moyen de langages de programmation. Ces langages permettent, indirectement, la communication entre les programmeurs et les ordinateurs. Définir formellement ces langages devient donc primordial afin d'éviter les problèmes d'interprétation entre humains et machines. La définition d'un langage se compose d'une syntaxe et d'une sémantique. La syntaxe exprime quels objets sont considérés comme des programmes valides, et peut facilement être formalisée à l'aide d'une grammaire. Par exemple, en supposant l'existence de variables (x) et d'entiers (n), on peut définir des expressions (e) et des programmes (c) avec la grammaire suivante : e ::= x | n | Plus(e 1 , e 2 ) | . . . c ::= skip | while e do c | . . . Ensuite, la sémantique d'un langage donne un sens à ces programmes. Dans ce document, nous nous concentrons sur les sémantiques opérationnelles, où un programme est interprété comme une séquence de calculs. Une sémantique opérationnelle explique donc comment exécuter un programme. Formaliser une sémantique opérationnelle peut s'avérer délicat. Pour de petits langages, il est préférable d'utiliser des règles d'inférence qui seront sans ambiguïté. Pour la syntaxe ci-dessus, on définit les règles suivantes.s(x) = n s, x ⇓ s, n s, e 1 ⇓ s 1 , n 1 s 1 , e 2 ⇓ s 2 , n 2 n 1 + n 2 = n s, Plus(e 1 ,e 2 ) ⇓ s 2 , n . . . s, e ⇓ s 1 , true s Plus(e 1 , e 2 ) consiste à évaluer successivement e 1 et e 2 , puis à additionner les résultats. Pour évaluer une boucle while e do c, on évalue d'abord la condition e ; si le résultat est vrai, on évalue la commande c avant de boucler sur while e do c ; sinon, on arrête le calcul. Pour des langages plus conséquents, les règles d'inférence peuvent être encombrantes et difficiles à lire, écrire et modifier. Il est donc fréquent de décrire la sémantique d'un langage à l'aide d'explications en anglais. Tous les langages ne sont ainsi pas définis avec le même niveau

  Il s'agit d'une méthode systématique pour déduire une machine abstraite à partir d'une sémantique à grand pas. Elle combine plusieurs transformation connues pour progressivement réécrire la sémantique en une forme interruptible. Puisque l'interprétation concrète est non déterministe, lui appliquer la correspondance fonctionnelle nous permet d'obtenir une machine abstraite non déterministe équivalente. Cette nouvelle sémantique est intéressante, mais ne permet toujours pas d'exécuter un programme en pratique. Pour créer une machine abstraite déterministe, nous utilisons une version plus sophistiquée de la correspondance fonctionnelle. Cela consiste à introduire des points de contrôle dans la partie non déterministe de la sémantique, permettant à l'exécution de revenir en arrière en cas de problème. Nous obtenons ainsi une sémantique déterministe et exécutable pour le métalangage Skel. Cette machine abstraite est correcte par rapport à l'interprétation concrète : lorsque la machine obtient un résultat, il s'agit d'un résultat correcte. En revanche, tous les résultats corrects ne sont pas forcement accessible Les deux machines abstraites (non déterministe et déterministe) ont été formalisées dans l'assistant de preuve Coq, et nous avons prouvé que ces deux nouvelles sémantiques sont effectivement respectivement équivalentes et correctes par rapport à l'interprétation concrète. Nous utilisons également le système d'extraction de Coq pour obtenir une version OCaml exécutable de la machine abstraite déterministe, exécutant le métalangage Skel. En utilisant les outils de Necro, cette machine peut être automatiquement instancié pour n'importe quelle sémantique squelettique, fournissant ainsi un interpréteur certifié pour tout langage défini en Skel.

  Partie I présente une stratégie pour transformer automatiquement une sémantique sque-lettique à grand pas en une sémantique squelettique à petit pas. Cette transformation est implémentée dans une ancienne version de Necro pour les sémantiques squelettiques sans fonctions d'ordre supérieur. Nous montrons, par une preuve papier, qu'une version légèrement simplifiée de la transformation est correcte. Nous ne certifions pas la version complète de l'algorithme, mais pour chaque utilisation nous générons une garantie de résultat. L'implémentation produit donc automatiquement à la fois une sémantique à petit pas équivalente, et un script Coq vérifiant cette équivalence. La Partie II de ce manuscrit se concentre sur le métalangage (Skel) des sémantiques squelettiques d'ordre supérieure actuelles. Nous utilisons des méthodes connues pour transformer l'interprétation concrète et ainsi générer des machines abstraites déterministe et non déterministe pour Skel. Nous montrons en Coq que la version non déterministe est équivalente à la sémantique de départ, et que la version déterministe est correcte par rapport à la sémantique à grand pas. Extraire la version déterministe nous permet d'obtenir un interpréteur certifié pour tout langage défini en Skel.

? 3 ?= 4 )

 34 ≜ ⊥. We assume an infinite set V of variables, ranged over by x. We define arithmetic expressions (a), boolean expressions (e), and commands (c) as follows. a ::= x | n | Plus(a 1 , a 2 ) e ::= b | Equal(a 1 , a 2 ) | And(e 1 , e 2 ) | Neg(e) c ::= skip | if e then c 1 else c 2 | while e do c | c 1 ; c 2 | x := a 20

Figure 2 . 1 :Example 2 . 2 . 1 .

 21221 Figure 2.1: Big-Step Semantics for IMP

Figure 2 . 2 :

 22 Figure 2.2: Small-Step Semantics for IMP

c

  ::= . . . | while e do c | While1(e 0 , e, c) | While2(c 0 , e, c) ⟨s, while e do c⟩ → ⟨s, While1(e, e, c)⟩ ⟨s, e 0 ⟩ → b ⟨s, e ′ 0 ⟩ ⟨s, While1(e 0 , e, c)⟩ → ⟨s, While1(e ′ 0 , e, c)⟩ ⟨s, While1(⊥, e, c)⟩ → ⟨s, skip⟩ ⟨s, While1(⊤, e, c)⟩ → ⟨s, While2(c, e, c)⟩

Example 2 . 4 . 1 .

 241 Evaluating the example program presented before (Example 2.3.2), we similarly have ⟨ϵ, x := 1 ; if Equal(x, 2) then (x := 3) else (x := 4)⟩ → * RS ⟨{x → 4}, skip⟩ with the same 6 steps. 1. The first step reduces under the context [•] ; if Equal(x, 2) then (x := 3) else (x := 4) and uses the contraction rule ⟨ϵ, x := 1⟩ ⇝ ⟨{x → 1}, skip⟩. 2. The second step directly uses the contraction rule ⟨{x → 1}, skip; if . . . then . . . else . . .⟩ ⇝ ⟨{x → 1}, if . . . then . . . else . . .⟩ (i.e., with the empty context [•]).3. The third step reduces under the context if Equal([•], 2) then (x := 3) else (x := 4) and uses the contraction rule ⟨{x → 1}, x⟩ ⇝ ⟨{x → 1}, 1⟩. 4. The fourth step reduces under the context if [•] then (x := 3) else (x := 4) and uses the contraction rule ⟨{x → 1}, Equal(1, 2)⟩ ⇝ ⟨{x → 1}, ⊥⟩.

  [•]), a) in Section 2.4 corresponds to the continuation Plus(n, [•]) :: Equal([•], a) :: • in an abstract machine state.

  a 2 ) :: κ⟩ e ⟨σ, And(e 1 , e 2 ) | κ⟩ e → ⟨σ, e 1 | And([•], e 2 ) :: κ⟩ e ⟨σ, Neg(e) | κ⟩ e → ⟨σ, e | Neg([•]) :: κ⟩ e ⟨σ, x := a | κ⟩ e → ⟨σ, a | x := [•] :: κ⟩ e ⟨σ, c 1 ; c 2 | κ⟩ e → ⟨σ, c 1 | [•] ; c 2 :: κ⟩ e ⟨σ, if e then c 1 else c 2 | κ⟩ e → ⟨σ, e | if [•] then c 1 else c 2 :: κ⟩ e ⟨σ, while e do c | κ⟩ e → ⟨σ, if e then (c ; while e do c) else skip | κ⟩ e ⟨Plus([•], a 2 ) ::

Figure 2 . 4 : 2 . 5 . 1 .

 24251 Figure 2.4: Abstract Machine for IMP

Figure 2 . 5 :

 25 Figure 2.5: Related Work on Interderivation of Operational Semantics

Example 3 . 3 . 1 .

 331 s ∈ T b we assume given a set C(s), representing its values. For every program type s p ∈ T p , we inductively construct its set of closed program terms C(s p ) from the constructors of the skeletal semantics and the values of the different sets C(s) for s ∈ T . For every filter f ∈ F with typing ftype(f ) = s, s ′ , we assume a relation R f between the elements of C(s) and C(s ′ ). If a are values of C(s) and b are values of C(s ′ ), we write R f ( a) ⇓ b when the interpretation of f relates a to b. For IMP, we instantiate the base type ident with strings, int with integers (Z), bool with Booleans (B = {⊤; ⊥}), value with the disjoint union (Z∪B), and store with partial functions from strings to values. The interpretations of the different filters are the following:

Figure 3 . 2 :

 32 Figure 3.2: Inference Rules for the (Inductive) Concrete Interpretation

Figure 3 .

 3 2 uses an auxiliary judgment a ⇓ h b saying that the hook h taking a as input can output b. This allows for simpler definitions and is use extensively in the proofs (see Chapter 5).

Figure 3 . 3 :

 33 Figure 3.3: Coinductive Interpretation

Figure 3 . 1 )

 31 Figure 3.1). Necro then provides tools to manipulate skeletal semantics. The transformation presented in Chapter 4 is implemented in this framework.

Figure 4 . 1 .

 41 Given a rule h( y, c( x)) := S, we compute the set of variables that are used exactly once in S, written SglUse(S), and we fix V and h as respectively SglUse(S) and h. The parameter V and hr are constants while L and b are initialized at ⊤ and may change during the analysis.

Figure 4 . 1 :

 41 Figure 4.1: Hook calls analysis

Example 4 . 2 . 3 .

 423 Consider the following hook and constructor: hook h (s : state, t : term) matching t : value := | C(t1, t2) -> let v1 = h (s, t1) in let v2 = h (s, t2) in merge (v1, v2)

Figure 4 . 2 :

 42 Figure 4.2: Transformation of a Skeleton

  2.2 can later perform the expected small-step reduction. One might be surprised the hook call disappears, it is simply delegated to the rule for the new constructor c (see Section 4.2.2 right before Example 4.2.1). To compute the arguments of c, we distinguish in its rule r c = (hr( y, c( w c , z c )) := S c ) the variables w c used as input of the analyzed call from the ones z c necessary to compute S c . The resulting skelement is then Return ( y, c( t, z c )), where we replace w c with the terms t being reduced. We know the variables z c exist at the program point we are transforming, because they have been extracted from the same hook call in r during the analysis. Similarly, the variables y of r c are the same as the environment variables of r by construction, so we can reuse them. Example 4.2.4. The call for which we need to create While2 is of the form (cf. Section 4.1.2): hook hstmt (s : state, t : stmt) matching t : state = ... | While1 (s0, e0, e1, t2) -> ... let (s1, v) = ... in ... let s2 = hstmt (new While2) (s1, t2) in ... The rule created for While2 in Example 4.2.1 is of the form hstmt(s, While2(s0, t0, e1, t2)) := S, where w c = (s0, t0) and z c = (e1, t2) are used to compute respectively the analyzed call and the rest of the skeleton. Replacing (s0, t0) with the arguments of the call (s1, t2),the resulting skelement is Return (s, While2 (s1, t2, e1, t2)), and we see that s, e1, and t2 are bound at the point we transform.

  ⇓ a I.e., < S 1 | x | S 2 > behaves like let x = Branching([S 1 ]) in S 2 , which we could write, with an abuse of notation, as let x = S 1 in S 2 . The proof of Lemma 5.1.1 is then a straightforward induction on S. Similarly, the distribution of branchings does not modify the coinductive semantics for diverging computations (see Lemma C.4.2).

Theorem 5 . 1 . 3 (Theorem 5 . 1 . 4 (

 513514 BS⇒EBS). For all hook h and initial terms a and b, a ⇓ BS h b =⇒ |a| ⇓ EBS h |b| EBS⇒BS). For all hook h, initial terms a, and extended terms b ′ ,

Theorem 5 . 1 . 10 (Lemma 5 . 1 . 11 .

 51105111 Div: EBS⇒SS). For all hook h and terms a, By coinduction on the definition of a ∞ → h , the proof needs to show that a ⇑ EBS h satisfies the corresponding property (Lemma C.6.3). For all hook h and terms a,

  type list<a> = | Nil | Cons of a * list<a> (** truncates a list 'l' and keeps the 'n' first elements *) hook firstn<a> (l:list<a>, n:nat) matching n : list<a> = | Zero -> Nil<a> | Succ (m) -> branch let Cons(x, l2) = l in let q = firstn l2 m in Cons(x, q) or let Nil = l in Nil<a> end If we consider this hook to be a basic operation to be evaluated atomically (i.e., keep it in big-step form), our implementation can transform the rest of the semantics without issue 1 . If we instead want a small-step version of this operation, the transformation of Chapter 4 needs to be extended. If all the polymorphic variables of the hook (here, a) are included in the polymorphic variables of the matched program type, our transformation should be applicable with very little modifications. A coercion can be added from the output type to the matched type. Intermediate program points can also be transformed into new constructors for the program type.

  type config<a> = | Conf of list<a> * nat hook firstn<a> (c : config<a>) matching c : list<a> = | Conf (l, n) -> branch let Zero = n in Nil<a> or let Succ m = n in branch let Cons(x, l2) = l in let q = firstn Conf(l2, m) in Cons(x, q) or let Nil = l in Nil<a> end end

  type m<a> = ... val ret<a> (v : a) : m<a> = ... val bind<a,b> (w : m<a>) (f : a → m<b>) : m<b> = ... binder @ := bind type value = | Int int | ... type term = | Plus (term, term) | ... val add : int -> int -> int val eval (t : term) : m<value> = branch let Plus (t1, t2) = t in let Int n1 =@ eval t1 in let Int n2 =@ eval t2 in let n = add n1 n2 in ret<value> (Int n) or ... end

  Value value val evalss (t : term) : m<term> = branch let Plus (t1, t2) = t in branch let t1' =@ evalss t1 in ret<term> (Plus(t1', t2)) or let Value (Int n1) = t1 in branch let t2' =@ evalss t2 in ret<term> (Plus(Value (Int n1), t2')) or let Value (Int n2) = t2 in let n = add n1 n2 in ret<term> (Value (Int n)) end end or ... end

Figure 8 . 1 :

 81 Figure 8.1: Skeletal Semantics for Call-by-Value λ-calculus

r

  ::= CVTuple(l r ) | CVConstr(c, r) | CVClos(Σ, p, S) | CVUnspec(x, n, l T , l r ) | CVRec([(d 1 , r 1 ); . . . ; (d m , r m )]) | CVBase(a) Σ ::= [(x 1 , r 1 ); . . . ; (x m , r m )]Environments are simply lists mapping identifiers to results. CVTuple(l r ) represents a tuple of results, CVConstr(c, r) a constructor packing another cvalue. CVClos(Σ, p, S) is a clo-sure, corresponding to the skelterm TFunc(p, S) bundled with an evaluation environment Σ.

Figures 8 .

 8 2 and 8.3), when the full version would have a few additional side-conditions and polymorphic instantiations.

1 ): Example 8 . 2 . 2 .

 1822 A list of multiple results can also represent non-determinism. For instance, for choosing a number in an interval, we can use:Arity("randInt") = 2UnspecDecl("randInt")[CVBase(5); CVBase[START_REF] Ambal | Certified Derivation of Small-Step From Big-Step Skeletal Semantics[END_REF]] = [CVBase(5); CVBase(6); . . . ; CVBase[START_REF] Ambal | Certified Derivation of Small-Step From Big-Step Skeletal Semantics[END_REF]]The non-deterministic inductive inference rules of Figure8.2 and 8.3 express how the different structures of skeletal semantics are evaluated. The important rules are those for the evaluation of skelterms (Σ ⊢ t ⇓ t r) and skeletons (Σ ⊢ S ⇓ s r) to concrete values.

Figure 8 . 2 :

 82 Figure 8.2: Concrete Interpretation (part 1)

Figure 8 . 3 :

 83 Figure 8.3: Concrete Interpretation (part 2)

|

  Nat of nat | Plus of aexp * aexpThen, for each evaluation predicate (here, ⇓ a ), we create an evaluation function (here named eval). The input elements are the terms being evaluated; the output type is the result of the evaluation. The body of the evaluation function should merge the behaviors of the different inference rules. Notably, each premise assuming an evaluation predicate (e.g., ⟨σ, a 1 ⟩ ⇓ a n 1 ) is translated into a call to the corresponding evaluation function (e.g., let n1 = eval s a1 in ...). When the semantics is deterministic, like here, the function can easily be defined using a patternmatching on the term to evaluate. Otherwise, we also need to introduce a non-deterministic choice operator in the pseudo-code (see Section 10.1). let eval (s : state) (a : aexp) : nat = match a with | Var(x) -> lookup s x | Nat(n) -> n | Plus(a1, a2) -> let n1 = eval s a1 in let n2 = eval s a2 in n1 + n2

  For each new object type (here, kt), we create what we call a dispatch function (here named disp) that will trigger the evaluation of the corresponding anonymous function. For an anonymous function of type t1 -> t2 for which we created a new object of type kt, we also create a dispatch function of type kt -> t1 -> t2. The dispatch function associates to each new constructor the code of its corresponding anonymous function. Then, we can replace every anonymous function with an object using the corresponding new constructor. At the same time, we change every continuation call to use the dispatch function, replacing for instance k n with disp k n. type kt = | KID | KPlus1 of state * aexp * kt | KPlus2 of nat * kt let disp (k : kt) (n : nat) : nat = match k with | KPlus1(s, a2, k') -> eval s a2 KPlus2(n, k') | KPlus2(n1, k') -> disp k' (n1+n) let eval s a (k : kt) : nat = match a with | Var(x) -> disp k (lookup s x) | Nat(n) -> disp k n | Plus(a1, a2) -> eval s a1 KPlus1(s, a2, k)

|

  ... | LetIn (p, sk1, sk2) -> let r = eval_sk sk1 e in let e2 = eval_pat p r e in eval_sk sk2 e2

  oracle picking the correct cvalue *) let r = pick rl in r | S m -> CVUnspec (f, m, []) | TConstr (c, t') -> let r = eval_trm t' e in CVConstr (c, r) | ...

let

  eval_sk sk e (k : cvalue -> cvalue) : cvalue = match sk with | Branching (skl) -> let sk' = pick skl in eval_sk sk' e k | ... | LetIn (p, sk1, sk2) -> eval_sk sk1 e (fun r -> eval_pat p r e (fun e2 -> eval_sk sk2 e2 k))

  KRLet of pattern * skeleton * env * krt | ... let eval_sk sk e (k : krt ) : cvalue = match sk with | Branching (skl) -> let sk' = pick skl in eval_sk sk' e k | ... | LetIn (p, sk1, sk2) -> eval_sk sk1 e (KRLet (p,sk2,e,k)) We create the type krt (and its corresponding dispatch function disp_kr) to represent continuations expecting a result, i.e., a concrete value. For example, the continuation of the evaluation of sk1 in the LetIn case of Section 10.2 is replaced by the constructor KRLet, whose arguments are the free variables of the continuation. We also replace any application of the continuation k r by a call disp_kr k r, like in the arity zero case. The extra constructor kr id represents the identity continuation. let disp_kr (k : krt) (r : cvalue) : cvalue = match k with | KRLet (p,sk,e,k') -> eval_pat p r e (KELet (sk,k')) | ... let eval_trm t e (k : krt ) : cvalue = match t with | TVar (v) -> match v with | VLet (x) -> lookup e x k | VSpec (h) -> eval_trm (specdecl h) [] k | VUnspec (f) -> match arity f with | Z -> let r = pick (unspecdecl f []) in disp_kr k r | S m -> disp_kr k (CVUnspec (f, m, [])) | ... Continuations expecting concrete values are not the only possibility in our CPS-transformed evaluator: some expect either a list of concrete values, an environment, or the content of a record.

Figure 10 . 1 :

 101 Figure 10.1: Non-Deterministic Abstract Machine

Figure 10 . 1 .

 101 Figure 10.1. The full abstract machine is available in Appendix D.1.4.

  Figure 8.2) so we can conclude. The other cases are similar. match sk with | Branching (skl) -> match skl with | [] -> fk () | sk'::l -> eval_sk sk' e k (fun _ -> eval_sk (Branching(l)) e k fk) | ... | LetIn (p, sk1, sk2) -> eval_sk sk1 e (fun r fk2 -> eval_pat p r e (fun e2 fk3 -> eval_sk sk2 e2 k fk3) fk2) fk

3 ,

 3 this defunctionalization phase generates a new type fkt and its dispatch function disp_fk for failure continuations. type fkt = | FEmpty | FSK of skeleton * env * krt * fkt | FList of (cvalue list) * krt * fkt let disp_fk fk = match fk with | FSK(sk, e, k, fk') -> eval_sk sk e k fk' | FList(rl, k, fk') -> select_list rl k fk' The constructors FSK and FList correspond to the anonymous functions of Section 11.1 where we construct backtracking checkpoints for trying respectively the arguments of a branching or the list of possible interpretations of an unspecified skelterm. We also create a constructor FEmpty representing an empty failure continuation, used to start a computation. It has no rule in the dispatch function, since no backtrack is possible. let select_list rl (k : krt ) (fk : fkt ): cvalue = match rl with | [] -> disp_fk fk | r::l -> k r (FList (l, k, fk)) let eval_sk sk e (k : krt ) (fk : fkt ): cvalue = match sk with | Branching (skl) -> match skl with | [] -> disp_fk fk | sk'::l -> eval_sk sk' e k (FSK (Branching(l), e, k, fk)) | ...

Figure 11 . 1 :

 111 Figure 11.1: Deterministic Abstract Machine

Lemma 11 . 3 . 2 .

 1132 For all n, AM mode m, and states ⟨ x, f ⟩ m and b

  step (a : state) : option state This stepping function can be repeated to form a partial evaluation function. Since Coq only accepts terminating functions, we need a fuel parameter, bounding the number of steps the function can do. Fixpoint evalfuel (n : nat) (a : state) : option cvalue This function extracts the result r of a final state ⟨kr id , r, f ⟩ kr , it fails on n = 0 or a = ⟨FEmpty⟩ fk , and it steps then recurses with one less fuel otherwise.

Figure 12 . 1 :

 121 Figure 12.1: Skeletal Semantics for Call-by-Value λ-calculus

  type basevalue = | Vident of string | Venv of (string * cvalue) list We choose to represent variables using OCaml strings, and environments using OCaml lists. For unspecified skelterms (extEnv and getEnv), we define OCaml functions implementing the desired behavior. let function_extenv = function | Cval_tuple ([Cval_base (Venv st); Cval_base (Vident x); v])::[] -> [Cval_base (Venv ((x,v)::st))] | _ -> []

(

  * shortcuts for readability *) let xlam x t = Cval_constructor ("Lam", Cval_tuple [Cval_base (Vident x); t]) let xapp t1 t2 = Cval_constructor ("App", Cval_tuple [t1; t2]) let xvar x = Cval_constructor ("Var", Cval_base (Vident x)) let emptyenv = Cval_base (Venv []) let _ = (* (λx. x x) (λy. y) *) let lambdaterm = xapp (xlam "x" (xapp (xvar "x") (xvar "x"))) (xlam "y" (xvar "y")) let sk = ... in let amstate = inject_skel sk [("s", emptyenv); ("t", lambdaterm)] in let r = evalfuel 614 amstate in ...

Figure 13 . 1 :

 131 Figure 13.1: Summary of Part II and comparison with related work

Figure 13 . 2 :

 132 Figure 13.2: Related Work on Interderivation of Operational Semantics

|

  Skip -> s | If(e, c1, c2) -> let b = eval_b s e in match b with | true -> eval_c s c1 | false -> eval_c s c2 | While(e, c) -> let b = eval_b s e in match b with | true -> let s' = eval_c s c in eval_c s' While(e, c) | false -> s | Seq(c1, c2) -> let s' = eval_c s c1 in eval_c s' c2 | Assign(x, a) -> let n = eval_a s a in extend s x n A.2 CPS Transform let eval_a s a (k : nat -> state) : state = match a with | Var(x) -> k (lookup s x) | Nat(n) -> k n | Plus(a1, a2) -> eval_a s a1 (fun n1 -> eval_a s a2 (fun n2 -> k (n1+n2))) let eval_b s e (k : bool -> state) : state = match e with | Bool(b) -> k b | Equal(a1, a2) -> eval_a s a1 (fun n1 -> eval_a s a2 (fun n2 -> k (n1 == n2))) | And(e1, e2) -> eval_b s e1 (fun b1 -> eval_b s e2 (fun b2 -> k (b1 && b2))) | Neg(e') -> eval_b s e' (fun b -> k (not b))Mode kb:⟨kb id , b⟩ kb ̸ → (* end of computation *) ⟨KBAnd1(σ, e, k), b⟩ kb → ⟨σ, e, KBAnd2(b, k)⟩ b ⟨KBAnd2(b 1 , k), b⟩ kb → ⟨k, (b 1 ∧ b)⟩ kb ⟨KBNeg(k), b⟩ kb → ⟨k, ¬b⟩ kb ⟨KBIf(σ, c 1 , c 2 , k), ⊤⟩ kb → ⟨σ, c 1 , k⟩ c ⟨KBIf(σ, c 1 , c 2 , k), ⊥⟩ kb → ⟨σ, c 2 ,k⟩ c ⟨KBWhile(σ, c, e, k), ⊤⟩ kb → ⟨σ, c, KCWhile(e, c, k)⟩ c ⟨KBWhile(σ, c, e, k), ⊥⟩ kb → ⟨k, σ⟩ kc Mode b: ⟨σ, b, k⟩ b → ⟨k, b⟩ kb ⟨σ, Equal(a 1 , a 2 ), k⟩ b → ⟨σ, a 1 , KAEqual1(σ, a 2 , k)⟩ a ⟨σ, And(e 1 , e 2 ), k⟩ b → ⟨σ, e 1 , KBAnd1(σ, e 2 , k)⟩ b ⟨σ, Neg(e), k⟩ b → ⟨σ, e, KBNeg(k)⟩ b Mode kc: ⟨kc id , σ⟩ kc ̸ → (* end of computation *) ⟨KCWhile(e, c, k), σ⟩ kc → ⟨σ, while e do c, k⟩ c ⟨KCSeq(c, k), σ⟩ kc → ⟨σ, c, k⟩ c Mode c: ⟨σ, skip, k⟩ c → ⟨k, σ⟩ kc ⟨σ, if e then c 1 else c 2 , k⟩ c → ⟨σ, e, KBIf(σ, c 1 , c 2 , k)⟩ b ⟨σ, while e do c, k⟩ c → ⟨σ, e, KBWhile(σ, c, e, k)⟩ b ⟨σ, c 1 ; c 2 , k⟩ c → ⟨σ, c 1 , KCSeq(c 2 , k)⟩ c ⟨σ, x := a, k⟩ c → ⟨σ, a, KAAssign(σ, x, k)⟩ a let s2 = hstmt (s1, t2) in hstmt (s2, While (e1, t2)) or let () = isFalse (v) in s1 end B.2 After Adding Coercions type int type bool type ident type state type value type expr = | Not of expr | Bconst of bool | Equal of expr * expr | Iconst of int | Plus of expr * expr | Var of ident | Ret_hexpr of state * value type stmt = | Assign of ident * expr | If of expr * stmt * stmt | Seq of stmt * stmt | Skip | While of expr * stmt | Ret_hstmt of state val add : value * value -> value val boolToVal : bool -> value val eq : value * value -> value val intToVal : int -> value val isFalse : value -> unit val isTrue : value -> unit val neg : value -> value val read : ident * state -> value val write : ident * state * value -> state hook getRet_hexpr (e : expr) matching e : state * value = | Ret_hexpr (v1, v2) -> (v1, v2) hook getRet_hstmt (t : stmt) matching t : state = | Ret_hstmt v1 -> v1 hook hexpr (s : state, e : expr) matching e : state * value = | Not e1 -> let (s1, v) = hexpr (s, e1) in let v1

( 1 .

 1 Figure C.2: Coinductive Interpretation

hbDefinition 6 .Lemma C. 2 . 4 .

 624 for the concrete evaluation, and Σ ⊢ S ⇑ R BS and a ⇑ R BS h for the divergence of the initial big-step semantics. We note a ∞ → h the divergence in the small-step skeletal semantics. It is defined coinductively with the following rule. = = = = = = = = = = = = = = Other equivalent definitions include: For all S, Σ 1 , Σ 2 , Σ 3 , and a, if (dom(

  and the result comes directly from the definition (see rules Div-LetR and Div-LetL of Figure C.2).

  and the result comes directly from the definition (see Figure C.1).

  straightforward. The only interesting case is when S is of the formlet v = Branching(S 1 , . . . , S n ) in S ′ . Then by definition ⌈S⌋ ≜ Branching(⌈< S 1 | v | S ′ >⌋, . . . , ⌈< S n | v | S ′ >⌋).

First, we check

  by following Definition 3 that we have SSA(let v = Branching(S i ) in S ′ )

3 . 5 .

 35 If all initial skeletons are in SSA form, then the new skeletons created during the generation phase respect the SSA conditions. I.e., if for all rule (h( y, c( z)) := S) ∈ R BS we initially have SSA(S), then for all rule (h( y, c( z)) := S) ∈ R gen we also have SSA(S).Proof. Initial rules are left untouched, so we only need to check SSA(S) for new skeletons, constructed from a generation operation Hook (New c) h t r E . The important part of the proof is to check SSA(E[Hook h t]), as the new skeleton is then E[Hook h w] where w are fresh variables, and the property trivially ensues.

Lemma C. 4 . 7 .

 47 If (h( y, c( w, z)) := let v = Hook Reuse h 1 w in S) ∈ R EBS , and e 0 is not of the form Ret_h1(...), then ( a, c(( e, e 0 ), a

Lemma C. 4 . 9 .

 49 {y → a}+ {w → e}+{w → Ret_h1( b)}+ {z → a ′ } and Σ ′ = Σ+ {v → b}. The first leaf uses Lemma C.4.5. The second leaf uses Lemma C.2.4 to go from {y → a} + {z → a ′ } + {v → b} to Σ ′ since the variables ( w, w) do not appear in S by construction. If (h( y, c(( w, w), z)) :

Lemma C. 4 . 10 .

 410 4.8 above. Indeed [Σ ⊢ let u = Hook h 1 ( w, w) in Return ( y, c( u, z)) ⇓ R SS . . .], with Σ(w) = Ret_h1( b), is not possible since, in R SS , the hook h 1 does not have a rule for the constructor Ret_h1. We also use Lemma C.2.4 to simplify the environment. For every rule r = (h( y, c( x)) := S) ∈ R BS , during the generation phase with the skeleton S, every time we apply the generation operation on a subskeleton, i.e. S 0 r E , we have ⌈E[S 0 ]⌋ ◁ ⌈S⌋. Proof. The property trivially holds at the start ( S r [•] ) of the generation, since ⌈S⌋ ◁ ⌈S⌋ by reflexivity. Now we check that we preserve this property during the generation, assuming we are at the point S 0 r E with ⌈E[S 0 ]⌋ ◁ ⌈S⌋. ▶ If S 0 is a skelement.

  by definition. The next two lemmas explain where the rules for new constructors come from. If after distribution, we have an initial skeleton with E[K H ] (where K H = Hook (New c 0 ) h 1 t) as its tail, then it comes from the distribution of a skeleton of the form E ′ [K H ]. The new rule for c 0 is the distribution of E ′ [K H ], which results in E[K H ] as well. E.g., if the skeleton of While ends with let v = Hook (New While2) h t in S then the skeleton of While2 is necessarily of the form let v = Hook Reuse h w in S. Lemma C.4.11. Forall

  1 , and need to show that E[KH ] is equal to ⌈E 1 [K H ]⌋ = (let v = K H in ⌈E 0 [S ′ ]⌋). Once again, E[K H ] ◁ ⌈E 1 [K H ]⌋ and K H does not appear in ⌈E 0 [S ′ ]⌋, so E[K H ] ⋪ ⌈E 0 [S ′ ]⌋and we have the equality from the only possible case of Definition 5.• If S 0 = (let v = K in S ′ ) where K ̸ = K H is not a branching, then K H ∈ S ′ and we generate S ′ r E 0 . Also ⌈E 0 [S 0 ]⌋ = (let v = K in ⌈E 0 [S ′ ]⌋). Thus E[K H ] ◁ ⌈E 0 [S ′]⌋ and we have our desired result by using our induction hypothesis on E 0 and S ′ .Lemma C.4.12. For all rule (h( y, c( x)):= ⌈S⌋) ∈ R dist , if E[Hook (New c 0 ) h 1 t] ◁ ⌈S⌋, then R distcontains a rule of the form: h( y, c 0 ( w, z)) := E[Hook Reuse h 1 w]

  h( y, c( x 1 , x 2 )) := E S [Hook Reuse h 2 x 1 ] ∈ R gen with x = x 1 , x 2 . So it has been created from a generation operation Hook (New c) h 2 t ′ r E S , with r = (h( y, c ′ ( v)) := S 0 ) ∈ R BSwhere c ′ is an initial constructor. Using Lemma C.4.10, we have ⌈S⌋ = ⌈E S [Hook Reuse h 2 x 1 ]⌋ ◁ ⌈S 0 ⌋. So by transitivity we recover the following hypothesis: (h( y, c ′ ( v)) := ⌈S 0 ⌋) ∈ R dist where c ′ is an initial constructor and E[Hook (New c 0 ) h 1 t] ◁ ⌈S 0 ⌋. Now, we can assume without loss of generality that c is an initial constructor, and so that we have r = (h( y, c( x)) := S) ∈ R BS . We use Lemma C.4.11 with S r [•] to get E ′ such that we generate Hook (New c 0 ) h 1 t r E ′ and have ⌈E ′ [Hook (New c 0 ) h 1 t]⌋ = E[Hook (New c 0 ) h 1 t]. Thus the creation of new skeletons produces a rule (h( y, c 0 ( w, z)) := E ′ [Hook Reuse h 1 w]) ∈ R gen with fresh variables w. And after distributing branchings we have (h( y, c 0 ( w, z)) := E[Hook Reuse h 1 w]) ∈ R dist Lemma C.4.13. ∀(h( y, c( x)) := S) ∈ R EBS , if (let v = Hook (New c 0 ) h 1 t in S 0 ) ◁ S, then R EBS contains a rule of the form: h( y, c 0 ( w, z)) := let v = Hook Reuse h 1 w in S 0 where w does not appear in fv(S 0 ).

Theorem C. 5 . 1 (

 51 BS⇒EBS). For all h and initial terms a, and b, a ⇓ R BS h b =⇒ |a| ⇓ R EBS h |b| Proof. Our hypothesis comes from a rule of the form h( y, c( x)) := S ∈ R BS ⊆ R gen with a = d, c( d ′ ) and a derivation {y → d}+ {x → d ′ } ⊢ S ⇓ b. By definition, h( y, c( x)) := ⌈S⌋ is a rule of R dist . And from Lemma C.4.1, we have {y → d} + {x → d ′ } ⊢ ⌈S⌋ ⇓ b and |a| ⇓ R dist h |b|. Since delaying returns does not change the behavior (Lemma C.4.3), we also have |a| ⇓ R EBS h |b|.Theorem C.5.2 (EBS⇒BS). For all h, initial terms a and extended terms b′ , |a| ⇓ R EBS h b ′ =⇒ ∃ b, b ′ = |b| ∧ a ⇓ R BSh b Proof. Since a are initial terms and do not use coercion constructors of the form Ret_h, we also have |a| ⇓ R dist h b ′ from Lemma C.4.3. This comes from a rule of the form h( y, c( x)) := ⌈S⌋ ∈ R dist with a = d, c( d ′ ) and a derivation {y → d} + {x → d ′ } ⊢ ⌈S⌋ ⇓ b. By definition, h( y, c( x)) := S is a rule of R gen , and from Lemma C.4.1, we have |a| ⇓ Rgen h b ′ . Since a are initial terms and do not use newly generated constructors (e.g., While1), then c is an initial constructor, h( y, c( x)) := S is also a rule of R BS , and we have a ⇓ R BS h b ′ . This shows that b ′ is necessarily restricted to initial constructors, which can be stated as ∃ b, b ′ = |b| ∧ a ⇓ R BS h b Theorem C.5.3 (Div: BS⇔EBS). For all h and initial terms a, a ⇑ R BS h ⇐⇒ | a| ⇑ R EBS h Proof. Similarly to the two previous theorems. Since we only focus on initial terms (no new generated constructors), a ⇑ R BS h ⇐⇒ | a| ⇑ Rgen h . From the definition of R dist and Lemma C.4.2, | a| ⇑ Rgen h ⇐⇒ | a| ⇑ R dist h . Since we only focus on initial terms (no coerced returns) and delaying returns is also equivalent for coinduction, | a| ⇑ R dist h ⇐⇒ | a| ⇑ R EBS h .

Theorem C. 6 . 1 (

 61 EBS ⇒ SS). For all a, c, a ′ , b, and h,( a, c( a ′ )) ⇓ R EBS h b =⇒ ( a, c( a ′ )) (⇓ R SS h ) * ( a, Ret_h( b))

Lemma C. 6 . 3 ..• 1 Σ 1 .• 1 dΣ⊢ 1 a 1 , a ′ 2 (⇓ R SS h 1 ) * a 1 ,

 6311111211 h b where a ′ = (( a ′ 0 , a ′ 1 ), a ′ 2 ), Σ 0 = {y → a} + { w → ( a ′ 0 , a ′ 1 )} + {z → a ′ 2 }, and we use the rule h( y, c( w, z)) := let v = Hook Reuse h 1 w in S. For all a, c, a ′ , and h,( a, c( a ′ )) ⇑ R EBS h =⇒ ∃ b, ( a, c( a ′ )) ⇓ R SS h b ∧ b ⇑ R EBS hProof. We prove this result by induction on ( a, c( a ′ )), keeping h universally quantified. We distinguish the different cases of ( a, c( a′ )) ⇑ R EBS h If it comes from a tree h( y, c( w, z)) := . . . ∈ R EBS a 1 ⇑ R EBS h ⊢ Hook h 1 w ⇑ R EBS = = = = = = = = = = = = = = = = = = = = Div-Pc Σ ⊢ let v = Hook Reuse h 1 w in S ⇑R EBS = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Div-LetL ( a, c( a 1 , a 3 )) ⇑ R EBS h = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Div-tuplewith Σ = {y → a} + {w → a 1 } + {z → a 3 } and a ′ = ( a 1 , a 3 ), then we can use our induction hypothesis since a 1 is a subterm of ( a, c( a ′ )). So there is b ′ such that a 1 ⇓ R SS h 1 b ′ and b ′ ⇑ R EBS h We choose b = ( a, c( b ′ , a 3 )) and we have ( a, c( a 1 , a 3 ))) ⇓ R SS h b from Lemma C.4.6 and b ⇑ R EBS hfrom a tree similar to the one above (replacing a 1 by b ′ ). If it comes from a treeh( y, c( w, z)) := . . . ∈ R EBS a 1 , a 2 ⇓ R EBS h Hook h 1 w ⇓ R EBS d ----------------------Σ ′ ⊢ S ⇑ R EBS = = = = = = = = = = = = Σ ⊢ let v = HookReuse h 1 w in S ⇑ R EBS = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Div-LetR ( a, c( a 1 , a 2 , a 3 )) ⇑ R EBS h = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Div-tuple with Σ = {y → a} + { w → ( a 1 , a 2 )} + {z → a 3 }, Σ ′ = Σ + {v → d} and a ′ = ( a 1 , a 2 , a 3 ), there is two subcases. If a 2 is not of the form Ret_h1(..), then Theorem C.6.1 gives us a 1 , a 2(⇓ R SS h 1 ) * a 1 , Ret_h1( d) which implies there is a ′ 2 such that a 1 , a 2 ⇓ R SS h Ret_h1( d). We take b = ( a, c( a 1 , a ′ 2 , a 3 )), we have ( a, c( a 1 , a 2 , a 3 ))) ⇓ R SSh b from Lemma C.4.6 and b ⇑ R EBS h from a tree similar to the one above (replacing a 2 by a ′ 2 ). Otherwise we have a 2 = Ret_h1( d) from Lemma C.4.4. Using Lemma C.6.2 above, there isd 0 such that Σ ′ ⊢ ∥ S ∥ r ⇓ R SS ( a, d 0 ) and ( a, d 0 ) ⇑ R EBS h, where r is the rule on the tree above.We choose b = ( a, d 0 ) and can conclude ( a, c( a ′ )) ⇓ R SS h b using Lemma C.4.8.• Lastly, if it comes from a treeh( y, c( x)) := S ∈ R EBS Σ 0 ⊢ S ⇑ R EBS ============ ( a, c( a ′ )) ⇑ R EBSh = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Div-tuplewhere S does not start with a Reuse label, with Σ 0 = {y → a} + {x → a ′ }.Once again we use Lemma C.6.2 and there isd such that Σ 0 ⊢ ∥ S ∥ r ⇓ R SS ( a, d) and ( a, d) ⇑ R EBS h, where r = (h( y, c( x)) := S). Then we complete our goal as such:h( y, c( x)) := ∥ S ∥ r ∈ R SS Σ 0 ⊢ ∥ S ∥ r ⇓ R SS ( a, d) ( a, c( a ′ )) ⇓ R SS h (a, d) Theorem C.6.4 (Div: EBS ⇒ SS). For all hook h and terms a, a ⇑ R EBS h Proof. Given Definition 6 of a ∞ → h , by coinduction, we want to show that a ⇑ R EBS h satisfies the stated property. Thus it is sufficient to prove: ∀ a, h, a ⇑ R EBS h =⇒ ∃ b, a ⇓ R SS h b ∧ b ⇑ R EBS h

Lemma C. 7 . 1 (

 71 Concatenation). For all a, c, a ′ , d, b, and h,( a, c( a ′ )) ⇓ R SS h ( a, d) ( a, d) ⇓ R EBS h b =⇒ ( a, c( a ′ )) ⇓ R EBS h b Proof. Once again ( a, c( a ′ )) ⇓ R SS h ( a, d) is defined mutually with the evaluation of skeletons (Σ ⊢ ∥ S ∥ r ⇓ R SS ( a, d)), so we will prove two results at the same time:

bb

  y, c 0 ( t, z)) = ( a, d)Σ ⊢ Return ( y, c 0 ( t, z)) ⇓ R SS ( a, d) ( a, d) ⇓ R EBS hFrom which we can deduce d = c 0 (Σ( t), Σ( z)), and our second hypothesis corresponds, with Σ ′ = {y → a} + {w → Σ(t)} + {z → Σ(b)}, to a tree of the form:h( y, c 0 ( w, z)) := ... ∈ R EBS Σ ′ ( w) ⇓ R EBS h 1 b ′ Σ ′ ⊢ Hook Reuse h 1 w ⇓ b ′ Σ ′ + {v → b ′ } ⊢ S 0 ⇓ R EBS b Σ ′ ⊢ let v = Hook Reuse h 1 w in S 0 ⇓ R EBS b ( a, d) ⇓ R EBS h Note that Σ ′ ( w) = Σ( t). Also, using Lemma C.2.4, we can deduce Σ + {v → b ′ } ⊢ S 0 ⇓ R EBS band create our goal derivation:Σ( t) ⇓ R EBS h 1 b ′ Σ ⊢ Hook (New c 0 ) h 1 t ⇓ b ′ Σ + {v → b ′ } ⊢ S 0 ⇓ R EBS b Σ ⊢ let v = Hook (New c 0 ) h 1 t in S 0 ⇓ R EBS b Theorem C.7.2 (SS ⇒ EBS). ∀ a, c, a ′ , b, h, ( a, c( a ′ )) (⇓ R SS h ) * ( a, Ret_h( b)) =⇒ ( a, c( a ′ )) ⇓ R EBS h bProof. We prove it by induction on the number of small-step reductions. If c( a ′ ) = Ret_h( b), i.e. there is zero small steps, the result holds from Lemma C.4.4. Otherwise, the induction case comes directly from Lemma C.7.1 above.

Lemma C. 7 . 3 .

 73 Assuming (h( y, c(( w, w), z)) := let v = Hook Reuse h 1 ( w, w) in S) ∈ R EBS , if we have a small-step divergence ( b, c(( a, a 0 ), b ′ )) ∞ → h then either the first hook call diverges ( a, a 0 ) ∞ → h 1or the first hook call finishes and the rest of the computation diverges∃ a ′ , ( a, a 0 ) (⇓ R SS h 1 ) * ( a, Ret_h1( a ′ )) ( b, c(( a, Ret_h1( a ′ )), b ′ )) ∞ → h

(

  b, c(( a, a 0 ), b ′ )) (⇓ R SS h ) * ( b, c(( a, Ret_h1( a ′ )), b ′ )) ∞ → h Now we show by induction on ( b, c(( a, a 0 ), b ′ )) (⇓ R SS h ) * ( b, c(( a, Ret_h1( a ′ )), b ′

∞ 1 (|

 1 → h . From Lemma C.4.7, we have t = ( b, c(( a, a ′ 0 ), b ′ )) with ( a, a 0 ) ⇓ R SS h a, a ′ 0 ). Also, ( b, c(( a, a ′ 0 ), b ′ )) ∞ → h still has the property of not having any intermediate tuple of the form ( b, c(( a, Ret_h1( a ′ )), b ′ )), so we can use our induction hypothesis with it to conclude. Theorem C.7.4 (Div: SS ⇒ EBS). For all hook h and terms a, a ∞ → h =⇒ a ⇑ R EBS h Apply (t, arglist) -> let r = eval_trm t e in let rl = eval_trmlst arglist e in apply_res r rl | Exists (p, sk) -> (* different oracle for the existential *) let r = magic () in let e2 = eval_pat p r e in eval_sk sk e2 def apply_res r arglist = match arglist with | [] -> r | arg::al -> match r with | CVUnspec (f, Z, parg) -> let rl = unspecdecl f (parg++[arg]) in (* oracle picking the correct cvalue *) let r' = pick rl in apply_res r' al | CVUnspec (f, S n, parg) -> apply_res (CVUnspec (f, n, parg++[arg])) al | CVClos (e, p, sk) -> let e' = eval_pat p arg e in let r' = eval_sk sk e' in apply_res r' al def eval_pat p r e : env = match p, r with | PWild, _ -> e | PVar (x), _ -> (x, r)::e | PConstr (c, p2), CVConstr (c2, r2) when (c=c2) -> eval_pat p2 r2 e | PTuple (pl), CVTuple (rl) -> eval_patlst pl rl e | PRec (ldp), CVRec (ldr) -> eval_patldp ldp ldr e def eval_patldp ldp ldr e : env = match ldp with | [] -> e | (d, p)::ldp' -> let r = lookup ldr d in let e2 = eval_pat p r e in eval_patldp ldp' ldr e2 def eval_patlst pl rl e : env = match pl, rl with | [], [] -> e | p::l1, r::l2 -> let e2 = eval_pat p r e in eval_patlst l1 l2 e2 D.1.2 CPS Transform def eval_trm (t : skelterm) (e : env) (k : cvalue -> cvalue) : cvalue = match t with | TVar (v) -> match v with | VLet (x) -> lookup e x k | VSpec (h) -> eval_trm (specdecl h) [] k | VUnspec (f) -> match arity f with | Z -> let r = pick (unspecdecl f []) in k r | S m -> k (CVUnspec (f, m, [])) | TConstr (c, t') -> eval_trm t' e (fun r -> k (CVConstr (c, r))) | TTuple (tl) -> eval_trmlst tl e (fun rl -> k (CVTuple (rl))) | TFunc (p, sk) -> k (CVClos (e, p, sk)) | TField (t', d) -> eval_trm t' e (fun r -> getfield r d k) | TNth (t', n) -> eval_trm t' e (fun r -> getnth r n k) | TRec (topt, ldt) -> match topt with | Some t' -> eval_trm t' e (fun r -> uprec r ldt e k) | None -> eval_ldt ldt e (fun ldr -> k (CVRec (ldr))) def uprec (r : cvalue) (ldt : (string * skelterm) list) (e : env) (k : cvalue -> cvalue) : cvalue = match r with | CVRec ldr1 -> eval_ldt ldt e (fun ldr2 -> k (CVRec (ldr2++ldr1))) def getfield (r : cvalue) (d : string) (k : cvalue -> cvalue) : cvalue = match r with | CVRec ldr -> lookup ldr d k def getnth (r : cvalue) (n : int) (k : cvalue -> cvalue) : cvalue = match r with | TTuple rl -> getsome (nth rl n) k def getsome (ropt : cvalue option) (k : cvalue -> cvalue) : cvalue = match ropt with | Some r -> k r def eval_ldt (ldt : (string * skelterm) list) (e : env) (k : (string * cvalue) list -> cvalue) : cvalue = match ldt with | [] -> k [] | (d, t)::ldt' -> eval_trm t e (fun r -> eval_ldt ldt' e (fun ldr -> k ((d, r)::ldr))) def lookup (e : env) (x : string) (k : cvalue -> cvalue) : cvalue = match e with | (y, r)::e2 -> if x=y then k r else lookup e2 x k def eval_trmlst (tl : skelterm list) (e : env) (k : cvalue list -> cvalue) : cvalue = match tl with | [] -> k [] | t::l -> eval_trm t e (fun r -> eval_trmlst l e (fun rl -> k (r::rl))) def eval_sk (sk : skeleton) (e : env) (k : cvalue -> cvalue) : cvalue = match sk with | Branching (skl) -> let sk' = pick skl in eval_sk sk' e k | LetIn (p, sk1, sk2) -> eval_sk sk1 e (fun r -> eval_pat p r e (fun e2 -> eval_sk sk2 e2 k)) | Return t -> eval_trm t e k | Apply (t, arglist) -> eval_trm t e (fun r -> eval_trmlst arglist e (fun rl -> apply_res r rl k)) | Exists (p, sk) -> let r = magic () in eval_pat p r e (fun e2 -> eval_sk sk e2 k) def apply_res (r : cvalue) (arglist : cvalue list) (k : cvalue -> cvalue) : cvalue = match arglist with | [] -> k r | arg::al -> match r with | CVUnspec (f, Z, parg) -> let r' = pick (unspecdecl f (parg++[arg])) in apply_res r' al k | CVUnspec (f, S n, parg) -> apply_res (CVUnspec (f, n, parg++[arg])) al k | CVClos (e, p, sk) -> eval_pat p arg e (fun e' -> eval_sk sk e' (fun r' -> apply_res r' al k)) def eval_pat (p : pattern) (r : cvalue) (e : env) (k : env -> cvalue) : cvalue = match p, r with | PWild, _ -> k e | PVar (x), _ -> k ((x, r)::e) | PConstr (c, p2), CVConstr (c2, r2) when (c=c2) -> eval_pat p2 r2 e k | PTuple (pl), CVTuple (rl) -> eval_patlst pl rl e k | PRec (ldp), CVRec (ldr) -> eval_patldp ldp ldr e k def eval_patldp (ldp : (string * pattern) list) (ldr : (string * cvalue) list) (e : env) (k : env -> cvalue) : cvalue = match ldp with | [] -> k e | (d, p)::ldp' -> lookup ldr d (fun r -> eval_pat p r e (fun e2 -> eval_patldp ldp' ldr e2 k)) def eval_patlst (pl : pattern list) (rl : cvalue list) (e : env) (k : env -> cvalue) : cvalue = match pl, rl with | [], [] -> k e | p::l1, r::l2 -> eval_pat p r e (fun e2 -> eval_patlst l1 l2 e2 k) eval_pat p r e KELet(sk, k) def apply_res (r : cvalue) (argl : cvalue list) (k : krt) : cvalue = match argl with | [] -> disp_kr k r | arg::al -> match r with | CVUnspec (f, Z, parg) -> let r' = pick (unspecdecl f (parg++[arg])) in apply_res r' al k | CVUnspec (f, S n, parg) -> apply_res (CVUnspec (f, n, parg++[arg])) al k | CVClos (e, p, sk) -> eval_pat p arg e KEApp(sk, al, k) def eval_pat (p : pattern) (r : cvalue) (e : env) (k : ket) : cvalue = match p, r with | PWild, _ -> disp_ke k e | PVar (x), _ -> disp_ke k ((x, r)::e) | PConstr (c, p2), CVConstr (c2, r2) when (c=c2) -> eval_pat p2 r2 e k | PTuple (pl), CVTuple (rl) -> eval_patlst pl rl e k | PRec (ldp), CVRec (ldr) -> eval_patldp ldp ldr e k def eval_patldp (ldp : (string * pattern) list) (ldr : (string * cvalue) list) (e : env) (k : ket) : cvalue = match ldp with | [] -> disp_ke k e | (d, p)::ldp' -> lookup ldr d KRLdp(p, e, ldp', ldr, k) def eval_patlst (pl : pattern list) (rl : cvalue list) (e : env) (k : ket) : cvalue = match pl, rl with | [], [] -> disp_ke k e | p::l1, r::l2 -> eval_pat p r e KEPat(l1, l2, k)Modes uprec, getf, getn, and gets:⟨CVRec(l dr ), l dt , Σ, k, f ⟩ uprec → ⟨l dt , Σ, KDUp(l dr , k), f ⟩ ldt ⟨_, l dt , Σ, k, f ⟩ uprec → ⟨f ⟩ fk otherwise ⟨CVRec(l dr ), d, k, f ⟩ getf → ⟨l dr , d, k, f ⟩ lkup ⟨_, d, k, f ⟩ getf → ⟨f ⟩ fk otherwise ⟨CVTuple(l r ), n, k, f ⟩ getn → ⟨(nth l r n), k, f ⟩ gets ⟨_, n, k, f ⟩ getn → ⟨f ⟩ fk otherwise ⟨Some(r), k, f ⟩ gets → ⟨k, r, f ⟩ kr ⟨None, k, f ⟩ gets → ⟨f ⟩ fk Mode ldt: ⟨[], Σ, k, f ⟩ ldt → ⟨k, [], f ⟩ kd ⟨(d, t) :: l dt , Σ, k, f ⟩ ldt → ⟨t, Σ, KRLdt(l dt , Σ, d, k), f ⟩ trm Mode lkup: ⟨[], x, k, f ⟩ lkup → ⟨f ⟩ fk ⟨(y, r) :: l, x, k, f ⟩ lkup → ⟨k, r, f ⟩ kr if x = y ⟨(y, r) :: l, x, k, f ⟩ lkup → ⟨l, x, k, f ⟩ lkup if x ̸ = y Mode trml: ⟨[], Σ, k, f ⟩ trml → ⟨k, [], f ⟩ kl ⟨t :: l, Σ, k, f ⟩ trml → ⟨t, Σ, KRList(l, Σ, k), f ⟩ trm Mode sk: ⟨Branching([]), Σ, k, f ⟩ sk → ⟨f ⟩ fk ⟨Branching(S :: l), Σ, k, f ⟩ sk → ⟨S, Σ, k, FSK(Branching(l), Σ, k, f )⟩ sk ⟨LetIn(p, S 1 , S 2 ), Σ, k, f ⟩ sk → ⟨S 1 , Σ, KRLet(p, S 2 , Σ, k), f ⟩ sk ⟨Return(t), Σ, k, f ⟩ sk → ⟨t, Σ, k, f ⟩ trm ⟨Apply(t, l), Σ, k, f ⟩ sk → ⟨t, Σ, KRApp1(l, Σ, k), f ⟩ trm ⟨Exists(p, S), Σ, k, f ⟩ sk → ⟨f ⟩ fkTitre : Transformations de Sémantiques Squelettiques Mots-clés : Sémantiques Squelettiques, Sémantiques Opérationnelles, Grand-Pas, Petit-Pas, Machines Abstraites, Interprétation Certifiée Résumé : Les sémantiques squelettiques sont un cadre logique pour décrire les sémantiques opérationnelles. Tout d'abord, nous présentons une transformation automatique d'une sémantique squelettique écrite en style grand-pas vers une sémantique équivalente en style petitpas. Cette transformation est implémentée dans l'outil Necro, ce qui nous permet de générer automatiquement un interpréteur OCaml pour la sémantique petit-pas ainsi qu'une formalisation Coq des deux sémantiques. Nous certifions la transformation de deux manières : nous donnons une preuve papier du coeur de la transformation, et nous générons des scripts de preuve Coq spécialisés durant la transformation. Nous proposons également une méthode automatique pour générer un interpréteur OCaml certifié pour n'importe quel langage défini en sémantiques squelettiques. Pour cela, nous présentons deux nouvelles interprétations des sémantiques squelettiques, sous la forme de machines abstraites déterministe et non-déterministe. Ces machines sont obtenues à partir de l'interprétation grand-pas principale en utilisant la correspondence fonctionnelle, une méthode connue pour transformer un évaluateur en machine abstraite. Ces nouvelles interprétations sont formalisées en Coq, et nous vérifions leur correction. Enfin, nous utilisons le système d'extraction de Coq vers OCaml pour obtenir un interpréteur certifié. Title: Skeletal Semantics Transformations Keywords: Skeletal Semantics, Operational Semantics, Big-Step, Small-Step, Abstract Machines, Certified Interpretation Abstract: Skeletal semantics is a framework to describe the operational semantics of programming languages.

TABLE OF CONTENTS 1 Introduction

 OF

  State) the set of all arithmetic expressions (resp. boolean expressions, commands, and states). States are partial maps with a finite domain. We note ϵ the empty state, and {x 1 → n 1 ; . . . ; x m → n m } the state mapping each x i to the corresponding n i . For x in the domain of σ, σ(x) is the associated integer. States σ can be extended with a new association x 1 → n 1 . If the variable x is already part of the domain of σ, the extension overwrites the associated integer instead.

2.2. Big-Step Semantics

In the syntax above, c 1 ; c 2 is a sequence of computations; x := a is an assignment operation; and skip can be seen as an empty or finished computation. For expressions, we use explicitly named constructors, e.g., Plus(a 1 , a 2 ), to tell them apart from the corresponding mathematical operation, e.g., n 1 + n 2 .

To evaluate a program, we need a state (σ) mapping variables to integers. We note AExp (respectively BExp, CExp, and

skip⟩

  Proof Sketch. The proof uses an even stronger relation between reduction semantics and abstract machine. We show that machine states ⟨σ, c | κ⟩ e and ⟨κ | σ, c⟩ k are both equivalent to a configuration ⟨σ, E κ [c]⟩, where E κ is the context corresponding to the continuation κ.

	Huizing 10 [31], Vesely ESOP19 [53]
	Big Step	Small Step
	PBS
	Abstract	Reduction
	Machine	Semantics
	Whenever the abstract machine takes a step, either it adjusts the term under focus by
	changing the continuation, as with the rule ⟨σ, Plus(a 1 , a 2 ) | κ⟩ e → ⟨σ, a 1 | Plus([•], a 2 ) :: κ⟩ e ,
	and nothing happens in reduction semantics; or we apply a rule equivalent to a contraction rule
	in reduction semantics, e.g., ⟨x := [•] :: κ | σ, n⟩ k → ⟨κ | σ[x → n], skip⟩ k is equivalent to
	using the contraction rule ⟨σ, x := n⟩ ⇝ ⟨σ[x → n], skip⟩ under the context E κ .
	Once this is formalized, the theorem is only a special case, proved by a simple induction on
	the length of the sequence.

  Types int and bool represent integers and booleans, collected under the type value. The type ident represents identifiers for the variables of the language and state represents environments mapping variables to values. The two program types define the expressions and statements of the language.

	type int	type ident
	type bool	type state
	type expr =	type value
	| Iconst of int	type stmt =
	| Bconst of bool	| Skip
	| Var of ident	| Assign of ident * expr
	| Plus of expr * expr	| Seq of stmt * stmt
	| Equal of expr * expr	| If of expr * stmt * stmt
	| Not of expr	| While of expr * stmt
	Types are defined with the keyword type; base types (int, bool, ident, state, and value)
	are only declared, while program types (expr and stmt) are declared alongside the signature of
	their constructors, like an algebraic datatype definition in OCaml.

Hooks in IMP matching. Branchings are written branch .. (or ..)* end, while the other sorts of skelements (filter call, hook call, return) are not preceded by keywords, as we can easily tell them apart.

		hook hstmt (s : state, t : stmt)
		matching t : state =
		| Skip -> s
		| Assign (x, e) ->
		let (s1, v) = hexpr (s, e) in
		write (x, s1, v)
		| Seq (t1, t2) ->
		let s1 = hstmt (s, t1) in
		hstmt (s1, t2)
		| If (e1, t2, t3) ->
		let (s1, v) = hexpr (s, e1) in
		branch
		let () = isTrue (v) in
		hstmt (s1, t2)
		or
		let () = isFalse (v) in
		hstmt (s1, t3)
		end
		| While (e1, t2) ->
		let (s1, v) = hexpr (s, e1) in
		branch
		let () = isTrue (v) in
		let s2 = hstmt (s1, t2) in
		hstmt (s2, While (e1, t2))
	')	or
		let () = isFalse (v) in
		s1
		end
	Figure 3.1:

  intermediate EBS semantics extends the initial BS one. It contains the user-defined rules for the initial constructors (e.g. Plus), and the rules created during the generation phase of Section 4.2.2 for the added constructors, like the rules for While1 and While2 in Section 4.2.2 or for Plus1 and Plus2 in Example 4.2.2.

  In the examples, we prefix the types of the extended semantics with a letter e, writing for instance estate for extended states, and we write |t| for the the canonical injection of an initial term t into the extended semantics.The complete EBS generated for IMP can be found in Appendix B.5.The first step of the certification checks that EBS and BS agree on the initial terms. It is easy to verify since EBS is a conservative extension of BS: we simply match every behavior-every applied rule-of each big-step semantics with exactly the same one on the other side. The distribution of branchings (Section 4.2.3) might reorganize skeletons a bit, but following a path would give out the same hypothesis, so Coq has no problem matching the two behaviors. The only difficulty is the manipulation of coercions back and forth between initial and extended terms, but a few general lemmas are enough to automate the rewriting.

	Theorem 5.2.2 (BS⇒EBS). For all (s, s 0 : state), (e : expr), (t : stmt), and (v : value),
	(s, e) ⇓ BS hexpr (s 0 , v) =⇒ (|s|, |e|) ⇓ EBS hexpr (|s 0 |, |v|)
	(s, t) ⇓ BS hstmt s 0 =⇒ (|s|, |t|) ⇓ EBS hstmt |s 0 |
	Theorem 5.2.3 (EBS⇒BS). For all (s : state), (e : expr), (t : stmt), (s ′ 0 : estate), and
	(v ′ : evalue),

intermediate EBS semantics extends the initial BS one on all constructors. It contains the (mostly unchanged) user-defined rules for the initial constructors (e.g. Plus), and the rules generated during the processing phase of Section 4.2.2 for the added constructors; see for instance the rules generated for While1 and While2 in Section 4.1.2. Finally, we add a rule for each return constructor to extract its resulting values, such as Ret(v) ⇓ EBS v.

Table 6 .

 6 1: Size of the Generated Semantics and Proof Scripts generated lemmas of the EBS implies SS part of the proof, which themselves depend on the number of different paths in the small-step rules, as explained in Section 5.2.4.

			Lines of Code			Constructors
	Language	Big-Step Small-Step Coq Big-Step Small-Step No Reuse
	Call-by-Name	28	41	110	3	4	5
	Call-by-Value	22	41	100	3	4	5
	CBV, choice	29	48	120	4	5	6
	CBV, fail	42	60	150	5	6	7
	Arithmetic	32	81	160	5	5	13
	IMP	79	144	330	11	13	21
	IMP, write in exp	84	154	350	12	14	23
	IMP, LetIn	85	166	360	12	16	24
	IMP, try/catch	123	192	420	15	17	26
	MiniML	155	299	720	18	28	33

W.r.t. to the number of constructors, we see the impact of reusing them, especially for IMP where we go from two new constructors (While1 and While2) with reuse to ten without.

The effectiveness of reusing constructors depends on how the input skeletal semantics is written. First, it depends on the ordering of the skelements in the initial rules. The process is more efficient when hook calls are grouped at the beginning of skeletons, since we do not reuse constructors after filter calls. For instance, the two following equivalent rules lead to respectively one and two new constructors.

5 Extended Big-Step for Coq Certification

  + e 2 → z 1 , z 2 + e 2 e 1 = (s 1 , v 1 ) s 1 , e 2 → z 3 , z 4 s, e 1 + e 2 → s, (z 3 , v 1 ) + z 4 If(e 1 , t 2 , t 3 ) → z 1 , If(z 2 , t 2 , t 3 ) If(e 1 , t 2 , t 3 ) → s 1 , t 2 , e 0 → z 1 , z 2 s, While1(s 0 , e 0 ,e 1 , t 2 ) → s, While1(z 1 , z 2 , e 1 , t 2 ) While1(s 0 , e 0 , e 1 , t 2 ) → s, While2(s 1 , t 2 , e 1 , t 2 ) While1(s 0 , e 0 , e 1 , t 2 ) → s, (s 1 ) s 0 , t 0 → z 1 , z 2 s, While2(s 0 , t 0 , e 1 , t 2 ) → s, While2(z 1 , z 2 , e 1 , t 2 ) t 0 = (s 2 ) s, While2(s 0 , t 0 , e 1 , t 2 ) → s 2 , While(e 1 , t 2 )

	s) in hook hstmt (s : state, t : stmt) matching t : state = (s, v) | Assign (x, e) -> let (s1, v) = hexpr (s, e) in write (x, s1, v) | If (e1, t2, t3) -> let (s1, v) = hexpr (s, e1) in branch let () = isTrue (v) in hstmt (s1, t2) or let () = isFalse (v) in hstmt (s1, t3) | Seq (t1, t2) -> val isTrue : value -> unit | Ret_hstmt v1 -> v1 end | While of expr * stmt hstmt (s1, t2) val isFalse : value -> unit hook getRet_hstmt (t : stmt) matching t : state = end | Skip let s1 = hstmt (s, t1) in val eq : value * value -> value val intToVal : int -> value | Ret_hexpr (v1, v2) -> (v1, v2) let v = add (v1, v2) in (s, Ret_hexpr (s2, v)) | Seq of stmt * stmt | If of expr * stmt * stmt end | Skip -> s | While (e1, t2) -> let (s1, v) = hexpr (s, e1) in branch let () = isTrue (v) in let s2 = hstmt (s1, t2) in hstmt (s2, (While (e1, t2))) or let () = isFalse (v) in s1 end B.3 After Creating New Constructors type int type bool type ident type state type value type expr = | Not of expr | Bconst of bool | Equal of expr * expr | Iconst of int | Plus of expr * expr | Var of ident | Ret_hexpr of state * value type stmt = | Assign of ident * expr | If of expr * stmt * stmt | Seq of stmt * stmt | Skip | While of expr * stmt | While1 of state * expr * expr * stmt | While2 of state * stmt * expr * stmt | Ret_hstmt of state val add : value * value -> value val boolToVal : bool -> value hstmt (s1, t2) (* tail-call *) s, e 1 + e 2 → s, (s 2 , v 1 + v 2 ) or let () = isFalse (v) in hstmt (s1, t3) (* tail-call *) end | Seq (t1, t2) -> let s1 = hstmt (s, t1) in (* reuse *) hstmt (s1, t2) (* tail-call *) | Skip -> s | While (e1, t2) -> let (s1, v) = hexpr (s, e1) in (* new constr: While1 *) branch let () = isTrue (v) in let s2 = hstmt (s1, t2) in (* new constr: While2 *) hstmt (s2, (While (e1, t2))) (* tail-call *) or let () = isFalse (v) in s1 end | While1 (s0, e0, e1, t2) -> let (s1, v) = hexpr (s0, e0) in (* reuse *) branch let () = isTrue (v) in let s2 = hstmt (s1, t2) in (* new constr: While2 *) hstmt (s2, (While (e1, t2))) (* tail-call *) or let () = isFalse (v) in s1 end | While2 (s0, t0, e1, t2) -> let s2 = hstmt (s0, t0) in (* reuse *) hstmt (s2, (While (e1, t2))) (* tail-call *) B.4 Final Small-Step Skeletal Semantics type int type bool type ident type state type value type expr = | Bconst of bool | Equal of expr * expr | Iconst of int | Not of expr | Plus of expr * expr | Var of ident | Ret_hexpr of state * value type stmt = | Assign of ident * expr | If of expr * stmt * stmt | Seq of stmt * stmt | Skip | While of expr * stmt | While1 of state * expr * expr * stmt | While2 of state * stmt * expr * stmt | Ret_hstmt of state val add : value * value -> value val boolToVal : bool -> value val eq : value * value -> value val intToVal : int -> value val isFalse : value -> unit val isTrue : value -> unit val neg : value -> value val read : ident * state -> value val write : ident * state * value -> state hook getRet_hexpr (e : expr) matching e : state * value = hook hexpr (s : state, e : expr) matching e : state * expr = | Bconst b -> let v = boolToVal (b) in (s, Ret_hexpr (s, v)) | Equal (e1, e2) -> branch let (z1, z2) = hexpr (s, e1) in (z1, Equal (z2, e2)) or let (s1, v1) = getRet_hexpr (e1) in branch let (z3, z4) = hexpr (s1, e2) in (s, Equal (Ret_hexpr (z3, v1), z4)) or let (s2, v2) = getRet_hexpr (e2) in let v = eq (v1, v2) in (s, Ret_hexpr (s2, v)) end end | Iconst i -> let v = intToVal (i) in (s, Ret_hexpr (s, v)) | Not e1 -> branch let (z1, z2) = hexpr (s, e1) in (z1, Not z2) or let (s1, v) = getRet_hexpr (e1) in let v1 = neg (v) in (s, Ret_hexpr (s1, v1)) end | Plus (e1, e2) -> branch let (z1, z2) = hexpr (s, e1) in (z1, Plus (z2, e2)) or let (s1, v1) = getRet_hexpr (e1) in branch let (z3, z4) = hexpr (s1, e2) in (s, Plus (Ret_hexpr (z3, v1), z4)) or let (s2, v2) = getRet_hexpr (e2) in s, b → s, (s, boolToVal(b)) s, e 1 → z 1 , z 2 s, (e 1 ? = e 2 ) → z 1 , (z 2 ? = e 2 ) e 1 = (s 1 , v 1 ) s 1 , e 2 → z 3 , z 4 s, (e 1 ? = e 2 ) → s, ((z 3 , v 1 ) ? = z 4 ) e 1 = (s 1 , v 1 ) e 2 = (s 2 , v 2 ) s, (e 1 ? = e 2 ) → s, (s 2 , (v 1 ? = v 2 )) s, i → s, (s, intToVal(i)) s, e 1 → z 1 , z 2 s, Not(e 1 ) → z 1 , Not(z 2 ) e 1 = (s 1 , v) s, Not(e 1 ) → s, (s 1 , ¬v) s, e 1 → z 1 , z 2 s, e 1 e 1 = (s 1 , v 1 ) e 2 = (s 2 , v 2 ) | Var x -> let v = read (x, s) in (s, Ret_hexpr (s, v)) hook hstmt (s : state, t : stmt) matching t : state * stmt = | Assign (x, e) -> s, x → s, (s, s(x)) s, e → z 1 , z 2 | If (e1, t2, t3) -> | While1 (s0, e0, e1, t2) -> branch let (z1, z2) = hexpr (s0, e0) in (s, While1 (z1, z2, e1, t2)) or let (s1, v) = getRet_hexpr (e0) in branch let (s1, v) = hexpr (s, e1) in branch let () = isTrue (v) in hstmt (s1, t2) or s 0 e 0 = (s 1 , v) isTrue(v) let () = isFalse (v) in branch let (z1, z2) = hexpr (s, e) in (z1, Assign (x, z2)) or let (s1, v) = getRet_hexpr (e) in let z3 = write (x, s1, v) in (s, Ret_hstmt z3) end | If (e1, t2, t3) -> branch let (z1, z2) = hexpr (s, e1) in (z1, If (z2, t2, t3)) or let (s1, v) = getRet_hexpr (e1) in branch let () = isTrue (v) in (s1, t2) or let () = isFalse (v) in (s1, t3) end end | Seq (t1, t2) -> branch let (z1, z2) = hstmt (s, t1) in (z1, Seq (z2, t2)) or let s1 = getRet_hstmt (t1) in (s1, t2) end | Skip -> s, (x := e) → z 1 , (x := z 2 ) let () = isTrue (v) in (s, (While2 (s1, t2, e1, t2))) or let () = isFalse (v) in hstmt (s1, t3) end | Seq (t1, t2) -> s, e 0 = (s 1 , v) isFalse(v) let s1 = hstmt (s, t1) in e = (s 1 , v) s, (x := e) → s, (s 1 [x → v]) s, e 1 → z 1 , z 2 s, e 1 = (s 1 , v) isTrue(v) s, e 1 = (s 1 , v) isFalse(v) s, If(e 1 , t 2 , t 3 ) → s 1 , t 3 s, t 1 → z 1 , z 2 (s, Ret_hstmt s1) end end | While2 (s0, t0, e1, t2) -> branch let (z1, z2) = hstmt (s0, t0) in (s, While2 (z1, z2, e1, t2)) or let s2 = getRet_hstmt (t0) in (s2, (While (e1, t2))) end hstmt (s1, t2) | Skip -> s | While (e1, t2) -> let (s1, v) = hexpr (s, e1) in branch let () = isTrue (v) in let s2 = hstmt (s1, t2) in hstmt (s2, (While (e1, t2))) or let () = isFalse (v) in s1 end | While1 (s0, e0, e1, t2) -> let (s1, v) = hexpr (s0, e0) in s, type int branch type bool let () = isTrue (v) in type ident let s2 = hstmt (s1, t2) in type state hstmt (s2, (While (e1, t2))) type value or s, t 1 ; t 2 → z 1 , z 2 ; t 2 t 1 = (s 1 ) s, t 1 ; t 2 → s 1 , t 2 s, Skip → s, (s) let () = isFalse (v) in type expr = s1 | Not of expr end | Bconst of bool | While2 (s0, t0, e1, t2) -> | Equal of expr * expr let s2 = hstmt (s0, t0) in | Iconst of int hstmt (s2, (While (e1, t2))) | Plus of expr * expr | Var of ident | Ret_hstmt v1 -> v1 (s, Ret_hstmt s) | Ret_hexpr of state * value | While (e1, t2) -> (s, (While1 (s, e1, e1, t2))) s, While(e 1 , t 2 ) → s, While1(s, e 1 , e 1 , t 2 ) type stmt = | Assign of ident * expr B.6

B.

Resulting Small-Step without Reuse

	hook hexpr (s : state, e : expr) matching e : state * expr =
	| Not e1 ->
	(s, Not1 (s, e1)) | Not1 (s0, e0) ->	s, Not(e 1 ) → s, Not1(s, e 1 )
	branch
	let (z1, z2) = hexpr (s0, e0) in
	(s, Not1 (z1, z2))
	or
	let (s1, v) = getRet_hexpr (e0) in
	let v1 = neg (v) in
	(s, Ret_hexpr (s1, v1))
	end
	| Bconst b ->
	let v = boolToVal (b) in
	(s, Ret_hexpr (s, v))
	| Equal (e1, e2) ->
	(s, Equal1 (s, e1, e2))
	| Equal1 (s0, e0, e2) ->
	branch
	let (z1, z2) = hexpr (s0, e0) in
	(s, Equal1 (z1, z2, e2))
	or
	let (s1, v1) = getRet_hexpr (e0) in
	(s, Equal2 (s1, e2, v1))
	end
	| Equal2 (s0, e0, v1) ->
	branch
	let (z1, z2) = hexpr (s0, e0) in
	(s, Equal2 (z1, z2, v1))
	or
	let (s2, v2) = getRet_hexpr (e0) in
	let v = eq (v1, v2) in
	(s, Ret_hexpr (s2, v))
	end
	| Iconst i ->
	let v = intToVal (i) in
	(s, Ret_hexpr (s, v))
	| Plus (e1, e2) ->
	(s, Plus1 (s, e1, e2))
	| Plus1 (s0, e0, e2) ->
	branch
	let (z1, z2) = hexpr (s0, e0) in
	type int (s, Plus1 (z1, z2, e2))
	type bool or
	type ident let (s1, v1) = getRet_hexpr (e0) in
	type state (s, Plus2 (s1, e2, v1))
	end

  This lemma is similar to Lemma C.2.4, but for divergence. See Figure C.2 for the coinductive definition of Σ ⊢ S ⇑. The proof is done by a straightforward induction on S, and case analysis on the rule used. The only two places where the environment Σ is used are:• in rule Div-Pc, where both environments agree on the mapping of the variables t by For all S 1 , x, S 2 , and Σ, if (bv(S 1 ) \

	hypothesis.
	• in the first leaf of rule Div-LetR, where we can use Lemma C.2.4 to go from one environment
	to the other.
	Lemma C.2.6.

  For all S 1 , x, S 2 , Σ, and a, if (bv(S 1 ) \

	by Lemma C.2.5
	{ v} ⊂ bv(S 1 ), so our hypothesis implies ({ v}\{ x})∩fv(S 2 ) = ∅, enough to use Lemma C.2.5.
	Lemma C.2.7.

To be more formal, we would need 6 modes: 3 computation modes and 3 continuation modes, depending on the type of the term.

I.e., a semantics not written using inference rules, but in a form similar to a program, either in pseudo-code or in a meta-language-usually an extended λ-calculus or an ML-like language.

Finally, we also cut the tail-calls to simplify the semantics. This creates administrative smallsteps of the form s, If(True, t 2 , t 3 ) → s, t 2 where no subcomputation takes place. It generates

The naming "SSA" is a slight abuse, as parallel branchings are allowed to define the same variables. However, every execution of the skeleton follows a single branch and never overwrites the content of a variable.

The actual property has a couple additional hypothesis, stating notably that S is part of the semantics and thus has gone through the generation phase, see Lemma C.7.1

However, the legacy Coq export mechanism does not support polymorphic types, so we cannot generate an equivalence certificate.

the hooks h ∈ H 0 . We also merge in the rules R getRet for the hooks getRet_h generated in Section 4.2.1 to create the final small-step rule set R SS . R SS = {h( y, c( x)) := ∥ S ∥ h( y,c( x)):=S ϵ | (h( y, c( x))

The set R SS , defined on C 2 and H 1 (where htype has been updated), is the final result of the transformation. It is a set of rules where every h ∈ H 0 makes a single step of computation.

Chapter 10

NON-DETERMINISTIC ABSTRACT MACHINE FOR SKELETAL SEMANTICS

The first step towards a certified interpreter is to transform the big-step interpretation of skeletons into an abstract machine while keeping the non-determinism of the concrete interpretation.

We thus derive a Non-Deterministic Abstract Machine (NDAM) from the concrete interpretation. While the NDAM obtained in this chapter is not executable, the transformation we present serves as a basis to generate a deterministic abstract machine in Chapter 11. Also, we use the NDAM in Coq as an intermediate semantics to certify the correctness of the final deterministic abstract machine with respect to the initial concrete interpretation.

We follow the textbook strategy of functional correspondence presented in Chapter 9. After rewriting the concrete interpretation from inference rules to pseudo-code (Section 10.1), we perform a CPS-transformation (Section 10.2), a phase of defunctionalization (Section 10.3), and create the abstract machine proper and its evaluation modes (Section 10.4). Finally, Section 10.5 presents the Coq equivalence result between the resulting non-deterministic abstract machine and the rules of Figure 8.2.

In each code snippet illustrating the different phases of the transformation, we use a gray background to indicate changes relative to previous sections. All intermediate phases of this derivation are available in Appendix D.1.

Pseudo-interpreter

We translate the concrete interpretation into pseudo-code, using an OCaml-like syntax [START_REF] Leroy | The OCaml system: Documentation and user's manual, version 4.12[END_REF] where natural numbers have constructors Z and S. We translate each predicate of Figure 8.2 into evaluation functions, resulting into the following pseudo-code for skeletons: let eval_sk (sk : skeleton) (e : env) : cvalue = match sk with | Branching (skl) -> (* oracle picking the correct skeleton *) let sk' = pick skl in eval_sk sk' e Sketch. For instance, for the constructor LetIn:

Applying the induction hypothesis on each premise gives us:

(1) ⟨S 1 , Σ, kr id ⟩ sk → * ⟨kr id , r ′ ⟩ kr

(2) ⟨p, r ′ , Σ, ke id ⟩ pat → * ⟨ke id , Σ ′ ⟩ ke

(3) ⟨S 2 , Σ ′ , kr id ⟩ sk → * ⟨kr id , r⟩ kr

We then merge them to reconstruct a sequence, held by a few additional steps. The identity continuations are lifted using Lemma 10.

For the reverse implication, we make explicit where continuations are needed by splitting sequences of steps at the point where the continuation is actually focused on. Up to that point, the continuation could be replaced by a basic continuation, as stated in the following result. If the sequence does not use the continuation (e.g., ⟨LetIn(p, S 1 , S 2 ), Σ, k⟩ sk → ⟨S 1 , Σ, KRLet(p, S 2 , Σ, k)⟩ sk ), there is no splitting point for us to exploit. To avoid this case, the lemma assumes the sequence leads to a final state, ensuring the continuation is used.

Lemma 10.5.3. For all S, Σ, k, n, and final state b, if we have ⟨S, Σ, k⟩ sk → n b then there exist n 1 , n 2 , and r such that ⟨S, Σ, kr id ⟩ sk → n 1 ⟨kr id , r⟩ kr , ⟨k, r⟩ kr → n 2 b, and

The proof is by strong induction on the length of the sequence of steps. Intuitively, it holds because we stop at the first step pattern-matching the initial continuation; the steps before the cut are still valid after changing the unused continuation.

With this lemma, we can tackle the soundness part of the certification.

Theorem 10.5.4. For all S, Σ, and r, if ⟨S, Σ, kr id ⟩ sk → * ⟨kr id , r⟩ kr , then Σ ⊢ S ⇓ s r.

Chapter 11

DETERMINISTIC ABSTRACT MACHINE FOR SKELETAL SEMANTICS

The abstract machine of Chapter 10 is non-deterministic, and thus does not offer a computable semantics for the empirical evaluation of skelterms and skeletons. In this chapter, we show how to obtain a deterministic machine using explicit backtracking. The deterministic machine returns at most one of the possible results; computing all of them is impossible in presence of non-terminating executions. We choose to try branches in order and to backtrack if necessary.

To this end, we consider a more complex CPS transformation with a failure continuation (Section 11.1). We then derive the resulting deterministic abstract machine through defunctionalization in Section 11.2, and present the Coq certification of its soundness in Section 11.3.

CPS-Transform

Starting from the big-step evaluator of Section 10.1, we modify every evaluation function to take two continuations [START_REF] Danvy | Abstracting Control[END_REF]:

• a success continuation k as in Section 10.

Related Work

The technique of functional correspondence has been applied by hand to many languages with many different features. Automatic application of the technique is only very recent. Buszka and Biernacki [START_REF] Buszka | Automating the Functional Correspondence between Higher-Order Evaluators and Abstract Machines[END_REF] present an algorithm and a tool to automatically generate abstract machines from evaluators, based on the same transformations with a more complex static analysis. We probably could have used their tool to create the NDAM, but the AM would have required the introduction of backtracking by hand. Their approach is general, in the sense that they can automatically generate an abstract machine for any language. On our side, we created a single AM for the meta-language Skel, that can then be automatically specialized for any language.

They did not certify their tool, while we prove the soundness of our AM in Coq.

In [START_REF] Courant | Necro: Animating Skeletons[END_REF], the authors propose a tool to automatically generate a shallow embedding of a skeletal semantics into an OCaml interpreter. Skel types are directly translated into OCaml types, and skelterms into OCaml functions. Because the meta-language Skel is completely transparent, the interpreter is at the level of the language we are interested in, and more intuitive to use.

However, unlike our approach, their ad-hoc translation is not certified and offers no guarantees on the behavior of the interpreter.

Appendix A

FUNCTIONAL CORRESPONDENCE ON IMP

A

A.4 Abstract Machine

Mode ka:

PROOF OF THE TRANSFORMATION

We present a pen-and-paper proof that the transformation of Chapter 4 is correct, in the simplified case where we do not perform the analysis of Section 4.2.2 and systematically create new constructors.

C.1 Definitions and Proof Structure

We recall the rules for the inductive ( We start with a few necessary definitions used in the different proofs.

We also recall notations from the transformation phases (Section 4.2). We use the following.

for the monadic bind of two skeletons;

• S r E for the generation of new constructors, skeletons, and big-step rules (after adding a "New c" annotation to every hook call);

• ⌈S⌋ for the distribution of branchings;

• ∥ S ∥ r for the final small-step transformation, redefined with simpler rules in Figure C.3.

A first section (C.2) covers simple lemmas about our different definitions, unconnected to the different rule sets. Afterwards, we check that our SSA property is preserved throughout the transformation (C.3). Then we certify important properties of the different phases of the transformation, to make sure they behave as intended (C.4). Using these results, we finally prove the equivalences between the different semantics. Section C.5 covers the equivalence between Big-

Step and Extended Big-Step. Section C.6 certifies that an EBS evaluation/divergence implies a small-step (in)finite sequence. Finally, Section C.7 proves the reverse direction: a small-step (in)finite reduction sequence implies an EBS evaluation/divergence.

C.2 Basic Lemmas

The simple proofs of the first few trivial lemmas are omitted.

Lemma C.2.1. For all S 1 , v, S 2 , w, and S 3 ,

Then we have our result by applying our induction hypothesis a second time with

) and we have our result from our induction hypothesis on S ′ 0 .

Lemma C.2.9. Forall S 1 , v, S 2 , E, and K H , where K H is not a branching, if

Proof. By structural induction on S 1 . All cases are straightforward. The only interesting case is when

) as we need to use our induction hypothesis twice.

In this case, by definition:

We use our induction hypothesis a first time knowing that K H ̸ ∈ S ′ i , and we get

Then we use our induction hypothesis a second time, knowing K H ̸ ∈ S ′ , to get the desired result.

C.3 SSA

Proof. By simply following Definitions 3 and 4.

Remark. Note that the reverse of the first point is not true. See for instance S 1 = (let y = K in S) and S 2 = Return y.

Proof. First, by unfolding definitions, note that we always have:

• NoRedef(Branching(S 1 )) ⇐⇒ NoRedef(S 1 )

• bv(Branching(S 1 )) = bv(S 1 )

• fv(Branching(S 1 )) = fv(S 1 )

We proceed by structural induction on

). We can easily unfold the definitions and check the desired results hold, using the three points above.

Else S 1 is of the form (let w = K in S).

Cutting our hypothesis gives us:

We have bv(S 1 ) = w ∪ bv(K) ∪ bv(S) so:

We can reformulate into:

) by checking the definition. Using our induction hypothesis, we have:

Then, we have enough pieces to prove our desired result, once again by directly checking the definition: (getRet_h(Ret_h( x))

The following two lemmas show the small-step semantics exhibits the usual congruence rules for hook calls labeled Reuse.

Proof. Let r be the rule in the formulation of the lemma.

Using our induction hypothesis on the first leaf we get (

We can also use our induction hypothesis on the second leaf (with S) to get d such that

Finally, using Lemma C.4.8 (and Lemma C.2.4 to rewrite the environment), we have

( a, d), and we just have to assemble the three pieces by transitivity.

• Otherwise, if c is an initial constructor, the bottom of the tree derivation is of the form:

Where Σ 0 = {y → a} + {x → a ′ } and S does not start with a label Reuse.

We use our induction hypothesis with Σ

). Now we can complete our goal as such:

▶ For the second result, we assume

S has no Reuse label Note that we have SSA(S 0 ), either from assumption for the initial skeletons, or by using Lemmas C.3.5 and C.3.4 for new skeletons. Thus we know extending the environment Σ will not change the mapping of variables y.

As said previously, we proceed by induction on the derivation of Σ ⊢ S ⇓ R EBS b.

• If S is of the form Return t, then we choose d = Ret_h( b):

• If S is of the form Branching (S 1 , . . . , S n ), using the induction hypothesis with S i :

• If S is of the form let v = Filter f t in S 0 , using the induction hypothesis with S 0 :

Let us name Σ( t) = ( e, e t ). We use our induction hypothesis on the first leaf and get ( e, e t ) (⇓ R SS h 1 ) * ( e, Ret_h1( b ′ )). We also use our induction hypothesis on the second leaf and we have

With d = c 0 (Σ(t), Σ(z)) = c 0 ( e, e t , Σ(z)), we have ∥ S ∥ r = Return ( y, c 0 ( t, z)) and thus:

To conclude, we are missing ( a, d) (⇓ R SS h ) * ( a, d 0 ) which can be cut into two pieces:

. . .

. . .

The first part comes directly from iterating Lemma C.4.6. Also, Lemma C.4.8 gives us:

Which is enough for the second part, using

We know tackle the infinite case. The proof can be separated in two successive lemmas.

Lemma C.6.2. For all h, y, c, x, S 0 , Σ, S, and a,

In other words, if evaluating S diverges during a (big-step) evaluation, then ∥ S ∥ r can be (small-step) reduced into an intermediate configuration ( a, d) whose (big-step) evaluation also diverges.

Proof. We prove this by induction on S.

▶ If S is of the form Branching (S 1 , . . . , S n ), using the induction hypothesis with S i :

, where K is a filter or a return, using the induction hypothesis with S 0 : We choose d = c 0 (Σ( t), Σ( z)), we have ∥ S ∥ r = Return ( y, c 0 ( t, z)) and thus:

We also need to check ( a, d) ⇑ R EBS h

. For this we have the following tree: If evaluating S 0 diverges, we go from Σ to Σ ′ using Lemma C.2.5.

The second result says that, if ∥ S ∥ r (small-step) reduces to a configuration ( a, d) that (big-step) evaluates to b, then we can create a big-step evaluation of S to b.

We reason by mutual induction on ( a, c( a ′ )) ⇓ R SS h b and on S.

▶ First result, by induction on ( a, c( a

⊛ If a ′ 1 is not of the form Ret_h1(. . .), then the small-step reduction uses the first branch and our small-step hypothesis corresponds to the following tree.

Σ ′ ( y, c( u, z)) = ( a, d)

b, corresponding to the tree:

We can use our first induction hypothesis to combine (

where we go from Σ 1 to Σ 0 with Lemma C.2.4, since w does not appear in S 0 .

), then the small-step reduction uses the second branch. We can single out the last variable of the hook call to simplify notations:

Our first hypothesis then corresponds to the following tree.

We can now use our second induction hypothesis (with S 0 and the second leaf) to get Σ 0 + {v → b ′ } ⊢ S 0 ⇓ R EBS b, which allows us to create our goal derivation:

h( y, c(( w, w), z)) := . . .

where the other leaf comes from Lemma C.4.4.

• If c is an initial constructor, the bottom of the tree derivation is of the form:

Where r = (h( y, c( x)) := S) ∈ R EBS , S does not start with a Reuse label, and Σ 0 = {y → a} + {x → a ′ }.

We use our induction hypothesis on the leaf, and we complete our goal as such:

We perform an induction on S.

• If S is of the form Return t:

• If S is of the form Branching (S 1 , . . . , S n ), using the induction hypothesis with S i :

, where K is a filter or a return, using the induction hypothesis with S 0 :

Proof. Since a ⇑ R EBS h is defined mutually with Σ ⊢ S ⇑ R EBS , we show two properties at the same time:

We note the predicates ▶ For elements of Q. We assume ( a, c( a ′ ))

• If c is an initial constructor, then R EBS contains a rule r of the form h( y, c( x)) := S 0 where S 0 has no label Reuse. We note Σ = {y → a} + {x → a ′ }. Splitting the first small-step of the infinite reduction, we have b such that Σ ⊢ ∥ S 0 ∥ r ⇓ R SS b and b ∞ → h . We immediately have (Σ, S 0 ) ∈ Q ′ using r and S 0 ◁ S 0 , so we can start constructing a derivation as follows. where S 0 has no label Reuse. We have a ′ = ( a 1 , a 2 ), we note Σ = {y → a}+ {w → a 1 }+ {z → a 2 }.

(h( y, c( x))

We use Lemma C.7.3 to perform a case disjunction on the behavior of a, c( a 1 , a 2 )

→ h 1 , then we can start constructing our goal as such:

) and ( a, c(. . . ,

Using Lemma C.4.9 on the second point (after splitting a step), we get there is b such that Σ

We can add the mapping of the unused variables w with Lemma C.2.4, and so (Σ + {v → a ′ 1 }, S 0 ) ∈ Q ′ using rule r, and we can construct our goal as such: ▶ For elements of Q ′ . We assume

S has no Reuse label First, we can see certain cases for S are not possible. S cannot be a hook or filter, because we delayed returns to create the set R EBS . Also, S cannot be a return, because b would be of the form (. . . , Ret_h(. . .)) and b ∞ → h contradicts this.

We have (Σ, S i ) ∈ Q ′ using the same rule and terms, since S i ◁ S ◁ S 0 . We can then start the derivation of the goal as follows.

where K is a return or a filter, then there is b

and can start the derivation of the goal as follows.

for filters and returns, as evaluating these skelements do not look up any rule.

Let a 0 = Σ( y), a 1 = Σ( t), and a 2 = Σ( z). We have ∥ S ∥ r = Return ( y, c 0 ( t, z)) and so b = ( a 0 , c 0 ( a 1 , a 2 )). We use Lemma C.7.3 to perform a case disjunction on the behavior of

then we can start constructing our goal as such:

) and ( a 0 , c 0 (. . . ,

1 . Using Lemma C.4.9 on the second point (after splitting a step), we get there is b

We can add the mapping of the unused variables w with Lemma C.2.4, and so (Σ + {v → a ′ 1 }, S ′ ) ∈ Q ′ using rule r ′ , and we can construct our goal as such: