Stéphane Devismes

Stefania Gabriela Dumbrava

Stéphane Grumbach

David

Il me semble qu'il ne serait pas raisonnable de ne pas attaquer ces remerciements par une double negation celles et ceux qui ont pris sur leur temps pour venir m'écouter, voire lire le reste de ce document. Merci donc d'abord à Évelyne Contejean et Damien Pous pour leur minutieux travail de relecture. Du reste, les différents contacts que j'ai eus avec vous deux au cours de la thèse m'ont souvent aidé quand j'étais perdu dans mes doutes, merci pour ce soutien providentiel.

Merci également à

L'expérience fut plus courte, mais je tiens aussi à remercier l'équipe de l'EJCP pour son accueil d'une très grande qualité. Cette semaine à Strasbourg m'aura fait le plus grand bien. J'ai eu la chance de pouvoir moi-même enseigner pendant la thèse. La tâche est (étrangement) bien résumée par Renaud Van Ruymbeke dans ses mémoires : "Instruire, c'est d'abord écouter et dialoguer ; ce n'est pas juger, mais chercher, formuler des hypothèses tout en se méfiant de ses intuitions, remettre en question, comprendre et, enfin, démontrer et expliquer. Le dialogue est essentiel".

Merci à Adeline, Alice (x2), Ahmed, Beyza, Camille, Chinatsu, Chloé, Cindy, Edouardo, Elise, Flora, Inae, Jiaqi, José, Maï-Ly, Mana, Margot, Mathilde, Navaoreethi, Nicolas, Ninoh, Orlando, Salomé, So-Hoon, Tiffany, Yanis et bien d'autres pour ce dialogue, et plus généralement d'avoir été de fantastiques étudiants et étudiantes. Vous voir apprendre (ou non), vous amuser (ou non), exercer votre curiosité (ou non) et progresser (ou non) fut non seulement intellectuellement très stimulant, mais surtout une grande source de fierté et de bonheur pour moi. Il va sans dire que je vous souhaite tout le meilleur pour la suite de vos études et vies respectives.

La thèse n'est pas le début de l'aventure (on fait en général un master avant), mais elle n'en est pas non plus la fin. Merci donc pour leur accueil à Stéphane, Olivier, Tony, Céline, Narjes, Etienne, Michel Eric, Erika, et tous les autres que j'ai désormais la chance d'avoir comme collègues.

Alex, Elina et Maël, merci pour votre fidélité, votre écoute, et tous ces week-ends normands. Le Havre n'est peut-être pas la plus belle ville de France (ni la deuxième ou la troisième), mais y aller est systématiquement pour nous une sacrée expérience.

Jules est une personne tellement extraordinaire qu'il serait insensé de ne pas le faire apparaître deux fois dans ces remerciements. Cependant, toute personne me connaissant sait qu'il serait virtuellement impossible d'énumérer tout ce que je lui dois, et que l'amitié que je lui porte est de toute façon depuis longtemps inconditionnelle.

Un grand merci à Sylvie, Hervé, Lucien et Gabin, bien sûr pour m'avoir accueilli dans leur famille comme si j'en avais toujours fait partie, mais aussi et surtout de m'avoir fait découvrir les fouées.

Impossible de ne pas rendre hommage à mes parents, qui n'ont eu de cesse d'oeuvrer à mon bonheur. Si ce dernier ne se résume -heureusement -pas à la bonne conduite d'une thèse, force est de constater que l'éducation qu'ils m'ont donnée fut déterminante dans la préservation de ma santé mentale pendant ces années.

Merci à Julie d'être venue voir Suspiria ce mercredi de juin 2018, et un très, très grand merci à tous les autres de s'être absentés.

Enfin, un sincère merci à Hafça et Zeinab, sans qui ma vie serait aujourd'hui bien différente -et sans doute substantiellement moins rigolote. lol

General-audience abstract

A 1995 Wired article titled "How Anarchy Works" states that "Part of what has made the Net successful is precisely that: it works". This is a good reflection of the general philosophy behind network engineering, which is much more built via concrete experimentations than formal reasoning.

Although efficient, this approach did not scale with the exponential digitalization of the last decades. Network failures, which are bound to happen with the level of care historically put into network design, are becoming more and more costly, if not critical. This situation led to the introduction of complex, but much sounder methods -generally refered to as formal methods -to networking ten to fifteen years ago. However, even the tools brought by this line of thought sometimes contain gaping holes that need to be addressed. This thesis identifies a tool that reaches a reasonable efficiency thanks to dubious shenanigans, and presents a formally defined and verified alternative.

Context and motivation

Over the last decades, the world has gone more and more digital. This trend was not refuted in 2020 or 2021, as professional and personal services are increasingly provided and accessed through computers, tablets and mobile devices. This intense digital shift means that network failures are more costly and prejudicial than ever1 , let alone critical in many instances2 . We emphasize that the failures we mention and are interested in do not result from external attacks -which do occur on a weekly, if not daily basis and in industrial proportions -, but are to be seen as bugs.

These bugs stem, first and foremost, from the extremely high complexity of network design, which in turn comes from the intrinsically distributed nature of networks. Moreover, networking has run for a long time on a duct tape culture, in the sense that it lacked formal foundations, and the possibilities that the existence and study of such foundations unlock.

Over the last ten to fifteen years, researchers with a background in programming language theory have started to take an interest in networking, and how they could apply their theoretical tools and approaches to this field. Combined with the critically increased need for safety (and security), this situation led to the introduction of formal methods for networks. This trend is also fostered by the latest advances in formal methods, both in terms of modeling techniques and computational efficiency (e.g. the existence of fast solvers such as Z3).

One such tool that was introduced is Network Optimized Datalog (NoD), a Datalog engine developed at Microsoft and tailored to handle programs that describe, in the form of Horn clauses, the behavior of a particular network. Although an interesting step in the desired direction, using this engine requires engineers to manually write encodings of each analyzed network, which in itself is a complex and error-prone process.

Moreover, NoD does not scale with naive translations of real-size networks. In practice, the authors work with programs that contain many inlined values, using manual, convoluted, undocumented and unjustified Datalog-level program transformations. This gap in an otherwise remarkable tool led us to work on the design and automatization of such program transformations, this time with a full formalization.

However, having a formalization of non-trivial operations is not enough to trust them. The aim of our work has then been the formal verification of these transformations in the Coq proof assistant, using (and slightly extending) an existing Coq implementation of Datalog.

Although inspired by network verification, our work is not circumscribed to it. Concretely, the analyses and rewritings we provide can be used -and relevant -in other contexts. Moreover, we believe that this works brings a new insight into the semantics and formal study of Datalog programs, which may serve as the basis of future works in other contexts.

Contribution(s)

Questions and results

The starting point of this thesis was the identification and analysis of a caveat in the Network Optimized Datalog engine in the presence of primitive predicates with multiple variables. To address this issue, we designed a static analysis for Datalog, as well as two program transformations that leverage it. Both the static analysis and the transformations have been verified in the Coq proof assistant, using a previously introduced Coq formalization and implementation of Datalog.

Our work required and led to the extension of some tools, mainly the introduction of a trace semantics for Datalog and its verified implementation in the aforementioned Coq formalization of Datalog. We also develop some new finite types for the Mathematical Components library, upon which this formalization relies.

Finally, we present a tighter version of our static analysis and show that it is not fit for every Datalog program. This leads us to try to caracterize the precise class of Datalog programs that supports it, but our intuition is yet to be formally verified.

List of publications

The work on this thesis resulted in the following contributions to the scientific discussion:

• a talk at the 2020 Coq workshop on the development of new finite types for the Math-Comp library [Bégay et al., 2020a] • a paper at the 19 èmes journées approches formelles dans l'assistance au développement de logiciels (AFADL 2020) conference [START_REF] Bégay | Octant, la vérification réseau simplifiée[END_REF] • a paper at Certified Programs and Proofs (CPP) 2021 [START_REF] Bégay | Developing and certifying Datalog optimizations in Coq/Mathcomp[END_REF]

Toolkit

The work presented in this dissertation has been performed using a variety of existing tools. The details of some (e.g. Datalog, Network Optimized Datalog, DatalogCert) play an important part in the rest of this document, meaning that they are fully introduced in subsequent chapters.

On the other hand, some tools are either too rich and well-known (i.e. Coq or Z3) or secondary (i.e. OpenStack) to require such a detailed presentation, and will be considered strictly from a user-perspective. In particular, we will not delve into their theoretical foundations or implementations, although some references are provided. These tools and the part they played in our work are outlined below.

Building theorems and proofs -Coq & MathComp

Coq is a well-established proof assistant, that contains a functional programming language as well as logical tools to reason about the developed programs and, more generally speaking, formaly defined systems. It has been used with much success in the development and verification of compilers [START_REF] Appel | Separation logic for small-step cminor[END_REF], Chlipala, 2010, Kumar et al., 2014, Letan and Régis-Gianas, 2020, Bodin et al., 2018], most notably the verified C compiler CompCert [Leroy, 2009, Appel et al., 2014]. It is also the framework in which we develop and verify the contributions presented in this document.

Another area where Coq has shined is the formal proof of more traditional mathematical results [START_REF] Cruz-Filipe | C-corn, the constructive Coq repository at Nijmegen[END_REF], O Connor, 2005, Bauer et al., 2017, Zsidó, 2013, Makarov and Spitters, 2013, Beeson et al., 2018, de Rauglaudre, 2017]. One of the main achievements in this field is the formal and verified proof of the four colour theorem [Gonthier, 2007], which led to the development of a new library, called Mathematical Components, or MathComp [START_REF] Gonthier | A Small Scale Reflection Extension for the Coq system[END_REF], which we used extensively in the course of our work. This dissertation assumes some familiarity with Coq, or at least another proof assistant, but not MathComp. In the eventuality that it should find its way into the hands (or screen) of someone with no prior knowledge of a proof assistant, Appendix A provides an introduction to Coq, from a very practical and user's perspective. This light introduction may not be exhaustive enough to follow the full details of our work, but should be sufficient to get the grasp of the main ideas behind it. A much more complete presentation of Coq, including theoretical foundations, can be found in [START_REF] Castéran | Interactive theorem proving and program development. Coq'Art: The Calculus of inductive constructions[END_REF]. The subset of MathComp relevant to this work will be introduced in Section 3.1.

Coq is not the only proof assistant or theorem prover available, as it coesxists with the HOL family (Isabelle/HOL [START_REF] Nipkow | Isabelle/HOL -A Proof Assistant for Higher-Order Logic[END_REF], HOL-Light [Harrison, 2013]), 9 This led Pierre Crégut, R&D engineer at Orange Labs and co-advisor of this work with Jean-François Monin, to work on a higher-level network verification tool built upon Network Optimized Datalog, called Octant. Unlike NoD, Octant separates the specification of general network properties and the description of specific networks, in the sense that it expects the former and checks it against the latter.

Octant uses a compotent of OpenStack called Neutron, which defines its mission as "provid[ing] on-demand, scalable, and technology-agnostic network abstraction"4 . More concretely, Neutron can be used to easily extract low-level informations about the network, such as the forwarding tables of switches, in an structured manner. Octant uses it to fetch the implementation of the analyzed network, and provides these informations to the deduction engine.

The work described in this dissertation has been integrated to Octant, which is described more in-depth in Chapter 6.

Outline

This dissertation is split into four parts, excluding the introduction (Part I) and conclusion (Part VI). The work presented being at the intersection between two different research areas (network verification and programming language theory), the first two parts are dedicated to the introduction of relevant tools and concepts from both. The other two parts then introduce and discuss our contributions.

Part II addressses the networking component of this work. First, it provides some context by outlining existing approaches in network verification. It then focuses on the Network Optimized Datalog (NoD) and Octant tools, which were the starting points of our work. In particular, we explain how the latter builds upon the former to provide more genericy, but is limited in doing so by the internals of NoD.

Part III first recalls the building blocks of first-order logic, and then uses them to formally define the Datalog logic programming language. Finally, it outlines a previously existing verified implementation of Datalog within the Coq proof assistant. This in-depth formalization of Datalog will serve as the reference specification and implementation for the rest of our work.

Part IV discusses why we had to extend some tools, and how it was done. More concretely, we introduce a trace semantics for Datalog and certify it in Coq, and develop some new finite types for the MathComp library.

Finally, Part V presents the static analysis and the two rewritings we designed, as well as their verification. It also contains a general discussion on these points, including the lessons learned from the Coq certification process.

Part II

Verified implementation of a logic programming language: Datalog This thesis being about the logic programming language Datalog, we need to formally define it. To do so, we first recall the inner workings of first-order logic, upon which Datalog is built, in Chapter 1. We then move on to the pen and paper definition of Datalog in Chapter 2. Since our work is developed on top of a Coq formalization of Datalog [Dumbrava, 2016], we reuse their definitions -which are themselves based upon [Lloyd, 1987b] -, although some minor modifications are made. We finally present in Chapter 3 the core of [Dumbrava, 2016], i.e. their Coq formalization of Datalog [START_REF] Benzaken | Data-logCert[END_REF].

Chapter 1

First-order logic

La vie serait tout de même beaucoup plus simple si tout le monde s'exprimait en logique du premier ordre Zeinab Galal, conversation privée

In his leçon inaugurale at the Collège de France [Leroy, 2019], Xavier Leroy emphasizes on the central and paramount role of logic throughout computer science1 . Most logical tools are built upon first-order logic, which is also fundamental in database theory.

The core feature of databases is to combine informations it stores to answer queries provided by a user. In traditional database theory, the dialog between user and database, in particular the formulation of query, relies on the logic-based relational calculus [Codd, 2002] -although it should be noted that some modern approaches to databases, such as NoSQL, shift away from logic to focus on graph-based or object-oriented methods [Dean andGhemawat, 2008, Abiteboul et al., 2011] -, as well as unification mechanisms that can be traced back to the early works of Jacques Herbrand [Herbrand, 1930].

The combination of informations is akin to logical deduction, and in particular relevance of the resolution principle [Robinson, 1974] was early noted [Minker, 1988, Kuhns, 1967, Levien and Maron, 1965, Green and Raphael, 1967]. Going further, [Van Emden and Kowalski, 1976] introduced the foundations of logic programming, which mechanized even further the use of logic in database settings. This line of work eventually produced the Datalog programming language, which will be studied in this document after the basics of first-order logic are recalled.

Sections 1.1 and 1.2 formalize its syntax and semantics, respectively. Then, Section 1.3 introduces the technical but useful concept of normal form, and Section 1.4 presents some inference systems within first-order logic.

Syntax

This Section formalizes the syntax of first-order logic. To do so, we first go over the way first-order logic formulae are built, and then how they are manipulated and transformed to be reasoned about.

Building blocks

First-order logic (FOL) usually brings to mind its well-known quantifiers, but a thorough study of the topic starts with the atomic, non-logical elements of the language. These are defined in the framework of so-called signatures.

Definition 1.1. A first-order Signature is a triple (F, P, ar). F is a set of function symbols, that are used to build the arguments for the predicates. The predicate symbols are found in P, and these two sets are disjoint. The good use of these function and predicate symbols is insured by the ar : F ∪ P → N function, which assigns an arity to every symbol.

Notation 1.2. For clarity, the function and predicate symbols can be augmented with their arity. For example, given the f and p symbols with ar(f ) = n and ar(p) = m, we can replace f and p by f /n and p /m . In that setting, the signature is simply defined as Σ ≡ (F, P).

Notation 1.3. A function symbol f such that ar(f ) = 0 is called a constant, and the set of constants is written C. A predicate symbol p such that ar(p) = 0 is called a propositional variable.

Example 1.4. Peano arithmetic is defined upon signature (F = {0 /0 , s /1 , + /2 , × /2 }, P = {= /2 }).

In that setting, an expression such as (1 + 2) × 3 is translated as ×(+(s(0), s(s(0))), s(s(s(0)))), which does enforce the arity constraints.

On top of the vocabulary -i.e. predicate and function symbols, as well as constants -of our first-order language L, we can introduce the grammar of first-order logic via the additional symbols:

• a countable set of variables X ;

• the existential and universal quantifiers, respectively denoted as ∃ and ∀;

• the connectives ∧ (conjunction), ∨ (disjunction), ¬ (negation), ⇒ (implication) and ⇔ (equivalence), as well as the (nullary) truth symbols ⊤ (true) and ⊥ (false);

• parentheses and punctuation.

Now that we have all the relevant symbols, we can build up to the actual first-order formulae, starting with the words of the language, called terms:

Definition 1.5. The set of L-terms is the minimal set T Σ (X ) that contains the variable set X , and satisfies for any f /n in F, if t 1 , ..., t n are in T Σ (X ), then so is f (t 1 , ..., t n )

In other words, the terms are the constants, the variables, and the function symbols applied using a number of terms corresponding to their arity.

Example 1.6. Reusing the signature of Example 1.4 with the variables X = {x, y},

• x, a variable, is a term;

• 0, a constant, is a term;

• s(0), ×(y, s(0)), and +(x, ×(y, s(0))), via three successive applications of the recursive rule, are terms.

The terms are then used in conjunction with predicate symbols to build so-called atoms, which are then enriched to form the set of base sentences of the language, called atomic formulae.

Definition 1.7. An atom is a predicate symbol applied to a number of terms corresponding to its arity, i.e. p(t 1 , ..., t ar(p) ) with p ∈ P and t i ∈ T Σ (X ) for all i.

Definition 1.8. An atomic formula is either an atom, or one of the two special symbols ⊤ and ⊥, also called true and false, respectively.

We now have all the building blocks that can be combined with the logical symbols to build the actual first-order formulae.

Definition 1.9. The set of L-formulae is the minimal set F Σ (X ) that contains the atoms built upon P and T Σ (X ) and satifies the two following rules:

• the L-formulae are stable under binary logical connectors, i.e. if φ 1 and φ 2 ∈ F Σ (X ), then φ 1 φ 2 ∈ F Σ (X ), with ∈ {∧, ∨, ⇒, ⇔}.

• the L-formulae are stable under negation and quantification, i.e. if φ ∈ F Σ (X ), then ¬φ, ∀x, φ and ∃x, φ ∈ F Σ (X ), where x ∈ X Example 1.10. Using X = {x, y}, P = {= /2 } and F = {0 /0 , s /1 , + /2 , × /2 }, the following two sentences are first-order logic formulae:

• ∀x, ∃y, = (+(x, ×(y, s(0))), 0)

Using infix and standard integer notations: ∀x, ∃y, x + (y × 1) = 0

• ∀x, ∀y, ⇒ (= (x, ×(s(s(0)), y)), = (s(s(x)), ×(s(s(0)), s(y))))

Using infix and standard integer notations: ∀x, ∀y, x = 2 × y ⇒ x + 2 = 2 × (y + 1)

The previous definitions are summed up in the following grammar:

Terms t ::= x ∈ X | c ∈ C | f (t 1 , ..., t n ), f ∈ F & ar(f ) = n Atomic Formulae A ::= ⊥ | ⊤ | p(t 1 , ..., t n ), p ∈ P & ar(p) = n Complex Formulae φ ::= A | φ 1 φ 2 , ∈ {∧, ∨, ⇒, ⇔} | ¬φ | ∀x, φ | ∃x, φ
This thesis focuses on the logic programming language Datalog, which only relies on a subset of first-order logic. One of the main restrictions is the exclusive use of clausal formulae.

Definition 1.11. A literal is a positive or negated atomic formula.

Definition 1.12. A clause is a disjunction of literals.

Remark 1.13. A clause L 1 ∨ ... ∨ L n can be written in an implicative but semantically equivalent form (¬L 1 ∧ ...

∧ ¬L i-1 ∧ ¬L i+1 ∧ ... ∧ ¬L n ) → L i
One of the main restrictions of Datalog is its exclusive use of such clauses. Another restriction is the absence of function symbols, i.e. F = ∅. The syntax of Datalog will be formally introduced in Section 2.1.

Manipulating formulae

The variables appearing in a first-order logic formula are a stand-in for many potential values. Reasoning about such formulae will then require tools to perform the replacement, or substitution, of variables by terms.

We build up to that concept, starting with a categorization of variables based on their quantification.

Definition 1.14. The set F V (t) of free variables of a L-term t is defined as:

• F V (x) = {x}, where x is a variable;

• F V (f (t 1 , ..., t n )) = n i=1 F V (t i )
, where all t i are terms and f a function symbol.

Definition 1.15. A L-term t is ground, or closed, if F V (t) = ∅, i.e. if the term contains no variable. The set of ground terms is written T Σ .

Definition 1.16. The set of free variables of a first-order logic formula φ, written F V (φ) is defined with the following rules:

• The free variables of an atom are the variables appearing in it

-F V (⊥) = F V (⊤) = ∅ -F V (p(t 1 , ..., t n ) = n i=1 F V (t i )
, where all t i are terms and p is a predicate symbol

• The free variables are propagated by the logical connectives

-F V (φ ψ) = F V (φ) ∪ F V (ψ)
, where ∈ {∧, ∨, ⇒, ⇔} -F V (¬φ) = F V (φ)

• The free variables are canceled by the quantifications

-F V (∀x, φ = F V (φ)\{x} -F V (∃x, φ = F V (φ)\{x}
In other words, the free variables of a formula φ are the variables that do not appear directly under (in a syntactic sense) a quantification.

Definition 1.17. The bound variables of a L-formula φ, written BV (φ), are the variables which appear directly under a quantification. They are computed using the following rules:

• There are no bound variables in an atomic formula, as there is no quantification either -BV (⊥) = BV (⊤) = BV (p(t 1 , ..., t n ) = ∅

• Just like free variables, the bound ones are propagated by the logical connectives -BV (φ ψ) = BV (φ) ∪ BV (ψ), where ∈ {∧, ∨, ⇒, ⇔}; -BV (¬φ) = BV (φ)

• The bound variables are introduced by the quantifications:

-BV (∀x, φ = BV (φ) ∪ {x} -BV (∃x, φ = BV (φ) ∪ {x} Definition 1.18. The set of variables of a formula φ, written V AR(φ), is the set union of F V (φ) and BV (φ).

Definition 1.19. An L-formula is called ground if V AR(φ) = ∅, i.e. if it contains no variable.

Definition 1.20. An L-formula is called closed, or a sentence if F V (φ) = ∅, i.e. if any variable appearing in the formula is bound (to a quantification). Intuitively, it means that there is no loose variable, i.e. a variable whose instantiation is not dictated by a quantifier, so that the meaning of the formula does not depend on the meaning of unbounded variables.

In practice, terms and formulae will be interpreted by assining a value to each such variable.

The set of L-sentences is written SEN L .

Now that we can reason about the different types of variables in a formula, we can move on to the actual substitutions and their application.

Definition 1.21. A (partial) substitution σ is a mapping from the set of variables X to the terms T Σ (X ). A substitution is represented as a list of the individual variable / term mappings, i.e. [x 1 → t 1 , ..., x n → t n ].

Definition 1.22. Given a variable x and a substitution σ = [x 1 → t 1 , ..., x n → t n ], the instantiation of x with σ, written σ(x), or σx, is defined as

σ(x) = t i if x = x i for some i ∈ [1, n] x otherwise
In other words, if x appears in σ, then the associated term is returned. Otherwise, the result is the variable itself.

Definition 1.23. The domain, or support, of a substitution σ = [x 1 → t 1 , ..., x n → t n ] is the set {x ∈ X | σ(x) = x}, i.e. the set of variables that appear (and are associated to a different term) in σ.

Remark 1.24. A substitution σ can also be extended to operate on terms, in which case the constants are left untouched and the complex terms are treated inductively. Such a σ : T Σ (X ) → T Σ (X ) can then be defined as

     σ(c) = c with c ∈ C σ(v) = σ(v)
with v ∈ X σ(f (t 1 , ..., t n )) = f (σ(t 1 ), ..., σ(t n )) with f ∈ F and t i ∈ T Σ (X ) for any i ∈ [1, n] We can now lift the notion of instantiation to first-order logic formulae.

Definition 1.25. The instantiation of a first-order logic formula using a substitution σ is defined inductively as

                     σ(⊥) = ⊥ σ(⊤) = ⊤ σ(t) = σ(t)
where t ∈ T Σ (X ) σ(φ 1 φ 2 ) = σ(φ 1 ) σ(φ 2 )

where ∈ {∧, ∨, ⇒, ⇔} σ(¬φ) = ¬σ(φ) σ( x, φ) = x, σ\[x → σ(x)](φ) where ∈ {∀, ∃}

In other words, the substitutions work inductively on the formulae. Moreover, a quantification over a variable x removes any mapping of x that may have been present in the used substitution.

Finally, we introduce a notion of order over substitions.

Definition 1.26. Let us consider a signature without function symbols of non-zero arity, i.e. ∀f ∈ F, ar(f ) > 0. In that setting, the terms are restricted to variables and constants. We can now define a partial order on substitutions as:

σ 1 σ 2 ≡ ∀x, σ 1 (x) ∈ C ⇒ σ 1 (x) = σ 2 (x)
In other words, σ 2 is compatible with, and more precise than σ 1 .

Semantics

As previosuly stated, a first-order language L is built over a signature Σ = {F, P, ar}, where F is a set of function symbols used to build terms, P is a set of predicate symbols used to build atoms, and ar is an arity function for both kinds of symbols. We first assume such a signature.

We need to specify the elements we are talking and reasoning about. This set is called the domain of discourse, or universe, and written U M . Once we have such a universe, we can interpret in it the (syntactical) symbols of Σ using a Σstructure.

→ {⊤, ⊥}

A Σ-structure is sometimes called Σ-interpretation or Σ-algebra.

In Chapter 2, the semantics of Datalog will be defined using a specific type of universe and Σ-structures, called Herbrand, where the syntactic part of the language is directly used for its interpretation.

Definition 1.28. Given a signature Σ, the Herbrand Universe U H is the set of ground terms of the language, i.e. T Σ .

Definition 1.29. A Herbrand Σ-structure H = (U H , I H ) is a Σ-structure based on a Herbrand universe.

A Σ-structure M is used to evaluate the veracity of a first-order logic formula, i.e. assign a boolean value. To do so, we need to be able to assign values to variables.

Definition 1.30. A valuation ι over M is a partial function X → U M .

Remark 1.31. In Section 1.1.2, we defined the similar notion of substitution. However, unlike valuations, substitutions work on a strictly syntactic level, and are total functionsthey associate a value to every variable.

Definition 1.32. The extension of a valuation ι, written ι[x → u] where x is a variable and u an element of the universe, works like ι with a special case when applied to x. More formally, given a variable y,

ι[x → u]y = u if x = y ι(y) otherwise
The interpretation now works homophorphically (or recursively) to assign an element of the universe to each term, and then a binary valuation to formulae.

Definition 1.33. The interpretation of L-terms in M under a valuation ι : X → U M is defined as a mapping • I,ι : T Σ (X ) → U M :

• x I,ι = ι(x) • f (t 1 , ..., t n ) I,ι = I(f )( t 1 I,ι , ..., t n I,ι )
Definition 1.34. The evaluation of L-formulae in M under a valuation ι : X → U M is defined as the mapping • I,ι : SEN L → {0, 1}, using the following rules.

• ⊥ and ⊤ correspond to 0 and 1, respectively:

-⊥ I,ι = 0 -⊤ I,ι = 1
• The evaluation of an atom is done homophorphically, using ι:

-p(t 1 , ..., t n ) I,ι = 0 if I(p)( t 1 I,ι , ..., t n I,ι ) = ⊥ 1 if I(p)( t 1 I,ι , ..., t n I,ι ) = ⊤
• The conjunction ∧ (resp. the disjunction ∨) returns 1 iff both (resp. at least one of the) sub-formulaes are (is) equal to 1:

-φ 1 ∧ φ 2 I,ι = min( φ 1 I,ι , φ 2 I,ι ) -φ 1 ∨ φ 2 I,ι = max( φ 1 I,ι , φ 2 I,ι )
• The implication ⇒ returns 1 iff the left sub-formula is evaluated to 02 or the right one is evaluted to 1:

-φ 1 ⇒ φ 2 I,ι = max(1 -φ 1 I,ι , φ 2 I,ι )
• The equivalence symbol ⇔ checks that the two sub-formulae behave similarly:

-

φ 1 ⇔ φ 2 I,ι = 1 -| φ 1 I,ι -φ 2 I,ι |
• The negation simply switches an evaluation from 0 to 1, and the other way around:

-¬φ I,ι = 1 -φ I,ι
• The universal quantification ∀ (resp. existential quantification ∃) returns 1 iff the subformula is evaluated to 1 for any (resp. at least one) extension of the valuation wrt the quantified variable:

-∀x, φ I,ι = min

u∈U M φ I,ι[x →u] -∃x, φ I,ι = max u∈U M φ I,ι[x →u]
One does not need to be writing his or her doctoral thesis to know that logic, broadly speaking, revolves around the notion of truth. The truthness of a formula, called validity, is formally defined using the following notions.

Definition 1.35. A formula φ ∈ F Σ (X ) is satisfiable if and only if there exists a Σ-structure M = (U M , I) and a valuation ι : X → U M such that φ I,ι = 1, which is written

M, ι |= I φ. Definition 1.36. A formula φ ∈ F Σ (X ) is valid in M iff φ I,ι = 1, for all valuations ι : X → U M . This is denoted as M |= I φ, and M is called a model of φ. Definition 1.37. A formula φ ∈ F Σ (X ) is valid in general iff M |= I φ for all Σ-structures M. This is written |= φ.
The notion of validity is also used to define that of logical consequence.

Definition 1.38. Let φ 1 and φ 2 be two formulae in F Σ (X ). φ 1 ∈ F Σ (X ) entails, or implies, iff, for all Σ-structures M = (U M , I),

M |= I φ 1 implies that M |= I φ 2
In that setting, φ 2 is said to be a semantic consequence, or logical implication, of φ 1 , which is written φ 1 |= φ 2 .

Definition 1.39. Formulae φ 1 and φ 2 , both in F Σ (X ), are equivalent to each other iff φ 1 |= φ 2 and φ 2 |= φ 1 . This is denoted as φ 1 ≡ φ 2 .

Example 1.40. The following, well-known equivalences can be verified using the formulae of Definition 1.34.

• φ ∧ ⊤ ≡ φ; φ ∧ ⊥ ≡ ⊥; φ ∨ ⊤ ≡ ⊤; φ ∨ ⊥ ≡ φ • φ 1 ⇒ φ 2 ≡ ¬φ 1 ∨ φ 2 • φ 1 ⇔ φ 2 ≡ (φ 1 ⇒ φ 2 ) ∧ (φ 2 ⇒ φ 1 ) • ¬(φ 1 ∧ φ 2 ) ≡ ¬φ 1 ∨ ¬φ 2 • ¬(φ 1 ∨ φ 2 ) ≡ ¬φ 1 ∧ ¬φ 2
-These last two rules are known as the De Morgan's laws

• φ 1 ∨ (φ 2 ∧ φ 3 ) ≡ (φ 1 ∨ φ 2 ) ∧ (φ 1 ∨ φ 3 ) • φ 1 ∧ (φ 2 ∨ φ 3 ) ≡ (φ 1 ∧ φ 2 ) ∨ (φ 1 ∧ φ 3 )
-These last two rules are the distributivity of ∨ over ∧, and ∧ over ∨, respectefully

• ¬¬φ ≡ φ • ¬(∀x, φ) ≡ ∃x, ¬φ • ¬(∃x, φ) ≡ ∀x, ¬φ
Given a signature and an interpretative structure, the set of sentences that are satisfied by that framework is called a theory.

Definition 1.41. The first-order theory of a Σ-structure M = (U M , I) is defined as

T h(M) = {φ ∈ F Σ (X ) | F V (φ) = ∅ and M |= I φ}
Notation 1.42. In the rest of this document, M |= I φ will simply be written M |= φ.

Many first-order theories are studied and used, such as Peano arithmetic, Presburger arithmic, equality and so on. Datalog, which will be presented in Section 2, is also formalized as a first-order theory.

Normal forms

Like many other mathematical structures (e.g. matrices), first-order logic formulae are most efficiently used when in some so-called normal forms. One of them, the Horn clauses, are a key concept of Datalog. The following definitions build up to them, starting with a normal form that gathers all the quantifications at the beginning of a new, quantifier-free formula.

Definition 1.43. Any formula φ can be converted into a (semantically) equivalent formula in Prenex Normal Form. The prenex formula is of the form 1 x 1 ... n x n ψ, with i ∈ {∀, ∃} for i, and ψ is quantifier-free. In that setting, 1 x 1 ... n x n is called the quantifier prefix, and ψ the matrix. This transformation is denoted as ⇒ * P .

The next transformation, called skolemization, erases the existential quantifications in the quantifier prefix.

Definition 1.44. Skolemization converts any prenex formula φ into an equally satisfiable skolem formula ∀x 1 ...∀x n ψ. The transformation embeds every (previously) existentially quantified variable into an explicit choice function that depends on every previously quantified variable. More formally, it applies the following transformation until all existential quantifications have been eliminated:

∀x 1 ...∀x n ∃yφ ⇒ Sk ∀x 1 ...∀x n [y → f (x 1 , ..., x n )]ψ
The skolem formulae can then be transformed into conjunctive normal form. where all L ij are atomic literals. Any skolem formula can be transformed into a CNF via a procedure found in [START_REF] Russell | Artificial Intelligence: a modern approach[END_REF]. Basically, it gets rid of any unnecessary negation using some of the equivalences shown in Example 1.40, puts the conjunctions above the disjunctions using the equivalence A ∨ (B ∧ C) ≡ (A ∨ B) ∧ (A ∨ C), and then simply erases the quantifications.

We finally introduce a subclass of CNF, which is used extensively in Datalog, and thus in Section 2.

∨ ... ∨ ¬L n ∨ L p is equivalent to (L 1 ∧ ... ∧ L n ) ⇒ L p .
A Horn clause can then be seen as a list of preconditions leading to (at most) one result. Lifting this to full formulae, a Horn formula is then akin to a list (conjunction) of rules which can be interpretated as "if L 1 and ... and L n are true, then so is L p ".

Inference

We have seen first-order logic as a way to formalize and manipulate statements, but we also need to be able to relate them.

Definition 1.49. An inference system is a set of judgements, or rules describing how to deduce new facts from a previously established set of formulae. They are presented as

J 1 . . . J n J n+1 R
where J n+1 is the (syntactical) consequence of the hypotheses J 1 to J n , and R is the label, or name, of the rule. This can also be written J 1 , ..., J n ⊢ R J n+1 . If n = 0, i.e. there is no required hypothesis, the judgement is called an axiom.

Definition 1.50. The hypotheses in an inference can themselves be the result of another inference, and so on. In that sense, the inference rules can be applied iteratively, which is called a derivation. When F can be deduced from the set of base hypotheses ∆ using such a derivation in the inference system I, it is denoted as ∆ ⊢ I F .

These inference rules are purely syntactical, in the sense that they need to be related to the semantics of the used language. We now introduce the most important concepts.

Definition 1.51. Let I be an inference system for a language L. If, for any set of closed formulae ∆ ⊆ SEN L and F ∈ SEN L :

• ∆ ⊢ I F implies ∆ |= F , then I is sound; • ∆ |= F implies ∆ ⊢ I F , then I is complete; • ∆ |= F implies ∆ ∪ {F } ⊢ I ⊥, then I is refutationally complete.
In other words, an inference system is complete if there exists a syntactical deduction for every semantic implication (the syntax covers the semantics), sound if a syntactical deduction corresponds to a semantic implication (the syntax does not step out of the semantics), and refutationally complete if adding as an axiom a statement that is not a semantic implication can lead to the deduction of ⊥, i.e. the false statement.

Remark 1.52. In practice, refutational completeness is used in deduction systems by first adding the negation of the statement one wants to prove to the set of hypotheses, and then showing that the false statement can be derived.

We skim over the historic and fundamental examples of inference systems, e.g. natural deduction, Hilbert or sequents, to focus on a specific, more computation-oriented family of techniques. The resolution based inference techniques work with clausal formulae, as illustrated in the following example.

Example 1.53. The Binary Resolution Inference system consists of two rules:

A ∨ C B ∨ ¬C ′ σ(A ∨ B) Binary resolution
where σ is the most general unifier (mgu) of C and C ′ , i.e. the smallest substitution such that σ(C) = σ(C ′ ). Intuitively, this rule looks for a substitution (which can be seen as constraints) such that C and C ′ match, meaning that C and ¬C ′ become incompatible. Given the hypotheses, at least one of the left compotents, also subjected to the substitution, must be true.

The second rule is

A ∨ B ∨ C σ(A ∨ B) Factoring
where σ is the mgu of B and C. In other words, this rule tries to collapse two atoms in a disjunction.

As previously stated, resolution is not an inference system in itself, but rather a family of similar techniques [START_REF] Bachmair | Resolution theorem proving. In Handbook of Automated Reasoning[END_REF]. In Section 2.2, we will revisit hyperresolution, introduced in [Robinson, 1974].

Definition 1.54. The hyperresolution rule is formulated as

A 1 ∨ C 1 . . . A n ∨ C n B ∨ ¬C ′ 1 ∨ • • • ∨ ¬C ′ n σ(A 1 ∨ ... ∨ A n ∨ B)
where σ is the mgu of C i and

C ′ i , for every i ∈ [1, n].
Remark 1.55. Hyperresolution is a generalization, or rather iteration, of the binary resolution rule introduced in Example 1.53. Its implementation can be augmented with multiple heuristics, regarding ordering and selection.

Theorem 1.56. Hyperresolution is sound and refutationally complete.

Proof. See [START_REF] Bachmair | Resolution theorem proving. In Handbook of Automated Reasoning[END_REF].

Chapter 2

Datalog

Rien ne dépasse la beauté simple et froide de la logique. Si Galilée pouvait prétendre que la Nature est un livre écrit en langage mathématique, c'est qu'il n'existe rien de plus élégant qu'un système parfaitement ordonné, où chaque conséquence est le fruit d'une clause, où chaque élément est imbriqué dans un tout plus grand que la somme de ses parties.

Emmanuel Denise, Canard PC 396

Datalog is a simple and declarative language, based on first-order logic and tuned to datacentric applications, usually described as "Prolog without function symbols" [Liu, 1999]. The author of [Greenman, 2017] tracks its origins in the sixties and seventies, when mathematical logic was first considered as a lens through which databases could be seen.

More concretely, relational algebra is proposed as the fundational bases for the relational model of databases in 1970 [Codd, 1970], the first Prolog interpreter is developed in 1973 by Colmenaur and his students, and a programming language semantics of predicate logic, i.e. of Datalog, is introduced in 1976 [Van Emden andKowalski, 1976].

Originally designed as a powerful query language on databases, Datalog has since then gained interest thanks to domain-specific extensions [Abiteboul et al., 1995, Ramakrishnan and[START_REF] Ramakrishnan | [END_REF].

The introduction of [Benzaken et al., 2017a] gives a comprehensive list of languages built upon Datalog [START_REF] Lu | An operational semantics of starlog[END_REF], Loo et al., 2005, Grumbach and Wang, 2010, Calì et al., 2009, Seo et al., 2013, Aref et al., 2015] and applications, in both academic [DeTreville, 2002, Whaley et al., 2005, Hellerstein, 2010, Huang et al., 2011] and industrial [START_REF] Chin | Yedalog: Exploring knowledge at scale[END_REF], Gottlob et al., 2004, log, 2020, dat, 2020, sem, 2020] settings.

As a first approximation, Datalog is a fragment of Prolog without function symbols. A program is then a set of Horn clauses. Some of these clauses have no tail, or precondition, and constitute a first set of facts. The semantics of a program is this set of initial facts as well as those that can be deduced in any number of steps using the other clauses, called rules. A key feature of Datalog is recursivity, which makes it possible to compute transitive closures, e.g. accessibility in graphs, in a simpler and more complete way than other query languages, such as SQL, XPath and SPARQL.

In contrast to Prolog, the evaluation mechanism of Datalog follows a bottom-up strategy which guarantees termination even in the presence of recursive rules [START_REF] Abiteboul | [END_REF].

The key idea is that, without function symbols, the set of derivable facts is always finite. Section 2.1 and 2.2 formalize the syntax and semantics of Datalog, then Sections 2.3 and 2.4 discuss how Datalog can be augmented with negation and runtime computations.

Syntax

We first present the rules and constraints upon which Datalog programs are built.

Building blocks

As previously stated, a Datalog program is a set of Horn clauses (Definition 1.46), split into facts and rules. We build up the syntax, starting with the different kinds of symbols.

Definition 2.1. Datalog symbols are either arity bound predicates, constants or variables. We fix P as the set of predicates together with an arity function ar : P → N, C as the set of constants and V as the set of variables.

These symbols are used to build the expressions, i.e. terms, atoms, clauses and programs.

Definition 2.2. A term t is either a constant or a variable.

A ::= p( #» t )
Notation 2.4. The terms t 1 , t 2 , ..., t n are the arguments of the atom. The predicate of an atom is accessed with function sym.

Definition 2.5. A clause C is defined as

C ::= A 0 ← A 1 , ..., A m .
Notation 2.6. The atom A 0 is called the head of C, whereas the atom list A 1 , ..., A m is its body. In the rest of this document, the writing convention will be to use H for the head of a clause, and B 1 , ..., B m for the atoms of the body.

Definition 2.7. As stated in Remark 1.48, a Horn clause can be understood as "if B 1 and B 2 ... and B m , then H". In that spirit, when m = 0, i.e. C ≡ H ← (or simply H), the clause represents a fact. Otherwise, when m ≥ 1, a clause is called a rule.

Definition 2.8. A program P is a finite set of clauses.

P ::= C 0 , ..., C k , where the commas denote conjunction.

path(X, Y ) ← edge(X, Y ). path(X, Y ) ← path(X, Z), edge(Z, Y ).

edge(1, 3). edge(2, 1). edge(4, 2). edge(2, 4). The Datalog programs are however not built entirely à la carte, as they must enforce two contraints.

Extensional vs. intensional predicates

Definition 2.10. In any Datalog program, the involved predicates must be split into two classes: extensional (or base) predicates, that are only defined using facts, and intensional (or derived) predicates, that are defined only via rules.

Corollary 2.11. In any Datalog program, a predicate can not appear as the head of both a rule and a fact.

Example 2.12. In the program of Figure 2.1, the path predicate is intensional (defined by the two rules), whereas edge is extensional (defined by the four facts), and Corollary 2.11 is respected.

Remark 2.13. From a database standpoint, the extensional predicates correspond to the relations actually stored in the database, whereas the intensional predicates are virtual, computed relations, usually called views. In that setting, the facts of a program P form the extensional database, written edb(P ), the rules are the intensional database idb(P ), and the program schema is sch(P ) = edb(P ) ∪ idb(P ).

Although Datalog programs are formalized as a mix of rules and facts in this section, the (usually) huge preponderance of facts over rules makes it convenient and more efficient to view them as two separate components. As discussed in Chapter 3, this is actually done in the aforementioned Coq formalization of Datalog [Dumbrava, 2016].

Notation 2.14. In the rest of this document, the considered programs will be clearly identified and the set of base facts shall be denoted as simply EDB, rather than edb(P ).

Remark 2.15. Surprisingly, this constraint does not mitigate the expressivity of Datalog. Assume an invalid program, where a predicate p is used both in an intensional and extensional manner. We can introduce a purely extensional predicate p EDB , which contains the same facts as p, and a rule p(X) ← p EDB (x). Then, we can remove p from the EDB, making it a purely intensional predicate, while preserving the originally intended semantics.

Program safety

On the other hand, the second constraint does limit the expressivity of Datalog. To formalize it, we first need the following definition, which will also be used extensively when introducing the semantics of Datalog.

Definition 2.16. The variable-free expressions of Datalog are called ground expressions.

The sets of ground atoms, clauses and programs are written A, C and P , respectively.

Definition 2.17. A clause is safe if all the variables in its head also appear in its body. Remark 2.22. The safety conditions may seem arbitrary at first, but they bound the semantics of any given Datalog program and guarantee the termination of the bottom-up evaluation strategy. This is discussed more in-depth hereafter.

Example 2.23. Datalog can be used for many access control policies or rule-based processes. For example, [START_REF] Dougherty | Specifying and reasoning about dynamic access-control policies[END_REF] presents a Datalog formalization of the access to conference review scores. As an alternative real life situation, Although the semantics of Datalog has not been formally introduced at this point of the document, the sharp-eyed reader will probably infer the informal meaning of the rules. The first one states that a person P can go see a movie M on day D if

• M is shown on D

• seeing a movie on D will cost C to P

• a person F (which in practice can be P him/herself, or maybe a friend) pays C for P

The other rules encode a fragment of the pricing table 1 . Note that the program safety condition in enforced using the any predicate, which contains every day constant. This trick can be used because there is only a finite number of possible values, i.e. seven days.

Semantics

We introduce two semantics for Datalog: the Minimal Model Semantics, which roughly views Datalog as a first-order logic theory, and the Fixpoint Semantics, which is more akin to an execution engine. These two semantics stem from the key result of fixpoint theory, called the Knaster-Tarski theorem [Tarski, 1955].

Theorem 2.24. (Knaster-Tarski theorem) Let us assume a complete lattice L, ≤ and an operator f on L. If f is monotonic, then it has a least fixpoint, denoted as lf p(f ).

The original proof establishes that pre-fixpoint are closed by min, meaning that the least fixpoint is the inf over all pre-fixpoints. This is the theoretical background of the Minimal Model Semantics detailed in Section 2.2.1.

If the lattice is finite, this original proof provides an algorithm to compute the least fixpoint. However, enumerating every pre-fixpoints is obviously not always feasible in practice. Another proof, attributed to Kleene, provides a more efficient algorithm. This algorithm iterates the f operator, starting from the bottom of the lattice, until a fixpoint is reached. This fixpoint is shown to be the least fixpoint. This second proof is the theoretical background of the Fixpoint Semantics detailed in Section 2.2.2.

Remark 2.25. Calling only the second semantics the (and not "a") fixpoint semantics may be misleading, as both are about fixpoints.

As expected given the fact that these two semantics are inspired by two different proofs of the same theorem, they are equivalent, as shown in [Van Emden and Kowalski, 1976].

Minimal Model Semantics

As previously seen, a Datalog program P is a set of Horn clauses, meaning that it can easily be translated into an actual first-order logic formula P * using the following rules:

Atom An atom A can simply be represented as a first-order logic formula A *

Clause A clause C ≡ H ← B 1 , ..., B m can then be translated into

C * ≡ ∀X 1 , ..., ∀X q ((∃X q+1 ...∃X l , (B 1 ∧ ... ∧ B m )) ⇒ H) with {X 1 , ..., X q } = V AR(H) and {X q+1 , ..., X l } = k∈[1,m] V AR(B k )\V AR(H)
In other words, given an instantiation [X 1 → c 1 , ..., X m → c m ] of the variables in the clause's head, H(c 1 , ..., c m ) can be deduced if the rest of the clause's variables can be instantiated so that the resulting ground atoms are all true.

∧ (∀X, ∀Y, (edge(X, Y ) ⇒ path(X, Y ))) ∧ (∀X, ∀Y, (∃Z, (edge(X, Z) ∧ path(Z, Y ))) ⇒ path(X, Y ))
Now that Datalog programs can be translated to first-order logic, we can define their semantics in this framework. To do so, we provide the interpretation (cf. Definition 1.27) of the fragment of FOL corresponding to the translation. The codomain of interpretation I is the union of the non-empty universe U and the boolean set, the former being used for the interpretation of constants and the latter for atoms. More formally,

• for every c ∈ C, I(c) ∈ U

• for every p ∈ P, I(p) is a mapping U ar(p) → {⊤, ⊥}. In other words, the interpretation expects a number of arguments as specified by the arity function for the given predicate and returns a boolean evaluation.

Definition 2.28. We can now define the actual interpretation function. Assuming a signature Σ, a set of variables V and a valuation ι : V → U , the interpretation e I,ι of a Datalog expression e is defined by structural induction on e using the following rules. Definition 2.32. We express the logical consequences of a Datalog program P using the Herbrand Semantics of P , the building blocks of which are:

• x I,ι = ι(x) • c I,ι = I(c) • p(t 1 , ..., t n ) I,ι = I(p)( t 1 I,ι , ..., t n I,ι ) = ⊤ if I(p)( t 1 I,ι , ..., t n I,ι ) = ⊤ ⊥ otherwise • H ← B 1 , ..., B n I,ι = ⊤ if ∃i ∈ [1, n], B i I,ι = ⊥ ∨ H I,ι = ⊤ ⊥ otherwise • H ← B 1 , ..., B n I = ⊤ if ∀ι, H ← B 1 , .
• as Herbrand Universe, the set of all program constants, written adom(P ).

• as Herbrand Base, the set of all ground atoms that can be built from predicates (in P) and constants in adom(P ), written B P .

• as Herbrand Interpretation, I H (cf. Definition 1.29), i.e. the symbols are their own interpretation.

Remark 2.33. As seen in the definition of the Herbrand Universe, the only constants which need to be considered are those that actually appear in the studied program. This stems from the safety condition (see Section 2.1.3) and the absence of terms, meaning that Datalog programs only pass around values rather than introducing or computing new ones, as can be done in Prolog for example.

Definition 2.34. In that setting, the grounding of a program P is defined as a valuation (cf. Definition 1.30) ι : BV (P ) → adom(P ) as

with ι(P ) = C∈P ι(C) with ι(p 0 ( t 0 ) ← p 1 ( t 1 ), ..., p m ( t m )) = p 0 (ι( t 0 )) ← p 1 (ι( t 1 )), ..., p m (ι( t m ))
Definition 2.35. Applying this definition to the satisfaction of a clause wrt a Herbrand interpretation I H ,

I H |= ι(C) iff {p 1 (ι( t 1 )), ..., p m (ι( t m ))} ⊆ I H implies that p 0 (ι( t 0 )) ∈ I H
We can now define, as a special case of Definition 1.36, the notion of Herbrand model. Example 2.37. Let P be the program {p(a), q(a), r(X) ← p(X)}. The set of predicates P is {p, q, r}, and that of constants, adom(P ) = {a, b}. They can be used to build the set of ground atoms B P = {p(a), p(b), q(a), q(b), r(a), r(b)}.

Now consider the following Herbrand interpretations:

• I 1 = ∅

• I 2 = {p(a), q(a)}

• I 3 = {p(a), q(a), r(a)}

• I 4 = {p(a), q(a), r(a), p(b), r(b)}

• I 5 = {p(a), q(a), r(a), q(b)}

• I 6 = B P I 1 is not a Herbrand model of P as it does not even contain the facts of the program. I 2 does have them, but the r(X) ← p(X) rule is not enforced, as it contains p(a) and not r(a). On the other hand, I 3 , I 4 , I 5 and I 6 are Herbrand models of P .

Theorem 2.38. If M 1 and M 2 are Herbrand models of a definite program P , then M 1 ∩ M 2 is also a model of P . This theorem is called the Model intersection property.

Proof. Let C be a Horn clause in P and ι a valuation. C is either a fact or a rule. In the first case, M 1 and M 2 must both contain ι(C) to be models of P , which means that M 1 ∩ M 2 does too.

If C ≡ p 0 ( t 0 ) ← p 1 ( t
I 3 = I 4 ∩ I 5 ∩ I 6 .
The unique minimal model is the intended semantics of Datalog. In other words, given a Datalog program P , cons(P ) = M (P ).

Remark 2.42. This choice, as explained in Chapter 12.2 of [START_REF] Abiteboul | [END_REF], is a consequence of a philosophical hypothesis fueling database theory, called the closed world assumption. It basically states that dabases should be considered to be complete, although they obviously do not contain every fact about the world in practice. In that setting, any fact which can not be derived (or proved) from a program should be considered false. The minimal model semantics enforces this philosophy.

Fixpoint Semantics

We now give an alternative, more computation-oriented but equivalent semantics of Datalog. This semantics being based upon fixpoint theory, we first recall its basics.

Definition 2.43. A complete lattice is a pair L, ≤ , where L is an ordered set wrt to the partial order ≤ and any set A ⊆ L has a greatest lower bound A, and a lowest upper bound A.

Definition 2.44. Given a set L equipped with a partial order ≤, an operator f :

L → L • is monotonic, if x 1 ≤ x 2 implies f (x 1 ) ≤ f (x 2 ) for all x 1 , x 2 ∈ L, i.e. f preserves the partial order ≤ • has a pre-fixed point p if f (p) ≤ p • has a fixpoint x if f (x) = p
We can now apply the Knaster-Tarski theorem (see Theorem 2.24) to the interpretation of Datalog. The partially ordered set here is the set of Herbrand interpretations P(B P ), i.e. the different combinations of ground atoms. The operator is introduced in the following definition.

Definition 2.45. Given a program P , the immediate consequence operator T P works on interpretations, i.e. T P : P(B P ) → P(B P ), and is defined as

T P (I) = I ∪ {head(ι(C)) | C ∈ P ∧ ι a grounding of P ∧ body(ι(C)) ⊆ I}
In other words, T P preserves the ground atoms in its argument (F ∈ I), and adds the heads of clauses in the program instantiated with valuations such that the ground atoms in the tail are all in the argument (right side of the ∨). Any ground atom F produced by the operator is called an immediate consequence of the program.

Lemma 2.46. T P is monotonic.

Proof. Let us assume I 1 and I 2 ⊆ B P such that I 1 ⊆ I 2 , and F ∈ T P (I 1 ). Either F ∈ I 1 , or there exists a C ∈ P and a valuation ι such that body(ι(C)) ⊆ I 1 . In both cases, since I 1 is a subset of I 2 , the former can be replaced by the latter in the assertion, showing that F is also in T P (I 2 ).

Theorem 2.47. T P has a least fixpoint, lf p(T P ). In practice, it is computed by iterating T P , starting with the minimal, i.e. empty interpretation.

Proof. Combination of Theorem 2.24 and Lemma 2.46.

Definition 2.48. The powers of the immediate consequence operator are defined as

T P ↑ 0 = ∅ T P ↑ (n + 1) = T P (T P ↑ n)
As previously stated, there exists some ω ∈ N such that T P ↑ ω = n≥0 T P ↑ n = lf p(T P ) Example 2.49. Let P be the graph connectivity program from Example 2.9, then

• T P ↑ 0 = ∅ • T P ↑ 1 = T P (∅) = ∅ ∪ {edge(1, 3), edge(2, 1), edge(4, 2), edge(2, 4)} • T P ↑ 2 = T P (∅) = T P ↑ 1 ∪ {path(1, 3), path(2, 1), path(4, 2), path(2, 4)} • T P ↑ 3 = T P (T P ↑ 2) = T P ↑ 2 ∪ {path(2, 3), path(4, 1), path(4, 4), path(2, 2)} • T P ↑ 4 = T P ↑ 3 ∪ {path(4, 3)} • T P ↑ 5 = T P (T P ↑ 4) = T P ↑ 4.
It then appears that T P ↑ ω = T P ↑ 4 = lf p(T P ). We can now relate the fixpoint and minimal model semantics.

Theorem 2.52. The unique minimal Herbrand Model M (P ) of a Datalog program P is lf p(P ) = T P ↑ ω.

Proof. See [Van Emden and Kowalski, 1976].

We end this section with a more operational view on the fixpoint semantics. Indeed, concretely, the consequence operator is equivalent to the following inference rule, called the Elementary Production Principle [START_REF] Ceri | What you always wanted to know about Datalog (and never dared to ask)[END_REF].

H ← B 1 , ..., B n {F 1 , ..., F n } ⊆ I ∃σ, σ(B 1 ) = F 1 ∧ ... ∧ σ(B n ) = F n σ(H) EP P
Remark 2.53. The EP P rule is an implementation of hyperresolution, (see Section 1.54).

Theorem 2.54. Given a Datalog program P , the semantics of the program cons(P ) can be obtained by iterating the EP P rule until a fixpoint is reached. This algorithm is indeed both sound and complete.

Proof. Corollary of the completeness and soundness properties of hyperresolution established in [START_REF] Bachmair | Resolution theorem proving. In Handbook of Automated Reasoning[END_REF].

Adding and handling negation

So far, we have introduced the so-called standard Datalog. It can however be augmented with various features, most notably negation. As explained in Chapter 6, this thesis, and the Datalog optimizations it introduces, were inspired by a network verification tool, called Octant, which did not initially scale. These performance issues were brought by the use of negation, meaning that we need to get an idea of how it is added and handled.

As always, we first discuss the syntactic side, before moving to the so-called stratified semantics. This semantics of Datalog with negation serves as the theoretical foundation for the implementation of negative Datalog in [Dumbrava, 2016] and will provide a good intuition of what happens during the execution of Octant. For the sake of being exhaustive, we will finally overview some alternative semantics of this extension.

Syntax

The first step is, unsurprisingly, to add a negation unary operator to Datalog's vocabulary. We shall write it ¬, as illustrated by the following example.

Example 2.55. The program of Figure 2.4 is similar to the one shown in Figure 2.1, which defined graph connectivity, with the addition of a rule. This (third) rule defines disjoint, the predicate, or set, of pairs of edges which are not connected.

path(X, Y ) ← edge(X, Y ). path(X, Y )

← path(X, Z), edge(Z, Y ). disjoint(X, Y ) ← ¬path(X, Y ) edge(1, 2). edge(2, 1). edge(2, 3). Remark 2.59. Section 6.1 of [Dumbrava, 2016] discusses the adaptation of the safety condition (see our own Section 2.1.3) to this new setting, as the use of negation opens the door to considering and computing an infinity of facts. However, it is stated that the previous notion is sufficient when the domain of values is restricted to the constants appearing in the program's database. Since this is the approach taken by the associated development [START_REF] Benzaken | Data-logCert[END_REF], and then by us as well, we do not dwelve into these considerations in this document.

While the changes to the syntax are minor, defining a semantics for Datalog augmented with negation is more involved. We first focus on a semantics called stratified semantics, and then overview some alternative approaches.

Stratified Semantics

The presentation of the stratified semantics will itself be stratified, as we first present a semantics for Datalog programs which use negation in a very limited way. Then, we will see how the idea behind this semantics can be lifted to unrestriced programs, by introducing a new logical consequence operator and an associated new iteration, as well as the notion of stratification of Datalog programs.

Semipositive Datalog

We start with a restricted use of negation in Datalog, which provides a simple intuition that will then serve as the bedrock of a generalization to more generic programs.

Definition 2.60. A semipositive Datalog program is a program where negation is only applied to atoms built with extensional predicates (see Section 2.1.2), i.e. predicates only defined by the EDB.

Example 2.61. The program shown in Figure 2.4 is not semipositive, since a negation is applied to path, which is an intensional predicate. This restriced setting is simply dealt with, as such negated atoms can be replaced by their complement w.r.t. the program's Herbrand base, i.e. the set of relevant facts that can be built using constant appearing in the EDB. Then, the semantics of standard Datalog can be reused. The following example illustrates this mechanism. Example 2.62. Figure 2.5 shows a semipositive Datalog program P . Considering the set of constants that appear in the EDB, the only ground atoms about R in B P , the program's Herbrand base, are R(1, 1), R(1, 2), R(2, 1) and R(2, 2). This program is then equivalent to Figure 2.5b, where a new predicate nR, is introduced to replace the negation of R. It is also an extensional predicate, whose definition in the EDB is the complement of R w.r.t. B P .

P (X, Y ) ← R(X, Y ), ¬R(Y, X). R(1, 2). R(2, 2). (a) A semipositive program... P (X, Y ) ← R(X, Y ), nR(Y, X). nR(1, 1). nR(2, 1). R(1, 2). R(2, 2).

Logical consequence

We now move up to the general setting, where negation may be applied to any predicate, may it be extensional or intensional. The heart of the operational semantics for standard Datalog previously defined was the T P operator (see Definition 2.45), which of course needs to account for the negated atoms.

Definition 2.63. Given a program P , the extended immediate consequence operator TP works on interpretations, i.e. TP : P(B P ) → P(B P ). The definition is similar to the previous one, but now only captures extensional predicates in the given (or previous) interpretation (I| EDB ) and checks that the instances of negated atoms have not been deduced:

TP (I) = {F ∈ P(B P ) | F ∈ I| EDB ∨ F = head(ι(C)), C ∈ P ∧ body + (ι(C)) ⊆ I ∧ body -(ι(C)) ∩ I = ∅}
Remark 2.64. Unlike the standard T P , this new TP operator is not inflationary, i.e. it is not the case that I ⊆ TP (I) for every I ⊆ B P (note that I is not restricted to facts about extensional predicates). Still unlike T P (see Lemma 2.46), it is not monotonic either. This remark on the monotonicity of TP is at the heart of the intricacies brought by the negation, as illustrated by the following example.

Example 2.65. We use a program P which only contains a single rule p ← ¬q and no variable. Several properties of the minimal model semantics (see Section 3.3.1) are violated when trying to apply it to P :

Uniqueness I 1 = {p} and I 2 = {q} are both minimal Herbrand models, i.e. they are both compatible with the only rule of P , whereas ∅ is not (the absence of q would imply the presence of p).

Models closed by intersection

As explained just above, I 1 ∩ I 2 = ∅, unlike I 1 and I 2 , is not a model of P .

Monotonicity TP (∅) = {p} (not decreasing) and TP ({q}) = ∅ (not increasing)

Beyond these fundamental infringements of the model-theoretic approach we previously relied on, a more immediate and practical issue arises with the use of negation, as shown by the following example.

Example 2.66. Consider the two-rule program P = {p ← ¬q, q ← ¬p}. Using the extended consequence operator of Definition 2.63 and iterating it using Definition 2.48, the results alternate between ∅ and {p, q}, indefinitely.

The immediate consequence operator has already been altered to account for the use of negation, but the way it is iterated has not. This is the heart of the solution developed by [START_REF] Apt | Towards a Theory of Declarative Knowledge[END_REF], which introduces the notion of stratified semantics.

The idea is that the programs are split into strata, defined by the use of negation (the construction of these strata is addressed in Section 2.3.2.3), and that the results of the computation of a stratum are preserved when moving on to the next. This way, the iterations are performed on a stable fact base and monotonicty is ensured, which clearly appears in the following definition.

Definition 2.67. Given a complete lattice L, ⊆ and a Datalog program P , the powers of the extended immediate consequence operator are defined as

                 TP ↑ 0 = ∅ TP ↑ (n + 1) = TP ( TP ↑ n) ∪ TP ↑ n TP ↑ ω = n≥0 TP ↑ n
Example 2.68. Going back to Example 2.66, but using the new iteration of TP , we obtain TP ↑ 0 = ∅ and TP ↑ 1 = TP ↑ ω = {p, q}. In particular, a fixpoint is now reached.

As stated above, for this method to work, the programs need to be stratified.

Stratifying a Datalog program with negation

To formally define the notion of stratified program, we need the notion of predicate definition, i.e. the set of clauses of a program that define a given (extensional or intensional) predicate. in P , we have

• σ(B i ) ≤ σ(H), for every i ∈ [1, n] • σ(C i ) < σ(H), for every i ∈ [1, m]
In other words, a stratification indexes the positive and negative atoms of any clause such that they are bounded and strictly bounded, respectively, by the index of the predicate symbol at the clause's head. This way, we obtain an order in which the different layers, or strata, of the program can be computed.

Definition 2.71. Given a program P and a stratification σ, we call P i a stratum, where We do not go into details in this document, as the technicities of the actual stratification of Datalog programs does not come into play in our work, its justification or explanation. The curious reader will find a synthesis and illustration in Section 6.2.3 of [Dumbrava, 2016], and a more complete version in [Ullman, 1990].

P i = {p ∈ P | σ(p) = i} and P i = ∅ A program P is then partitioned into P 1 ⊔ • • • ⊔ P n ,
We now have all the tools and notions required to formally define a first semantics of Datalog programs using negation.

Iterated Fixpoint Models

As previously intuited (see Remark 2.74), the stratification of a program splits it into a series of components, which will be executed in a sequential manner and on top of one another, i.e. using the semantics of the previous slice(s) as the initial interpretaion.

Definition 2.76. Let P be a Datalog program with negation, stratified as P 1 ⊔ • • • ⊔ P n . The model of P is defined iteratively, using the following relation:

M 1 = TP 1 ↑ ω (∅) M i = TP i ↑ ω (M i-1 )
Is that setting, the intended semantics of P is M n .

Example 2.77. We reuse the program of Example 2.55, which defined graph disconnectedness. In that context, the edge and path predicate symbols can be the first stratum, whereas disjoint should go to the second one. The computation of the first stratum returns the set M 1 = {edge(1, 2), edge(2, 1), edge(2, 3), path(1, 2), path(2, 1), path(1, 1), path(2, 3), path(1, 3)} Using this set as the EDB for the second stratum, the semipositive method returns the set

M 1 ∪ {disjoint(2, 2), disjoint(3, 1), disjoint(3, 2), disjoint(3, 3)} = M 2 .
It might be surprising to compute disjoint(2, 2) and disjoint(3, 3). This is because path(X, Y ) is defined as the existence of a non-empty path between vertices X and Y . One might be tempted to add a reflexivity using a trick similar to what is shown in Example 2.21.

Remark 2.78. A program may admit more than one stratification (e.g. by using multiple strata for a set of compatible predicates). It is however shown in [START_REF] Apt | Towards a Theory of Declarative Knowledge[END_REF] that the semantics of a program is independent from its stratification.

As stated and proved in Chapter 15.2 of [START_REF] Abiteboul | [END_REF], the stratified semantics is adequate w.r.t. the previously introduced interpretation of Datalog:

Theorem 2.79. For each stratifiable Datalog program P and instance I over edb(P ):

• The stratified semantics of P w.r.t. I is a minimal model of P * , the first-order logic translation of P (see Section 2.2.1), and its restriction to edb(P ) equals I

• The stratified semantics of P w.r.t. I is a minimal fixpoint of T P , and its restriction to edb(P ) equals I Proof. See Section 9.6.1 of [Dumbrava, 2016], also implemented in Coq in [START_REF] Benzaken | Data-logCert[END_REF].

Alternative semantics

The stratified semantics is used as a reference point in [Dumbrava, 2016], but alternative semantics are introduced and quickly discussed.

Perfect model semantics The perfect model semantics [Przymusinski, 1988] generalizes the notion of stratification introduced above, as it is defined on the level of atoms rather than predicates. The computation of the semantics of a locally-stratified program is then similar to the process seen just above.

Stable model semantics The stable model semantics [START_REF] Gelfond | The stable model semantics for logic programming[END_REF], rather than dealing with the negation at the level of the iterated logical consequence operator, transforms the Datalog program. After it has been grounded, rules containing negated facts that appear in the original interpretation I are deleted, as their body can not be satisfied. On the other hand, negated facts which do not appear in I are also deleted, as they are considered satisfied.

This leads to a negation-free program, whose semantics can be computed using the standard tools. If this unique minimal model of the transformed program is the original interpretation I, then it is also the unique minimal model of the initial program, called a stable model. Given a stratifiable program, this model is the same as the iterated fixpoint model.

Well-founded model semantics The well-founded model semantics [Van Gelder et al., 1991] uses a logical setting that introduces a third, intermediate truth value [Przymusinski, 1990].

The interpretation is then split into two components, founded and unfounded facts, and the logical consequence operator combines the usual immediate consequence with the negation of elements in the greatest unfounded set. For locally stratified programs, the (iterated) well-founded and perfect model semantics are equal, whereas for Datalog programs with unstratified negation, the total well-founded model is the same at the unique stable one.

The takeaway of this section on Datalog extended with negation is that, in practice, reasoning about the negation of a predicate requires a saturation of its definition. More concretely, this means that the use of negation may introduce bottlenecks in the execution of Datalog programs, as we will see in Section 6.2.

Adding on-the-fly constraints

As defined above, Datalog passes around values rather than buidling (using complex terms) or introducing new ones, which ensures its finiteness. However, this setting is also a strong limitations for the implementation of many real-life scenarios and problems. Another practical extension of Datalog, which is lighter than negation but also comes into play in our work, is the use of non-strictly symbolic predicates.

Definition 2.80. A primitive predicate is a predicate which is not defined by rules or the EDB (meaning that it is neither intensional nor extensional), but by actual computations within the Datalog engine.

Example 2.81. Assume we have an EDB filled with people represented as facts of the form p(N, H), where N is the person's name (or id) and H his or her height, and we want to use Datalog to compute the set of heightest people in this EDB. This can be done within the strict frame of Datalog, for example with the rules of Figure 2.6, with a new extensional predicate greater. This approach then requires the addition to the EDB of every fact greater(x, y) where x is indeed greater than y and x and y appear as the second argument of a fact about p in the EDB. This method relies on the finite nature of Datalog programs, and in particular of the set of relevant constants. However, the domain of such a program is usually an almost infinite set, in the sense that they can not be naively enumerated in practice -let alone in a quadratic way, as in Figure 2.6. Purely symbolic Datalog, as introduced in this chapter, are then not fit to handle some use cases.

taller(N 1 , N 2 ) ← p(N 1 , H 1 ), p(N 2 , H 2 ), greater(H 1 , H 2 ). tallest(N 1 ) ← ¬taller(N 2 , N 1 ).
In contrast, individual Datalog engines may support primitive predicates, e.g. efficient implementations of = or ≥. Such an engine could run the program of Figure 2.7, which does not require the introduction of new predicates or facts. Remark 2.82. The addition of primitive predicates should not be at the cost of breaking the finite nature of the execution of Datalog programs. To avoid this caveat, the safety constraint defined in Section 2.1.3, does not take into account the occurrences of arguments in primitive predicates. This way, they can be seen as additional, very convenient constraints added at the level of the engine rather than the actual language.

taller(N 1 , N 2 ) ← p(N 1 , H 1 ), p(N 2 , H 2 ), H 1 ≥ H 2 . tallest(N 1 ) ← ¬taller(N 2 , N 1 ).
Chapter 3

Datalog in Coq

Et maintenant, je me demande : quand vient la nuit, est-ce que la Machine pleure, elle aussi ? Est-ce qu'elle hurle dans un oreiller, comme moi, depuis le fond de sa solitude ?

Emmanuel Denise, Canard PC 396

In her thesis [Dumbrava, 2016], Stefania Dumbrava developed a formalization of Datalog within the Coq proof assistant, called DatalogCert [START_REF] Benzaken | Data-logCert[END_REF]. It contains two engines, one for standard Datalog, implementing and certifying the T P operator as presented in Section 2.2.2, and one for Datalog extended with negation, using the semantics introduced in Section 2.3.2.

Remark 3.1. This chapter introduces the version of DatalogCert corresponding to the paper [Benzaken et al., 2017a] and found at [START_REF] Benzaken | Data-logCert[END_REF]. Some of its authors have since developed more complete or alternative versions (see for example [START_REF] Bonifati | Certified graph view maintenance with regular datalog[END_REF]). The work presented in this thesis should be adapted to such newer versions, which should serve as the basis of potential future works.

Our work is also formalized in Coq, using the development of the positive Datalog engine.

Although we formally defined the computation of Datalog programs using negation above, it was only to give an intuition of the way Datalog engines work in practice, and how it can raise performance issues, as explained in Section 6.2.

As these Datalog engines are heavily based on the Mathematical Component (MathComp) library, Section 3.1 first introduces the relevant basics with a simple, user-oriented overview.

We then present in Sections 3.2 and 3.3 the core syntactic and semantic components of the positive Datalog engine found in DatalogCert, to provide context and tools for our own work.

Finite types and notations in MathComp

MathComp introduces a type hierarchy for algebra, where refined structures inherit properties and structural functions of their ancestors [START_REF] Garillot | Packaging Mathematical Structures[END_REF]. Figure 1 of [Sakaguchi, 2020] shows the hierarchy of structures found in version 1.10.0 of MathComp. As an illustration, an algebra enthusiast may notice a ring type (ringType), which can be refined into the types of commutative rings (comRingType), or rings whose units have computable inverses (unitRingType). These two types can then be specialized into the type of commutative rings whose units have computable inverses, comUnitRingType.

However, both [START_REF] Benzaken | Data-logCert[END_REF] and the work we built upon it do not venture far into the algebraic types introduced in MathComp. They rather use -and, in our case, extendthe generic types which form the backbone of MathComp. We then first need to introduce these types and the possibilities they unlock.

eqType

The basis of this backbone is a type called eqType. It consists of another type packaged with a decidable equality, denoted as ==. More concretely, given an eqType and two elements of this type, their equality can be computed as a boolean, as shown in Figure 3.1

Variable A : eqType. Variable x y : A.

Check x == y.

x == y : bool Conversely, to build an eqType, the base method is to provide a boolean equality relation e, and show that it enforces the axiom of Figure 3.2, i.e. that its behavior follows the propositional equality.

Definition axiom T (e : rel T) := forall x y, reflect (x = y) (e x y). Remark 3.2. To the best of our understanding, the general methodology of MathComp is to fit structures into constrained types such as eqType, or the following subtypes, which unlocks many definitions and notations. In particular, DatalogCert relies heavily on this approach by leveraging the decidable and finite nature of Datalog.

choiceType and countType

The next two types are choiceType and countType. The first is the interface of types with a choice operator, i.e. a function that takes a predicate and a witness of its non-emptiness, and returns a standard element of the type satisfying the predicate.

The second type, countType, is an interface for countable types, i.e. types which are indexed. More concretely, a countType is packaged with an injective function that associates a nat index to any element of the type, as well as its partial (if the type contains finitely many elements) inverse.

Remark 3.3. All the types presented in this section form a strict hierarchy, in the sense that a countType is a choiceType, which in turn is an eqType. The most precise and interesting type, which inherits the properties of all the others, is finType.

finType

The final interface, finType, describes types with finitely many elements. This setting unlocks new possibilities, as having only a finite number of elements in a type allows for terminating iterations. We introduce some Examples of finTypes and associated notations which will be used in the rest of this document.

Example 3.4. Given an integer n, one can define the finType of nats strictly lower than n, called Ordinal n and written 'I_n. To build a 'I_n from an integer x, one needs a proof that x < n. Conversely, such a proof can be extracted from an ordinal.

Example 3.5. Functions with a finite domain, i.e. from a finType A to a (generic) type B form a type called finfun, and written {ffun A -> B}. If B is also a finType, then {ffun A -> B} is a finType itself.

Example 3.6. Given a type A and an integer n, n.-tuple A is the type of lists over A with exactly n elements. If A is a finType, so is n.-tuple A.

Example 3.7. Given a finite type A, MathComp provides the finType of sets over A, written {set A} (which, by transitivity, allows the definition of {set {set A}}, and so on).

Definition 3.8. Such types, along with related operations are axioms, are packed within dependent records usually called mixins (see Section 2.1 of [START_REF] Garillot | Packaging Mathematical Structures[END_REF]). Chapter 7 will show how we define and fit some types into such structures.

Using MathComp types

One of the benefits of MathComp is the introduction of some "paper-like" notations for lists and sets. These notations, which rely on straightforward definitions, allow for much more readable and higher-level definitions and proofs, and are used in Parts IV and V of this thesis.

Example 3.10. Figure 3.3 shows a notation on lists which mixes filter and map. All occurrences of y are filtered out, using the decidable (in)equality packed within the eqType (filter expects a bool predicate), and a function f is applied.

Definition 3.11. The seq module in MathComp contains many useful notations and functions. In particular, x \in s is the boolean membership of x in sequence s, and the all and has functions check that all or at least one element of a seq enforce(s) a given predicate.

The associated lemmas of these functions, allP and hasP, are used throughout the rest of this thesis.

Variable A : eqType. Check (@allP A P s). Check (@hasP A P s).

x \in s : bool all P s : bool has P s : bool (* forall x, x \in s -> P x *) allP : reflect {in s, forall x : A, P x} (all P s) hasP :

reflect (exists2 x : A, x \in s & P x) (has P s) Example 3.12. The set interface includes many definitions and notations, such as

• set union, intersection, difference and complement, respectively :|:, :&:, :\: and ~:.

• "big operators" notations, such as \bigcup_{x in X} f x (for

x ∈ X f (x))
• comprehension notations, such as

[set f x | x in X & p x]
• the notions of subset and partitions, with the associated lemmas

• decidable quantifications, such as [forall x in X, P x] and [exists x in X, P x] where X is a {set A} and P a predicate over A, which are defined as booleans Now that we have reviewed the relevant subset of MathComp, we can introduce the positive Datalog engine of [START_REF] Benzaken | Data-logCert[END_REF], Dumbrava, 2016].

Datalog syntax

We proceed as in Section 1.1, i.e. we first present the construction of Datalog programs, and then their manipulation with groundings and substitutions.

Building blocks

Just like the paper syntax, the formalization first assumes sets, seen as finTypes, for constants and predicate symbols, as well as an arity function.

Variable constype : finType. Variable symtype : finType. Variable arity : {ffun symtype -> nat}. As for the variables, they are encoded using ordinals. To do so, the formalization assumes a number of variables:

(* the type of variables will be 'I_n *)

Variable n : nat. With the atoms, clauses and programs can now be defined as well. Note that normal clauses and ground clauses are again separated at the type level. Also note that a program is defined as a sequence (MathComp's nomenclature for list) of clauses rather than a set, as in Definition 2.8, because atoms, and thus clauses, are not defined as a finTtype. This point is discussed in Section 7.3.

Manipulating formulae

A grounding is defined as a finfun (see Example 3.5) from variables to constants. Since the codomain is defined as a finType, groundings are finite themselves.

Definition gr := {ffun 'I_n -> constant}. Like the rest of the syntax, groundings are built inductively, starting with their application to terms. If the given term is already a constant, then it is left unchanged. Otherwise, i.e. if it is a variable, the associated constant w.r.t. the grounding is returned.

Definition gr_term (g : gr) (t : term) := match t with | Var v => g v | Val c => c end. The next steps of the definition are straightforward but illustrate the need to work with the well-formedness proofs carried by the atoms, i.e. show that applying the grounding to an atom does not break the property on the number of arguments.

(* raw atom grounding *) Definition gr_raw_atom g ra := RawGAtom (sym_atom ra) [seq gr_term g x | x <-arg_atom ra].

(* lift to full atoms *) Definition gr_atom_proof g a : wf_gatom (gr_raw_atom g a).

(* Building an atom with a well-formedness proof *) Definition gr_atom g a := GAtom (gr_atom_proof g a).

(* clause grounding *) Definition gr_cl g cl := GClause (gr_atom g (head_cl cl)) [seq gr_atom g a | a <-body_cl cl]. Lemma satom_proof a s : wf_atom (sraw_atom a s).

Definition satom a : sub -> atom := fun s => Atom (satom_proof a s). The substitutions work similarly, the difference being that a variable is not necessarily mapped to a constant, i.e. the codomain is an option type and the term substitution may leave a variable untouched if it is mapped to None. The concrete changes in the application of substitutions are found at the level of terms, i.e. function sterm.

Another change is the fact that substitutions can be compared in a more fine-grained manner than groundings. Figure 3.15 introduces the s1 \sub s2 notation, which checks that substitution s2 extends s1, i.e. contains at least the same mappings to constants.

Definition sub_st s1 s2 := [forall v : 'I_n, if s1 v is Some b1 then (v, b1) \in s2 else true].

Notation "A \sub B" := (sub_st A B). The functions of Figure 3.14, due to the potentially incomplete nature of substitutions and unlike gr_atom, return atoms rather than ground atoms. Datalogcert also provides an application of a substitution, shown in Figure 3.16, that expects a constant def to fill the blanks, and returns a ground atom.

Definition gr_term_def s t : constant := match t with | Val c => c (* odflt d x returns x if it is of the form Some y, d otherwise *) | Var i => odflt def (s i) end.
Definition gr_raw_atom_def s ra : raw_gatom := RawGAtom (sym_atom ra) (map (gr_term_def s) (arg_atom ra)).

Lemma gr_atom_def_proof s a : wf_gatom (gr_raw_atom_def s a).

Definition gr_atom_def s a : gatom := GAtom (gr_atom_def_proof s a).

Semantics

Like the paper definition of Datalog (see Section 2.2), DatalogCert contains and implements both the Minimal Model and Fixpoint semantics. The former, more abstract, is used as a reference in the certification of the latter, more applicative one, which is ultimately extracted and exported as the actual Datalog engine.

Minimal Model Semantics

With the formalization of substitutions, the minimal model semantics as defined in Section 2.2.1 is implemented in a concise and clear manner, shown in Figure 3.17 (* An interpretation is a set of ground atoms *) Notation interp := {set gatom}.

(* If every ground atom in the body is in the interpretation, then so is the head *) Definition gcl_true gcl (i : interp) : bool := all (mem i) (body_gcl gcl) ==> (head_gcl gcl \in i).

Definition cl_true cl i := forall g : gr, gcl_true (gr_cl g cl) i.

Definition prog_true p i := forall g : gr, all (fun cl => gcl_true (gr_cl g cl) i) p.

Fixpoint Semantics

The implementation of the Fixpoint Semantics can be split into two components. In Definition 2.45, the T P operator tries out every possible substitution to build new facts. This method would be easy to define in Coq using MathComp's set notations and the finiteness of the substitution or ground types, but the efficiency of the extracted Datalog engine would then be seriously impacted.

In consequence, rather than actually matching any substitution, the engine builds the minimal set of substitutions that, by construction, match the given clause and interpretation. Since this constructive matching will be used and discussed in our own work, we first introduce all the relevant definitions.

For shortness and clarity, the completeness and soundness results are mentioned but we do not show or discuss their formalization and proof (you may find these informations in Section 8.5.1 of [Dumbrava, 2016]). We then quickly outline how the certification of the fixpoint semantics, i.e. its relation to the minimal model one, is stated.

Constructive matching of clause bodies

The constructive matching is developped in a bottom-up fashion. The first step, at the level of terms, has three arguments: a term t, an expected constant d against which t is matched, and a substitution s that will (potentially) be enriched to store the result of the match.

If t is the same as d, then no addition to s is required, and the substitution is returned. On the other hand, if t is a constant but not the same as d, the match fails and None is returned. If t is a variable v that is already mapped to a constant by s, a similar equality check occurs. Finally, if s does no associate v to a constant, the substitution is enriched with its mapping to the expected constant d and returned.

( This role of the expected constant appears more clearly in the next function, which defines the matching between an atom and a gatom. The two lists of arguments (terms for the atom and constant for the gatom) are zipped, meaning that they are browsed in parallel. At each step, the constant from the gatom is used as the expected constant for the match_term function. The substitution is enriched step by step, using a fold. Although it is called match, this operation may then be better understood as a unification procedure between an atom and a ground atom. The matching between an atom and a ground atom can be lifted to atom and interpretation, i.e. set of ground atoms. To do so, match_atom is called on each pair, and the substitutions which were successfuly computed are collected. This way, match_atom_all i a s returns the set of substitutions built upon s such that, when applied to a, produce a ground atom in i. The next step is to define the matching of a full clause's tail, still w.r.t. an interpretation.

To do so, a join and a monadic fold for the set monad are defined. Concretely, function bindS applies f : A -> {set B} to each element of a set i and flattens the result (cover is the union amongst a set of sets). The monadic fold applies f to all elements of the l list, using the s0 value for its first iteration.

Definition bindS {A B : finType} (i : {set A}) (f : A -> {set B}) : {set B} := cover [set f x | x in i].
Fixpoint foldS {A : Type} {B : finType} This special fold is applied to match_atom_all, starting with a set only containing the empty substitution. This way, at each new atom, every substitution computed so far is (potentially) expanded into a new set of enriched substitutions, and the result is flattened.

(f : A -> B -> {set B}) (s0 : {set B}) (l : seq A) := if l is [:: x & l] then bindS s0 (fun y => foldS f (f x y) l) else s0.
Definition match_body i (tl : seq atom) : {set sub} := foldS (match_atom_all i) [set emptysub] tl. The match_body function returns the set of substitutions which, when applied to the provided tail (i.e. list of atoms), results in a list of ground atoms that all appear in the considered interpretation. This function is also generalized by match_pbody, which expects an initial set of substitutions rather than using a singleton with the empty substitution. Section 9.3 discusses how we tried to use it for inductions which could not be performed on match_body.

Definition match_pbody tl i ss0 := foldS (match_atom_all i) ss0 tl. The constructive matching being fully defined, it can be used in the definition of the actual T P operator.

Certified T P operator in Coq

DatalogCert splits T P into two functions. The first, cons_clause, computes the deductive part of the operator. Remark 3.13. Note that the matching of the computed substitutions is not checked. As previously stated, match_body builds a set of substitutions that, by construction, match clause cl and interpretation i.

Remark 3.14. The def argument is here simply for typing purposes, i.e. to compute a set of ground atoms rather than normal atoms (see the end of Section 1.1.2). Its value is irrelevant, as match_body cl i produces substitutions that actually associate a value to each variable appearing in clause cl, meaning that def will never be used in practice.

The cons_clause function is then encapsulated into fwd_chain, which both enforces the preservation of the interpretation and applies the deduction to every clause in the program.

Definition fwd_chain def p i : {set gatom} := i :|: \bigcup_(cl <-p) cons_clause def cl i. This lemma states that iterating T P B P times, i.e. as many times as there are possible atoms, captures the whole semantics m of the program while being minimal.

Part III Network Verification

The introduction of this thesis discusses the increasing need for safety and security in the design and maintenance of networks, and the resulting introduction of formal methods in the field of networking.

Chapter 4 unboxes the concept of network verification and outlines existing tools. Then, Chapter 5 focuses on a Datalog-based tool called Network Optimized Datalog (NoD). Finally, Chapter 6 introduces a tool built on top of NoD, and explains how its development led us to work on optimizations for the Datalog language.

Overall, this aim of this part is simply to familiarize the reader with the notion(s) of network verification, the various approaches from formal methods that have been leveraged so far, and the way they challenge traditional Datalog engines. The many intricacies and details of networking are then left out for shortness and clarity, and their knowledge should not be required.

Chapter 4

Approaches to network verification

Et la causerie, descendant des théories élevées sur la tendresse, entra dans le jardin fleuri des polissonneries distinguées Guy de Maupassant, Bel-Ami

Historically, the design and implementation of networks has not relied on the use of formal methods. As an illustration, the authors of [START_REF] Doenges | Petr4: formal foundations for p4 data planes[END_REF] recall the words of internetpioneer David D. Clark: "we believe in rough consensus and running code", which they analyze as a reflection of the notion of robustness upon which networking is built. Basically, robustness is not seen as a formal specification coupled with a proof that the analyzed system does not deviate from it, but rather a system which may contain small deviations from its general design, as long as they do not threaten the intended behavior.

As another illustration of the way priorities are established within the networking community, the seminal paper [Clark, 1988] states "While tools to verify logical correctness are useful, [...] they do not help with the severe problems that often arise related to performance".

The bottom-up philosophy described just above has been challenged over the last ten to fifteen years, with the introduction of Software-Defined Networking, or SDN [START_REF] Hu | A survey on software-defined network and openflow: From concept to implementation[END_REF]. This framework separates the forwarding plane, i.e. the switches and their forwarding rules, from the control plane, i.e. the higher-level routers which establish these local rules to implement a network-wide policy. As the name indicates, this approach roughly aims at designing networks like software, i.e. with programming languages rather than at the hardware level.

As a side note, in his talk at PEPM'201 , Nate Foster presents SDN as a form of partial evaluation, in the sense that the network-wide program that eventually computes the actual configuration of all devices is specialized for the underlying topology. This notion is not completely disjoint from the ideas we design and implement in Part V.

This top-down approach of networking opens the way to more traditional, higher-level verification tools, and even to the synthesis of network configurations. Section 4.1 first outlines the specificities of networking with which researchers struggle. Then, Sections 4.2, 4.3 and 4.4 introduce the three main types of network verification problems, i.e. dataplane verification, control plane verification and network synthesis, as well as some tools and techniques they harness.

The difficulty of network verification

The field of network verification is inherently difficult. One of the most obvious reasons is what networks fundamentally are, i.e. highly distributed systems, the complexivity of which is well-known. However, there are also some specificities in the way networks are designed and built, which we quickly outline for context.

A distributed and opaque development

The internet, like numerous other systems, relies on many protocols. These protocols, such as BGP, GRE or MPLS, are defined in documents called Request For Comments [rfc, 1989, Li et al., 2000, Viswanathan et al., 2001], or RFCs, which are published by the Internet Engineering Task Force. At the time of writing, there are over 8,500 RFCs, which are all informal [START_REF] Doenges | Petr4: formal foundations for p4 data planes[END_REF].

On the other hand, some documentations try to be exhaustive, but lack the theoretical tools (or habit) to provide an efficient formalization of both the actual systems and their semantics.

Moreover, many such systems are developped iteratively, leading to an inflation in the sheer size of the documentation. These two caveats are illustrated by the documentation of the OpenFlow protocol [START_REF] Mckeown | Openflow: enabling innovation in campus networks[END_REF], which grows from 44 pages2 to 1653 , and then to a rather obstruse set of 283 pages4 , where the protocol is fundamentally presented in the form of C code -which makes it de facto the language used to describe its semantics.

The points discussed so far in this Section are very elegantly summarised in [Shenker et al., ], which first recalls that mastering complexity and extracting simplicity are two very different tasks, which do not rely on the same abilities. It then states that networking never made the distinction, and historically focused strictly on mastering complexity. Overall, it advocates for the development of the intellectual foundations and capacity of abstractions required to shift from complexity to simplicity.

This absence of solid and clear foundations led to the existence of many protocols, which are hard to study and justify in the absence of formalization, and whose implementation is subject to interpretation. These implementations then vary across vendors and internet providers, meaning that verification tools must be flexible in the specifications they check or enforce, but at the same time be as automatic and precise as possible.

Packet-level combinatorics

This matrix of heterogeneous implementations is built on top of another combinatorics issue in networking, which is the size of packet headers.

Each packet contains not only the pure data it is supposed to carry around, but also a header, whose content roughly encodes where the packet is coming from, where it is going, some informations on the actual content and so on (in that sense, at the level of said content, this header is akin to meta-data, whereas it could be thought of as a type at the protocol level). This header is used by the various protocols, e.g. some bit is set to 1 when the packet goes through some required network component, to store the information.

In other words, whereas the actual data carried by a packet is of no interest to determine its behavior, the entierity of the header must be taken into account throughout its flow in the analyzed network, rewritings included. An IPv6 packet header has at least 40 bytes, i.e. 320 bits, which can be extended by the various protocols involved. This means that, in practice, a naive representation of packet headers will lead to a state explosion of the tool's underlying model. This precise point is crucial for network verification in general, Sections 5.3 and 6.2 discuss it in-depth in the context of the Network-Optimized Datalog and Octant tools.

A variety of questions

So far, we have used the phrase network verification without explaining what are the properties one may want to check and enforce. There is a multitude of such questions, among which

• Can a packet flow from A to B?

• Are sub-networks X and Y strictly isolated?

• Are packets efficiently shared across the different possible paths?

• Will my network still be operational if up to n links fail?

• Do all packets from sub-network X go through a network component which performs operation P?

• Do two networks have the same behavior?

The questions in network verification are then very diverse in nature, meaning that a (theoretical or practical) tool may be relevant for some of them, but not all, which complicates matters. As a sidenote, Section 5.2 discusses how the expressivity and flexibility of Datalog allow it to formalize and efficiently answer many of such questions.

With the exception of the last one, the questions above belong to the field of dataplane verification, which the next section discusses. Then, Section 4.3 introduces control plane verification, which includes the last question of the list and can roughly be seen as a kind of a priori verification process. Finally, Section 4.4 discusses the synthesis of correct-byconstruction networks.

Dataplane verification and testing

In networking, the dataplane -sometimes also called forwarding plane -is the set of low-level mechanisms that define the way packets are forwarded and transformed. For example, the authors of [START_REF] Fayaz | Efficient network reachability analysis using a succinct control plane representation[END_REF] see it as a function of the form (paquet, port) → (paquet, port).

More concretely, it is the topology and the forwarding tables contained in the switches spread across the network. These forwarding tables (very) roughly contain rules of the form "if a packet has a header that matches pattern t, it should rewrite its i th and j th bits to 0 and go through port p", which are assigned priority levels. The sum of these very local rules, along with other specialized components (firewall, network functions and middleboxes), define the behavior of the full network.

Dataplane verification covers any task that analyzes the properties of such a given configured network. This section outlines some tools that address these questions.

Finding a counterexample

One of the earlier works on network verification is [START_REF] Xie | On static reachability analysis of ip networks[END_REF], which tries to formalize and give a higher-level view of network reachability. After that, part of the research efforts focused on encoding networks into existing verification engines to automatically perform such analyses. Since these engines were not designed to handle networks and their specificities, the encodings had to be carefully chosen to allow scalability -eventually, specific engines were introduced, as discussed in the rest of this Chapter and the next two.

Such an example is found in FlowChecker [START_REF] Al-Shaer | Network configuration in a box: Towards end-to-end verification of network reachability and security[END_REF][START_REF] Al-Shaer | Flowchecker: Configuration analysis and verification of federated openflow infrastructures[END_REF], a model-checking tool that uses Computation Tree Logic [START_REF] Clarke | Automatic verification of finite-state concurrent systems using temporal logic specifications[END_REF] as the specification language, and NuSVM [START_REF] Cimatti | Nusmv: a new symbolic model checker[END_REF] as its backend engine. Flowchecker has only been used in small experiments and, despite smart encodings such as the use of Binary Decision Diagrams [Bryant, 1986] to perform the matching mechanism within forwarding tables, it does not scale to industrial-size uses.

The Z3 SMT-solver [de Moura and Bjørner, 2008] is another classical verification tool used for network verification. Its use as a backend for a specific Datalog-based verification tool is discussed in-depth in Section 5.3, but it had previously been used more directly in [START_REF] Zhang | Sat based verification of network data planes[END_REF]. In that setting, the network is encoded as a formula N and the negation of the property to enforce is encoded as ¬P . Then, the satisfiability of N ∧¬P is checked, and Z3 provides a counterexample to P w.r.t. N if it is valid.

This work is optimized by focusing on the behavior of a single packet on a single path, hence the return of a single counterexample. A very similar method is used in [START_REF] Mai | Debugging the data plane with anteater[END_REF].

On the other hand, some tools use abstraction to handle at once a set of packets to cover all the possible paths.

Finding all counterexamples

Although finding a counterexample to a specification or having a proof that there is none is, in itself, a valuable information, this approach has some limitations. The main one is that a single counterexample is not always very helpful -and even sometimes misleading -to fix a system as complex and intricate as a network configuration.

For example, computing the set of all counterexamples rather than a single one fosters incremental verification. When two versions of a configuration are checked, the singular counterexamples computed may have nothing in common, whereas sets of counterexamples are comparable, e.g. if one is a subset of the other, we may deduce that one of multiple problems has been fixed (or introduced). Network Optimized Datalog [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] belongs to this category, but is discussed in length in Chapter 5.

Moving from a computation of one to all counterexamples changes the underlying problem, which goes from SAT to AllSAT. Modern SAT solvers are not optimized for AllSAT, meaning that smart representation of the domain (mainly packet headers) must be leveraged.

The prime example of this approach is Header Space Analysis [START_REF] Kazemian | Header space analysis: Static checking for networks[END_REF]. This tool relies on the observation that reachability verification is more efficient when performed on equivalence classes rather than individual packets [START_REF] Yousefi | Liveness verification of stateful network functions[END_REF]. To represent a group of packet headers, ternary bit vectors (e.g. 1 ⋆ ⋆0) are used. Networking boxes (switches, firewalls etc) are seen as functions that take a packet pattern and an entry port, and return a set of pairs of packet and output port, i.e. T : (h, p) → {(h 1 , p 1 ), ..., (h n , p n )}. The behavior of a network is then seen as the composition of (lifted) routing functions, e.g. T 3 (T 2 (T 1 (h, p))).

According to its own authors, this approach worked terribly until they used a better representation of packet headers called Differences of Cubes [START_REF] Bjørner | Network verification: when hoare meets cerf[END_REF], which are formally introduced and discussed in Section 5.3. This representation roughly allows an efficient definition of mechanisms of the form "if a packet header matches shape s, but not exceptions e 1 , e 2 and e 3 , then...".

Other optimizations have been introduced. NetPlumber [ [START_REF] Kazemian | Real time network policy checking using header space analysis[END_REF] computes a dependency graph that relates forwarding rules to allow incremental updates and parallelization, SecGuru [START_REF] Jayaraman | Automated analysis and debugging of network connectivity policies[END_REF][START_REF] Bjørner | [END_REF] tries to detect locally that different nearby routers will forward clusters of packets the same way rather than relying on costly propagated analyses, VeriFlow [START_REF] Khurshid | Veriflow: Verifying network-wide invariants in real time[END_REF] observes and leverages the fact that the number of header equivalence classes is small in practice, and [START_REF] Plotkin | Scaling network verification using symmetry and surgery[END_REF] eliminates redundancy and reasons up to network symmetry (quotients parallelizable processes) to work on a simplified model of the analyzed network.

Finally, like traditional program analysis, recent research in dataplane verification dwelve in probabilistic territory. For example, Netter [START_REF] Zhang | Netter: Probabilistic, stateful network models[END_REF] translates a dataplane into a probabilistic network (which itself encodes a finite discrete Markov chain) and harnesses existing model-checking tools such as PRISM [START_REF] Kwiatkowska | PRISM 4.0: Verification of Probabilistic Real-time Systems[END_REF].

Testing

Although exhaustive verification is much more powerful, testing can provide a first, easier analysis of the correctness of a program. One of the main measures by which a test set is evaluated is coverage, i.e. the fact that these tests at least go through every line of the analyzed program, although not with every possible configuration.

Symbolic execution is a technique that maximizes coverage. To do so, a program is seen as a decision tree, where the nodes are its conditionals. Then, the constraints across all paths are collected, and solvers generate test values for each set of constraints. The main tool that uses symbolic execution is Klee [START_REF] Cadar | Klee: unassisted and automatic generation of high-coverage tests for complex systems programs[END_REF], which has been developed for LLVM. This technique has been used for dataplane testing [Zeng et al., 2012, Dobrescu and[START_REF] Dobrescu | [END_REF], by replacing program lines with rules and test cases by packet headers.

Control plane verification and testing

In networking, the control plane is the set of routers and protocols that set up the dataplane. Roughly, they explore and learn the (physical) topology and paths, compute the actual forwarding rules that are installed in the different switches to enforce the given configuration, implement dynamic updates and so on. Even more informally, if the data plane is the muscle of the network, the control plane is its brain. In that setting, the goal of control plane verification is ensuring that, given a collection of router configurations, the resulting dataplane will enforce a given property [Beckett et al., 2018].

Formal analysis and verification of networking protocols is actually older than the notion of SDN -as are networks themselves -, but the problems and techniques have significantly changed since, following the evolutions of telecommuncations and formal methods. Let us only jestingly mention two lines of research pursued at CNET Lannion, now Orange Labs Lannion, a few decades ago. The first is a mix of model-checking techniques à la Sifakis [START_REF] Queille | Specification and verification of concurrent systems in cesar[END_REF] and the simulation tool Véda [START_REF] Jard | Development of Veda, a prototyping tool for distributed algorithms[END_REF], Monin, 1989], which was developed using Prolog. The second, led by our other PhD advisor, applies Coq to the verification of distributed protocols [Heyd, 1997].

Going back to SDN, first research works focused on pen and paper, non-automated analysis of the highly complex BGP protocol [START_REF] Rekhter | A Border Gateway Protocol 4 (BGP-4). RFC 4271[END_REF]. For examples, works such as [Chang et al., 2003, Griffin and[START_REF] Griffin | [END_REF] study the instability and possibility of loops, [START_REF] Gao | Stable internet routing without global coordination[END_REF] develops criterias to avoid loops in the computation of forwarding tables, [START_REF] Le | Shedding light on the glue logic of the internet routing architecture[END_REF] showed that the route redistribution technique (allowing routes to be imported from one routing process into another process on the same router) may cause loops.

As for the automated tools, they can be split into two categories [Beckett et al., 2018]. The first category is verification not based on formal semantics model, such as checking configurations against a set of good practices and syntactic patterns [START_REF] Feamster | Detecting BGP configuration faults with static analysis[END_REF].

In a more modern fashion, [START_REF] Bauer | Detecting and resolving policy misconfigurations in access-control systems[END_REF] uses machine learning to find such dubious configurations.

Regarding semantics-based approaches, one of the main examples is BatFish [START_REF] Fogel | A general approach to network configuration analysis[END_REF]. This tool uses Datalog to specify and check the dataplane that would be generated by a set of router configurations w.r.t. a given environment or scenario. However, Batfish can only check the control plane for a single context, meaning that checking the robustness of a control plane w.r.t. a realistic set of possible environments and scenarios is not feasable in practice.

Subsequent tools have tried to address a higher-level verification problem, i.e. checking properties about many or all dataplanes that may emerge from a given control plane, although often at the cost of network design coverage. For example, Bagpipe [START_REF] Weitz | Formal semantics and automated verification for the border gateway protocol[END_REF] performs a symbolic execution of the message-passing semantics of BGP in all possible environments, but makes strong assumptions about the underlying network.

Another example is ARC [START_REF] Gember-Jacobson | Fast control plane analysis using an abstract representation[END_REF], which abstracts the configurations as weighted graphs, allowing the consideration of many failure scenarios at once. However, this abstraction is only possible if some features of BGP are not used.

Vericon [START_REF] Ball | Vericon: towards verifying controller programs in software-defined networks[END_REF], in the spirit of (traditional) program verification, analyzes SDN programs à la Floyd-Hoare-Dijkstra using (partially manual) deductive reasoning and firstorder logic, with Z3 as a back-end. However, it only checks safety properties and requires manual invariants for the proofs. More recently, Minesweeper [START_REF] Beckett | A general approach to network configuration verification[END_REF] and Crys-talNet [START_REF] Liu | Crystalnet: Faithfully emulating large production networks[END_REF] both introduce various heuristics to avoid performing eager computations, or work on circomscribed spaces and infer the effects of propagations.

Finally, the control plane may also be subject to testing rather than verification. Klee remains a major tool in this context, and is used to uncover latent bugs in BGP configurations before they appear in the dataplane. On the other hand, the dataplane testing tools introduced in Section 4.2.3 can not handle the level of complexity of control plane models. As another example, NICE [START_REF] Canini | A NICE way to test openflow applications[END_REF] avoids the state-space explosion described in Section 4.1.2 by analyzing the routing code within the control plane to extract practical equivalence classes on the packet headers.

Synthesis of correct-by-construction networks

The introduction of Section 4.1 quickly mentions the inherently distributed nature of networks as a general difficulty for verification. It is also true at the stage of development, as a highlevel, network-wide behavior has to be manually implemented as a collection of interacting local systems (the forwardings tables in the switches) which communicate using catch-all stacks (packet headers) rather than ad-hoc, clearer structures.

This hard and error-prone process is reminiscent of very low-level programming, e.g. in an assembly language, where the stack is used to encode in a sometimes obstruse way high-level concepts. The solution to this situation is also similar, as the introduction of Software-Defined Networking (SDN, see the introduction of this Chapter) is, in part, the research community's advocacy for the introduction of abstraction layers [START_REF] Casado | Ethane: Taking control of the enterprise[END_REF].

Some early, lightweight abstractions are found outside of the setting of SDN, for example in the introduction of templates and vendor-neutral configuration languages, e.g. RPSL [START_REF] Kessens | Routing Policy Specification Language (RPSL)[END_REF], Yang [Björklund, 2010] and Netconf [START_REF] Enns | Network Configuration Protocol (NETCONF)[END_REF]. These tools provide some notion of consistency and mitigate one of the difficulties of networking, i.e. the diversity of hardware vendors and associated implementations. However, these languages are not fundamentally different from previous configurations languages and do not address the gap between hardware and high-level intents.

On the other hand, the Frenetic project5 aims at the development of declarative network programming languages that would allow reasonning about the behavior of the network at a suitably high level of abstraction, and even formally establishing the correctness of the associated compiler and run-time system.

The people behind this project first introduced the homonymous language [START_REF] Foster | Frenetic: A network programming language[END_REF].

The two main features of the Frenetic language are the introduction of constructs to read the state of the network and specify forwarding policies, and the modularity allowed by the introduction of policy combinators, e.g. parallelization. It is then refined in [START_REF] Monsanto | A compiler and run-time system for network programming languages[END_REF], which introduces an actual policy language called NetCore, allowing more expressiveness and modularity. The run-time system is also extended and leveraged to handle features that can not be translated efficiently into forwarding tables, such as intricate packet classifications that could only be encoded in a switch using billions of prefix matching rules.

NetCore is then refined again in [START_REF] Monsanto | Composing software defined networks[END_REF], which notably introduces sequential composition, as well as a Python implementation of these abstractions called Pyretic. As a sidenote, Pyretic serves as a basis for Kinetic [START_REF] Monsanto | Composing software defined networks[END_REF], which leverages its compositional features to express network policies as finite-state machines, which allows the use of previously existing verification methods.

Finally, [START_REF] Guha | Machine-verified network controllers[END_REF] provides a network-wide semantics to NetCore and a Coq proof of the compiler and run-time system. The results of the first real deployment of this language are also presented.

Although definitely a step in the right direction (i.e. importing programming language theory into networking), the Frenetic project has been built in an iterative fashion, meaning that it lacks a clear metatheory and direction. Moreover, although modular and equipped with theoretical foundations, the languages of this collection define the behavior of the switches, meaning that an analysis of the network-wide behavior must be extracted from a low-level specification.

To build more solid foundations for network synthesis, the same authors introduce NetKat [START_REF] Anderson | Netkat: Semantic foundations for networks[END_REF], a network programming language that relies on Kleene Algebra with Tests (KAT, see [START_REF] Kozen | Kleene algebra with tests: Completeness and decidability[END_REF]). In practice, the behavior of a network is then specified as regular expressions augmented with a packet algebra that encodes packet matching and rewritings.

This well-studied theory comes with some results and algorithms, such as the decidability of equivalence between two programs (seen as automata). Many of the usual network questions (see Section 4.1.

3) can then be encoded into equivalences between the analyzed program and specific, minimal programs. For example, isolation between X and Y in network N can be stated as the equivalence between a packet that goes from X to Y (or the other way around) in N , and the empty program. Since then, NetKat has received various optimizations [START_REF] Foster | A coalgebraic decision procedure for netkat[END_REF], Smolka et al., 2015], been enriched with Linear Temporal Logic [START_REF] Beckett | Temporal netkat[END_REF], and gone into probabilistic territory [START_REF] Foster | Probabilistic netkat[END_REF].

However, over the years, the tool that has gained the most attention is the P4 domain-specific language. P4 combines high-level abstractions (packet parsers, match-action contructs) with an efficient compiler [START_REF] Bosshart | P4: Programming protocol-independent packet processors[END_REF]. However, like many other networking tools, it has been largely developped within the industry without much consideration for theoretical or formal foundations, as illustrated by its current 163-page documentation that leaves many aspects of the semantics unspecified6 .

Since its introduction, there have been multiple enhancements to P4, the most notable being the recent development of fully formal foundations [START_REF] Doenges | Petr4: formal foundations for p4 data planes[END_REF]. More precisely, this work presents a full definitional interpreter for the language as well as a simple core calculus with formal syntax, typing and operational semantics. Moreover, the type soundness and termination of the calculus are proved, and an implementation is developed and tested.

This last and, again, very recent work on a highly popular networking programming language allows one to hope that this field will continue to take inspiration from more traditional language theory and harness decades of research to build safer networking tools and foundations.

Chapter 5

Network Optimized Datalog Bjørner, 2008, Hoder et al., 2011] as a back-end. It models network policies at a higher-level of abstraction than other tools (e.g., VeriFlow [START_REF] Khurshid | Veriflow: Verifying network-wide invariants in real time[END_REF]) and handles dynamicity, in the sense that it is resilient to various changes in the modeled network without requiring changes to internals. This Chapter tries to convey the main ideas behind NoD, and is based heavily on [START_REF] Lopes | Network verification in the light of program verification[END_REF], Lopes et al., 2015].

We first discuss in Section 5.1 the choice of Datalog as a specification language, and outline in Section 5.2 the modelization of various network policies with Datalog. We finally introduce in Section 5.3 the modifications made to the underlying Datalog engine for NoD to scale, focusing on a specific component that will be of importance in the next Chapter.

Datalog as a specification language for network behavior

The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] identify five features that should be provided by an ideal network verification tool. Three of these features are natively found in Datalog, whereas the other two require some more work.

The first identified feature is that, when computing reachability, one wants to find all packet headers that can go from a network element A to B. Classical model checkers and SAT solvers [Biere et al., 2009, Jhala and[START_REF] Jhala | [END_REF] can go from existential to universal answers by adding the negation of the provided solution and iterating, but the performances would not be satisfactory. In contrast, once reachability has been encoded into it (see Section 5.2.1), and given a set of starting packets and locations, Datalog will natively compute the set of all reachable configurations (packet header h at port p).

The second desired feature offered by Datalog is the availability of higher-level constructs, 65 basically boolean operators. More concretely, the use of multiple rules to define a single predicate is akin to disjunction and can be used for example to combine reachability sets ("a packet can access location L 1 from L 2 using path P 1 or path P 2 "), whereas the bodies of Datalog rules are conjunctive in nature, which notably allows the expression of forwarding rule priorities ("A packet P can follow this switching rule if it matches it and there is no higher priority matching rule") -although not necessarily in the best and clearest fashion, as we will discuss in Section 6.1.

Finally, the authors underline that, using predicate arguments, Datalog can encode a notion of state. This can in turn be used to model evolving networks (zone failures, packet format changes) and protocols. The next section will emphasize these three benefits of Datalog by showing how it is used to specify the semantics of a network in [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF].

In contrast, Datalog does not handle natively the last two desired features: the ability to model the rewriting of large packet headers. Checking the semantics of a network implies reasoning about headers of around 80 bytes, whose rewritings impact the general behavior of the network. Concretely, the gigantic header space requires a compact representation that still allows dynamic rewriting. Section 5.2 illustrates how the rewriting is modeled at the level of Datalog specifications.

Although one can leverage the fact that many of the bits in a header do not matter to determine the way the packets moves through the network, the amount of information to track and account for remains important, where handling as much as 3 or 4 bytes leads to a state explosion. The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] then introduce modifications to the Datalog engines, which are presented in Section 5.3.

Datalog modelization of network beliefs

We re-introduce the core example of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF], which shows how Datalog can be used to specify and verify network reachability, the cardinal problem of network verification.

We will then overview some other examples of the paper, which illustrate -still from a very high-level point of view -the main ideas behind the modelization of various network policies one may want to enforce and check.

Reachability

We use the topology of Figure 5.1a, where R1, R2 and R3 are routers, whereas A, B and D are end-points. For simplicity, the example assumes that packets carry only two attributes in their headers, dst and src, which are both encoded over 3-bit vectors.

The industry standard in terms of memory management within routers and switches is Ternary Content-Addressable Memory, or TCAM [START_REF] Lakshminarayanan | Algorithms for advanced packet classification with ternary cams[END_REF]. This hardware-level consensus has driven the design choices in forwarding, notably having the possibility to match only a subset of the bits of a packet header. Concretely, such matchings rules can then contain an "any" bit denoted as ⋆.

Figure 5.1b displays the forwarding table of the example, which contains rules such as described above. Routers and switches may also rewrite some bits, as illustrated by the last rule of the table: when matched and processed, it rewrites the central bit of the dst vector to 0. Finally, some rules are mutually compatible, e.g. a packet can match the first two simultaneously. To deal with such cases, rules are assigned priorities. Given a packet, these rules are tried out in decreasing order until one matches, meaning that when a packet can match multiple rules, only the highest-priority one is enforced. In the case of Figure 5.1b, the higher the rule in the table, the higher its priority. The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] show how they translate to Datalog reachability from A to B and compute the set of packets that flow this way. Although it is not a requirement to understand the encoding, we invite the reader to try to compute this set by hand, and realize that even such a minimal and extremely simplified problem is actually non-trivial and error-prone. The details of these computations are found, for readability, in Appendix B.

A R1 R2 R3 D B (a) A simple network topology ... in dst src rewrite out R1 10⋆ 01⋆ R2 R1 1 ⋆ ⋆ ⋆ ⋆ ⋆ R3 R2 10⋆ ⋆ ⋆ ⋆ B R3 ⋆ ⋆ ⋆ 1 ⋆ ⋆ D R3 1 ⋆ ⋆ ⋆ ⋆ ⋆ dst[1] := 0 R2 (b) ...

and its configuration

The surpising complexity of this modest example justifies the use of automatic method for actual cases. Figures 5.2 and 5.3 show the Datalog translation of this example. The first figure contains syntactic sugar that encodes the various relevant guard conditions and effects of the routing table, whereas the second uses them to define the actual routing rules. Remark 5.1. Figure 5.2 uses a primitive predicate (cf. Section 2.4) denoted as =. However, the authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] do not comment on this predicate. On the other hand, they explicitely state that the only fact they put in the EDB is a symbolic packet, i.e. B(⋆⋆⋆ ⋆⋆⋆). In that setting, if x = y is the syntactical equality, the third rule of Figure 5.3 can never be used. It should be interpreted as x is compatible and at least as precise as y.

G 12 := dst = 10⋆, src = 01⋆. G 13 := ¬G 12 (dst, src), dst = 1 ⋆ ⋆. G 2B := dst = 10⋆. G 3D := src = 1 ⋆ ⋆. G 32 := ¬G 3D (src), dst = 1 ⋆ ⋆. Id := src ′ = src, dst ′ = dst. Set0 := src ′ = src, dst ′ = dst[2] 0 dst[0].
The modelization goes backwards. For example, the third rule of Figure 5.3 can be read as "if a packet with header dst ′ src ′ can reach B, and it can be specialized as 10 ⋆ src, then this specialization can reach R2". Note that the priorities between rules are manually encoded at the level of guards (cf. the second and fifth rules of Figure 5.2). Given these rules and an EDB containing the fact B(⋆ ⋆ ⋆ ⋆ ⋆⋆), a Datalog engine can be asked to compute the set of deductible facts about A, i.e. the set of packet headers that, starting from A, can reach B in any form.

Various policies

Although reachability is the backbone of network verification, one may want to check more intricate properties in practice. The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] introduce the Datalog formalizations of such problems, which we quickly overview.

Example 5.2. (Protection sets) It can be checked that some parts of the network are not reachable by specific elements (e.g., fabric managers are not reachable from guest virtual machines) by looking for a counterexample using the previously introduced method.

Example 5.3. (Reachability sets) Still using the search of a counterexample and the reachability encoding, one can check that all network elements of a given set are reachable from another set (e.g., all fabric managers are reachable from jump boxes (internal management devices)).

Example 5.4. (Equivalence of load balanced paths) In general, traffic from A to B does not follow a single path, but is balanced across multiple paths. The way packets are split between the available paths is usually determined by a hash function, which associates an index (within the number of paths) to every packet header.

A more complex question is then to check whether reachability across load balanced paths is identical regardless of other variables such as hash functions. This question can also be translated into Datalog, by encoding a hashing scheme as a bit vector h which determines the hashing choices made at every routers, and have a primitive predicate Select that selectively enables a rule given a hashing and a packet header. This Select predicate can then be added as an extra guard to the translation of the routing. Such rules would then look like those of Figure 5.4. With these new rules in hand, one can issue a query of a node that can receive a packet using one hash assignement, but would not be reached by the same packet under another hash assignement. Such a query, illustrated by Figure 5.5, would then detect inconsistent hashing.

R2(dst, h) ← G 12 (dst, h), R1(dst, h), Select(h, dst). R3(dst, h) ← G 13 (dst, h), R1(dst, h), Select(h, dst).
?A(dst, h 1 ) ∧ ¬A(dst, h2) The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] state that network verification tools of the time, such as [START_REF] Kazemian | Header space analysis: Static checking for networks[END_REF], Kazemian et al., 2013, Khurshid et al., 2013], were not able to answer this kind of query.

Example 5.5. (Locality) Another belief one may want to check, is whether, in a data center, traffic within a rack does not leave it. In other words, when a packet in a rack is meant for another place of the same rack, it should not leave the top-of-rack switch. One can easily define a set of places which should not be reached (e.g., using a "forbidden" predicate and a rule for each such place) and check that a packet is meant for a specific subnetwork (e.g., using equality modulo a mask). A counterexample can then be queried.

Example 5.6. (Dynamic packet headers) Whereas some network verification tools such as [START_REF] Kazemian | Real time network policy checking using header space analysis[END_REF], Kazemian et al., 2012] require a priori definitions and implementions to support various protocols, Datalog is flexible enough to only require such work at the level of the definition of the analysis, or even the query.

The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] illustrate this possibility with an outline of the Datalog encoding of the MPLS protocol [START_REF] Viswanathan | Multiprotocol Label Switching Architecture[END_REF], which relies on label stacking, i.e. the use of packet headers as a stack to store labels as it enters each new layer of the protocol, and unstack them when these layers are exited. Datalog does not support terms, and in particular lists, but it can encode within the predicate the number of stacked labels, and use one argument for each.

Remark 5.7. In that sense, the expressivity of Datalog is similar to that of finite automata, where the problem needs to be bounded a priori in the absence a dynamic structure such as a stack. The main difference between these two tools in this case are the simplicity and efficiency in which such problems can be formulated and solved. The use of automata for network verification is explored in NetKat (see Section 4.4).

Given a router R, we denote as R i the forwarding state with a stack of i MPLS labels. This is illustrated by the rules of Figure 5.6, which encode the stacking when going from a router R 5 to R 2 . The first rule adds an arbitrary label (2018) when there is none. The second rule expects that a such label is already present and stacks another on top, and so on. The last rule states that the stack overflows when trying to stack more than three labels.

R 1 2 (dst, src, 2018) ← G(dst, src), R 0 5 (dst, src) R 2 2 (dst, src, l 1 , 2019) ← G(dst, src), R 1 5 (dst, src, l 1 ) R 3 2 (dst, src, l 1 , l 2 , 2020) ← G(dst, src), R 2 5 (dst, src, l 1 , l 2 ) Ovlf (dst, src, l 1 , l 2 , l 3 ) ← G(dst, src), R 3 5 (dst, src, l 1 , l 2 , l 3 ) Figure 5.6: Encoding label stacking in Datalog
Finally, the authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF]] also mention two subtle bugs they encountered when working with middleboxes traversal (i.e. ensuring that some class of packets goes some networking component such as a firewall or a load balancer) and the use of backup routers. Both examples require the introduction of too many technicalities to be consistent with the rest of this document, but can be easily and elegantly formalized within Datalog.

Although these various examples illustrate how convenient Datalog can be to model and specify network behaviors, actually verifying them requires the aforementioned programs to scale, which is not the case natively. NoD has been designed with that objective in mind, i.e. with modifications made to the Datalog engine, which are discussed in the next section.

Modifying a Datalog engine for network verification

The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] build their tool upon a Z3 implementation of Datalog, called µZ [START_REF] Hoder | µz-an efficient engine for fixed points with constraints[END_REF]. Their modifications can be split into two main components: a packing of two operations in the algebraic preprocessing of the queries, which avoids the very costly representation of the intermediate step, and the choice of data structures for the packets and their rewriting. The first point is not addressed in this document, as it has no impact on our work. The representation of packets, on the other hand, was decisive in the reasoning which led to the developments presented in this thesis.

Relations are one of the key ingredients of Datalog. They are used to model the routers, which, unlike switches, handle some complex network behaviors, such as multicast (a packet gets duplicated into multiple copies which are all sent on their own path) or load balancing. Datalog encodes relations as tables, where each row represents a value tuple.

In the context of network verification, the naive approach is to represent a set of packet headers as a relation, and this relation as a table. However, doing so would result in a table with a number of rows that would be exponential in the number of bits used in the packet headers. For example, assuming we use 128-bit packet headers, accounting for all source addresses that start with a 1 would amount to a table of 2 127 rows. This approach obviously does not scale.

The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] mention that they first encoded the Datalog tables using Binary Decision Diagrams [Bryant, 1986]. However, they do not focus on this approach, as they show in [START_REF] Lopes | Network verification in the light of program verification[END_REF] that the results are significantly weaker than those of their second representation, which they call difference of cubes.

Definition 5.8. A difference of cubes, or DoC, is a set of packet patterns modulo exceptions. More precisely, they are of the form

i (ν i \ j ν i,j )
where ν i and ν j are bit vectors. In the setting of NoD, it can be understood as, for example, "every packet of the form ν 1 except those matching ν 1,1 or ν 1,2 , as well as the packets of the form ν 2 with the exception of those matching ν 2,1 ".

Example 5.9. We do not use the outer set union, which requires no illustration, to focus on the exception mechanism. The relation on 4-bit integers * * * 0 \ { * 11 * , * 00 * , 1010}

represents the set of naturals that are even (the binary representation must end with a 0), do not have identical "middle bits" (first two exceptions) and different from 6 (hard-coded special case).

The point of this representation lies in the handling of priority among rules. For example, in Figure 5.1, the second rule is used for a packet only if it does not match the first, meaning that the underlying formula for this possibility is of the form φ ∧ ¬ψ. Having a set of exceptions means that this can be used to model the n + 1 th rule as

φ ∧ ¬φ 1 ∧ • • • ∧ ¬φ n
where φ is the criteria of the rule and φ 1 to φ n are those of the first n rules.

Although very efficient in the context of NoD, where the Datalog programs are tailored for each analyzed network, this representation does not always fare well with more generic, abstract programs, as discussed in the next Chapter.

Chapter 6

Octant

Lorsqu'elle est pratiquée dans les règles de l'art, la prospective permet de repérer les principales métamorphoses qui couvent à bas bruit dans la société avant qu'elles ne s'expriment au grand jour, ce qui nous permet d'anticiper les grandes évolutions à venir.

Jean-Philippe Toussaint, Les émotions

Octant is a tool to formalize virtual network models and implementation in Datalog and check properties of those formalizations. It is based on the previously introduced Network Optimized Datalog engine. We justify the need for tools like Octant and show some examples of network and policy formalizations in the context of Openstack in Section 6.1, and discuss in Section 6.2 how the generic description of networking mechanisms triggers a state space explosion issue in NoD.

A higher-level Datalog model

Network Optimized Datalog, introduced in Chapter 5, allows the specification and efficient verification of network behaviors. However, the Datalog programs have to be tailored for each network and belief to check (see the example of Section 5.2.1, where the rules of the forwarding table are hard-coded into the program). Scaling this proof of concept result as it is to real-world, industrial level would then require either a kind of expertise that is not common amongst network engineers and architects, or a formally introduced and justified program transformation process. Moreover, such specialized programs are more complicated to write, understand and maintain than we could expect or require.

Octant works on networks that were deployed using the OpenStack [START_REF] Sefraoui | Openstack: toward an open-source solution for cloud computing[END_REF] cloud computing platform. In that setting, Octant can fetch the network's configuration and service databases through the REST APIs of the relevant services, i.e. mainly Neutron. This way, the specificities of such analyzed network can be abstracted away at the Datalog level, resulting in simpler and more generic programs.

OpenStack's back-end is based on relational databases, which makes this modelization process relatively straightforward. The two following examples, displaying the Octant detection of multi-attachment and of (a simplified version of) reachability (which does not handle security groups and firewalls), highlight the higher-level nature of this tool w.r.t. NoD.

In both examples, we consider the following extensional predicates: server(id), router(id), network(id), which denote the existence of an homonymous device associated to the provided id, and port(id, net id, device id, ip), that relates a port to network and a server or router. OpenStack uses many options to define networks, meaning that Neutron tables usually have many columns themselves. Arguments of extensional predicates are then named explicitly, so that those not used in a rule can be omitted (see the program of Figure 6.1, where two or one arguments of the port predicate are associated to a variable, depending on the rule).

Note that this also helps to quickly distinguish extensional and intensional predicates.

Example 6.1. The program of Figure 6.1 detects virtual machines connected to a network through a chain of routers and other networks. The linked predicate is defined as the set of pairs of ports appearing in the same router. Then, cnt (connectivity) is akin to the graph reachability specification of Example 2.9. Finally, we say that a server X is connected to a network Z when it contains a network Y which is connected to Z. This program fragment can be used as a basis to check that a virtual machine does not have a double attachment on networks with different security levels. For example, given the EDB shown in figure 6.2, we can deduce cntV M ( ′ M 1 ′ , ′ test ′ ) and cntV M ( ′ M 1 ′ , ′ prod ′ ). The rule of Figure 6.3 will then be able to detect the multi-attachment of M 1. Let route(id, router id, pref ix, mask, port id) represent an explicit rule on a router that identifies every packet whose destination attribute matches pref ix projected over mask, and sends them to the router port denoted by port id. This program fragment represents a typical use of a rule selection mechanism, as the linked predicate requires not only that the selected route matches (match route), but also that it has the highest priority possible (or, equivalently, that there exists no matching higherpriority rule, cf. not better route). In this context, priority is indexed on the length of the mask, i.e. the longest matching routing rule is selected.

linked(X, Y ) ← port(net id = X, device id = T ), router(id = T ), port(net id = Y, device id = T ). cnt(X, X) ← port(net id = X). cnt(X, Z) ← linked(X, Y ), cnt(Y, Z). cntV M (X, Z) ← server(id = X), port(net id = Y, device id = X), cnt(Y, Z).
port(id = ′ p1 ′ , device id = ′ M 1 ′ , net id = ′ test ′ ). port(id = ′ p2 ′ , device id = ′ M 1 ′ , net id = ′ inter ′ ). port(id = ′ p3 ′ , device id = ′ R1 ′ , net id = ′ inter ′ ). port(id = ′ p4 ′ , device id = ′ R1 ′ , net id = ′ prod ′ ). server(id = ′ M 1 ′ ). router(id = ′ R1 ′ ).
doubleAttach(X) ← cnt(X, Y ), cnt(X, Z), not Y = Z.
Even though it is omitted for clarity and conciseness, the security layer, i.e. firewalls and security groups, is also modelized in Octant. Just like the core of forwarding, its specification is both generic and abstract. Overall, Examples 6.1 and 6.2 display much higher-level specifications than what is done within NoD, for example in Section 5.2.1.

We emphasize that NoD and Octant, although closely related, do not address the same problems. A NoD program checks properties over a given network by mixing the descriptions of these general network properties (e.g. accessibility) and the specificities of the given network (e.g. topology, forwarding tables). On the other hand, an Octant program abstracts the aforementioned specificities to focus on the description of network properties, which can then be checked against a variety of concrete networks. Although highly beneficial, this lift in abstraction and clarity comes at a cost, which is discussed in the next section.

The cost of genericity

Octant is executed using Network Optimized Datalog [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF], a choice made for its efficency with the representation of routing rules and packet rewriting (see Section 5.3 of this document). Natively, NoD is simply not able to execute a program such as the one seen in Figure 6.4, as it does not come with DoC representations for primitives such as <. Section 6.2.1 first discusses the difficulties with the implementation of said primitive in DoC, then Section 6.2.2 explains how these difficulties blow up with the use of negation in Octant programs -two factors that would probably be harmless taken separately, but become a significant bottleneck when in conjunction.

Need and implementation of generic primitives

Handling in practice the new level of abstraction brought by Octant introduces the need for the addition and efficient implementation of primitive predicates, such as bitwise conjunction (&), equality and comparison, which all appear in Figure 6.4. NoD encodes everything using the difference of cubes (DoC) representation, introduced in Section 5.3. Many usual primitives, such as the comparison with a constant, equality and bitwise operations can be encoded very efficiently using DoCs, as illustrated by the following examples.

Example 6.3. Given a variable v typed as a four-bit integer, the v ≥ 1101 comparison can be encoded in DoCs as

⋆ ⋆ ⋆ ⋆ \ {0 ⋆ ⋆⋆, 10 ⋆ ⋆, 1100}
In general, any comparison of a variable and a constant will be encoded by a similar mechanism, where a linear number of prefixes are enough to eliminate any irrelevant value.

Example 6.4. Given two variables v 1 and v 2 representing four-bit integers, the equality relation v 1 = v 2 can be encoded linearly in DoCs by forbidding the existence of a dissonant pair of values, as illustrated by the following eight-bit DoC:

⋆ ⋆ ⋆⋆ v 1 ⋆ ⋆ ⋆⋆ v 2 \ {1 ⋆ ⋆⋆ v 1 0 ⋆ ⋆⋆ v 2 , 0 ⋆ ⋆⋆ v 1 1 ⋆ ⋆⋆ v 2 , ⋆1 ⋆ ⋆ v 1 ⋆0 ⋆ ⋆ v 2 , ...}
Example 6.5. The encoding of bitwise operations is slightly more twisted but linear as well.

The idea is simply to harness the binary nature of bits and forbid the opposite of the given operation's logical table, as illustrated by the DoC formalization of bitwise conjunction shown in Figure 6.5.

⋆ ⋆ ⋆⋆ v 1 ⋆ ⋆ ⋆⋆ v 2 \ {0 ⋆ ⋆⋆ v 1 1 ⋆ ⋆⋆ v 2 , 00 ⋆ ⋆ v 1 01 ⋆ ⋆ v 2 , 10 ⋆ ⋆ v 1 11 ⋆ ⋆ v 2 , 000⋆ v 1 001⋆ v 2 , 010⋆ v 1 011⋆ v 2 , 100⋆ v 1 101⋆ v 2 , 110⋆ v 1 111⋆ v 2 ...} Figure 6.6: Encoding the v 1 ≥ v 2 relation
As seen in Figure 6.4, optimality is a key component of the definition of forwarding, which itself is a building block of the other components. However, optimality can only be expressed using comparisons, whose representations using differences of cubes are exponential. One might expect that this exponential representation1 is not really a problem in practice, as we intuitively do no want to compute the full set of v 1 < v 2 pairs in Octant, but rather the set of addresses which are greaters than the one corresponding to the given M variable. This is also the mechanism used in µZ, as indicated in Section 2.2 of [START_REF] Hoder | µz-an efficient engine for fixed points with constraints[END_REF]. This means that, when executing the forwarding program of Figure 6.4, the rule defining linked requires the whole table of better route to be computed. Combined with the inefficient representation of the comparison between two variables, the overall result is extremely inefficient and unusable in practice.

Effects of the use of negation

However, as intuited above, the mechanism in practice does not match the behavior that one might first expect without knowing the handling of negation in Datalog. Indeed, from a more operational point a view, the better route rule is used whenever we need to check that the currently considered route is not surpassed in priority by another.

More concretely, seeing Datalog as a more traditional programming language with functions that are called in a top-down fashion, one may consider that the M parameter in the better route rule is fully defined at run-time, which does not match the bottom-up, stratified bevahior of µZ. The core of this document, and the work it represents, was then to introduce optimizations which provide clues to the Datalog engine and, somehow, help it simulate a top-down behavior to avoid the performance caveat just described. This is the topic of Part V, whereas Part IV first extends the tools at our disposal.

Part IV Extension of tools

Some of the tools previously introduced have been extended for the purpose of our work. This part introduce some new sequence and tree finTypes that we built upon the Mathematical Components Coq library, and then present the Datalog trace semantics we developed, with both the paper and (certified) Coq definitions.

Chapter 7

New sequence and tree finTypes

On ne peut pas entrer deux fois dans le même fleuve Héraclite, Fragments recomposés, traduit du grec ancien par Marcel Conche Mathematical Components (MathComp) is a Coq library that contains types and tools to define and formally prove traditional, pen and paper mathematical results within Coq. It notably contains a hierarchy of types with the following properties: having decidable equality (eqType), having a choice function (choiceType), being countable (countType), and finiteness (finType). These types are properly introduced in Section 3.1.

Our main focus here is finType, of which DatalogCert [START_REF] Benzaken | Data-logCert[END_REF], Dumbrava, 2016] already made an extensive use (see Chapter 3). Some of our additions could not fit in the different structures provided by finType, meaning that we had to develop our own sequence and tree finite types. We first introduce these new types, and then present some changes we make to DatalogCert. The uses of the tree finTypes will be shown when discussing the relevant definitions, i.e. in Sections 8.3 and 10.4.4.

Bounding sequences

We first introduce two types of sequences, the finiteness of which comes from syntactic or semantic criteria.

Syntactically bounded sequences

MathComp already contains a type for sequences of exactly a given length, called tuple, which inherits the finiteness property of any finType used for the elements. We introduce Wlist, the type of lists (sequence, or seq, in MathComp's nomenclature) bounded by a given nat.

The definition of Wlist can be found in Figure 7.1. Unlike tuple, it is not implemented using a signature type, but an inductive. Its definition follows that of traditional lists, with the addition of a bound of the number of elements within the type. The empty list case introduces any bound, meaning that precision can be lost at this level, whereas the addition of an element simply increments the bound.

Inductive Wlist (X: Type): nat -> Type := Bnil : forall n, (Wlist X n) | Bcons : forall n, X -> (Wlist X n) -> (Wlist X n.+1). Lemma cancelffgg: forall n, cancel (@ff n) (@gg n). Remark 7.2. The straightforward implementation of ff failed to deal with the n argument without adding return unit + A * Wlist A (pred n) to the match. Before thinking about this addition, we have been advised to use the Equations library [Sozeau, 2010], which does work and remained in the code.

The sum or product of two finTypes is a finType itself, and the finiteness of Wlist A n is the induction hypothesis. Figure 7.4 shows how these facts are combined to end the proof and fit Wlist within the finType framework, or mixin (see Definition 3.8).

Variable A: finType.

Definition wlist0_finMixin := @CanFinMixin (wlist0_countType A) unit_finType (@f A) (@g A) (@cancelfg A).

Definition wlist0_finType := FinType (wlist0_countType A) wlist0_finMixin.

Fixpoint wlistn_finMixin (n:nat): Finite.mixin_of (wlistn_countType A n). elim n. rewrite cteq. (* wlistn_countType 0 = wlist0_countType *) exact wlist0_finMixin. intros n0 EF. apply (@CanFinMixin (wlistn_countType A n0.+1) (sum_finType unit_finType (prod_finType A (FinType (wlistn_countType A n0) EF))) (@ff A n0) (@gg A n0) (@cancelffgg A n0)). Defined.

Definition wlistn_finType n :=

Eval hnf in (@FinType (wlistn_choiceType A n) (wlistn_finMixin n)). Remark 7.3. The proof is shorter and simpler than the one for the finiteness of tuple. This probably stems from the fact that it is less precise, as we do not explicitely state or prove the cardinal of Wlist.

Using this type in practice can be cumbersome, as adding an element to a Wlist bounded by nat m returns an element of type Wlist m+1, even though there may actually be much less than m+1 items in the list. We wrote functions that map elements of type Wlist to usual sequences, and the other way around. In the second case, elements from the list can be lost if it was too long for the bound of the returned Wlist.

However, we also wrote cancellation lemmas, shown in Figure 7.5, relying on properties about a given sequence's length. This controlled back and forth allowed us to use Wlist in practice, at the reasonable cost of tracking the size of the studied lists explicitly.

Lemma wlist_seqK (l : Wlist X m) : (seq_to_wlist m (wlist_to_seq l)) = l.

Lemma seq_wlistK (l : seq X) (H : size l <= m) :

(wlist_to_seq (seq_to_wlist m l)) = l. We used Inductive instead as it allowed us to rely on traditional proof methods. Showing the finiteness of such a signature type would be similar to what is done for the next type.

Sequences bounded by unicity

The second type of bounded sequences we introduce is a signature type called uniq seq, shown in Figure 7.7. It uses the uniq predicate already defined in MathComp to circumscribe the seq type to lists that do not contain the same element multiple times.

Fixpoint uniq s := if s is x :: s' then (x \notin s') && uniq s' else true. Structure uniq_seq {A : eqType} := {useq :> seq A ; buniq : uniq useq}. Lemma tag_of_dbranchK {A : finType} : pcancel (@tag_of_uniq_seq A) uniq_seq_of_tag.

Definition uniq_seq_finMixin {A : finType} := PcanFinMixin (@tag_of_dbranchK A).

Canonical uniq_seq_finType {A : finType} := Eval hnf in FinType (@uniq_seq A) uniq_seq_finMixin. The \notin predicate being of boolean type, i.e. decidable, the second version of the function simply checks whether the given element is absent from the list and, if it is the case, extracts the relevant proof to build the new, enriched uniq seq. If the element was already in the list, the latter is simply returned, as shown in Figure 7 Finally, we also provide in Figure 7.12 a definition of the empty list seen as an uniq_seq.

Definition unil {A : eqType} : @uniq_seq A := {| useq := [::]; buniq := is_true_true |}.

Figure 7.12: Empty sequence with unicity

Bounding trees

The second family of types we introduce are trees. Like the sequence types of Section 7.1, there are two finite tree types, one of them being bounded purely syntactically, whereas the other partially relies on a semantics criterion. Also note that, in this instance, the former is used as a backbone to the latter.

Generic trees in MathComp

MathComp contains an unbounded, variable-arity tree type, called tree, which is shown to be a countType. However, the nodes of this type can only contain nat elements 1 , whereas we need to use other types for our work. If these types are shown to be countable themselves, we could use the nat encoding of their elements, but that seemed like an unnecessary layer of technicality. We then start by introducing our own generic, unbounded variable-arity tree type, called ABtree, show in Figure 7.13.

Inductive ABtree: Type := ABLeaf : B -> ABtree | ABNode : A -> seq ABtree -> ABtree. We needed to develop several functions around this type. A core tool to reason about any recursive type is an induction principle, but the principle automatically generated by Coq did not handle properly the sequence of descendents, so we introduced ours, the boolean version of which is shown in Figure 7.14. The base case scenario requires that the given property is enforced by leaves, whereas recursion ensures that a property enforced by a list of trees will be preserved when the same list is used as the descendents of a new node.

Lemma abtree_ind : forall (P : ABtree -> bool), (forall x : B, P (ABLeaf x)) -> (forall h : A, forall l : (seq ABtree), ((all P l) -> P (ABNode h l))) -> forall t : ABtree, P t. A first definition we require later (cf. Section 7.2.3) is tree membership. Figure 7.15 shows its definition as a boolean predicate. Note that having the returned type as a boolean implies the use of the decidable equality ==, which in turn requires the node type A to be defined at least as an eqType.

Notation 7.5. The ~~notation is a boolean negation defined in MathComp (and not a double propositional one).

Notation 7.6. Predicates all and has, which were formally introduced in Definition 3.11, are roughly the decidable versions of forall and exists for sequences over eqTypes. Our end goal is the definition of a tree type where the finiteness comes from a bound on the number of descendants (syntactic width bound) and unicity across branches (semantic height bound). This type will be defined as a signature type over ABtree, but we first introduce another finType, which will be used in the finiteness proof.

Syntactically bounded trees

We define in Figure 7.17 the type of trees with bounded height and number of successors, called Htree. The bound on the number of successors w is enforced using the Wlist type, whereas the bound on the height n is incremented each time a root is added.

(* Max width of the trees *) Variable w: nat.

Inductive Htree: nat -> Type := BLeaf : forall n, B -> (Htree n) | BNode : forall n, A -> (Wlist (Htree n) w) -> (Htree n.+1). Definition fl x := (@fl_aux 0 x (@refl_equal nat 0)). The finiteness is shown similarly to that of Wlist, with an induction on the height (the finiteness of the width bounding is already encapsulated in Wlist). In the base case, we show that trees with height 0, i.e. leaves, are equivalent to the leaf type B. We first introduce in Figure 7.18 the transformation from trees to B. The cancellation lemma can then be used to prove the finiteness of Htrees of height 0, as shown in Figure 7.20.

Definition htree0_finMixin := @CanFinMixin (htree0_countType A B) B (@fl A B) (@gl A B) (@cancelflgl A B).

Definition htree0_finType := FinType (htree0_countType A B) htree0_finMixin. Lemma cancel_fflggl : forall n, (cancel (@ffl n) (@ggl n)). Finiteness is preserved by Wlist (cf. Figure 7.4), as well as the type product and sum. Assuming finite types A and B, we can then propagate their finiteness with the induction, as shown in Figure 7.22.

Fixpoint htreen_finMixin (n:nat): Finite.mixin_of (htreen_countType A B n). elim n. rewrite cteql. (* htreen_countType 0 = htree0_countType. *) exact htree0_finMixin. intros n0 EF. apply (@CanFinMixin (htreen_countType A B n0.+1) (sum_finType B (prod_finType A (wlistn_finType (FinType (htreen_countType A B n0) EF) w))) (@ffl A B n0) (@ggl A B n0) (@cancel_fflggl A B n0)). Defined.

Definition htreen_finType n :=

Eval hnf in (@FinType (htreen_choiceType A B n) (htreen_finMixin n)). Remark 7.7. Just like Wlist (cf. Remark 7.4), this type could have been defined more in MathComp's spirit, i.e. as a signature type over ABtree.

Semantically bounded trees using unicity

Having two different syntactic bounds makes the use of Htree quite tedious. Bounding the number of successors of nodes was not a problem in our use case, so we only had to deal with the height. Our trick is again to use unicity, to define -as a signature type -trees with at most n successors and unicity across paths, i.e. forbidding an element to appear twice in any branch.

Reusing tree membership (cf. Figure 7.15, the definition of unicity across branches in a tree as a boolean predicate -so it can be used in a signature type -is straightforward, as shown in Figure 7.23.

Fixpoint ABuniq {A : eqType} {B : Type} (t : @ABtree A B) : bool := match t with | ABLeaf _ => true | ABNode x l => ((all (ABnotin x) l) && (all ABuniq l)) end. The unicity property will be used to bound the height of our trees. We also need an horizontal bound, for which we define the width of a tree in Figure 7.24.

Fixpoint ABwidth {A B : Type} (t : @ABtree A B) : nat := match t with | ABLeaf _ => 0 | ABNode _ l => (foldr maxn (size l) (map ABwidth l)) end. Structure WUtree {A : eqType} {B : Type} (w : nat) := Wht {wht :> @ABtree A B ; Hwht : @wu_pred A B w wht}. It remains to show that WUtree is, or can be seen as a finite type. Our strategy is to use Htree as a backbone, i.e. show that unicity across branches enforces a height bounded by the cardinal of the node type, meaning that WUtree A B w can be injected into Htree A B w #|A|.

We do not go after this result directly, but rather demonstrate a more general lemma, stating that a tree with unicity and elements forming a subset of E has a height bounded by |E|. This allows us to reason about and isolate the "unused element" (the root of the tree) in the recursive part of the proof, whereas a type is set in stone. The formalization of these lemmas in shown in Figure 7.26.

Lemma uniq_ab_size {A B : eqType} (t : @ABtree A B) (s : seq A) : ABuniq t -> all (fun x => x \subset s) (ABbranches t) -> ABheight t <= size #|s|. Replacing set E by the full type A, the subset condition stated in uniq_ab_size is trivially true, which provides the height bound of any WUtree, as shown in Figure 7.27. It can then be shown that WUtree A B w is indeed a subtype of Htree A B w #|A|, and thus a finType.

Lemma height_WUtree {A B : finType} {w : nat} (t : @WUtree A B w) :

ABheight (wht t) < #|A|.+1. As for uniq seq, we developed two insertion functions. The first one takes as an argument a proof of the absence of the inserted root in the provided subtrees, whereas the other dynamically checks this (boolean) criteria. The code of these functions is omitted here for succinctness but very similar to Figures 7.10 and 7.11.

Adding new finite structures to DatalogCert

These new types were designed for additions to DatalogCert (namely, a trace semantics and a static analysis), which are subsequently introduced and discussed in this document. However, their development required some changes in the definitions presented in Chapter 3, the main goal being to define Datalog clauses as a finType.

The base ingredient of clauses, i.e. the set of atoms, is not defined as a finite type in DatalogCert, meaning that the first step is to encapsulate it within a finType. Figure 7.28 shows how we encode them, i.e. triples consisting of number of arguments bounded by max_ar, the predicate symbol, and the tuple of arguments. All these types are finite, meaning that such triples are as well.

Notation atom_enc := ({x : 'I_(max_ar.+1) & (symtype * x.-tuple term_finType)%type}). We make other minor changes to DatalogCert. The most notable one, shown in Figure 7.30, is the formalization and addition of the constraint to Datalog's syntax described in Section Chapter 8

A trace semantics for Datalog As stated in Section 1.2, a Datalog program consists of a set of base facts, called the EDB, and a set of Horn clauses, called rules. During the execution of a program, some rules are first used to deduce new facts from the EDB, then the newly enriched set of facts is used to compute new facts, and so on. The semantics of a Datalog program is a set of facts, meaning that the series of rules used in any deduction of one of these facts is lost in the process.

A program can contain multiple rules, the heads of which are atoms built with the same predicate, which implies that a fact can be deduced using different series of rules. If we want to be able to reason on the full deduction process leading to a fact, we need to introduce a richer semantics for Datalog.

Section 8.1 introduces the notion of collecting and trace semantics, and discusses some forms a Datalog trace semantics may take. Then, Sections 8.2 and 8.3 formalize on paper and in Coq respectively the semantics we introduce, as well as its certification, and discuss its use in DatalogCert.

Trace semantics and Datalog

Whereas a traditional semantics aims at giving a short and (relatively) simple meaning to a long series of computations, which can then be used to define program properties, which in turn will be determined by static analyses. A collecting semantics defines the strongest static property of interest, i.e. a class of static analyses [Hoare, 1978, Cousot, 2002].

Such semantics include transitive closures of a program's transition relation, state or predicate transformers, forward or backward reachability relations and so on [Cousot, 2005]. However, the basic example is that of computation traces, i.e. semantics that not only contain the meaning, or result, of a program, but also the computations that led to this result.

Remark 8.1. Although it might be an unorthodox use of the term, we denote this special case of collecting semantics, to which the semantics we introduce belongs, as trace semantics.

A basic example of trace semantics in the context of transition systems is a trace that corresponds to the (ordered) sequence of visited states. In the same spirit, a collecting semantics for an imperative language may typically take the form of an ordered sequence of program location / memory state couples. In comparison, the semantics we introduce is closer to what is done for a functional programming language in [START_REF] Perera | Functional programs that explain their work[END_REF], where the trace takes the form of a tree that unrolls the execution of the program.

However, the trace semantics we introduce only provides a partial order of the different steps of the execution. In that sense, it focuses less on the computation itself, but rather more on the logical structure of the deduction. An alternative, more traditional approach would have been to formalize the trace semantics as a transition system where the states are sets of available facts, and the transitions are labeled with rule / substitution pairs. In that setting, the traces would have been paths, the ordering of the program's execution would have been complete, and the logical structure would have been recomputable.

The choice of a lighter form of trace semantics stems from the use of the traces in the certification of the static analysis we introduce (see Chapter 10.4), where the logical structure is needed. We then chose to go with a formalism in which this structure was explicit.

Finally, we reckon that this idea of is not entirely novel, at least in presentation. For example, [START_REF] Halevy | Static analysis in Datalog extensions[END_REF] uses derivation trees (cf. their Figure 1), which look a lot like our traces. What we claim to introduce is rather the formalization and verified mechanization of such structures.

Definition

Programs tend to be defined and computed in a linear way, meaning that a classical form of trace semantics is a series of states in a transition system representing the program. On the other hand, the order in which the rules of a Datalog program are used and the new facts are deduced is of no importance, semantics-wise, as long as each predicate is saturated. This is reflected by our trace semantics, which represents computations as trees that store the logical structure of the program's execution, i.e. which rules were used to deduce which facts. The high-level idea is that the leaves are the starting points of the deductions, i.e. facts taken from the EDB, whereas internal nodes represent a deduction via a clause and a substitution, both stored in the node as a couple. The ground atoms of the instantiated body of the clause required for the deduction are (recursively) defined as the descendants of the node.

Definition 8.2. (Datalog trace semantics) More formally, a trace t is recursively defined using the following rules.

t ::= Leaf (ga),
where ga is a ground atom

| N ode( C, ν , [t]
), where C is a clause, ν is a substitution, and [t] is a list of elements of some type t Notation 8.3. In definition 2.32, the set of ground atoms relevant to a program P was denoted as B P . In that spirit, the set described above is denoted as B T (P ).

Example 8.4. Consider again the graph reachability program of Example 2.9. Denoting the first and second rules of the program C 1 and C 2 , Figure 8.1 displays the construction of the trace corresponding to the deduction of path(4, 3).

More precisely, in Figure 8.1a, the first rule is used with the substitution that maps X to 4 and Y to 2. The instantiated body only contains ground atom edge(4, 2), which is in the EDB of the program, meaning that it can used as a leaf.

Then, Figure 8.1b shows the deduction of path(1, 4). Using the second rule requires two subtrees, corresponding to deductions of the instantiated two atoms of the tail. The substitution used at the root requires as a first child a trace for the deduction of path(4, 2), so we reuse the previous one. On the right, we need edge(2, 1), which is in the EDB. Finally, Figure 8.1c builds upon Figure 8.1b in a similar manner. Remark 8.5. Such trees can be seen as proof trees w.r.t. the theory given by a Datalog program and an EDB. The fact that we call them traces stems from our computational approach of Datalog, in the sense that our goal was to study and optimize the execution of Datalog programs, rather than see them as deduction systems.

C 1 , X → 4 Y → 2 edge(4,2) (a) Deduction of path(4, 2) C 2 ,      X → 4 Y → 1 Z → 2 edge(2,1) C 1 , X → 4 Y → 2 edge(4,2) (b) Deduction of path(4, 1) C 2 ,      X → 4 Y → 3 Z → 1 edge(1,3) C 2 ,      X → 4 Y → 1 Z → 2 edge(2,1) C 1 , X → 4 Y → 2 edge(4,2) (c)
We can now introduce the various functions that lead to an operational view of this trace semantics. We first need a function that maps a trace to the corresponding deduced fact.

Definition 8.6. (ded -Erasing function from trees to facts)

ded(x) = f if x = Leaf (f ) ν(head(C)) if x = N ode( C, ν , descendants)
Example 8.7. The deduced fact in Figure 8.1c is path(4, 3), i.e. ded(t) = path(4, 3), where t is the trace of the figure.

Our implementation of the trace semantics also starts with the initial interpretation, which needs to be seen as a set of traces rather than facts. The tb function simply lifts these facts to leaves.

Definition 8.8. (tb -Interpretation to trees)

tb(I) = {Leaf (f ) | f ∈ I}
In the spirit of Section 2.2, the trace semantics is implemented via an operator called T t P . Also like T P , it is iterated to build new traces on top of the previously deduced ones. When deducing a new fact with a clause C and a substitution ν, the previous iteration must contain traces for the body of ν(C). The relation between traces and facts is specified using ded.

Definition 8.9. (T t P -Consequence operator on traces)

T t P (I t ) = I t ∪ {N ode( C, ν , [F 1 , ..., F n ]) ∈ B T (P ) | C = A 0 ← A 1 , • • • , A n ∈ P ∧ ∀i ∈ [1..n], (F i ∈ I t ∧ ded(F i ) = ν(A i ))}
Since this is our own semantics, we need to relate it with the usual one, i.e. show its adequacy. Given a program P with interpretation I, this result is expressed in the following lemmas, which are both proved by induction on the number of steps k.

Lemma 8.10. (Datalog trace semantics completeness) For any number of steps k, ∀x ∈ (T P ↑ k)(I), ∃t ∈ (T t P ↑ k)(tb(I)), ded(t) = x.

In other words, for any deduction using the fixpoint semantics, the trace semantics contains a tree representing a deduction of the same fact.

Proof. Let x ∈ (T P ↑ k)(I). We need to expose a t in (T t P ↑ k)(tb(I)) such that ded(t) = x.

If k = 0, then (T P ↑ k)(I) = I and (T t P ↑ k)(tb(I)) = tb(I) = {Leaf (f ) | f ∈ I}. Since x ∈ I, tb(I) contains Leaf (x).
From the definition, ded(Leaf (x)) = x.

If k = n + 1, then • (T P ↑ k)(I) = {head(ι(C)) | C ∈ P ∧ body(ι(C)) ⊆ (T P ↑ n)(I)} ∪ (T P ↑ n)(I) (definition) • (T t P ↑ k)(tb(I)) = (T t P ↑ n)(tb(I)) ∪ {N ode( C, ν , [F 1 , ..., F n ]) ∈ B T (P ) | C = A 0 ← A 1 , • • • , A n ∈ P ∧ ∀i ∈ [1..n], F i ∈ (T t P ↑ n)(tb(I)) ∧ ded(F i ) = ν(A i )} (definition) • ∀y ∈ (T P ↑ n)(I), ∃t ∈ (T t P ↑ n)(tb(I)), ded(t) = y (induction hypothesis)
Ground atom x is either on the left or right side of the ∪ in the definition of (T P ↑ k)(I). In the second case, we apply the induction hypothesis, which provides us a t in (T t P ↑ n)(tb(I)) such that ded(t) = x. Since t is in (T t P ↑ n)(tb(I)), it is also in (T t P ↑ k)(tb(I)) (left side of the definition). If x is in the left side of the definition of (T P ↑ k)(I), we can extract a clause C ∈ P and a substitution ι such that

(H1) x = head(ι(C)) (H2) body(ι(C)) ⊆ (T P ↑ n)(I)
We now use the right-hand part of the definition of (T t P ↑ k)(tb(I)), with C and ν. The condition is true thanks to (H2) and the induction hypothesis, whereas (H1) shows that the tree just built has the right shape.

Lemma 8.11. (Datalog trace semantics soundness) For any number of steps k, ∀t ∈ (T t P ↑ k)(tb(I)), ded(t) ∈ (T P ↑ k)(I). In other words, any trace corresponds to a deduction of a fact that is actually part of the fixpoint semantics.

Proof. If k = 0, then (T P ↑ k)(I) = I and (T t P ↑ k)(tb(I)) = tb(I) = {Leaf (f ) | f ∈ I}. Since t ∈ tb(I), there exists a f ∈ I such that t = Leaf (f ), meaning that ded(t) = f .

If k = n + 1, then • (T P ↑ k)(I) = {head(ι(C)) | C ∈ P ∧ body(ι(C)) ⊆ (T P ↑ n)(I)} ∪ (T P ↑ n)(I) (definition) • (T t P ↑ k)(tb(I)) = (T t P ↑ n)(tb(I)) ∪ {N ode( C, ν , [F 1 , ..., F n ]) ∈ B T (P ) | C = A 0 ← A 1 , • • • , A n ∈ P ∧ ∀i ∈ [1..n], F i ∈ (T t P ↑ n)(tb(I)) ∧ ded(F i ) = ν(A i )} (definition) • ∀t ∈ (T t P ↑ n)(tb(I)), ded(t) ∈ (T P ↑ n)(I) (induction hypothesis)
The trace t is either in the left or right part of the definition of (T t P ↑ k)(tb(I)). In the first case, we apply the induction hypothesis, which shows that ded(t) is in (T P ↑ n)(I), meaning that it is also in (T P ↑ k)(I) (left-hand part of the definition).

If t is found on the right side of the definition of (T t P ↑ k)(tb(I)), we can extract a clause C ∈ P and a substitution ν such that

(H1) C = A 0 ← A 1 , • • • , A n ∈ P (H2) ∀i ∈ [1..n], F i ∈ (T t P ↑ n)(tb(I)) ∧ ded(F i ) = ν(A i )
We now use the left-hand part of the definition of (T P ↑ k)(I), with C and ν (for ι). C is in P (H1), and the body of ν(C) is in (T P ↑ n)(I) (induction hypothesis and (H2)).

Remark 8.12. Section 2.3 recalls how Datalog handles negation, basically by dividing the program into multiple strata which are computed on top one of another. The T t P operator can then be used within each stratum, the same way the fixpoint semantics is. Now that our trace semantics is shown adequate, we can move on to its Coq implementation.

Coq implementation and certification

This section introduces the core definitions and lemmas of the Coq implementation of our Datalog trace semantics. In particular, we discuss how the finiteness of the trace type impacts the completeness proof shown in Section 8.2.

Definitions

The leaves of the traces are facts, or ground atoms, which are already defined in Figure 3.7. The node elements, which form a type called rul_gr, are pairs with a clause and a substitution, as shown in Figure 8.2.

Inductive rul_gr := | RS : clause -> sub -> rul_gr. Remark 8.13. Defining this node type as an Inductive rather than a simple pair is more of a personal taste than an actual need.

(** Conversion to and from pair so that we have a cancellable *) Definition rul_gr_rep l := match l with | RS c g => (c, g) end.

Definition rul_gr_pre l := match l with | (c, g) => RS c g end.

Lemma rul_gr_repK : cancel rul_gr_rep rul_gr_pre.

Definition rul_gr_finMixin := (@CanFinMixin rul_gr_countType (prod_finType clause_finType sub) _ _ rul_gr_repK).

Canonical rul_gr_finType := Eval hnf in FinType rul_gr rul_gr_finMixin. Clauses and substitutions being now both defined as finite types, this node type is then simply proved to be a finType itself, as shown in Figure 8.3.

As for the actual traces, we want them to form a finType so that the trace semantics can be expressed as a set (of traces rather than ground atoms), similarly to the usual Datalog semantics. To do so, we use the Wutree type, as defined in Section 7.2.3. The provided width bound is bn, the maximal size for the body of a clause (see Figure 7.29). The use of Wutree requires a default value for the leaf type, so we assume one, as shown in Figure 8.4.

Variable gat_def : gatom.

Definition trace_sem_trees := (@WUtree_sf rul_gr_finType gatom_finType bn gat_def). The implementation of the trace semantics still relies on match_body, which expects the set of ground atoms deduced so far, meaning that we need the ability to see a set of traces as an interpretation. The ded is also lifted to sequences of traces, as we will need to compare the ground atoms represented by a list of traces to instantiated bodies of rules. Both lifts are shown in Figure 8 Our implementation of the trace semantics leverages on a function that takes a set over an option type, e.g. option A, and filters out all the None elements to return a set over A. The implementation of this function, called pset, is discussed in Section 12.3.1, but Figure 8.7 shows its specification.

Lemma pset_spec {A : finType} (x : A) (s : {set (option A)}) : Some x \in s <-> x \in pset s. We first compute the set of relevant substitutions by applying match_body to the body of the clause, with k seen as an intepretation. Then, we use MathComp's set notations to iterate over every substitution s in subs and -using the tuple type for finiteness -any sequence of traces lx that matches the instantiated body (ded_sub_equal). For any such pair, we try to build a tree with root RS cl s and lx as children. If RS cl s was already in a trace of lx, wu_pcons_seq fails, None is returned and filtered out by pset. Otherwise, the actual tree is returned and preserved.

The second part of T t P , shown in Figure 8.9, is similar to function fwd_chain from Figure 3.25, as it applies cons_clause_t to every clause of the program and adds the original interpretation k, here as a set of trees. Now that the trace semantics is fully defined in Coq, we can move on to its formal certification.

Adequacy proofs

The certification of the trace semantics and its use in other proofs led to the development of various technical, very specialized lemmas which are not all listed here for conciceness and clarity. The most important two, shown in Figure 8.11, are about the reverse-monotonicity, i.e. the fact that all subtrees of a deduced trace are also part of the trace semantics. Note that the second lemma is about strict subtrees, which means that we can add some precision to the number of iterations needed for the capture of t1.

Lemma trace_sem_prev_trees nb_iter def init : forall (t1 t2 : trace_sem_trees), t2 \in (sem_t def nb_iter init) -> subtree (val t1) (val t2) -> t1 \in (sem_t def nb_iter init).

Lemma trace_sem_prev_trees_m1 nb_iter def init :

forall (t1 t2 : trace_sem_trees), t2 \in (sem_t def nb_iter init) -> strict_subtree (val t1) (val t2) -> t1 \in (sem_t def nb_iter.-1 init). Lemma trace_sem_soundness nb_iter def i: prog_safe p -> [forall t in (sem_t def nb_iter i), ded def t \in (sem p def nb_iter i)]. Remark 8.14. The proof of Lemma 8.10 needs to be adjusted, because of the type used to define trace_sem_trees. Indeed, we do not need to show that there exists a trace t representing a deduction of the fact x, but that there is a trace t with unicity representing a deduction of the fact x. This unicity constraint is not stated explicitely in trace_sem_completeness, as it is implied by the use of the WUtree type. In that sense, this completeness lemma does not state that every deduction is captured by the trace semantics of a program, but that a sufficient and representative subset is.

Proof. The proof is modified in its final case, i.e. when a fact x has just been deduced using T P , from which we extract a clause C and a substitution ι. Instead of directly building a tree with C, ι as its root, we need to check that it does not already appear in one of the descendants (provided by the induction hypothesis and hypothesis (H2)). If it does not, we can build the tree and proceed as in the previous proof. Otherwise, we extract the incriminated tree with C, ι as its root. Thanks to Lemma trace_sem_prev_trees and the monotonicity of T t P , it is captured by k iterations of T t P .

Chapter 9

Partial program instantiation

Pour un probabiliste, c'est un rêveur, il a des yeux verts qui le feraient prendre pour un théoricien des nombres, même s'il porte les cheveux aussi longs qu'un théoricien des jeux, de petites lunettes d'acier trotskisantes de logicien et de vieux T-shirts troués d'algébriste Hervé Le Tellier, L'anomalie Section 6.2 discussed how seriously -i.e. exponentially -the number of variables in the instances of some primivite predicates impacts the performances of the Network Optimized Datalog engine. This Chapter introduces a first program transformation that aims at the reduction of the number of such variables, by duplicating and partially instantiating clauses.

We first provide the intuition behind this rewriting in Section 9.1, then introduce and justify the paper definitions in Section 9.2. We finally dive into the Coq definitions and certfication in Section 9.3. The rewriting assumes some information about the behavior of the transformed program. The next chapter introduces a static analysis that computes this requirement.

Intuition

In theory, each iteration of the T P operator (see Definition 2.45), considers every pair of rule and substitution, and deduces new facts using only pairs that match the available facts. In practice, Datalog engines try to be smarter and more optimal. For example, the Coq formalization of Datalog introduced in Chapter 3 produces the minimal set of relevant substitutions using function match_body (see Section 3.3.2.1).

Let us now assume we have an efficient way to statically compute an overapproximation of these substitutions, i.e. a set S of n-tuples of values representing instantiations of a set of variables V 1 , ..., V n that all appear in a single rule. The idea behind our rewriting is to provide these value sets to the engine, roughly saying "you do not have to actually take these variables into account, consider only the value tuples in S".

However, we do not want to actually modify any Datalog engine, as it is a tricky and error prone process. It would also obviously be engine-specific, whereas our goal is to build and validate a method that may ultimately be used on top of existing tools (cf. Remark 12.2). Our idea is then to work at the level of the executed program, i.e. to rewrite it so that the 101 value overapproximations are passed on as clues.

To do so, a simple and surprisingly effective way is to duplicate the rules, instantiating the targeted variables with the provided value tuples. This both eliminates variables and preserves the semantics as, at any point of the computation, the available facts act as a safeguard, in the sense that the instantiated bodies of the clauses must indeed still be checked against them.

s(X, Y, Z) ← q(X), p(X, Y, Z). Example 9.1. Consider the program fragment in Figure 9.1, equipped with the interpretation {q(1), q(2), p(1, 3, 4), p(1, 3, 5), p(1, 7, 8), p(3, 4, 5)}. Figure 9.2 shows the minimal set of substitutions that would be built -for example by function match_body -to use the rule in that setting.

{      X → 1 Y → 3 Z → 4 ,      X → 1 Y → 3 Z → 5 ,      X → 1 Y → 7 Z → 8 } Figure 9
.2: Minimal set of substitutions to compute s and the semantics amounts to F = {s(1, 3, 4), s(1, 3, 5), s(1, 7, 8)}. Assume we want to get rid of variables X and Y , which would then correspond to V 1 and V 2 in the second paragraph of the section. In that setting, the set of instantiations S should contain at least the projections on X and Y of the substitutions given shown in Figure 9.3. Finally, let us assume that S is complete but not correct, in the sense that it contains another partial substitution is not relevant w.r.t. the rule and the provided interpretation, such as a mapping from X to 3 and Y to 4. Figure 9.4 sums up the overapproximation -i.e. set of partial substitutions -we consider. We can use the S set to replace the rule from Figure 9.1 by the three found in Figure 9.5. The first two rules can be used -in conjunction with the projections on Z of the substitutions of Figure 9.2 -to deduce the facts in F . On the other hand, the third rule can not be used, as q(3) does not match any fact in the EDB, or that will be subsequently deduced. In that sense, although it comes from a strict and incorrect overapproximation, this rule is harmless.

{ X → 1 Y → 3 , X → 1 Y → 7 }
{ X → 1 Y → 3 , X → 1 Y → 7 , X → 3 Y → 4 }
s(1, 3, Z) ← q(1), p(1, 3, Z). s(1, 7, Z) ← q(1), p(1, 7, Z). s(3, 4, Z) ← q(3), p(3, 4, Z). Remark 9.2. Example 9.1 is of course particularly simple, in particular as the code snippet used for illustration does not make use of recursion. However, the argument made at the end of the example can be adapted to this setting. The idea is that the first, recursion-free iteration of T P on the considered rule does not change the deduced intermediate set of facts.

Then, the second iteration will be based on the right interpretation, thus preserving again the deduced facts, and so on. The next section formalizes both the transformation (including the hypothesis on the provided set of substitutions) and justification.

Definition and proof

The partial program instantiation first assumes a program P and an initial interpretation -or EDB -I. As stated in the previous section, we may want to focus on a subset of the variables appearing in the program. This set, written V 1 , ..., V n at the beginning of Section 9.1, shall be denoted as R here. Finally, the instantiation requires a set of substitutions S that captures the actual computation for the program w.r.t. the variables in R. To formally define this hypothesis, we first need to define the restriction over a substitution.

Definition 9.3. (Restriction of a substitution) Given a substitution σ and a set of variables X, the restriction of σ over X, written σ |X , is defined as

σ |X (x) = σ(x) if x ∈ X x otherwise
We can now express the completeness condition over the set of substitutions S. The idea is to state that, whenever a clause C contains at least one variable to instantiate, the restriction over the relevant variables of any substitution that can match C after some number of iterations of T P appears in S.

Since both the match relation and T P operator are monotonic, w.r.t. the given interpretation and number of steps respectively, a substitution matches C after some number of iterations of T P iff it matches C w.r.t. the full semantics of the program. This fact justifies the following definition of the completeness of S, where vars(C) returns the set of variables that appear in clause C. Remark 9.5. Any method that takes a program and computes a set of substitutions which satisfies the previous criteria, such as the static analysis introduced in the next chapter, can then be used in conjunction with this rewriting, to fuel it. Now that we introduced the context of the rewriting, we can define the actual transformation.

scenario, the induction hypothesis gives us that f is also in (T inst(P ) ↑ k)(I), and thus in (T inst(P ) ↑ k + 1)(I) by definition.

In the second scenario, we can extract a clause C from P and a substitution ν such that ν matches C w.r.t. (T P ↑ k)(I) and f is the head of ν(C). If C has no relevant variable, i.e. |vars(C) ∩ R| = 0, we reuse the same clause and substitution. The atoms in the body of the instantiated clause being in (T inst(P ) ↑ k)(I) (induction hypothesis), we can indeed deduce f in inst(P ).

Otherwise, we use the completeness hypothesis on S to show that inst(P ) contains ν |R (C). We also use Lemma 9.10 to show that, using (T inst(P ) ↑ k)(I) as an interpretation, this partially instantiated clause matches ν |R to produce f . The proof of this lemma works in a both similar and dual way w.r.t. that of Theorem 9.11, as we get two substitutions (one for the clause instantiation, one matching the transformed clause) that need to be combined to retrieve the substitution used in the original program.

The combination operator between two functions to be used is the following.

Definition 9.13. (Union of substitutions) Given two substitutions σ 1 and σ 2 , the union of σ 1 and σ 2 , written σ 1 ∪ σ 2 , is defined as

σ 1 ∪ σ 2 (x) = σ 1 (x) if σ 1 (x) is a constant σ 2 (x) otherwise
Remark 9.14. Despite the first case of the definition, the union of two substitutions returns a term, i.e. a constant or a variable. A mapping to a constant will be prioritized over one to a variable, but if σ 1 (x) and σ 2 (x) are both variables, then so will σ 1 ∪ σ 2 (x).

Remark 9.15. If σ 1 (x) and σ 2 (x) are both variables or both constants, this operation defines different priorities between the two given substitutions. More concretely, if σ 1 and σ 2 both map x to different constants c 1 and c 2 , then σ 1 ∪ σ 2 will map x to c 1 , whereas if they map x to variables v 1 and v 2 , the latter will be returned by the union.

In that sense, calling it "union" might be slightly misleading, as it is not a commutative function. However, in practice, the domains of the two substitutions are disjoint wherever we use this notion in our work, meaning that defining the priority either way has no impact.

Proof. The soundness of the transformation is proved using an induction on the number of steps k. In the base case, the definitions again imply that (T P ↑ 0)(I) = (T inst(P ) ↑ 0)(I) = I, which is more general than our goal.

In the recursive case, let f be a fact in (T inst(P ) ↑ k + 1)(I). Then, f was either already in the previous iteration (T inst(P ) ↑ k)(I), or it has just been deduced and added. In the first scenario, the induction hypothesis gives us that f is also in (T P ↑ k)(I), and thus in (T P ↑ k + 1)(I) by definition.

In the second scenario, we can extract a clause C from inst(P ) and a substitution ν such

Coq implementation and certification

We first assume the variables introduced earlier: a safe (in the sense defined in Section 2.1.3, cf. Figure 3.10) program, an initial interpretation, a default constant (used to transform substitutions into groundings, cf. Figure 3.16), the set of variables to instantiate Rv (previously R) and the provided substitutions subs (previously S). Figure 9.6 sums up these assumptions.

Variable p : program. Hypothesis psafe : prog_safe p.

Variable i : interp. Variable def : syntax.constant.

Variable Rv : {set 'I_n}.

Variable subs : {set sub}. Surprisingly, the original development of DatalogCert does not provide a boolean substitution matching function. The closest thing is gcl_true (which is lifted to cl_true and prog_true, see Figure 3.17), used to implement the model-theoretic semantics. However, it only relies on groundings rather than (partial) substitutions, and is not explicitely related (i.e. via lemmas) to the other components of the formalization.

This led us to focus on the constructive matching mechanism, using match_pbody (cf. Figure 3.23) to define the substitutions involved in the proofs. However, proving results stated with this function was much harder than expected. Even trying to prove a simple, technical result such as Lemma 9.10 with a classical induction on the atom list required us to find non trivial formulations and abstractions. This is because match_pbody collects contraints as it goes through the atom list, meaning that we have to relate the constraints collected on both sides of the implication.

Using this constructive matching, we were able to (painstakingly) prove most of the technical lemmas needed for the adequacy proofs, but not all of them. Even if we had managed to do so, the effort required felt uncorrelated to the intrinsic difficulty of the targeted results. We eventually completed the adequacy proof after introducing a boolean matching predicate.

This predicate, shown in Figure 9.7, first checks that the variables of the given clause all appear in the domain of the given substitution. If it is the case, the substitution can safely be made into a grounding using function to_gr and the def constant, as it will not be used in the instantiation of the clause. Function gr_tl simply maps a given grounding to a sequence of atoms, here the body of the clause to instantiate.

Definition bmatch i cl s : bool := (cl_vars cl \subset dom s) && all (mem i) (gr_tl (to_gr def s) (body_cl cl)) .8 shows how this boolean matching was related to its constructive counterpart, and thus to the fixpoint semantics. The first lemma simply states that any built substitution indeed matches the given clause. The second lemma, given a boolean match, ensures that the constructive match would succeed on the given clause w.r.t. the interpretation, and that the result would be a subsubstitution, i.e. be sound w.r.t. the first one (cf. Figure 3.15).

Lemma match_body_bmatch def (cl : clause) i s :

s \in match_body i (body_cl cl) -> bmatch def i cl s.

Lemma bmatch_match_body def (cl : clause) i s: bmatch def i cl s -> exists2 r : sub, r \in match_body i (body_cl cl) & r \sub s. With all these definitions in hand, the hypothesis on subs can be defined as shown in Figure 9.11. .13 shows the Coq definition of Theorems 9.11 and 9.12, whose proofs are fundamentally similar to the paper versions above. The reasoning is actually quite recognizable when reading the proof script, which is always very satisfactory when working with Coq. The main difficulty was the use of substitutions and matchings, which we already discussed.

Lemma ccompleteness (m : nat) : (sem p def m i) \subset (sem tprog def m i).

Lemma csoundness (m : nat) : (sem tprog def m i) \subset (sem p def m i).

Theorem cadequacy (m : nat) : (sem tprog def m i) = (sem p def m i). In this chapter, we introduce a new static analysis for Datalog. This analysis approximates the behavior of a variable in a program, i.e. the set of values it will be instantiated with during the program's execution, by simulating a more local and less constrained version of the fixpoint semantics for Datalog introduced in Section 2.2.2. Section 10.1 first introduces some (harmless) assumptions made by the analysis about the studied program. Then, Section 10.2 outlines via an example the main ideas behind the analysis. These ideas are formalized in Coq in Section 10.3, and certified in the same framework in Section 10.4. This work led us to design another, stronger version of the analysis, whose introduction and discussion are relagated to Chapter 12.

Hypotheses and notations

The first hypothesis slightly simplifies the analysis by restricting the syntax of Datalog, but not its expressivity.

Hypothesis 10.1. (Constant-free heads) We first assume that the heads of rules in the program do not contain constants, but only variables.

Lemma 10.2. Hypothesis 10.1 is harmless, i.e. any Datalog program can be rewritten into an equivalent program that enforces it.

Proof. The proof of Theorem 12.5.2 of [START_REF] Abiteboul | [END_REF] provides a transformation that eliminates any given constant from a program. Roughly, the idea of this transformation is to introduce an extensional predicate C a for each constant a that appears in a head of the program, and transform any rule of the form H(..., a, ...) ← B 1 (...), ..., B n (...) into H(..., x, ...) ← B 1 (...), ..., B n (...), C a (x) where x is a fresh variable. The only fact about C a found in the EDB is C a (a), meaning that x will be constrained to be instantiated to a during the program's execution.

Our other hypothesis on the analyzed programs is even simpler but also helps keeping the formalization of the analysis minimal.

Hypothesis 10.3. (Unicity of variables across rules) We only consider programs where individual variables may only appear in a single rule.

Lemma 10.4. Hypothesis 10.3 is a harmless assumption, i.e. any Datalog program can be rewritten into an equivalent program where no variable is shared across multiple rules.

Proof. The property can be enforced by indexing the variable names w.r.t. the rules. For example, Figure 10.1 shows how an arbitrary program (Figure 10.1a) is transformed (Figure 10.1b) so that a variable does not appear in two different rules.

P (X, Y ) ← Q(X, Y ). Q(X, Y ) ← Q(Y, X). Q(X, Y ) ← f (X, X, Y ).
(a) A program with shared variables... Finally, we introduce two notations that will be used throughout the rest of this document.

P (X 1 , Y 1 ) ← Q(X 1 , Y 1 ). Q(X 2 , Y 2 ) ← Q(Y 2 , X 2 ). Q(X 3 , Y 3 ) ← f (X 3 , X 3 , Y 3 ).
Notation 10.5. (Predicate index) Given a predicate P and an integer i, we denote as P.i the i th index, or argument position (starting at 0) of P .

Notation 10.6. (tocc -Term body occurrences) We denote as toccs (for term occurrences) the occurrences of terms within the bodies of rules. These occurrences are 3-tuples in N 3 , where the components are the indexes of, respectively, the rule, the atom (within the body of the rule) and the argument (within the atom), starting at 0.

Example 10.7. In the program of Figure 10.1a, the toccs for the occurrences of X within the f atom are 2, 0, 0 and 2, 0, 1 .

Intuition and Example

We first introduce, via an example, the general principle of the static analysis. We then focus on a specific aspect, i.e. the way it deals with Datalog recursion. Finally, we outline how we extract a set of substitutions from the analysis results.

General principle

The work on this static analysis started by noticing that the T P operator can be split into three distinct constraints, and realizing that implementing a subset of these constraints could lead to an overapproximating but much faster analysis. We first use the program of Figure 10.2, where s, q and r are intensional predicates, and f 1 to f 3 extensional, to illustrate the three properties T P must fullfil. We will then give an intuition of the static analysis itself, and finally provide a detailed example of its application, using once again the program of Figure 10.2.

1. Given any EDB, the set of values with which X 1 can be instantiated in practice is a subset of the intersection of the sets of values that go through its instances within the body of the rule, i.e. the values of toccs q.0 and f 1 .0.

2. Focusing on the first, the set of values with which q.0 can be instantiated is a subset of the union of the values returned by the second and third rules.

3. Given a value for X 1 and another for Y 1 , they must be compatible, i.e. form a valid tuple of arguments for f 1 .

s(X 1 , Y 1 ) ← q(X 1 ), f 1 (X 1 , Y 1 ). q(X 2 ) ← r(X 2 , X 2 ). q(X 3 ) ← f 2 (Y 3 , Y 3 , Z 3 ), f 3 (Y 3 , X 3 , Z 3 ). r(X 4 , Y 4 ) ← f 2 (X 4 , Y 4 , X 4 ), f 3 (Z 4 , X 4 , Z 4 ) Figure 10.2: Defining s(X,Y)
Unlike the third constraint, the first two deal with variables and toccs individually, meaning that they involve much less computation than the entirety of T P , while hopefully still providing interesting constraints. Our idea is then to design an analysis that fulfills the first two constraints.

Since these two constraints fundamentally are intersections and unions of value sets (or, from a logical standpoint, conjunction and disjunction), the idea is to build, for any chosen variable, a tree with nodes labeled with ∩ and ∪. The leaves, which correspond to the starting points of any series of deduction, represent columns in the EDB tables, e.g. f 1 .0 or f 2 .1. This way, the tree represents the way values flow from the EDB to a variable during the execution of the program, without enforcing the unification across different variables.

The branches are annotated with the index of the corresponding clause (for the descendants of ∪-nodes) or atom (descendants of ∩-nodes). Although this helps understanding and relating the tree with the analyzed program, the main point of these annotations is that they can be used to simulate a weaker version of the third constraint of T P on top of the actual analysis, as we will discuss in Section 12.2.

Example 10.8. The flow of X 1 in the program of Figure 10.2 is shown in Figure 10.3. Let us relate it to the actual program, using the annotations.

Variable X 1 has two occurrences in the body of the first rule. The set of possible values for X 1 is a subset of the intersection of those for the two occurrences, meaning that the root of the tree has to be labeled with ∩. The first occurrence is in the q atom, which itself is the first element of the body, and the second occurrence is in the f 1 atom, second element of the body. This means that the node must have two children, annoted 0 (for the occurrence in q) and 1 (occurrence in f 1 ). Since f 1 is an extensional predicate, the second child is a leaf annotated with the corresponding EDB table. Let us now focus on the left child.

∩ f 1 .0 ∪ ∩ f 3 .1 1 ∩ ∪ ∩ f 2 .1 0 3 ∪ ∩ f 3 .1 f 2 .2 f 2 .0 0 0 1 3 0 0 1 2 0 1 Figure 10.3: Analysis of a variable
Since the first occurrence of X 1 was as the first (and only) argument of an atom of an intensional predicate (namely q) the subtree should represent the different ways to obtain values for q.0. This implies that the root will be annotated ∪ (in general, there will be a strict alternation between intersections and disjunctions throughout the analysis), and will have one child for each rule defining q.

We then have two descendants, annotated 1 and 2, which refer to the second and thrid rules of the program, respectively. We first focus on the latter. This rule defines q(X 3 ) using two atoms. However, only the second one, defined with the extensional predicate f 3 , contains an occurrence of X 3 , at its second argument. We then return a ∩ node, which only leads to a leaf that refers to the f 3 .1 column of the EDB.

As for the left child, annotated with 1, it refers to a rule that contains only one atom in its body, but two occurrences of the relevant variable, X 2 . We then have two descendants, both annotated 0, which compute overapproximations of the values for r.0 and r.1. Since the only rule defining r is the fourth one, the roots of both subtrees start are a ∪-node and have only one successor, labeled with 3.

The left subtree is an analysis of r.0, and thus of the first argument of the head of the last rule, X 4 . This variable has three occurrences in the body, meaning that there are just as many descendants. On the other hand, the second argument, Y 4 , has only one occurrence.

Dealing with recursion

The general principle introduced in Example 10.8 will obviously not fare well (i.e. loop indefinitely) in the presence of recursion, which happens to be a core feature of Datalog. To circumvent this issue, the analysis stores all previously visited toccs when recursively calling itself, to avoid having a program location twice in a branch of the returned tree. This bounds the derivations despite potentially recursive programs.

The rationale behind this fix is that, to find an approximation of the values going through some tocc, one should not look at the recursive part of the corresponding predicate, but rather the other predicates that "ground" it. This idea is formalized is Section 10.4, but let us provide a first intuition.

Example 10.9. Figure 10.4b shows the analysis of variable X 1 in the program of Figure 10.4a, where f is an extensional predicate. The analysis starts with the p atom in the body of the first clause. It then considers two different ways to deduce a p fact, i.e. the two clauses. When looking at the first clause, the analysis moves on to the variable matching p.1, i.e. Y 1 . It also occurs in the p atom, so we could expect to have again two children, one for each rule.

However, going through the first clause once again would require analyzing p.0. Since this is precisely what we are doing, all occurrences of X 1 have already been visited and stored. We then drop this possibility, meaning that the ∪-node we were considering only has one child. The analysis eventually captures the fact that values of p can be permuted, and that the set of values of X 1 is a subset those of q.0 and q.1. The first rule contains two occurrences of variable Y 1 in its head, and two occurrences of X 1 in its body, not at the same indexes. The static analysis does handle this program, and returns the analyses shown in Figure 10.6. In both trees, the nodes in red are those where a cut happens to avoid looping. Replacing the occurrences by the predicate and index they correspond to, the tree of Figure 10.6a represents (f.0 ∪ f.1) ∩ (f.0 ∪ f.2) = f.0 ∪ (f.1 ∩ f.2)

p(X 1 , Y 1 ) ← p(Y 1 , X 1 ) . p(X 2 , Y 2 ) ← f (X 2 , Y 2 ) .
p(X 1 , Y 1 , Y 1 ) ← p(Y 1 , X 1 , X 1 ). p(X 2 , Y 2 , Z 2 ) ← q(X 2 , Y 2 , Z 2 ).
∩ ∪ 1, 0, 2 ∩ ∪ 1, 0, 0 1 0 0 1 ∪ 1, 0, 1 ∩ ∪ 1, 0, 0 1 0 0 1 0 0 (a) Analysis of X1 ∩ ∪ 1, 0, 0 ∩ ∪ 1, 0, 2 1 ∪ 1, 0, 1 1 0 0 0 1 0 (b) Analysis of Y1
The other tree corresponds to the same formula, and, in both cases, the result corresponds to the actual behavior of the analyzed variable.

Now that the actual analysis of a variable has been fully introduced, we can discuss how the returned trees are used to extract a set of substitutions, as required by the program transformation presented in Chapter 9.

From trees to values

Given a tree representing the flow of a variable, we can easily extract a set of values with a structural induction. Intuitively, the ∩ and ∪ nodes are treated as set intersection and union, respectively. The leaves return the values in the corresponding column of the EDB tables, i.e. the extraction of a f.i leaf will return the set of constants appearing at the i th position of a f fact in the EDB.

Given the analyses of multiple variables appearing in the same rule, partial substitutions can be built as the cartesian product of the value sets extracted from the different trees. This simplistic procedure is not entirely satisfactory, as it may produce many irrelevant value tuples. Section 12.2 discusses how a more complex extraction process can use the annotations of multiple trees to circumvent this issue.

Formalization

There is usually a gap in clarity and simplicity between a paper definition and its concrete implementation. However, thanks to MathComp's set notations, both versions are actually very similar. We then reckon that presenting both would make for a rather redundant read, and restrict ourselves to the latter, i.e. the Coq/MathComp formalization.

From trees to DNF

Replacing ∪ and ∩ with ∨ and ∧, the trees produced by the analysis can be seen as propositional formulae, where the EDB columns are atomic elements. With that in mind, the Coq formalization works with Disjunctive Normal Forms (DNF). In this form, the relation with the actual computation seems less natural, as we lose the alternation between intersections and disjunctions, but is surprisingly much easier to prove.

Intuitively, the intersections in the DNF encode the different paths values can take, i.e. which rule is used to deduce an element of a predicate that is defined by multiple clauses. This way, one of the core lemmas of the certification process will state that every such possibility is indeed captured by the returned DNF.

Building a DNF requires the computation of every combination of propositional formulae across different disjunctions -here, sets. MathComp already provides a cartesian product between two sets, which is shown in Figure 10.7. Remark 10.12. Figure 10.8 leverages finTypes to use set notations, but the resulting definition, like setX, is not very constructive. In fact, the cartesian product is defined here using its specification. Thanksfully, such universal and useful constructs are easily found in traditional programming languages.

Handling occurrences

Implementing the static analysis requires a precise definition of toccs, i.e. program occurrences, as well as some related technical results. We present the main definitions, both as for exhaustivity, and to illustrate how, in the spirit of DatalogCert, the focus on finTypes sometimes implies a heavy, potentially cumbersome use of dependent types to bound the manipulated objects.

First, we need to define and bound the occurrence type. As shown in Figure 10.9, the coordinates are encoded as ordinals. The rule index is simply bounded by the number of rules. The index within the body of the rule uses the bn parameter introduced in Section 7.3. Finally, the index of the term within the arguments of an atom is bounded by the maximal arity within all the available predicates, max_ar, which was already defined in DatalogCert.

Notation max_ar := (\max_(s in symtype) arity s).

Record t_occ p := T_occ {r_ind : 'I_(size p) ; b_ind : 'I_bn ; t_ind : 'I_max_ar}.

Definition tocs p := {set (t_occ p)}. The t_occ type was easily shown finite using CanFinMixin. This finiteness results then allows the definition of sets of occurrences, called tocs.

We can now easily define functions that return the atom or predicate corresponding to a given program occurrence. As seen in Figure 10.10, we use the nth_error function, which does not require a default element like nth but returns an option type1 . Functions at(om)_at and p(redicate)_at then do as well. Remark 10.13. The use of an option return type generates some frustration during the proofs, as in practice we make sure to use these fonctions only with occurrences that actually correspond to something in the program, and have to care about irrelevant cases in matches and such. However, these functions being technically partial, there was probably no way to circumvent this -using a default return value rather than an encapsulation within an option type would have led to the same problem.

We also need functions that collect the set of occurrences of a given variable within the program. However, due to the bounding of occurrences as seen above, this process requires a surprising quantity of machinery. In particular, after trying, we will argue that writing this function ex nihilo seems illusory. We then build up to it, starting with the minimal relevant structures, i.e. lists of terms of atoms, as seen in Figure 10.11. Within these structures, the occurrences only contain an index, bounded by the maximal predicate arity.

Definition occsInTermList (v : 'I_n) (l : seq term) : {set 'I_max_ar} := [set i : 'I_max_ar | nth_error l i == Some (Var v)].

Definition occsInAtom (a : atom) (v : 'I_n) : {set 'I_max_ar} := @occsInTermList v (arg_atom a).

Lemma occsInAtomV occ a v : occ \in occsInAtom a v -> nth_error (arg_atom a) occ = Some (Var v). We then lift this to lists of atoms. To do so, we add a second ordinal (in the sense of MathComp, see Example 3.4) to the occurrences, corresponding to the index of the atom containing the variable occurrence. When exploring a new atom, this (atom) index is set to 0, and those of the previously seen atoms are incremented.

Figure 10.12 first shows how we define the increment of an Ordinal. Lemma ord_shift1 shifts a bound on x, ord_shift uses it to build the actual ordinal, and shift1 simply lifts it to sets of pairs. This function can then be used in the actual collection of occurrences of a variable in a sequence of atoms, shown in Figure 10.13. For each new atom a, we compute the set of occurrences it contains and assign it 0 seen as an ordinal (Ordinal (ltn0Sn _)), whereas the previously computed indexes are shifted by one on the left component. Remark 10.14. The machinery used to return finite types is specific to Coq, and would not appear in a natural implementation. Although it may hurt the performances of the extracted program, especially considering how heavily these functions are used, they are so fundamentally simple and easy to implement that one may safely consider recoding them in an actual programming language. We had to implement them in Coq for the analysis, but the verification of these definitions is not an earth-shattering contribution.

Finally, we will need the functions shown in Figure 10.17. Given default variable and terms, term_to_var cl j returns the j th term in the head of clause cl. Since Hypothesis 10.1 states that there are no constants in the heads of rules, we can add a safe cast to variables for typing purposes. In particular, the dv and dt default variable and term assumptions are only introduced for the exhaustiveness of pattern matching and use of the nth function.

With these tools in hand, we can move on to the definition and verification of the actual program analysis. Figure 10.17: Fetching head variables and body predicates

Analysis implementation

As stated in Section 10.3.1, an analysis result comes in the form of a Disjunctive Normal Form.

Although there are infinitely many DNF formulae, the leaf type (pairs with a predicate symbol and an ordinal bound by the maximal predicate arity) being a finType and the absence of repetition across branches in the trees produced by the analysis allow us to encode the formulae in a finite structure, namely a set of sets.

In that setting, the main, or outer set represents the disjunction, implying that the inner sets are the multiple intersections. These intersections contain the full branches as uniq_seqs (cf. Section 7.1.2) rather than simply the leaves, as it will help us relate the structure to the analyzed program's behavior. As previously intuited, using DNFs is akin to having an unfolding of the different possible behaviors of the studied program, which simplifies the core certification lemmas.

Figure 10.18 shows the Coq definition of the static analysis, analyze_var, which is broken down in the following paragraphs.

Arguments The actual implementation is (obviously) found in the analyze_var_prev function, which is parametrized by the set of previously visited toccs prev. Along with prev and the studied variable v, the function uses an argument count, which is used to ensure termination in a simple way. The analyze_var_prev function first checks whether it has reached 0 and, if it is the case, returns the empty set. In Section 10.4, when discussing the verification of the analysis, we will show how count can be instantiated by a program-specific bound while retaining compleness. Fetching occurrences Once it has checked that count has not reached 0, the function computes the set of occurrences of the v variable that are not in the prev argument, i.e. occurrences that have not been previously visited (:\: is the MathComp notation for set difference). Then, for each occurrence, we need to both make recursive calls over the corresponding predicate and index par, and store the current tocc on top of the result. Finally, we will have to combine the results over all occurrences.

Recursive calls Function analyze_pi focuses on a single tocc, o. It then computes the predicate corresponding to the occurrence, written f (due to the definition of occsInProgram, the None case is never used in practice, and is only here for the matching's exhaustivity). We then take a look at the kind of predicate, i.e. intensional or extensional, that f is.

If it is extensional, then we actually do not want to perform a recursive call, and just return a leaf. Since the addition of the currently studied occurrence is performed later in the code (in the case where it has to be put on top of multiple branches), we simply return an empty uniq_seq (cf. Figure 7.12) encapsulated in two layers of sets.

If f is intensional, we go over the program, looking for clauses whose head is an occurrence of the predicate. For each of these clauses, we compute the variable corresponding to the currently studied occurrence o, i.e. the variable in the head whose index is the same as the argument index (t_ind) of o. We then perform the analysis on these variables, using a prev argument enriched with the current occurrence (cf. the call of analyze_pi). The results, which are all sets of sets representing DNFs, must be merged. Since the different rules defining a predicate correspond to multiple ways to deduce a fact, we want a disjunction over all these possibilities. We then unify all the disjunctions into a single one, by merging the outer sets using a bigcup notation.

Storing the current occurrence Now that we have a flow leading to the currently studied occurrence, we need to put it on the top of the different branches. This is done, as seen with function all_add_o, with a simple set notation. Merging analyses across occurrences We now have a DNF, seen as a set of sets, for each occurrence of the studied variable v. All these occurrences are from the same rule (thanks to the assumption that a variable does not appear in two different clauses), so we want to return an intersection of these results. To do so, we perform a cartesian product over them (gen_setX, defined in Figure 10.8), followed by some technical manipulations found in Figure 10.19 for type coherence. We illustrate this process via a semi-formal example, where DNFs and their implementation as sets of sets are interchangeable.

Figure 10.20 shows two DNFs to merge -the atomic elements, which are sequences in the analysis, are abstracted as their actual nature does not impact the process -whereas Figure 10.21 shows the expected result. The cartesian product, as implemented above, will return a set of tuples of conjunctions when applied to a sequence containing the two DNFs. This structure is shown in Figure 10.22.

We then need to get rid of the tuple layer to be left with sets. Function bigcup_tup shown in Figure 10.19 merges the sets contained in a tuple. Function bigcup_cart, found in the same figure, uses it to return a set of sets as shown in Figure 10.23, which happens to be the encoding of the expected result. .24 shows how the analysis is called. Arguments def and dv are default constants and variables, introduced only for typing purposes, whereas p and v are the analyzed program and variable. Finally, #|rul_gr_finType| is the fuel with which the count argument is instantiated. The use of this value is discussed in Section 10.4.4. Now that we have defined an analysis, we need to be able to extract a set of substitutions from it.

Definition analysis := analyze_var p (Val def) dv v #|rul_gr_finType|.

From analyses to sets of values

The analysis being formalized as a set of sets of lists (or branches), we define the extraction at all these levels, starting with the lists. These branches represent, from right to left, paths taken by values from the EDB to the analyzed variable. Before going through the EDB to look for facts, we need to figure out which predicate and argument index to use, i.e. those that correspond to the extensional predicate at the start of the path. More concretely, we need to be able to extract the predicate and argument index of the last element of the occurrence list, which is done by the functions of Figure 10. Remark 10.15. By construction, the cases than return the assumed default predicate value df are never used. Similarly, the last function expects a default value to return in case the provided list is empty, but it will not be the case in practice.

Notation 10.16. The functions of Figure 10.25 are defined on sequences of toccs. However, they will be used in practice on the type of branches the analysis returns , i.e. uniq_seqs rather than simple lists. As shown in Figure 10.26, we call this type dbranch.

Once we have the predicate and argument index of the last element of a branch, we go through the EDB, looking for any fact defined with the predicate, and extract the relevant constant argument, as shown in Figure 10.26. Default constant and predicate values def and df, introduced above, are once again irrelevant in practice but required.

Definition dbranch := (@uniq_seq_finType (t_occ_finType p)). Finally, and as previously stated, the inner sets are seen as set intersections, whereas the outer set is a union. These are simply translated using the bigcap and bigcup notations of MathComp. Figure 10.27 shows this step and how, once we have extracted the variables of an extraction, we build a set of singleton substitutions, i.e. substitutions that only map the studied variables to the different values. Although small in size and despite the heavy use of set notations, the definition of the static analysis, and especially the implementation of the analysis itself, might seem surprisingly convoluted to the reader, considering how simple the base idea driving it is. This odditywhich appeared to us early on -motivated us to verify the whole optimization process in Coq. The next section introduces the certification of the static analysis.

Certification

The certification of the static analysis requires the introduction and formalization of a new tool we call the no-recursion trace. This section first explains why it is required and provides an intuition of its nature. We then formalize it, which allows its use in the verification of the static analysis. After discussing that general process, we focus on a specific aspect, namely the certification of the termination of the analysis.

Another angle on traces

As previously stated, the analysis does not consider the same program point twice, allowing a fast and terminating analysis of recursive programs. However, it also makes it harder to relate the produced tree and the actual deduction of facts, i.e. traces. To do so, we introduce an intermediate layer, which we call the no-recursion trace. The idea is, given a deduction, to identify a truncated version of the trace that is closer to the analysis while still preserving enough information to be related to the actual trace semantics -a problem already tackled in other verification contexts [START_REF] Jeannet | [END_REF]Serwe, 2004, Shivers, 1991].

The truncation of the trace is done at the level of branches. More concretely, given a deduction trace and a variable, we look at the branches of the former, which all correspond to a path followed by a value from the EDB to the variable, and transform them into truncated, repetition-free sequences of toccs that approximate the aforementioned path. The no-recursion trace is then presented as a set of uniq_seqs, as introduced in Section 7.1.2.

Example 10.17. We reuse the graph connectivity program from Example 2.9 and the trace of Figure 8.1c, which represents a deduction of path(4, 3). Let us compute the no-recursion trace of X in the second rule. X has only one occurrence in the body of the rule, at index 0 of the path atom. We then look at the child corresponding to the atom -the left childof the actual trace, which contains the same clause. The corresponding term, i.e. the one at index 0 of the head of the clause, is again X, which only occurs in the path predicate.

Since this tocc, 1, 0, 0 , has already been visited, it is not added again to the sequence. In a sense, we ignore this step and keep exploring the trace, which leads us to the first clause. We add the tocc of X in the edge predicate, i.e. 0, 0, 0 . The next child in the trace is a leaf (the edge predicate is extensional), so we stop here and return a set that only contains the sequence [ 1, 0, 0 , 0, 0, 0 ]. This sequence indeed is a truncated, repetition-free version of the flow taken by values from the EDB to the variable w.r.t. the actual deduction, or trace, of path(4, 3).

Remark 10.18. In spite of its name, and as seen in Example 10.17, the no-recursion trace can capture executions which do use recursive rules. Moreover, the returned lists have the unicity property, but can contain multiple toccs which refer to the same clause. Example 12.3 will illustrate this point.

Formalization of the no-recursion trace

The Coq definition of the no-recursion trace is shown in Figure 10.28. The tr argument is the trace to be truncated, prev is the set of previously visited program locations, v the variable to focus on, and count is used to ensure the termination of the function. Given a strictly positive value of count, we take a look at the shape of the trace. If tr is a leaf, we return a set only containing an empty list, which will be enriched at the level of nodes. Indeed, given a node storing a clause cl and a substitution s, and whose children are stored in the descs list, we first compute the occurrences of v that have not been visited yet. Then, for each such occurrence o, we perform a recursive call on the i th child, where i is the atom index within the body (b_ind) of the given occurrence. However, since the function takes the currently variable as an argument, we need to be able, when performing the recursive call, to provide it, and thus to have access to the next clause the trace leads us to.

To circumvent this issue, we partially unfold the recursion in the unrec_b function. If the i th child is a leaf, then, in concordance the general definition, we return a singleton containing the empty list. Otherwhise, we get access to the new clause stored in the child node, called clb. We then get the variable at the j th position of its head, where j is the argument index of o. After that -and adding o to prev, the recursive call is performed.

When recursion w.r.t. an occurrence returns a set of uniq_seqs, we add that occurrence on top of all of them, using the pucons function defined in Figure 7.11. Finally, the set union of all such sets is returned.

We can now use this no-recursion trace to bridge the gap between the actual traces and the analyses we produce.

Relating executions and analyses

The first step of the certification is to state, and prove, the completeness of the no-recursion trace. More concretely, for every trace and involved variable, every branch in the no-recursion version of the trace must lead to a fact in the EDB that is relevant to the execution. Even more concretely, for every trace tr leading to a deduction combining a clause C with a substitution σ, and every variable v occurring in C, any branch in the no-recursion version of tr w.r.t. v leads to a tocc x, y, z that corresponds to an extensional predicate p such that the EDB contains a fact f whose z th argument is the σ(v) constant. Figure 10.29 shows the Coq definitions and the exact formulation of the completeness.

In other words, the no-recursion trace is used to compute the bounded part of the constraints which the actual semantics enforces, and, in that sense, it is complete. Another interpretation is that, as intuited by the theorem's name, dealing with recursion (see Remark 10.18) is not necessary to get a first approximation of the semantics of a program. Now that we related the actual traces and their no-recursion counterparts, we need to do the same with the latter and the static analysis. This is where the encoding of the analyses as sets of sets of branches comes in handy, as the core lemma simply states that the no-recursion trace is actually and simply an element of the analysis, meaning that it is captured. This lemma is shown in Figure 10.30.

Combining the results of Figures 10.29 and 10.30 would obviously be the key ingredient to prove that the static analysis captures (and may approximate) the semantics of Datalog. However, doing so would leave a blindspot, which is the termination of the analysis.

Lemma no_rec_capt prev tr i m cl s v : tr \in sem_t p gat_def def m i -> ABroot (val tr) = inl (RS cl s) -> v \in tail_vars (body_cl cl) -> unrec_trace_gen prev tr v (height tr).+1 \in analyze_var_prev prev v (height tr).

Termination of the analysis

Lemma no_rec_capt uses analyze_var_prev with its fuel argument, count, set to the height of the considered trace. However, the height of traces is theorically not bounded (see for example the program of Figure 10.4a, where one may use the first rule an arbitrary number of times), meaning that this lemma does not provide a satisfiable guarantee that the analysis is usable in practice. We then need to find a bounded value for count and show that its use preserves the completeness result.

Our first intuition for its concrete value was the cardinal of the tocc type, i.e. the number of arguments positions within the bodies of the given program. Although it probably could have easily been shown to be an adequate value, a different, potentially surprising answers emerges from no_rec_capt and our development of finite tree types. Indeed, as stated in Section 8.3, we implement the traces as WUtree rul_gr gatom. Lemma height_WUtree, defined in Figure 7.27, then implies that the height of any trace we consider is bounded by #|rul_gr_finType|, the cardinal of the rul_gr type. Morever, Lemma trace_sem_completeness stated in Figure 8.12 shows that restricting ourselves to these bounded traces is sufficient to capture the full semantics of Datalog. Thanks to these results, and after showing the monotonicity of the analysis w.r.t. its count argument, we can define and certify an execution of the static analysis with a program-specific bound, as seen in Figure 10.31.

Lemma analyze_incr prev v (m1 m2 : nat) : m1 <= m2 -> analyze_var_prev prev v m1 \subset analyze_var_prev prev v m2.

Theorem no_rec_capt_nf tr i m cl s v : tr \in sem_t p gat_def def m i -> ABroot (val tr) = inl (RS cl s) -> v \in tail_vars (body_cl cl) -> unrec_trace p tr v (height tr).+1 \in analyze_var v #|rul_gr_finType|. , where c and v are the number of constants and variables, respectively (the +1 takes into account the fact that substitutions are partial functions, and thus may associate no value to some variables). This number is obviously much higher than the number of toccs in most programs2 , but it is actually irrelevant. In practice, the analysis will stop when there is no new tocc to explore in each branch. What Theorem no_rec_capt_nf shows is that this will happen at some point.

Remark 10.20. Some alternative strategies can be used to show the termination of complex functions without the use of fuel -which happens to pollute the extracted code -, such as the Braga method [Larchey-Wendling and [START_REF] Larchey-Wendling | Simulating Induction-Recursion for Partial Algorithms[END_REF]. However, the goal of our work was simply to validate an optimization scheme before implementing it in a Python-based project (see Chapter 6), rather than extract it in the form of OCaml code. Due to this point, and timing issues, we did not consider these more complex methods.

Value and substitution extraction

We showed in Section 10.3.4 how we implement the extraction of substitutions from a given analysis. The certification of this code is in three steps. First, we provide an alternative definition of the extraction, shown in Figure 10.32. This definition, although de facto computable, is rather used as a specification. Remark 10.21. The reader may notice a discrepancy between the analysis and the rewriting, as the former fundamentally provides values for only one variable, and the latter assumes a set of substitutions, i.e. potentially instantiates multiple variables at once. The extraction of values from an analysis does return a set of substitutions (see function extract_vals_sub in Figure 10.27), but they map only one variable to an actual value, meaning that this is an artificial step to fit into the formalism used by the partial program instance of Chapter 9.

In practice, to instantiate multiple variables in a single rule using the previous definitions, one can either apply the analysis and rewriting multiple times with a substitution on a single variable, or generate substitutions using a cross product of the different value sets. These two methods produce the same programs, as well as the same (still) inefficient results. Section 12.2 adresses this performance issue, by proposing and discussing an improved version of the static analysis from which one can actually extract actual, multi-variable substitutions.

Chapter 11

Predicate specialization The second program transformation we introduce aims at the reduction of the sizes of the tables used in a Datalog engine. To do so, it introduces new predicates to partition existing relations into smaller ones. Section 11.1 first presents the general idea of the transformation, then Section 11.2 formalizes and justifies it. Finally, Section 11.3 presents the implementation of the rewriting in Coq.

Intuition

The transformation assumes an intensional predicate p such that one of its arguments is always a constant in the rules defining it. This is a very simple and syntactic criteria to determine a subset -technically an overapproximation -of the constants that can be found during the execution of the program in deduced facts about p at the corresponding predicate and index.

Example 11.1. Figure 11.1 shows a program fragment. The first two rules define a predicate p of arity 3, and the third and fourth rules use it. Assuming the full program contains no other rule defining p, and considering the separation between extensional and intensional predicates (see Section 2.1.2), we can be sure that the first argument of any deduced p fact will be 1 or 2.

p(1, Y, Z) ← q(Y, Z). p(2, Y, Z) ← r(Z, Z, Y ). t(X) ← p(1, X, X). u(X)
← p(X, X, X). Assuming such a setting, the predicate with a clearly bounded argument can be replaced by a set of specialized versions, one for each identified relevant value. We introduce two new predicates, called p 1 and p 2 , of arity 2. These predicates are meant to be specialized version of p, where the first argument does not explicitely appear but is implicitely considered to be 1 or 2. The rules of Figure 11.1 can then be replaced by those of Figure 11.2. Remark 11.2. As previously stated, looking for predicates with arguments that are statically defined is a very simple, if not simplistic, way to determine a set of values that overapproximates the behavior of a predicate's argument. It might even feel suspicious to rely on this shallow method when the previous chapter introduced a static analysis that computes a similar information in a less trivial way.

p 1 (Y, Z) ← q(Y, Z). p 2 (Y, Z) ← r(Z, Z, Y ). t(X) ← p 1 (X, X). y(X) ← p(X, X, X)
As discussed in Section 12.1, Octant uses the predicate specialization on top of the partial program instance of Chapter 9, which relies on the static analysis of Chapter 10 to produce predicates with arguments that are statically defined. In other words, in practice, the predicate specialization leverages the static analysis via the partial instance.

Formalization and justification

The building block of this program transformation is the following definition.

Definition 11.3. (spec -atom specialization) Given a predicate symbol p, an index i, and an atom a, the spec atom specialization function is defined as

spec(a, p, i) =            p c (t 1 , ..., t i-1 , t i+1 , ..., t arity(p) )
if a is of the form p(t 1 , ..., t i-1 , c, t i+1 , ..., t arity(p) ) with c a constant a if a is of the form q(t 1 , ..., t i-1 , t i , t i+1 , ..., t arity(q) ) with t i a variable or q = p Notation 11.4. In the rest of this section, we assume a Datalog program P where the i th argument of an intensional predicate p of arity m is always defined and no predicate of the form p c appears. The spec function is lifted to clauses (heads and bodies) and programs, and the transformed program will be denoted as spec(P, p, i). Although it is not formally implied by the definition of spec, for clarity purposes (i.e. reduce notations), we will consider that the rules such as those of Figure 11.3 are packaged in spec(P, p, i).

To certify the transformation, we first need to address the formulation of the targeted adequacy results. Theorem 11.5 shows the targeted completeness result. The number of steps used in the transformed program is doubled, to allow the use of rules such as those shown in Figure 11.3.

Theorem 11.5. (Predicate specialization completeness) For any number of steps k, (T P ↑ k)(I) ⊆ (T spec(P,p,i) ↑ 2k)(I).

The transformed program produces specialized facts that did not appear in the original program, which is not a concern for the completeness theorem, as the new facts are on the right side (both literally and figuratively). In comparison, the soundness must be formulated modulo those new facts, as in Theorem 11.6.

Theorem 11.6. (Predicate specialization soundness) For any number of steps k, {x ∈ (T spec(P,p,i) ↑ k)(I) | x is not specialized} ⊆ (T P ↑ k)(I).

Remark 11.7. One might expect that, in Theorem 11.6, the number of steps used in the transformed program might again be doubled compared to the original one, as in Theorem 11.5, but that would not account for the normal, not specialized part of the program.

The strategy is of course to prove these results by induction on the number of steps k. However, given the formulations above, the induction hypotheses would not be strong enough. For example, the completeness theorem does not say anything about the new, specialized facts, which may be required in the execution of the new program. We then first prove the two following alternative lemmas.

Lemma 11.8. (Strong predicate specialization completeness) For any number of steps k, (T P ↑ k)(I) ∪ {spec(a, p, i) | a ∈ (T P ↑ k)(I)} ⊆ (T spec(P,p,i) ↑ 2k)(I).

Remark 11.9. When applied to a ground atom whose predicate symbol is not p, spec(a, p, i) simply returns a. It is then safe to apply the spec function to the entirity of (T P ↑ k)(I) rather than specifically the relevant facts.

Proof. We proceed by induction on the number of steps k. In the base case, the definitions imply that (T P ↑ 0)(I) = (T spec(P,p,i) ↑ 0)(I) = I. The goal then becomes

I ∪ {spec(a, p, i) | a ∈ I} ⊆ I
Since p is assumed to be an intensional predicate, there is no fact about it in the EDB I. We then have {spec(a, p, i) | a ∈ I} = I, which makes the goal for the base case trivial.

In the recursive case, let f be a fact in (T P ↑ k + 1)(I) ∪ {spec(a, p, i) | a ∈ (T P ↑ k + 1)(I)}.

We split the proof in three cases, depending on the predicate symbol of f . This disjunction determines which side of the union f belongs to.

f is a p fact newline The spec function transforms any ground atom about p into a p c fact, meaning that {spec(a, p, i) | a ∈ (T P ↑ k + 1)(I)} does not contain such an atom. This in turn implies that f , in the current hypothesis, has to be in (T P ↑ k + 1)(I). In that setting, f was either already in the previous iteration (T P ↑ k)(I), or it has just been deduced.

f previously deduced newline In this scenario, f is also in (T spec(P,p,i) ↑ 2k)(I) (induction hypothesis), and thus in (T spec(P,p,i) ↑ 2(k + 1))(I) by monotonicity of the T P operator.

f just deduced newline We can extract a clause C from P and a substitution ν such that ν matches C w.r.t. (T P ↑ k)(I).

Let us write f = p(c 1 , ..., c m ), where every c k is a constant (f is a ground atom). Since f is a fact about p, we know that C is headed the same predicate symbol. Then our hypothesis on p implies that its head is of the form p(t 1 , ..., t i-1 , c i , t i+1 , ..., t m ). In that setting, f is deduced at step 2(k + 1) of the transformed program using the added rule

p(X 1 , ..., X i-1 , c i , X i+1 , X m ) ← p c i (X 1 , ..., X i-1 , X i+1 , .
.., X m ). newline with a substitution that maps X k to c k for every k. We then need to show that p c i (c 1 , ..., c i-1 , c i+1 , ..., c m is deduced at step 2k + 1. newline This deduction is performed using rule spec(C, p, i) with substitution ν. The recursion hypothesis ensures that every ground atom in the body of ν(spec(C, p, i)), even those of the form p c ′ (...)1 , is in (T spec(P,p,i) ↑ 2k)(I).

f is a p c fact newline Predicates of the form p c are assumed not to appear in the original program. In this configuration, ground atom f can not be in (T P ↑ k + 1)(I), meaning that it is in {spec(a, p, i) | a ∈ (T P ↑ k + 1)(I)}. Then we can extract a fact g from (T P ↑ k + 1)(I) such that f = spec(g, p, i), which implies that g is of the form p(c 1 , ..., c i-1 , c, c i+1 , ..., c m ).

newline This new fact g was either just deduced or already present. In the second scenario, the recursion hypothesis and the monotony of the fixpoint operator show our goal. Otherwise, it was deduced in the original program P using a clause C ∈ P with a substitution ν.

newline Fact f is then deduced in the transformed program at step 2k + 1 using the clause spec(C, p, i) with the same substitution. The recursion hypothesis ensures that every ground atom in the body of this clause is in (T spec(P,p,i) ↑ 2k)(I). The fact is then preserved from (T spec(P,p,i) ↑ 2k + 1)(I) to (T spec(P,p,i) ↑ 2(k + 1))(I).

We can extract a fact g ∈ (T P ↑ k)(I), st p c i (c 1 , ...c i-1 , c i+1 , ..., c m ) = spec(g, p, i), which means that g = p(c 1 , ...c i-1 , c i , c i+1 , ..., c m ) = f ∈ (T P ↑ k)(I). By monotonicity of T P , f is preserved in (T P ↑ k + 1)(I).

f is a p c fact newline Any deduction in the transformed program of a fact whose predicate is of the form p c is done via a rule of the form spec(C, p, i), with C a rule of the original program P , a substitution ν, and every ground atom in the body of ν(spec(C, p, i)) is in the interpretation (T spec(P,p,i) ↑ k)(I).

Consider C and ν in the original program. We want to show that every ground atom in the body of ν(C) is in (T P ↑ k)(I). Since we are considering a clause of the original program, none of these atoms carries a predicate symbol of the form p c .

Given such an atom a ∈ body(C), if it is defined using a predicate symbol that is not p, or its i th argument is a variable rather than a constant, then spec(a, p, i) = a.

Since body(ν(spec(C, p, i)) ∈ (T spec(P,p,i) ↑ k)(I), so is ν(a). Using the induction hypothesis, we then obtain that ν(a) ∈ (T P ↑ k)(I).

On the other hand, if a is of the form p(t 1 , ..., t i-1 , c, t i+1 , ..., t m ) and ν(t j ) = c j for every j between 1 and m, then f is a fact about another predicate symbol newline In this case, f is deduced in the transformed program using a rule spec(C, p, i), whose head did not change after the application of the spec function, and substitution ν.

We use in the original program the C rule and the same substitution. The reasoning regarding the availability of the body of ν(C) is the same as in the previous case, but the deduced fact is directly f and in (T P ↑ k + 1)(I).

Theorem 11.6 is easily proved using the previous lemma. We can now move on to the formal definition and proof of these results.

Coq implementation

The idea behind this optimization is rather simple, and although the adequacy proofs require the consideration of many technical cases, they do not contain any fundamental difficulty. In If the atom is an occurrence of f and has a constant c as its j th argument, we substitute f by f c and remove the incriminated argument.

As explained in Section 3.2.1, DatalogCert defines raw and ground atom types, and builds the actual atom and ground atom types on top of them, by adding the well-foundeness condition, i.e. ensuring that the number of arguments in the atom matches the arity of the involved predicate. Figure 11.8 shows the additional steps required to lift the specialization from raw atoms to actual atoms and clauses.

Lemma wf_clone (j : 'I_(arity f)) (a : atom) : wf_atom (raw_atom_clone j a).

Definition atom_clone (j : 'I_(arity f)) (a : atom) : atom := Atom (wf_clone j a).

(* wmap is map for Wlist *) Definition tail_clone (j : 'I_(arity f)) (tl : tail) : tail := wmap (atom_clone j) tl.

Definition cl_clone (j : 'I_(arity f)) (cl : clause) : clause := match cl with Clause h tl => Clause (atom_clone j h) (tail_clone j tl) end. The program is not the only place where atoms have to be specialized, as we will need to reason on the semantics of the transformed program, and thus specialize ground atoms. For the sake of exhaustivity, Figure 11.9 shows the simple adaptation of the previously seen functions to this type.

Definition raw_gatom_clone (j : 'I_(arity f)) (a : raw_gatom) : raw_gatom := match a with | RawGAtom pr args => if (pr == f) then match nth_error args j with | Some c => RawGAtom (pclone c) (sremove args j) | None => RawGAtom pr args end else RawGAtom pr args end.

Lemma wf_gclone (j : 'I_(arity f)) (ga : gatom) :

wf_gatom (raw_gatom_clone j ga).

Definition gatom_clone (j : 'I_(arity f)) (ga : gatom) : gatom := GAtom (wf_gclone j ga). As explained in Example 11.1, on top of the specialization of relevant occurrences of f , we need to add rules that relate them to the original definition. As shown by the next few pages, formalizing and using these rules turned out to be more of a challenge than one might have expected.

Writing new generic rules

We add one f ← f c rule for each constant c used -at the ind th index -in the definition of f . We then first need to collect the values used to define the ind th term of f . This is done using the functions shown in Figure 11.10. Function ind_terms filters the clauses that are headed by f (hsym_cl is defined in Figure 7.30) and fetches the ind th arguments of the head within an option type using nth_error.

Since ind is defined as being at most the arity of f , this will only return Some elements in practice, meaning that we can safely extract the terms using pmap. Then, ind_vals performs another fictional job for typing purposes, i.e. filtering out the variables which do not happen in practice thanks to the hypothesis on the definition of f .

We now have to manually add the rules to the produced program. As explained in Section 3.2, the atoms carry a proof that their number of arguments is the arity of the associated predicate, and the variables are encoded as ordinals, meaning that we need to deal with a lot of dependent types. This is illustrated by Figure 11.11, which shows how we define the sequence of variables that will serve as a building block of the added rules.

Once again, we generalize ind to allow usable inductions in the proofs. Also note that we must ensure that n is at least the arity of f , i.e. that there are enough available variables (see Section 3. We can now discuss the certification of this transformation. The biggest challenge lied in the use of the generic rules we just defined, so let us first focus on this point.

Using the generic rules

The point of DatalogCert is the verification of a Datalog engine, i.e. ensuring that iterating an implementation of the T P operator on any given program will compute the expected semantic.

In that sense, the matching mechanism is verified, not used (see Section 3.3.2.1)). However, to show that the predicate specialization preserves the semantics, we need to manually trigger the f ← f c rules defined in Figure 11.15, i.e. explicitely provide the substitutions they are instantiated with. These substitutions are computed by a function, shown in Figure 11.16, that takes a list of variables and a list of terms, and creates a substitution that maps each variable to the value at the corresponding index. In practice, this function is used with deduced specialized facts.

The main difficulty in the certification of this method is that a shift in the list of variables fully changes the extracted substitution, meaning that lemmas on matching using this function could not be proved using straightforward inductions. The multiple technical lemmas then had to be proved using an abstraction of the list of variables.

Figure 11.17 shows some of the main lemmas, and the hypotheses encoding the [X 1 , ..., X k ] list of variables. These lemmas all make use the unicity property, which ensures that extract_sub_seq_c does not overwrite itself. They also enforce that the elements of the provided list list of terms are all variables, to capture the full provided ground atom. Finally, Lemma extract_sub_seq_rem_map uses find to relate a variable and constant. Due to the heavy use of dependent types, the actual proofs of these lemmas are rather technical, and their statements remain quite circumvoluted. Defining and certifying this part of the rewriting felt like working against DatalogCert rather than with it. It is unclear whether our method -both the computation of substitutions or the abstractions used in its certification -can be made simpler. However, in its current state, these functions and results can be used if other program transformations that add rules are to be introduced and verified in the future. Now that we have the definitions and technical lemmas for all the components of the transformation, we can conclude the adequacy proof.

Combining the pieces

As explained in Section 11.2, the formulation of Theorems 11.5 and 11.6 does not allow a powerful enough induction principle, which led us to introduce Lemmas 11.8 and 11.10. Figures 11.18 Remark 11.12. The soundness one introduces a & sym_gatom ga == f filter that was not present in the paper version above. This condition is theoretically useless, as ground atoms not headed by f are not transformed by gatom_clone and already appear on the left side of the set union :|:. However, it makes easier the use of one of our technical lemmas, hence its addition.

With these new inductions and the previously presented results, we prove the results of The contributions introduced in this thesis are the results of three years that have been filled with surprises, errors and doubts -and, every now and then, a suitable idea. This chapter dwelves a bit into this process, as it tries to convey some questions raised by the work on this thesis and explain the answers we provide.

Octant, the network verification programs it contains and the limitations of the underlying Datalog engine are the starting point of our work, as well as our reference point. It is then fitting that we open this chapter with Section 12.1, which outlines the effects of our optimizations in the context of Octant. Then, Section 12.2 introduces an alternative definition of the extraction process within our static analysis, which leads to more precise but unsafe results for the analysis of multiple variables in a single rule. Then, Section 12.3 discusses various modelization choices, both at the paper and Coq levels. Finally, Section 12.4 tries to quantify the general proof effort, and Section 12.5 discusses related works, focusing on static analyses of logic programs, dealing with recursion in the analysis of programs in general, and the certification of static analyses.

Effects of the rewritings in the context of Octant

As explained in Sections 5.3 and 6.2, the Network Optimized Datalog engine uses a representation called Differences of Cubes, which does not fare well with some primitive predicates. We did not come up with a formal characterization of this class of primitives, but Section 145 6.2.1 should give the intuition that the Differences of Cubes representation does not handle well dependencies across the encodings of multiple variables. Focusing on Example 6.6, one might want to be able to write

⋆ ⋆ ⋆⋆ v 1 ⋆ ⋆ ⋆⋆ v 2 \ {0 ⋆ ⋆⋆ v 1 1 ⋆ ⋆⋆ v 2 , f 1 0 ⋆ ⋆ v 1 f 1 1 ⋆ ⋆ v 2 , f 2 0⋆ v 1 f 2 1⋆ v 2 , f 3 ⋆ 0 v 1 f 2 1⋆ v 2 , }
where f i is a factor of size i, or even

⋆ ⋆ ⋆⋆ v 1 ⋆ ⋆ ⋆⋆ v 2 \ {f p 1f s 1 v 1 f p 0f s 2 v 2 }
where a f is a factor of any size.

The complexity of the computation of these predicates grows exponentially in the number of (bits across) cubes, i.e. variables, in their instances. The goal of our optimizations is then to minimize both the number of variables and sizes of cubes in any given program.

The clause specialization introduced in Chapter 9 reduces the number of variables occuring in primitive predicates, but also specializes the head of rules that, in fine, depend on facts from the EDB. This allows and fosters the use of the predicate specialization introduced in Chapter 11, which reduces the sizes of the cubes used in NoD.

An intuition of the effect of this transformation in our setting, network verification, is that it unrolls the topology and replaces predicates on the global states of all the network elements by local predicates on the state of, for example, a given switch. Then, the state of the ports or the packets received by the other switches will not be considered to compute the output of the switch. This processing emulates the style found in the examples provided with Network Optimized Datalog, such as shown in Section 5.2.1 or found in [Lopes, ].

Example 12.1. Applying the partial instance and the predicate specialization to the program of Figure 6.4 transforms the definition of linked(X, Y, IP) into a set of specialized predicates linked_X_Y(IP) for each pair of linked locations X and Y. These new predicates are then described independently of the rest of the topology.

Remark 12.2. The analysis and optimizations introduced in this thesis have been designed with Network Optimized Datalog and Octant in mind, but their use is not limited to this context. In particular, we emphasize that the Coq formalization and proof of these tools are completely independent of the considered Datalog engine -actually, DatalogCert is a Datalog engine itself.

Towards a stronger static analysis

The analysis introduced in Chapter 10 overapproximates the behavior of a variable. To instantiate multiple variables in a single rule, the best we can do so far is to use a crossproduct of the results of the different analyses (see Remark 10.21). The section sketches out a more precise but unsafe value extraction mechanism for such cases. Section 12.2.1 first makes an observation about the dependencies across value flows in Datalog, and how ignoring them entirely may lead to efficiency issues. We then relate it in Section 12.2.2 to the analysis as it has been formalized. Finally, Section 12.2.3 presents the limitations of this approach.

Minding dependencies across values

In Section 10.2.1, we identified three constraints of the T P operator. The first two, conjunction and disjunction of candidate values, are enforced by the static analysis we introduced. However, some fundamentally different programs can only be distinguished using the third constraint, i.e. the unification of value tuples across atoms.

Example 12.3. Figure 12.1 shows two different definitions of a predicate p. In the program of Figure 12.1a, the number of deduced facts about p will be the same as the number of facts about q, whereas in Figure 12.1b, the former will be (up to) quadratic in the latter. However, the static analysis and extraction mechanism, as introduced in Chapter 10, are not able to distinguish both cases.

p(X, Y ) ← q(X, Y ).

(a) Linear definition of p p(X, Y ) ← q(X, Z 1 ), q(Z 2 , Y ). Concretely, both definitions would be treated as the quadratic one, meaning that a program containing a similar construction could be instantiated using many irrelevant rules, which would hurt performances (Section 12.1 discusses this point in the context of our use case, Octant). Moreover, this dichotomy between dependent or independent values can be harder to determine, as illustrated by Figure 12.2, where the dependency between the values of s is lost at a deeper level than those of q were in Figure 12.1b.

p(X 1 , Y 1 ) ← q(X 1 , Y 1 ). q(X 2 , Y 2 ) ← r(X 2 , Z), r(Z ′ , Y 2 ) r(X 3 , Y 3 ) ← s(X 3 , Y 3 )

Overlapping for better precision

This is where the annotations in the trees produced by the analysis come into play. The idea is to overlap the trees resulting from the analyses of multiple variables in a single clause, using the annotations to exclude incompatible flows, e.g. an atom being instantiated with different rules defining the corresponding predicate.

Example 12.4. In Example 10.9, variables X 1 and Y 1 appear in the same clause. The roots of their respective analyses (Figures 10.4b and 10.4c) have only one descendant, annotated with 0 in both cases. This implies that the two trees represent deductions that go through the same atom, i.e. p in the first rule. Then, we have ∨-nodes, and two descendants, annotated with 0 and 1. The left (resp. right) subtree represents in both cases a use of the first (resp. second) clause.

Mixing the values extracted from the left branch of one of the trees and the right branch of the other tree would amount to consider a program execution where the p atom in the body of the first clause is instantiated with both the first and second clause at the same time. This possibly can then be excluded.

Figure 12.3 shows what the overlapping of the two analyses using annotations would look like. The leaves are now pairs of value origins, as the idea of merging trees is to extract from the EDB tuples of values rather than individual values. The tree itself states that the values for X 1 and Y 1 can be extracted from the q facts of the EDB, either directly (left branch) or after permutation (right branch). More formally, X 1 and Y 1 can be instantiated using the values in { x, y | q(x, y) ∈ EDB ∨ q(y, x) ∈ EDB}, which matches exactly the actual behavior of the program. This example shows how the annotations in the trees produced by the analysis can be used to avoid extracting many irrelevant values, where a quadratic -or worse, depending on the number of instantiated variables in a single rule -number of rules can be circumscribed to a linear process. However, although engaging, this process is not entirely safe.

Limitations

When applied to some programs, this limited extraction will drop relevant value tuples, as illustrated by the following example. If we try to merge them, things do not go as smoothly as before. Indeed, the ∨-node of the first tree, just under the root, has one more child than its equivalent node in the tree of Z 1 . There are two ways to approach this: either drop the branch (indexed by 0) that has no counterpart, or consider it in the merger with a wild card ⊤ indicating an absence of related values. Figure 12.6 illustrates these two options. In Figure 12.6a, we rule out the possibility for a value to flow from q.0 to X 1 , although it is clearly part of the semantics of the analyzed program, meaning that this method loses the completeness of the analysis. On the other hand, Figure 12.6b does not provide any more precision compared to what the naive process of Remark 10.21 would, as the ⊤ wild card should be replaced by the analysis of Z 1 . In other words, this method, in this case, would only provide a circumvoluted unfolding of the normal one. Moreover, even though it is harmless in this example, there is currently no evidence that it would retain completeness when used on any program. This has not been explored yet, as we chose to focus on the branch-dropping path. Indeed, the program of Figure 12.4 is highly artificial, and actual Datalog programs, in our experience, do not contain this kind of mechanism. They rather tend to simply carry around full or partial value tuples without such local, partial permutations. In particular, this is exactly what Octant does, as illustrated by Figure 6.4.

p(X 1 , Y 1 , Z 1 ) ← p(Y 1 , X 1 , Z 1 ). p(X 2 , Y 2 , Z 2 ) ← q(X 2 , Y 2 , Z 2 ).
∧ ∨ ∧ q.1 0 ∧ ∨ ∧ q.0 0 1 0 0 1 0 (a) Analysis of X1 ∧ ∨ ∧ q.0 0 ∧ ∨ ∧ q.1 0 1 0 0 1 0 0 (b) Analysis of Y1 ∧ ∨ ∧ q.2 0
Moreover, a preliminary implementation and certification of the analysis partially validated this form of the extraction. We indeed first designed an analysis, introduced in Section 12.3.4, that only considered non-recursive programs. In that setting, the no-recursion trace introduced in Section 10.4 was unnecessary, as we could prove that any trace directly matched a subtree of any analysis, in the sense that it could be properly overlapped. This way, the completeness results on traces could be imported in the certification of the mix of analyses.

When analyzing multiple variables in a given clause, the returned trees then all contained a subtree that fully matched the same trace. We were able to prove that there is no inconsistency between trees that match the same trace, thus ensuring that excluding incompatible branches did not break the completeness property, which provides a certification of this process in the context of non-recursive programs.

Recursion happens to be a core feature of Datalog, meaning that this result is encouraging but not satisfactory. However, we expect that there is a class of Datalog programs, strictly larger than non-recursive ones, where recursion is only used in a way that allows this more precise version of our analysis.

Our first intuition is that a program is homogeneously recursive if, for any given rule, all argument cycles have the same length. The idea behind this notion, which we do not define formally, is to see a rule as a graph whose nodes are the different atom arguments, and the edges relate head variables to their body occurrences, as well as the body arguments to the variables at the same index in the head.

Example 12.6. Figure 12.7 shows the argument cycle of variable X 1 in the head of the first rule of Figure 12.4. There is one occurrence of X 1 in the body, so we add an edge (→) between these two occurrences.

p(X 1 , Y 1 , Z 1 ) ← p( Y 1 , X 1 , Z 1 ) Figure 12.7: Argument cycle of X 1
This body occurrence is the second argument of the p atom, so we add an edge (←) between the X 1 in the body and the Y 1 in the head. This head occurrence then leads us to the Y 1 in the body (→), which gets us back to the X 1 in the head (←). The result is a cycle of length 4. The same figure also shows that the cycle of Y 1 is of size 4 as well.

On the other hand, the argument cycle of Z 1 is of length 2, as shown by Figure 12.8. Our intuition is that the discrepency between the sizes of these argument cycles characterizes -

p(X 1 , Y 1 , Y 1 ) ← p(Y 1 , X 1 , X 1 ). p(X 2 , Y 2 , Z 2 ) ← q(X 2 , Y 2 , Z 2 ).
Figure 12.9: Shifting variables Figure 12.10 shows the argument cycles of the rule. We start with X 1 . Since it has two occurrences in the body, we add two transitions (→) from the head occurrence of X 1 . The other way around, i.e. from body to head, there can never be two edges from the same node, since these edges map a variable to the corresponding index in the head.

p(X 1 , Y 1 , Y 1 ) ← p Y 1 , X 1 , X 1 )
Figure 12.10: Argument cycles with multiple occurrences of a variable

We are left with two cycles of length 4. And, true to our intuition, the mix of the analyses of variables X 1 and Y 1 -which can be found in Figure 10.6 -works well. The result is shown in Figure 12.11, with the occurrences replaced by the corresponding predicate and index for clarity. These examples strengthen our intuition that our alternative, more precise extraction mechanism can be safely used on Datalog programs where all argument cycles within each rule are equal. However, this remains to be formally verified, in a proof assistant of course, but also on paper, as we do not even provide a core argument.

∩ ∪ f.2, f.0 ∧ ∪ f.0, f.2 1 ∪ f.0, f.1 1 0 0 0 1 ∪ f.1, f.0 ∧ ∪ f.0, f.2 1 ∪ f.0, f

Modelization choices

Many modelization choices were made in the course of this work, both at the design and certification levels. This section discusses some of these choices, which have been more complex, surprising or critical than one could expect.

Implementation of pset

Figure 8.7 introduces the specification of a function called pset, which takes a set over an option type, e.g. option A, and filters out all the None elements to return a set over A. We provide two implementations of this pset, both of which leave us unsatisfied.

Figure 12.12 introduces a definition that roughly translates the given set into a list, which allows the use of the pmap function found in MathComp1 , which is very simply defined by induction on the list argument -a technique that, to the best of our knowledge, can not be used in the context of sets. The result in then translated back into a set. We reckon that this implementation is rather inelegant, and somewhat reminiscent of what can be seen in some imperative programming languages. We introduce another formulation, which does not rely on another function. The idea is simply to go through every element of the provided set and only keep the inner elements of Some objects.

However, this approach requires us to name these inner elements. The top of Figure 12.13 shows the notation we wish we could use, which is not handled by Coq and MathComp. The bottom of the same figure displays the actual definition we use, where the inner element is universally quantified in the set definition. The introduction of this quantification feels like a cheap trick, which moreover may impact the extraction of the function very inefficient in practice if the process is too naive. we do not know enough about the inner workings of MathComp to comment further on this precise point, but wanted to mention how surprised we were by the discrepency between the straightforward definition of pmap, and the fact that we could not find a simple and satisfying adaptation to the set type.

Introducing new (specialized) predicates

Chapter 11 introduces an optimization that rewrites a program where the ind th argument of an intensional predicate f is always defined. The rewriting introduces new specialized predicates f c for each constant c that appears at the ind th index of the occurrences of f as the head of a rule.

To use these new specialized predicates in Coq, we assume a function of type constant → symtype (see Figure 11.5) and some associated hypotheses (Figure 11.6). Behind this approach, there are two Coq modelization choices we would like to discuss.

12.3.2.1 Where are the new symbols?

First, this method comes with an implicit, which is the presence a priori of those specialized predicates in the type of predicate symbols, symtype. DatalogCert [START_REF] Benzaken | Data-logCert[END_REF] defines symtype as a finType, and justifies it by recalling that the underlying model of a Datalog program is finite (see Section 8.1.1 of [Dumbrava, 2016]). More concretely, symtype is seen as a component of the program signature (Section 8.2.1 of [Dumbrava, 2016]), meaning that it corresponds to the set of predicates which appear in a given program.

In that setting, assuming that the set of f c predicate symbols, which do not appear in the original program (see Figure 11.6), is in symtype is slightly contradictory. A more natural, less tricky way to introduce those new predicates would have been to define a new finType, e.g. spec_preds, and define the signature of the new program as the union of symtype and spec_preds.

However, the atoms and ground atoms are defined strictly using symtype, meaning that this approach would have required to rework a lot of definitions. It may have been manageable if DatalogCert had split the predicate symbols into a type for the symbols themselves, and a set of such objects in the program signatures. This would nonetheless raise another problem, which is the definition of the type for predicate symbols not with respect to a given program.

In that setting, it seems that there is no way or criteria to bound these symbols and obtain the finType required to define a finset over it.

In conclusion, assuming that the specialized predicate symbols are in symtype is not fully satisfactory, but it would seem that there is no simple and cleaner alternative to circumvent an issue that is specific to the Coq formalization.

12.3.2.2 When genericity leads to troubles (bis)

The new predicates are introduced via the pclone function (see Figure 11.5), whose only argument is a constant, because it is implicitely applied to function f at index ind. It might have seemed more natural and general to have a specialization function that can be applied to any predicate symbol and argument index, but this may have led to a paradox in the hypotheses, or a least some needless troubles.

As explained just above, in Section 12.3.2.1, the new specialized predicates are packaged with the original ones in symtype. Having a more general version of pclone would then have meant that it could have been applied recursively to the new predicates. This would have not been compatible with the hypotheses we introduce (see Figure 11.6). In particular, hypothesis parity (on the arity of these new predicates) implies that a predicate that has been specialized until it reaches the absence of arguments should stay at this level when the specialization is applied again.

However, we also need the specialization to be injective (cf. hypothesis pinj in Figure 11.6), meaning that we could not simply map an empty predicate P () to itself. A workaround would be to create a hierarchy of predicates P () → P 1 () → P 2 ()..., but that would contradict the finiteness of symtype. A more valid but less natural fix would be to have a circle of benign specializations, such as P () → Q() → P (). This method illustrates how, in the setting of a generalized pclone, paradoxes may be avoided, but still lead to convoluted, abstruse problems. On the other hand, limiting the use of the specialization to a given predicate (f ) at a given index (ind) makes these very questions irrelevant.

This is in constrast with our general experience with Coq, where introducing genericity may lead to much more flexibility and convenience, in particular for the use of inductions. Such an example is also found in the definition and certification of the predicate specialization.

See for example the Coq snippets of Section 11.3.3), where the j argument is fundamentally a stand-in for ind that allows proofs via induction.

Bound of the traces

The proof introduced by Remark 8.14 basically states that having a repetition of a clause and substitution couple in a trace would amount to proving a fact x as an intermediate step in the proof of x, which we can do without. However, different couples of clause and substitution can represent the same (head) fact, meaning that even with the WUtree type, a same fact can be deduced multiple times in a single trace.

Although this bounded form of repetition is harmless, as it is already finite and functional, it may feel a bit awkward and unsatisfying. We then considered the possibility to define a type of trees with unicity across branches modulo a function (here, λC.λι.head(ι(C))). However, we dropped this idea due to its lack of actual benefit, but would like to highlight that it would change (and lower even further) the surprising bound discussed in Section 10.4.4.

Implementation of the static analysis

Section 6 of [START_REF] Tristan | Formal verification of translation validators: a case study on instruction scheduling optimizations[END_REF] states that "generally speaking, there are two ways to specify an algorithm in Coq: either as inductive predicates using inference rules, or as computable functions defined by recursion and pattern-matching over tree-shaped data structures". Even if Datalog programs are not naturally seen as trees, both from syntax2 and semantics standpoints, the version of the static analysis introduced in Chapter 10, is a computable function.

However, this development is the result of many reworks and adjustements, and used to be defined using an inductive. We outline this preliminary version, and explain why we departed from it.

Original paper definition

In our first approach, the analysis was defined on paper in the form of a typing system, and already returned trees encoded by Disjuncted Normal Forms. The types are built using the following rules, which preserve the DNF.

• ⊤ is a wildcard type, which provides no information on the analyzed variable or predicate argument. It was introduced in this version to modulate the thoroughness of the analysis.

• tInit def = {{[::]}}, the base type for extensional predicates.

• tInsert : tocc → T ypes → T ypes adds a tocc at the top of each path under the root.

Given tocc a and Disj d, it returns

{{a :: b | b ∈ c} | c ∈ d}
If the input type is ⊤, then ⊤ is also returned.

• tDisj : T ypes → T ypes → T ypes is simply set union, corresponding to the aggregation of possible sources of values. If one of the input types is ⊤, then ⊤ is returned as well.

• tConj : T ypes → T ypes → T ypes. Given input Disjs d 1 and d 2 , it returns

{x ∪ y | x ∈ d 1 ∧ y ∈ d 2 }
This operation is equivalent to putting d 1 and d 2 under a Conj, while preserving the DNF. If one of the input types is ⊤, the other type is returned.

These constructs are then used by the typing rules shown in Figure 12.14, where P is the analyzed program and the list of toccs already visited (prev in Figure 10.18) is formalized as the typing context Γ.

p is an extensional predicate

Γ ⊢ p.i : tInit predb newline ∀(C : p( -→ v ) ← • • • ) ∈ P, Γ ⊢ v i : τ C aa p intensional Γ ⊢ p.i : tDisj (C:p( -→ v )←••• )∈P τ C ∅ predr newline ∀ x, y, z ∈ occs(v)\Γ, Γ ∪ { x, y, z } ⊢ (p at( x, y, z )).z : τ x,y,z Γ ⊢ v : tConj x,y,z ∈occs(v)\Γ
(tInsert x, y, z τ x,y,z ) ⊤ var We do not detail these rules, but they should be reminiscent of the algorithm introduced in Section 10.2, although presented in a circumvoluted (and somewhat inelegant) manner.

Original Coq implementation

Figure 12.15 shows the Coq formalization of these typing rules. They are encoded as four mutually-defined inductives, which are supposed to emulate a loop-based implementation.

We do not dwelve into this code, or introduce every definition it relies on, as a simple look at it should be enough for a comparison with the implementation shown in Chapter 10.

From Inductive to sets

We have been working on and with this version of the static analysis for approximately a year and a half. During this interval, there were some technical difficulties, mainly the fact that the induction principle required four manually-defined invariants and produced many, sometimes abstruse proof obligations. Moreover, this presentation did not allow us to reason about the termination of the analysis, which we felt was really missing. As a corollary, there was also something awkward about defining what is supposed to be a deterministic function as an inductive predicate.

A posteriori, we reckon that these elements would and should have been reason enough for us to erase the inductive definition of the analysis and start working on another version. However, the reason that changed our mind was realizing that the inductive definition is actually flawed: whenever it is used on a recursive program, it returns the trivial type ⊤.

This bug comes from the use of ⊤ as the "base case" of the fold of tConj in rule var, when occs(v)\Γ is empty, which infects the whole return type through tDisj.

The point of certification is of course to avoid this kind of subtle and technical but critical error. We had written and proved a completeness lemma, roughly stated as "if the analysis returns an actual result (or type), it does capture an overapproximation of the behavior of the analyzed variable". We were sure -and wrongfully so -that the analysis would always return a non-trivial result, so we thought we could safely add the hypothesis on the returned type to help with the technicalities of the proof, which in fact were legit safeguards.

We eventually understood that something was wrong thanks to our work on the smarter extraction mechanism introduced in Section 12.2. Concretely, after formalizing it in Coq, we could certify it with a lemma roughly stating that "any trace directly matched a subtree of any analysis, in the sense that it could be properly overlapped. This way, the completeness results on traces could be imported in the certification of the mix of analyses" (cf. Section 12.2.3).

Both the traces and analyses are trees bounded in height, but by different values (number of rules times the cardinal of the substitution type for the traces, number of toccs for the analyses). In particular, the bound for the analysis is tighter, meaning that our lemma probably contained a contradiction.

We investigated the problem by reflecting on the proofs, and noticed that Datalog recursion was actually not dealt with in the certification of the inductive version of the static analysis.

One of the reasons we had not realized that when actually writing the proofs is quantity and very technical -sometimes obscure -nature of the proof obligations generated by the four mutually-defined inductives. In comparison, working with the set-based version was much clearer and allowed easier high-level reasoning.

We eventually switched to the set-based definition seen in Figure 10.18. Since we had no experience with MathComp prior to this project, and could not even find a satisfactory paper definition of the analysis, we first hoped little of the Coq version. MathComp fin-Types and set notations had already been noted particularly relevant to formalize Datalog [Benzaken et al., 2017a], and it was as well for our static analysis. Although the definition is not completely straightforward, the intricacy seems inherent to the analysis rather than a consequence of the formalization itself.

The authors of [START_REF] Tristan | Formal verification of translation validators: a case study on instruction scheduling optimizations[END_REF] also recall that defining a function, such as our analysis, in a computational way rather than as an inductive also allows its extraction as an Ocaml program (which we have not experimented with this development yet). Alternatively, numerous non-functional programming languages (e.g. Python) now support set notations. Another advantage of this version of the analysis is then its simplified translation in many languages, which reduces the gap between formalization and implementation, making the latter more trustworthy.

In summary, even in the context of machine-aided verification, the mix of an error in a minor definition -which would have been spotted with correct lemmas -and lacking formulations of completeness properties -which would have been benign with correct definitions -could lead to a broken result. Computational definitions, higher-level tools (both provided by Math-Comp in our case), not adding "apparently free hypotheses which help with the technicalities of proofs", and a more introspective view should help avoid this kind of situation.

General proof effort

Although the use of finite types and set notations was eventually most beneficial to us, our proof style remained more classical. This is in contrast to [START_REF] Benzaken | Data-logCert[END_REF], which uses the tactic language SSReflect extensively. Combined with the heavy use of dependent types to obtain finite types, it resulted in a development that was probably longer than what could be expected, i.e. approx. 7000 lines of code. In comparison, the positive Datalog engine within DatalogCert we use consists of approx. 1500 lines of code.

Our development can be found at https://orange-opensource.github.io/octant-proof/, and its 7000 lines of code are roughly split as follows:

• 1000 loc. for a general-purporse library used throughout our development (file utils.v)

• 400 and 1200 loc. for the finite sequence and tree types introduced in Chapter 7 (files finseqs.v and fintrees.v)

• 1000 loc. for the design and certification of the Datalog trace semantics introduced in Chapter 8

• 300 loc. for the definition and certified collection of variable occurrences, as presented in Section 10.3.2 (file occurrences.v)

• 300 loc. for the design and certification of the partial program instance introduced in Chapter 9 (file rinstance.v)

• 450 loc. for the no-recursion traces introduced in Section 10.2.2 (file norec_sem.v)

• 450 loc. for the design and high-level certification (many technical results are found in other files) of the static analysis introduced in Chapter 10 (file static.v)

• 200 loc. for the extraction of values from a static analysis (file extract_static.v)

• 1500 loc. for the design and certification of the predicate specialization introduced in Chapter 11 (file projection.v)

• 300 loc. for a preliminary implementation of the alternative value extraction mechanism introduced in Section 12.2 (file dep.v)

Finally, some additions to DatalogCert have been made in situ, and are flagged with --added.

Related works

The work presented in this thesis is at the interface of various domains, which can roughly be abstracted as network verification, logic programming, and (certified) program analysis and transformation. The network verification component is fundamentally a background that explains how and in what context we started this work, so we introduced it earlier, in Part III. On the other hand, this section discusses research works that are related to the concrete questions we addressed and the answers we provided.

Static analysis for logic programs

The vast majority of static analyses for logic programming languages are developed for Prolog. For example, [START_REF] Jacobs | [END_REF]Langen, 1992, Marriott et al., 1994] introduces a general abstract framework for the static analysis of Prolog programs, and provides an example that focuses on the groundness and sharing of variables across terms to set up parallelism in the program's execution.

Prolog was extended into λProlog [START_REF] Nadathur | An overview of lambdaprolog[END_REF], which contains a typing system. As a side note, the static analysis we introduce could very easily be leveraged to determine the type of variables in a Datalog program, as it relates those variables to values in the EDB which are available at compile time, although it probably would not be the most efficient typing method.

Such typing systems for Datalog can be found in [START_REF] Zook | Typed datalog[END_REF] and [de Moor et al., 2008]. The latter also introduces two type-based optimizations: type erasure, which removes dynamic type tests once a program has been shown to be type-safe, and type specialization, which specializes predicate definitions to the type contexts in which they are called, e.g. removing clauses which are shown to contain type inconsistencies.

There has been some work [Chaudhuri, 1993, Chaudhuri and[START_REF] Chaudhuri | [END_REF]] that aims at approximating potentially recursive predicates via a set of nonrecursive, simpler rules. However, this line of research has not produced many usable results, as many classes of predicates, such as transitivity closure, are not approximated in a satisfying manner by these methods [Duschka, 1998].

The domain of a program can be leveraged by static analyses and transformations. For example, [START_REF] Campagna | Approximating constraint propagation in Datalog[END_REF] introduces a source-to-source transformation of Datalog programs handling arithmetics, that relies on propagation techniques from constraint programming.

The work introduced in [Miller, 2006, Miller, 2008] views Horn clauses as instances of a higherorder logic (e.g. Simple Theory of Types), which makes it possible to see the execution of programs as cut-free sequent calculus proofs, which in turn allows the presentation of these executions through the lens of linear logic. λProlog is then used to implement a successful prototype tool that approximates the content of multiple data-structures (lists, sets...) within logic programs.

The authors of [START_REF] Mesnard | Applying static analysis techniques for inferring termination conditions of logic programs[END_REF] introduce a static analysis that tries to infer the termination of Prolog programs, using an abstraction of the terms by their height and the computation of numerical relations between the values in rules. The vigilant reader may notice that Definition 3 of this paper, which describes the cornerstone of their analysis, is reminiscent of the one we introduce in Figure 8.9. In a sense, we both try to capture the variable-level logical structure of the analyzed program, although with a different approach and problem in mind (Datalog programs do not contain terms and terminate by construction).

The closest work to our own is probably found in [START_REF] Halevy | Static analysis in Datalog extensions[END_REF]. This paper introduces a static analysis of Datalog programs that represents as a (set of) tree(s) not only the dataflow of a variable, but the whole program. Another similarity is that they also limit the recursion of Datalog in their analysis to be able to encode in a finite manner an infinite (or rather sufficient, see Remark 8.14 of the present document) number of deductions, although with different criteria and implementation: they do not allow to have in a branch two rules whose heads have the same predicate and variable pattern.

This may sligthly echo the approach we propose in Remark 12.3.3, although their formalism does not account for the substitutions (or as an abstract and simplified form they call labels). This is due to the fact that their analyses do not try to convey the same information as ours, as they aim less at the actual behavior of the analyzed program than the decidability of properties such as satisfiability (existence of an EDB such that at least one fact about a predicate is decidable) or equivalence, for which they provide instances of their general method and a characterization of the relevant class of Datalog programs.

In the same spirit, the authors of [START_REF] Caballero | A new proposal for debugging datalog programs[END_REF] introduce a debugging method for Datalog programs that focuses on semantics rather than actual computation mechanisms. Concretely, they introduce a structure called computation graphs, that finitely represents relations between predicates and is used to investigate discrepencies between the expected and actual semantics of a given program.

They oppose their approach to computation trees, which have for example been used for Prolog (SLD-trees, cf. [Lloyd, 1987a]) or Java [START_REF] Caballero | Algorithmic debugging of java programs[END_REF], claiming that tree-like structures are not fit to handle the always-terminating recursion of Datalog. We did introduce a trace semantics for Datalog in Chapter 8, but its aim was only to be able to reason about full deductions at the proof level, and not be used as an actual debugging tool. We however believe that our trace semantics can be used as a simple, off-the-shelf tool to investigate the deduction of an unexpected ground atom. On the other hand, unlike [START_REF] Caballero | A new proposal for debugging datalog programs[END_REF], it would not help with the absence of an expected fact.

Soufflé [START_REF] Scholz | On fast large-scale program analysis in Datalog[END_REF] is a static analysis tool using Datalog as a specification language.

It performs Datalog-level optimizations, such as magic sets (specialization of a program w.r.t. a query [START_REF] Balbin | Efficient bottom-up computation of queries on stratified databases[END_REF]) and user-directed rule inlinings. The Datalog code is translated into a relational algebra via the Futamura projection, where the interpreter is the semi-naive evaluation [START_REF] Abiteboul | [END_REF]. Although a form of specialization, this transformationnot discussed or illustrated -does not seem to produce an explicit analysis of the program value flows.

Apart from the aforementioned papers, there has not been much research in the field of static analysis for Datalog. Ironically, it has recently been used as a framework to build static analysis tools, such as Soufflé, but also [START_REF] Whaley | Using datalog with binary decision diagrams for program analysis[END_REF], Arntzenius and Krishnaswami, 2016, Madsen et al., 2016]. The rationale is that Datalog, as a language, is close to the logic usually underlying the specification of a static analysis [Greenman, 2017].

Dealing with recursion for static analyses

The main difficulty we faced in this work is the development and certification of a finite representation for an infinite set of behaviors. This challenge is found in the development of the analysis (Section 10.2.2), the implementation of our trace semantics (see Remark 8.14), and the proof of their connection (Section 10.4.1). In all these cases, the name of the game is to truncate our representations enough to make them finite, while retaining the required information.

This general process is reminiscent of earlier works in the field of verification, such as [Shivers, 1991], which introduces the k-CFA family of static analyses. This work tackles the problem of dynamic dispatch for the design of static analyses for higher-order, functional languages, i.e. the fact that the flow of values has an impact on the call-target resolution procedure [START_REF] Might | Resolving and exploiting the k-cfa paradox: illuminating functional vs. object-oriented program analysis[END_REF]. The k integer parameter of k-CFA, called context-sensitivity, is the number of previously visited call sites which are considered. This truncated notion of trace ensures finiteness. Note that this work linearly abstracts the traces, in the sense that the last k steps are all considered and the rest is left out, whereas we fundamentally ensure finiteness by allowing the analysis or trace semantics to drop multiple, not necessarliy contiguous set of steps.

As another example, [START_REF] Jeannet | Abstracting call-stacks for interprocedural verification of imperative programs[END_REF], introduces an abstract semantics for imperative programs that roughly abstracts the sequence of intermediate contexts (i.e. values associated to global variables) in a trace by any such sequence that would have led to the final context. Note that, in this case, the semantics is not truncated into a technically finite representation, but still a more manageable one.

Certified static analyses

David Pichardie and his colleagues work on the development and certification of static analyses within Coq [START_REF] Cachera | Extracting a data flow analyser in constructive logic[END_REF], Besson et al., 2006], which are then extracted using Letouzey's method [Letouzey, 2008]. The main application area of this work is the analysis of Java [START_REF] Barthe | A certified lightweight non-interference Java bytecode verifier[END_REF] but is also notably used in the development of CompCert [Leroy, 2009, Barthe et al., 2017].

Chapter 3 presents a Coq formalization of Datalog, called DatalogCert. It is part of a lager project, called DataCert 3 , which aims at building a fully and deeply verified environment for data intensive management tools, the same way CompCert [Leroy, 2009] and CakeML [START_REF] Kumar | Cakeml: a verified implementation of ml[END_REF] provide verified realistic C and ML compilers. As a side note, Kriener et al. used Coq in [Kriener et al., 2013] to prove the equivalence of different Prolog semantics.

As far as we know, our work is the first formally proved implementation of non-trivial static analyses and rewritings for a declarative and popular language, Datalog. It is also the first full-blown application of DatalogCert. Although we had to slightly extend the formalization, our work shows that it can concretely be used to prove concrete and non-trivial results on the use of Datalog, giving credits to the ambition of DataCert to provide a full environment for Datalog, among other aspects of data intensive applications.

Exploring Datalog subclasses

In Section 12.2, we introduce a stronger static analysis and lay out the hypothesis that there exists a subset of Datalog programs, strictly greater than the set of recursion-free programs, on which the analysis is complete. The idea of restricting Datalog to a viable or more efficient fragment is found in other works.

For example, [START_REF] Reutter | Regular Queries on Graph Databases[END_REF] identifies Regular Datalog, i.e. the class of programs using binary predicates with recursion limited to transitive closures, as a suitable Datalog fragment for graph queries. The Regular Datalog theory and an efficient, incremental engine for this fragment have then been implemented and certified in Coq [START_REF] Bonifati | Certified graph view maintenance with regular datalog[END_REF]. Due to timing constraints, we did not investigate this side of research about Datalog, but such works would obviously be a natural and strong starting point for a precise characterization of a Datalog fragment that could handle our stronger static analysis.

Part VI Conclusion

La route suivie jusque-là était comme un rêve dont la trace s'effaçait au fur à mesure qu'elle avançait Hwang Sok-Yonh, Shim Chong, fille vendue, traduit du coréen par Choi Mikyung et Jean-Noël Juttet

Lessons learned

During the course of this thesis, we attended a summer school where Andrei Paskevich stated that "Roughly speaking, the formal certification of a system is an order of magnitude more complex and time-consuming than the actual design of said system". This result, which may sound abstract to some, has ended up feeling very real to us. In particular, we were baffled by how very simple and straightforward ideas, such as those behind the static analysis we introduce, can be implemented in intricate and error prone -sometimes misleading -ways, and require complex, layered reasonings to be certified. This brings us back to the conclusion of [Benzaken et al., 2017a], which underlines that the justification of many foundational and "intuitively clear" database results had always been treated with a high-level persective rather than "scrupulous proofs", meaning that "low-level details were either glanced over or left to the reader". Knowing that there is a continuity and coherence in terms of such difficulties and motivations between the implementation of DatalogCert and our own work has been of great comfort to us.

The other core lesson (or rather set of lessons) we learned during this work is more qualitative than quantitative, and found in Section 12.3.4. This section basically explains that 1) our first implementation of the static analysis contained a bug 2) and was designed in a way that made the proofs much harder and less clear 3) which led us to write a deficient completeness statement that allowed the bug to go unnoticed for a long time.

We have been through many formal methods classes -and have been told roughly as many times about the crash of Ariane 5 Flight 501 -, so we were not entirely surprised by 1). This kind of "the devil is in the detail" situation is, after all, why we need formal methods.

The lesson of 2), like the statement of Andrei Paskevich discussed above, was already theoretically known but not concretely experienced by us. The choice of adequate models and formulations is of course discussed, or at least introduced, in formal methods classes, but the systems and algorithms which are verified in such settings are usually defined a priori. This means that we never had to work on the certification of an inadequately formulated component and did not expect this aspect to be as critical as it has been to us.

Finally, the issue that taught us the most was undoubtedly 3). We have been raised -in an academic sense -to think that the specification and verification of algorithm or program properties was an extremely powerful process. This intense promotion of certification may have led us to believe in the all mightiness of the existence of a verification pipeline, and thus not put enough care into its foundations, i.e. the statements that are checked. Throughout the rest of our life as a formal methods enthusiast and practitioner, we will remember this experience to help us reach and maintain a high level of self-criticism and perspective.

Notation A.1. This document tries to reproduce the interface used by Coq during proofs. Figure A.1 displays the general shape. The left side shows a list of instructions, or proof steps, entered by the user. On the right is the list of current (i.e. after the execution of the list of instructions) hypotheses and the current goal, i.e. the formula that still needs to be proved to obtain the original statement.

instruction 1. instruction 2. instruction 3.

Hypothesis 1 Hypothesis 2 -----------------Goal This process and the associated notation are illustrated by the next Section, which introduces the base use of Coq, i.e. the manipulation of first-order logic.

A.2 Playing with first-order logic

We provide two detailed examples, introducing first some propositional-level proof, and then how first-order quantifiers are handled.

Example A.2. We start with a proof of the commutativity of conjunction in formal logic, which we state as ∀A, ∀B, ((A ∧ B) → (B ∧ A)). Assuming for simplicity that A and B are already defined elsewhere as logical propositions, this lemma, which we name and_comm, is easily translated in Coq, as shown in -apply HB.

-apply HA.

No more subgoals. apply HB.

apply HA. Qed. Here, the A /\ B hypothesis is directly broken down into HA and HB. Just after the B /\ A goal is split, each generated branch calls a tactic, called auto. A tactic is roughly tactic, which detects that the subgoal exactly matches one of the hypotheses.

Example A.3. We now illustrate how quantifications are handled in Coq, by building a proof of (∃x, ∀y, P (y, x)) → (∀x, ∃y, P (x, y)). The logic behind this seemingly abstruse statement is easier understood by replacing P (x, y) with "y is at least as tall as x" and considering a universe of people, as it becomes If someone is at least as tall as everyone, then, when considering any given person, you can find someone who is at least as tall as him or her.

In Coq, we first assume an arbitrary type A, a binary predicate P over A, and translate our statement in Figure A.11.

Variable A : Type. Variable P : A -> A -> Prop.

Lemma quant_comm : (exists x, forall y, P y x) -> (forall x, exists y, P x y). We start by introducing the hypothesis. This leads us to a universally quantified statement, forall x, exists y, P x y. As shown in Figure A.12, we also introduce the x variable, meaning that we need to prove the rest of the statement with no information on the value of x.

intros H x. H : exists x : A, forall y : A, P y x x : A ----------------exists y : A, P x y The goal is now a special case of the Hw hypothesis. No more subgoals. These examples illustrate the most simple and basic use of Coq, which also has more practical uses, such as the verification of actual algorithms and programs.

A.3 Certified programming using Coq

Coq contains a full functional programming language, called Gallina. This language, oriented towards recursion and pattern-matching, is very similar to OCaml, as illustrated by the function defined in The max_in_list function expects a list of integers (seq nat, in Coq/MathComp notations) and returns the maximal integer found in that list (or 0 if the list is empty). The Fixpoint construct indicates that the function is recursive, like let rec in OCaml.

Such functions and programs can then be caracterized using the logic language of Coq. For example, an intuitive specification of a function that extracts the maximal integer from a list would be that 1) the extracted element does appear in the list and 2) there is no greater example in the list. Figure A.17 shows how this specification would be written in Coq.

Lemma max_in_list_bad_spec : forall l, (max_in_list l \in l /\ forall x, x \in l -> max_in_list l <= x). The 0 on the fourth position of the right side is made obselete by the 0 just added, so we can take it off. Moreover, the central bit of dst is set to 0 by the rule. We add this fact and get 10 ⋆ 0 ⋆ ⋆\10 ⋆ ⋆ 1⋆, which can be simplified as 10 ⋆ 0 ⋆ ⋆\⋆⋆ ⋆ ⋆ 1⋆. Since there is now only one bit defined on the right side on the pattern, we can invert it and put it on the left side, which gives us 10 ⋆ 00⋆. This pattern matches the (only) rule of R2, which leads to B.

Putting the two possibilities together, we can denote the set of packets going from A to B as 10 ⋆ 01 ⋆ ∪ 10 ⋆ 00⋆ = 10 ⋆ 0 ⋆ ⋆ Remark B.1. The authors of [START_REF] Lopes | Checking beliefs in dynamic networks[END_REF] We believe that the authors wanted to provide the intuition of the use of the negation \, and had to make it appear in the result since they provide it directly and do not show the details of the computation.

Contexte et motivations

Au cours des dernières décennies, le monde est devenu de plus en plus numérique. Cette tendance ne s'est pas inversée en 2020 ou 2021, les services professionnels et personnels étant de plus en plus fournis et utilisés au travers d'ordinateurs, tablettes ou téléphones portables.

Cet intense basculement numérique implique que les pannes réseaux sont plus coûteuses et nuisibles que jamais 1 , voire parfois critiques 2 . Nous insistons sur le fait que les pannes mentionnées et auxquelles nous nous intéressons ne sont pas le résultat d'attaques externes -qui par ailleurs arrivent toutes les semaines, sinon tous les jours et dans des proportions industrielles -, mais sont simplement des bugs.

Ces bugs sont avant tout dus à l'incroyable complexité de la conception de réseaux, qui ellemême vient de la nature hautement distribuée de ces derniers. De plus, la communauté réseau s'est longtemps basée sur une culture bricolo, dans le sens où elle ne disposait pas de fondations formelles, et donc des possibilités que l'existence et l'étude de telles fondations permet.

Durant les dix à quinze dernières années, des chercheurs et chercheuses avec un passif en théorie des langages de programmation ont commencé à s'intéresser au réseau, et à la façon dont ils pourraient appliquer leurs outils et approches théoriques à ce domaine. Combinée à l'augmentation critique des besoins en sûreté (et sécurité), cette situation a mené à l'introduction de méthodes formelles pour le réseau. Cette tendance a également été renforcée par les dernières avancées en méthodes formelles, à la fois en termes de techniques de modélisation et d'efficacité concrète (voir par exemple les solvers rapides comme Z3).

Parmi les outils créés, on trouve Network Optimized Datalog (NoD), un moteur Datalog developpé chez Microsoft conçu pour gérer des programmes qui décrivent, sous la forme de clauses de Horn, le comportement d'un réseau donné. Bien qu'étant un pas dans la bonne direction, utiliser ce moteur demande aux ingénieurs réseaux d'écrire manuellement un codage de chaque réseau analysé, ce qui est en soi un processus complexe et risqué.

De plus, NoD ne passe pas à l'échelle en utilisant des traductions naïves de réseaux de taille industrielle. En pratique, les auteurs se basent sur des programmes qui contiennent beaucoup de valeurs en dur, en utilisant des transformations (au niveau Datalog) manuelles, pas totalement claires et non-documentées. Cet angle mort dans un outil par ailleurs remarquable nous a poussé à travailler sur la conception et l'automatisation de transformations de programme similaires, cette fois avec une formalisation complète.

Cependant, avoir une formalisation d'opérations non-triviales n'est pas suffisant pour avoir confiance en elles. Le but de notre travail a donc été la vérification formelle de cette transformation dans l'assistant de preuve Coq, en utilisant (et étendant légèrement) une implémentation Coq de Datalog préexistante.

Bien qu'inspiré par le cadre de la vérification réseau, notre travail n'y est pas circonscrit. Concrètement, les analyses et réécritures que nous proposons peuvent être utilisées -et pertinentes -dans d'autres contextes. De plus, nous pensons que ce travail apporte un nouvel éclairage concernant la sémantique et l'étude formelle de programmes Datalog, éclairage qui pourrait servir comme base de travaux futurs, potentiellement dans d'autres contextes.

Contribution(s)

Questions et résultats

Liste de publications

Le travail sur cette thèse a mené aux contributions qui suivent :

• Une présentation au Coq workshop 2020 sur le développement de nouveaux types finis pour la bibliothèque MathComp [Bégay et al., 2020a] • Un papier aux 19 èmes journées approches formelles dans l'assistance au développement de logiciels (conférence AFADL 2020) [START_REF] Bégay | Octant, la vérification réseau simplifiée[END_REF] • Un papier à la conférence Certified Programs and Proofs (CPP) 2021 [START_REF] Bégay | Developing and certifying Datalog optimizations in Coq/Mathcomp[END_REF]

Partial program instantiation

Ce chapitre introduit notre première réécriture, qui requiert une surapproximation des substitutions calculées in fine par l'exécution d'un programme Datalog et s'en sert pour produire une instance partielle -et en générale beaucoup plus longue -du programme, où une partie des résultats des calculs apparaît d'office dans les règles. Le chapitre discute également de la formalisation et justification Coq de cette réécriture.

Static analysis

Nous intoduisons une analyse statique qui fournit une surapproximation du comportement de n'importe quelle variable d'un programme Datalog, et peut donc être utilisée en conjonction avec la réécriture du chapitre précédent. Fondamentalement, cette analyse représente le chemin parcouru par une variable lors de l'exécution comme un arbre étiqueté par des conjonctions et des disjonctions, et dont les feuilles sont des colonnes dans la base de données extensionnelles du programme analysé. Nous présentons également la formalisation et justification Coq de cette analyse statique, en particulier la difficulté liée à la preuve de terminaison.

Predicate specialization

Ce chapitre introduit notre deuxième transformation de programme, qui analyse des programmes partiellement instanciés -elle s'utilise donc en conjonction avec la première -et en spécialise les prédicats pour diminuer le nombre d'arguments manipulés. Encore une fois, la formalisation et preuve Coq sont présentées dans le chapitre.

Discussion and related works

Nous discutons certains points de notre travail sur lesquels il nous semblait intéressant de revenir. En particulier, nous commentons les effets et l'efficacité de l'optimisation dans Octant, présentons les contours d'une analyse statique plus fine qui reste à étudier plus finement, revenons sur certains choix de modélisations et erreurs faites pendant ces trois années, commentons l'effort général représenté par l'ensemble des preuves, et introduisons des travaux qui nous semblent liés à cette thèse.

Conclusion et perspectives

Le travail présenté dans ce document est, comme souvent en recherche, le résultat fortuit du petit projet de deux semaines proverbial. Plus concrètement, cette thèse était initialement censée s'intéresser au langage NetKat (voir la section 4.4), mais il nous a été demandé de jeter un oeil au moteur Network Optimized Datalog (chapitre 5) pour améliorer les performances d'Octant (chapitre 6). Le résultat est, comme souligné dans l'introduction, l'identification d'un angle mort dans le moteur susmentionné, la conception d'une analyse statique, deux transformation de programme et une sémantique de trace pour Datalog, leur certification dans une formalisation Coq/MathComp préexistente du langage et l'introduction de nouveaux types finis pour MathComp.

Nous pensons fortement qu'une des caractéristiques clefs de cette thèse est le fait qu'elle utilise DatalogCert (voir le chapitre 3), en fournissant un exemple d'utilisation non-trivial, et donc une illustration de son utilisabilité. De plus, comprendre les choix de conception d'implémentation de ce moteur, ce qui nous a mené a proposer quelques additions ou modifications, fut un exercice intellectuel très satisfaisant. Il va sans dire que nous sommes très redevables aux auteurs de DatalogCert -S ¸tefania-Gabriela Dumbravȃ, Véronique Benzaken et Evelyne Contejean -, sans qui notre travail aurait clairement manqué de fondations et justifications solides.

A un niveau plus concret, l'analyse statique et les transformataions que nous introduisons dans ce document peuvent réécrire des spécifications génériques, réutilisables, (généralement) courtes et claires en une forme spécifique à un réseau plus proche d'un programme NoD typique. Le faire à la main est bien sûr possible, mais aussi long, complexe et risqué, nos optimisations certifiées ont donc pour but de concilier performances et sûreté. Les programmes réécrits s'exécutent plusieurs ordres de grandeur plus rapidement que les originaux, mais restent parfois significativement plus lents que leurs équivalents écrits à la main dans NoD. Cette situation justifie notre travail sur une analyse statique plus fine (section 12.2), qui essaie d'échanger un peu de sûreté (il devient inadapté à certains programmes Datalog) pour beaucoup plus d'efficacité. La question clef devient alors de déterminer précisément quels programmes sont exclus -en particulier en trouvant un critère syntaxique simple -, et si les programmes restants sont pertinents. Nous donnons une esquisse de ces réponses, mais nos intuitions restent à formaliser et vérifier. L'analyse statique et les deux réécritures que nous intoduisons sont toutes définies et certifiées indépendamment. Si une meilleure analyse statique pour Datalog que la nôtre (chapitre 10)que ça soit la version alternative mentionnée plus haut ou une approche entièrement différente -apparaissait, elle pourrait être facilement branchée dans l'instance partielle de programmes du chapitre 9, notamment grâce à la très large définition de l'hypothèse de complétude de l'ensemble de substitutions fourni (voir la définition 9.4).

A l'inverse, le travail présenté dans ce document a été conçu pour Network Optimized Data-182 log et la vérification réseau, mais pourrait être utilisé dans d'autres contextes. Cette possibilité reste à étudier.

Leçons

Pendant cette thèse, nous avons assisté à une école d'été durant laquelle Andrei Paskevich a expliqué que "La certification formelle d'un système est en général un ordre de grandeur plus complexe et longue que la conception du système en question". Ce résultat, qui pourrait au premier abord sembler abstrait, nous a finalement semblé très réél. En particulier, nous avons été étonné de constater combien des idées très simples, par exemple celles derrières l'analyse statique que nous proposons, peuvent être implémentées de façons inutilement absconses et risquées, et nécessiter un raisonnement complexe et abstrait pour les certifier.

Ces reflexions nous ramènent à la conclusion de [Benzaken et al., 2017a], qui souligne que la justification de nombreux résultats centraux et "intuitivement clairs" à propos des bases de données ont toujours été traités "de loin" plutôt qu'avec des "preuves scrupuleuses", impliquant que "des détails de bas niveaux étaient soient ignorés, soit laissés au lecteur ou la lectrice". Savoir qu'il y a une continuité et une sorte de cohérence en termes de motivations et difficultés entre l'impleméntation de DatalogCert et notre propre travail fut d'un grand réconfort.

L'autre grande leçon (ou plutôt ensemble de leçons) que nous avons apprise(s) durant ce travail est d'ordre plus qualitatif que quantitatif, et détaillée dans la section 12.3.4. Fondamentalement, nous y expliquons que 1) notre première implémentation de l'analyse statique contenait une erreur 2) et été conçue d'une façon qui rendait les preuves bien plus dures et moins claires 3) ce qui nous a mené à écrire un résultat de complètude incorrect qui a permis pendant très longtemps à une erreur de ne pas être repérée.

Nous avons suivi de nombreux cours de méthodes formelles -durant lesquels on nous a presque systématiquement raconté le crash du vol 501 d'Ariane 5 -, donc nous n'avons pas été surpris par 1). Cette situation à la "le diable est dans les détails" est, après tout, ce qui justifie les méthodes formelles.

Le leçon de 2), comme la citation de Andrei Paskevich discutée plus haut, nous la connaissions théoriquement mais n'en avions jamais fait l'expérience concrète. Les cours de méthodes formelles discutent bien sûr du choix de modèles et formulation appropriés, mais les systèmes et algorithmes qui y sont vérifiés sont généralement définis a priori. Nous n'avions donc jamais eu à travailler sur la certification d'un système mal défini ou implémenté, et ne nous attendions donc pas à ce que cet aspect soit aussi critique qu'il l'a été dans notre travail.

Enfin, le problème qui nous a le plus enseigné aura sans l'ombre d'un doute été 3). Nous avons été élevé -dans un sens académiquee -dans la croyance que la spécification et la vérification de propriétés d'un algorithme ou programme était un processus extrêmement puissant. Cette très forte mise en avant de la certification nous a fait croire en la toute-184 puissance de la simple existence d'un processus de vérification, et donc nous pousser à ne pas mettre assez de soin dans ses fondations, i.e. les lemmes et théorèmes qui sont vérifiés. Tout au long du reste de notre vie en tant qu'admirateur et professionnel des méthodes formelles, nous nous souviendrons de cette expérience, afin d'atteindre et maintenir un haut niveau d'auto-critique et de perspective.

  of a logic programming language: Datalog 1 First-order logic 1.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Normal forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4 Inference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Datalog 2.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Adding and handling negation . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 Adding on-the-fly constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Datalog in Coq 3.1 Finite types and notations in MathComp . . . . . . . . . . . . . . . . . . . . 3.2 Datalog syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . III Network Verification 4 Approaches to network verification 4.1 The difficulty of network verification . . . . . . . . . . . . . . . . . . . . . . . 4.2 Dataplane verification and testing . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Control plane verification and testing . . . . . . . . . . . . . . . . . . . . . . . 3 Part I Introduction Donc si vous décidez d'écrire cet article, je vous colle au violon pour incitation à l'émeute, mensonge, trouble de l'ordre public, folie paranoïaque, tentative de suicide et prose pitoyable Lewis Trondheim, Les formidables aventures de Lapinot (tome 3, Walter )

  Definition 1.27. A Σ-Structure M = (U M , I) consists of a non-empty universe U M and an interpretation function I : Σ → U M ∪ {⊤, ⊥}, such that • for every f ∈ F, I(f ) : U ar(f ) M → U M • for every p ∈ P, I(p) : U ar(p) M

t

  ::= x | c, where x ∈ V, c ∈ C Definition 2.3. Let p be a predicate and #» t a term sequence with | #» t | = ar(p). An atom A is an expression of the form

Figure 2 . 1 :

 21 Figure 2.1: Directed graph connectivity in Datalog

Figure 2 . 2 :

 22 Figure 2.2: Expressing equality in Datalog

  Figure 2.3 translates into Datalog a simplified version of the access to the parisian MK2 Bibliothèque movie theater. can watch(P, M, D) ← showing(D, M ), cost(P, D, C), pays f or(F, P, C). cost(P, D, 4.90) ← child(P ), any(D). cost(P, D, 4.90) ← young(P ), week(D). cost(P, D, 7.90) ← young(P ), weekend(D). cost(P, D, 7.90) ← student(P ), any(D).

Figure 2 . 3 :

 23 Figure 2.3: Buying a ticket at the MK2 Bibliothèque

Definition 2. 27 .

 27 Let us assume a program P and the signature Σ = (C, P, ar). As in Section 2.1.1, C is the set of program constants, and ar the arity function for the set of predicates P. A Σ-structure I comes in the form I = (U, I : Σ ′ → U ∪ {⊤, ⊥}), where Σ ′ = C ∪ P.

Figure 2 . 4 :

 24 Figure 2.4: Directed graph disconnectedness in Datalog augmented with negation

  (b) and an equivalent standard Datalog program

Figure 2 . 5 :

 25 Figure 2.5: Interpretation of semipositive Datalog programs

Figure 2 . 6 :

 26 Figure 2.6: Computing the tallest people in strict Datalog

Figure 2 . 7 :

 27 Figure 2.7: Computing the tallest people in Datalog with primitive predicates

Figure 3 . 1 :

 31 Figure 3.1: Using the decidable equality of an eqType

Figure

  Figure 3.2: eqType axiom

Figure 3 . 4 :

 34 Figure 3.4: Enforcing predicates in sequences

Figure 3 . 5 :

 35 Figure 3.5: Constants, predicate symbols and arity

Figure 3 . 6 :

 36 Figure 3.6: Defining variables in DatalogCert

Figure 3 . 7 :

 37 Figure 3.7: Defining (ground) atoms

  Inductive clause : Type := Clause of atom & seq atom. Inductive gclause : Type := GClause of gatom & seq gatom. Definition program := seq clause.

Figure 3 . 8 :

 38 Figure 3.8: Lifting to full Datalog programs in DatalogCert

Figure 3 . 9 :

 39 Figure 3.9: Collecting variables in DatalogCert

Figure 3 . 10 :

 310 Figure 3.10: Datalog safety in DatalogCert

Figure 3 . 11 :

 311 Figure 3.11: Groundings as finite functions

Figure 3 . 12 :

 312 Figure 3.12: Defining term groundings in DatalogCert

Figure 3 . 13 :

 313 Figure 3.13: Lifting groundings to atoms and clauses

Figure 3 . 14 :

 314 Figure 3.14: Defining substitutions in DatalogCert

Figure 3 . 15 :

 315 Figure 3.15: Comparing substitutions in DatalogCert

Figure 3 . 16 :

 316 Figure 3.16: Grounding with a substitution

  . The authors first define the satisfaction of a clause w.r.t. a Herbrand Interpretation (denoted as interp), then the notion of Herbrand model at the level of clauses and programs. The implementation of minimal model semantics is itself rather minimal, which reflects its fundamental aspect. In comparison, the translation of the fixpoint semantics requires more work. (* Head and body of a ground clause *) Definition head_gcl gcl := let: GClause h b := gcl in h. Definition body_gcl gcl := let: GClause h b := gcl in b.

Figure 3 . 17 :

 317 Figure 3.17: Minimal model semantics in DatalogCert

Figure 3 . 18 :

 318 Figure 3.18: Constructive term matching in DatalogCert

  Definition match_raw_atom s ra rga : option sub := match ra, rga with | RawAtom s1 arg1, RawGAtom s2 arg2 => if (s1 == s2) && (size arg1 == size arg2) then foldl (fun acc p => obind (match_term p.1 p.2) acc) (Some s) (zip arg2 arg1) else None end.Definition match_atom s a (ga : gatom) := match_raw_atom s a ga.

Figure 3 . 19 :

 319 Figure 3.19: Constructive atom matching in DatalogCert

  Definition match_atom_all (i : interp) a s := [set x | Some x \in [set match_atom s a ga | ga in i]].

Figure 3 . 20 :

 320 Figure 3.20: Constructive matching of a set of atoms in DatalogCert

Figure 3 . 21 :

 321 Figure 3.21: Folding and flattening sets

Figure 3 . 22 :

 322 Figure 3.22: Constructive tail matching in DatalogCert

Figure 3 . 23 :

 323 Figure 3.23: Generalization of tail matching

  Definition cons_clause (def : constant) (cl : clause) i : {set gatom} := [set gr_atom_def def s (head_cl cl) | s in match_body i (body_cl cl)].

Figure 3 . 24 :

 324 Figure 3.24: Deduction of new facts with a clause

Figure 3 . 25 :

 325 Figure 3.25: Iterating deduction over a program

Figure 3 . 26 :

 326 Figure 3.26: Adequacy of the fixpoint semantics

Figure 5

 5 Figure 5.1: Example network

Figure 5 . 2 :

 52 Figure 5.2: Encoding routing constraints and effects

Figure 5 . 3 :

 53 Figure 5.3: Datalog translation of the packet flows

Figure 5 . 4 :

 54 Figure 5.4: Accounting for load balancing in Datalog formalization

Figure 5 . 5 :

 55 Figure 5.5: Looking for load balancing inconsistencies

Figure 6 . 1 :

 61 Figure 6.1: Connectivity in Datalog

Figure

  Figure 6.2: Sample EDB

Figure 6 . 3 :

 63 Figure 6.3: Catching multi-attachment

  Figure 6.4 extends the linked predicate for a given IP address. The & and > primitive predicates stand respectively for the bit-wise AND and comparison on bit vectors. linked(X, Y, IP ) ← port(net id = X, device id = T ), router(id = T ), match route(T, M, IP, P ), not better route(T, IP, M ), port(id = P, net id = Y, device id = T ). match route(T, M, IP, P ) ← router(router id = T, pref ix = S, mask = M, port = P ), IP & M = S. better route(T, IP, M ) ← match route(T, M 2, IP, P ), M 2 > M.

Figure 6 . 4 :

 64 Figure 6.4: Simple network reachability in Datalog

Figure 7 . 1 :

 71 Figure 7.1: Definition of syntactically bounded sequences

Figure 7 . 2 :

 72 Figure 7.2: Base case for the finiteness proof of Wlist

Figure 7 . 3 :

 73 Figure 7.3: Inductive case for the finiteness proof of Wlist

Figure 7 . 4 :

 74 Figure 7.4: Wrapping-up the finiteness proof of Wlist

Figure 7 . 5 :

 75 Figure 7.5: Relating sequences and Wlists

Figure 7 . 6 :

 76 Figure 7.6: Signature version of Wlist

Figure 7 . 7 :

 77 Figure 7.7: Definition of lists with unicity

Figure 7 . 8 :

 78 Figure 7.8: Injecting uniq seq into tuples

Figure 7 . 9 :

 79 Figure 7.9: Finiteness of uniq seq

Figure 7 . 10 :

 710 Figure 7.10: Adding an element to a uniq seq

  .11. Definition pucons {A : eqType} (t : A) (b : @uniq_seq A) : @uniq_seq A := match Sumbool.sumbool_of_bool (t \notin (useq b)) with | left H => ucons H | in_right => b end.

Figure 7 . 11 :

 711 Figure 7.11: Trying to add an element to a uniq seq

Figure 7 . 13 :

 713 Figure 7.13: Generic tree type

Figure 7 . 14 :

 714 Figure 7.14: Induction principle for generic trees

Fixpoint

  ABin {A : eqType} {B : Type} (x : A) (t : @ABtree A B) : bool := match t with | ABLeaf _ => false | ABNode y l => (x == y) || (has (ABin x) l) end.Definition ABnotin {A : eqType} {B : Type} (x : A) (t : @ABtree A B) : bool := ~~ABin x t.

Figure 7 . 15 :

 715 Figure 7.15: Generic tree membership

Figure 7 . 16 :

 716 Figure 7.16: Implementing the notion of (strict) subtree

Figure 7 .

 7 Figure 7.17: Definition of syntactically bounded trees

Figure 7 . 18 :

 718 Figure 7.18: From leaves to actual objects

  Figure 7.19 then shows the reverse transformation, and the cancellation lemma. Definition gl (x: B) := (BLeaf 0 x). Lemma cancelflgl : cancel fl gl.

Figure 7 . 19 :

 719 Figure 7.19: Equivalence between Htree of height 0 and leaves

Figure 7 . 20 :

 720 Figure 7.20: Base case for the finiteness proof of Htree

Figure 7 . 21 :

 721 Figure 7.21: Inductive case for the finiteness proof of Htree

Figure 7 .

 7 Figure 7.22: Wrapping-up the finiteness proof of Htree

Figure 7 . 23 :

 723 Figure 7.23: Unicity across branches

Figure 7 .

 7 Figure 7.24: Tree width

Figure 7 . 25 :

 725 Figure 7.25: Definition of trees bounded by width and unicity

Figure 7 .

 7 Figure 7.26: Core lemma in the proof of finiteness of WUtree

Figure 7 . 27 :

 727 Figure 7.27: WUtree have a bounded height

Figure 7 . 28 :

 728 Figure 7.28: Atoms as finite types

Figure 7 . 29 :

 729 Figure 7.29: Defining clauses as a finite type

Figure 7 . 30 :

 730 Figure 7.30: Formalization of the extensional vs. intensional predicates constraint

  Figure 8.1: Building a Datalog trace

Figure 8 . 2 :

 82 Figure 8.2: Node type for the trace semantics

Figure 8 . 3 :

 83 Figure 8.3: Redefining rul gr as a finite type

Figure 8 . 4 :

 84 Figure 8.4: Definition of the trace type

Figure 8 . 5 :

 85 Figure 8.5: Coq implementations of ded and tb

. 6 .

 6 Definition sem_tree_to_inter def (ts : {set trace_sem_trees}) : interp := [set ded def x | x in ts]. Definition ded_sub_equal (def : syntax.constant) (lx : seq trace_sem_trees) (s : sub) (ats : seq atom) : bool := (map (ded def) lx) == (map (gr_atom_def def s) ats).

Figure 8 . 6 :

 86 Figure 8.6: Lifting ded to sets and sequences of traces

Figure 8 . 7 :

 87 Figure 8.7: Specification of pset

Figure 8 . 8 :

 88 Figure 8.8: Computing new traces

  Definition fwd_chain_t def (k : {set trace_sem_trees}) : {set trace_sem_trees} := k :|: \bigcup_(cl <-p) cons_clause_t def cl k.

Figure 8 . 9 :

 89 Figure 8.9: Forward chain step for the trace semantics

Figure 8 .

 8 Figure 8.10: Iterating T t P

Figure 8 .

 8 Figure 8.11: (Reverse-)Monotonicity of the trace semantics

Figure 8 . 12 :

 812 Figure 8.12: Coq implementation of the trace semantics adequacy

Figure 9 . 1 :

 91 Figure 9.1: Defining s(X,Y, Z)

Figure 9 . 3 :

 93 Figure 9.3: Substitutions for s projected over X and Y

Figure 9 . 4 :

 94 Figure 9.4: Overapproximation S of variables X and Y

Figure 9 . 5 :

 95 Figure 9.5: New, partially instantiated definition of s(X,Y, Z)

  Definition 9.4. (Completeness of S) ∀C ∈ P, |vars(C) ∩ R| > 0 ⇒ (∀ν, match(ν, C, (T P ↑ ω)(I)) ⇒ ν |R ∈ S).

Theorem 9 .

 9 12. (Transformation soundness) For any program P , initial interpretation I and number of steps k, any fact deduced by the transformed program in k iterations of T P was already computed after the same number of steps in the original program, i.e. (T inst(P ) ↑ k)(I) ⊆ (T P ↑ k)(I).

  that ν matches C w.r.t. (T inst(P ) ↑ k)(I) and f is the head of ν(C). Clause C is the result of applying inst to a clause C o of the original program, i.e. C = inst(C o ). Just like in the definition of inst, we need to consider whether C o contains variables to instantiate. If C o has no relevant variable, i.e. |vars(C o ) ∩ R| = 0, C = C o . We then deduce f in P reusing C, ν. The atoms in the body of the instantiated clause being in (T P ↑ k)(I) (induction hypothesis), we can indeed do so. Otherwise, there is a substitution σ such that C = σ(C o ), with dom(σ) = R∩vars(body(C o )). To deduce f in P w.r.t. (T P ↑ k)(I), we use the C o clause with the ν ∪ σ substitution. Since ν and σ have disjoint domains, we can easily show that ν ∪ σ(C o ) = ν(σ(C o )) = ν(C). We then obtain the same instantiated clause as in the transformed program inst(P ). Thanks to the induction hypothesis, (T inst ↑ k)(I) ⊆ (T P ↑ k)(I), the intermediate interpretation for the original program contains at least the facts that were used for the deduction in the transformed program. We use them, with the aforementioned instantiated clause ν ∪ σ(C o ), to deduce f . Now that the partial program instantiation is defined and justified on paper, we can formalize and certify it within Coq and MathComp.

Figure 9 . 6 :

 96 Figure 9.6: Coq hypotheses the partial program instantiation

Figure 9 . 7 :

 97 Figure 9.7: Coq version of the boolean matching

Figure 9 . 8 :

 98 Figure 9.8: Relating the two kinds of matching

Figure 9 . 9 :

 99 Figure 9.9: Definition and absorption of a program's semantics

Figure 9 . 10 :

 910 Figure 9.10: Restricting a substitution in Coq

  (body_cl cl) :&: Rv| > 0) ==> [forall s : sub, (bmatch def ffp cl s) ==> (sub_filter s Rv \in subs)]].

Figure 9 . 11 :

 911 Figure 9.11: Coq implementation of the completeness hypothesis on the overapproximation

Figure 9 . 12 :

 912 Figure 9.12: Coq implementation of the partial program instantiation

Figure 9 . 13 :

 913 Figure 9.13: Coq definition of the adequacy of the partial program instantiation

  Figure 10.1: Indexing variable names

Figure 10 . 4 :

 104 Figure 10.4: Analysis of a recursive program

Figure 10

 10 Figure 10.5: Shifting variables

Figure 10 . 6 :

 106 Figure 10.6: Another program analysis

  Definition setX := [set u | u.1 \in A1 & u.2 \in A2].

Figure 10 . 7 :

 107 Figure 10.7: Cartesian product between two sets in MathComp

Figure 10 . 8 :

 108 Figure 10.8: Cartesian product between an arbitrary number of sets

Figure 10 . 9 :

 109 Figure 10.9: Bounding program occurrences

  Definition at_at (o : t_occ) : option atom := match nth_error p (r_ind o) with | None => None | Some cl => nth_error (body_cl cl) (b_ind o) end. Definition p_at (t : t_occ) := match (at_at t) with | None => None | Some ato => Some (sym_atom ato) end.

Figure 10 . 10 :

 1010 Figure 10.10: Computing an occurrence's predicate

Figure 10 . 11 :

 1011 Figure 10.11: Finding occurrences in an atom

  Lemma ord_shiftl {i : nat} : forall x : 'I_i, x.+1 < i.+1. Definition ord_shift {i : nat} (x : 'I_i) : 'I_i.+1 := Ordinal (ord_shiftl x). Definition shift1 {k : nat} {A : finType} (l : {set ('I_k * A)%type}) : {set ('I_k.+1 * A)%type} := [set ((ord_shift x.1), x.2) | x in l].

Figure 10 . 12 :

 1012 Figure 10.12: Incrementing an ordinal

Fixpoint

  occsInAtomList (al : seq atom) (v : 'I_n) : {set ('I_(size al) * 'I_max_ar)} := match al with | [::] => set0 | a::al => ([set (Ordinal (ltn0Sn _), x) | x in (occsInAtom a v)] :|: (shift1 (@occsInAtomList al v))) end.

Figure 10 . 13 :

 1013 Figure 10.13: Finding occurrences in an atom list

Figure 10 . 14 :

 1014 Figure 10.14: Certification of occsInAtomList

Figure 10 . 15 :

 1015 Figure 10.15: Finding occurrences in a rule

Figure 10

 10 Figure 10.16: Finding occurrences in a program

  Variable dv : 'I_n. Variable dt : term. Definition term_to_var (t : term) := match t with | Val c => dv | Var v => v end. Definition get_cl_var (cl : clause) (j : nat) : 'I_n := term_to_var (nth dt (arg_atom (head_cl cl)) j).

Fixpoint

  analyze_var_prev (prev : {set tocc p}) (v : var) (count : nat) : {set {set (uniq_seq tocc)}} := (* Ensuring termination *) match count with | 0 => set0 | count.+1 => (* Computing non-visited occurrences of v *) let occs := occsInProgram p v :\: prev in (* Analysis of a predicate and index pair *) let analyze_pi (prev : {set tocc}) (o : tocc) := match p_at o with | None => set0 | Some f => match predtype f with | Edb => [set [set unil]] | Idb => let arec := [set (analyze_var_prev prev (get_cl_var cl (t_ind o))) count | cl in p & head_predicate cl == f] in \bigcup_(x in arec) x end end in (* Adding the current tocc on top of a DNF *) let all_add_o (dt : {set {set (uniq_seq tocc)}}) (o : tocc) {set {set (uniq_seq tocc)}} := [set [set o::br | br in ct] | ct in dt] in (* Recursive call *) let arec := [seq all_add_o (analyze_pi (occ |: prev) occ) occ | occ <-enum occs] in bigcup_cart (gen_setX arec) end.(* Version used in practice, with prev set to empty set for maximal precision *) Definition analyze_var (v : var) (count : nat) : {set {set (uniq_seq tocc)}} := analyze_var_prev set0 v count.

Figure 10 . 18 :

 1018 Figure 10.18: The static analysis in Coq

  Definition bigcup_tup {m} {A : finType} (t : m.-tuple {set A}) : {set A} := \bigcup_(x <-tval t) x. Definition bigcup_cart {m} {A : finType} (s : {set m.-tuple {set A}}) : {set {set A}} := [set bigcup_tup y | y : m.-tuple {set A} in s].

Figure 10 . 19 :

 1019 Figure 10.19: From cartesian product to set of sets

  Figure 10.20: Two DNFs to merge

Figure 10 . 24 :

 1024 Figure 10.24: Storing the analysis

  25. (* Predicate to which branch [br] leads *) Definition branch_pred (br : seq (t_occ p)) := match br with | [::] => df (* default predicate symbol *) | a :: l => match p_at (last a l) with | None => df | Some f => f end end. (* Argument index of the last occurrence in the branch *) Definition branch_t_ind (br : seq (t_occ p)) := match br with | [::] => 0 | a :: l => t_ind (last a l) end.

Figure 10 . 25 :

 1025 Figure 10.25: Fetching predicate and argument index from analysis branch

  Definition extract_vals_br (br: dbranch p) : {set syntax.constant} := [set (nth def (arg_gatom f) (branch_t_ind br)) | f : gatom in i & sym_gatom f == branch_pred df br].

Figure 10 . 26 :

 1026 Figure 10.26: Extracting values from a branch

  Definition extract_vals_conj (cj : {set dbranch p}) : {set syntax.constant} := \bigcap_(br in cj) extract_vals_br br. Definition extract_vals_disj (disj : {set {set dbranch p}}) : {set syntax.constant} := \bigcup_(cj in disj) extract_vals_conj cj. Definition extract_vals_sub : {set sub} := [set (add emptysub v c) | c in extract_vals_disj analysis].

Figure 10 . 27 :

 1027 Figure 10.27: From branches to substitutions

Fixpoint

  unrec_trace_gen (prev : {set tocc}) (tr : trace) (v : var) (count : nat) : {set (uniq_seq tocc)} := (* Ensuring termination of the funciton *) match count with | 0 => set0 | count.+1 => (* If tr is a leaf, the empty list encapsulated in a set is returned *) match tr with | ABLeaf _ => [set unil] | ABNode (RS cl s) descs => (* Unfolding the next call *) let unrec_b (o : t_occ p) : {set dbranch} := match (nth_error descs (b_ind o)) with | None => set0 (* None case not used in practice *) | Some (ABLeaf _) => [set unil] | Some (ABNode (RS clb sb) descsb) => (* recursive call, with o added to prev *) unrec_trace_gen (o |: prev) (ABNode (RS clb sb) descsb) (get_cl_var dt dv clb (t_ind o)) count end in (* Computing non-visited occurrences of v *) let occs := (occsInProgram p v) :\: prev in (* adding occ on top of the returned set of sequences *) \bigcup_(occ in occs) [set pucons occ l | l in unrec_b occ] end end.

Figure 10 . 28 :

 1028 Figure 10.28: The no-recursion trace in Coq

(

  * Characterization of the adequacy of a dbranch w.r.t. a substitution, a variable and an interpretation *) Definition br_adequate def (br : dbranch) (s : sub) (v : 'I_n) (i : interp) : bool := [exists c : syntax.constant, (s v == Some c) && [exists ga in i, (sym_gatom ga == branch_pred br) && (nth def (arg_gatom ga) (branch_t_ind br) == c)]]. Theorem no_rec_needed tr v (i : interp) (m : nat) (cl : clause) (s : sub) : (* tr is a trace obtained with interpration i *) tr \in sem_t p def m i (* whose root is cl,s *) -> ABroot tr = inl (RS cl s) (* v is a variable of cl *) -> v \in tail_vars (body_cl cl) (* each dbranch of the no-recursion trace is adequate w.r.t. s, v and i *) -> [forall br in unrec_trace tr v (height tr).+1, br_adequate def br s v i].

Figure 10 . 29 :

 1029 Figure 10.29: Completeness of no-recursion traces

Figure 10 .

 10 Figure 10.30: No-recursion trace capture

Figure 10 .

 10 Figure 10.31: No-recursion trace is captured by a bounded analysis

  Definition extract_subs_spec : {set sub} := [set s : sub | (dom s == [set v]) && [exists ct in analysis, [forall br in pred_of_set ct, @br_adequate p df def br s v i]]].

Figure 10 . 32 :

 1032 Figure 10.32: Specification of the extraction

Figure 10 . 33 :

 1033 Figure 10.33: Completeness of the specification of the extraction

Figure 10 . 34 :

 1034 Figure 10.34: Adequacy of the partial instance with the static analysis

Figure 11 . 1 :

 111 Figure 11.1: Defining and using p

Figure 11 . 2 :

 112 Figure 11.2: Specialized program

Figure 11 . 3 :

 113 Figure 11.3: Relating normal and specialized definitions

  spec(ν(a)) = p c i (c 1 , ..., c i-1 , c i+1 , ...c m ) ∈ (T spec(P,p,i) ↑ k)(I) and (recursion hypothesis)(T spec(P,p,i) ↑ k)(I) ⊆ (T P ↑ k)(I) ∪ {spec(a, p, i) | a ∈ (T P ↑ k)(I)} Since (T P ↑ k)(I)does not contain ground atoms about predicates of the form p c , we know that spec(ν(a)) ∈ {spec(a, p, i) | a ∈ (T P ↑ k)(I)}, which in turns shows that ν(a) ∈ (T P ↑ k)(I).Every ground atom in the body of ν(C) is in (T P ↑ k)(I), meaning that we can use C and ν in the original program to deduce the non-specialized version of f at step k + 1. Then, f ∈ {spec(a, p, i) | a ∈ (T P ↑ k + 1)(I)}, which implies our goal.

Figure 11 . 8 :

 118 Figure 11.8: Specializing atoms and clauses

Figure 11 . 9 :

 119 Figure 11.9: Specializing a ground atom

  Definition ind_terms := pmap (fun cl => nth_error (arg_atom (head_cl cl)) ind) [seq cl <-p | hsym_cl cl == f].Definition ind_vals := pmap (fun t => if t is Val c then Some c else None) ind_terms.

Figure 11 . 10 :

 1110 Figure 11.10: Computing the approximation of f

  2.1) to build the rules. Definition dep_iota (m k : nat) : seq ('I_(m+k)) := pmap insub (iota m k). (** [X_1, X_2, ..., X_j] *) Definition gen_vars_j (j : 'I_n.+1): seq term := map (fun x => Var x) (map (fun x : 'I_j => widen_ord (ltn_ord j) x) (dep_iota 0 j)). Hypothesis arity_vars : arity f < n.+1. (** [X_1, X_2, ..., X_(arity f)] *) Definition gen_vars : seq term := gen_vars_j (Ordinal arity_vars).

Figure 11 . 11 :

 1111 Figure 11.11: Manually defining a sequence of variables

Figure 11 . 15 :

 1115 Figure 11.15: Building generic rules

(

  * Enriches [s] to map the variables of [args] to the values of [gargs] *) Fixpoint extract_sub_seq_c (args : seq term) (gargs : seq syntax.constant) (s : sub) : sub := match gargs with | [::] => s | x::l => match args with | [::] => s | Val x'::l' => (extract_sub_seq_c l' l s) | Var x'::l' => add (extract_sub_seq_c l' l s) x' x end end. (* [extract_sub_seq_c] in practice *) Definition extract_sub_ga (a : atom) (ga : gatom) := extract_sub_seq_c (arg_atom a) (arg_gatom ga) emptysub.

Figure 11 . 16 :

 1116 Figure 11.16: Computing substitutions for the generic rules

(

  * The extraction of [lc] via [lt] applied to [lt] returns [lc] *) Lemma extract_sub_seq_map (lt : seq term) (lc : seq syntax.constant) : uniq lt -> size lt = size lc -> [forall t in lt, exists v, t == Var v] -> lc = [seq gr_term_def def (extract_sub_seq_c lt lc emptysub) i0 | i0 <-lt]. (* Extraction s(a2) via a1 applied to a1 returns s(a2) *) Lemma extract_gr_v (s : sub) (a1 a2 : atom) : sym_atom a1 = sym_atom a2 -> [forall t in (arg_atom a1), exists v:'I_n, t == Var v] -> uniq (arg_atom a1) -> gr_atom_def def s a2 = gr_atom_def def (extract_sub_ga a1 (gr_atom_def def s a2)) a1. (* Similar to the previous lemmas, but handles the removal/addition of constant c *) Lemma extract_sub_seq_rem_map (lt : seq term) (v : 'I_n) (lc : seq syntax.constant) (j : 'I_n) (c : syntax.constant) : j < size lt -> size lt = size lc -> uniq lt -> [forall t in lt, exists vb, t == Var vb] -> find (fun y => y == Var v) lt = j -> nth_error lc j = Some c -> lc = [seq gr_term_def def (extract_sub_seq_c (rem (Var v) lt) (sremove lc j) emptysub) i | i <-set_nth (Val c) lt j (Val c)].

Figure 11 . 17 :

 1117 Figure 11.17: Certification of the use of the generic rules

  introduces the Coq formalization of these intermediate lemmas. Lemma proj_completeness_u (m : nat) : sem p def m i :|: [set gatom_clone ind ga | ga in sem p def m i] \subset sem proj_prog def m.*2 i. Lemma proj_soundness_u (m : nat) : sem proj_prog def m i \subset sem p def m i :|: [set gatom_clone ind ga | ga in sem p def m i & sym_gatom ga == f].

Figure 11 . 18 :

 1118 Figure 11.18: Intermediate completeness and soundness results

  Figure 11.19. Theorem proj_completeness (m : nat) : sem p def m i \subset sem proj_prog def m.*2 i. Theorem proj_soundness (m : nat) : [set x in sem proj_prog def m i | ~~is_clone_ga x ] \subset sem p def m i.

Figure 11 . 19 :

 1119 Figure 11.19: Final completeness and soundness results

  Figure 12.1: Two definitions of p

Figure 12 . 2 :

 122 Figure 12.2: Deep quadratic definition of p

  Figure 12.3: Merged analyses

Figure 12 . 4 :

 124 Figure 12.4: Mixing some values

Figure 12 . 5 :

 125 Figure 12.5: Analysis of a heterogeneously recursive program

  Figure 12.6: Attempts at a difficult merger

Figure 12 . 11 :

 1211 Figure 12.11: Mix of the analyses of X 1 and Y 1

  Definition pimset {A B : finType} (f : A -> option B) (s : {set A}) : {set B} := [set id x | x in (pmap f [seq y | y in s])]. Definition pset {A : finType} (s : {set (option A)}) : {set A} := pimset id s.

Figure 12 . 12 :

 1212 Figure12.12: Defining pset using pmap

(

  * Not accepted by Coq/MathComp *) Definition pset_alt_target {A : finType} (s : {set option A}) : {set A} := [set y | x in s & x is Some y]. (* Used in practice *) Definition pset_alt {A : finType} (s : {set option A}) : {set A} := [set y | x in s, y in A & x == Some y].

Figure 12 . 13 :

 1213 Figure 12.13: Defining pset using set notations

Figure 12 . 14 :

 1214 Figure 12.14: Core typing rules

Figure A. 1 :

 1 Figure A.1: Feedback in Coq

Figure A. 2 .

 2 Lemma and_comm: A /\ B -> B /\ A.

Figure A. 2 :Figure A. 3 :Figure A. 4 :Figure A. 5 :Figure A. 6 :Figure A. 7 :

 234567 Figure A.2: Commutativity of ∧ in Coq

Figure A. 8 :

 8 Figure A.8: Ending the proof

Figure A. 9 :

 9 Figure A.9: Full Coq proof of the commutativity of ∧

Figure A. 10 :

 10 Figure A.10: Full but shorter Coq proof of the commutativity of ∧

Figure A. 11 :

 11 Figure A.11: Writing quantifiers in Coq

Figure A. 12 :

 12 Figure A.12: Moving a universally quantified variable to the hypotheses

  Figure A.15 shows how we then can finish the proof, i.e. by applying the rule with the right instance 2 of y. intros H x. destruct H as [w Hw]. exists w. apply (Hw x).

Figure A. 15 :

 15 Figure A.15: Using a universally quantified hypothesis

Figure A. 16 .

 16 Fixpoint max_in_list (l : seq nat) : nat := match l with | [::] => 0 | h::tl => max h (max_in_list tl) end.

Figure A. 16 :

 16 Figure A.16: Extracting max from a list of integers in Coq

Figure A. 17 :

 17 Figure A.17: Incorrect Coq specification of max extraction from a list

  denote their result as 10 ⋆ 01 ⋆ ∪ (10 ⋆ ⋆ ⋆ ⋆ \ ⋆ ⋆ ⋆ 1 ⋆ ⋆). The two results are equivalent, as shown by the following equalities:10 ⋆ 01 ⋆ ∪ (10 ⋆ ⋆ ⋆ ⋆ \ ⋆ ⋆ ⋆ 1 ⋆ ⋆) = 10 ⋆ 01 ⋆ ∪ 10 ⋆ 0 ⋆ ⋆ (integrating negation) = 10 ⋆ 0 ⋆ ⋆(right side is more general)

  Definition 1.45. A formula in Conjunctive Normal Form (or Clausal Normal Formal, CNF in both cases) is a conjunction of clauses, i.e. a formula of the form

	m	k i
		L ij
	i=1	j=1

  Remark 2.18. Since it has no body, a fact is safe iff it is ground. Definition 2.19. (Program safety condition) A program P is safe if all the clauses it contains are safe. Example 2.20. The program in Figure 2.1 is safe. Note that the last four clauses, or facts, are an illustration of Remark 2.18.

Example 2.21. The program of Figure

2

.2a is invalid in Datalog, as variable X appears in the head of the only rule but not the body (note that the safety constraint more generally forbids rules with empty bodies). On the other hand, the program of Figure

2

.2b, where element(X) is an extensional predicate that contains every constant of the EDB, is a hacky but valid program. Finally, note that in practice, equality is seen as a primitive predicate (see Section 2.4).

equal(X, X) ← .

  Program As a program P is a set of clauses, P * is simply Example 2.26. (Graph Transitive Closure) Using these rules to translate the program of Figure 2.1 returns the following formula, where the first line contains the facts, and the other two correspond to the two rules of the program.

C∈P

C * e(1, 3) ∧ e(2, 1) ∧ e(4, 2) ∧ e(2, 4)

  An interpretation I is a model for a clause C if the latter is evaluated to ⊤ wrt the former, i.e. C I = ⊤. Such an interpretation is also a model for a program P if it is for every clause of the program, i.e. ∀C ∈ P, C I = ⊤. Definition 2.30. A fact F is a logical consequence of a program P , written P |= F , iff any interpretation I satisfying P also satisfies F , i.e. I |= P implies I |= F . Notation 2.31. The set of all logical consequences of a program P is denoted as cons(P ).

	⊥ otherwise	.., B n	I,ι = ⊤
	Definition 2.29. We now give a FOL view of the deduction of new facts in Datalog. To do so, we rely on
	Herbrand Semantics (cf. Section 1.2).

  Lemma 2.50. Given a Datalog program P and a Herbrand Structure H = (U H , I), I is a pre-fixed point of T P iff I |= P . Let I be a pre-fixed point of T P , i.e. T P (I) ⊆ I, H ← B 1 , ..., B n a clause in P and ι a valuation. If {ι(B 1 ), ..., ι(B n )} ⊆ I then ι(H) ∈ T P (I) (second condition in the definition of T P ). Since T P (I) ⊆ I, ι(H) ∈ I, and thus I |= ι(H ← B 1 , ..., B n ). ⇐ Let A be a ground atom in T P (I), we need to show that A ∈ I. Then, based on T P 's definition, A was either already present in I, or there exists a rule H ← B 1 , ..., B n ∈ P and a valuation ι such that A = ι(H) and {ι(B 1 ), ..., ι(B n )} ⊆ I. Since I |= P , ι(H) = (A) ∈ I.

	Proof. secret
	⇒ Remark 2.51. Since T P (I) preserves the elements of I, I ⊆ T P (I). With that in mind, Lemma 2.50 can be re-stated as I is a fixpoint of T

P iff I |= P .

  where ⊔ is the disjoint set union.Notation 2.73. Such a stratification of P into {P 1 , . . . , P n } is denoted as Pn . Each stratum P i may also be refered to as a program slice.

	Remark 2.72. Given a clause C that appears in the P i stratum, lifting Definition 2.70, we
	obtain that any predicate symbol which appears positively (resp. negatively) in the body of
	C is fully defined by strata which are below or at the same level as (resp. strictly below) P i .
	Concretely, for any predicate symbol p ∈ P,
	• if p ∈ • if p ∈	L ∈ body + (C) L ∈ body -(C)	sim(L), then def (p) ⊆ sim(L), then def (p) ⊆	j≤i j<i	P j P j
	Remark 2.74. Given a stratification Pn , every program slice P i is a semipositive Datalog
	program w.r.t. the lower strata. In other words, assuming we have computed slices P 1 to
	P i-1 , and seeing the result as a new EDB, P i can be dealt with using the method of Section
	2.3.2.1.
	Remark 2.75. Some Datalog programs, such as the one in Example 2.66, are not stratifiable.

  As discussed in Section 2.3, negation in Datalog is dealt with by stratifying the program and saturating each intermediate stratum. Having a full and complete definition of what is true being the requirement to define something as false.

  and possibly explains -the fact that the new extraction introduced in this section is not appropriate for the program of Figure 12.4. The rule studied in Example 12.6 contains only one head and one body occurrences for every variable. The next examples outlines how this notion deals with more occurrences. Example 12.7. Let us consider the program of Example 10.10, which is replicated in Figure 12.9 for clarity. Like the program of Figure 12.4, it mixes values, but in a slightly different way.

	p(X 1 ,	Y 1 ,	Z 1 )	←	p(Y 1 ,	X 1 ,	Z 1 )
			Figure 12.8: Argument cycle of Z 1

  Le point de départ de cette thèse fut l'identification et analyse d'un angle mort dans le moteur Network Optimized Datalog en la présence de prédicats primitifs avec plusieurs variables. Pour s'attaquer à ce problème, nous avons conçu une analyse statique pour Datalog, ainsi que deux transformations de programme sur laquelle elles se basent. L'analyse statique et les transformations ont été vérifiées dans l'assistant de preuve Coq, en se basant sur une formalisation et implémentation de Datalog dans Coq préexistante.Notre travail a requis et mené à l'extension de certains outils, principalement l'ajout d'une sémantique de trace pour Datalog et son implémentation vérifiée dans la formalisation Coq de Datalog susmentionnée. Nous avons aussi développé des nouveaux types finis pour la bibliothèque Mathematical Components, sur laquelle la formalisation s'appuie.Enfin, nous présentons une version plus fine de notre analyse statique, et montrons qu'elle ne marche pas sur tout programme Datalog. Ce résultat nous pousse à essayer de caractériser la classe précise de programmes Datalog sur lesquels elle peut être utilisée, mais notre intuition reste à vérifier formellement.

Léo & Léo, vous êtes absolument tordants, il me semble donc inconcevable que tous les groupes du MPRI n'aient cherché à vous récupérer, et je suis ravi que nous ayons gagné les enchères. Un merci tout particulier à Léo pour son accueil si chaleureux à Bruxelles, nous gardons un souvenir émerveillé de notre séjour.En revanche, passer trois ans à Lannion fut une expérience quelque peu traumatisante, mais j'y ai heureusement fait la rencontre de personnes fantastiques qui m'ont permis d'y survivre. Merci donc à Alice, Sergio et David. Partager ma cuisine, mon bureau ou un couscous avec vous fut systématiquement source de joie, et c'est peu dire que votre compagnie quotidienne me manque aujourd'hui. Merci également à Patrick et Dom pour leur accueil, ainsi qu'à La Medina et Le Gringo, clairement les deux endroits les plus cool de la ville.J'ai bien sûr rencontré moult enseignants au cours de mes études supérieures, dont beaucoup sont de fantastiques pédagogues, voire des modèles dans ma propre pratique de l'enseignement. Sans être exhaustif, j'ai une pensée pour David Baelde, qui m'a soutenu jusqu'au bout et à qui j'ai volé sa super posture au tableau, pour Claudine Picaronny, véritable modèle de clarté aux devoirs toujours passionnants, pour Hubert Comon-Lundh, qui nous a enseigné la rigueur, ou Carlos Geraci, dont le cours de linguistique de langue des signes fut d'un grand vertige humain et intellectuel. J'ai également une pensée pour Rached Mneimné et Gilles Dowek, qui m'ont appris qu'un cours de maths peut être à la fois terriblement précis et merveilleusement humain, et pour Timo Jolivet et Irène Marcovici, qui m'ont poussé à aller plus loin, et sans lesquels je ne saurais sans doute pas aujourd'hui ce dont je suis capable.Enfin, je remercie Jean Goubault-Larrecq pour ses innombrables et incroyables anecdotes, qui me permettent aujourd'hui de briller dans les dîners en ville.

https://techcrunch.com/2020/07/17/cloudflare-dns-goes-down-taking-a-large-piece-of-the-internet-with-it/

https://www.reuters.com/business/media-telecom/orange-blames-network-outage-software-failure-audit-2021-06-11/

http://datacert.lri.fr/

https://wiki.openstack.org/wiki/Neutron

"La logique ! On y revient encore et toujours ! C'est le leitmotiv de cette leçon"

Intuitively, if the precondition is false, the rest does not matter.

At the time of writing, the full pricing specification of the MK2 Bibliothèque can be found at https://www.mk2.com/sites/default/files/grille tarifaire bibliotheque2.pdf. A complete Datalog implementation is of course left as an exercice to the reader.

https://www.youtube.com/watch?v=dHLa1SMILik

https://opennetworking.org/wp-content/uploads/2013/04/openflow-spec-v1.0.0.pdf

https://opennetworking.org/wp-content/uploads/2014/10/openflow-spec-v1.3.3.pdf

https://opennetworking.org/wp-content/uploads/2014/10/openflow-switch-v1.5.1.pdf

http://frenetic-lang.org/

https://p4.org/p4-spec/docs/P4-16-v1.2.1.pdf

v 1 v 2 v 1 &v 2 v 1 v 2 v 1 &v 2 v 1 v 2 v 1 &v 2 v 1 v 2 v 1 &v 2 ⋆ ⋆ ⋆ ⋆ \ { 0 ⋆ ⋆⋆ 0 ⋆ ⋆⋆ 1 ⋆ ⋆⋆, ⋆0 ⋆ ⋆ ⋆0 ⋆ ⋆ ⋆1 ⋆ ⋆, ⋆ ⋆ 0⋆ ⋆ ⋆ 0⋆ ⋆ ⋆ 1⋆, ⋆ ⋆ ⋆0 ⋆ ⋆ ⋆0 ⋆ ⋆ ⋆1, ⋆ ⋆ ⋆⋆ 0 ⋆ ⋆⋆ 1 ⋆ ⋆⋆ 1 ⋆ ⋆⋆, ⋆0 ⋆ ⋆ ⋆1 ⋆ ⋆ ⋆1 ⋆ ⋆, ⋆ ⋆ 0⋆ ⋆ ⋆ 1⋆ ⋆ ⋆ 1⋆, ⋆ ⋆ ⋆0 ⋆ ⋆ ⋆1 ⋆ ⋆ ⋆1, ⋆ ⋆ ⋆⋆ 1 ⋆ ⋆⋆ 0 ⋆ ⋆⋆ 1 ⋆ ⋆⋆, ⋆1 ⋆ ⋆ ⋆0 ⋆ ⋆ ⋆1 ⋆ ⋆, ⋆ ⋆ 1⋆ ⋆ ⋆ 0⋆ ⋆ ⋆ 1⋆, ⋆ ⋆ ⋆1 ⋆ ⋆ ⋆0 ⋆ ⋆ ⋆1, 1 ⋆ ⋆⋆ 1 ⋆ ⋆⋆ 0 ⋆ ⋆⋆, ⋆1 ⋆ ⋆ ⋆1 ⋆ ⋆ ⋆0 ⋆ ⋆, ⋆ ⋆ 1⋆ ⋆ ⋆ 1⋆ ⋆ ⋆ 0⋆, ⋆ ⋆ ⋆1 ⋆ ⋆ ⋆1 ⋆ ⋆ ⋆0, } Figure 6.5: Inverse truth table in differences of cubesWe use twelve-bit vectors, which represent the two values of the two variables, and the result of the bitwise conjunction, on four bits each. This difference of cube uses the binary nature of bits to encode the conjunction by specifying what should not be produced, e.g. two 0 at the same index in v 1 and v 2 , and a 1 at the same index in the result.However, not every primitive can be handled in an efficient way with the difference of cubes representation.Example 6.6. When representing the relation v 1 ≥ v 2 , we need to compare prefixes rather than isolated bits, as illustrated by Figure6.6. This means that the tricks seen in the previous examples do not apply to this case, and end up with a representation that is exponential in the number of bits the integers use.

The program of Figure6.4 uses a strict comparison whereas it was broad in Example 6.6. That is because < is even harder to directly implement and is easier described as the negation of ≥.

For no particuliar reason, according to a member of the MathComp team.

This rewriting requires -both on the paper and Coq levels -an overapproximation of the transformed program's behavior. The next chapter introduces, formalizes and discusses a static analysis that provides one.

https://coq.inria.fr/library/Coq.Lists.List.html#nth_error

In fact, the unicity of variables across rules makes that inequality hold in general. Without this hypothesis, one can come up with programs where it is reversed

This is where the induction hypothesis of the lemma's straightforward but weak formulation falls short.

Theoretically, the hypothesis that no cloned predicate appears in the rules implies that they are all different from f . However, Due to Coq's thoroughness, we need to consider the case where f does not appear in the studied program, in which case we lose the implication. We deemed it simpler and more elegant to add the hypothesis of inequality between the cloned predicates and f .

https://github.com/math-comp/math-comp/blob/master/mathcomp/ssreflect/seq.v#L2626

Although they implement them as lists in the formalization, Datalog programs are even defined as sets of clauses in[Dumbrava, 2016].

See https://wikimpri.dptinfo.ens-cachan.fr/doku.php?id=cours:c-2-7-1 for the foundations and https://wikimpri.dptinfo.ens-cachan.fr/doku.php?id=cours:c-2-7-2 for a comprehensive presentation of Coq as a tool.

Actually, Coq can figure it out by itself, using unification, but explicitely providing unnecessary arguments in Coq generally helps a lot understanding the high-level reasoning of a proof.

Remerciements

Part V Optimizations Section 6.2 discussed how the use of some primitive predicates with multiple variables leads to performance issues within the Network Optimized Datalog engine. The core of this thesis is then to introduce program analyses and rewritings that transform a Datalog program into a semantically equivalent one that is free of such constructs.

Chapter 9 introduces a first program transformation that aims at the reduction of variables in a Datalog program by trading them for more rules. This rewriting requires an overapproximation of the behavior of the variables to be removed, which may be provided by the static analysis we introduce in Chapter 10. Then, Chapter 11 presents a second rewriting that reduces the encodings of some predicates within Network Optimized Datalog.

These developments are all implemented and certified in Coq, using the formalization presented in Chapter 3 and the extensions introduced in Part IV. Chapter 12 reflects on this process, in particular discussing the lessons we learned along the way, as well as related works.

Using function dom, that returns the domain of a substitution (see Definition 1.23), any clause can be instantiated using the inst function. Remark 9.8. The |vars(C)∩R| > 0 hypothesis in Definitions 9.4 and 9.6 is rather inelegant, but was required to work with clauses that do not contain any variables of R. An alternative way to deal with them would have been to always manually add the empty substitution to S. Since we did not think this solution was much more satisfactory, we went for the one that felt more efficient in practice. We indeed expect that, in real use cases, many rules will not contain variables to instantiate, meaning that we should quickly take them out of the instantiation process rather than vainly go over the entirety of S for each of them.

Now that the transformation and its hypothesis have been defined, we need to prove that it is semantics-preserving. We first need the following technical result.

Notation 9.9. Given a set X, we denote as X the set complement (within the type of the elements). Note that, in particular, for any substitution σ and set of variables V , σ |V is the restriction of σ to the variables which are not in V .

Lemma 9.10. For any substitution σ and clause C such that σ matches C, then, for any subset of the program variables X, then σ |X matches σ |X (C). In other words, when part of σ is used to instantiate the clause, then the rest of the substitution matches the result.

Proof. First proved at the level of atoms by induction on the arguments, then for clauses using an induction as well, on the body this time.

The actual adequacy results are broken down into completeness and soundness. Our reference point is the Datalog fixpoint semantics introduced in Section 2.2.2. Intuitively, we show that the same facts are actually deduced in the same number of steps.

Theorem 9.11. (Transformation completeness) For any program P , initial interpretation I and number of steps k, the transformed program deduces in k iterations of T P every fact that was computed after the same number of steps in the original program, i.e.

Proof. We proceed by induction on the number of steps k. In the base case, the definitions imply that (T P ↑ 0)(I) = (T inst(P ) ↑ 0)(I) = I, which is even more general than our goal.

In the recursive case, let f be a fact in (T P ↑ k + 1)(I). Then, f was either already in the previous iteration (T P ↑ k)(I), or it has just been deduced and added. In the first f is a fact about another predicate symbol newline The predicate symbol of atom f is different from p, meaning that spec(f, p, i) = f . Then, our working hypothesis f ∈ (T

The rest of the proof is familiar: if f was already in (T P ↑ k)(I), we obtain our goal with the induction hypothesis and monotonicity of the fixpoint operator. Otherwise, we extract a clause C and a substitution ν, and use spec(C, p, i) with ν to deduce f . Theorem 11.5 is then a corollary of Lemma 11.8.

As for the soundness, the formulation of Theorem 11.6 considers the specialized facts in a negative way, in the sense that it removes them from the semantics of the transformed program, whereas we need to reason about their presence. We then introduce the following soundness result.

Lemma 11.10. (Strong pred. specialization soundness) For any number of steps k,

Proof. We proceed by induction on the number of steps k. In the base case, the definitions imply that (T P ↑ 0)(I) = (T spec(P,p,i) ↑ 0)(I) = I. The goal then becomes

which is automatically true.

In the recursive case, let f be a fact in (T spec(P,p,i) ↑ k + 1)(I). We split the proof in three cases, depending on the predicate symbol of f .

f is a p fact newline Due to the hypothesis on the rules defining p in the P program, every corresponding rule in inst(P, p, i) is headed by a predicate of the form p c . The only rules of the transformed program that can deduce facts about p are the "specialized to general" rules, such as those of Figure 11.3. This means that f has been deduced via a rule of the form

Writing f as p(c 1 , ..., c m ), this means that p c i (c 1 , ...c i-1 , c i+1 , ..., c m ) ∈ (T spec(P,p,i) ↑ k)(I). Combined with the induction hypothesis, we obtain that

The original program P does not deduce facts about predicates of the form p c , meaning that

comparison, the Coq implementation is not as straightforward. In particular, the introduction of new, specialized predicates and the use of the relating rules, such as seen in Figure 11.3, require some hypotheses and machinery, as presented in this section.

Hypotheses on the program and specialization

Let us assume a safe (see Figure 3.10) Datalog program P , which contains an intensional predicate f whose ind th argument of a predicate is always a constant. We need a mechanism to build the new, specialized predicates. To do so, we assume a function, named pclone, that associates a predicate to any constant. Figure 11.5 shows its type and a characterization of the newly introduced predicates.

Variable pclone : syntax.constant -> symtype.

Definition is_clone_pred (g : symtype) : bool := [exists c, g == pclone c].

Definition is_clone_ga (ga : gatom) : bool := is_clone_pred (sym_gatom ga). We also need some assumptions on the cloning function. The introduced predicates must be new, in the sense that they do not already appear in the rules or EDB. It must also be different from f 2 , and the cloned predicates must all be different. Finally, we need to relate their arity with that of f . Remark 11.11. These hypotheses only concern our Coq formalization, and their enforcement by an implementation of this transformation still has to be checked.

We can now use the predicate specialization function to rewrite the rules of the program.

Rewriting the rules

We proceed incrementally, starting with atoms. We need to either take away (for the instances of the specialized predicates) or replace by a constant (for the left side of the added rules) a variable in these sequences. Figure 11.12 shows how it is done.

(* [X_1, X_2, ..., X_(k-1), X_(k+1), ..., X_(j)] *) Definition gen_vars_rem_j (j : 'I_n.+1) (k : 'I_n) : seq term := rem (Var k) (gen_vars_j j).

(* [X_1, X_2, ..., X_(j-1), X_(j+1), ..., X_(arity f)] *) Definition gen_vars_rem (j : 'I_(arity f)) : seq term := gen_vars_rem_j (Ordinal arity_vars) (@widen_ord (arity f) n arity_vars j). forall fp v ct pred j, @progPredTyping fp v ct [::] pred j [::] | ppt_rec_no : forall p v (ctxt : tocs p) ip pred new_cl j typs, @progPredTyping p v ctxt ip pred j typs -> (pred <> (hsym_cl new_cl)) -> @progPredTyping p v ctxt (new_cl :: ip) pred j typs | ppt_rec_yes : forall p v (ctxt : tocs p) ip new_cl j typs ntyp v', @progPredTyping p v ctxt ip (hsym_cl new_cl) j typs -> (nth_error (arg_atom (head_cl new_cl)) j) == Some (Var v') -> @varTyping p ctxt v' ntyp -> @progPredTyping p v ctxt (new_cl :: ip) (hsym_cl new_cl) j (ntyp :: typs) with OccsToTypes : forall p (v : 'I_n) (ctxt : tocs p), {set (enotin ctxt)} -> seq (DDtype ctxt) -> Prop := | colt_base : forall p v (ct : tocs p), @OccsToTypes p v ct (seq_to_enotin set0 ct) [::] (* The set l has (recursively) been typed as the list typs. Adding occurrence tocc to the set triggers a call to predTyping, with tocc added to the context *) | colt_rec : forall p v ct tocc l typs (dt : DDtype (ct :|: [set (elnotin tocc)])) pato rul_ind body_ind aind, @OccsToTypes p v ct l typs -> (elnotin tocc) = (T_occ rul_ind body_ind aind) -> p_at (val tocc) = Some pato -> @predTyping p v (ct :|: [set (elnotin tocc)]) pato aind dt -> @OccsToTypes p v ct ([set tocc] :|: l) ((@DtInsert p ct (elnotin tocc) dt (Helnotin tocc) (@ddtextract p ct (val tocc) dt))::typs).

Scheme varTyping_mrec := Induction for varTyping Sort Prop with occsToTypes_mrec := Induction for OccsToTypes Sort Prop with predTyping_mrec := Induction for predTyping Sort Prop with progPredTyping_mrec := Induction for progPredTyping Sort Prop.

Summary and perspectives

The work presented in this document is, as it is often the case in research, the adventitious result of the proverbial two-week side project. More concretely, this thesis was supposed to be about the NetKat language (see Section 4.4), but we have been asked to take a quick look at the Network Optimized Datalog engine (Chapter 5) to help with the performances of Octant (Chapter 6). The result is, as outlined in the introduction, the identification of a caveat in the aforementioned engine, the design of a static analysis, two program transformations and a trace semantics for Datalog, their certification within an existing Coq/MathComp formalization of the language and the introduction of new finite types for MathComp.

We strongly believe that one of the key features of the present thesis is the fact that it leverages DatalogCert (see Chapter 3) and provides a nontrivial proof-of-concept and strong argument for its reusability and scalability (which are quickly discussed in the conclusion of [Dumbrava, 2016]). Moreover, understanding the design and implementation choices behind this engine, which led us to propose some additions or modifications, was a very interesting and enjoyable intellectual journey. It goes without saying that we are very grateful to the authors of DatalogCert -S ¸tefania-Gabriela Dumbravȃ, Véronique Benzaken and Evelyne Contejean -, without whom our work would clearly have lacked strong foundations and justifications.

On a more concrete level, the static analysis and transformations we introduce (usually) clear and short specifications into a network-specific form closer to NoD programs. Doing so by hand is obviously possible, but also lengthy, complex and error-prone, meaning that our certified optimizations conciliate performances and safety. The rewritten programs are computed orders of magnitude faster than the original ones, but remain significantly slower than their hand-written NoD counterparts in some instances. This justifies the work on a smarter static analysis (see Section 12.2), which tries to trade a bit more safety (it becomes inadequate for some Datalog programs) for much more efficiency. The key question is then to determine precisely what programs are excluded -in particular provide a simple syntactic criteria -and if the remaining programs are relevant. We outline answers to these questions, but they remain to be formalized and verified.

Moreover, we emphasize the fact that the static analysis and the two rewritings we introduce are all defined and certified independently. In particular, should someone find a better static analysis for Datalog than ours (Chapter 10) -may it be the alternative version mentioned just above or an entirely different approach -, it could very easily be plugged into the partial program instantiation of Chapter 9, notably thanks to the very broad definition of the completeness hypothesis on the provided set of substitutions (see Definition 9.4).

The other way around, the work presented in this document has been designed with Network Optimized Datalog and network verification in mind, but may be helpful in other contexts. This possiblity remains to be investigated.

Appendix A

Coq basics

This document presents a research work that has been designed, developed and certified using the Coq proof assistant. As such, Parts IV and V contain a lot of Coq code, and assume some familiarity with it. In case this document would ever find its way into the hands or screen of anyone who has strictly no experience with Coq or a similar proof assistant, this appendix provides a very simple, high-level and usage-oriented introduction.

This introduction does not dwelve into the theoretical foundations of proof assistants, and it is not designed to replace a proper academic course 1 . It should, however, convey the spirit of Coq and help a confused reader get a grasp of what is going on in the aforementioned parts of this document, if not the full details.

Section A.1 introduces the interactive nature of Coq. Then, Section A.2 illustrates it with two simple proofs in first-order logic. Finally, Section A.3 discusses the use of Coq in the development of verified programs and compilers.

A.1 Interactive theorem proving

A proof assistant is, as the name suggests, a tool designed to help people prove results. To do so, the user must be able to define objects (logics, algebras, programs...) and state properties about them.

In the introduction of his PhD thesis [Winterhalter, 2020], Théo Winterhalter provides a great discussion on proof assistants, in which he compares them to chatbots. Apart from the fact that it is sometimes surprisingly hard and frustrating to make oneself understood by a proof assistant, the main similarity is the "feedback loop" working environment it sets up.

Once the user has formally defined an object and a statement he or she wants to prove, called the goal, the proof assistant awaits a step of the proof. Each time the user provides one, the proof assistant checks that it indeed can be applied, computes the new goal, i.e. what is now left to prove, and expects a new proof step. This process terminates once all these steps form a full proof of the initial statement.

realize that he or she wrote a deficient specification, as it does not capture the case where the list is empty. Figure A.18 shows the correct specification, which would easily be proven using an induction on l.

Lemma max_in_list_spec : forall l, size l > 0 -> (max_in_list l \in l /\ forall x, x \in l -> max_in_list l <= x). Remark A.4. Both max_in_list and max_in_list_spec are about lists of integers for simplicity, but could be generalized over the type of the list. In that case, the function would expect a specialized max function with the type and list, as well as a default element for the empty case. The specification would start by a universal quantification over the type, and its verification would require a proof that the provided default element is indeed minimal w.r.t. the provided max, just like 0 w.r.t. ≤.

Once the max_in_list_spec specification has been proved, it can be used in the certification of more complex functions built upon max_in_list, and so on. This way, very complex programs can be written and certified in a modular manner, as illustrated by Chapter 3, which implements a verified engine for the logic programming language Datalog. Morever, Coq allows the extraction of such verified (or even unverified) programs into OCaml [Letouzey, 2008].

Finally, Coq only considers functions which are shown to terminate. Informally, in the framework of the Curry-Howard isomorphism upon which Coq is built, a non-terminating function is a proof of the false statement, ⊥, which should of course not be accepted.

Coq can infer for itself that the program of Figure A.18 does, but some recursions are much more complex. Section 10.4.4 discusses how we had to tackle this issue for our own work.

Appendix B

Computing (very simplified) network reachability

This Appendix details the computations discussed in Section 5.2.1. The point of introducing them is to familiarize the reader with the example network, and show that even an extremely simplified, high-level view of network reachability is a surprisingly tricky problem.

The analyzed network is replicated in Figure B.1 for readability. We want to compte by hand the set of packets that will go from A to B. To do so, we will denote the dst and src attributes as a single, 6-bit vector. The first three bits represent dst, whereas the last three are src. A packet flowing from A to B must first go through R1, which can be done via the first or second rule. In the first case, the packet header must be of the form 10 ⋆ 01⋆. Then, once it has reached R2, it can only go directly to B. The only relevant rule in the routing table first checks that the packet header matches 10 ⋆ ⋆ ⋆⋆, which is more general than the form of all packets going straight from R1 to R2. In a sense, we compute a conjunction, but it does not explicitely appear in the result.

and its configuration

We then know that all packets matching the 10 ⋆ 01⋆ pattern will go from A to B, using the R1 → R2 link. Another possibility is to go through R3. The first step is then to leave R1 using the R1 → R3 link, which requires using the second rule rather than the first. The set of relevant packets is those of the form 1 ⋆ ⋆ ⋆ ⋆⋆ but not 10 ⋆ 01⋆. This set is denoted as 1 ⋆ ⋆ ⋆ ⋆ ⋆ \ 10 ⋆ 01⋆.

We then want to use the R3 → R2 link rather than the R3 → D one. This has two consequences: 1) the leftmost bit of src must be 0 rather than 1 and 2) the leftmost bit of dst must be 1. Requirement 2) is already enforced by the R1 → R3 rule. We add 1) and obtain

Contenu des différents chapitres

First-order logic

On rappelle les fondations de la logique du premier ordre (syntaxe et sémantique), ainsi que certains points utilisés par ailleurs dans la thèse (formes normales et systèmes d'inférence).

Datalog

Introduction formelle du langage de programmation logique Datalog, dont on définit formellement la syntaxe et deux sémantiques (dont une opérationnelle) équivalentes. On discute de plus comment traiter la négation dans Datalog, ainsi que l'utilisation de prédicats primitifs pour ajouter des calculs "non-symboliques" au langage.

Datalog in Coq

On présente d'abord succintement la bibliothèque Mathematical Components de Coq. On introduit ensuite la modélisation de Datalog en Coq intitulée DatalogCert, sur laquelle notre propre travail de formalistion et vérification s'appuie.

Approaches to network verification

On discute d'abord des difficultés inhérentes à la vérification réseau, puis on en présente les différents types : vérification et test du dataplane (d'un réseau déployé), du control plane (la partie automatique du déploiement d'un réseau), et enfin la synthèse de réseaux corrects par construction.

Network Optimized Datalog

Introduction de l'outil de vérification réseau NoD. On discute en particulier de l'intérêt de Datalog pour modéliser le comportement d'un réseau, et les modifications qui sont faites à un moteur Datalog prééxistant pour passer à l'échelle.

Octant

On présente des limites de Network Optimized Datalog en termes de généricité, puis on introduit l'outil Octant qui s'y attaque. On discute ensuite du coût que le nouveau niveau de généricité a en termes d'efficacité.

New sequence and tree finTypes

On présente les nouveaux types de listes et arbres finis que nous développons dans MathComp. Les listes sont bornées soit syntaxiquement (type des listes contenant au plus x éléments), soit sémantiquement (type des listes sur un type fini ne contenant pas deux fois le même élément). Les arbres suivent un principe similaire (arbres bornés syntaxiquement en largeur et hauteur, ou syntaxiquement en largeur et par unicité en hauteur). Les preuves de ces types -ou plutôt de leur caractère fini -sont également présentées.

A trace semantics for Datalog

Nous introduisons une nouvelle sémantique, dite de trace pour Datalog. Au lieu de considérer le résultat de l'exécution d'un programme comme un ensemble de faits, nous le voyons comme un ensemble d'arbres détaillant les calculs de chacun de ces faits. Nous décrivons également la formalisation de cette sémantique dans Coq, et comment nous l'avons rendue finie pour l'intégrer au cadre de DatalogCert.