• No results found

Tracing semantics

In document Interactive functional programming (Page 80-85)

We now define a tracing semantics for the reference language just presented. The rules, given in Figure4.6, are identical to those for ⇓ref, except that they construct a trace as well as a value. A terminating tracing evaluation for an expression Γ ⊢ e : τ in environment ρ for Γ, written ρ, e ⇓ v, T , yields both a value v : τ and a trace Γ ⊢ T : τ describing how v was computed. Before explaining the tracing semantics, we dispense with a few preliminary properties. Where the proofs are straightforward inductions or closely analogous to those for the reference language they are omitted. First, tracing evaluation is deterministic.

Lemma 3 (Determinism of⇓). If ρ, e ⇓ v, T and ρ, e ⇓ v′, Tthenv = vandT = T. The tracing semantics and the reference semantics agree on values.

Γ ⊢ T : τ Γ ⊢ () : 1 Γ ⊢ x : τ x : τ ∈ Γ Γ ⊢ c : b c : b ∈ Σ Γ ⊢ T1: b1 ⊢ c1: b1 Γ ⊢ T2: b2 ⊢ c2: b2 Γ ⊢ T1⊕c1,c2 T2: b ⊕ : b1× b2→ b ∈ Σ Γ, f : τ1→ τ2, x : τ1⊢ e : τ2 Γ ⊢ fun f (x).e : τ1→ τ2 Γ ⊢ T1: τ1→ τ2 Γ ⊢ T2: τ1 Γ′, f : τ1→ τ2, x : τ1⊢ T : τ2 Γ ⊢ T1T2⊲ hvars(Γ′), fun f (x).T i : τ2 Γ ⊢ T1: τ1 Γ ⊢ T2: τ2 Γ ⊢ (T1, T2) : τ1× τ2 Γ ⊢ T : τ1× τ2 Γ ⊢ fst T : τ1 Γ ⊢ T : τ1× τ2 Γ ⊢ snd T : τ2 Γ ⊢ T : τ1 Γ ⊢ inl T : τ1+ τ2 Γ ⊢ T : τ2 Γ ⊢ inr T : τ1+ τ2 Γ ⊢ T : τ1+ τ2 Γ, x1: τ1⊢ T1: τ Γ, x2: τ2⊢ e2: τ

Γ ⊢ case T of {inl(x1).T1; inr(x2).e2} : τ

Γ ⊢ T : τ1+ τ2 Γ, x1: τ1⊢ e1: τ Γ, x2: τ2⊢ T2 : τ

Γ ⊢ case T of {inl(x1).e1; inr(x2).T2} : τ

Γ ⊢ T : µα.τ Γ ⊢ unroll T : τ [µα.τ /α] Γ ⊢ T : τ [µα.τ /α]

Γ ⊢ roll T : µα.τ

Figure 4.5 Typing rules for traces

Theorem 1. ρ, e ⇓ref v ⇐⇒ ∃T.ρ, e ⇓ v, T Tracing evaluation is type-preserving.

Lemma 4 (Type preservation for⇓). If Γ ⊢ e : τ and Γ ⊢ ρ with ρ, e ⇓ v, T , then ⊢ v : τ and Γ ⊢ T : τ . We can now explain the tracing evaluation judgement. The idea is that tracing evaluation equips every value with a trace which “explains” it. The trace of a variable x is just the corresponding trace form x; as stated in Lemma4, the trace of Γ ⊢ e : τ is not closed but is instead also typed in Γ. The traces of other nullary expressions are just the corresponding nullary trace form. For non-nullary forms, such as projections, pairs, and case expressions, the general pattern is to produce a trace which looks like the original expression except that any executed sub-expressions have been inflated into their traces. For example a trace of the form case T of {inl(x1).T1; inr(x2).e2} records the scrutinee and the taken branch unrolled into their respective executions T and T1. The non-taken branch e2is kept in the trace to be consistent with our notion of a trace as an unrolled expression.

Tracing a primitive operation e1⊕ e2records not only the traces T1and T2of the operands, but also their values c1and c2. As mentioned earlier, this anticipates the backward-slicing technique presented in the next chapter (§5.3), which relies on being able to back-propagate neededness information through each step in the

ρ, e ⇓ v, T

ρ, x ⇓ ρ(x), x ρ, c ⇓ c, c

ρ, e1⇓ c1, T1 ρ, e2⇓ c2, T2

ρ, e1⊕ e2⇓ c1⊕ cˆ 2, T1⊕c1,c2 T2

ρ, fun f (x).e ⇓ hρ, fun f (x).ei, fun f (x).e ρ, e1⇓ v1, T1 ρ, e2⇓ v2, T2 ρ′[f 7→ v1][x 7→ v2], e ⇓ v, T ρ, e1e2⇓ v, T1T2⊲ hvars(ρ′), fun f (x).T i v1= hρ′, fun f (x).ei ρ, e1⇓ v1, T1 ρ, e2⇓ v2, T2 ρ, (e1, e2) ⇓ (v1, v2), (T1, T2) ρ, e ⇓ (v1, v2), T ρ, fst e ⇓ v1, fst T ρ, e ⇓ (v1, v2), T ρ, snd e ⇓ v2, snd T ρ, e ⇓ v, T ρ, inl e ⇓ inl v, inl T

ρ, e ⇓ v, T ρ, inr e ⇓ inr v, inr T ρ, e ⇓ inl v1, T ρ[x17→ v1], e1⇓ v, T1

ρ, case e of {inl(x1).e1; inr(x2).e2} ⇓ v, case T of {inl(x1).T1; inr(x2).e2}

ρ, e ⇓ inr v2, T ρ[x27→ v2], e2⇓ v, T2

ρ, case e of {inl(x1).e1; inr(x2).e2} ⇓ v, case T of {inl(x1).e1; inr(x2).T2}

ρ, e ⇓ v, T ρ, roll e ⇓ roll v, roll T

ρ, e ⇓ roll v, T ρ, unroll e ⇓ v, unroll T

computation. The value annotations are used in the slicing of primitive operations, which are not amenable to this propagation technique.

Aside from the values of primitive operands, a trace has more content than its original expression only where functions are called. Tracing an application e1 e2 yields the application trace T1 T2⊲ hvars(ρ′), funf (x).T i, where T1and T2record the evaluation of e1and e2, and T records the evaluation of the body of the closure v1 = hρ′, fun f (x).ei. Here, vars(−) is overloaded to mean the function which discards the value bindings from ρ′, leaving only its variables. This ensures that the overall application trace is well- typed. A practical implementation might retain the values for visualisation purposes; at present LambdaCalc does not visualise closure environments.

In Related Work (§3.1), we proposed that traces should record the behaviour of a program according to a specific operational model. This suggests that a correctness criterion for our tracing semantics would relate traces to derivation trees in the reference semantics. However, we take a different approach which involves formalising our notion of a trace explaining a value. Our tracing semantics is correct in that it yields values equipped with traces which do indeed explain them in this technical sense (Theorem4, §5.3.2). The formal notion of “explanation” is fundamentally tied to slicing, and so we defer the statement and proof of this theorem to the next chapter.

5

Slicing Computation

Computations can usually be broken down into parallel execution flows that are at least somewhat inde- pendent, regardless of whether implementations actually take advantage of this. These parallel strands of execution are called “slices” because they cut vertically through the dependency structure of the computa- tion. Being able to view slices interactively allows a programmer to explore the relationship between parts of the output and parts of the program, as we saw in Chapter2, §§2.5and2.6. In this thesis we consider only dynamic slicing, i.e. queries of this nature which pertain to a specific execution.

Dynamic slicing questions can be asked in two directions. Forward slicing questions concern the parts of the output which must be deleted as consequence of deleting some part of the program. Backward slicing questions concern the parts of the program which may be deleted as a consequence of deleting some part of the output. In this chapter we show how such questions can be supported in a way that both complements and utilises the reified computations introduced in the previous chapter.

In §5.1we make precise the notion of a “part” of a program or value. We start by extending the syntax with holes, written , in the style of Biswas [Bis97]. A hole is a no-op expression, inhabiting every type, which evaluates to itself. Expressions and values generalise to partial expressions and partial values which may contain holes, and which are partially ordered. The order has e ⊑ e′whenever we can obtain e from e′ by replacing some sub-expressions by holes; we say that e is a prefix of e′.

In §5.2 we model forward dynamic slicing, for a fixed program, as the execution of some prefix of the program to obtain a prefix of its output, where holes represent lack of availability. Our key contribution here is to show that this formulation of forward slicing uniquely determines the backward dynamic slicing problem for the same computation. What we show is that backward slicing, where holes represent lack of demand, is the problem of calculating the lower adjoint of the function which computes forward slices. To account for our construal of slicing questions as changes in availability (forward slicing) or demand (backward slicing), we introduce the idea of a differential program slice, the pair of a partial program and a smaller one. Differential slices enable more fine-grained Q&A about the relationship between input and output.

In §5.3we give an algorithm called unevaluation which efficiently calculates backward slices, making use of the computational history recorded in a trace. Whereas evaluation unrolls a program, unevaluation rolls it back up again, recovering enough of the original program to be able to compute the required prefix of its output. Unevaluation only computes program slices; in §5.4, we give an additional algorithm which slices a trace into a partial trace retaining just enough information to “explain” the partial output.

In document Interactive functional programming (Page 80-85)