aos/tools/fof/IL/FoF/Compile.lhs
Daniel Schwyn 6d444bf552 Main handout
Signed-off-by: Daniel Schwyn <daniel.schwyn@inf.ethz.ch>
2022-03-03 14:57:51 +01:00

242 lines
8.3 KiB
Plaintext

%if false
Copyright (c) 2009, ETH Zurich.
All rights reserved.
This file is distributed under the terms in the attached LICENSE file.
If you do not find this file, copies can be found by writing to:
ETH Zurich D-INFK, Universitaetstrasse 6, CH-8092 Zurich. Attn: Systems Group.
%endif
%include polycode.fmt
%if false
> module IL.FoF.Compile where
> import Semantics
> import PureExpressions
> import {-# SOURCE #-} Expressions
> import Constructs
> import IL.FoF.FoF
%endif
\section{Translating @FoFCode@ to @IL.FoF@}
\label{sec:il_fof_compile}
\subsection{The compiler}
We already know how to translate individual statements of the
|FoFCode| language, by using the one step compiler |compileAlgebra|
defined in @./Expressions.lhs@ and provided a |Binding| capturing the
state of the compiler. The game is then to chain up these compilation
steps into a single one. Here, |foldSemantics| nicely comes to the
rescue and automatically build this compiler.
> compileSemtoFoF' :: FoFCode PureExpr -> Binding -> (ILFoF, Binding)
> compileSemtoFoF' = foldSemantics compilePure compileAlgebra
Where |compilePure| is used to compile pure expressions. Pure
expressions are, by definition, constants and returned as such. This
is used when generating tests for conditional expressions: the
computational part is generated above the test handler and only the
(pure) result is tested.
> compilePure :: PureExpr -> Binding -> (ILFoF, Binding)
> compilePure x binding = (FConstant x, binding)
For our convenience, we can define the following |compileSemToFoF|
function that takes a closed |FoFCode| and compiles it in the empty
environment: that's our compiler for self-contained expressions.
> compileSemtoFoF :: FoFCode PureExpr -> ILFoF
> compileSemtoFoF term = fst $ compileSemtoFoF' term emptyBinding
> where emptyBinding = Binding { freshVar = 1,
> defStructs = [],
> defUnions = [],
> defEnums = [] }
\subsection{The machinery}
\subsubsection{Manipulating the compiler environment}
We very often need to generate fresh names, while keeping the
freshness invariant of the compiler environment. The following
function just does that:
> getFreshVar :: Binding -> (Int, Binding)
> getFreshVar binding = (loc, binding1)
> where loc = freshVar binding
> binding1 = binding { freshVar = loc + 1 }
Note that a clever implementation would be something of type:
> better_getFreshVar :: Binding -> (Int -> Binding -> t) -> t
> better_getFreshVar binding f = undefined
Which enforces the fact that the function |f| is provided a
synchronized compiler state. This ensures that people don't
inadvertently mess up the compiler state. This remark holds for too
many functions below, I'm a bit sad about that.
In order to ensure the freshness of names across bindings, we define
the following |passFreshVar| function that builds a |stableBinding|
whose fresh variables are ensured not to clash with the one generated
using |upBinding|. Similarly, it carries the structures defined in
|upBinding|.
> passFreshVar :: Binding -> Binding -> Binding
> passFreshVar upBinding stableBinding =
> stableBinding { freshVar = freshVar upBinding,
> defStructs = defStructs upBinding,
> defUnions = defUnions upBinding,
> defEnums = defEnums upBinding }
> (|->) = passFreshVar
From variable identifier and an origin, we can later make
|VarName|. In a craze of Hungarian naming, the origin dictactes the
name of variables.
> makeVarName :: Origin -> Int -> VarName
> makeVarName orig loc = Generated $ makeVarName' orig loc
> where makeVarName' Local x = "fof_x" ++ show x
> makeVarName' Param x = "fof_y" ++ show x
> makeVarName' Dynamic x = "fof_d" ++ show x
> makeVarName' Global x = "fof_g" ++ show x
The Hungarian fever can go further: when a variable is somehow related
to another |VarName|, the |heritVarName| makes it explicit at the name
level by deriving a fresh name from the previous one.
> heritVarName :: Binding -> VarName -> (Int, VarName, Binding)
> heritVarName binding name = (loc, Inherited loc name, binding1)
> where (loc, binding1) = getFreshVar binding
>
\subsubsection{From Expressions to Types}
Let us be honest: the code which follows is tricky. Change something
there and the generated code will be wrong, if it is not already. I'm
looking at you |readOf| and |liftType|. They came to life during the
implementation of References and its painful compiler. After a
lot of work, I came to the conclusion (and proof) that they are
correct. The question is now: are they correct when mixed with complex
data-types, such as structs and arrays. The practician seems to say
``yes'', the theoretician remains proofless.
The intrinsic difficulty is that a Reference abstracts both a C
variable and a C pointer. However, in C, both concepts are quite
distinct. Hence, the compiler needs to be clever to translate the
unified notion of Reference in two semantically different
objects. Hence that horrible machinery.
\paragraph{|typeOf|:}
Obviously, there exists a map going from each well-typed element of
|PureExpr| to an element of |TypeExpr|. Hence, this map assigns a
\emph{type} to a given, well-typed expression. As for ill-typed
expressions, we simply return an error message.
Computing the type of base values as well as of unary operations is
straightforward:
> typeOf :: PureExpr -> TypeExpr
> typeOf (Void) = TVoid
> typeOf (CLInteger sign size _) = TInt sign size
> typeOf (CLFloat _) = TFloat
> typeOf (CLRef _ typ _) = typ
> typeOf (Unary _ x) = typeOf x
A binary operation is well-typed if and only if both sub-terms are
well-typed and of same type. The same goes for the branches of a
conditional expression:
> typeOf (Binary _ x y) =
> if (typeOfx == typeOfy) then
> typeOfx
> else error "typeOf: Binop on distinct types."
>
> where typeOfx = typeOf x
> typeOfy = typeOf y
>
> typeOf (Test _ t1 t2) =
> if (typeOft1 == typeOft2) then
> typeOft1
> else error "typeOf: Test exits on distinct types"
>
> where typeOft1 = typeOf t1
> typeOft2 = typeOf t2
By convention, the value returned by |sizeof| is an unsigned 64 bits
integer:
> typeOf (Sizeof _) = TInt Unsigned TInt64
Finally, the type of a casted expression is the assigned type. Note
that we do not judge of the legality of this cast here. This aspect is
handled by the dynamic semantics of FoF's meta-language.
> typeOf (Cast t _) = t
\paragraph{|readOf| and |unfoldPtrType|:}
When we \emph{read} the content of the reference cell, of type
|TPointer typeCell modeCell|, the type of the object read is either:
\begin{itemize}
\item A constant of type |typeCell|, or
\item A reference cell of type |typeCell|, in a |Read| mode
\end{itemize}
We can distinguish both cases thanks to |typeCell|. If |typeCell| is a
|TPointer| itself (first case, below), this means that we are dealing
with a reference cell. If |typeCell| is a base type (second case), this means that
this is a constant.
> readOf :: TypeExpr -> TypeExpr
> readOf (TPointer typ _) = TPointer typ Read
> readOf x = x
>
> unfoldPtrType :: PureExpr -> TypeExpr
> unfoldPtrType (CLRef _ (TPointer typ _) _) = readOf typ
\paragraph{|liftType|:}
Although our Reference Cell representation abstracts away the
distinction between variables and pointers, it has one drawback. A
variable is assigned a |TPointer| type, whereas, in C, we will be
working one |TPointer|-level below: our reference cell types
corresponds to the same C type but one pointer dereference. Hence, we
introduce the following lifting function:
> liftType :: TypeExpr -> TypeExpr
> liftType (TPointer x _) = x
> liftType x = x
\paragraph{|deref|:}
The |deref| is another operator dealing with the specify of reference
cells. In the compiler, we translate the high-level reference cell
operators by pointer manipulations and assignment. Therefore, when
manipulating a reference cell, we will not interested in its actual
content but its address. Hence the following function. Values will
manipulated just as usual, by value.
%if false
What about ... ? CLRef _ (TPointer _ _) _ : CLRef _ (TPointer _ _) _ ??
%endif
> deref :: PureExpr -> String
> deref (CLRef _ (TPointer _ _) _) = "&"
> deref _ = ""