======================== 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 " > , "#include " > , 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;" > , "}" > ]