As the complexity of a language grows, it becomes increasingly more difficult to reason formally about the language. In other cases the programmer might make a simple mistake during implementation, for example the mistakes seen with list concatenation (§4.5.2) and the interpreter for the Arith language—§4.6.2. If it can be shown, how- 54
4.7. Types as (Abstract) Interpreters ever, that expressions in the language can be mapped to expressions from an existing
formalism then the formalism can be used to reason about the language itself. If the execution costs of the formalism are lightweight in comparison to the original language then this modelling can be said to beefficient. This is a core concept in the technique
ofAbstract Interpretation[JN95].
Abstract interpretation allows for correctness guarantees over a language to be given in direct relation to an easier to model formalism. This technique has found success in a variety of practical settings. One is the analysis of program execution for optimisations by compilers. Another the analysis of program flow for detecting erroneous states, as used by static analysis tools.
This section describes the technique of abstract interpretation and how, using de- pendent types, abstract interpretations can be used to provide compile-time correctness- by-construction guarantees and run-time checks to a language w.r.t. to a given abstrac- tion.
4.7.1
‘Casting Out the Nines’
To illustrate the technique of abstract interpretation, Jones and Nielson [JN95] show how arithmetic computations are checked for correctness using the technique known as: ‘Casting out the Nines’. This same illustrative example is repeated here.
Given an arithmetic expressionethat evaluates to some valuev, the same operations
ineare performed. However, for each value inethat is greater than nine, the value is
replaced with the sum of its digits until the sum is less than nine. For the expressione
to be correct w.r.t. to the valuev, the sum modulo nine of the digits ofvmust be equal
to the value obtained through ‘casting out the nines’ fore. Let the calculation to be
checked, be defined as follows:
123×457+76543 =? 132654
The result132654can be checked by reducing the expression on the left-hand side
by ‘casting out the nines’, and calculating the sum modulo nine of the digits on the right-hand side:
4. Well-Typed (Abstract) Interpreters 123×457+76543 =? 132654 6×16+25 =? 21 (mod 9) 6×7+7 =? 3 42+7 =? 3 6+7 =? 3 4 =? 3
The results of the calculation performed on both the left and right hand sides (four and three) are not equal. Hence, the answer found by the calculation is incorrect. Modelling arithmetic operations, and thus checking the result of, using the cast the nines technique allows for correctness guarantees to be made. As the complexity of the calculations grow, the complexity of the correctness proof does not. Onecasts out the nines.
4.7.2
Working with Abstract Interpretations
A problem, however, with the approach of abstract interpretation is that this modelling and reasoning of a language w.r.t. an interpretation is normally performed external to the implementation of the modelled language. There is a disconnect between the two representations. As types in a dependently typed language can contain any value, it stands to reason that a dependent type can be used to capture at the type level the abstract interpretation of a given language. Brady [Bra05, Chapter 5] demonstrated this approach by modelling operations on GMP integers with an abstract interpretation of the same operations by using natural numbers. Capturing the abstraction at the type level allows for compile time correctness-by-construction guarantees to be given over the presented language; to build the language correctly, the abstract interpretation must also be constructed correctly. Further, by having access to the abstraction at run-time allows for further checks to be made.
The work of Brady [Bra05] on using dependent types as abstract interpreters has been taken further. Preliminary work by Castro et al. [Cas+15] demonstrates how these techniques can be used to reason about structured parallel programs. The work presented in this thesis is an application of the same techniques but used to provide homomorphisms between DSMLs and the GRL.
4.7. Types as (Abstract) Interpreters
4.7.3
The Simplified Arith Language
Recall the Arith language from §4.2. Its complexity in operating over two domains of operation (integer arithmetic and boolean algebra) is not suitable for introducing how an abstract interpretation can be modelled using dependent types. To simplify the example, the support for boolean algebra is dropped, the reformulation will support integer arithmetic only3.
§4.7.1 has already shown how integer arithmetic can be checked using a ‘cast nines’ abstraction. The remainder of this section introduces the interpretation semantics for transforming expressions in Arith to the ‘cast nines’ representation, and details its implementation using dependent types. The same formal representation for Arith as presented in §4.2 will be used to represent the simplified Arith language.
4.7.4
Implementing an (Abstract) Interpreter for Arith
Suppose there is a functioncastNinethat sums the digits of a number until the result- ing value is less than nine. Figure 4.4 presents interpretation semantics for constructing the ‘cast nine’ abstraction of an expression from Arith.Arith:Arith→Z Numn=castNine(n) Negn= (−1)∗castNine(n) Addx y=castNine(x+y) Subx y=castNine(x−y) Mulx y=castNine(x ∗ y) Divx y=castNine(x/y)
Figure 4.4: Interpretation semantics for abstracting Arith expressions into a ‘Cast Nine’ abstraction.
As with the description of the ‘cast nine’ approach in §4.7.1, each expression in Arith has the values first converted to their ‘cast nine’ abstraction prior to the operation being performed. The inductive interpretation presented in Figure 4.4 will take any Arith expression and compute the resulting abstract representation.
3Chapter 11 will detail techniques that provides abstractions for both boolean algebra and integer
4. Well-Typed (Abstract) Interpreters Traditional Implementation
1 convert : Arith ty -> Int
2 convert (Num n ) = castNine n
3 convert (Neg n ) = ( -1) * convert n
4 convert (Add x y) = castNine $ ( convert x) + ( convert y)
5 convert (Sub x y) = castNine $ ( convert x) - ( convert y)
6 convert (Mul x y) = castNine $ ( convert x) * ( convert y)
7 convert (Div x y) = castNine $ div ( convert x) ( convert y)
Listing 4.7: Traditional Implementation of Interpretation Semantics from Figure 4.4 Traditionally computing the ‘cast nine’ representation of Arith expressions re- quired the construction of an evaluation function separate from the standard evaluation function. For example, Listing 4.7 details one such function. In this examplecastNine
is a pre-given function used to calculate the ‘cast nine’ representation of a given integer. Notice, that the results of the operations are passed through thecastNinefunction to ensure that the abstraction is provided.
When constructing an evaluation function, theconvertfunction from Listing 4.7 can be used to convert the expression to its ‘cast nine’ representation. This is shown in Listing 4.8, wheredoEvalevaluatesArithexpressions. The actual result of the evaluation (fromdoEval) is converted usingsumMod9that sums the digits mod nine of the result. If the resulting values are equal (i.e. the result of callingconvertand
sumMod9) then a run-time soundness guarantee can be made over the result ofdoEval.
1 eval : Arith ty -> Maybe Int
2 eval expr = let res = doEval expr in
3 if sumMod9 res == convert expr
4 then ( Just res)
5 else Nothing
Listing 4.8: Evaluation function forArithconstructed using traditional techniques.
New Implementation
§4.6 introduced theWell-Typed Interpreterstyle of implementing the Arith language.
As types can be parameterised by more than one value, the data type used to represent Arith expressions can be further parameterised by their ‘cast nine’ abstraction. 58
4.8. Summary
1 data Arith : ArithTy -> Int -> Type where
2 Num : (v : Int) -> Arith TyNum ( castNine v)
3 Neg : Arith TyNum a -> Arith TyNum (-1 * a)
4 Add : Arith TyNum a
5 -> Arith TyNum b
6 -> Arith TyNum ( castNine (a + b))
7 Sub : Arith TyNum a
8 -> Arith TyNum b
9 -> Arith TyNum ( castNine (a - b))
10 Mul : Arith TyNum a
11 -> Arith TyNum b
12 -> Arith TyNum ( castNine (a * b))
13 Div : Arith TyNum a
14 -> Arith TyNum b
15 -> Arith TyNum ( castNine (a `div ` b))
Listing 4.9: Dependently Typed Implementation of the Language Grammar & Inter- pretation Semantics from Figure 4.4
With dependent types the interpretation of constructs fromArithto their ‘cast nine’ representation now occurs directly within the type of Arith. Previously, this was achieved using an external function—seeconvertfrom Listing 4.7. Using this representation, not only do Arith expressions have to be well-typed, but they must also be valid expressions in the ‘cast nine’ abstraction as well. This connection allows for correctness-by-construction guarantees to be made w.r.t. to a given abstraction. Listing 4.10 illustrates how the abstract representation is made accessible during runtime using Idris’ ability to access implicit type-level values and bring them down to the value level.
1 eval : Arith ty a -> Maybe Int
2 eval expr {a} = let res = doEval expr in
3 if sumMod9 res == a
4 then ( Just res)
5 else Nothing
Listing 4.10: Evaluation function forArithconstructed using new techniques.
4.8
Summary
Programming language theory provides a series of techniques useful for defining and working with languages. Formal grammars are used for defining abstract syntax; types
4. Well-Typed (Abstract) Interpreters
to describe expressions, typing rules to express correct composition of expressions; and semantics to describe a language’s interpretation. This section has only covered the basics of programming language theory, and more topics such as dealing with variables have not been covered. Regardless, the knowledge presented in this section is enough to understand how a declarative EDSL can be modelled and constructed within a dependently typed language such as Idris. This is the style of construction that the modelling languages in this thesis are presented.
Using dependently typed languages such as Idris provides programmers with an environment to support compact, efficient, and correct implementations of EDSLs. This was shown with theWell-Typed Interpreter for the Arith language. These
techniques are used in the implementation of Sif and NovoGRL, and the tooling to support pattern document interaction.
TheTypes as (Abstract) Interpreterapproach has illustrated how an abstract inter-
pretation for a language can be represented directly within the type of the expressions representing the language. Such modelling allows for compile time correctness-by- construction guarantees to be made, and also runtime checks to be made available. It is using this technique that the Sif language was implemented with NovoGRL being used as the abstract interpretation. Chapter 11 discusses this approach in more depth.
C
h
a
p
t
e
r
5
Sif: A Design Pattern Modelling
Language
This chapter introduces and details the Sif language and evaluator. Presenting its design (§5.1 to 5.3); important aspects of the evaluator implementation (§5.4); and evaluation of the language to model existing, and new design patterns—§5.5.
5.1
Overview
Sif is a requirements-based goal-oriented DSML for prototyping design patterns, that uses NovoGRL (Chapter 10) as a host language. The language has been de- signed as a declarative DSML for design patterns that respects the pattern troika of problem×solution×context. Problem specifications are modelled separately from their
potential solutions such that different problem solution pairs can be combined and evaluated to determine how well the given solution satisfies the presented problem. Problems and solutions are parameterised by a domain of operation (i.e. context) such that only problems and solutions indexed by the same domain can be paired.
Specifically, Sif problem specifications are modelled as requirements specifications based upon the Furps requirements model [Gra92] in which requirements are cat- egorised according to how they relate to the system being modelled. The categories supported by Furps are: Functional; Usability; Reliability; Performance; or Sup-
5. Sif
portability. Problems are presented using textual descriptions, with associated forces presented as a set of requirements that must be addressed by a solution. Use of Furps provides a more nuanced requirements model to be provided in comparison to that of the GRL. The GRL only provides goals and soft-goals, representing functional and non-functional requirements.
Solution specifications are not designed per the software artefacts of the presented solution. Not all software design patterns are software based. Rather solution specific- ations are presented as a set of abstract properties that represent the different aspects of the solution. A concrete link between a problem and solution is provided in the form of traits that describe the advantages, disadvantages, and general aspects of the prop- erty and the effect that these traits have on the requirements specified in the problem. Each ‘affect’ that a trait has on a problem is detailed using a qualitative contribution value originating from the GRL. Further, for each trait specified an evaluation value (also taken from the GRL) must be given that describes the level of satisfaction that a modeller has in the existence of said trait.
Alongside the language specification is the Sif evaluator, developed in the de- pendently typed programming language Idris. This evaluator provides a reference implementation of the Sif language as both a DSML andEmbedded Domain Specific Modelling Language(EDSML), and facilitates the creation of pattern document stubs
in various document formats, including that of Freyja—Chapter 6.