UniTO/tesi/gabriel/part4_unicode.org
2024-10-29 09:11:05 +01:00

32 KiB
Executable file
Raw Permalink Blame History

Translation validation of the Pattern Matching Compiler

Source program

Our algorithm takes as its input a source program and translates it into an algebraic data structure which type we call decision_tree.

type decision_tree =
  | Unreachable
  | Failure
  | Leaf of source_expr
  | Guard of source_blackbox * decision_tree * decision_tree
  | Switch of accessor * (constructor * decision_tree) list * decision_tree

Unreachable, Leaf of source_expr and Failure are the terminals of the three. We distinguish

  • Unreachable: statically it is known that no value can go there
  • Failure: a value matching this part results in an error
  • Leaf: a value matching this part results into the evaluation of a source black box of code

Our algorithm doesn't support type-declaration-based analysis to know the list of constructors at a given type. Let's consider some trivial examples:

function true -> 1

is translated to

Switch ([(true, Leaf 1)], Failure)

while

function
| true -> 1
| false -> 2

will be translated to

Switch ([(true, Leaf 1); (false, Leaf 2)])

It is possible to produce Unreachable examples by using refutation clauses (a "dot" in the right-hand-side)

| true -> 1
| false -> 2
| _ -> .

that gets translated into

Switch ([(true, Leaf 1); (false, Leaf 2)], Unreachable)

We trust this annotation, which is reasonable as the type-checker verifies that it indeed holds.

Guard nodes of the tree are emitted whenever a guard is found. Guards node contains a blackbox of code that is never evaluated and two branches, one that is taken in case the guard evaluates to true and the other one that contains the path taken when the guard evaluates to true.

\begin{comment} [ ] Finisci con Switch [ ] Spiega del fallback \end{comment}

The source code of a pattern matching function has the following form:

match variable with
| pattern₁ → expr₁
| pattern₂ when guard → expr₂
| pattern₃ as var → expr₃
| pₙ → exprₙ

Patterns could or could not be exhaustive.

Pattern matching code could also be written using the more compact form:

| pattern₁ → expr₁
| pattern₂ when guard → expr₂
| pattern₃ as var → expr₃
| pₙ → exprₙ

This BNF grammar describes formally the grammar of the source program:

start ::= "match" id "with" patterns | "function" patterns
patterns ::= (pattern0|pattern1) pattern1+
;; pattern0 and pattern1 are needed to distinguish the first case in which
;; we can avoid writing the optional vertical line
pattern0 ::= clause
pattern1 ::= "|" clause
clause ::= lexpr "->" rexpr
lexpr ::= rule (ε|condition)
rexpr ::= _code ;; arbitrary code
rule ::= wildcard|variable|constructor_pattern| or_pattern ;;
wildcard ::= "_"
variable ::= identifier
constructor_pattern ::= constructor (rule|ε) (assignment|ε)
constructor ::= int|float|char|string|bool |unit|record|exn|objects|ref |list|tuple|array|variant|parameterized_variant ;; data types
or_pattern ::= rule ("|" wildcard|variable|constructor_pattern)+
condition ::= "when" bexpr
assignment ::= "as" id
bexpr ::= _code ;; arbitrary code

The source program is parsed using the ocaml-compiler-libs library. The result of parsing, when successful, results in a list of clauses and a list of type declarations. Every clause consists of three objects: a left-hand-side that is the kind of pattern expressed, an option guard and a right-hand-side expression. Patterns are encoded in the following way:

pattern type
_ Wildcard
p₁ as x Assignment
c(p₁,p₂,…,pₙ) Constructor
(p₁| p₂) Orpat

Once parsed, the type declarations and the list of clauses are encoded in the form of a matrix that is later evaluated using a matrix decomposition algorithm.

Patterns are of the form

pattern type of pattern
_ wildcard
x variable
c(p₁,p₂,…,pₙ) constructor pattern
(p₁| p₂) or-pattern

The pattern p matches a value v, written as p ≼ v, when one of the following rules apply

_ v ∀v
x v ∀v
(p₁ | p₂) v iff p₁ ≼ v or p₂ ≼ v
c(p₁, p₂, …, pₐ) c(v₁, v₂, …, vₐ) iff (p₁, p₂, …, pₐ) ≼ (v₁, v₂, …, vₐ)
(p₁, p₂, …, pₐ) (v₁, v₂, …, vₐ) iff pᵢ ≼ vᵢ ∀i ∈ [1..a]

When a value v matches pattern p we say that v is an instance of p.

During compilation by the translators, expressions at the right-hand-side are compiled into Lambda code and are referred as lambda code actions lᵢ.

We define the pattern matrix P as the matrix |m x n| where m bigger or equal than the number of clauses in the source program and n is equal to the arity of the constructor with the gratest arity.

\begin{equation*} P = \begin{pmatrix} p_{1,1} & p_{1,2} & \cdots & p_{1,n} \\ p_{2,1} & p_{2,2} & \cdots & p_{2,n} \\ \vdots & \vdots & \ddots & \vdots \\ p_{m,1} & p_{m,2} & \cdots & p_{m,n} ) \end{pmatrix} \end{equation*}

every row of P is called a pattern vector $\vec{p_i}$ = (p₁, p₂, …, pₙ); In every instance of P pattern vectors appear normalized on the length of the longest pattern vector. Considering the pattern matrix P we say that the value vector $\vec{v}$ = (v₁, v₂, …, vᵢ) matches the pattern vector pᵢ in P if and only if the following two conditions are satisfied:

  • pi,1, pi,2, ⋯, pi,n ≼ (v₁, v₂, …, vᵢ)
  • ∀j < i pj,1, pj,2, ⋯, pj,n ⋠ (v₁, v₂, …, vᵢ)

We can define the following three relations with respect to patterns:

  • Pattern p is less precise than pattern q, written p ≼ q, when all instances of q are instances of p
  • Pattern p and q are equivalent, written p ≡ q, when their instances are the same
  • Patterns p and q are compatible when they share a common instance

Wit the support of two auxiliary functions

  • tail of an ordered family

    tail((xᵢ)i ∈ I) := (xᵢ)i ≠ min(I)
  • first non-⊥ element of an ordered family

    First((xᵢ)ⁱ) := ⊥ \quad \quad \quad if ∀i, xᵢ = ⊥
    First((xᵢ)ⁱ) := x_min{i | xᵢ ≠ ⊥} \quad if ∃i, xᵢ ≠ ⊥

we now define what it means to run a pattern row against a value vector of the same length, that is (pᵢ)ⁱ(vᵢ)ⁱ

pᵢ vᵢ resultpat
(∅) []
(_, tail(pᵢ)ⁱ) (vᵢ) tail(pᵢ)ⁱ(tail(vᵢ)ⁱ)
(x, tail(pᵢ)ⁱ) (vᵢ) σ[x↦v₀] if tail(pᵢ)ⁱ(tail(vᵢ)ⁱ) = σ
(K(qⱼ)ʲ, tail(pᵢ)ⁱ) (K(v'ⱼ)ʲ,tail(vⱼ)ʲ) ((qⱼ)ʲ + tail(pᵢ)ⁱ)((v'ⱼ)ʲ + tail(vᵢ)ⁱ)
(K(qⱼ)ʲ, tail(pᵢ)ⁱ) (K'(v'ₗ)ˡ,tail(vⱼ)ʲ) ⊥ if K ≠ K'
(q₁|q₂, tail(pᵢ)ⁱ) (vᵢ)ⁱ First((q₁,tail(pᵢ)ⁱ)(vᵢ)ⁱ, (q₂,tail(pᵢ)ⁱ)(vᵢ)ⁱ)

A source program tₛ is a collection of pattern clauses pointing to bb terms. Running a program tₛ against an input value vₛ, written tₛ(vₛ) produces a result r belonging to the following grammar:

tₛ ::= (p → bb)i∈I
tₛ(vₛ) → r
r ::= guard list * (Match bb | NoMatch | Absurd)
\begin{comment} TODO: understand how to explain guard \end{comment}

We can define what it means to run an input value vₛ against a source program tₛ:

tₛ(vₛ) := ⊥ \quad \quad \quad if ∀i, pᵢ(vₛ) = ⊥
First((xᵢ)ⁱ) := x_min{i | xᵢ ≠ ⊥} \quad if ∃i, xᵢ ≠ ⊥
tₛ(vₛ) = Absurd if bb_min{pᵢ → bbᵢ | pᵢ(vₛ) ≠ ⊥} = refutation clause
tₛ(vₛ) = Match bb_min{pᵢ → bbᵢ | pᵢ(vₛ) ≠ ⊥}

[ … ] Big part that I think doesn't need revision from you [ … ]

In our prototype we make use of accessors to encode stored values.

\begin{minipage}{0.6\linewidth}

\begin{verbatim} let value = 1 :: 2 :: 3 :: [] (* that can also be written *) let value = [] |> List.cons 3 |> List.cons 2 |> List.cons 1 \end{verbatim}

\end{minipage} \hfill \begin{minipage}{0.5\linewidth}

\begin{verbatim} (field 0 x) = 1 (field 0 (field 1 x)) = 2 (field 0 (field 1 (field 1 x)) = 3 (field 0 (field 1 (field 1 (field 1 x)) = [] \end{verbatim}

\end{minipage} An \emph{accessor} a represents the access path to a value that can be reached by deconstructing the scrutinee.

a ::= Here | n.a

The above example, in encoded form:

\begin{verbatim} Here = 1 1.Here = 2 1.1.Here = 3 1.1.1.Here = [] \end{verbatim}

In our prototype the source matrix mₛ is defined as follows

SMatrix mₛ := (aⱼ)j∈J, ((pij)j∈J → bbᵢ)i∈I

Source matrices are used to build source decision trees Cₛ. A decision tree is defined as either a Leaf, a Failure terminal or an intermediate node with different children sharing the same accessor a and an optional fallback. Failure is emitted only when the patterns don't cover the whole set of possible input values S. The fallback is not needed when the user doesn't use a wildcard pattern. %%% Give example of thing

Cₛ ::= Leaf bb | Switch(a, (Kᵢ → Cᵢ)i∈S , C?) | Failure | Unreachable
vₛ ::= K(vᵢ)i∈I | n ∈
\begin{comment} Are K and Here clear here? \end{comment}

We say that a translation of a source program to a decision tree is correct when for every possible input, the source program and its respective decision tree produces the same result

∀vₛ, tₛ(vₛ) = 〚tₛ〛ₛ(vₛ)

We define the decision tree of source programs 〚tₛ〛 in terms of the decision tree of pattern matrices 〚mₛ〛 by the following:

〚((pᵢ → bbᵢ)i∈I〛 := 〚(Here), (pᵢ → bbᵢ)i∈I

Decision tree computed from pattern matrices respect the following invariant:

∀v (vᵢ)i∈I = v(aᵢ)i∈I → 〚m〛(v) = m(vᵢ)i∈I for m = ((aᵢ)i∈I, (rᵢ)i∈I)
v(Here) = v
K(vᵢ)ⁱ(k.a) = vₖ(a) if k ∈ [0;n[
\begin{comment} TODO: EXPLAIN \end{comment}

We proceed to show the correctness of the invariant by a case analysys.

Base cases:

  1. [| ∅, (∅ → bbᵢ)ⁱ |] ≡ Leaf bbᵢ where i := min(I), that is a decision tree [|ms|] defined by an empty accessor and empty patterns pointing to blackboxes bbᵢ. This respects the invariant because a source matrix in the case of empty rows returns the first expression and (Leaf bb)(v) := Match bb
  2. [| (aⱼ)ʲ, ∅ |] ≡ Failure

Regarding non base cases: Let's first define some auxiliary functions

  • The index family of a constructor

    Idx(K) := [0; arity(K)[
  • head of an ordered family (we write x for any object here, value, pattern etc.)

    head((xᵢ)i ∈ I) = x_min(I)
  • tail of an ordered family

    tail((xᵢ)i ∈ I) := (xᵢ)i ≠ min(I)
  • head constructor of a value or pattern

    constr(K(xᵢ)ⁱ) = K
    constr(_) = ⊥
    constr(x) = ⊥
  • first non-⊥ element of an ordered family

    First((xᵢ)ⁱ) := ⊥ \quad \quad \quad if ∀i, xᵢ = ⊥
    First((xᵢ)ⁱ) := x_min{i | xᵢ ≠ ⊥} \quad if ∃i, xᵢ ≠ ⊥
  • definition of group decomposition:
let constrs((pᵢ)i ∈ I) = { K | K = constr(pᵢ), i ∈ I }
let Groups(m) where m = ((aᵢ)ⁱ ((pᵢⱼ)ⁱ → eⱼ)ⁱʲ) =
\quad \quad let (Kₖ)ᵏ = constrs(pᵢ₀)ⁱ in
\quad \quad ( Kₖ →
\quad \quad \quad \quad ((a₀.ₗ)ˡ + tail(aᵢ)ⁱ)
\quad \quad \quad \quad (
\quad \quad \quad \quad if pₒⱼ is Kₖ(qₗ) then
\quad \quad \quad \quad \quad \quad (qₗ)ˡ + tail(pᵢⱼ)ⁱ → eⱼ
\quad \quad \quad \quad if pₒⱼ is _ then
\quad \quad \quad \quad \quad \quad (_)ˡ + tail(pᵢⱼ)ⁱ → eⱼ
\quad \quad \quad \quad else ⊥
\quad \quad \quad \quad )ʲ
\quad \quad ), (
\quad \quad \quad \quad tail(aᵢ)ⁱ, (tail(pᵢⱼ)ⁱ → eⱼ if p₀ⱼ is _ else ⊥)ʲ
\quad \quad )

Groups(m) is an auxiliary function that decomposes a matrix m into submatrices, according to the head constructor of their first pattern. Groups(m) returns one submatrix m_r for each head constructor K that occurs on the first row of m, plus one "wildcard submatrix" mwild that matches on all values that do not start with one of those head constructors. Intuitively, m is equivalent to its decomposition in the following sense: if the first pattern of an input vector (v_i)^i starts with one of the head constructors Kₖ, then running (v_i)^i against m is the same as running it against the submatrix mKₖ; otherwise (its head constructor ∉ (Kₖ)ᵏ) it is equivalent to running it against the wildcard submatrix.

We formalize this intuition as follows

Lemma (Groups):

Let m be a matrix with

Groups(m) = (k_r → m_r)^k, mwild

For any value vector $(v_i)^l$ such that $v_0 = k(v'_l)^l$ for some constructor k, we have:

if k = kₖ \text{ for some k then}
\quad m(vᵢ)ⁱ = mₖ((vl')ˡ + (vi)i∈I\DZ)
\text{else}
\quad m(vᵢ)ⁱ = mwild(vᵢ)i∈I\DZ
\begin{comment} TODO: fix \{0} \end{comment}

Proof:

Let m be a matrix ((aᵢ)ⁱ, ((pᵢⱼ)ⁱ → eⱼ)ʲ) with

Groups(m) = (Kₖ → mₖ)ᵏ, mwild

Below we are going to assume that m is a simplified matrix such that the first row does not contain an or-pattern or a binding to a variable.

Let (vᵢ)ⁱ be an input matrix with v₀ = Kᵥ(v'l)ˡ for some constructor Kᵥ. We have to show that:

  • if Kₖ = Kᵥ for some Kₖ ∈ constrs(p₀ⱼ)ʲ, then m(vᵢ)ⁱ = mₖ((v'ₗ)ˡ + tail(vᵢ)ⁱ)
  • otherwise m(vᵢ)ⁱ = mwild(tail(vᵢ)ⁱ)

Let us call (rₖⱼ) the j-th row of the submatrix mₖ, and rⱼwild the j-th row of the wildcard submatrix mwild.

Our goal contains same-behavior equalities between matrices, for a fixed input vector (vᵢ)ⁱ. It suffices to show same-behavior equalities between each row of the matrices for this input vector. We show that for any j,

  • if Kₖ = Kᵥ for some Kₖ ∈ constrs(p₀ⱼ)ʲ, then

    (pᵢⱼ)ⁱ(vᵢ)ⁱ = rₖⱼ((v'ₗ)ˡ + tail(vᵢ)ⁱ
  • otherwise

    (pᵢⱼ)ⁱ(vᵢ)ⁱ = rⱼwild tail(vᵢ)ⁱ

In the first case (Kᵥ is Kₖ for some Kₖ ∈ constrs(p₀ⱼ)ʲ), we have to prove that

(pᵢⱼ)ⁱ(vᵢ)ⁱ = rₖⱼ((v'ₗ)ˡ + tail(vᵢ)ⁱ

By definition of mₖ we know that rₖⱼ is equal to

if pₒⱼ is Kₖ(qₗ) then
\quad (qₗ)ˡ + tail(pᵢⱼ)ⁱ → eⱼ
if pₒⱼ is _ then
\quad (_)ˡ + tail(pᵢⱼ)ⁱ → eⱼ
else ⊥
\begin{comment} Maybe better as a table? | pₒⱼ | rₖⱼ | |--------+---------------------------| | Kₖ(qₗ) | (qₗ)ˡ +++ tail(pᵢⱼ)ⁱ → eⱼ | | _ | (_)ˡ +++ tail(pᵢⱼ)ⁱ → eⱼ | | else | ⊥ | \end{comment}

By definition of (pᵢ)ⁱ(vᵢ)ⁱ we know that (pᵢⱼ)ⁱ(vᵢ) is equal to

(K(qⱼ)ʲ, tail(pᵢⱼ)ⁱ) (K(v'ₗ)ˡ,tail(vᵢ)ⁱ) := ((qⱼ)ʲ + tail(pᵢⱼ)ⁱ)((v'ₗ)ˡ + tail(vᵢ)ⁱ)
(_, tail(pᵢⱼ)ⁱ) (vᵢ) := tail(pᵢⱼ)ⁱ(tail(vᵢ)ⁱ)
(K(qⱼ)ʲ, tail(pᵢⱼ)ⁱ) (K'(v'ₗ)ˡ,tail(vⱼ)ʲ) := ⊥ if K ≠ K'

We prove this first case by a second case analysis on p₀ⱼ.

TODO

In the second case (Kᵥ is distinct from Kₖ for all Kₖ ∈ constrs(pₒⱼ)ʲ), we have to prove that

(pᵢⱼ)ⁱ(vᵢ)ⁱ = rⱼwild tail(vᵢ)ⁱ

TODO

Target translation

The target program of the following general form is parsed using a parser generated by Menhir, a LR(1) parser generator for the OCaml programming language. Menhir compiles LR(1) a grammar specification, in this case a subset of the Lambda intermediate language, down to OCaml code. This is the grammar of the target language (TODO: check menhir grammar)

start ::= sexpr
sexpr ::= variable | string | "(" special_form ")"
string ::= "\"" identifier "\"" ;; string between doublequotes
variable ::= identifier
special_form ::= let|catch|if|switch|switch-star|field|apply|isout
let ::= "let" assignment sexpr ;; (assignment sexpr)+ outside of pattern match code
assignment ::= "function" variable variable+ ;; the first variable is the identifier of the function
field ::= "field" digit variable
apply ::= ocaml_lambda_code ;; arbitrary code
catch ::= "catch" sexpr with sexpr
with ::= "with" "(" label ")"
exit ::= "exit" label
switch-star ::= "switch*" variable case*
switch::= "switch" variable case* "default:" sexpr
case ::= "case" casevar ":" sexpr
casevar ::= ("tag"|"int") integer
if ::= "if" bexpr sexpr sexpr
bexpr ::= "(" ("!="|"="\vert{}">"|"<="|">"|"<") sexpr digit | field ")"
label ::= integer

The prototype doesn't support strings.

The AST built by the parser is traversed and evaluated by the symbolic execution engine. Given that the target language supports jumps in the form of "catch - exit" blocks the engine tries to evaluate the instructions inside the blocks and stores the result of the partial evaluation into a record. When a jump is encountered, the information at the point allows to finalize the evaluation of the jump block. In the environment the engine also stores bindings to values and functions. Integer additions and subtractions are simplified in a second pass. The result of the symbolic evaluation is a target decision tree Cₜ

Cₜ ::= Leaf bb | Switch(a, (πᵢ → Cᵢ)i∈S , C?) | Failure
vₜ ::= Cell(tag ∈ , (vᵢ)i∈I) | n ∈

Every branch of the decision tree is "constrained" by a domain

Domain π = { n|n∈ x n|n∈Tag⊆ }

Intuitively, the π condition at every branch tells us the set of possible values that can "flow" through that path. π conditions are refined by the engine during the evaluation; at the root of the decision tree the domain corresponds to the set of possible values that the type of the function can hold. C? is the fallback node of the tree that is taken whenever the value at that point of the execution can't flow to any other subbranch. Intuitively, the πfallback condition of the C? fallback node is

πfallback = ¬\bigcuplimitsi∈Iπᵢ

The fallback node can be omitted in the case where the domain of the children nodes correspond to set of possible values pointed by the accessor at that point of the execution

domain(vₛ(a)) = \bigcuplimitsi∈Iπᵢ

We say that a translation of a target program to a decision tree is correct when for every possible input, the target program and its respective decision tree produces the same result

∀vₜ, tₜ(vₜ) = 〚tₜ〛ₜ(vₜ)

Equivalence checking

The equivalence checking algorithm takes as input a domain of possible values \emph{S} and a pair of source and target decision trees and in case the two trees are not equivalent it returns a counter example. Our algorithm respects the following correctness statement:

\begin{comment} TODO: we have to define what \coversTEX mean for readers to understand the specifications (or we use a simplifying lie by hiding \coversTEX in the statements). \end{comment} \begin{align*} \EquivTEX S {C_S} {C_T} \emptyqueue = \YesTEX \;\land\; \coversTEX {C_T} S & \implies \forall v_S \approx v_T \in S,\; C_S(v_S) = C_T(v_T) \\ \EquivTEX S {C_S} {C_T} \emptyqueue = \NoTEX {v_S} {v_T} \;\land\; \coversTEX {C_T} S & \implies v_S \approx v_T \in S \;\land\; C_S(v_S) \neq C_T(v_T) \end{align*}

Our equivalence-checking algorithm $\EquivTEX S {C_S} {C_T} G$ is a exactly decision procedure for the provability of the judgment $(\EquivTEX S {C_S} {C_T} G)$, defined below.

\begin{mathpar} \begin{array}{l@{~}r@{~}l} & & \text{\emph{constraint trees}} \\ C & \bnfeq & \Leaf t \\ & \bnfor & \Failure \\ & \bnfor & \Switch a {\Fam i {\pi_i, C_i}} \Cfb \\ & \bnfor & \Guard t {C_0} {C_1} \\ \end{array} \begin{array}{l@{~}r@{~}l} & & \text{\emph{boolean result}} \\ b & \in & \{ 0, 1 \} \\[0.5em] & & \text{\emph{guard queues}} \\ G & \bnfeq & (t_1 = b_1), \dots, (t_n = b_n) \\ \end{array} \begin{array}{l@{~}r@{~}l} & & \text{\emph{input space}} \\ S & \subseteq & \{ (v_S, v_T) \mid \vrel {v_S} {v_T} \} \\ \end{array} \\ \infer{ } {\EquivTEX \emptyset {C_S} {C_T} G} \infer{ } {\EquivTEX S \Failure \Failure \emptyqueue} \infer {\trel {t_S} {t_T}} {\EquivTEX S {\Leaf {t_S}} {\Leaf {t_T}} \emptyqueue} \infer {\forall i,\; \EquivTEX {(S \land a = K_i)} {C_i} {\trim {C_T} {a = K_i}} G \\ \EquivTEX {(S \land a \notin \Fam i {K_i})} \Cfb {\trim {C_T} {a \notin \Fam i {K_i}}} G } {\EquivTEX S {\Switch a {\Fam i {K_i, C_i}} \Cfb} {C_T} G} \begin{comment} % TODO explain somewhere why the judgment is not symmetric: % we avoid defining trimming on source trees, which would % require more expressive conditions than just constructors \end{comment} \infer {C_S \in {\Leaf t, \Failure} \\ \forall i,\; \EquivTEX {(S \land a \in D_i)} {C_S} {C_i} G \\ \EquivTEX {(S \land a \notin \Fam i {D_i})} {C_S} \Cfb G} {\EquivTEX S {C_S} {\Switch a {\Fam i {D_i} {C_i}} \Cfb} G} \infer {\EquivTEX S {C_0} {C_T} {G, (t_S = 0)} \\ \EquivTEX S {C_1} {C_T} {G, (t_S = 1)}} {\EquivTEX S {\Guard {t_S} {C_0} {C_1}} {C_T} G} \infer {\trel {t_S} {t_T} \\ \EquivTEX S {C_S} {C_b} G} {\EquivTEX S {C_S} {\Guard {t_T} {C_0} {C_1}} {(t_S = b), G}} \end{mathpar}

Running a program tₛ or its translation 〚tₛ〛 against an input vₛ produces as a result r in the following way:

( 〚tₛ〛ₛ(vₛ) ≡ Cₛ(vₛ) ) → r
tₛ(vₛ) → r

Likewise

( 〚tₜ〛ₜ(vₜ) ≡ Cₜ(vₜ) ) → r
tₜ(vₜ) → r
result r ::= guard list * (Match blackbox | NoMatch | Absurd)
guard ::= blackbox.

Having defined equivalence between two inputs of which one is expressed in the source language and the other in the target language, vₛ ≃ vₜ, we can define the equivalence between a couple of programs or a couple of decision trees

tₛ ≃ tₜ := ∀vₛ≃vₜ, tₛ(vₛ) = tₜ(vₜ)
Cₛ ≃ Cₜ := ∀vₛ≃vₜ, Cₛ(vₛ) = Cₜ(vₜ)

The result of the proposed equivalence algorithm is Yes or No(vₛ, vₜ). In particular, in the negative case, vₛ and vₜ are a couple of possible counter examples for which the decision trees produce a different result.

In the presence of guards we can say that two results are equivalent modulo the guards queue, written r₁ ≃gs r₂, when:

(gs₁, r₁) ≃gs (gs₂, r₂) ⇔ (gs₁, r₁) = (gs₂ ++ gs, r₂)

We say that Cₜ covers the input space S, written covers(Cₜ, S) when every value vₛ∈S is a valid input to the decision tree Cₜ. (TODO: rephrase) Given an input space S and a couple of decision trees, where the target decision tree Cₜ covers the input space S we can define equivalence:

equiv(S, Cₛ, Cₜ, gs) = Yes ∧ covers(Cₜ, S) → ∀vₛ≃vₜ ∈ S, Cₛ(vₛ) ≃gs Cₜ(vₜ)

Similarly we say that a couple of decision trees in the presence of an input space S are not equivalent in the following way:

equiv(S, Cₛ, Cₜ, gs) = No(vₛ,vₜ) ∧ covers(Cₜ, S) → vₛ≃vₜ ∈ S ∧ Cₛ(vₛ) ≠gs Cₜ(vₜ)

Corollary: For a full input space S, that is the universe of the target program:

equiv(S, 〚tₛ〛ₛ, 〚tₜ〛ₜ, ∅) = Yes ⇔ tₛ ≃ tₜ
\begin{comment} TODO: put ^i∈I where needed \end{comment}

⊂subsection{The trimming lemma} The trimming lemma allows to reduce the size of a decision tree given an accessor a → π relation (TODO: expand)

∀vₜ ∈ (a→π), Cₜ(vₜ) = Ct/a→π(vₜ)

We prove this by induction on Cₜ:

  • Cₜ = Leafbb: when the decision tree is a leaf terminal, the result of trimming on a Leaf is the Leaf itself

    Leafbb/a→π(v) = Leafbb(v)
  • The same applies to Failure terminal

    Failure/a→π(v) = Failure(v)
  • When Cₜ = Switch(b, (π→Cᵢ)ⁱ)/a→π then we look at the accessor a of the subtree Cᵢ and we define πᵢ' = πᵢ if a≠b else πᵢ∩π Trimming a switch node yields the following result:

    Switch(b, (π→Cᵢ)i∈I)/a→π := Switch(b, (π'ᵢ→Ci/a→π)i∈I)
\begin{comment} TODO: understand how to properly align lists check that every list is aligned \end{comment}

For the trimming lemma we have to prove that running the value vₜ against the decision tree Cₜ is the same as running vₜ against the tree Ctrim that is the result of the trimming operation on Cₜ

Cₜ(vₜ) = Ctrim(vₜ) = Switch(b, (πᵢ'→Ci/a→π)i∈I)(vₜ)

We can reason by first noting that when vₜ∉(b→πᵢ)ⁱ the node must be a Failure node. In the case where ∃k | vₜ∈(b→πₖ) then we can prove that

Ck/a→π(vₜ) = Switch(b, (πᵢ'→Ci/a→π)i∈I)(vₜ)

because when a ≠ b then πₖ'= πₖ and this means that vₜ∈πₖ' while when a = b then πₖ'=(πₖ∩π) and vₜ∈πₖ' because:

  • by the hypothesis, vₜ∈π
  • we are in the case where vₜ∈πₖ

So vₜ ∈ πₖ' and by induction

Cₖ(vₜ) = Ck/a→π(vₜ)

We also know that ∀vₜ∈(b→πₖ) → Cₜ(vₜ) = Cₖ(vₜ) By putting together the last two steps, we have proven the trimming lemma.

\begin{comment} TODO: what should I say about covering??? I swap π and π' Covering lemma: ∀a,π covers(Cₜ,S) → covers(C_{t/a→π}, (S∩a→π)) Uᵢπⁱ ≈ Uᵢπ'∩(a→π) ≈ (Uᵢπ')∩(a→π) %% %%%%%%% Also: Should I swap π and π' ? \end{comment}

⊂subsection{Equivalence checking} The equivalence checking algorithm takes as parameters an input space S, a source decision tree Cₛ and a target decision tree Cₜ:

equiv(S, Cₛ, Cₜ) → Yes | No(vₛ, vₜ)

When the algorithm returns Yes and the input space is covered by Cₛ we can say that the couple of decision trees are the same for every couple of source value vₛ and target value vₜ that are equivalent.

\begin{comment} Define "covered" Is it correct to say the same? How to correctly distinguish in words ≃ and = ? \end{comment}
equiv(S, Cₛ, Cₜ) = Yes and cover(Cₜ, S) → ∀ vₛ ≃ vₜ∈S ∧ Cₛ(vₛ) = Cₜ(vₜ)

In the case where the algorithm returns No we have at least a couple of counter example values vₛ and vₜ for which the two decision trees outputs a different result.

equiv(S, Cₛ, Cₜ) = No(vₛ,vₜ) and cover(Cₜ, S) → ∀ vₛ ≃ vₜ∈S ∧ Cₛ(vₛ) ≠ Cₜ(vₜ)

We define the following

Forall(Yes) = Yes
Forall(Yes::l) = Forall(l)
Forall(No(vₛ,vₜ)::_) = No(vₛ,vₜ)

There exists and are injective:

int(k) ∈ (arity(k) = 0)
tag(k) ∈ (arity(k) > 0)
π(k) = {n| int(k) = n} x {n| tag(k) = n}

where k is a constructor.

\begin{comment} TODO: explain: ∀v∈a→π, C_{/a→π}(v) = C(v) \end{comment}

We proceed by case analysis:

\begin{comment} I start numbering from zero to leave the numbers as they were on the blackboard, were we skipped some things I think the unreachable case should go at the end. \end{comment}
  1. in case of unreachable:
Cₛ(vₛ) = Absurd(Unreachable) ≠ Cₜ(vₜ) ∀vₛ,vₜ
  1. In the case of an empty input space

    equiv(∅, Cₛ, Cₜ) := Yes

    and that is trivial to prove because there is no pair of values (vₛ, vₜ) that could be tested against the decision trees. In the other subcases S is always non-empty.

  2. When there are Failure nodes at both sides the result is Yes:

    equiv(S, Failure, Failure) := Yes

    Given that ∀v, Failure(v) = Failure, the statement holds.

  3. When we have a Leaf or a Failure at the left side:

    equiv(S, Failure as Cₛ, Switch(a, (πᵢ → Cₜᵢ)i∈I)) := Forall(equiv( S∩a→π(kᵢ)), Cₛ, Cₜᵢ)i∈I)
    equiv(S, Leaf bbₛ as Cₛ, Switch(a, (πᵢ → Cₜᵢ)i∈I)) := Forall(equiv( S∩a→π(kᵢ)), Cₛ, Cₜᵢ)i∈I)

    Our algorithm either returns Yes for every sub-input space Sᵢ := S∩(a→π(kᵢ)) and subtree Cₜᵢ

    equiv(Sᵢ, Cₛ, Cₜᵢ) = Yes ∀i

    or we have a counter example vₛ, vₜ for which

    vₛ≃vₜ∈Sₖ ∧ cₛ(vₛ) ≠ Cₜₖ(vₜ)

    then because

    vₜ∈(a→πₖ) → Cₜ(vₜ) = Cₜₖ(vₜ) ,
    vₛ≃vₜ∈S ∧ Cₛ(vₛ)≠Cₜ(vₜ)

    we can say that

    equiv(Sᵢ, Cₛ, Cₜᵢ) = No(vₛ, vₜ) for some minimal k∈I
  4. When we have a Switch on the right we define πfallback as the domain of values not covered but the union of the constructors kᵢ

    πfallback = ¬\bigcuplimitsi∈Iπ(kᵢ)

    Our algorithm proceeds by trimming

    equiv(S, Switch(a, (kᵢ → Cₛᵢ)i∈I, Csf), Cₜ) :=
    Forall(equiv( S∩(a→π(kᵢ)i∈I), Cₛᵢ, Ct/a→π(kᵢ))i∈I + equiv(S∩(a→πₙ), Cₛ, C_{a→πfallback}))

    The statement still holds and we show this by first analyzing the Yes case:

    Forall(equiv( S∩(a→π(kᵢ)i∈I), Cₛᵢ, Ct/a→π(kᵢ))i∈I = Yes

    The constructor k is either included in the set of constructors kᵢ:

    k | k∈(kᵢ)ⁱ ∧ Cₛ(vₛ) = Cₛᵢ(vₛ)

    We also know that

    (1) Cₛᵢ(vₛ) = Ct/a→πᵢ(vₜ)
    (2) CT/a→πᵢ(vₜ) = Cₜ(vₜ)

    (1) is true by induction and (2) is a consequence of the trimming lemma. Putting everything together:

    Cₛ(vₛ) = Cₛᵢ(vₛ) = CT/a→πᵢ(vₜ) = Cₜ(vₜ)

    When the k∉(kᵢ)ⁱ [TODO]

    The auxiliary Forall function returns No(vₛ, vₜ) when, for a minimum k,

    equiv(Sₖ, Cₛₖ, CT/a→πₖ = No(vₛ, vₜ)

    Then we can say that

    Cₛₖ(vₛ) ≠ Ct/a→πₖ(vₜ)

    that is enough for proving that

    Cₛₖ(vₛ) ≠ (Ct/a→πₖ(vₜ) = Cₜ(vₜ))