• Nenhum resultado encontrado

Abstract Interpretation as a Programming Language

N/A
N/A
Protected

Academic year: 2017

Share "Abstract Interpretation as a Programming Language"

Copied!
21
0
0

Texto

(1)

A. Banerjee, O. Danvy, K.-G. Doh, J. Hatcliff (Eds): David A. Schmidt’s 60th Birthday Festschrift

EPTCS 129, 2013, pp. 84–104, doi:10.4204/EPTCS.129.7

c

Mads Rosendahl

This work is licensed under the Creative Commons Attribution License. Mads Rosendahl∗

Computer Science, Roskilde University Roskilde, Denmark

madsr@ruc.dk

In David Schmidts PhD work he explored the use of denotational semantics as a programming lan-guage. It was part of an effort to not only treat formal semantics as specifications but also as inter-preters and input to compiler generators. The semantics itself can be seen as a program and one may examine different programming styles and ways to represent states.

Abstract interpretation is primarily a technique for derivation and specification of program anal-ysis. As with denotational semantics we may also view abstract interpretations as programs and examine the implementation. The main focus in this paper is to show that results from higher-order strictness analysis may be used more generally as fixpoint operators for higher-order functions over lattices and thus provide a technique for immediate implementation of a large class of abstract inter-pretations. Furthermore, it may be seen as a programming paradigm and be used to write programs in a circular style.

1

Introduction

The transition from a denotational semantics to an interpreter in a functional language is often fairly straightforward and may even be used to check a specification for mistakes. An abstract interpretation is often less obvious to realise and the path from a specification to a prototype implementation may re-quire specialised techniques for finding fixpoints in domains. One of the obstacles is that semantics are often written in a higher-order style which is not easy to implement using traditional fixpoint iteration techniques. There seem to be two major approaches to making generalised systems for abstract inter-pretations. One can make a system for a specific language which can then be analysed using a selection of abstract interpretations [3]. In this way one may represent control flow independently of the analysis and typically avoid some higher-order dependencies in the interpretation. An alternative approach is to use attribute grammars as a specification language for abstract interpretations [13]. This provides a more restricted framework for specifying semantics so that dependencies in the semantics are represented as synthesised and inherited attributes and where the semantics can be interpreted using first or second-order fixpoint iteration.

We present a method for demand-driven fixpoint iteration on domains with higher-order functions. The fixpoint operation can be used in the implementation of a large class of abstract interpretations and is not restricted to analyses that can be implemented using first or second-order fixpoint iteration. The technique uses partial function graphs to represent higher-order objects. The main problem in finding fixpoints for higher-order functions is to establish a notion ofneedednessso as to restrict the iteration to those parts of the function that may influence the result. The central principle is to instrument a higher-order function with neededness information so that functional arguments can be tabulated for the needed part at the time of the call and, thus, before they are actually needed in the functional. This is done here

The research leading to these results has received funding from the European Union Seventh Framework Programme

(2)

through a uniform extension of the domain of values with need information. The result is an iteration strategy which will terminate if the base domains are finite.

The technique is a generalisation of the minimal function graph approach for fixpoint iteration of second-order functionals. For second-order functionals a demand-driven evaluation of the value of the fixpoint for a given argument will tabulate those parts of the fixpoint that may be called, directly or indirectly, from the initial argument.

For a number of years the implementation of higher-order strictness analysis on non-flat domains [5, 16, 25] has been a benchmark for how well different techniques perform. In a previous work [18] we constructed a strictness analysis for Haskell. It had some very attractive properties both regarding speed and precision. In this paper we extend that work and show that the underlying fixpoint evaluation method is not restricted to strictness analysis but can be used as a more general fixpoint operation on higher-order domains.

The use of a general fixpoint operator makes it possible to explore circularity as a programming paradigm. Functional definitions can be circular in a number of different ways. Functions may be defined recursively in terms of themselves. We may have programs that use circular data structures, and functions may have circular argument dependencies. Circular data structures or circular dependencies can be used as a powerful algorithmic principle in a lazy language. The phrase “circular programs” was coined by R. Bird [4] who showed how several passes over a data structure can be expressed in a single function, provided one may pass parts of the result of the function as an argument to the function.

In the field of attribute grammars, circularity testing plays an important role. The traditional approach is to disallow circular attribute grammars but under certain restrictions it is well-defined and useful to allow circularity. A number of evaluators for circular attribute grammars have been reported in the literature [11, 30].

The fixpoint operation is currently being used to make various resource related analyses for embed-ded software systems, where we both want to change between different languages and analyse different properties.

2

Higher-order fixpoint iteration

There are a number of challenges and pitfalls in implementing fixpoint iteration for higher-order func-tions. We will examine a few of these as a motivation for introducing the demand-driven technique presented later in this paper.

With the extension of strictness analysis to higher-order functions and analysis of lazy lists came examples of simple programs that were surprisingly complicated to analyse. An example examined in numerous papers is thecat function.

foldr[]b=b

foldr f(a:as)b= f a(foldr f as b) cat l=foldr append l[]

When we analyse the strictness of this function we examine a function in the domain[[444]→

(3)

Partial fixpoint iteration. Instead of tabulating a higher-order function for all values one may want to limit the evaluation to fewer arguments and thus work with partial functions. If one is not careful this may lead to a wrong result.

LetG:(D1→D2)→(D1→D2)be continuous and letSi be a sequence of subsets ofD1. We will

now define a sequence of approximationsφi as follows

φ0 =λx.⊥

φi+1=λx.ifxSithenGφix elseφix

The aim is to find a limitφnwhich for some subset ofD1should compute the same function aslfpG. Unfortunately the functionsφiare not guaranteed to be monotonic, nor will the sequence in general

be increasing.

As an example consider the domain{0,1,2}with ordering 0≤1≤2, and the function:

Gφx=φ(φ(x))⊕1 x⊕1 = (x+1)⊓2

and setsSi={1}. The functionsφiare then:

φ0 = [07→0,17→0,27→0]

φ1 = [07→0,17→1,27→0]

φ2 = [07→0,17→2,27→0]

φ3 = [07→0,17→1,27→0]

. . .

The problem is that function application in general is not monotonic. As a consequence a selective re-evaluation of the function on needed arguments does not necessarily form an increasing chain and could result in non-termination of a fixpoint algorithm. Further conditions are needed to guarantee such a sequence to approximate the fixpoint. We will explore such methods in section 3.

Closure-based fixpoint iteration. An alternative to using partial functions could be to describe

func-tional arguments as closures. In some situations this may work and give simple solutions to examples like thecat example above [29, 20]. The technique, however, is not generally applicable as a fixpoint iteration technique as it may lead to infinite sequences of closures.

Consider the functionsg:2→(22)→2andm:2→(22)→22, where2is the two element domain{⊥,⊤}with⊥ ⊑ ⊤, and⊔and⊓as least upper bound and greatest lower bound operations:

gn k =n⊓((k⊤)⊔(g′n(m′n k))) mn k x=k(nx)

These functions are generated as strictness function for the CPS converted factorial function.

g n k = ifn=0thenk1elseg(n−1) (m n k) m n k x =k(nx)

(4)

One approach to fixpoint iteration could be to represent functional values as closures of partial function applications. We will here write such partial applications in angled brackets. The callg′⊤ hbotiwith botx.⊥will lead to the following sequence of calls tog′:

g′⊤ hboti g′⊤ hm′⊤ hbotii g′⊤ hm′⊤ hm′⊤ hbotiii

...

A closure-based semantics may, however, be used as a standard semantics in an abstract interpretation [20] to prove the correctness of a closure analysis. In a similar style minimal function graphs were introduced [19] as an instrumented standard semantics to prove the correctness of constant propagation of an applicative language. It was not introduced as a fixpoint iteration technique.

3

Domains and fixpoints

The theory behind the circular definitions is based on the fixpoint theorems of monotonic maps on plete lattices or continuous maps on chain-complete partially ordered sets. We are interested in com-putable fixpoints and will therefore impose some extra requirements regarding finite, unique representa-tion and existence of certain extra operarepresenta-tions. In general our approach is to separate the sets of values from the domain structure.

Domain. A domainDis a set with the following properties and operations.

Dhas a partial order⊑. A partial order is a reflexive, antisymmetric and transitive relation.

Dhas a least element⊥. Hence∀x∈D.⊥ ⊑x.

Dis chain-complete. For any subset(xi) wherexiD and wherexixi+1 (i.e. a chain) there exists a least upper boundFixi:∀d.(∀i.xid)⇒Fixid.

• Any two elements ofDwith an upper bound will also have a least upper bound. The least upper bound of two elementsxandyinDis writtenxy.

Notice that we do not require general pair-wise least upper bounds to exist. We can therefore have flat domains and other domains without a top element. The last condition is an alternative to a stronger requirement ofDbeing a complete lattice. We will frequently use domains where not all pairs of elements have a least upper bound. This is the case for flat domains larger than the two-point domain.

Any element ofDwhich is the least upper bound of a chain without being an element of the chain is called alimit point.

We will require that all values which are not limit points can be represented in our programming lan-guage in a finite and unique way and that there is a total ordering of values for implementation purposes. This ordering does not need to be related in any way to the partial order on the domain. We will return to the implementation issue in section 4.

Function domain. Given a set S and a domainD. The set SD of all functions fromS toDis a

domain. The domain has the following structure:

The bottom element isλx.⊥D, where⊥Dis the bottom element inD.

(5)

Assume fhandghfor functions f,g, andh, then fgx.f(x)⊔g(x). Given a chain(fi), thenFifix.Fi fi(x).

In the remainder of this paperSDwill denote this function domain without any implicit require-ment of the functions being monotonic or continuous.

Monotonicity. A function f from a domainD1 toD2 is monotonic if it preserves the order: ∀x,y

D1.xyf(x)⊑ f(y)

The function f iscontinuous if it is monotonic and preserves the least upper bound of chains. ∀(xi)∈

D.f(Fixi) =Fi(f(xi))

Fixpoint iteration. The central result in the domain theory is that continuous functions on a domain

have a least fixpoint. A similar result exists for monotonic functions on a complete lattice [34]. The fixpoint can be found as the limit of a chain starting with the bottom element with repeated applications of the function.

⊥ ⊑ f(⊥)⊑ f(f(⊥))⊑ f(f(f(⊥)))⊑...f(Fi fi(⊥)) =Fi(f(fi(⊥)))

We will refer to the fixpoint of the function aslfpf - the least fixpoint of f.

Pseudo-monotonicity. If f is a function on non-functional domains then the fixpoint iteration is

im-mediately applicable [2]. In the second-order case the main complication is that function application in general is not monotonic as illustrated in the first example of section 2. As a consequence a selective re-evaluation of the function on needed arguments does not necessarily form an increasing chain and could result in non-termination of the fixpoint algorithm. Fortunately function application has another property: pseudo-monotonicity [10] (in [12] called weak monotonicity).

A functionG:(D1→D2)→(D1→D2)is pseudo-monotonic iffGpreserves monotonicity, is con-tinuous for monotonic arguments and

f,f′:D1D2,ff′.

f monotonic⇒G(f)⊑G(f′)∧ f′monotonic⇒G(f)⊑G(f′)

Function application satisfies this extra condition.

Function needs. GivenG:(D1→D2)→(D1→D2), a function f :D1D2and an elementxD1.

Theneedsof the evaluationG f xis the set of values inD1for which the function f will be called during the evaluation. We will writeNG f xfor this set.

Needs is in this way an internal property but may be externalised [26] as follows: given functionsG, f and a valuex, letS=NG f x, then

∀g.(∀y∈S.f y=g y)G f x=G g x

In other words: if a value is not needed then the function will return the same value independently of how the argument was defined for that value.

This externalised view gives a lower bound of needed arguments.

y6∈NG f x⇒ ∀z∈D2.G f x=G(f[y7→z])x

where

f[y7→z] =λx.ifx=ythenzelse f x

(6)

Partial fixpoint iteration. Let G:(D1 →D2)→ (D1→D2) be pseudo-monotonic and let Si be a

sequence of subsets ofD1. We will now define a sequence of approximationsφias follows

φ0 =λx.⊥

φi+1=λx.ifxSithenφixGφixelseφix

These functions will form an increasing chain (see below).

Chaotic iteration and minimal function graphs. This sequence is similar to chaotic fixpoint iteration

[8] where one will require that needed arguments from one iteration appears in the set of values to be evaluated in the next. A simple example of this is the sequence

Si+1=FxSiNGφix

This is the approach often referred to as the minimal function graph sequence [19].

Stability. Rather than to consider when to re-evaluate the approximations it is easier to examine the

situation for a stabilised iteration sequence where for somen

∀i>n.Si=Snin

∀x∈Sn.NGφnxSn

If this is the case then we have

∀x∈Snnx= (lfpG)x

In the remainder of this section we will assume we have a stabilised iteration sequence for a functionG

and sets(Si), and that it stabilised afterniterations.

Lemma. The sequence(φi)is well-defined and forms an increasing chain.

Proof. The least upper bound operation is well-defined sinceφix andGφix are both less than or equal

to(lfpG)x.

Define the chain(θi), where

θ0=λx.⊥ and θi+1=Gθi

SinceGpreserves monotonicity we know thatθiis monotonic and thatθi⊑θi+1. By induction we prove thatφi⊑θi.

The base caseφ0⊑θ0holds trivially.

Assume for someithatφi⊑θi, and givenxSithen

φi+1xixGφix

and

φix⊑θix⊑θi+1x and GφixGθixi+1x

henceφixandGφixhaveθi+1xas an upper bound and the least upper bound will exist.

(7)

Lemma. Define the sequence(ψi), where

ψ0=φn and ψi+1=Gψi

The sequence(ψi)is well-defined and forms an increasing chain withψi⊑θi+n.

Proof. The proof is by induction as above. The base case holds trivially and in the induction step we

have

ψi+1=GψiGθi+ni+n+1

Lemma. θi⊑ψi.

Proof. The proof is by induction. In the induction step we have

θi+1=GθiGψii+1

Lemma. Fiθi=Fiψi.

Theorem. For a sequence(φi)which has stabilised afterniterations we have:

∀x∈Snnx=lfpG x

Proof. By induction we prove that∀y∈Snnyiyfor alli. By definitionφn=ψ0. Assume that for someithat∀y∈Snnyiyand givenxSn

ψi+1xixGψixnx

since

ψixnx

Gψix=Gφnxnx

In the last step we used the externalised view of neededness. Sinceφnandψi return the same value for

needed arguments then evaluatingGwith any of these functions will return the same result.

Comment. The theorem holds independently of the evaluation strategy. The traditional approach in

fixpoint iteration is to use a breadth-first strategy. But it is also possible to use depth-first or a worklist to schedule re-evaluation.

In a fixpoint iteration some values are needed early on but not when the iteration has stabilised. Such values are called spurious arguments. The theorem shows that it is safe to omit those when they are no longer needed.

Termination. The theorem above states a partial correctness of certain types of fixpoint iteration. There

are various ways to guarantee that the iteration terminates. The easiest is to require the domainsD1and D2to be finite and that the sequence(Si)is a chain.

(8)

Higher-order fixpoint iteration. The higher-order fixpoint operation is based on these principles: In the fixpoint iteration we use the least upper bound of the previous approximation and the re-evaluated value. When we evaluate a function the result only depends on the needed parts of the arguments. This will be developed in the rest of the paper.

4

An abstract datatype for domains

Using fixpoint iteration in practice requires efficient representation of values and function graphs. This is even more important in the higher-order case where arguments to functions may be functions or function graphs. In this section we describe a datatype for domains and in the next section how to use it for fixpoint iteration.

We will use Standard ML as implementation language but we only use constructions that are well-known in many programming languages. Values can be represented in a similar way in the Collections Framework in Java.

Given a domainDwe assume that all but limit points may be represented uniquely in a programming language using a typeD. We further assume that we can provide the following values and operations.

• A bottom value:bot

• A less-or-equal operation:leq

• A least upper bound operation: lub. This operation is not necesarily defined for all arguments since we only require that any two values with an upper bound has a least upper bound. The operation

• A total order: cmp. This ordering is only used for implementation purposes and does not need to agree with the partial ordering of the domain. Equality in the two orderings must, however, agree since we require values inD to be represented uniquely. The ordering is implemented with an operation that returns a value in the set{LESS, EQUAL, GREATER}. Alternatively one could have used values -1, 0, and 1 as often done in the Java/C world.

All in all a domain is a set of values plus a tuple of the following operations.

datatype ’a domain =

Dom of ’a (* bottom *) * (’a*’a -> bool) (* lessOrEq *) * (’a*’a -> bool) (* equal *) * (’a*’a -> order) (* compareTo *) * (’a*’a -> ’a) (* lub *)

* (’a -> string); (* toString *)

From base domains we may construct more complex domains.

String domain: strdom. We can definestrdom: string domainas the flat domain of strings. As

bottom element we will use the empty string, and the ordering just specifies that the bottom element is less than or equal to all values in the domain.

Integer domain:intdom. The set of natural numbers can be made into a domain using the usual integer

ordering. We then get the domain N∞

(9)

val bot = 0;

fun leq(a,b:int) = a < b;

fun lub(a,b) = if a < b then b else a; fun cmp(a,b:int)

= if a = b then EQUAL else if a < b then LESS else GREATER;

List domain. From any domain we can construct the domain of lists of values from that domain. The

empty list is the bottom element and shorter lists are less than or equal to longer lists provided that they are related element-wise.

listdom: ’a domain -> ’a list domain

Tuples Two domains can be tupled into a domain of tuplestupdom. The bottom element is the tuple

of the bottom element from the two domains.

tupdom: (’a domain *’b domain) -> (’a *’b) domain

Power set domain. For a domain we can construct the domain of subsets of values from that domain.

The empty set is the bottom element and we use ordinary subset ordering. Sets are represented as sorted lists where elements are sorted according to the total ordering on the domain.

setdom: ’a domain -> ’a list domain

Least upper bound is set union and can be implemented as a merge of two sorted lists. On sets we also define amember, intersection and a set difference function. Notice that the singleton set operation and the set difference function in general are not monotonic.

We could also construct a set domain using a binary tree representation but we seem to use least upper bounds more frequently than membership tests and in that context the list representation is quite acceptable.

Domain of partial function graphs. For domainsD1and D2 we can construct the domain of partial

function graphs fromD1 to D2. A function graph is just a tabulation of a function and it is a way to represent a function as a first-order value. This domain is essentially just the powerset domain of tuples of values fromD1 and D2. Since we tend to use lookups more frequently than least upper bounds of function graphs, a representation using binary trees seems preferable. In our implementation language this can be achieved with a special dictionary structure. We could, however, equally well have used a list of tuples.

grfdom: (’a domain *’b domain) -> (’a *’b) dict domain

We will also define some extra functions of function graphs:

lookupg: (’a *’b) dict -> ’a -> ’b isdef: (’a *’b) dict -> ’a -> boolean

updateg: (’a *’b) dict -> (’a,’b) -> (’a *’b) dict

This domain does not use the ordering onD1and is the domain of all functions from the set D1toD2. One can also define the domain of all continuous functions fromD1 toD2 but that is not what we will use here.

(10)

5

A domain of higher-order function graphs

The idea in using function graphs to represent functions is to tabulate functions only for needed ar-guments. In the higher-order case this is a special challenge since we should only tabulate functional arguments to functions for the needed arguments. Consider a function

g x k=k(0)⊔k(x) m x =1⊔x

f x =g x m

and an initial call f1. The functionmis not called directly but since a call togwhere the first argument is 1 will call the second argument with 0 and 1 then we should tabulatemfor those values.

Calls through arguments can also be described as an analysis of whichpartsof an argument will be used. If an argument is a function we may only need to call this function with a small set of arguments, hence only using a small part of this function. The purpose of this section is to establish a representation of this set.

The central idea is to tabulate arguments for the needed parts when it is used as an argument and not when the argument is used as a function. The problem in this is to specify which parts of arguments will be needed prior to the call. What is needed of the arguments may both depend on the function and the arguments to the function. In this way we have an extra level of circularity in the higher-order case.

Since partially applied functions can appear nested inside expressions we will use a uniform way to record needs throughout the type structure. To record which parts of the arguments to a functional value will be used during evaluation we extend the domain with an extra part that describes needs.

Let Vt be the usual set we would use to describe values of type t. We will now instrument the

description with extra need information in a domainVt

Vt1∗...∗tntm=Vt1∗..∗VtnVtm

Vbase= .. numbers and strings

Vt1...tntm=Vt1∗..∗Vtn′ →(Vtm′ ∗Nt1∗...∗Ntn)

Vbase′ =Vbase∗U

Nt1∗...∗tntm=℘(Vt1∗...∗Vtn′)

Nbase=U

HereVbaseis a domain of base values (e.g. numbers and strings) andUa one-point domain.

The Res domain. We will now define a domain of higher-order function graphs. In the higher-order

case the argument needs of functions should be an externalised property. For now we only consider how this should be represented; in the next section we will show how they are computed.

We will use a datatype Resto describe these instrumented values. They are either simple values or function graphs fromRes listto tuples ofResvalues and needs described asRes list list list.

datatype Res = S1 of string | I1 of int | Bot1 | G1 of ResGraph

withtype ResGraph = (Res list,Res*Res list list list) dict; type Needs=Res list list list;

(11)

The datatypeRescan be equipped with both a partial order and a total ordering for implementation purposes, a bottom elementbot_rand a least upper bound operationlub_rusing the domain construc-tions described in section 4. Similarly the types Needscan be given a domain structure with bottom elementbot_nand least upper boundlub_n.

The type ResGraph will represent function graphs with extra need information and will have the operations defined for graph domains described in section 4:

bot_g : ResGraph

lub_g : ResGraph * ResGraph

isdef : ResGraph -> (Res list) -> boolean

lookupg : ResGraph -> (Res list) -> (Res * Needs)

updateg : ResGraph -> (Res list,Res * Needs) -> ResGraph

Higher-order values. The second part of the representation of values is to include the extra graph

information into ordinary values in the higher-order functions. We will use a slightly modified definition of the typeVcompare to the setsVt shown above.

datatype V = S of string | I of int | Bot | C of ResGraph*(V list -> V);

We can establish a close relationship between the V set and the Res domain with two functions v2r : V -> Resandr2v : Res -> V.

fun v2r (S s) = S1 s | v2r (I i) = I1 i | v2r Bot = Bot1 | v2r (C(g,_)) = G1 g;

fun r2v (S1 s) = S s | r2v (I1 i) = I i | r2v Bot1 = Bot

| r2v (G1 g) = C(g,fn xs => r2v(#1(lookupg g (map v2r xs)));

We may note that forrinResthatr=v2r(r2v(r))but forvinVthatv⊒r2v(v2r(v)).

At the outer level we can externalise argument needs with a function callneed. It will memoize functions in the argument list, perform the function call and return the result together with the memo information.

callneed : (V list -> V) -> V list -> V * Needs

From a need set we may tabulate functions in a list for the needed parts. Such a function will have type tabulate : V list -> Needs -> Res list

Both these functions are fairly straightforward and will not be described further.

6

Higher-order iteration

We are now ready to construct the higher-order fixpoint iterator. We have two graphsphi2and phi1of evaluated values from the current and the previous iteration. The iteration continues until stability has been achieved.

There is an extra level of iteration involved since the set of needed parts of arguments is defined circularly. Initially we assume that no part of functional arguments is needed. We will then attempt to evaluate the function and record which parts of the arguments were needed. The arguments are then tabulated for these parts, and this continues as long as more needs are recorded.

(12)

Fix: ((V list-> V)-> V list-> V)-> V list-> V

Internally it uses four functions

FF: (V list-> V) -> Res list-> V * Needs gg: Needs -> V list -> V * Needs

ff: V list -> V iterate: V list-> V

It uses the functions from the previous section plus a few extra functions

fun fstcall (C(g,f)::rs) = f(rs) | fstcall _ = Bot ; fun lub_v(x,y) = r2v (lub_r(v2r x,v2r y));

fun lub_rn ((v1,n1),(v2,n2)) = (lub_r(v1,v2),lub_n(n1,n2)); fun rs2v rs = map r2v rs;

fun isBot1 (Bot1::xs) = true | isBot1 [] = true | isBot1 _ = false; fun isC1 ((G1 _)::xs) = true | isC1 _ = false;

The evaluation of functional arguments to functions is done during the tabulation of the arguments.

fun Fix F =

let val phi1 = ref bot_g; val phi2 = ref bot_g fun FF ff rs =

if isBot1 rs then (Bot,bot_n) else

if isC1 rs then callneed fstcall (rs2v rs) else if isdef (!phi2) rs then

let val (r1,n2)=lookupg (!phi2) rs in (r2v r1,n2) end else

let val _ = phi2 := update (!phi2) (rs,lookupg(!phi1) rs); val (v1,nds) = callneed (F ff) (rs2v rs)

val r2 = lookupg (!phi2) rs

in phi2 := update (!phi2) (rs,lub_rn(r2,(v2r v1,nds))); (v1,nds) end;

fun gg nd vs =

let val (v1,n1) = FF ff (tabulate vs nd) val n2 = lub_n (n1,nd)

in if eq_n(nd,n2) then (v1,n2) else gg n2 vs end and ff ws = let val (v,n)= gg bot_n ws in v end fun iterate xs =

let val _ = ( phi1:=(!phi2); phi2 := bot_g ); val v1 = ff xs ;

in if eq_g(!phi2,!phi1) then v1 else iterate xs

end;

in iterate end ;

The functioniteratewill re-evaluate a call and recursively needed argument until stability. It uses the the functions ffand gg to tabulate arguments to a function based on which parts of its arguments a function needs. The functionFFevaluates other function calls in a depth-first style.

Example: A higher-order fixpoint. To use the fixpoint iteration we should express a functional over

the domainV List -> V. We will here rexamine the functiong′ from the example with closure-based fixpoint iteration in section 2.

fun F call [S "g",n,k] = glb(n,lub(call [k,I 1],call [S "g",n,call [S "m",n,k]])) | F call [S "m",n,k,x] = call[k,glb(n,x)]

| F call [S "top",x] = I 1 | F call [S "bot",x] = I 0

(13)

| F call [S "ft",x] = call [S "g",x,call [S "top"]] | F call xs = C(bot_g,fn ys => call(xs@ys))

and lub xy = lub_v xy

and glb(I x,I y) = if x > y then I y else I x | glb _ = Bot; val f = Fix F;

val rr = f [S "ft", I 1];

ft: [1] =>(1,[])

g: [1,[17→1]] =>(1,[[],[[1]]]) m: [1,[17→1],1] =>(1,[[],[[1]],[]])

top:[1] =>(1,[])

This should be read as follows. Ifgis called with 1 and a function that map 1 to 1 then it will return 1 and call its second argument with 1. Ifmis called with 1, a function that maps 1 to 1 and 1 then it will return 1 and call its second arguemnt with 1.

7

Memoization and second-order fixpoint iteration

The higher-order fixpoint iteration may be seen as a generalisation of techniques for memoizing func-tions. Memoization is a well-known technique for avoiding re-evaluation of repeated calls of funcfunc-tions. It requires a certain amount of administrative overhead and is in consequence not normally worth the extra effort.

Let us start with the Fibonacci function written in Standard ML. fun fib x = if x < 2 then 1

else (fib (x-1)) + (fib (x-2));

A call to the function may be written asfib 12but the function is impractical when used with larger arguments due to re-evaluation of smaller Fibonacci values.

One approach could be to memoize the Fibonacci function. A simple memoizing transformation of a first order function might look as follows

fun memo f =

let val l = ref [] fun eval [] x

= let val r = f x

in l := (x,r)::(!l); r end | eval ((a,b)::t) x

= if x=a then b else eval t x; fun ff x = val (!l) x

in ff end;

val mfib = memo fib;

The function could be called as before and repeated calls will now not result in re-evaluation. It is, how-ever, only the external calls which will be memoized so it is still impractical to use for larger arguments. If we want to memoize the internal calls the first step is to define the Fibonacci function as a fixpoint of a second-order function.

fun Fib f x = if x < 2 then 1

else (f (x-1)) + (f (x-2)); fun fix F x = (F (fix F)) x;

(14)

The Fibonacci function defined in this way will behave just as the original definition. The fixpoint operation may, however, be changed into a memoizing operation. The fixpoint operation is then

fun memoFix F xx = let val l = ref []

fun eval [] x = let val r = F ff x

in l := (x,r)::(!l); r end

| eval ((a,b)::t) x = if x=a then b else eval t x and ff x = eval (!l) x

in ff xx end;

val mmfib = memoFix Fib;

This Fibonacci function will tabulate its internal calls and run in almost linear time if we used an efficient representation of argument-result pairs.

Truncated depth-first evaluation. The second-order fixpoint operator may be seen as a generalisation

of the memoization of second-order functionals and as a special case of the higher-order fixpoint operator. In the second-order case we do not need to iteratively tabulate arguments to functions for needed parts since arguments are of base type.

We call the evaluation strategy for “truncated depth-first” since it evaluates in a depth-first style as long as no circularities are detected. When we encounter circularities we truncate the evaluation and just use previously evaluated values or the bottom element instead.

LetD1andD2be two domains and letD3be the graph domain fromD1toD2. Let us further assume that we have defined the functionslookupg, isdef,andupdateon the graph domain (see section 4), let lubbe the least upper bound operation onD2, andbot g,eq gandlub gis the bottom element, equality and least upper bound operations on the graph domainD3. We will here present the fixpoint operation for fixed domainsD1 and D2 but in practice it may be parameterised on the domains so that the same function can be used for any domain. The function should have type

Fix: ’a domain -> ’b domain -> ((’a -> ’b) -> ’a -> ’b) -> ’a -> ’b

but we will here leave out the domain arguments and assume the various function on domains are defined with the appropriate type as described above:

fun Fix F =

let val phi1 = ref bot_g (* previous *) val phi2 = ref bot_g (* current *) fun f x =

if isdef(!phi2) x then lookupg (!phi2) x else let val r0 = lookupg (!phi1) x

val _ = phi2:= update(!phi2) (x,r0) val r = F f x

val _ = phi2:= update(!phi2) (x,lub(r,r0)) in r end;

fun iterate x = (

phi1 := !phi2; phi2 := bot_g; f x;

if eq_g(!phi1,!phi2) then lookupg (!phi2) x else iterate x);

in iterate end;

Discussion. The iteration uses two function graphs: phi1andphi2wherephi1contains values found

(15)

There is no guarantee that users of this function provide a correct pseudo-monotonic function. If the lubdoes not exist then it must be because of errors in the functionF. Similarly if the iteration does not stop then it is because the iterated needs are infinite.

The truncated depth-first evaluation strategy has been used in a number of different versions in pro-gram analysis, circular attribute pro-grammars etc. over the years [28, 11]. It is both simple and more efficient than the breadth-first evaluation strategy often referred to as minimal function graphs.

The approach here will automatically remove spurious calls [29, 14]. In the stable situation the graph phi2will be tabulated for all arguments which directly or indirectly are needed from the initial call -except for those which are already in the graph of found fixpoints.

Variations. An obvious extension is to memoize the fixpoint function so that repeated calls will not

require new iteration. The memoization can easily be built into the fixpoint function since argument-result pairs already are stored in a graph.

The iteration stops when re-evaluation of the argument and all dependent arguments do not result in changed values. This also means that the iteration is at least done twice. If the dependency is not circular we will do an unnecessary re-evaluation. An alternative approach could be to store used values during evaluation in a separate graph and to re-evaluate until the used-value graph is a subgraph of the computed graph in the iteration. This method will be optimal for programs without circular dependencies but at the cost of some extra book-keeping. Whether it is worthwhile depends on the function used as argument to the fixpoint.

When the iteration is truncated because of circularities we will use the value from the previous iteration or the bottom element. In some domains it could be relevant instead to use the least upper bound of all values in the graph where the arguments are less than or equal to the one we are searching for. Such an operation is quite costly but may be worth it if the argument domain has a complex structure.

Dependency-based methods. Over the last two decades a number of authors have proposed techniques

which store a dependency graph during the iteration. The idea is to restrict re-evaluations so that they only occur when the graph has been changed for some of the arguments it depends on. The top-down evaluator [6] evaluates in a depth-first order but with local computation of fixpoints. The neededness analysis method [22] uses breadth-first strategy, but limits the re-evaluation to arguments that depend on arguments that have been changed. The time stamp solver WRT [12] uses extra time stamps in the work list to schedule re-evaluation in a fair manner.

There is a significant cost in maintaining dependency information and any efficiency gain requires that each evaluation of the functionFis costly.

8

Examples

In this example we will need a second-order fixpoint operator which has the type

Fix: ’a domain -> ’b domain -> ((’a -> ’b) -> ’a -> ’b) -> ’a -> ’b

We will now give two examples of the programming style one may use with this fixpoint operator.

First-order strictness analysis. This example is a strictness analyser for a small first-order functional

(16)

datatype exp =

par of string | cst of int | add of exp * exp |

cond of exp * exp * exp | call of string * exp list;

type progs = (string*(string list * exp)) list;

The strictness analyser is defined as the fixpoint of a second-order function.

fun F prg seval (f,args) =

let fun seval1 env (cst i) = 1

| seval1 env (par s) = lookup s env | seval1 env (add (e1,e2)) =

glb(seval1 env e1,seval1 env e2) | seval1 env (cond (e0,e1,e2)) =

glb(seval1 env e0,

lub(seval1 env e1,seval1 env e2)) | seval1 env (call (f,exps)) =

seval (f,map (seval1 env) exps); val (pars,exp) = lookup f prg

in seval1 (zip pars args) exp end; fun Strict p

= Fix (tupdom (strdom,listdom intdom)) intdom (F p);

We have here used azipfunction to collapse two lists into an association list, and lookupto lookup values in an environment.

For a program p which contains a function f with two parameters the strictness analysis may be performed as follows.

val strict = Strict p;

strict ("f",[0,1]); strict ("f",[1,0]);

The function strictis now a function of typestring*int list -> int. The domain structure is hidden as soon as we have defined how the fixpoint should be computed and the function may be used as an ordinary function in the language. Notice that we do not define the function as

fun strict args = Strict p args;

If we used the latter definition every new call tostrictwould re-evaluate the fixpoint rather than using the built-in memoization in the fixpoint operator.

First-set for grammars. For a non-terminal in a context-free grammar the First-set is the set of

ter-minals it may generate. The First and Follow sets for context-free grammars play an important role in constructing parsers and they are normally defined as least fixpoints.

In this example a grammar is a list of productions with a string as left-hand-side and a list of terminals or non-terminals as right-hand side:

datatype elem = nt of string | tm of string ; type gram = (string * elem list) list;

(17)

fun F g first s = firstg first s g and firstg first s [] = []

| firstg first s ((nt1,rh)::r) =

union(if s=nt1 then firstrh first rh else [], firstg first s r) and firstrh first [] = [""]

| firstrh first ((nt s)::rr) = let val ff = first s

in union(ff, if member "" ff

then firstrh first rr else []) end

| firstrh first ((tm s)::_) = [s];

fun First g = Fix strdom (setdom strdom) (F g);

Let us now consider a small grammar for simple expressions.

val grm =

[("exp", [nt("term")]),

("exp", [nt("exp"),tm("+"),nt("term")]), ("term", [nt("factor")]),

("term", [nt("term"),tm("+"),nt("factor")]), ("factor",[tm("name")]),

("factor",[tm("number")]),

("factor",[tm("("),nt("exp"),tm(")")]) ]; val first = First grm;

val r = first "exp";

The whole fixpoint will be tabulated during this call so subsequent calls offirstfor other non-terminals will not result in any re-evaluation.

9

Discussion and related work

This paper is concerned with demand-driven solution of fixpoint iteration. The immediate application is in a solution of demand-driven versions of program analysis problems. The fixpoint iteration makes it possible to implement more precise and complex higher-order analyses. The fixpoint operation is implemented in Standard ML (Moscow ML [27]).

Representation techniques. Several methods are based on ways of representing the full tabulation

with only a small set of points. Amongst these one should mention the frontiers algorithm [17, 24] and binary decision diagrams. Such techniques may drastically reduce the need for tabulation, though they still seem to require too much storage for certain examples. If it is not vital to find the least solution but only important to find a safe solution, such methods may be connected with various approximation techniques in order to improve efficiency.

Lazy types. Our approach seems comparable in efficiency to the work by Le Metayer & Hankin on

(18)

Minimal function graphs This paper is concerned with demand-driven solution of fixpoint iteration. The immediate application is in the solution of demand-driven versions of (bottom-up) program analysis problems. If we want to analyse the strictness of a parameter to a given function then these techniques may restrict the fixpoint iteration to those parts of the program that may influence the result. Chaotic fixpont iteration [8] in abstract interpretation is a method to construct demand-driven versions for anal-ysis problems. In fact whether an abstract interpretation should solve a demand or a global dataflow analysis problem is independent of its specification but merely a question of how the fixpoint operator is implemented [7, 8].

A different problem is the construction of top-down analyses which, from a description of the possi-ble initial calls, examines which values may reach a given part of the program. For functional program-ming languages such analyses are often based on a minimal function graph semantics [19, 26, 29].

Third-order fixpoints. Since function graphs can be made into a domain we can tabulate a first order

function and use it as a simple finite value. In this way a third order function may be treated as if it were a second-order function and used in fixpoint evaluation as described above. The classical example of strictness analysis of foldris quite easy to analyse in this way. In principle this may be used for even higher-order functions, but it quickly becomes impractical to tabulate functions for all possible arguments.

Comparison. We have implemented a number of different fixpoint operations and compared them for

a number of different applications. Below we show some results from calling thefirstfunction with a grammar for Java and the nonterminal for expressions as arguments. We compare the number of times the functional is called during the fixpoint iteration (#rhs) and the number of comparisons on values in the string domain (#cmp)

The fixpoint operations are:

• Kleene: a simple breadth-first evaluation strategy

• Dep: a neededness-analysis based evaluator. It is a fairly direct implementation of the evaluator from [22], though without the monotonic completion which is quite costly.

• TD: a top-down solver based on [6], though here taken from [12]. • W: a worklist solver from [12].

• TDF: the truncated-depth-first solver described earlier

• TDF-sub: the truncated-depth-first solver but with an extra graph of used values. It iterates until the graph has the graph of used values as a subgraph.

Method #cmp #rhs Kleene 31352 572

Dep 15353 190

TD 11377 66

W 10413 147

TDF 4873 148

TDF-sub 4331 111

The strategies fall into three groups

(19)

• Dependency-based strategies may be able to use dependency information to determine whether a re-evauation can change the result and thereby avoid some evaluations. The savings in fewer eval-uations are at the expense of maintaining dependency information and testing whether dependent values have changed.

• The truncated depth-first strategy does not maintain dependency information. The depth-first ap-proach will normally give fewer iterations but it may re-evaluate some expressions that does not depend on parts that have changed value.

In our experience it is quite costly to maintain a dependency graph during fixpoint evaluation. It may avoid some evaluations of the function but at the cost of a certain amount of extra bookkeeping. This may be attractive if each call to the function is very expensive but not otherwise. We have made the same comparison of the fixpoint operation in different program analyses, and in our experience the TDF is both simple and efficient.

Logical relations. We have not examined correctness proofs of abstract interpretations in this paper.

One way to integrate correctness proofs and a standard semantics into the same framework is to base proofs on logical relations and let standard semantics and abstract interpretations follow a similar pro-gramming style. This is explored in [28].

10

Conclusion

We have presented a technique for solving fixpoint equations involving higher-order functions. The method may be used for higher-order program analysis and more generally as a tool in programming. For second-order functionalsneedsare sets of argument tuples. Our fixpoint iteration approach extends this notion ofneedsto the higher-order case whereneedsare higher-order functionals in arguments and needs.

The fixpoint operator on domains makes it possible to view a class of abstract interpretations as programs and examine the style and properties of the programming paradigm. The work by David Schmidt on denotational semantics led to further analysis of state in functional programs and techniques for identifying global variables in programs. Similarly it would be interesting to see whether more algorithms could be written in a higher-order fixpoint iteration style.

References

[1] Martin Alt & Florian Martin (1995):Generation of Efficient Interprocedural Analyzers with PAG. In:SAS,

LNCS983, Springer, pp. 33–50, doi:10.1007/3-540-60360-3 31.

[2] Krzysztof R. Apt (1997): From Chaotic Iteration to Constraint Propagation. In: ICALP,LNCS 1256, Springer, pp. 36–55, doi:10.1007/3-540-63165-8 163.

[3] Roberto Bagnara, Patricia M. Hill, Andrea Pescetti & Enea Zaffanella (2007): On the Design of Generic Static Analyzers for Modern Imperative Languages. CoRR abs/cs/0703116. Available at http://arxiv.org/abs/cs/0703116.

[4] Richard S. Bird (1984):Using Circular Programs to Eliminate Multiple Traversals of Data.Acta Inf.21, pp. 239–250, doi:10.1007/BF00264249.

(20)

[6] Baudouin Le Charlier & Pascal Van Hentenryck (1992): On the Design of Generic Abstract Interpretation Frameworks.In:WSA, pp. 229–246.

[7] Patrick Cousot & Radhia Cousot (1977): Abstract Interpretation: A Unified Lattice Model for Static Analysis of Programs by Construction or Approximation of Fixpoints. In: POPL, ACM, pp. 238–252, doi:10.1145/512950.512973.

[8] Patrick Cousot & Radhia Cousot (1978): Static determination of dynamic properties of recursive proce-dures. In: Formal Description of Programming Concepts, North-Holland, pp. 237–277. Available at http://www.di.ens.fr/~cousot/COUSOTpapers/IFIP77.shtml.

[9] Patrick Cousot, Radhia Cousot, J´erˆome Feret, Laurent Mauborgne, Antoine Min´e, David Monniaux & Xavier Rival (2005): The ASTRE ´E Analyzer. In: ESOP, LNCS 3444, Springer, pp. 21–30, doi:10.1007/978-3-540-31987-0 3.

[10] Alan Dix (1988): Finding Fixed Points in Non-Trivial Domains: Proofs of Pending Analysis and Related Algorithms. Technical Report 107, Univ. of York. Available at http://alandix.com/academic/papers/fixpts-YCS107-88/.

[11] Rodney Farrow (1986): Automatic generation of fixed-point-finding evaluators for circular, but well-defined, attribute grammars. In: SIGPLAN Symposium on Compiler Construction, ACM, pp. 85–98, doi:10.1145/12276.13320.

[12] Christian Fecht & Helmut Seidl (1996):An Even Faster Solver for General Systems of Equations. In:SAS,

LNCS1145, Springer, pp. 189–204, doi:10.1007/3-540-61739-6 42.

[13] Jeroen Fokker & S. Doaitse Swierstra (2009): Abstract Interpretation of Functional Programs us-ing an Attribute Grammar System. Electr. Notes Theor. Comput. Sci. 238(5), pp. 117–133, doi:10.1016/j.entcs.2009.09.044.

[14] John P. Gallagher & Maurice Bruynooghe (1990):The Derivation of an Algorithm for Program Specialisa-tion.In:ICLP, pp. 732–746, doi:10.1007/BF03037167.

[15] Chris Hankin & Daniel Le M´etayer (1995): Lazy Type Inference and Program Analysis. Sci. Comput. Program.25(2-3), pp. 219–249, doi:10.1016/0167-6423(95)00012-7.

[16] John Hughes (1985): Strictness detection in non-flat domains. In: Programs as Data Objects,LNCS217, Springer, pp. 112–135, doi:10.1007/3-540-16446-4 7.

[17] Sebastian Hunt & Chris Hankin (1991):Fixed Points and Frontiers: A New Perspective. J. Funct. Program.

1(1), pp. 91–120, doi:10.1017/S0956796800000071.

[18] Kristian Damm Jensen, Peter Hjæresen & Mads Rosendahl (1994):Efficient Strictness Analysis of Haskell.

In:SAS, pp. 246–362, doi:10.1007/3-540-58485-4 51.

[19] Neil D. Jones & Alan Mycroft (1986):Data Flow Analysis of Applicative Programs Using Minimal Function Graphs.In:POPL, ACM Press, pp. 296–306, doi:10.1145/512644.512672.

[20] Neil D. Jones & Mads Rosendahl (1994):Higher-Order Minimal Function Graphs. In:Algebraic and Logic Programming, ALP’94,LNCS850, Springer, pp. 242–252, doi:10.1007/3-540-58431-5 17.

[21] Neil D. Jones & David A. Schmidt (1980):Compiler generation from denotational semantics.In: Semantics-Directed Compiler Generation,LNCS94, Springer, pp. 70–93, doi:10.1007/3-540-10250-7 19.

[22] Niels Jørgensen (1994):Finding Fixpoints in Finite Function Spaces Using Neededness Analysis and Chaotic Iteration.In:SAS, Springer, pp. 329–345, doi:10.1007/3-540-58485-4 50.

[23] Ryszard Kubiak, John Hughes & John Launchbury (1991):Implementing Projection-based Strictness Anal-ysis. In:Functional Programming, pp. 207–224.

[24] Chris Martin & Chris Hankin (1987):Finding fixed points in finite lattices. In:FPCA,LNCS274, Springer, pp. 426–445, doi:10.1007/3-540-18317-5 23.

(21)

[26] Alan Mycroft & Mads Rosendahl (1992): Minimal Function Graphs are not Instrumented. In: WSA, pp. 60–67. Available athttp://akira.ruc.dk/~madsr/webpub/wsa92.pdf.

[27] Sergei A. Romanenko, Claudio Russo & Peter Sestoft (2000): Moscow ML Language Overview. Technical Report, Russian Academy of Science, Moscow. Available at http://www.itu.dk/people/sestoft/mosml/manual.pdf.

[28] Mads Rosendahl (1992): Abstract Interpretation and Attribute Grammars. Ph.d. thesis, Cambridge Univ. Available athttp://akira.ruc.dk/~madsr/webpub/phd.pdf.

[29] Mads Rosendahl (1993):Higher-Order Chaotic Iteration Sequences. In: PLILP,LNCS714, Springer, pp. 332–345, doi:10.1007/3-540-57186-8 89.

[30] Shmuel Sagiv, O. Edelstein, Nissim Francez & Michael Rodeh (1989): Resolving Circularity in At-tribute Grammars with Applications to Data Flow Analysis. In: POPL, ACM Press, pp. 36–48, doi:10.1145/75277.75281.

[31] David A. Schmidt (1985):Detecting Global Variables in Denotational Specifications.ACM Trans. Program. Lang. Syst.7(2), pp. 299–310, doi:10.1145/3318.3323.

[32] David A. Schmidt (1986): Denotational Semantics: A Methodology for Lan-guage Development. Allyn and Bacon, Newton, MA. Available at https://www.scss.tcd.ie/Andrew.Butterfield/Teaching/CS4003/DenSem-full-book.pdf. [33] David A. Schmidt (Jan. 1982):Denotational semantics as a programming language. Internal Report

CSR-100-82, Univ. of Edinburgh.

Referências

Documentos relacionados

A Lei Federal nº 8.080/1990 dispõe sobre as condições para promoção, proteção e recuperação da saúde, a organização e o funcionamento dos serviços correspondentes. Sobre

JA- CHRODIS Abordagens eficazes centradas no paciente para doentes com multimorbilidade Boas práticas em matéria de promoção da saúde e prevenção de doenças crónicas

Sistemas de Processa- mento de Transações Sistemas de Controle de Processos Sistemas Colaborativos Sistemas de Apoio às Operações Sistemas de Informação Gerencial Sistemas de Apoio

Como um campo de investiga- ção, baseado nos fundamentos teóricos do feminismo e do ambienta- lismo, busca analisar por que as mulheres são tratadas como inferiores pelos homens e

As diferenças entre as duas bases de dados estão dentro de uma margem já observada na proporção de domicílios que recebem renda do trabalho, que não foi imputada.. A última

O filósofo Francisco Ortega (2008) denomina de biossociabilidade a criação de critérios e valores de avaliação das pessoas, baseados em higiene, desempenho

Os resultados obtidos com as análises de re- gressão linear demonstraram que o escore total de suporte social não contribuiu significativa- mente para explicar as variações no

This limitation in the present study indicates that, as well as measuring resilience, variables should also be measured that make it possible to infer the states of health or