======================== REDUCERON MEMO 22 Compiling F-lite to C Matthew N, 30 April 2009 ======================== This memo defines a compiler from supercombinators to portable C. It is intended as a back-end to the F-lite implementation. The aim is to run F-lite programs on an FPGA soft-core, such as the Microblaze.
> module Flite.CompileBackend where
> import Data.List
Heap layout ----------- A node is a tagged pointer, storable in a single word of memory.
> nodeType = "typedef unsigned long Node;"
The least-significant bit of a node is a tag stating whether the node is an AP, containing a pointer to an application (a sequence of nodes) on the heap, or an OTHER, containing something else. typedef enum {AP = 0, OTHER = 1} Tag; The 2nd least-significant bit of a node is a flag stating whether or not the node is the final node of an application.
> macros = unlines
>   [ "#define isFinal(n) ((n) & 2)"
>   , "#define clearFinal(n) ((n) & (~2))"
>   , "#define setFinal(n) ((n) | 2)"
>   , "#define markFinal(n,final) ((final) ? setFinal(n) : clearFinal(n))"
If a node is an AP, its remaining 30 bits is a word-aligned heap address.
>   , "#define getAP(n) ((Node *) ((n) & (~3)))"
If the node is an OTHER, its 3rd least-significant bit contains a sub-tag stating whether the the node is an INT or a FUN. typedef enum {INT = 0, FUN = 1} Subtag; If a node is an INT, its remaining 29-bits is an unboxed integer.
>   , "#define getINT(n) (((signed long) n) >> 3)"
If a node is a FUN, its remaining 29-bits contains a 6-bit arity and a 23-bit function identifier.
>   , "#define getARITY(n) (((n) >> 3) & 63)"
>   , "#define getFUN(n) ((n) >> 9)"
More precisely:
>   , "#define isAP(n) (((n) & 1) == 0)"
>   , "#define isINT(n) (((n) & 5) == 1)"
>   , "#define isFUN(n) (((n) & 5) == 5)"
>   , "#define makeAP(a,final) ((unsigned long) (a) | ((final) << 1))"
>   , "#define makeINT(i,final) (((i) << 3) | ((final) << 1) | 1)"
>   , "#define makeFUN(arity,f,final) " ++
>              "(((f) << 9) | ((arity) << 3) | ((final) << 1) | 5)"
>   , "#define arity(n) (isFUN(n) ? getARITY(n) : 1)"
>   ]
Update records -------------- An update record is a pair containing a stack pointer (to detect when a head normal form has been reached) and a heap pointer (stating where to write the head normal form).
> updateType = "typedef struct { Node *s; Node *h; } Update;"
Registers ---------
> registers = unlines
>   [ "Node top;"           {- top of stack -}
>   , "Node *sp;"           {- stack pointer -}
>   , "Node *hp;"           {- heap pointer -}
>   , "Node *tsp;"          {- to-space pointer -}
>   , "Update *usp;"        {- update-stack pointer -}
>   , "unsigned int dest;"  {- destination address for computed jumps -}
>   ]
Swapping -------- The following code swaps the top two elements of the stack. It is used in the evaluation of strict primitive functions.
> swapCode = unlines
>   [ "{"
>   , "  Node tmp;"
>   , "  tmp = top;"
>   , "  top = sp[-1];"
>   , "  sp[-1] = tmp;"
>   , "}"
>   ]
Unwinding --------- Unwinding copies an application from the heap onto the stack, and pushes an update record onto the update stack.
> unwindCode = unlines
>   [ "{"
>   , "  Node *p;"
>   , "  p = getAP(top);"
>   , "  usp++; usp->s = sp; usp->h = p;"
>   , "  for (;;) {"
>   , "    top = *p++;"
>   , "    if (isFinal(top)) break;"
>   , "    *sp++ = top;"
>   , "  }"
>   , "}"
>   ]
Updating -------- The following code determines if a normal form has been reached, and if so, performs an update.
> updateCode = unlines
>   [ "{"
>   , "  unsigned int args, ari;"
>   , "  Node *base;"
>   , "  Node *p;"
>   , "  ari = arity(top);"
>   , "  if (sp - ari < stack) goto EXIT;"
>   , "  DO_UPDATE:"
>   , "  args = ((unsigned int) (sp - usp->s));"
>   , "  if (ari > args && usp > ustack) {"
>   , "    base = hp;"
>   , "    p = sp - args;"
>   , "    while (p < sp) *hp++ = clearFinal(*p++);"
>   , "    *hp++ = setFinal(top);"
>   , "    *(usp->h) = makeAP(base, 1);"
>   , "    usp--;"
>   , "    goto DO_UPDATE;"
>   , "  }"
>   , "}"
>   ]
Evaluation driver ----------------- Evalution proceeds depedning on the element on top of the stack.
> evalCode = unlines
>   [ "EVAL:"
>   , "if (isAP(top)) {"
>   ,    unwindCode
>   , "  goto EVAL;"
>   , "}"
>   , "else {"
>   , "  EVAL_OTHER:"
>   , "  if (hp > heapFull) collect();"
>   ,    updateCode
>   , "  if (isFUN(top)) {"
>   , "    dest = getFUN(top);"
>   , "    goto CALL;"
>   , "  }"
>   , "  else {"
>   ,      swapCode
>   , "    goto EVAL;"
>   , "  }"
>   , "}"
>   ]
Abstract syntax of source code ------------------------------ The body of a function is a list of identifier/application pairs. The first element in the list contains the spine application of the function.
> type Binding = (Id, App)
> type Body = [Binding]
An application is a list of nodes.
> type App = [Node]
> data Node
>   = VAR Id        {- variable reference -}
>   | ARG Int       {- argument reference -}
>   | FUN Arity Id  {- function identifier -}
>   | INT Int       {- integer -}
>   deriving Show
> type Id = String
A function definition consists of an identifier, an arity, and a body.
> type Defn = (Id, Arity, Body)
> type Arity = Int
For example, the F-lite function definition s f g x = f x (g x); is represented in abstract syntax as follows. ("s", 3, [ ("v0", [ARG 0, ARG 2, VAR "v1"]) , ("v1", [ARG 1, ARG 2]) ]) A data constructor consists of identifier, an arity, and an index.
> type Cons = (Id, Arity, Index)
> type Index = Int
A program consists of a list of constuctors and a list of function definitions.
> type Program = ([Cons], [Defn])
Function calling ---------------- Each function body is implemented as a case alternative in a large switch statement. To jump to the code for a function, place the function's identifier in the 'dest' register and then 'goto CALL'. This double jump is not very efficient, but its not obvious how to do any better in C.
> switchCode (cs, ds) = unlines
>   [ "CALL:"
>   , "switch (dest)"
>   , "{"
>   , prims          -- primitive definitions
>   , constrs cs     -- constructor definitions
>   , defns ds       -- function definitions
>   , "}"
>   ]
Constructor compilation ----------------------- Each constructor C used in the program is treated as a function with the following definition. Ci v1 ... vn f = (f+i) v1 ... vn f where i is the index of the constructor, n is the artiy of the constructor, and (f+i) represents the function occuring i definitions after the definition of f in the program code. It is assumed that case alternatives occur contiguously, ordered by index. For example, the F-lite program rev acc Nil = acc; rev acc (Cons x xs) = rev (Cons x acc) xs; is transformed down to rev acc xs = xs revCons acc; revCons x xs acc = rev (Cons x acc) xs; revNil acc = acc; if Cons has index 0 and Nil has index 1. (See Memo 13 for a more detailed explanation of how constructors and case expressions are treated.)
> cons :: Cons -> String
> cons (f, n, i) = unlines
>   [ "case " ++ fun f ++ ":"
>   , "{"
>   , "dest = getFUN(sp[-" ++ show (n+1) ++ "]) + " ++ show i ++ ";"
>   , "goto CALL;"
>   , "}"
>   , "break;"
>   ]
NB. No update is required because a case expression is not a normal form.
> constrs :: [Cons] -> String
> constrs = concatMap cons
Function compilation --------------------
> arg :: Int -> String
> arg i = "ARG_" ++ show i
> var :: Id -> String
> var v = "VAR_" ++ v
Map F-lite primitives to suitable C identifiers.
> fun :: Id -> String
> fun "(+)" = "PRIM_PLUS"
> fun "(-)" = "PRIM_MINUS"
> fun "(<=)" = "PRIM_LEQ"
> fun "(==)" = "PRIM_EQ"
> fun "(/=)" = "PRIM_NEQ"
> fun "emit" = "PRIM_EMIT"
> fun "emitInt" = "PRIM_EMITINT"
> fun "_|_" = "PRIM_UNDEFINED"
> fun f = "FUN_" ++ f
> declareArgs :: Int -> String
> declareArgs n = unlines $ map save [1..n]
>   where save i = "Node " ++ arg i ++ " = sp[-" ++ show i ++ "];"
> declareLocals :: String
> declareLocals = "Node *base = hp;"
> type Locs = [(Id, Int)]
> node :: String -> Locs -> String -> Node -> String
> node r vs final (INT i) =
>   r ++ " = makeINT(" ++ show i ++ "," ++ final ++ ");"
> node r vs final (ARG i) =
>   r ++ " = markFinal(" ++ arg (i+1) ++ "," ++ final ++ ");"
> node r vs final (VAR v) =
>   r ++ " = makeAP(base+" ++ offset ++ "," ++ final ++ ");"
>   where offset = show $ lookupVar v vs
> node r vs final (FUN n f) = 
>   r ++ " = makeFUN(" ++ show n ++ "," ++ fun f ++ "," ++ final ++ ");"
> lookupVar v vs = case lookup v vs of { Nothing -> error msg ; Just i -> i }
>   where msg = error ("Unknown identifier '" ++ v ++ "'")
> app :: Locs -> App -> String
> app vs app = unlines $ zipWith (node "*hp++" vs) finals app
>   where finals = map (const "0") (init app) ++ ["1"]
> spine :: Locs -> App -> String
> spine vs ns = unlines
>   [ unlines $ map (node "*sp++" vs "0") (init ns)
>   , node "top" vs "0" (last ns)
>   ]
> varLocs :: Body -> Locs
> varLocs body = zip vs (scanl (+) 0 (map length apps))
>   where (vs, apps) = unzip body
> body :: App -> Body -> String
> body s b = unlines
>   [ concatMap (app vs . snd) b
>   , spine vs s
>   , "goto EVAL;"
>   ] where vs = varLocs b
> defn :: Defn -> String
> defn (f, n, bs) = unlines
>   [ "case " ++ fun f ++ ":"
>   , "{"
>   , declareArgs n
>   , declareLocals
>   , "sp -= " ++ show n ++ ";"
>   , body (snd s) b
>   , "}"
>   , "break;"
>   ]
>   where s:b = [(v, reverse a) | (v, a) <- bs]
> defns :: [Defn] -> String
> defns = concatMap defn
Primitives ----------
> primIds :: [Id]
> primIds =
>   [ "(+)" , "(-)" , "(<=)" , "(==)", "(/=)", "emit", "emitInt", "_|_" ]
Apply primitive arithmetic operator to 2nd and 3rd stack elements; store result in top.
> arithPrim :: Id -> String -> String
> arithPrim p op = unlines
>   [ "case " ++ fun p ++ ":"
>   , "{"
>   , "top = makeINT(getINT(sp[-1]) " ++ op ++ " getINT(sp[-2]),0);"
>   , "sp -= 2;"
>   , "goto EVAL;"
>   , "}"
>   , "break;"
>   ]
Ditto for boolean operator.
> boolPrim :: Id -> String -> String
> boolPrim p op = unlines
>   [ "case " ++ fun p ++ ":"
>   , "{"
>   , "top = (getINT(sp[-1]) " ++ op ++ " getINT(sp[-2])) ? "
>       ++ "makeFUN(1," ++ fun "True"  ++ ",0) "
>       ++ ": makeFUN(1," ++ fun "False" ++ ",0);"
>   , "sp -= 2;"
>   , "goto EVAL;"
>   , "}"
>   , "break;"
>   ]
Print the second stack element.
> emitPrim :: Id -> String -> String
> emitPrim p format = unlines
>   [ "case " ++ fun p ++ ":"
>   , "{"
>   , "top = sp[-2];"
>   , "printf(\"" ++ format ++ "\", getINT(sp[-1]));"
>   , "sp -= 2;"
>   , "goto EVAL;"
>   , "}"
>   , "break;"
>   ]
> undefPrim :: String
> undefPrim = unlines
>   [ "case " ++ fun "_|_" ++ ":"
>   , "{"
>   , "printf(\"ERROR: bottom!\\n\");"
>   , "goto EXIT;"
>   , "}"
>   , "break;"
>   ]
> prims :: String
> prims = unlines
>   [ arithPrim "(+)" "+"
>   , arithPrim "(-)" "-"
>   , boolPrim "(<=)" "<="
>   , boolPrim "(==)" "=="
>   , boolPrim "(/=)" "!="
>   , emitPrim "emit" "%c"
>   , emitPrim "emitInt" "%i"
>   , undefPrim
>   ]
Garbage collection ------------------
> copyAPCode = unlines
>   [ "Node *copyAP(Node *src) {"
>   , "  Node n;"
>   , "  Node *from = src;"
>   , "  Node *dst = tsp;"
>   , "  n = *from;"
>   , "  if (isAP(n)) {"
>   , "    Node *loc = getAP(n);"
>   , "    if (loc >= toSpace && loc < toSpaceEnd) return loc;"
>   , "  }"
>   , "  do {"
>   , "    n = *from++; *tsp++ = n;"
>   , "  } while (! isFinal(n));"
>   , "  *src = (Node) dst;"
>   , "  return dst;"
>   , "}"
>   ]
> copyCode = unlines
>   [ "void copy() {"
>   , "  Node n;"
>   , "  Node *low = toSpace;"
>   , "  while (low < tsp) {"
>   , "    n = *low;"
>   , "    if (isAP(n)) {"
>   , "      Node *loc = copyAP(getAP(n));"
>   , "      *low = markFinal((Node) loc, isFinal(n));"
>   , "    }"
>   , "    low++;"
>   , "  }"
>   , "}"
>   ]
> collectCode = unlines
>   [ "void collect () {"
>   , "  Node n;"
>   , "  Node *p1;"
>   , "  Update *p2;"
>   , "  Update *p3;"
>   , "  tsp = toSpace;"
>   , "  p1 = stack;"
>   , "  while (p1 < sp) {"
>   , "    n = *p1;"
>   , "    if (isAP(n)) *p1 = (Node) copyAP(getAP(n));"
>   , "    p1++;"
>   , "  }"
>   , "  if (isAP(top)) top = (Node) copyAP(getAP(top));"
>   , "  copy();"
>   , "  p2 = ustack+1;"
>   , "  p3 = ustack;"
>   , "  while (p2 <= usp) {"
>   , "    n = *(p2->h);"
>   , "    if (isAP(n) && getAP(n) >= toSpace && getAP(n) <= toSpaceEnd) {"
>   , "      p3++;"
>   , "      p3->s = p2->s;"
>   , "      p3->h = getAP(n);"
>   , "    }"
>   , "    p2++;"
>   , "  }"
>   , "  usp = p3;"
>   , "  hp = tsp;"
>   , "  p1 = toSpace; toSpace = heap; heap = p1;"
>   , "  p1 = toSpaceEnd; toSpaceEnd = heapEnd; heapEnd = p1;"
>   , "  p1 = toSpaceFull; toSpaceFull = heapFull; heapFull = p1;"
>   , "}"
>   ]
Global variables ---------------- We need to store the beginning and end address of each memory partition, to detect termination and exhaustion.
> globals :: String
> globals = unlines
>   [ "Node *heap;"
>   , "Node *heapEnd;"
>   , "Node *heapFull;"
>   , "Node *toSpace;"
>   , "Node *toSpaceEnd;"
>   , "Node *toSpaceFull;"
>   , "Node *stack;"
>   , "Node *stackEnd;"
>   , "Update *ustack;"
>   , "Update *ustackEnd;"
>   ]
Memory allocation -----------------
> allocate :: Int -> Int -> String
> allocate heapSize stackSize = unlines
>   [ "heap = (Node *) malloc(sizeof(Node) * " ++ show heapSize ++ ");"
>   , "hp = heap;"
>   , "heapEnd = heap + " ++ show heapSize ++ ";"
>   , "heapFull = heapEnd - 1000;"
>   , "toSpace = (Node *) malloc(sizeof(Node) * " ++ show heapSize ++ ");"
>   , "tsp = toSpace;"
>   , "toSpaceEnd = toSpace + " ++ show heapSize ++ ";"
>   , "toSpaceFull = toSpaceEnd - 1000;"
>   , "stack = (Node *) malloc(sizeof(Node) * " ++ show stackSize ++ ");"
>   , "sp = stack;"
>   , "stackEnd = stack + " ++ show stackSize ++ ";"
>   , "ustack = (Update *) malloc(sizeof(Update) * " ++ show stackSize ++ ");"
>   , "usp = ustack;"
>   , "ustackEnd = ustack + " ++ show stackSize ++ ";"
>   ]
Program compilation -------------------
> funIds :: Program -> [String]
> funIds (cs, ds) = map first cs ++ map first ds
>   where first (x, y, z) = x
> declareFuns :: Program -> String
> declareFuns p =
>   unlines $ [def f i | (f, i) <- zip (primIds ++ funIds p) [0..]]
>   where def f i = "#define " ++ fun f ++ " " ++ show i
> program :: Program -> String
> program p = unlines
>   [ "#include <stdio.h>"
>   , "#include <stdlib.h>"
>   , nodeType
>   , updateType
>   , macros
>   , declareFuns p
>   , registers
>   , globals
>   , copyAPCode
>   , copyCode
>   , collectCode
>   , "int main(void) {"
>   , allocate 8000000 1000000
>   , "dest = " ++ fun "main" ++ ";"
>   , switchCode p
>   , evalCode
>   , "EXIT:"
>   , "return 0;"
>   , "}"
>   ]